diff --git a/apps/web/components/pages/HomeTwo/studio/StudioFeatures.tsx b/apps/web/components/pages/HomeTwo/studio/StudioFeatures.tsx index 91b48d8a687..4081c94a4d4 100644 --- a/apps/web/components/pages/HomeTwo/studio/StudioFeatures.tsx +++ b/apps/web/components/pages/HomeTwo/studio/StudioFeatures.tsx @@ -1,37 +1,28 @@ "use client"; -import { classNames } from "@cap/utils/helpers"; import { useRef } from "react"; import { Eyebrow } from "../Eyebrow"; import { Fit, LazyMount } from "../scenes/engine"; -import { - BAND, - BODY_TEXT, - grainBg, - H_SECTION, - MODE_THEME, - meshStyle, -} from "../theme"; +import { BAND, BODY_TEXT, grainBg, H_SECTION, MODE_THEME } from "../theme"; import { useInView, useReducedMotion } from "../visibility"; import { CARDS_A, CARDS_B } from "./catalog"; import { CANVAS, type StudioCard } from "./shared"; const ORDER = [ "mask", - "text", "scenes", "zoom", "captions", + "text", "three-d", "canvas", "grades", "clips", ]; -const SPAN: Record = { mask: 2, scenes: 2, clips: 2 }; const ALL = [...CARDS_A, ...CARDS_B]; const CARDS = ORDER.flatMap((key) => { const card = ALL.find((item) => item.key === key); - return card ? [{ ...card, span: SPAN[key] ?? 1 }] : []; + return card ? [card] : []; }); const Card = ({ card }: { card: StudioCard }) => { @@ -41,35 +32,22 @@ const Card = ({ card }: { card: StudioCard }) => { return (
  • -
    - - +
    + +
    -
    -

    +
    +

    {card.title}

    {card.body}

    @@ -81,17 +59,17 @@ const Card = ({ card }: { card: StudioCard }) => { export const StudioFeatures = () => (
    -
    +
    Studio Mode · The editor

    Polish it before anyone sees it

    Studio Mode opens straight into an editor made for screen recordings. Blur what is private, switch scenes between screen and camera, add @@ -99,7 +77,7 @@ export const StudioFeatures = () => (

    -
      +
        {CARDS.map((card) => ( ))} diff --git a/apps/web/components/pages/HomeTwo/studio/cards/canvas.tsx b/apps/web/components/pages/HomeTwo/studio/cards/canvas.tsx new file mode 100644 index 00000000000..5c3971fc567 --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/canvas.tsx @@ -0,0 +1,428 @@ +"use client"; + +import { classNames } from "@cap/utils/helpers"; +import Image from "next/image"; +import { useRef } from "react"; +import { useCursor, useSceneState, type Way } from "../../scenes/engine"; +import { + Chip, + ED, + EditorButton, + Ground, + Panel, + RecordedWindow, + Segmented, + Slider, + useLoop, +} from "../shared"; + +const SOURCES = [ + "Desktop", + "Wallpaper", + "Image", + "Color", + "Gradient", + "Animated", +] as const; +const CATEGORIES = ["macOS", "Dark", "Blue", "Cities", "Purple", "Orange"]; +const CITIES = ["liverpool", "santorini", "miami", "monaco", "london", "rome"]; +const GRADIENTS = [ + "linear-gradient(135deg, #FF7E5F 0%, #FEB47B 100%)", + "linear-gradient(135deg, #667EEA 0%, #764BA2 100%)", + "linear-gradient(135deg, #43CEA2 0%, #185A9D 100%)", + "linear-gradient(135deg, #F857A6 0%, #FF5858 100%)", + "linear-gradient(135deg, #1FA2FF 0%, #12D8FA 55%, #A6FFCB 100%)", + "linear-gradient(135deg, #0F2027 0%, #203A43 50%, #2C5364 100%)", +]; + +const WALL_FROM = 3; +const WALL_TO = 1; +const GRADIENT_TO = 1; + +const DURATION = 10000; +const WALL_AT = 1400; +const GRADIENT_AT = 3600; +const SWATCH_AT = 4600; +const BROWSER_AT = 5800; +const MACBOOK_AT = 7600; +const UNFRAME_AT = 9400; + +const S = 1.15; +const PANEL = { left: 12, top: 211, w: 500, pad: 12 } as const; +const ROW = { pill: 12, chips: 52, grid: 84 } as const; +const THUMB = { w: 34, h: 34, gap: 6 } as const; +const RESULT = { w: 336, h: 189, left: 114, top: 12 } as const; +const WIN_SCALE = 0.46; +const WIN = { + left: Math.round((RESULT.w - 360 * WIN_SCALE) / 2), + top: 46, +} as const; + +const toCanvas = (x: number, y: number) => ({ + x: PANEL.left + x * S, + y: PANEL.top + y * S, +}); +const thumbCenter = (i: number) => + toCanvas( + PANEL.pad + i * (THUMB.w + THUMB.gap) + THUMB.w / 2, + ROW.grid + THUMB.h / 2, + ); +const pillCenter = (i: number) => { + const inner = PANEL.w - PANEL.pad * 2 - 4; + const item = (inner - 10) / 6; + return toCanvas(PANEL.pad + 2 + item * (i + 0.5) + 2 * i, ROW.pill + 15); +}; + +const PATH: Way[] = [ + { t: 0, x: 380, y: 130 }, + { t: WALL_AT - 500, ...thumbCenter(WALL_TO) }, + { t: WALL_AT, ...thumbCenter(WALL_TO), click: true }, + { t: GRADIENT_AT - 600, ...pillCenter(4) }, + { t: GRADIENT_AT, ...pillCenter(4), click: true }, + { t: SWATCH_AT - 400, ...thumbCenter(GRADIENT_TO) }, + { t: SWATCH_AT, ...thumbCenter(GRADIENT_TO), click: true }, + { t: SWATCH_AT + 900, x: 470, y: 150 }, + { t: DURATION, x: 470, y: 150 }, +]; + +type Frame = "none" | "browser" | "macbook"; + +const uiAt = (t: number) => { + const source = t >= GRADIENT_AT ? "Gradient" : "Wallpaper"; + const wall = t >= WALL_AT ? WALL_TO : WALL_FROM; + const gradient = t >= SWATCH_AT ? GRADIENT_TO : 0; + const frame: Frame = + t >= UNFRAME_AT + ? "none" + : t >= MACBOOK_AT + ? "macbook" + : t >= BROWSER_AT + ? "browser" + : "none"; + return { source, wall, gradient, frame }; +}; + +const title = (city: string) => city.charAt(0).toUpperCase() + city.slice(1); + +const MiniField = ({ + label, + value, + fill, +}: { + label: string; + value: string; + fill: number; +}) => ( +
        + + {label} + +
        + +
        + + {value} + +
        +); + +const Swatch = ({ + selected, + children, +}: { + selected: boolean; + children: React.ReactNode; +}) => ( + + {children} + +); + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const [ui, setUi] = useSceneState(uiAt(0)); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 2400, + tick: (t, seek) => { + setUi(uiAt(t)); + cursor.tick(PATH, t, seek); + }, + }); + + const wallpaper = ui.source === "Wallpaper"; + const browser = ui.frame === "browser"; + const macbook = ui.frame === "macbook"; + const label = + ui.frame === "browser" + ? "Frame · Browser" + : ui.frame === "macbook" + ? "Frame · MacBook" + : wallpaper + ? `Wallpaper · ${title(CITIES[ui.wall] ?? "")}` + : "Gradient"; + + return ( +
        + +
        + {[WALL_FROM, WALL_TO].map((index) => ( + + ))} +
        + +
        +
        +
        +
        +
        + + {["#FF5F57", "#FEBC2E", "#28C840"].map((color) => ( + + ))} + + + Dashboard + + + acme.com/dashboard + +
        +
        +
        + +
        +
        +
        +
        +
        + +
        + + + {label} + +
        + + + + +
        + {wallpaper ? ( +
        + {CATEGORIES.map((category) => { + const on = category === "Cities"; + return ( + + {category} + + ); + })} +
        + ) : ( + <> + + Presets + + + Randomize + + + )} +
        + +
        +
        + {wallpaper + ? CITIES.map((city, i) => ( + + + + )) + : GRADIENTS.map((gradient, i) => ( + + + + ))} +
        +
        + {wallpaper ? ( + <> + + + + ) : ( + <> + + + + )} +
        +
        +
        + {cursor.Cursor} + +
        + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/captions.tsx b/apps/web/components/pages/HomeTwo/studio/cards/captions.tsx new file mode 100644 index 00000000000..3c65ac356ad --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/captions.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { classNames } from "@cap/utils/helpers"; +import { useRef } from "react"; +import { LucideChevronDown } from "../../demo/capIcons"; +import { useCursor, useSceneState, type Way } from "../../scenes/engine"; +import { + ED, + EditorButton, + Field, + Ground, + Panel, + RecordedWindow, + Toggle, + useLoop, +} from "../shared"; + +const WIN = { left: 10, top: 38, scale: 1.12 }; +const PANEL = { left: 368, top: 12, w: 196, scale: 1.1 }; +const LINES = [ + { start: 500, words: "Here is the new dashboard".split(" ") }, + { start: 4400, words: "Every card pulls live data".split(" ") }, +]; +const WORD_MS = 380; +const TOGGLE_AT = 7600; +const DURATION = 9000; +const TOGGLE = { + x: PANEL.left + 151 * PANEL.scale, + y: PANEL.top + 283 * PANEL.scale, +}; + +const PATH: Way[] = [ + { t: 0, x: 300, y: 190 }, + { t: 6400, x: 300, y: 190 }, + { t: 7300, ...TOGGLE }, + { t: TOGGLE_AT, ...TOGGLE, click: true }, + { t: 8400, x: TOGGLE.x - 40, y: TOGGLE.y + 60 }, + { t: DURATION, x: TOGGLE.x - 40, y: TOGGLE.y + 60 }, +]; + +const uiAt = (t: number) => { + let line = 0; + for (let i = 0; i < LINES.length; i++) { + if (t >= (LINES[i]?.start ?? 0)) line = i; + } + const current = LINES[line]; + const elapsed = t - (current?.start ?? 0); + const active = Math.min( + (current?.words.length ?? 1) - 1, + Math.floor(Math.max(0, elapsed) / WORD_MS), + ); + return { + line, + active, + shown: t >= (LINES[0]?.start ?? 0), + exportOn: t >= TOGGLE_AT, + }; +}; + +const SelectBox = ({ + children, + sub, + right, +}: { + children: React.ReactNode; + sub?: string; + right?: string; +}) => ( +
        + + + {children} + + {sub ? ( + + {sub} + + ) : null} + + + {right ? ( + + {right} + + ) : null} + + +
        +); + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const [ui, setUi] = useSceneState(uiAt(0)); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 2200, + tick: (t, seek) => { + setUi(uiAt(t)); + cursor.tick(PATH, t, seek); + }, + }); + + const line = LINES[ui.line] ?? LINES[0]; + + return ( +
        + + +
        + +
        + {line?.words.map((word, i) => ( + + {word} + + ))} +
        + +
        + +
        + + Captions + + + Beta + +
        + + + Recommended + + + + English + + + Generate Captions + +
        + + Export with Subtitles + + +
        +
        +
        + {cursor.Cursor} + +
        + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/clips.tsx b/apps/web/components/pages/HomeTwo/studio/cards/clips.tsx new file mode 100644 index 00000000000..4953194520d --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/clips.tsx @@ -0,0 +1,322 @@ +"use client"; + +import { classNames } from "@cap/utils/helpers"; +import { useRef } from "react"; +import { CapAudioOn, CapClapperboard, LucidePlus } from "../../demo/capIcons"; +import { + easeInOut, + lerp, + span, + useCursor, + useSceneState, + type Way, +} from "../../scenes/engine"; +import { + CANVAS, + ED, + Ground, + HUE, + Lane, + mix, + Playhead, + RULER_H, + Ruler, + Segment, + SpeedChip, + TRACK_GAP, + TRACK_GUTTER, + TRACK_H, + TrackLabel, + useLoop, + Waveform, +} from "../shared"; + +const S = 1.5; +const LOGICAL = { w: CANVAS.w / S, h: CANVAS.h / S }; +const INSET = 14; +const PAD = 12; +const CARD = { + x: INSET, + w: LOGICAL.w - INSET * 2, + h: 10 + RULER_H + TRACK_GAP + TRACK_H + TRACK_GAP + TRACK_H + PAD, +}; +const CARD_Y = Math.round((LOGICAL.h - CARD.h) / 2) + 4; +const LANE_X = CARD.x + PAD + TRACK_GUTTER; +const LANE_W = CARD.w - PAD * 2 - TRACK_GUTTER; +const VIDEO_Y = CARD_Y + 10 + RULER_H + TRACK_GAP; +const AUDIO_Y = VIDEO_Y + TRACK_H + TRACK_GAP; + +const DURATION = 10000; +const CLIP_A = { + start: Math.round(LANE_W * 0.48), + trimmed: Math.round(LANE_W * 0.38), +}; +const CLIP_B = { + start: Math.round(LANE_W * 0.44), + fast: Math.round(LANE_W * 0.32), +}; +const JOIN = 4; +const TRIM = { start: 2200, end: 3400 }; +const SPEED_AT = 3900; +const FADE_AT = 5200; +const AUDIO_AT = 6400; + +const g = (x: number, y: number) => ({ x: x * S, y: y * S }); +const mid = VIDEO_Y + TRACK_H / 2; + +const PATH: Way[] = [ + { t: 0, ...g(LANE_X + 40, CARD_Y - 6) }, + { t: 1900, ...g(LANE_X + CLIP_A.start + 1, mid) }, + { t: TRIM.start, ...g(LANE_X + CLIP_A.start + 1, mid) }, + { t: TRIM.end, ...g(LANE_X + CLIP_A.trimmed + 1, mid) }, + { + t: SPEED_AT - 150, + ...g(LANE_X + CLIP_A.trimmed + JOIN + CLIP_B.start - 22, mid), + }, + { + t: SPEED_AT, + ...g(LANE_X + CLIP_A.trimmed + JOIN + CLIP_B.start - 22, mid), + click: true, + }, + { t: FADE_AT - 150, ...g(LANE_X + CLIP_A.trimmed + JOIN / 2, mid) }, + { t: FADE_AT, ...g(LANE_X + CLIP_A.trimmed + JOIN / 2, mid), click: true }, + { t: AUDIO_AT + 400, ...g(LANE_X + LANE_W - 24, AUDIO_Y + TRACK_H + 14) }, + { t: DURATION, ...g(LANE_X + LANE_W - 24, AUDIO_Y + TRACK_H + 14) }, +]; + +const uiAt = (t: number) => ({ + trimmed: t >= TRIM.start + 400, + speed: t >= SPEED_AT, + fade: t >= FADE_AT, + audio: t >= AUDIO_AT, +}); + +const widthsAt = (t: number) => { + const trim = easeInOut(span(t, TRIM.start, TRIM.end)); + const a = lerp(CLIP_A.start, CLIP_A.trimmed, trim); + const b = t >= SPEED_AT ? CLIP_B.fast : CLIP_B.start; + return { a, b }; +}; + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const clipARef = useRef(null); + const clipBRef = useRef(null); + const seamRef = useRef(null); + const playheadRef = useRef(null); + const [ui, setUi] = useSceneState(uiAt(0)); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 7200, + tick: (t, seek) => { + setUi(uiAt(t)); + const { a, b } = widthsAt(t); + if (clipARef.current) clipARef.current.style.width = `${a}px`; + if (clipBRef.current) { + clipBRef.current.style.left = `${a + JOIN}px`; + clipBRef.current.style.width = `${b}px`; + } + if (seamRef.current) seamRef.current.style.left = `${a - 6}px`; + const frac = (t % 3200) / 3200; + const split = t >= SPEED_AT ? 0.66 : a / (a + b); + const head = + frac < split + ? lerp(0, a, frac / split) + : lerp(a + JOIN, a + JOIN + b, (frac - split) / (1 - split)); + if (playheadRef.current) { + playheadRef.current.style.transform = `translateX(${head}px)`; + } + cursor.tick(PATH, t, seek); + }, + }); + + return ( +
        + +
        + +
        + +
        +
        + + + Add track + +
        + +
        + +
        + } + > + Clip + + + 1x} + style={{ left: 0, width: CLIP_A.start }} + > + + + + {ui.speed ? "2x" : "1x"} + + } + style={{ left: CLIP_A.start + JOIN, width: CLIP_B.start }} + > + + +
        + + Crossfade · 0.5s + +
        +
        +
        + +
        + } + > + Audio + + + + + + + +
        + +
        + +
        +
        + {cursor.Cursor} + +
        + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/grades.tsx b/apps/web/components/pages/HomeTwo/studio/cards/grades.tsx new file mode 100644 index 00000000000..2b807329777 --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/grades.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { classNames } from "@cap/utils/helpers"; +import { useRef } from "react"; +import { + easeOut, + span, + useCursor, + useSceneState, + type Way, +} from "../../scenes/engine"; +import { + CANVAS, + ED, + Field, + Ground, + Panel, + RecordedWindow, + SectionTitle, + Slider, + useLoop, +} from "../shared"; + +type Grade = { name: string; filter: string; tint: string }; + +const GRADES: Grade[] = [ + { name: "None", filter: "none", tint: "transparent" }, + { + name: "Cinematic", + filter: "contrast(1.15) saturate(0.82) sepia(0.12)", + tint: "rgba(24,52,92,0.18)", + }, + { + name: "Noir", + filter: "grayscale(1) contrast(1.25) brightness(0.96)", + tint: "rgba(0,0,0,0.08)", + }, + { + name: "Vintage", + filter: "sepia(0.45) contrast(0.95) brightness(1.04) saturate(0.9)", + tint: "rgba(255,196,120,0.16)", + }, + { + name: "Frost", + filter: "saturate(0.75) brightness(1.06) hue-rotate(-8deg)", + tint: "rgba(168,208,255,0.24)", + }, + { + name: "Golden", + filter: "sepia(0.3) saturate(1.2) brightness(1.03)", + tint: "rgba(255,186,74,0.2)", + }, + { + name: "Midnight", + filter: "brightness(0.82) contrast(1.15) saturate(0.9) hue-rotate(12deg)", + tint: "rgba(22,30,86,0.3)", + }, + { + name: "Vivid", + filter: "saturate(1.55) contrast(1.08)", + tint: "transparent", + }, + { + name: "Dreamy", + filter: "brightness(1.08) contrast(0.88) saturate(1.15)", + tint: "rgba(255,190,230,0.18)", + }, +]; + +const GROUND_GRADIENT = + "linear-gradient(135deg, #C3DCF8 0%, #DACBF9 55%, #F6D9EC 100%)"; + +const DURATION = 9600; +const STOPS: { name: string; at: number }[] = [ + { name: "Cinematic", at: 1000 }, + { name: "Noir", at: 2400 }, + { name: "Golden", at: 3800 }, + { name: "Midnight", at: 5200 }, + { name: "Vivid", at: 6600 }, + { name: "Dreamy", at: 8000 }, +]; +const SWEEP = 900; +const WIPE_MAX = 0.52; + +const WIN = { left: 10, top: 46, scale: 0.95 } as const; +const S = 1.3; +const PANEL = { w: 190, pad: 12 } as const; +const PANEL_POS = { left: CANVAS.w - PANEL.w * S - 12, top: 18 } as const; +const SWATCH = { w: 50, h: 28, gap: 8, label: 14 } as const; +const GRID_TOP = PANEL.pad + 22 + 10; + +const swatchCenter = (index: number) => { + const row = Math.floor(index / 3); + const col = index % 3; + return { + x: + PANEL_POS.left + + (PANEL.pad + col * (SWATCH.w + SWATCH.gap) + SWATCH.w / 2) * S, + y: + PANEL_POS.top + + (GRID_TOP + row * (SWATCH.h + SWATCH.label + SWATCH.gap) + SWATCH.h / 2) * + S, + }; +}; + +const PATH: Way[] = [ + { t: 0, x: 300, y: 330 }, + ...STOPS.flatMap((stop) => { + const point = swatchCenter(GRADES.findIndex((g) => g.name === stop.name)); + return [ + { t: stop.at - 220, ...point }, + { t: stop.at, ...point, click: true }, + ]; + }), + { t: DURATION - 700, x: 290, y: 330 }, + { t: DURATION, x: 290, y: 330 }, +]; + +const gradeAt = (t: number) => { + let current = "None"; + for (const stop of STOPS) if (t >= stop.at) current = stop.name; + return current; +}; + +const wipeAt = (t: number) => { + let latest: (typeof STOPS)[number] | undefined; + for (const stop of STOPS) if (t >= stop.at) latest = stop; + if (!latest) return 0; + return easeOut(span(t, latest.at, latest.at + SWEEP)) * WIPE_MAX; +}; + +const Scene = ({ grade }: { grade: Grade }) => ( + + + + + +); + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const gradedRef = useRef(null); + const lineRef = useRef(null); + const [ui, setUi] = useSceneState({ grade: "None" }); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 4600, + tick: (t, seek) => { + setUi({ grade: gradeAt(t) }); + const wipe = wipeAt(t); + if (gradedRef.current) { + gradedRef.current.style.clipPath = `inset(0 ${(1 - wipe) * 100}% 0 0)`; + } + if (lineRef.current) { + lineRef.current.style.transform = `translateX(${wipe * CANVAS.w}px)`; + lineRef.current.style.opacity = wipe > 0.005 ? "1" : "0"; + } + cursor.tick(PATH, t, seek); + }, + }); + + const grade = GRADES.find((item) => item.name === ui.grade) ?? GRADES[0]; + const graded = grade?.name !== "None"; + + return ( +
        + + + +
        +
        + +
        +
        +
        + +
        + + + +
        + +
        + + {grade?.name} +
        + + + Color correction +
        + {GRADES.map((item) => { + const selected = ui.grade === item.name; + return ( + + + + + + {item.name} + + + ); + })} +
        + + + +
        + {cursor.Cursor} + +
        + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/mask.tsx b/apps/web/components/pages/HomeTwo/studio/cards/mask.tsx new file mode 100644 index 00000000000..50eaf7df333 --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/mask.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useRef } from "react"; +import { + easeOut, + lerp, + span, + useCursor, + useSceneState, + type Way, +} from "../../scenes/engine"; +import { ED, Ground, RecordedWindow, Segmented, useLoop } from "../shared"; + +const WIN = { left: 40, top: 44, scale: 1.45 }; +const MASK = { + x: WIN.left + Math.round(72 * WIN.scale), + y: WIN.top + Math.round(112 * WIN.scale), + w: Math.round(268 * WIN.scale), + h: Math.round(52 * WIN.scale), +}; +const DURATION = 8200; +const DRAG = { start: 600, end: 1700 }; +const BLUR_AT = 1900; +const PIXELATE_AT = 3600; +const HIGHLIGHT_AT = 5400; +const KIND = ["Sensitive", "Highlight"] as const; +const EFFECT = ["Blur", "Pixelate"] as const; +const ITEM_W = 78; +const CARD = { x: MASK.x, y: MASK.y + MASK.h + 12, pad: 8, gap: 12 }; +const kindCenter = (i: number) => ({ + x: CARD.x + CARD.pad + 2 + ITEM_W * (i + 0.5), + y: CARD.y + CARD.pad + 2 + 13, +}); +const EFFECT_X = CARD.x + CARD.pad + ITEM_W * 2 + 4 + CARD.gap + 46; +const effectCenter = (i: number) => ({ + x: EFFECT_X + 2 + ITEM_W * (i + 0.5), + y: CARD.y + CARD.pad + 2 + 13, +}); + +const PATH: Way[] = [ + { t: 0, x: MASK.x - 40, y: MASK.y - 30 }, + { t: DRAG.start, x: MASK.x, y: MASK.y }, + { t: DRAG.end, x: MASK.x + MASK.w, y: MASK.y + MASK.h }, + { t: BLUR_AT, x: MASK.x + MASK.w, y: MASK.y + MASK.h }, + { t: 3400, ...effectCenter(1) }, + { t: PIXELATE_AT, ...effectCenter(1), click: true }, + { t: 5200, ...kindCenter(1) }, + { t: HIGHLIGHT_AT, ...kindCenter(1), click: true }, + { t: 6600, x: MASK.x + MASK.w + 40, y: CARD.y + 70 }, + { t: DURATION, x: MASK.x + MASK.w + 40, y: CARD.y + 70 }, +]; + +const PIXELS = Array.from({ length: 22 * 4 }, (_, i) => { + const seed = Math.sin(i * 12.9898 + 4.1414) * 43758.5453; + const v = seed - Math.floor(seed); + const l = 62 + Math.floor(v * 30); + return `hsl(216 22% ${l}%)`; +}); + +const HANDLES = [ + [0, 0], + [0.5, 0], + [1, 0], + [0, 0.5], + [1, 0.5], + [0, 1], + [0.5, 1], + [1, 1], +] as const; + +const uiAt = (t: number) => ({ + drawing: t >= DRAG.start && t < BLUR_AT, + kind: t >= HIGHLIGHT_AT ? "Highlight" : t >= BLUR_AT ? "Sensitive" : null, + effect: t >= PIXELATE_AT ? "Pixelate" : "Blur", +}); + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const boxRef = useRef(null); + const [ui, setUi] = useSceneState(uiAt(0)); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 4400, + tick: (t, seek) => { + setUi(uiAt(t)); + const drag = easeOut(span(t, DRAG.start, DRAG.end)); + if (boxRef.current) { + boxRef.current.style.width = `${lerp(0, MASK.w, drag)}px`; + boxRef.current.style.height = `${lerp(0, MASK.h, drag)}px`; + boxRef.current.style.opacity = t >= DRAG.start ? "1" : "0"; + } + cursor.tick(PATH, t, seek); + }, + }); + + const applied = ui.kind !== null; + const highlight = ui.kind === "Highlight"; + const blur = applied && !highlight && ui.effect === "Blur"; + const pixelate = applied && !highlight && ui.effect === "Pixelate"; + + return ( +
        + + + +
        +
        +
        + {PIXELS.map((color, i) => ( + + ))} +
        +
        + + {HANDLES.map(([fx, fy]) => ( + + ))} + +
        + +
        + + Effect + + +
        +
        + {cursor.Cursor} + +
        + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/scenes.tsx b/apps/web/components/pages/HomeTwo/studio/cards/scenes.tsx new file mode 100644 index 00000000000..83fdf15691c --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/scenes.tsx @@ -0,0 +1,302 @@ +"use client"; + +import { useRef } from "react"; +import { LucideVideo } from "../../demo/capIcons"; +import { useVideoAttrs, VIDEO_POSTERS } from "../../demo/media"; +import { + useCursor, + useSceneState, + useVideo, + type Way, +} from "../../scenes/engine"; +import { + CANVAS, + ED, + Field, + Ground, + HUE, + Lane, + Panel, + Playhead, + RecordedWindow, + Segment, + Tile, + TRACK_GUTTER, + TRACK_H, + TrackLabel, + useLoop, +} from "../shared"; + +const MODES = [ + "Default", + "Camera Only", + "Hide Camera", + "Split Screen", + "Floating", +] as const; +type Mode = (typeof MODES)[number]; +const SEQUENCE: Mode[] = ["Default", "Camera Only", "Split Screen", "Floating"]; +const SLOT = 3000; +const DURATION = SLOT * SEQUENCE.length; + +const FRAME = { left: 16, top: 48, w: 306, h: 172 }; +const SCREEN = { w: 278, h: 150 }; +const PANEL = { left: 338, top: 44, w: 196, scale: 1.25 }; + +type Box = { + left: number; + top: number; + w: number; + h: number; + r: number; + opacity: number; +}; + +const LAYOUT: Record = { + Default: { + screen: { left: 14, top: 11, w: 278, h: 150, r: 8, opacity: 1, scale: 1 }, + camera: { left: 12, top: 80, w: 80, h: 80, r: 40, opacity: 1 }, + }, + "Camera Only": { + screen: { left: 14, top: 11, w: 278, h: 150, r: 8, opacity: 0, scale: 1 }, + camera: { left: 0, top: 0, w: FRAME.w, h: FRAME.h, r: 8, opacity: 1 }, + }, + "Hide Camera": { + screen: { left: 14, top: 11, w: 278, h: 150, r: 8, opacity: 1, scale: 1 }, + camera: { left: 12, top: 80, w: 80, h: 80, r: 40, opacity: 0 }, + }, + "Split Screen": { + screen: { + left: 8, + top: 22, + w: 145, + h: 128, + r: 8, + opacity: 1, + scale: 128 / SCREEN.h, + }, + camera: { left: 153, top: 22, w: 145, h: 128, r: 8, opacity: 1 }, + }, + Floating: { + screen: { + left: 10, + top: 35, + w: 190, + h: 102, + r: 8, + opacity: 1, + scale: 190 / SCREEN.w, + }, + camera: { left: 206, top: 40, w: 92, h: 92, r: 14, opacity: 1 }, + }, +}; + +const EASE = "cubic-bezier(0.22, 1, 0.36, 1)"; +const MOVE = `left 480ms ${EASE}, top 480ms ${EASE}, width 480ms ${EASE}, height 480ms ${EASE}, border-radius 480ms ${EASE}, opacity 320ms ease, transform 480ms ${EASE}`; + +const tileCenter = (mode: Mode) => { + const i = MODES.indexOf(mode); + const col = i % 2; + const row = Math.floor(i / 2); + const x = 16 + col * 86 + 39; + const y = 43.5 + row * 38 + 15; + return { x: PANEL.left + x * PANEL.scale, y: PANEL.top + y * PANEL.scale }; +}; + +const PATH: Way[] = [ + { t: 0, ...tileCenter("Default") }, + { t: 150, ...tileCenter("Default"), click: true }, + { t: 1200, ...tileCenter("Default") }, + { t: SLOT - 300, ...tileCenter("Camera Only") }, + { t: SLOT, ...tileCenter("Camera Only"), click: true }, + { t: SLOT + 1200, ...tileCenter("Camera Only") }, + { t: SLOT * 2 - 300, ...tileCenter("Split Screen") }, + { t: SLOT * 2, ...tileCenter("Split Screen"), click: true }, + { t: SLOT * 2 + 1200, ...tileCenter("Split Screen") }, + { t: SLOT * 3 - 300, ...tileCenter("Floating") }, + { t: SLOT * 3, ...tileCenter("Floating"), click: true }, + { t: SLOT * 3 + 1200, ...tileCenter("Floating") }, + { t: DURATION - 300, ...tileCenter("Default") }, + { t: DURATION, ...tileCenter("Default") }, +]; + +const modeAt = (t: number): Mode => + SEQUENCE[Math.min(SEQUENCE.length - 1, Math.floor(t / SLOT))] ?? "Default"; + +const S = 1.15; +const LOGICAL = { w: CANVAS.w / S, h: CANVAS.h / S }; +const STRIP = { left: 12, top: 254, pad: 8 }; +const STRIP_W = LOGICAL.w - STRIP.left * 2; +const LANE_W = STRIP_W - STRIP.pad * 2 - TRACK_GUTTER; + +const CameraPane = ({ playing, box }: { playing: boolean; box: Box }) => { + const ref = useRef(null); + const attrs = useVideoAttrs(VIDEO_POSTERS.webcam); + useVideo(playing, ref); + return ( +
        +
        + ); +}; + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const playheadRef = useRef(null); + const [mode, setMode] = useSceneState("Default"); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 7000, + tick: (t, seek) => { + setMode(modeAt(t)); + if (playheadRef.current) { + playheadRef.current.style.transform = `translateX(${(t / DURATION) * LANE_W}px)`; + } + cursor.tick(PATH, t, seek); + }, + }); + + const layout = LAYOUT[mode]; + + return ( +
        + +
        +
        + +
        + +
        + +
        + + +
        + {MODES.map((item) => ( + + {item} + + ))} +
        +
        +
        +
        + +
        +
        + } + > + Scene + + + {SEQUENCE.map((item, i) => ( + + ))} + + +
        +
        + {cursor.Cursor} +
        +
        + ); +}; + +export const SCENE_TRACK_H = TRACK_H; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/text.tsx b/apps/web/components/pages/HomeTwo/studio/cards/text.tsx new file mode 100644 index 00000000000..0f445b30520 --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/text.tsx @@ -0,0 +1,256 @@ +"use client"; + +import { classNames } from "@cap/utils/helpers"; +import { useRef } from "react"; +import { easeOut, lerp, span, typed, useSceneState } from "../../scenes/engine"; +import { Chip, ED, Ground, RecordedWindow, useLoop } from "../shared"; + +const WIN = { left: 120, top: 12, scale: 1 }; +const DURATION = 10000; +const LOWER = { enter: 400, exit: 2900 }; +const STAT = { enter: 3700, exit: 6100 }; +const TYPE = { enter: 6600, exit: 9400 }; +const TYPEWRITER_TEXT = "Try the new flow →"; + +const TEMPLATES = [ + "Title", + "Lower Third", + "Kicker", + "Big Stat", + "Quote", + "Typewriter", +] as const; +type Template = (typeof TEMPLATES)[number]; + +const ROW = { left: 16, top: 282, scale: 1.2, card: 73, gap: 8 }; + +const activeAt = (t: number): { template: Template; anim: string } | null => + t >= TYPE.enter - 100 && t < TYPE.exit + 300 + ? { template: "Typewriter", anim: "Typewriter" } + : t >= STAT.enter - 100 && t < STAT.exit + 300 + ? { template: "Big Stat", anim: "Pop" } + : t >= LOWER.enter - 100 && t < LOWER.exit + 300 + ? { template: "Lower Third", anim: "Slide up" } + : null; + +const Sample = ({ template }: { template: Template }) => { + switch (template) { + case "Title": + return Title; + case "Lower Third": + return ( + + + + Lower third + + + ); + case "Kicker": + return ( + + Kicker + + ); + case "Big Stat": + return 42%; + case "Quote": + return ( + + “Quote” + + ); + case "Typewriter": + return ( + + Typewriter_ + + ); + } +}; + +const Selection = ({ + children, + className, + style, + boxRef, +}: { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; + boxRef?: React.RefObject; +}) => ( +
        + + Text + + {[ + { left: -7, top: -7 }, + { right: -7, top: -7 }, + { left: -7, bottom: -7 }, + { right: -7, bottom: -7 }, + ].map((dot) => ( + + ))} + {children} +
        +); + +export const Visual = ({ playing }: { playing: boolean }) => { + const lowerRef = useRef(null); + const statRef = useRef(null); + const typeRef = useRef(null); + const typeTextRef = useRef(null); + const [ui, setUi] = useSceneState(activeAt(0)); + const lastRef = useRef<{ template: Template; anim: string }>({ + template: "Lower Third", + anim: "Slide up", + }); + if (ui) lastRef.current = ui; + + useLoop({ + duration: DURATION, + playing, + pose: 4800, + tick: (t) => { + setUi(activeAt(t)); + if (lowerRef.current) { + const inF = easeOut(span(t, LOWER.enter, LOWER.enter + 480)); + const outF = easeOut(span(t, LOWER.exit, LOWER.exit + 360)); + lowerRef.current.style.opacity = `${inF * (1 - outF)}`; + lowerRef.current.style.transform = `translateY(${lerp(28, 0, inF) + outF * 18}px)`; + } + if (statRef.current) { + const inF = span(t, STAT.enter, STAT.enter + 520); + const overshoot = 1 + Math.sin(inF * Math.PI) * 0.12; + const outF = easeOut(span(t, STAT.exit, STAT.exit + 320)); + statRef.current.style.opacity = `${easeOut(inF) * (1 - outF)}`; + statRef.current.style.transform = `translateX(-50%) scale(${lerp(0.6, 1, easeOut(inF)) * overshoot})`; + } + if (typeRef.current && typeTextRef.current) { + const outF = easeOut(span(t, TYPE.exit, TYPE.exit + 300)); + typeRef.current.style.opacity = `${t >= TYPE.enter ? 1 - outF : 0}`; + typeTextRef.current.textContent = + t >= TYPE.enter + ? typed(TYPEWRITER_TEXT, t, TYPE.enter + 200, 22) + : ""; + } + }, + }); + + const shown = ui ?? lastRef.current; + + return ( + + +
        + + + + + + + Sofia Chen + + + Head of Product + + + + + + + + 3.2× + + + faster onboarding + + + + + + + + + + +
        + In · {shown.anim} +
        + +
        + {TEMPLATES.map((template) => { + const on = shown.template === template; + return ( +
        + + + {template} + +
        + ); + })} +
        + + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/three-d.tsx b/apps/web/components/pages/HomeTwo/studio/cards/three-d.tsx new file mode 100644 index 00000000000..e695c863ffd --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/three-d.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useRef } from "react"; +import { easeInOut, lerp, span, useSceneState } from "../../scenes/engine"; +import { CANVAS, Ground, HUE, mix, RecordedWindow, useLoop } from "../shared"; + +const DARK = { + card: "#1b1b1e", + line: "rgba(255,255,255,0.08)", + text1: "#f4f4f5", + text2: "#9a9aa3", + ctl: "rgba(255,255,255,0.06)", + accent: "#0a84ff", +} as const; + +const DURATION = 9000; +const MOVES = [ + { + name: "Glide across", + helper: "Move smoothly across the details", + from: 0, + to: 3000, + }, + { + name: "Unfold", + helper: "Reveal your screen with a gentle tilt", + from: 3000, + to: 6000, + }, + { + name: "Pull back", + helper: "Pull out to show the bigger picture", + from: 6000, + to: DURATION, + }, +] as const; + +const WIN_SCALE = 0.85; +const WIN = { + w: 360 * WIN_SCALE, + h: 250 * WIN_SCALE, + left: (CANVAS.w - 360 * WIN_SCALE) / 2, + top: 30, +} as const; +const S = 1.3; +const ROW = { left: 24, top: 272, gap: 10 } as const; +const ROW_W = (CANVAS.w - ROW.left * 2) / S; +const CARD_W = (ROW_W - ROW.gap * 2) / 3; + +type Pose = { ry: number; rx: number; tx: number; s: number }; + +const poseAt = (t: number): Pose => { + if (t < 3000) { + const f = easeInOut(span(t, 0, 3000)); + return { ry: lerp(-16, 16, f), rx: 0, tx: lerp(-26, 26, f), s: 1 }; + } + if (t < 6000) { + const f = easeInOut(span(t, 3000, 6000)); + const arc = Math.sin(f * Math.PI); + return { + ry: lerp(16, 0, f), + rx: arc * 24, + tx: lerp(26, 0, f), + s: lerp(1, 0.97, arc), + }; + } + if (t < 8300) { + const f = easeInOut(span(t, 6000, 8300)); + return { + ry: lerp(0, -8, f), + rx: lerp(0, 9, f), + tx: lerp(0, -10, f), + s: lerp(1, 0.8, f), + }; + } + const f = easeInOut(span(t, 8300, DURATION)); + return { + ry: lerp(-8, -16, f), + rx: lerp(9, 0, f), + tx: lerp(-10, -26, f), + s: lerp(0.8, 1, f), + }; +}; + +const moveAt = (t: number) => + MOVES.find((move) => t >= move.from && t < move.to)?.name ?? MOVES[0].name; + +export const Visual = ({ playing }: { playing: boolean }) => { + const groupRef = useRef(null); + const sideRef = useRef(null); + const topRef = useRef(null); + const floorRef = useRef(null); + const [ui, setUi] = useSceneState({ move: moveAt(0) }); + + useLoop({ + duration: DURATION, + playing, + pose: 700, + tick: (t) => { + setUi({ move: moveAt(t) }); + const pose = poseAt(t); + const tilt = Math.min(1, Math.abs(pose.ry) / 24); + const lean = Math.min(1, Math.abs(pose.rx) / 24); + if (groupRef.current) { + groupRef.current.style.transform = `translateX(${pose.tx}px) scale(${pose.s}) rotateY(${pose.ry}deg) rotateX(${pose.rx}deg)`; + } + if (sideRef.current) { + sideRef.current.style.opacity = `${tilt * 0.55}`; + sideRef.current.style.background = + pose.ry > 0 + ? "linear-gradient(to right, rgba(9,12,20,0) 35%, rgba(9,12,20,0.8))" + : "linear-gradient(to left, rgba(9,12,20,0) 35%, rgba(9,12,20,0.8))"; + } + if (topRef.current) { + topRef.current.style.opacity = `${lean * 0.5}`; + } + if (floorRef.current) { + floorRef.current.style.transform = `translateX(${pose.tx * 1.15}px) scaleX(${0.7 + pose.s * 0.4}) scaleY(${pose.s})`; + floorRef.current.style.opacity = `${0.5 + tilt * 0.3}`; + } + }, + }); + + return ( + +
        +
        +
        +
        + +
        +
        +
        +
        + +
        + {MOVES.map((move) => { + const active = ui.move === move.name; + return ( +
        + + {move.name} + + + {move.helper} + +
        + ); + })} +
        + + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cards/zoom.tsx b/apps/web/components/pages/HomeTwo/studio/cards/zoom.tsx new file mode 100644 index 00000000000..7490e08ec80 --- /dev/null +++ b/apps/web/components/pages/HomeTwo/studio/cards/zoom.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useRef } from "react"; +import { LucideSearch } from "../../demo/capIcons"; +import { + easeInOut, + span, + useCursor, + useSceneState, + type Way, +} from "../../scenes/engine"; +import { + CANVAS, + ED, + Field, + Ground, + HUE, + Lane, + Panel, + Playhead, + RecordedWindow, + Segment, + Segmented, + Slider, + TRACK_GUTTER, + TrackLabel, + useLoop, +} from "../shared"; + +const WIN = { left: 196, top: 18, scale: 1.05 }; +const FOCUS = { x: WIN.left + 300 * WIN.scale, y: WIN.top + 74 * WIN.scale }; +const FRAME_H = 292; +const CLICK_AT = 1500; +const ZOOM_IN = { start: 1600, end: 2500 }; +const ZOOM_OUT = { start: 5200, end: 6100 }; +const DURATION = 8000; +const AMOUNT = 1.5; +const PANEL = { left: 16, top: 18, w: 150, scale: 1.15 }; + +const PATH: Way[] = [ + { t: 0, x: 330, y: 226 }, + { t: 1400, ...FOCUS }, + { t: CLICK_AT, ...FOCUS, click: true }, + { t: 3400, ...FOCUS }, + { t: 4600, x: FOCUS.x - 30, y: FOCUS.y + 44 }, + { t: DURATION, x: FOCUS.x - 30, y: FOCUS.y + 44 }, +]; + +const scaleAt = (t: number) => + 1 + + (AMOUNT - 1) * + (easeInOut(span(t, ZOOM_IN.start, ZOOM_IN.end)) - + easeInOut(span(t, ZOOM_OUT.start, ZOOM_OUT.end))); + +const S = 1.3; +const LOGICAL = { w: CANVAS.w / S, h: CANVAS.h / S }; +const STRIP = { left: 12, top: 226, pad: 8 }; +const STRIP_W = LOGICAL.w - STRIP.left * 2; +const LANE_W = STRIP_W - STRIP.pad * 2 - TRACK_GUTTER; +const SEGMENT = { left: 0.16, width: 0.56 }; + +export const Visual = ({ playing }: { playing: boolean }) => { + const rootRef = useRef(null); + const zoomRef = useRef(null); + const playheadRef = useRef(null); + const [ui, setUi] = useSceneState({ segment: false }); + const cursor = useCursor(rootRef); + + useLoop({ + duration: DURATION, + playing, + pose: 3600, + tick: (t, seek) => { + setUi({ segment: t >= CLICK_AT }); + if (zoomRef.current) { + zoomRef.current.style.transform = `scale(${scaleAt(t)})`; + } + if (playheadRef.current) { + playheadRef.current.style.transform = `translateX(${(t / DURATION) * LANE_W}px)`; + } + cursor.tick(PATH, t, seek); + }, + }); + + return ( +
        + +
        +
        + +
        +
        + +
        + + + + + + + + +
        + +
        +
        + } + > + Zoom + + + Generate zoom segments automatically + · Generate + + ) + } + > + + + +
        +
        + {cursor.Cursor} +
        +
        + ); +}; diff --git a/apps/web/components/pages/HomeTwo/studio/cardsA.tsx b/apps/web/components/pages/HomeTwo/studio/cardsA.tsx deleted file mode 100644 index a6571e326fb..00000000000 --- a/apps/web/components/pages/HomeTwo/studio/cardsA.tsx +++ /dev/null @@ -1,795 +0,0 @@ -"use client"; - -import { classNames } from "@cap/utils/helpers"; -import { useRef } from "react"; -import { ContentWindow } from "../demo/MacDesktop"; -import { useVideoAttrs, VIDEO_POSTERS } from "../demo/media"; -import { - easeInOut, - easeOut, - lerp, - restartAnimation, - SCENE_CSS, - span, - typed, - useCursor, - useSceneState, - useVideo, - type Way, -} from "../scenes/engine"; -import { - CameraBubble, - CanvasStage, - Chip, - RECORDED, - RecordedWindow, - useLoop, -} from "./shared"; - -const MASK_DURATION = 8200; -const MASK = { x: RECORDED.left + 72, y: RECORDED.top + 112, w: 268, h: 52 }; -const MASK_DRAG = { start: 600, end: 1700 }; -const MASK_BLUR = 1900; -const MASK_PIXELATE = 3600; -const MASK_HIGHLIGHT = 5400; -const MASK_CONTROL = { x: MASK.x, y: MASK.y + MASK.h + 14 }; -const MASK_MODES = ["Blur", "Pixelate", "Highlight"] as const; -const SEGMENT_W = 74; - -const MASK_PATH: Way[] = [ - { t: 0, x: MASK.x - 36, y: MASK.y - 26 }, - { t: MASK_DRAG.start, x: MASK.x, y: MASK.y }, - { t: MASK_DRAG.end, x: MASK.x + MASK.w, y: MASK.y + MASK.h }, - { t: MASK_BLUR, x: MASK.x + MASK.w, y: MASK.y + MASK.h }, - { t: 3400, x: MASK_CONTROL.x + SEGMENT_W * 1.5, y: MASK_CONTROL.y + 14 }, - { - t: MASK_PIXELATE, - x: MASK_CONTROL.x + SEGMENT_W * 1.5, - y: MASK_CONTROL.y + 14, - click: true, - }, - { t: 5200, x: MASK_CONTROL.x + SEGMENT_W * 2.5, y: MASK_CONTROL.y + 14 }, - { - t: MASK_HIGHLIGHT, - x: MASK_CONTROL.x + SEGMENT_W * 2.5, - y: MASK_CONTROL.y + 14, - click: true, - }, - { t: 6600, x: MASK.x + MASK.w + 60, y: MASK.y + MASK.h + 90 }, - { t: MASK_DURATION, x: MASK.x + MASK.w + 60, y: MASK.y + MASK.h + 90 }, -]; - -const PIXELS = Array.from({ length: 22 * 4 }, (_, i) => { - const seed = Math.sin(i * 12.9898 + 4.1414) * 43758.5453; - const v = seed - Math.floor(seed); - const l = 62 + Math.floor(v * 30); - return `hsl(216 22% ${l}%)`; -}); - -const maskUiAt = (t: number) => ({ - drawing: t >= MASK_DRAG.start && t < MASK_BLUR, - mode: - t >= MASK_HIGHLIGHT - ? "Highlight" - : t >= MASK_PIXELATE - ? "Pixelate" - : t >= MASK_BLUR - ? "Blur" - : null, -}); - -export const BlurVisual = ({ playing }: { playing: boolean }) => { - const rootRef = useRef(null); - const boxRef = useRef(null); - const [ui, setUi] = useSceneState(maskUiAt(0)); - const cursor = useCursor(rootRef); - - useLoop({ - duration: MASK_DURATION, - playing, - pose: 4400, - tick: (t, seek) => { - setUi(maskUiAt(t)); - const drag = easeOut(span(t, MASK_DRAG.start, MASK_DRAG.end)); - if (boxRef.current) { - boxRef.current.style.width = `${lerp(0, MASK.w, drag)}px`; - boxRef.current.style.height = `${lerp(0, MASK.h, drag)}px`; - boxRef.current.style.opacity = t >= MASK_DRAG.start ? "1" : "0"; - } - cursor.tick(MASK_PATH, t, seek); - }, - }); - - const applied = ui.mode !== null; - const highlight = ui.mode === "Highlight"; - - return ( -
        - - - - -
        -
        -
        - {PIXELS.map((color, i) => ( - - ))} -
        -
        - - {[ - { left: MASK.x - 5, top: MASK.y - 5 }, - { left: MASK.x + MASK.w - 5, top: MASK.y - 5 }, - { left: MASK.x - 5, top: MASK.y + MASK.h - 5 }, - { left: MASK.x + MASK.w - 5, top: MASK.y + MASK.h - 5 }, - ].map((handle) => ( - - ))} - -
        - {MASK_MODES.map((mode) => ( - - {mode} - - ))} -
        - {cursor.Cursor} - -
        - ); -}; - -const TRACK = { top: 328, height: 36, left: 96, right: 584 } as const; - -const TrackStrip = ({ - label, - children, - playhead, -}: { - label: string; - children: React.ReactNode; - playhead: React.RefObject; -}) => ( -
        - - {label} - -
        - {children} -
        - -
        -
        -
        -); - -const CameraPane = ({ - playing, - style, - className, -}: { - playing: boolean; - style?: React.CSSProperties; - className?: string; -}) => { - const ref = useRef(null); - const attrs = useVideoAttrs(VIDEO_POSTERS.webcam); - useVideo(playing, ref); - return ( -
        -
        - ); -}; - -const SCENES_DURATION = 12000; -const SCENE_MODES = [ - { key: "default", label: "Default" }, - { key: "cameraOnly", label: "Camera only" }, - { key: "splitScreen", label: "Split screen" }, - { key: "floating", label: "Floating" }, -] as const; -type SceneKey = (typeof SCENE_MODES)[number]["key"]; -const SCENE_SLOT = SCENES_DURATION / SCENE_MODES.length; -const SCENE_SCREEN = { w: 360, h: 250 } as const; - -const SCENE_LAYOUT: Record< - SceneKey, - { - screen: { - left: number; - top: number; - w: number; - h: number; - shift: number; - scale: number; - radius: number; - opacity: number; - }; - camera: { - left: number; - top: number; - w: number; - h: number; - radius: number; - opacity: number; - }; - } -> = { - default: { - screen: { - left: 120, - top: 30, - w: 360, - h: 250, - shift: 0, - scale: 1, - radius: 10, - opacity: 1, - }, - camera: { left: 28, top: 222, w: 88, h: 88, radius: 44, opacity: 1 }, - }, - cameraOnly: { - screen: { - left: 120, - top: 30, - w: 360, - h: 250, - shift: 0, - scale: 1, - radius: 10, - opacity: 0, - }, - camera: { left: 120, top: 30, w: 360, h: 250, radius: 12, opacity: 1 }, - }, - splitScreen: { - screen: { - left: 24, - top: 30, - w: 272, - h: 250, - shift: -6, - scale: 1, - radius: 12, - opacity: 1, - }, - camera: { left: 304, top: 30, w: 272, h: 250, radius: 12, opacity: 1 }, - }, - floating: { - screen: { - left: 52, - top: 40, - w: 320, - h: 222, - shift: 0, - scale: 320 / 360, - radius: 14, - opacity: 1, - }, - camera: { left: 396, top: 84, w: 160, h: 160, radius: 18, opacity: 1 }, - }, -}; - -const SCENE_EASE = "cubic-bezier(0.22, 1, 0.36, 1)"; -const sceneTransition = `left 480ms ${SCENE_EASE}, top 480ms ${SCENE_EASE}, width 480ms ${SCENE_EASE}, height 480ms ${SCENE_EASE}, border-radius 480ms ${SCENE_EASE}, opacity 320ms ease, transform 480ms ${SCENE_EASE}`; - -const sceneAt = (t: number): SceneKey => - SCENE_MODES[Math.min(SCENE_MODES.length - 1, Math.floor(t / SCENE_SLOT))] - ?.key ?? "default"; - -export const ScenesVisual = ({ playing }: { playing: boolean }) => { - const playheadRef = useRef(null); - const [scene, setScene] = useSceneState("default"); - const scrollRef = useRef(null); - - useLoop({ - duration: SCENES_DURATION, - playing, - pose: 7000, - tick: (t) => { - setScene(sceneAt(t)); - if (playheadRef.current) { - playheadRef.current.style.left = `${(t / SCENES_DURATION) * 100}%`; - } - }, - }); - - const layout = SCENE_LAYOUT[scene]; - - return ( - -
        -
        - -
        -
        - - - {SCENE_MODES.map((mode, i) => ( - - {mode.label} - - ))} - -
        - ); -}; - -const TEXT_DURATION = 10000; -const LOWER = { enter: 400, exit: 2900 }; -const STAT = { enter: 3700, exit: 6100 }; -const TYPE = { enter: 6600, exit: 9400 }; -const TYPEWRITER_TEXT = "Try the new flow →"; - -const textPresetAt = (t: number) => - t >= TYPE.enter - 100 && t < TYPE.exit + 300 - ? "Typewriter · Typewriter" - : t >= STAT.enter - 100 && t < STAT.exit + 300 - ? "Big Stat · Pop" - : t >= LOWER.enter - 100 && t < LOWER.exit + 300 - ? "Lower Third · Slide up" - : null; - -export const TextVisual = ({ playing }: { playing: boolean }) => { - const lowerRef = useRef(null); - const statRef = useRef(null); - const typeRef = useRef(null); - const typeTextRef = useRef(null); - const [preset, setPreset] = useSceneState(null); - const lastPreset = useRef("Lower Third · Slide up"); - if (preset) lastPreset.current = preset; - - useLoop({ - duration: TEXT_DURATION, - playing, - pose: 4800, - tick: (t) => { - setPreset(textPresetAt(t)); - if (lowerRef.current) { - const inF = easeOut(span(t, LOWER.enter, LOWER.enter + 480)); - const outF = easeOut(span(t, LOWER.exit, LOWER.exit + 360)); - lowerRef.current.style.opacity = `${inF * (1 - outF)}`; - lowerRef.current.style.transform = `translateY(${lerp(26, 0, inF) + outF * 18}px)`; - } - if (statRef.current) { - const inF = span(t, STAT.enter, STAT.enter + 520); - const overshoot = 1 + Math.sin(inF * Math.PI) * 0.12; - const outF = easeOut(span(t, STAT.exit, STAT.exit + 320)); - statRef.current.style.opacity = `${easeOut(inF) * (1 - outF)}`; - statRef.current.style.transform = `translateX(-50%) scale(${lerp(0.6, 1, easeOut(inF)) * overshoot})`; - } - if (typeRef.current && typeTextRef.current) { - const outF = easeOut(span(t, TYPE.exit, TYPE.exit + 300)); - typeRef.current.style.opacity = `${t >= TYPE.enter ? 1 - outF : 0}`; - typeTextRef.current.textContent = - t >= TYPE.enter - ? typed(TYPEWRITER_TEXT, t, TYPE.enter + 200, 22) - : ""; - } - }, - }); - - return ( - - - - -
        - - - - Sofia Chen - - - Head of Product - - -
        - -
        - - 3.2× - - - faster onboarding - -
        - -
        - - -
        - -
        - {preset ?? lastPreset.current} -
        -
        - ); -}; - -const ZOOM_DURATION = 8000; -const ZOOM_FOCUS = { x: 420, y: 104 }; -const ZOOM_CLICK = 1500; -const ZOOM_IN = { start: 1600, end: 2600 }; -const ZOOM_OUT = { start: 5200, end: 6200 }; -const ZOOM_PATH: Way[] = [ - { t: 0, x: 300, y: 230 }, - { t: 1400, x: ZOOM_FOCUS.x, y: ZOOM_FOCUS.y }, - { t: ZOOM_CLICK, x: ZOOM_FOCUS.x, y: ZOOM_FOCUS.y, click: true }, - { t: 3200, x: ZOOM_FOCUS.x, y: ZOOM_FOCUS.y }, - { t: 4600, x: ZOOM_FOCUS.x + 26, y: ZOOM_FOCUS.y + 34 }, - { t: ZOOM_DURATION, x: ZOOM_FOCUS.x + 26, y: ZOOM_FOCUS.y + 34 }, -]; - -const zoomScaleAt = (t: number) => - 1 + - easeInOut(span(t, ZOOM_IN.start, ZOOM_IN.end)) - - easeInOut(span(t, ZOOM_OUT.start, ZOOM_OUT.end)); - -export const ZoomVisual = ({ playing }: { playing: boolean }) => { - const rootRef = useRef(null); - const zoomRef = useRef(null); - const ringRef = useRef(null); - const playheadRef = useRef(null); - const clickRef = useRef(-1); - const [ui, setUi] = useSceneState({ segment: false }); - const cursor = useCursor(rootRef); - - useLoop({ - duration: ZOOM_DURATION, - playing, - pose: 3600, - tick: (t, seek) => { - setUi({ segment: t >= ZOOM_CLICK }); - if (zoomRef.current) { - zoomRef.current.style.transform = `scale(${zoomScaleAt(t)})`; - } - if (playheadRef.current) { - playheadRef.current.style.left = `${(t / ZOOM_DURATION) * 100}%`; - } - if (seek) clickRef.current = t - 1; - if (clickRef.current < ZOOM_CLICK && t >= ZOOM_CLICK) { - restartAnimation(ringRef.current, "ht-scene-ripple 700ms ease-out"); - } - clickRef.current = t; - cursor.tick(ZOOM_PATH, t, seek); - }, - }); - - return ( -
        - - -
        - - - - -
        - - - 2× - - - {cursor.Cursor} -
        -
        - ); -}; - -const CAPTIONS_DURATION = 9000; -const CAPTION_LINES = [ - { - start: 600, - words: "So this is the new dashboard we’re shipping".split(" "), - }, - { start: 4400, words: "Every card here pulls live data".split(" ") }, -]; -const WORD_MS = 380; -const CAPTION_CLOCK_BASE = 4; -const BURN_TOGGLE = { x: 548, y: 30 }; -const BURN_AT = 7600; -const CAPTION_PATH: Way[] = [ - { t: 0, x: 470, y: 250 }, - { t: 6600, x: 470, y: 250 }, - { t: 7400, x: BURN_TOGGLE.x, y: BURN_TOGGLE.y }, - { t: BURN_AT, x: BURN_TOGGLE.x, y: BURN_TOGGLE.y, click: true }, - { t: 8400, x: BURN_TOGGLE.x - 60, y: BURN_TOGGLE.y + 70 }, - { t: CAPTIONS_DURATION, x: BURN_TOGGLE.x - 60, y: BURN_TOGGLE.y + 70 }, -]; - -const captionAt = (t: number) => { - let line = 0; - for (let i = 0; i < CAPTION_LINES.length; i++) { - if (t >= (CAPTION_LINES[i]?.start ?? 0)) line = i; - } - const current = CAPTION_LINES[line]; - const elapsed = t - (current?.start ?? 0); - const active = Math.min( - (current?.words.length ?? 1) - 1, - Math.floor(Math.max(0, elapsed) / WORD_MS), - ); - return { - line, - active, - shown: t >= (CAPTION_LINES[0]?.start ?? 0), - burn: t >= BURN_AT, - }; -}; - -export const CaptionsVisual = ({ playing }: { playing: boolean }) => { - const rootRef = useRef(null); - const clockRef = useRef(null); - const [ui, setUi] = useSceneState(captionAt(0)); - const cursor = useCursor(rootRef); - - useLoop({ - duration: CAPTIONS_DURATION, - playing, - pose: 2200, - tick: (t, seek) => { - setUi(captionAt(t)); - if (clockRef.current) { - const seconds = - CAPTION_CLOCK_BASE + Math.floor(Math.max(0, t - 600) / 1000); - clockRef.current.textContent = `0:${String(seconds).padStart(2, "0")}`; - } - cursor.tick(CAPTION_PATH, t, seek); - }, - }); - - const line = CAPTION_LINES[ui.line] ?? CAPTION_LINES[0]; - - return ( -
        - - - - -
        - - - English · Whisper - -
        - -
        - Burn in - - - -
        - -
        - {line?.words.map((word, i) => ( - - {word} - - ))} -
        - - 0:04 - - {cursor.Cursor} -
        -
        - ); -}; diff --git a/apps/web/components/pages/HomeTwo/studio/cardsB.tsx b/apps/web/components/pages/HomeTwo/studio/cardsB.tsx deleted file mode 100644 index 9a9a2a3b219..00000000000 --- a/apps/web/components/pages/HomeTwo/studio/cardsB.tsx +++ /dev/null @@ -1,843 +0,0 @@ -"use client"; - -import { classNames } from "@cap/utils/helpers"; -import Image from "next/image"; -import { useRef } from "react"; -import { - easeInOut, - lerp, - span, - useCursor, - useSceneState, - type Way, -} from "../scenes/engine"; -import { - CANVAS, - CameraBubble, - CanvasStage, - Chip, - RECORDED, - RecordedWindow, - useLoop, -} from "./shared"; - -const THREE_D_DURATION = 9000; - -const threeDPoseAt = (t: number) => { - if (t < 3000) { - const f = easeInOut(span(t, 0, 3000)); - return { - ry: lerp(-14, 14, f), - rx: 0, - tx: lerp(-28, 28, f), - s: 1, - label: "Glide across", - }; - } - if (t < 6000) { - const f = easeInOut(span(t, 3000, 6000)); - return { - ry: lerp(14, 0, f), - rx: lerp(0, 10, f), - tx: lerp(28, 0, f), - s: lerp(1, 0.86, f), - label: "Pull back", - }; - } - if (t < 7500) { - const f = easeInOut(span(t, 6000, 7500)); - return { - ry: lerp(0, -24, f), - rx: lerp(10, 6, f), - tx: lerp(0, -10, f), - s: lerp(0.86, 1, f), - label: "Tilt away", - }; - } - const f = easeInOut(span(t, 7500, THREE_D_DURATION)); - return { - ry: lerp(-24, -14, f), - rx: lerp(6, 0, f), - tx: lerp(-10, -28, f), - s: 1, - label: "Tilt away", - }; -}; - -export const ThreeDVisual = ({ playing }: { playing: boolean }) => { - const groupRef = useRef(null); - const depthRef = useRef(null); - const vignetteRef = useRef(null); - const floorRef = useRef(null); - const [ui, setUi] = useSceneState({ label: "Glide across" }); - - useLoop({ - duration: THREE_D_DURATION, - playing, - pose: 6900, - tick: (t) => { - const pose = threeDPoseAt(t); - setUi({ label: pose.label }); - const tilt = Math.min(1, Math.abs(pose.ry) / 24); - if (groupRef.current) { - groupRef.current.style.transform = `translateX(${pose.tx}px) scale(${pose.s}) rotateY(${pose.ry}deg) rotateX(${pose.rx}deg)`; - } - if (depthRef.current) { - depthRef.current.style.opacity = `${tilt * 0.55}`; - depthRef.current.style.background = - pose.ry > 0 - ? "linear-gradient(to right, rgba(9,12,20,0) 35%, rgba(9,12,20,0.75))" - : "linear-gradient(to left, rgba(9,12,20,0) 35%, rgba(9,12,20,0.75))"; - } - if (vignetteRef.current) { - vignetteRef.current.style.opacity = `${0.18 + tilt * 0.4}`; - } - if (floorRef.current) { - floorRef.current.style.transform = `translateX(${pose.tx * 1.15}px) scaleX(${0.7 + pose.s * 0.4}) scaleY(${pose.s})`; - floorRef.current.style.opacity = `${0.35 + tilt * 0.3}`; - } - }, - }); - - return ( - -
        -
        -
        -
        - - -
        -
        -
        -
        - - - 3D camera - - {ui.label} -
        - - ); -}; - -const CANVAS_DURATION = 10000; -const WALLS = [ - "/backgrounds/monaco.webp", - "/backgrounds/santorini.webp", - "/backgrounds/nyc.webp", -]; -type FrameKind = "none" | "browser" | "macbook"; - -const canvasUiAt = (t: number) => ({ - wall: t < 2500 ? 0 : t < 5000 ? 1 : 2, - frame: (t < 5000 - ? "none" - : t < 7600 - ? "browser" - : t < 9500 - ? "macbook" - : "none") as FrameKind, -}); - -const FRAME_LABEL: Record = { - none: "Frame · None", - browser: "Frame · Browser", - macbook: "Frame · MacBook", -}; - -const canvasScaleAt = (t: number) => { - const grow = easeInOut(span(t, 2500, 3500)); - const back = easeInOut(span(t, 9500, CANVAS_DURATION)); - const s = lerp(lerp(1, 0.9, grow), 1, back); - const r = lerp(lerp(10, 20, grow), 10, back); - return { s, r }; -}; - -export const CanvasVisual = ({ playing }: { playing: boolean }) => { - const groupRef = useRef(null); - const clipRef = useRef(null); - const [ui, setUi] = useSceneState(canvasUiAt(0)); - - useLoop({ - duration: CANVAS_DURATION, - playing, - pose: 6400, - tick: (t) => { - setUi(canvasUiAt(t)); - const { s, r } = canvasScaleAt(t); - if (groupRef.current) groupRef.current.style.transform = `scale(${s})`; - if (clipRef.current) clipRef.current.style.borderRadius = `${r}px`; - }, - }); - - const browser = ui.frame === "browser"; - const macbook = ui.frame === "macbook"; - - return ( - - {WALLS.slice(1).map((wall, i) => ( - = i + 1 ? 1 : 0 }} - /> - ))} -
        -
        -
        -
        - - {["#FF5F57", "#FEBC2E", "#28C840"].map((color) => ( - - ))} - - - Dashboard - - - acme.com/dashboard - -
        -
        -
        - -
        -
        -
        - -
        - {FRAME_LABEL[ui.frame]} -
        - - ); -}; - -type Grade = { - name: string; - filter: string; - tint: string; - sliders: [number, number, number]; -}; - -const NONE_GRADE: Grade = { - name: "None", - filter: "none", - tint: "transparent", - sliders: [0.5, 0.5, 0.5], -}; - -const GRADES: Grade[] = [ - { - name: "Cinematic", - filter: "contrast(1.15) saturate(0.82) sepia(0.12)", - tint: "rgba(24,52,92,0.18)", - sliders: [0.45, 0.68, 0.42], - }, - { - name: "Noir", - filter: "grayscale(1) contrast(1.25) brightness(0.96)", - tint: "rgba(0,0,0,0.08)", - sliders: [0.48, 0.78, 0.02], - }, - { - name: "Vintage", - filter: "sepia(0.45) contrast(0.95) brightness(1.04) saturate(0.9)", - tint: "rgba(255,196,120,0.16)", - sliders: [0.56, 0.42, 0.44], - }, - { - name: "Frost", - filter: "saturate(0.75) brightness(1.06) hue-rotate(-8deg)", - tint: "rgba(168,208,255,0.24)", - sliders: [0.6, 0.46, 0.36], - }, - { - name: "Golden", - filter: "sepia(0.3) saturate(1.2) brightness(1.03)", - tint: "rgba(255,186,74,0.2)", - sliders: [0.55, 0.52, 0.66], - }, - { - name: "Midnight", - filter: "brightness(0.82) contrast(1.15) saturate(0.9) hue-rotate(12deg)", - tint: "rgba(22,30,86,0.3)", - sliders: [0.3, 0.66, 0.45], - }, - { - name: "Vivid", - filter: "saturate(1.55) contrast(1.08)", - tint: "transparent", - sliders: [0.52, 0.6, 0.88], - }, - { - name: "Dreamy", - filter: "brightness(1.08) contrast(0.88) saturate(1.15)", - tint: "rgba(255,190,230,0.18)", - sliders: [0.66, 0.36, 0.62], - }, -]; - -const GRADES_DURATION = 9600; -const SWATCH = { w: 62, gap: 6, x: 16, y: CANVAS.h - 16 - 26 }; -const GRADE_STOPS: { name: string; at: number }[] = [ - { name: "Cinematic", at: 1000 }, - { name: "Noir", at: 2400 }, - { name: "Golden", at: 3800 }, - { name: "Midnight", at: 5200 }, - { name: "Vivid", at: 6600 }, - { name: "Dreamy", at: 8000 }, -]; -const SLIDER_LABELS = ["Exposure", "Contrast", "Saturation"]; - -const swatchCenter = (name: string) => { - const index = GRADES.findIndex((grade) => grade.name === name); - return { - x: SWATCH.x + index * (SWATCH.w + SWATCH.gap) + SWATCH.w / 2, - y: SWATCH.y + 13, - }; -}; - -const GRADE_PATH: Way[] = [ - { t: 0, x: 320, y: 190 }, - ...GRADE_STOPS.flatMap((stop) => { - const point = swatchCenter(stop.name); - return [ - { t: stop.at - 160, ...point }, - { t: stop.at, ...point, click: true }, - ]; - }), - { t: GRADES_DURATION - 600, x: 420, y: 210 }, - { t: GRADES_DURATION, x: 420, y: 210 }, -]; - -const gradeAt = (t: number) => { - let current = NONE_GRADE.name; - for (const stop of GRADE_STOPS) if (t >= stop.at) current = stop.name; - return current; -}; - -export const GradesVisual = ({ playing }: { playing: boolean }) => { - const rootRef = useRef(null); - const [ui, setUi] = useSceneState({ grade: NONE_GRADE.name }); - const cursor = useCursor(rootRef); - - useLoop({ - duration: GRADES_DURATION, - playing, - pose: 4600, - tick: (t, seek) => { - setUi({ grade: gradeAt(t) }); - cursor.tick(GRADE_PATH, t, seek); - }, - }); - - const grade = GRADES.find((item) => item.name === ui.grade) ?? NONE_GRADE; - - return ( -
        - -
        - - -
        -
        -
        - -
        - {SLIDER_LABELS.map((label, i) => ( -
        - - {label} - - - - - - - -
        - ))} -
        - -
        - {GRADES.map((item) => ( - - {item.name} - - ))} -
        - {cursor.Cursor} - -
        - ); -}; - -const CLIPS_DURATION = 10000; -const PANEL = { x: 16, y: 206, w: 568, h: 153 }; -const GUTTER = 68; -const LANE_X = PANEL.x + 16 + GUTTER + 8; -const LANE_W = PANEL.w - 32 - GUTTER - 8; -const TRACK_Y = PANEL.y + 16 + 22 + 8; -const TRACK_H = 40; -const CLIP_A = { start: 232, trimmed: 196 }; -const CLIP_B = { start: 190, fast: 95 }; -const TRIM = { start: 2200, end: 3400 }; -const SPEED_AT = 3900; -const FADE_AT = 5200; -const AUDIO_AT = 6400; - -const WAVES = Array.from({ length: 48 }, (_, i) => { - const a = Math.sin(i * 0.7) * 0.5 + 0.5; - const b = Math.sin(i * 1.9 + 1) * 0.5 + 0.5; - return { key: `w${i}`, h: 0.25 + 0.7 * (0.5 * a + 0.5 * b) }; -}); - -const clipsUiAt = (t: number) => ({ - speed: t >= SPEED_AT, - fade: t >= FADE_AT, - audio: t >= AUDIO_AT, -}); - -const clipWidthsAt = (t: number) => { - const trim = easeInOut(span(t, TRIM.start, TRIM.end)); - const a = lerp(CLIP_A.start, CLIP_A.trimmed, trim); - const b = t >= SPEED_AT ? CLIP_B.fast : CLIP_B.start; - return { a, b }; -}; - -const CLIPS_PATH: Way[] = [ - { t: 0, x: 420, y: 120 }, - { t: 1900, x: LANE_X + CLIP_A.start + 2, y: TRACK_Y + TRACK_H / 2 }, - { t: TRIM.start, x: LANE_X + CLIP_A.start + 2, y: TRACK_Y + TRACK_H / 2 }, - { t: TRIM.end, x: LANE_X + CLIP_A.trimmed + 2, y: TRACK_Y + TRACK_H / 2 }, - { - t: SPEED_AT - 150, - x: LANE_X + CLIP_A.trimmed + 6 + CLIP_B.start / 2, - y: TRACK_Y + TRACK_H / 2, - }, - { - t: SPEED_AT, - x: LANE_X + CLIP_A.trimmed + 6 + CLIP_B.start / 2, - y: TRACK_Y + TRACK_H / 2, - click: true, - }, - { t: FADE_AT - 150, x: LANE_X + CLIP_A.trimmed + 3, y: TRACK_Y - 6 }, - { t: FADE_AT, x: LANE_X + CLIP_A.trimmed + 3, y: TRACK_Y - 6, click: true }, - { t: AUDIO_AT + 400, x: 470, y: 150 }, - { t: CLIPS_DURATION, x: 470, y: 150 }, -]; - -export const ClipsVisual = ({ playing }: { playing: boolean }) => { - const rootRef = useRef(null); - const clipARef = useRef(null); - const clipBRef = useRef(null); - const seamRef = useRef(null); - const playheadRef = useRef(null); - const [ui, setUi] = useSceneState(clipsUiAt(0)); - const cursor = useCursor(rootRef); - - useLoop({ - duration: CLIPS_DURATION, - playing, - pose: 7200, - tick: (t, seek) => { - setUi(clipsUiAt(t)); - const { a, b } = clipWidthsAt(t); - if (clipARef.current) clipARef.current.style.width = `${a}px`; - if (clipBRef.current) { - clipBRef.current.style.left = `${a + 6}px`; - clipBRef.current.style.width = `${b}px`; - } - if (seamRef.current) seamRef.current.style.left = `${a - 6}px`; - const frac = (t % 3200) / 3200; - const fast = t >= SPEED_AT; - const split = fast ? 0.66 : a / (a + b); - const head = - frac < split - ? lerp(0, a, frac / split) - : lerp(a + 6, a + 6 + b, (frac - split) / (1 - split)); - if (playheadRef.current) { - playheadRef.current.style.transform = `translateX(${head}px)`; - } - cursor.tick(CLIPS_PATH, t, seek); - }, - }); - - return ( -
        - - - - -
        -
        -
        -
        - {["0:00", "0:05", "0:10", "0:15", "0:20"].map((label, i) => ( - - {label} - - - ))} -
        -
        - -
        -
        - Video -
        -
        -
        - - Clip 1 - - - {WAVES.slice(0, 30).map((wave) => ( - - ))} - -
        -
        - - Clip 2 - - - 2× - - - {WAVES.slice(18, 40).map((wave) => ( - - ))} - -
        -
        - - Crossfade - -
        -
        - - -
        -
        -
        - -
        -
        - Audio -
        -
        - - Lofi 03 - - - {WAVES.map((wave) => ( - - ))} - -
        -
        -
        - {cursor.Cursor} - -
        - ); -}; diff --git a/apps/web/components/pages/HomeTwo/studio/catalog.ts b/apps/web/components/pages/HomeTwo/studio/catalog.ts index ef5d8234eba..b1468cee710 100644 --- a/apps/web/components/pages/HomeTwo/studio/catalog.ts +++ b/apps/web/components/pages/HomeTwo/studio/catalog.ts @@ -3,48 +3,39 @@ import dynamic from "next/dynamic"; import type { StudioCard } from "./shared"; +const visual = (load: () => Promise<{ Visual: StudioCard["Visual"] }>) => + dynamic(() => load().then((module) => module.Visual)); + export const CARDS_A: StudioCard[] = [ { key: "mask", title: "Blur what is private", body: "Drop a mask over a password, an email, or a face and choose blur or pixelate. Or flip it to a highlight that dims everything else.", - span: 2, - Visual: dynamic(() => - import("./cardsA").then((module) => module.BlurVisual), - ), + Visual: visual(() => import("./cards/mask")), }, { key: "scenes", title: "Scenes for screen and camera", body: "Switch any stretch of the timeline to camera only, hide camera, split screen, or floating cards, each with its own transition.", - span: 2, - Visual: dynamic(() => - import("./cardsA").then((module) => module.ScenesVisual), - ), + Visual: visual(() => import("./cards/scenes")), }, { key: "text", title: "Text that animates", body: "Titles, lower thirds, big stats, and typewriter callouts as stackable tracks, with fade, slide, pop, or typewriter in and out.", - Visual: dynamic(() => - import("./cardsA").then((module) => module.TextVisual), - ), + Visual: visual(() => import("./cards/text")), }, { key: "zoom", title: "Automatic zoom", body: "Generate zooms from your recorded clicks, or draw your own from 1x to 4.5x with a fixed focal point.", - Visual: dynamic(() => - import("./cardsA").then((module) => module.ZoomVisual), - ), + Visual: visual(() => import("./cards/zoom")), }, { key: "captions", title: "Captions, generated locally", body: "Transcribe on your machine in 19 languages, fix the words, style them, and burn them in with the active word highlighted.", - Visual: dynamic(() => - import("./cardsA").then((module) => module.CaptionsVisual), - ), + Visual: visual(() => import("./cards/captions")), }, ]; @@ -53,32 +44,24 @@ export const CARDS_B: StudioCard[] = [ key: "three-d", title: "3D camera moves", body: "Tilt the frame into perspective and glide, sweep, or pull back across it with focus blur.", - Visual: dynamic(() => - import("./cardsB").then((module) => module.ThreeDVisual), - ), + Visual: visual(() => import("./cards/three-d")), }, { key: "canvas", title: "Any canvas, one recording", body: "Wallpapers, gradients, or your own image, then padding, corners, shadows, and a macOS, Windows, browser, or MacBook frame.", - Visual: dynamic(() => - import("./cardsB").then((module) => module.CanvasVisual), - ), + Visual: visual(() => import("./cards/canvas")), }, { key: "grades", title: "Color grades", body: "Cinematic, Noir, Vintage, Frost, Golden, Midnight, Vivid, or Dreamy, then dial exposure, contrast, and vignette.", - Visual: dynamic(() => - import("./cardsB").then((module) => module.GradesVisual), - ), + Visual: visual(() => import("./cards/grades")), }, { key: "clips", title: "Clips, speed, and music", body: "Trim, split, reorder, speed up to 8x, crossfade between clips, and lay a track from the built in library underneath.", - Visual: dynamic(() => - import("./cardsB").then((module) => module.ClipsVisual), - ), + Visual: visual(() => import("./cards/clips")), }, ]; diff --git a/apps/web/components/pages/HomeTwo/studio/shared.tsx b/apps/web/components/pages/HomeTwo/studio/shared.tsx index ed0f0fa8cac..352079e4c30 100644 --- a/apps/web/components/pages/HomeTwo/studio/shared.tsx +++ b/apps/web/components/pages/HomeTwo/studio/shared.tsx @@ -1,29 +1,63 @@ "use client"; import { classNames } from "@cap/utils/helpers"; -import Image from "next/image"; import { type ComponentType, type CSSProperties, type ReactNode, + type RefObject, useEffect, useRef, } from "react"; import { ContentWindow } from "../demo/MacDesktop"; import { useVideoAttrs, VIDEO_POSTERS } from "../demo/media"; -import { useVideo } from "../scenes/engine"; +import { SCENE_CSS, useVideo } from "../scenes/engine"; import { useReducedMotion } from "../visibility"; export type StudioCard = { key: string; title: string; body: string; - span?: 1 | 2; Visual: ComponentType<{ playing: boolean }>; }; export const CANVAS = { w: 600, h: 375 } as const; +// Mirrors the desktop editor's ed-* light tokens (apps/desktop/src/styles/theme.css) and +// Timeline/styles.css; keep in sync when the editor theme changes. +export const ED = { + window: "#f1f1f3", + card: "#ffffff", + card2: "#f6f6f7", + stage: "#e9e9ec", + line: "rgba(0,0,0,0.075)", + lineStrong: "rgba(0,0,0,0.12)", + text1: "#1c1c1e", + text2: "#6e6e75", + text3: "#a0a0a8", + ctl: "rgba(0,0,0,0.045)", + ctlHover: "rgba(0,0,0,0.075)", + ctlActive: "rgba(0,0,0,0.11)", + accent: "#007aff", + playhead: "#ff3b30", + cardShadow: "0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.075)", + popShadow: "0 12px 32px -12px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.075)", +} as const; + +export const HUE = { + clip: "#3b82f6", + zoom: "#64748b", + caption: "#0ea5e9", + text: "#14b8a6", + mask: "#ef4444", + scene: "#8b5cf6", + audio: "#22c55e", + threeD: "#6366f1", +} as const; + +export const mix = (hue: string, pct: number, base = "#ffffff") => + `color-mix(in srgb, ${hue} ${pct}%, ${base})`; + export const useLoop = ({ duration, playing, @@ -64,32 +98,37 @@ export const useLoop = ({ }, [playing, reduced, duration, pose]); }; -export const CanvasStage = ({ - wallpaper = "/backgrounds/monaco.webp", +export type Tone = "light" | "chrome" | "dark" | "gradient"; + +const GROUND_BG: Record = { + light: "#F4F6F9", + chrome: ED.window, + dark: "#141518", + gradient: "linear-gradient(135deg, #C3DCF8 0%, #DACBF9 55%, #F6D9EC 100%)", +}; + +export const Ground = ({ + tone = "light", className, style, children, }: { - wallpaper?: string; + tone?: Tone; className?: string; style?: CSSProperties; children: ReactNode; }) => (
        - + {children}
        ); @@ -106,6 +145,7 @@ export const RecordedWindow = ({ top = RECORDED.top, width = RECORDED.width, height = RECORDED.height, + scale = 1, className, style, }: { @@ -113,6 +153,7 @@ export const RecordedWindow = ({ top?: number; width?: number; height?: number; + scale?: number; className?: string; style?: CSSProperties; }) => { @@ -120,7 +161,15 @@ export const RecordedWindow = ({ return (
        @@ -188,13 +237,506 @@ export const Chip = ({ className, )} style={{ - background: "#fcfcfc", - color: "#202020", - border: "1px solid rgba(0,0,0,0.08)", - boxShadow: "0 8px 20px -10px rgba(16,24,40,0.45)", + background: ED.card, + color: ED.text1, + boxShadow: ED.popShadow, ...style, }} > {children} ); + +export const Panel = ({ + className, + style, + children, +}: { + className?: string; + style?: CSSProperties; + children: ReactNode; +}) => ( +
        + {children} +
        +); + +export const SectionTitle = ({ + children, + right, +}: { + children: ReactNode; + right?: ReactNode; +}) => ( +
        + + {children} + + {right} +
        +); + +export const Field = ({ + label, + value, + badge, + disabled, + children, +}: { + label: ReactNode; + value?: ReactNode; + badge?: ReactNode; + disabled?: boolean; + children?: ReactNode; +}) => ( +
        +
        + + {label} + + {badge ? ( + + {badge} + + ) : null} + {value !== undefined ? ( + + {value} + + ) : null} +
        + {children} +
        +); + +export const InlineField = ({ + label, + value, + children, +}: { + label: ReactNode; + value?: ReactNode; + children?: ReactNode; +}) => ( +
        + + {label} + +
        + {children} + {value !== undefined ? ( + + {value} + + ) : null} +
        +
        +); + +export const Slider = ({ + fill, + className, +}: { + fill: number; + className?: string; +}) => ( +
        +
        +
        +
        + +
        +); + +export const Segmented = ({ + options, + value, + itemWidth, + disabled, + className, + style, +}: { + options: readonly string[]; + value: string | null; + itemWidth?: number; + disabled?: boolean; + className?: string; + style?: CSSProperties; +}) => ( +
        + {options.map((option) => { + const on = value === option; + return ( + + {option} + + ); + })} +
        +); + +export const Toggle = ({ on }: { on: boolean }) => ( + + + +); + +export const EditorButton = ({ + children, + primary, + className, + style, +}: { + children: ReactNode; + primary?: boolean; + className?: string; + style?: CSSProperties; +}) => ( + + {children} + +); + +export const Tile = ({ + selected, + children, + className, + style, +}: { + selected?: boolean; + children: ReactNode; + className?: string; + style?: CSSProperties; +}) => ( + + {children} + +); + +export const TRACK_H = 44; +export const TRACK_GAP = 6; +export const TRACK_GUTTER = 96; +export const RULER_H = 26; + +export const TrackLabel = ({ + hue, + icon, + children, + style, +}: { + hue: string; + icon?: ReactNode; + children: ReactNode; + style?: CSSProperties; +}) => ( +
        + + {icon ? {icon} : null} + + + {children} + +
        +); + +export const Lane = ({ + empty, + className, + style, + children, +}: { + empty?: ReactNode; + className?: string; + style?: CSSProperties; + children?: ReactNode; +}) => ( +
        + {empty ? ( +
        + {empty} +
        + ) : null} + {children} +
        +); + +export const Segment = ({ + hue, + label, + sublabel, + trailing, + selected, + segmentRef, + className, + style, + children, +}: { + hue: string; + label?: ReactNode; + sublabel?: ReactNode; + trailing?: ReactNode; + selected?: boolean; + segmentRef?: RefObject; + className?: string; + style?: CSSProperties; + children?: ReactNode; +}) => ( +
        + + {label !== undefined ? ( + + + {label} + + {sublabel !== undefined ? ( + + {sublabel} + + ) : null} + {trailing} + + ) : null} + {children} +
        +); + +export const Ruler = ({ + labels, + span: total, + className, + style, +}: { + labels: string[]; + span: number; + className?: string; + style?: CSSProperties; +}) => ( +
        + {labels.map((label, i) => ( + + {label} + + + ))} +
        +); + +export const Playhead = ({ + playheadRef, + top = 0, + bottom = 0, + left = 0, +}: { + playheadRef: RefObject; + top?: number; + bottom?: number; + left?: number; +}) => ( +
        + + +
        +); + +export const WAVES = Array.from({ length: 64 }, (_, i) => { + const a = Math.sin(i * 0.7) * 0.5 + 0.5; + const b = Math.sin(i * 1.9 + 1) * 0.5 + 0.5; + return { key: `w${i}`, h: 0.25 + 0.7 * (0.5 * a + 0.5 * b) }; +}); + +export const Waveform = ({ + from = 0, + to = WAVES.length, + color, + className, +}: { + from?: number; + to?: number; + color?: string; + className?: string; +}) => ( + + {WAVES.slice(from, to).map((wave) => ( + + ))} + +); + +export const SpeedChip = ({ + children, + active, + className, +}: { + children: ReactNode; + active?: boolean; + className?: string; +}) => ( + + {children} + +);