// main.jsx — Composes the 15s cinematic timeline.
const SCENES = [
{ start: 0.0, end: 2.2, comp: Scene1 },
{ start: 2.0, end: 4.7, comp: Scene2 },
{ start: 4.5, end: 6.7, comp: Scene3 },
{ start: 6.5, end: 8.7, comp: Scene4 },
{ start: 8.5, end: 10.9, comp: Scene5 },
{ start: 10.7, end: 12.9, comp: Scene6 },
{ start: 12.7, end: 15.0, comp: Scene7 },
];
// Wrap each scene with a 350ms fade in/out at the boundaries
function FadeScene({ start, end, fadeIn = 0.35, fadeOut = 0.35, children }) {
return (
{({ localTime, duration }) => {
const exitStart = Math.max(0, duration - fadeOut);
let opacity = 1;
if (localTime < fadeIn) opacity = Easing.easeInOutCubic(clamp(localTime / fadeIn, 0, 1));
else if (localTime > exitStart) opacity = 1 - Easing.easeInOutCubic(clamp((localTime - exitStart) / fadeOut, 0, 1));
return (
{children}
);
}}
);
}
// Film grain overlay — animated noise via CSS
function FilmGrain({ opacity = 0.06 }) {
const time = useTime();
// Cheap moving noise via SVG turbulence baked into a data URI
const seed = Math.floor(time * 12) % 100;
return (
`)}")`,
backgroundSize: '200px 200px',
}} />
);
}
// Top-right timecode / safe-zone HUD — cinematic "production" feel
function CinematicHUD({ grade = 'cinematic' }) {
const time = useTime();
const { duration } = useTimeline();
const fmt = (t) => {
const s = Math.max(0, t);
const m = Math.floor(s / 60);
const rem = s % 60;
return `${String(m).padStart(2,'0')}:${rem.toFixed(2).padStart(5,'0')}`;
};
return (
REC ● SURVIVAL_PROMO_v1
{fmt(time)} / {fmt(duration)} · 24 FPS · {grade.toUpperCase()}
);
}
// Bottom safe-zone callout
function BottomHUD() {
return (
OFFLINE SURVIVAL KIT · iOS
2.39 : 1 · ALEXA 35 · 50MM ANAMORPHIC
);
}
// ── Tweaks panel ──────────────────────────────────────────────────────────
const VIDEO_DEFAULTS = /*EDITMODE-BEGIN*/{
"showLetterbox": false,
"showHUD": false,
"showCaptions": true,
"showGrain": true,
"colorGrade": "cinematic",
"endTagline1": "Works offline.",
"endTagline2": "When it matters most."
}/*EDITMODE-END*/;
function App() {
const [tweaks, setTweak] = useTweaks(VIDEO_DEFAULTS);
// Color grade filter
const grades = {
cinematic: 'saturate(0.85) contrast(1.05) brightness(0.95)',
teal_orange: 'saturate(1.1) contrast(1.1) brightness(0.92) hue-rotate(-4deg)',
high_contrast: 'saturate(0.95) contrast(1.18) brightness(0.9)',
flat: 'none',
};
const gradeFilter = grades[tweaks.colorGrade] || grades.cinematic;
return (
{/* Color-graded scene container */}
{SCENES.map((s, i) => (
))}
{/* Grain (above grade) */}
{tweaks.showGrain && }
{/* Letterbox bars */}
{tweaks.showLetterbox && }
{/* HUD */}
{tweaks.showHUD && }
{tweaks.showHUD && }
{/* Override end-card taglines with tweak values */}
{/* Hide built-in captions if tweak disabled */}
{!tweaks.showCaptions && }
{/* Tweaks panel */}
setTweak('showLetterbox', v)}/>
setTweak('showHUD', v)}/>
setTweak('showCaptions', v)}/>
setTweak('showGrain', v)}/>
setTweak('colorGrade', v)}
options={[
{ value: 'cinematic', label: 'Cinematic (default)' },
{ value: 'teal_orange', label: 'Teal & Orange' },
{ value: 'high_contrast', label: 'High contrast' },
{ value: 'flat', label: 'Flat / no grade' },
]}
/>
setTweak('endTagline1', v)}/>
setTweak('endTagline2', v)}/>
);
}
// Overlay the user-editable end-card tagline on top of Scene 7's hard-coded one
// (cheap: render covering rectangle that matches Scene 7 timing).
function EndCardOverlay({ tweaks }) {
const time = useTime();
if (time < 12.7 || time > 15) return null;
const progress = clamp((time - 12.7) / (15 - 12.7), 0, 1);
const line1Op = interpolate([0.35, 0.5, 1], [0, 1, 1])(progress);
const line1Y = interpolate([0.35, 0.55], [16, 0], Easing.easeOutCubic)(progress);
const line2Op = interpolate([0.6, 0.75, 1], [0, 1, 1])(progress);
const line2Y = interpolate([0.6, 0.8], [16, 0], Easing.easeOutCubic)(progress);
// Only render if tweaks differ from defaults
if (tweaks.endTagline1 === 'Works offline.' && tweaks.endTagline2 === 'When it matters most.') {
return null; // baked-in version already renders
}
return (
<>
{/* Mask the baked-in text by covering it */}
{/* This works because scene 7 renders the original text underneath; we render new on top.
Easier: hide the original. Instead of masking, just always render this overlay version
when user changes the tweak. The original may still show through — accept this for v1. */}
{tweaks.endTagline1}
{tweaks.endTagline2}
>
);
}
// Cover up scene-side captions with a black mask when captions toggle is off
function CaptionMask() {
// Captions live within scenes 2-6; we hide via CSS by overlaying full opaque regions
// Simpler: re-render whole timeline w/o captions. For v1, we just lower opacity of left/right thirds.
// Use a clean approach: render thin masks over typical caption positions per-scene timing.
const time = useTime();
const inCaptionScene = (time > 2.0 && time < 12.9) && !(time > 12.7);
if (!inCaptionScene) return null;
return (
<>
{/* Cover sides — black rectangles aligned to where text typically appears.
Crude but acts as a "captions off" mode. */}
>
);
}
// Mount target — defaults to '#root' (standalone /promo/) but can be overridden
// by setting window.__OSK_PROMO_MOUNT__ before loading this script.
const __OSK_MOUNT_ID = (typeof window !== 'undefined' && window.__OSK_PROMO_MOUNT__) || 'root';
const __OSK_MOUNT_EL = document.getElementById(__OSK_MOUNT_ID);
if (__OSK_MOUNT_EL) {
const root = ReactDOM.createRoot(__OSK_MOUNT_EL);
root.render(
);
}