Spaces:
Running
Running
| // @ts-nocheck | |
| // ============================================================================ | |
| // MOTION GRAPHICS · OBSIDIAN ORBIT — Scene 08 / CREATE EPISODE (13s, 390f) | |
| // | |
| // "Keynote object-worship". ONE hero artifact: a DASHBOARD slab. A thick glass | |
| // slab whose face is a project dashboard — a glowing "+ Create Episode" button | |
| // gets clicked (a ripple fires), a name field TYPES a title, then a back-end | |
| // directory SKELETON self-assembles row by row on the face. Instant scaffolding, | |
| // keynote calm. The WORLD orbits (never the camera) in slow hold->ease arcs; | |
| // spec Callouts billboard on drawn leader lines at waypoints ("+ CREATE EPISODE", | |
| // "AUTO-SCAFFOLD"); the angle-tracked sheen rakes the turn; a gentle settle on | |
| // the payoff as the tree finishes locking in. | |
| // | |
| // Built ENTIRELY on the Plinth module (mirrors ObsidianOrbit.tsx's contract) + | |
| // the shared Chronixel engine. CSS-3D only, deterministic, no WebGL. | |
| // ============================================================================ | |
| import React from "react"; | |
| import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion"; | |
| import { | |
| Plinth, | |
| OrbitWorld, | |
| Extrude, | |
| Callout, | |
| DustMotes, | |
| } from "../../plinth/plinth"; | |
| import { C, withAlpha, ACCENT_GRAD } from "../../chronixel/theme"; | |
| import { FONT, MONO } from "../../chronixel/fonts"; | |
| import { | |
| beat, | |
| clamp01, | |
| mix, | |
| osc, | |
| enter, | |
| springAt, | |
| EASE, | |
| SPRING, | |
| } from "../../chronixel/motion"; | |
| import { Shockwave, GlowPulse, LightSweep } from "../../chronixel/fx"; | |
| import { TypeOn } from "../../chronixel/viz/text"; | |
| /* ---------------------------------------------------------------------------- | |
| A small mono eyebrow tag pinned top-left over the void — frames the beat. | |
| -------------------------------------------------------------------------- */ | |
| const Eyebrow: React.FC<{ at: number; children: React.ReactNode }> = ({ | |
| at, | |
| children, | |
| }) => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return ( | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: 120, | |
| top: 120, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 14, | |
| fontFamily: MONO, | |
| fontSize: 15, | |
| letterSpacing: 5, | |
| textTransform: "uppercase", | |
| color: C.accent2, | |
| ...enter(frame, fps, at, { from: "left", dist: 32, blur: 0 }), | |
| }} | |
| > | |
| <span | |
| style={{ | |
| width: 30, | |
| height: 2, | |
| background: ACCENT_GRAD, | |
| boxShadow: `0 0 12px ${C.accentGlow}`, | |
| }} | |
| /> | |
| {children} | |
| </div> | |
| ); | |
| }; | |
| /* ============================================================================ | |
| CREATE EPISODE. Hero = the DASHBOARD artifact. A glass slab whose face is a | |
| project dashboard: a glowing "+ Create Episode" button is clicked (ripple), | |
| a name field types a title, then a directory skeleton self-assembles. The | |
| world orbits in, callouts fire, and the scaffold locks in on the payoff. | |
| ========================================================================== */ | |
| export const MgOrbitScene08CreateEpisode: React.FC = () => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| const t = frame / fps; | |
| // --- beat timeline (seconds) ------------------------------------------- | |
| const CLICK = 2.4; // the "+ Create Episode" button is pressed (ripple) | |
| const TYPE = 3.0; // the name field begins typing the title | |
| const SCAFFOLD = 5.6; // the directory skeleton starts self-assembling | |
| const LOCK = 9.0; // the scaffold finishes — a calm settle payoff | |
| // --- face choreography ------------------------------------------------- | |
| const dashIn = beat(frame, fps, 0.8, 2.0, EASE.out); // dashboard fades up | |
| const btnGlow = 0.5 + 0.5 * osc(t, 1.4); // the create button breathes | |
| const pressDip = 1 - 0.06 * clamp01(1 - Math.abs(t - CLICK) / 0.18); // squash | |
| const ripple = clamp01((t - CLICK) / 0.7); // click ripple expands | |
| const rippleLife = clamp01(1 - (t - CLICK) / 0.7); | |
| // the name field appears right after the click, then the title types in. | |
| const fieldP = beat(frame, fps, CLICK + 0.15, CLICK + 0.6, EASE.backOut); | |
| // the directory tree rows assemble one at a time after SCAFFOLD. | |
| const tree = [ | |
| { label: "episode-08/", depth: 0 }, | |
| { label: "scenes/", depth: 1 }, | |
| { label: "scene-01.tsx", depth: 2 }, | |
| { label: "scene-02.tsx", depth: 2 }, | |
| { label: "assets/", depth: 1 }, | |
| { label: "render.config.ts", depth: 1 }, | |
| ]; | |
| // --- the lock-in: a gentle, weighty settle on the payoff ---------------- | |
| const stamp = springAt(frame, fps, LOCK, SPRING.HEAVY); // 0->1 settle | |
| const stampScale = mix(1.04, 1, clamp01(stamp)); // a soft, calm seat | |
| const flash = clamp01(1 - (t - LOCK) / 0.5) * (t >= LOCK ? 1 : 0); | |
| const riser = beat(frame, fps, LOCK - 1.4, LOCK, EASE.out); // build into apex | |
| const lockRing = clamp01(stamp); | |
| const ringPulse = 0.5 + 0.5 * osc(t, 3); | |
| // hold->ease orbit: shows the button face on the way in, turns to reveal the | |
| // scaffolding, then a final short arc that settles to centre (0°) at the lock. | |
| const legs = [ | |
| { arc: -18, hold: 0.7, travel: 2.4 }, // ease left to read the button | |
| { arc: 26, hold: 1.6, travel: 2.8 }, // swing across as it types/scaffolds | |
| { arc: -8, hold: 0.6, travel: 1.8 }, // lands ~0° near LOCK — settle | |
| ]; | |
| const HeroFace = ( | |
| <div | |
| style={{ | |
| position: "absolute", | |
| inset: 0, | |
| padding: 30, | |
| display: "flex", | |
| flexDirection: "column", | |
| gap: 16, | |
| transform: `scale(${stampScale})`, | |
| opacity: dashIn, | |
| }} | |
| > | |
| {/* dashboard chrome — title bar */} | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 10, | |
| paddingBottom: 14, | |
| borderBottom: `1px solid ${C.glassBorder}`, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| width: 10, | |
| height: 10, | |
| borderRadius: "50%", | |
| background: withAlpha(C.accent, 0.85), | |
| boxShadow: `0 0 10px ${C.accentGlow}`, | |
| }} | |
| /> | |
| <div | |
| style={{ | |
| fontFamily: MONO, | |
| fontSize: 12, | |
| letterSpacing: 3, | |
| textTransform: "uppercase", | |
| color: C.textDim, | |
| }} | |
| > | |
| chronixel · dashboard | |
| </div> | |
| <div style={{ flex: 1 }} /> | |
| <div | |
| style={{ | |
| fontFamily: MONO, | |
| fontSize: 11, | |
| letterSpacing: 2, | |
| color: withAlpha(C.accent2, 0.7), | |
| }} | |
| > | |
| new project | |
| </div> | |
| </div> | |
| {/* main body: left = create + name field, right = scaffold tree */} | |
| <div | |
| style={{ | |
| flex: 1, | |
| display: "grid", | |
| gridTemplateColumns: "1fr 1fr", | |
| gap: 22, | |
| alignItems: "stretch", | |
| }} | |
| > | |
| {/* LEFT — the glowing "+ Create Episode" button + the name field */} | |
| <div | |
| style={{ | |
| display: "flex", | |
| flexDirection: "column", | |
| justifyContent: "center", | |
| gap: 18, | |
| }} | |
| > | |
| {/* the create button (with click ripple) */} | |
| <div | |
| style={{ | |
| position: "relative", | |
| alignSelf: "flex-start", | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 12, | |
| padding: "16px 26px", | |
| borderRadius: 16, | |
| background: ACCENT_GRAD, | |
| transform: `scale(${pressDip})`, | |
| boxShadow: `0 8px 30px ${withAlpha( | |
| C.accent, | |
| 0.4 + 0.25 * btnGlow, | |
| )}, inset 0 1px 0 ${withAlpha("#ffffff", 0.4)}`, | |
| overflow: "hidden", | |
| }} | |
| > | |
| <span | |
| style={{ | |
| fontFamily: FONT, | |
| fontWeight: 800, | |
| fontSize: 30, | |
| color: "#1a0d03", | |
| lineHeight: 1, | |
| }} | |
| > | |
| + | |
| </span> | |
| <span | |
| style={{ | |
| fontFamily: FONT, | |
| fontWeight: 800, | |
| fontSize: 24, | |
| color: "#1a0d03", | |
| letterSpacing: "-0.01em", | |
| lineHeight: 1, | |
| }} | |
| > | |
| Create Episode | |
| </span> | |
| {/* click ripple emanating from the press point */} | |
| {t >= CLICK && ( | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: "50%", | |
| top: "50%", | |
| width: 40, | |
| height: 40, | |
| marginLeft: -20, | |
| marginTop: -20, | |
| borderRadius: "50%", | |
| border: `2px solid ${withAlpha("#ffffff", 0.7)}`, | |
| transform: `scale(${mix(0.4, 6, ripple)})`, | |
| opacity: rippleLife * 0.8, | |
| pointerEvents: "none", | |
| }} | |
| /> | |
| )} | |
| </div> | |
| {/* the name field that types the episode title */} | |
| <div | |
| style={{ | |
| opacity: fieldP, | |
| transform: `translateY(${(1 - fieldP) * 12}px)`, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontFamily: MONO, | |
| fontSize: 10, | |
| letterSpacing: 3, | |
| textTransform: "uppercase", | |
| color: C.textDim, | |
| marginBottom: 8, | |
| }} | |
| > | |
| episode name | |
| </div> | |
| <div | |
| style={{ | |
| padding: "12px 16px", | |
| borderRadius: 12, | |
| background: withAlpha("#0a0e16", 0.75), | |
| border: `1px solid ${withAlpha(C.accent, 0.45)}`, | |
| boxShadow: `inset 0 0 16px ${withAlpha("#000", 0.5)}`, | |
| minHeight: 26, | |
| display: "flex", | |
| alignItems: "center", | |
| }} | |
| > | |
| <TypeOn | |
| text="Motion Graphics — Pilot" | |
| at={TYPE} | |
| cps={16} | |
| size={20} | |
| mono={false} | |
| color={C.text} | |
| caret | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* RIGHT — the back-end directory skeleton self-assembling */} | |
| <div | |
| style={{ | |
| position: "relative", | |
| borderRadius: 14, | |
| padding: "18px 18px", | |
| background: withAlpha("#0a0e16", 0.6), | |
| border: `1px solid ${C.glassBorder}`, | |
| display: "flex", | |
| flexDirection: "column", | |
| gap: 11, | |
| justifyContent: "center", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontFamily: MONO, | |
| fontSize: 10, | |
| letterSpacing: 3, | |
| textTransform: "uppercase", | |
| color: withAlpha(C.accent2, 0.7), | |
| marginBottom: 4, | |
| }} | |
| > | |
| scaffolding | |
| </div> | |
| {tree.map((row, i) => { | |
| const rp = beat( | |
| frame, | |
| fps, | |
| SCAFFOLD + i * 0.28, | |
| SCAFFOLD + i * 0.28 + 0.5, | |
| EASE.backOut, | |
| ); | |
| const isDir = row.label.endsWith("/"); | |
| return ( | |
| <div | |
| key={i} | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 10, | |
| marginLeft: row.depth * 22, | |
| opacity: rp, | |
| transform: `translateX(${(1 - rp) * -14}px)`, | |
| }} | |
| > | |
| {/* node glyph: folder vs file */} | |
| <div | |
| style={{ | |
| width: 14, | |
| height: 11, | |
| borderRadius: 3, | |
| flexShrink: 0, | |
| background: isDir | |
| ? withAlpha(C.accent, 0.55) | |
| : "transparent", | |
| border: isDir | |
| ? "none" | |
| : `1.5px solid ${withAlpha(C.accent2, 0.7)}`, | |
| boxShadow: isDir | |
| ? `0 0 8px ${withAlpha(C.accent, 0.5)}` | |
| : "none", | |
| }} | |
| /> | |
| <div | |
| style={{ | |
| fontFamily: MONO, | |
| fontSize: 15, | |
| color: isDir ? C.text : withAlpha(C.text, 0.7), | |
| letterSpacing: 0.5, | |
| }} | |
| > | |
| {row.label} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* lock ring that snaps tight as the scaffold finishes (calm settle) */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| inset: 16, | |
| borderRadius: 24, | |
| border: `2px solid ${withAlpha(C.accent, 0.25 + 0.4 * ringPulse)}`, | |
| opacity: lockRing, | |
| boxShadow: `0 0 28px ${withAlpha(C.accent, 0.28 * ringPulse)}`, | |
| pointerEvents: "none", | |
| }} | |
| /> | |
| </div> | |
| ); | |
| return ( | |
| <Plinth> | |
| <Eyebrow at={0.3}>One Click To Scaffold</Eyebrow> | |
| {/* riser glow swelling behind the hero into the settle */} | |
| <AbsoluteFill style={{ pointerEvents: "none", zIndex: 1 }}> | |
| <GlowPulse | |
| size={1000} | |
| color={withAlpha(C.accent, 0.09 * riser)} | |
| strength={riser} | |
| /> | |
| </AbsoluteFill> | |
| {/* a soft flash + shockwave the instant the button is clicked */} | |
| <AbsoluteFill style={{ pointerEvents: "none", zIndex: 6 }}> | |
| <GlowPulse | |
| size={900} | |
| color={withAlpha(C.accent2, 0.14 * rippleLife)} | |
| strength={rippleLife} | |
| /> | |
| <Shockwave at={CLICK} size={620} /> | |
| </AbsoluteFill> | |
| {/* a gentle settle flash as the scaffold locks in on the payoff */} | |
| <AbsoluteFill style={{ pointerEvents: "none", zIndex: 6 }}> | |
| <GlowPulse | |
| size={1200} | |
| color={withAlpha(C.accent, 0.14 * flash)} | |
| strength={flash} | |
| /> | |
| <Shockwave at={LOCK} size={780} /> | |
| <LightSweep at={LOCK} dur={1.1} strength={0.13} /> | |
| </AbsoluteFill> | |
| <OrbitWorld legs={legs} perspective={2300} bobAmp={12}> | |
| <DustMotes layers={3} opacity={0.42} /> | |
| <Extrude w={820} h={460} depth={22} layers={16} radius={28}> | |
| {HeroFace} | |
| </Extrude> | |
| {/* billboarded spec callouts firing at orbit waypoints */} | |
| <Callout | |
| title="The Trigger" | |
| value="+ CREATE EPISODE" | |
| at={fps * CLICK} | |
| x={0} | |
| y={-310} | |
| leader={120} | |
| side="right" | |
| /> | |
| <Callout | |
| title="Instant" | |
| value="AUTO-SCAFFOLD" | |
| at={fps * (SCAFFOLD + 0.4)} | |
| x={490} | |
| y={140} | |
| leader={150} | |
| side="right" | |
| /> | |
| <Callout | |
| title="Zero Setup" | |
| value="READY TO BUILD" | |
| at={fps * (LOCK + 0.3)} | |
| x={-490} | |
| y={150} | |
| leader={160} | |
| side="left" | |
| /> | |
| </OrbitWorld> | |
| {/* flat masked mirror sibling — re-renders the hero, simplified & matte */} | |
| <Plinth.Reflection> | |
| <Extrude w={820} h={460} depth={22} layers={10} radius={28} sheen={false}> | |
| {HeroFace} | |
| </Extrude> | |
| </Plinth.Reflection> | |
| </Plinth> | |
| ); | |
| }; | |
| export default MgOrbitScene08CreateEpisode; |