/* TowerSystem — promo video scenes Exports scene components + shared visual primitives to window. */ const TS = { bg: '#0E0F12', bg2: '#16181C', panel: '#1B1E23', red: '#DE2F2F', redDim: '#A81C1C', white: '#F5F6F7', gray: '#9AA0A6', grayDim: '#5C6166', line: 'rgba(255,255,255,0.10)', lineFaint:'rgba(255,255,255,0.05)', }; const DISPLAY = "'Archivo', system-ui, sans-serif"; const BODY = "'Archivo', system-ui, sans-serif"; // ── Shared easing-driven helpers ──────────────────────────────────────────── function rampIn(localTime, dur = 0.5, ease = Easing.easeOutCubic) { return ease(clamp(localTime / dur, 0, 1)); } // ── Small uppercase technical label with red tick ─────────────────────────── function Kicker({ text, x, y, delay = 0, color = TS.red }) { const t = useTime(); const lt = t - delay; const op = clamp(lt / 0.4, 0, 1); const w = clamp(lt / 0.5, 0, 1); return (
{text}
); } // ── Animated scaffold grid (the brand motif) ──────────────────────────────── // Draws vertical standards + horizontal ledgers + one red diagonal brace, // each member fading/extending in with a stagger driven by `build` (0..1). function ScaffoldFrame({ x, y, cols, rows, cell, build, faint = false, redBrace = true }) { const W = cols * cell, H = rows * cell; const members = []; let idx = 0; const total = (cols + 1) + (rows + 1) + (redBrace ? 1 : 0); const stagger = 0.6 / Math.max(1, total); const memberP = (i) => clamp((build - i * stagger) / 0.4, 0, 1); const stroke = faint ? TS.lineFaint : TS.line; // verticals (standards) for (let c = 0; c <= cols; c++) { const p = Easing.easeOutCubic(memberP(idx++)); members.push(); } // horizontals (ledgers) for (let r = 0; r <= rows; r++) { const p = Easing.easeOutCubic(memberP(idx++)); members.push(); } // node dots const dotP = clamp((build - 0.55) / 0.4, 0, 1); const dots = []; if (!faint) for (let c = 0; c <= cols; c++) for (let r = 0; r <= rows; r++) { dots.push(); } // red diagonal brace across one bay let brace = null; if (redBrace) { const p = Easing.easeOutCubic(memberP(idx++)); brace = ; } return ( {members}{dots}{brace} ); } // Persistent faint background lattice that slowly drifts the whole video function BackdropLattice() { const t = useTime(); const drift = Math.sin(t * 0.18) * 14; const build = clamp(t / 1.2, 0, 1); return (
{/* vignette */}
{/* floor line */}
); } // ── Photo panel: real image, Ken Burns drift, technical frame + corner ticks ─ // `images` is an array of src strings; if more than one they crossfade across // the [start,end] window. Stays composited; slow zoom keeps the frame alive. function PhotoPanel({ images, start, end, x, y, w, h, label, focus = '50% 50%', zoom = 0.12 }) { const t = useTime(); const list = Array.isArray(images) ? images : [images]; // container entry / exit let cOp = 0, ty = 26; if (t >= start) { const inP = Easing.easeOutCubic(clamp((t - start) / 0.6, 0, 1)); cOp = inP; ty = (1 - inP) * 26; if (t > end - 0.5) { const outP = Easing.easeInCubic(clamp((t - (end - 0.5)) / 0.5, 0, 1)); cOp = 1 - outP; ty = -outP * 14; } } if (cOp <= 0 && t < start) return null; const total = end - start; const seg = total / list.length; const xf = 0.55; return (
{list.map((src, i) => { const segStart = start + i * seg, segEnd = segStart + seg; let op = 1; if (list.length > 1) { if (i > 0 && t < segStart + xf) op = clamp((t - segStart) / xf, 0, 1); if (i < list.length - 1 && t > segEnd - xf) op = 1 - clamp((t - (segEnd - xf)) / xf, 0, 1); if (t < segStart - xf || t > segEnd + xf) op = (i === 0 && t < segStart) ? 1 : (i === list.length-1 && t>segEnd?1:0); } const kp = clamp((t - segStart) / seg, 0, 1); const ease = Easing.easeInOutSine ? Easing.easeInOutSine(kp) : kp; // Ken Burns: alternate zoom-in / zoom-out per image, with a gentle pan. // Baseline scale stays ≥1.06 so thin lines are always downsampled (no shimmer). const dir = i % 2 === 0 ? 1 : -1; const baseScale = 1.06; const scale = baseScale + zoom * (dir > 0 ? ease : (1 - ease)); const panX = (dir > 0 ? 1 : -1) * (ease - 0.5) * 2.2; // % drift const panY = (dir > 0 ? -1 : 1) * (ease - 0.5) * 1.6; return ( ); })} {/* subtle dark grade for text legibility near edges */}
{[['0','0'],['100%','0'],['0','100%'],['100%','100%']].map((p,i)=>(
))} {label &&
{label}
}
); } const IMG = 'assets/img/'; // ── List item that slides in with an index number ─────────────────────────── function ServiceRow({ n, text, x, y, start, accent = false }) { const t = useTime(); const lt = t - start; const p = Easing.easeOutCubic(clamp(lt / 0.5, 0, 1)); const op = clamp(lt / 0.4, 0, 1); return (
{n} {text}
); } // ════════════════════════════════════════════════════════════════════════════ // SCENES // ════════════════════════════════════════════════════════════════════════════ // 1 — INTRO (0–4.5) logo build function IntroScene() { const t = useTime(); const build = clamp((t - 0.2) / 1.4, 0, 1); const logoP = Easing.easeOutCubic(clamp((t - 1.3) / 0.7, 0, 1)); const logoOut = t > 4.0 ? clamp((t - 4.0) / 0.5, 0, 1) : 0; const taglineP = clamp((t - 2.0) / 0.6, 0, 1); return ( {/* central scaffold tower assembling behind the logo */}
TowerSystem
Échafaudages  Fixes  &  Mobiles
); } // 2 — SPÉCIALISTE (4.5–9) function SpecialisteScene() { return ( ); } // 3 — ÉCHAFAUDAGE FIXE (9–14) function FixeScene() { return ( ); } // 4 — ÉCHAFAUDAGE MOBILE (14–19) function MobileScene() { return ( {/* Instant UpRight callout */} {({ localTime }) => { const p = Easing.easeOutBack(clamp(localTime / 0.6, 0, 1)); return (
Représentant agréé Instant UpRight
); }}
); } // 5 — ATOUTS (19–24) function AtoutsScene() { const pillars = [ ['Livraison rapide', 'Pose par des professionnels'], ['Montage & démontage', 'Pris en charge de A à Z'], ['Normes SIA & SUVA', 'Sécurité conforme, sur-mesure'], ]; return ( {pillars.map((p, i) => ( {({ localTime }) => { const pr = Easing.easeOutCubic(clamp(localTime / 0.55, 0, 1)); return (
{p[0]}
{p[1]}
); }}
))}
); } // 6 — PROMO (24–27.5) function PromoScene() { return ( {/* black wash sweeping in */} {({ localTime }) => { const p = Easing.easeOutQuart(clamp(localTime / 0.5, 0, 1)); return
; }}
{({ localTime }) => { const p = Easing.easeOutBack(clamp(localTime / 0.7, 0, 1)); return (
−20%
); }}
{/* Snappy 300 product photo, right */} {({ localTime }) => { const p = Easing.easeOutCubic(clamp(localTime / 0.6, 0, 1)); const out = localTime > (3.5 - 0.5) ? clamp((localTime - 3.0) / 0.5, 0, 1) : 0; const span = 27.5 - 24.7; const kp = clamp(localTime / span, 0, 1); return (
Pont roulant Snappy 300
); }}
); } // 7 — OUTRO (27.5–31) function OutroScene() { const t = useTime(); const build = clamp((t - 27.6) / 1.2, 0, 1); const logoP = Easing.easeOutCubic(clamp((t - 27.9) / 0.6, 0, 1)); return (
TowerSystem
{({ localTime }) => { const p = clamp(localTime/0.5,0,1); return (
); }}
); } function ContactBlock({ label, value }) { return (
{label}
{value}
); } Object.assign(window, { TS, DISPLAY, BODY, Kicker, ScaffoldFrame, BackdropLattice, PhotoPanel, ServiceRow, ContactBlock, IntroScene, SpecialisteScene, FixeScene, MobileScene, AtoutsScene, PromoScene, OutroScene, });