/* 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 (
);
}
// 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 (