diff --git a/frontend/apps/artcraft-webapp/tsconfig.app.json b/frontend/apps/artcraft-webapp/tsconfig.app.json index 15daebf644..b0ce196f48 100644 --- a/frontend/apps/artcraft-webapp/tsconfig.app.json +++ b/frontend/apps/artcraft-webapp/tsconfig.app.json @@ -46,6 +46,9 @@ { "path": "../../libs/components/upload-modal/tsconfig.lib.json" }, + { + "path": "../../libs/components/pagedraw/tsconfig.lib.json" + }, { "path": "../../libs/components/slider-v2/tsconfig.lib.json" }, diff --git a/frontend/apps/artcraft-webapp/tsconfig.json b/frontend/apps/artcraft-webapp/tsconfig.json index 23a7c50fd2..e53f73d97d 100644 --- a/frontend/apps/artcraft-webapp/tsconfig.json +++ b/frontend/apps/artcraft-webapp/tsconfig.json @@ -11,6 +11,9 @@ { "path": "../../libs/components/upload-modal" }, + { + "path": "../../libs/components/pagedraw" + }, { "path": "../../libs/components/slider-v2" }, diff --git a/frontend/apps/artcraft-website/src/components/model-badge-grid.tsx b/frontend/apps/artcraft-website/src/components/model-badge-grid.tsx index 9b1ea132f0..b6598aadef 100644 --- a/frontend/apps/artcraft-website/src/components/model-badge-grid.tsx +++ b/frontend/apps/artcraft-website/src/components/model-badge-grid.tsx @@ -1,141 +1,105 @@ -import { ReactNode } from "react"; -import { twMerge } from "tailwind-merge"; +import { + getCreatorIconPath, + IMAGE_MODELS, + VIDEO_MODELS, +} from "@storyteller/model-list"; interface BadgeInfo { id: string; - label?: string | ReactNode; - svg?: ReactNode | null; + label: string; + iconSrc: string; } interface ModelBadgeGridProps { className?: string; - highlight?: string; - rowOffsets?: number[]; } -const baseBadgeClasses = - "rounded-2xl px-6 py-3 text-2xl font-normal flex-shrink-0 bg-[#121212] text-white/90 text-center h-[58px] flex items-center justify-center gap-1.5"; -const highlightClasses = - "bg-primary/30 text-white font-semibold shadow-lg border-2 border-primary/60"; +const ROW_COUNT = 3; +// Per-row marquee durations (seconds). Deliberately mismatched so the three +// rows never move in lockstep. +const ROW_DURATIONS = [56, 68, 62]; -const rows: BadgeInfo[][] = [ - [ - { id: "empty1", svg: null }, - { id: "empty2", svg: null }, - { id: "empty3", svg: null }, - { - id: "seedance-2", - label: "Seedance 2.0", - svg: ( - Seedance logo - ), - }, - { id: "empty4", svg: null }, +// Transparent border in the base keeps the box size constant when the hover +// highlight border appears, so the marquee doesn't jitter. The chip lights up +// only while the pointer is over it. +const badgeClasses = + "rounded-2xl px-6 py-3 text-2xl font-normal flex-shrink-0 bg-[#121212] text-white/90 text-center h-[58px] flex items-center justify-center gap-1.5 border-2 border-transparent transition-all duration-300 hover:bg-primary/30 hover:text-white hover:font-semibold hover:shadow-lg hover:border-primary/60"; - { id: "empty6", svg: null }, - { id: "empty7", svg: null }, - { id: "empty8", svg: null }, - ], - [ - { id: "empty9", svg: null }, - { - id: "midjourney", - label: "Midjourney", - svg: ( - Midjourney logo - ), - }, - { - id: "nano-banana-2", - label: "Nano Banana 2", - svg: ( - Google gemini icon - ), - }, - { id: "empty10", svg: null }, +// Build one badge per unique model (deduped by display name so e.g. an +// image+video pair like "Grok" only shows once), pulling the correct brand +// icon from the canonical model mapping. +const BADGES: BadgeInfo[] = (() => { + const seen = new Set(); + const badges: BadgeInfo[] = []; + for (const model of [...IMAGE_MODELS, ...VIDEO_MODELS]) { + if (seen.has(model.fullName)) continue; + seen.add(model.fullName); + badges.push({ + id: model.id, + label: model.fullName, + iconSrc: getCreatorIconPath(model.creator), + }); + } + return badges; +})(); - { id: "empty11", svg: null }, - { id: "empty12", svg: null }, - { id: "empty13", svg: null }, - { id: "empty14", svg: null }, - ], - [ - { id: "empty15", svg: null }, - { id: "empty16", svg: null }, - { id: "empty17", svg: null }, - { - id: "kling", - label: "Kling 3.0 Pro", - svg: ( - Kling AI logo - ), - }, - { id: "empty18", svg: null }, - { id: "empty19", svg: null }, - { id: "empty20", svg: null }, - { id: "empty21", svg: null }, - ], -]; +// Distribute badges round-robin across the rows so each row carries a mix of +// brands rather than all of one creator clumping together. +const ROWS: BadgeInfo[][] = Array.from({ length: ROW_COUNT }, () => []); +BADGES.forEach((badge, i) => { + ROWS[i % ROW_COUNT].push(badge); +}); export default function ModelBadgeGrid({ className = "", - highlight, - rowOffsets = [], }: ModelBadgeGridProps) { return (
- {/* Gradient fade overlays */} + {/* Gradient fade overlays mask the marquee edges */}
-
- {rows.map((row, rowIdx) => { - const offset = rowOffsets[rowIdx] ?? 0; - return ( +
+ {ROWS.map((row, rowIdx) => ( +
- {row.map(({ id, label, svg }, idx) => { - const isHighlight = highlight && highlight.toLowerCase() === id; - return ( -
- {svg ? ( - {svg} - ) : null} - {label} -
- ); - })} + {/* Two identical copies make the loop seamless: translating the + track by -50% lands exactly on the start of the second copy. */} + {[0, 1].map((copy) => ( +
+ {row.map((badge) => ( +
+ + + + {badge.label} +
+ ))} +
+ ))}
- ); - })} +
+ ))}
); diff --git a/frontend/apps/artcraft-website/src/pages/landing-sd2/landing-sd2.tsx b/frontend/apps/artcraft-website/src/pages/landing-sd2/landing-sd2.tsx index 7c89afd486..da75a0640a 100644 --- a/frontend/apps/artcraft-website/src/pages/landing-sd2/landing-sd2.tsx +++ b/frontend/apps/artcraft-website/src/pages/landing-sd2/landing-sd2.tsx @@ -643,11 +643,7 @@ const LandingSD2 = () => { all in one place. Log in with your existing subscriptions.

- +
diff --git a/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx b/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx index e639c51ecc..e5ab8b8ebf 100644 --- a/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx +++ b/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx @@ -640,11 +640,7 @@ const Landing = () => { all in one place. Log in with your existing subscriptions.

- + diff --git a/frontend/apps/artcraft-website/src/pages/landing3/landing3.tsx b/frontend/apps/artcraft-website/src/pages/landing3/landing3.tsx index c72d88f114..6b61ac424b 100644 --- a/frontend/apps/artcraft-website/src/pages/landing3/landing3.tsx +++ b/frontend/apps/artcraft-website/src/pages/landing3/landing3.tsx @@ -115,7 +115,6 @@ const MADE_WITH_VIDEOS = [ const MANIFESTO_WORDS: ReadonlyArray = [ "ArtCraft", "brings", - "3D", "control", "to", "AI", @@ -353,6 +352,50 @@ const Landing3 = () => { }), }); + // Feature deck: all cards pin flush at the same top. As the NEXT card + // rises past roughly halfway over this one, scale this card down (top + // edge stays put) so it reads as receding behind the incoming card. + const stackCards = gsap.utils.toArray("[data-stack-card]"); + stackCards.forEach((card, i) => { + const next = stackCards[i + 1]; + if (!next) return; // top card is never covered + gsap.fromTo( + card, + { scale: 1 }, + { + scale: 0.9, + ease: "none", + transformOrigin: "center top", + scrollTrigger: { + // Drive off the incoming card: start once it's about halfway up + // the viewport (~half over this card), finish when it pins flush. + trigger: next, + start: "top center", + end: "top top+=96", + scrub: true, + }, + }, + ); + // Only AFTER the next card has fully overlapped this one (it pins flush + // at the same top) do we fade this card out. It's hidden behind the + // next card by then, so the fade is invisible — it just drops this + // card's shadow so the edge shadows don't stack across the deck. + gsap.fromTo( + card, + { opacity: 1 }, + { + opacity: 0, + ease: "none", + scrollTrigger: { + trigger: next, + start: "top top+=96", + end: "top top-=40", + scrub: true, + }, + }, + ); + }); + // Hero pattern parallax (slower than scroll, drifts upward) const heroPattern = document.querySelector( "[data-hero-pattern]", @@ -428,6 +471,23 @@ const Landing3 = () => { ); }); + // Phase 1b — once the sentence is fully revealed, draw a blue + // underline in under "control" to land the emphasis. + const manifestoUnderline = manifestoSection.querySelector( + "[data-manifesto-underline]", + ); + if (manifestoUnderline) { + gsap.set(manifestoUnderline, { + scaleX: 0, + transformOrigin: "left center", + }); + tl.to( + manifestoUnderline, + { scaleX: 1, duration: 4, ease: "power2.out" }, + ">", + ); + } + // Phase 2 — hold the fully-revealed manifesto frozen so the user gets // a beat to read it before the transition triggers. tl.to({}, { duration: 4 }); @@ -678,7 +738,18 @@ const Landing3 = () => { className="text-2xl sm:text-3xl tracking-[-0.035em] font-medium text-white px-4 sm:px-0" style={{ lineHeight: 1.4 }} > - {MANIFESTO_WORDS.join(" ")} + {MANIFESTO_WORDS.map((w, i) => ( + + {w === "control" ? ( + + {w} + + ) : ( + w + )} + {i < MANIFESTO_WORDS.length - 1 ? " " : ""} + + ))} @@ -729,7 +800,18 @@ const Landing3 = () => { data-manifesto-word className="inline-block opacity-15 will-change-[opacity,transform]" > - {w} + {w === "control" ? ( + + {w} + + + ) : ( + w + )} {i < MANIFESTO_WORDS.length - 1 ? " " : ""} ))} @@ -758,7 +840,7 @@ const Landing3 = () => { {/* FEATURES: alternating cards */} -
+
{ speed={-22} rotate={8} /> -
+
{FEATURES.map((feature, i) => ( -
-
-
- - - {feature.label} - +
+
+
+ + + {feature.label} + +
+

+ {feature.title} +

+

+ {feature.description} +

-

- {feature.title} -

-

- {feature.description} -

-
-
- -
-
+
+ +
+ +
))}
@@ -837,45 +922,57 @@ const Landing3 = () => { Ownership

- Stop renting from{" "} - websites. + Stop renting from + websites.

- ArtCraft is yours to own and keep,{" "} - forever. - No subscriptions, no aggregator middleman, no rent payments. + ArtCraft is yours to own and keep, forever. No subscriptions + needed, no aggregator middleman, no rent payments.

- {/* Websites column */} -
-
- - Other tools - -
-

- The Rental Trap -

-

- With browser-based tools, you're paying for access, not a - product. Your work, models, and history live on someone else's - servers, and disappear with them. -

-
- {["No ownership", "Monthly fees"].map((tag) => ( - + {/* Websites column — deliberately styled to feel like a downgrade */} +
+
+
+
+ - {tag} + Other tools - ))} +
+

+ The Rental Trap +

+

+ With browser-based tools, you're paying for access, not a + product. Your work, models, and history live on someone else's + servers, and disappear with them. +

+
+ {["No ownership", "Monthly fees", "Locked in"].map((tag) => ( + + + {tag} + + ))} +
@@ -906,18 +1003,20 @@ const Landing3 = () => { keys, or use ours.

- {["Yours forever", "BYO keys"].map((tag) => ( - - - {tag} - - ))} + {["Yours forever", "BYO keys", "No subscriptions needed"].map( + (tag) => ( + + + {tag} + + ), + )}
@@ -1073,11 +1172,7 @@ const Landing3 = () => { all in one place. Log in with your existing subscriptions.

- +
@@ -1191,25 +1286,30 @@ const Landing3 = () => {
{/* COMMUNITY CTA */}
-
-
-

- Open source & community-driven -

-

- Join the conversation on Discord and star us on GitHub. -

-
+
+
+
+ {/* + + */} +
+

+ Join our community +

+

+ ArtCraft is open source and community-driven. Come build with + us. +

+
+
+