Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,25 @@ 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 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.
v4 and [three.js](https://threejs.org). A pinned WebGL stage
(`src/components/marketing/engine/clip-engine-scene.ts`) renders the ClippyOS
engine as an exploded editing timeline drawn as a glowing holographic wireframe:
a projector deck with a spinning jog wheel and a media bin, audio (A1), footage
(V1) and caption (C1) tracks floating under a timecode ruler, a playhead light
sheet that sweeps the stack, a source port that streams footage in, hook markers
on the waveform, a razor that cuts V1 into twelve clips, a review gate, four
output lanes that end in X, YouTube, Instagram and TikTok screens, and the Hermes
loop around the machine. The clips move between formations (segments on the
footage track, cut apart, vertical 9:16 cards, the review conveyor, the output
lanes, the media bin and the Hermes loop) while the camera dollies along the
timeline.

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.
anime.js also runs the engine boot (the wireframe draws itself in), stage
pulses, pointer parallax, 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`.
Expand Down
10 changes: 2 additions & 8 deletions src/components/marketing/anime/landing-motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
utils,
type AnimatableObject,
} from "animejs";
import { ENGINE_STAGES, LAST_STAGE, engineStateAt, stageIndex, timecode } from "@/lib/clip-engine";
import { ENGINE_STAGES, LAST_STAGE, readout, stageIndex, timecode } from "@/lib/clip-engine";
import type {
EngineHandle,
EngineTheme,
Expand Down Expand Up @@ -41,12 +41,6 @@ function currentTheme(): EngineTheme {
return document.documentElement.dataset.theme === "light" ? "light" : "dark";
}

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]");
Expand Down Expand Up @@ -164,7 +158,7 @@ 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);
const nextCam = readout(timeline.progress * LAST_STAGE);
if (camLabel && nextCam !== camText) {
camText = nextCam;
camLabel.textContent = nextCam;
Expand Down
6 changes: 2 additions & 4 deletions src/components/marketing/clip-engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import {
NETWORKS,
PIPELINE_STEPS,
STORAGE_LAYERS,
stageState,
readout,
} from "@/lib/clip-engine";

const LEDS = 5;
const CLIPS_PER_NETWORK = CLIP_COUNT / NETWORKS.length;
const start = stageState(0).cam;

function pad(value: number) {
return String(value).padStart(2, "0");
Expand Down Expand Up @@ -54,8 +53,7 @@ export function ClipEngine() {
<span data-engine-tc>00:00:00:00</span>
</span>
<span className="ce-readout__meta" data-engine-cam>
orbit {String(Math.round(start.rot)).padStart(3, "0")}° · pitch {start.pitch}° · dist{" "}
{start.dist.toFixed(1)}
{readout(0)}
</span>
</div>
<div className="ce-readout__side">
Expand Down
Loading
Loading