From ba2d3f0b4e4e436f0350a87550e5758fa12424a5 Mon Sep 17 00:00:00 2001 From: SomeRandmGuyy <127457986+SomeRandmGuyy@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:47:55 +0000 Subject: [PATCH 1/5] feat(marketing): three.js clipping engine with moving parts Replace the CSS-3D clip engine with a WebGL clipping engine in the style of the Spectrum Web Co engine: an octagonal chassis, pumping pistons, a glass core with a spinning crystal, three render rotors, two film reels feeding a film strip, a hook scanner, a cutter that slams the strip into clips, an approval gate, four transmitters that broadcast ripples, and a library vault. Twelve clips move between formations while the camera orbits the engine. The anime.js scroll timeline drives the engine's stage progress, readout, panels and HUD; anime.js also runs the assembly intro, stage pulses, pointer parallax and transmitter ripples. three.js is loaded with a dynamic import so only the landing page pays for it. Stage configs and clip formations live in src/lib/clip-engine.ts with tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AzZUDyzfUAfxJAXaPfiq8t --- README.md | 35 +- package-lock.json | 65 ++ package.json | 2 + .../marketing/anime/landing-motion.ts | 246 ++---- src/components/marketing/clip-engine.tsx | 115 +-- .../marketing/engine/clip-engine-scene.ts | 787 ++++++++++++++++++ src/lib/clip-engine.test.ts | 187 ++--- src/lib/clip-engine.ts | 488 +++++++---- src/styles.css | 340 +------- 9 files changed, 1398 insertions(+), 867 deletions(-) create mode 100644 src/components/marketing/engine/clip-engine-scene.ts diff --git a/README.md b/README.md index 734d2b9..75015f0 100644 --- a/README.md +++ b/README.md @@ -67,25 +67,30 @@ Plus two audiences beyond the operator: ### Landing clip engine The landing page is a scroll-driven story built on [anime.js](https://animejs.com) -v4, in the same style as the Spectrum Web Co and Desk Lanes sites. A pinned -CSS-3D stage (`src/components/marketing/clip-engine.tsx`) holds the ClippyOS -clip engine: twelve footage frames around a spinning ClippyOS core. Scrolling -the chapters in `src/components/marketing/landing-page.tsx` scrubs one timeline -in `src/components/marketing/anime/landing-motion.ts` with `onScroll`: the ring -unrolls into a film strip, hooks are detected and cut into four clips, the -clips are captioned into vertical cards, signed off in Approvals, fanned out to -X, YouTube, Instagram and TikTok, stacked into the Library, and pulled into the -Hermes loop. The same file runs the hero intro (split text, boot LEDs, frames -dropping into the ring), split-word reveals, the scroll HUD and magnetic -buttons, all inside one `createScope` that is reverted on unmount. - -The engine is a labelled demo run. Poses per stage come from +v4 and [three.js](https://threejs.org), in the same style as the Spectrum Web Co +engine. A pinned WebGL stage (`src/components/marketing/engine/clip-engine-scene.ts`) +renders the ClippyOS clipping engine as a machine with moving parts: an octagonal +chassis, pumping pistons, a glass core with a spinning crystal, three render +rotors, two film reels feeding a film strip, a hook scanner, a cutter that slams +the strip into clips, an approval gate, a crown of four transmitters that +broadcast ripples, and a library vault. Twelve clips move between formations +(halo, reel, cut row, render spiral, gate arc, broadcast, vault stack, Hermes +orbit) while the camera orbits the engine. + +Scrolling the chapters in `src/components/marketing/landing-page.tsx` scrubs one +anime.js timeline in `src/components/marketing/anime/landing-motion.ts` with +`onScroll`; it drives the engine's stage progress, the readout, panels and HUD. +anime.js also runs the engine assembly intro, stage pulses, pointer parallax, +transmitter ripples, split-word reveals and magnetic buttons. three.js is loaded +with a dynamic import so it only ships to the landing page. + +The engine is a labelled demo run. Stage configs and clip formations live in `src/lib/clip-engine.ts` and are covered by `src/lib/clip-engine.test.ts`. Ingest, detect, cut and caption/render are marked Rolling out until the native clipping pipeline ships (see [Roadmap](#roadmap)); approvals, the Social Machine, the Library and Hermes are marked Live. With -`prefers-reduced-motion`, the stage renders one static formation and nothing -animates. +`prefers-reduced-motion`, the engine renders once, assembled, and nothing +animates. Without WebGL the stage keeps its readouts over a static glow. ## Architecture diff --git a/package-lock.json b/package-lock.json index 9f3e1fd..65d5262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.3.0", + "three": "^0.186.1", "tw-animate-css": "^1.3.4", "undici": "^8.10.0", "vaul": "^1.1.2", @@ -72,6 +73,7 @@ "@types/pg": "^8.11.10", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/three": "^0.186.0", "@vitejs/plugin-react": "^5.2.0", "eslint": "^9.20.0", "eslint-config-prettier": "^10.1.1", @@ -893,6 +895,13 @@ "axios": "^1.6.1" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/@electric-sql/pglite": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.5.tgz", @@ -5043,6 +5052,13 @@ "node": ">= 10" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5206,6 +5222,35 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.186.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.186.0.tgz", + "integrity": "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.3", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.67.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", @@ -7073,6 +7118,13 @@ "integrity": "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==", "license": "MIT" }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8113,6 +8165,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9693,6 +9752,12 @@ "node": ">=18" } }, + "node_modules/three": { + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/package.json b/package.json index f7adddc..8cc4e26 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.3.0", + "three": "^0.186.1", "tw-animate-css": "^1.3.4", "undici": "^8.10.0", "vaul": "^1.1.2", @@ -88,6 +89,7 @@ "@types/pg": "^8.11.10", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/three": "^0.186.0", "@vitejs/plugin-react": "^5.2.0", "eslint": "^9.20.0", "eslint-config-prettier": "^10.1.1", diff --git a/src/components/marketing/anime/landing-motion.ts b/src/components/marketing/anime/landing-motion.ts index 1b5c2d6..6776b5a 100644 --- a/src/components/marketing/anime/landing-motion.ts +++ b/src/components/marketing/anime/landing-motion.ts @@ -8,40 +8,18 @@ import { stagger, utils, type AnimatableObject, - type AnimationParams, - type JSAnimation, } from "animejs"; -import { - ENGINE_STAGES, - HOOK_SLOT, - LAST_STAGE, - SCAN_RANGE, - framePose, - rigPose, - spinsAt, - stageIndex, - timecode, - type FramePose, -} from "@/lib/clip-engine"; +import { ENGINE_STAGES, LAST_STAGE, engineStateAt, stageIndex, timecode } from "@/lib/clip-engine"; +import type { + EngineHandle, + EngineTheme, + EngineView, +} from "@/components/marketing/engine/clip-engine-scene"; type Cleanup = () => void; const STAGE_MS = 1000; -const SPIN_MS = 28000; const noop: Cleanup = () => {}; -const FRAME_PROPS = [ - "x", - "y", - "z", - "ry", - "rx", - "w", - "h", - "sp", - "hot", - "cap", - "pub", -] as const satisfies readonly (keyof FramePose)[]; function all(root: ParentNode, selector: string): T[] { return Array.from(root.querySelectorAll(selector)); @@ -59,43 +37,19 @@ function scramble(el: HTMLElement, text: string, chars = "uppercase") { animate(el, { innerHTML: scrambleText({ text, chars, cursor: true }), duration: 460 }); } -function frameIndex(el: unknown) { - return Number((el as HTMLElement).dataset.index); +function currentTheme(): EngineTheme { + return document.documentElement.dataset.theme === "light" ? "light" : "dark"; } -function frameTween(stage: number): AnimationParams { - const params: AnimationParams = {}; - FRAME_PROPS.forEach((prop) => { - params[`--${prop}`] = (el: unknown) => [ - framePose(frameIndex(el), stage - 1)[prop], - framePose(frameIndex(el), stage)[prop], - ]; - }); - return params; -} - -function applyStage( - frames: HTMLElement[], - rig: HTMLElement, - core: HTMLElement | null, - stage: number, -) { - frames.forEach((frame) => { - const pose = framePose(frameIndex(frame), stage); - FRAME_PROPS.forEach((prop) => frame.style.setProperty(`--${prop}`, String(pose[prop]))); - }); - const camera = rigPose(stage); - rig.style.setProperty("--cam-rx", String(camera.rx)); - rig.style.setProperty("--cam-ry", String(camera.ry)); - rig.style.setProperty("--zoom", String(camera.zoom)); - core?.style.setProperty("--core", String(camera.core)); +function cameraText(progress: number) { + const { cam } = engineStateAt(progress); + const orbit = ((Math.round(cam.rot) % 360) + 360) % 360; + return `orbit ${pad(orbit, 3)}° · pitch ${Math.round(cam.pitch)}° · dist ${cam.dist.toFixed(1)}`; } function heroIntro(root: HTMLElement, reduced: boolean): Cleanup { const title = one(root, "[data-hero-title]"); const items = all(root, "[data-hero-item]"); - const frames = all(root, "[data-frame]"); - const core = one(root, "[data-engine-core]"); const leds = all(root, "[data-led]"); const status = one(root, "[data-engine-status]"); if (!title) return noop; @@ -108,34 +62,19 @@ function heroIntro(root: HTMLElement, reduced: boolean): Cleanup { const split = splitText(title, { words: { wrap: "clip" }, chars: true }); utils.set(split.chars, { y: "115%" }); utils.set(items, { opacity: 0, "--rise": 24 }); - utils.set(frames, { "--drop": 320, opacity: 0 }); - if (core) utils.set(core, { "--boot": 0 }); const ledsOn = 620; const ledStep = 200; const online = ledsOn + leds.length * ledStep + 300; - const intro = createTimeline({ defaults: { ease: "out(4)" } }) + createTimeline({ defaults: { ease: "out(4)" } }) .add(split.chars, { y: "0%", duration: 900, delay: stagger(14) }, 120) .add(items, { opacity: 1, "--rise": 0, duration: 900, delay: stagger(110) }, 520) - .add( - frames, - { - "--drop": 0, - opacity: 1, - duration: 1100, - ease: "outBack(1.2)", - delay: stagger(55, { from: "center" }), - }, - 240, - ) .add(leds, { "--amber": [0, 1], duration: 90, delay: stagger(ledStep) }, ledsOn) - .add(leds, { "--lit": [0, 1], duration: 160 }, online); - if (core) - intro.add(core, { "--boot": [0, 1], duration: 900, ease: "outBack(1.6)" }, online - 200); - intro.call(() => { - if (status) scramble(status, "Engine online", "lowercase"); - }, online); + .add(leds, { "--lit": [0, 1], duration: 160 }, online) + .call(() => { + if (status) scramble(status, "Engine online", "lowercase"); + }, online); return () => { split.revert(); @@ -144,21 +83,35 @@ function heroIntro(root: HTMLElement, reduced: boolean): Cleanup { function clipEngine(root: HTMLElement, reduced: boolean): Cleanup { const spine = one(root, "[data-spine]"); - const rig = one(root, "[data-engine-rig]"); - const spinner = one(root, "[data-engine-spin]"); - if (!spine || !rig || !spinner) return noop; - - const core = one(root, "[data-engine-core]"); - const frames = all(root, "[data-frame]"); - const hooks = frames.filter((frame) => frame.dataset.slot === String(HOOK_SLOT)); - const scan = one(root, "[data-scan]"); - const blades = one(root, "[data-blades]"); + const host = one(root, "[data-engine-host]"); + if (!spine || !host) return noop; + const mode = one(root, "[data-engine-mode]"); const stageLabel = one(root, "[data-engine-stage]"); const clock = one(root, "[data-engine-tc]"); + const camLabel = one(root, "[data-engine-cam]"); const flag = one(root, "[data-engine-live]"); const fills = all(root, "[data-step-fill]"); + const view: EngineView = { progress: 0, assembly: reduced ? 1 : 0, px: 0, py: 0, pulse: 0 }; + let engine: EngineHandle | null = null; + let disposed = false; + const themeObserver = new MutationObserver(() => engine?.setTheme(currentTheme())); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"], + }); + + import("@/components/marketing/engine/clip-engine-scene") + .then(({ createClipEngine }) => { + if (disposed) return; + engine = createClipEngine(host, { theme: currentTheme(), reduced, view }); + if (!engine) host.dataset.fallback = "true"; + }) + .catch(() => { + host.dataset.fallback = "true"; + }); + const showFlag = (index: number) => { if (!flag) return; const live = ENGINE_STAGES[index].live; @@ -166,58 +119,42 @@ function clipEngine(root: HTMLElement, reduced: boolean): Cleanup { flag.dataset.live = String(live); }; + const cleanup = () => { + disposed = true; + themeObserver.disconnect(); + engine?.dispose(); + engine = null; + }; + if (reduced) { - const still = stageIndex("publish"); - applyStage(frames, rig, core, still); - hooks.forEach((hook) => hook.style.setProperty("--ok", "1")); - if (mode) mode.textContent = ENGINE_STAGES[still].mode; - if (stageLabel) stageLabel.textContent = `${pad(still)}/${pad(LAST_STAGE)}`; - showFlag(still); - return noop; + if (mode) mode.textContent = ENGINE_STAGES[0].mode; + showFlag(0); + return cleanup; } - const cube = core - ? animate(core, { "--cube": [0, 360], duration: 16000, ease: "linear", loop: true }) - : null; - - const spin = { deg: 0 }; - const paintSpin = () => spinner.style.setProperty("--spin", String(spin.deg)); - let spinning: JSAnimation | null = null; - let settling: JSAnimation | null = null; - const startSpin = () => { - if (spinning) return; - settling?.pause(); - settling = null; - const from = spin.deg; - spinning = animate(spin, { - deg: [from, from + 360], - duration: SPIN_MS, - ease: "linear", - loop: true, - onUpdate: paintSpin, - }); - }; - const stopSpin = () => { - if (!spinning) return; - spinning.pause(); - spinning = null; - settling = animate(spin, { - deg: Math.round(spin.deg / 360) * 360, - duration: 900, - ease: "out(3)", - onUpdate: paintSpin, - }); - }; + animate(view, { assembly: [0, 1], duration: 2600, delay: 250, ease: "out(2)" }); + + let offPointer: Cleanup = noop; + if (window.matchMedia("(pointer: fine)").matches) { + const tilt = createAnimatable(view, { px: 1200, py: 1200, ease: "out(3)" }); + const move = (event: PointerEvent) => { + tilt.px((event.clientX / window.innerWidth) * 2 - 1); + tilt.py((event.clientY / window.innerHeight) * 2 - 1); + }; + window.addEventListener("pointermove", move, { passive: true }); + offPointer = () => window.removeEventListener("pointermove", move); + } let current = -1; + let camText = ""; const setStage = (index: number) => { if (index === current) return; + const first = current === -1; current = index; if (mode) scramble(mode, ENGINE_STAGES[index].mode); if (stageLabel) stageLabel.textContent = `${pad(index)}/${pad(LAST_STAGE)}`; showFlag(index); - if (spinsAt(index)) startSpin(); - else stopSpin(); + if (!first) animate(view, { pulse: [1, 0], duration: 1100, ease: "out(3)" }); }; const at = (stage: number, offset = 0) => stage * STAGE_MS + offset; @@ -227,6 +164,11 @@ function clipEngine(root: HTMLElement, reduced: boolean): Cleanup { onUpdate: (timeline) => { setStage(Math.round(timeline.progress * LAST_STAGE)); if (clock) clock.textContent = timecode(timeline.progress); + const nextCam = cameraText(timeline.progress * LAST_STAGE); + if (camLabel && nextCam !== camText) { + camText = nextCam; + camLabel.textContent = nextCam; + } }, autoplay: onScroll({ target: spine, @@ -236,48 +178,7 @@ function clipEngine(root: HTMLElement, reduced: boolean): Cleanup { }), }); - for (let stage = 1; stage <= LAST_STAGE; stage++) { - const from = rigPose(stage - 1); - const to = rigPose(stage); - run.add(frames, frameTween(stage), at(stage - 1)); - run.add( - rig, - { - "--cam-rx": [from.rx, to.rx], - "--cam-ry": [from.ry, to.ry], - "--zoom": [from.zoom, to.zoom], - }, - at(stage - 1), - ); - if (core) run.add(core, { "--core": [from.core, to.core] }, at(stage - 1)); - } - - const detect = stageIndex("detect"); - if (scan) { - run - .add(scan, { "--scan-o": [0, 1], duration: 150 }, at(detect - 1)) - .add( - scan, - { "--scan": [SCAN_RANGE[0], SCAN_RANGE[1]], duration: 900, ease: "linear" }, - at(detect - 1), - ) - .add(scan, { "--scan-o": [1, 0], duration: 200 }, at(detect - 1, 800)); - } - - const cut = stageIndex("cut"); - if (blades) { - run - .add(blades, { "--blade-o": [0, 1], duration: 120 }, at(cut - 1)) - .add(blades, { "--blade-o": [1, 0], duration: 380 }, at(cut - 1, 380)); - } - - run - .add( - hooks, - { "--ok": [0, 1], duration: 320, delay: stagger(150) }, - at(stageIndex("approve") - 1, 350), - ) - .add(hooks, { "--ok": [1, 0], duration: 250 }, at(stageIndex("library") - 1)); + run.add(view, { progress: [0, LAST_STAGE], duration: LAST_STAGE * STAGE_MS, ease: "linear" }, 0); const panel = (name: string, enter: number, exit: number) => { const el = one(root, `[data-panel="${name}"]`); @@ -301,9 +202,8 @@ function clipEngine(root: HTMLElement, reduced: boolean): Cleanup { setStage(0); return () => { - spinning?.pause(); - settling?.pause(); - cube?.pause(); + offPointer(); + cleanup(); }; } diff --git a/src/components/marketing/clip-engine.tsx b/src/components/marketing/clip-engine.tsx index be690ec..81e3d46 100644 --- a/src/components/marketing/clip-engine.tsx +++ b/src/components/marketing/clip-engine.tsx @@ -1,36 +1,23 @@ -import type { CSSProperties } from "react"; import { Check } from "lucide-react"; -import { ClippyMark } from "@/components/brand/clippy-mark"; import { AGENT_LINES, - BLADE_X, - CLIP_COUNT, + ENGINE_PARTS, ENGINE_STAGES, LAST_STAGE, NETWORKS, PIPELINE_STEPS, - SCAN_RANGE, STORAGE_LAYERS, - engineFrames, hookNetwork, - rigPose, + stageState, } from "@/lib/clip-engine"; -const frames = engineFrames(); -const rig = rigPose(0); -const CLIP_HUES = [160, 32, 198, 268]; -const SUBJECT_X = ["36%", "50%", "64%"]; -const CUBE_FACES = ["front", "back", "right", "left", "top", "bottom"] as const; const LEDS = 5; +const start = stageState(0).cam; function pad(value: number) { return String(value).padStart(2, "0"); } -function vars(values: Record): CSSProperties { - return values as CSSProperties; -} - function PanelHead({ title, live }: { title: string; live: boolean }) { return (
@@ -45,85 +32,7 @@ export function ClipEngine() {