research-landing / scroll-world.js
aayu-outdooragi's picture
Upload folder using huggingface_hub
7ecf849 verified
Raw
History Blame Contribute Delete
8.95 kB
/*
* Complide Enterprise β€” scroll-world engine.
*
* True scroll-world technique: one continuous generated camera-flight video
* (5 Veo dive-in clips joined by first/last-frame connector clips, so every
* seam is frame-identical β€” no cuts). Scroll position only drives time: the
* video is encoded scrub-friendly and its currentTime is scrubbed by a
* lerped scroll progress.
*
* If the flight video is missing or fails, the engine falls back to the
* still-image mode: exponential zoom-through + crossfade connectors built
* from the same diorama stills.
*/
(function () {
"use strict";
var scenes = Array.prototype.slice.call(document.querySelectorAll(".scene"));
var cards = Array.prototype.slice.call(document.querySelectorAll(".card"));
var dots = Array.prototype.slice.call(document.querySelectorAll(".rail__dots li"));
var rail = document.querySelector(".rail");
var railFill = document.getElementById("railFill");
var spacer = document.getElementById("worldSpacer");
var world = document.getElementById("world");
var video = document.getElementById("flightVideo");
var N = scenes.length;
var SCENE_VH = 2.2; // scroll length per scene, in viewport heights
var TAIL_VH = 0.4; // extra runway after the last scene
// Segment layout of flight.mp4 (written by tools/assemble_flight.sh).
// Dives carry the copy cards; connectors are the in-between flights.
var TIMELINE = window.FLIGHT_TIMELINE || null;
var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var LERP = reducedMotion ? 1 : 0.1;
var scrollLen = 1;
var target = 0;
var current = 0;
var videoMode = false;
var duration = 0;
scenes.forEach(function (s, i) { s.style.zIndex = String(N - i); });
/* ── shared helpers ─────────────────────────────────────────────────── */
var clamp01 = function (v) { return Math.min(1, Math.max(0, v)); };
var easeInCubic = function (t) { return t * t * t; };
var easeOutCubic = function (t) { return 1 - Math.pow(1 - t, 3); };
function layout() {
scrollLen = Math.max(1, (N * SCENE_VH + TAIL_VH) * window.innerHeight);
spacer.style.height = scrollLen + "px";
readScroll();
current = target;
render(current);
}
function readScroll() {
var max = Math.max(1, spacer.offsetTop + scrollLen - window.innerHeight);
target = clamp01(window.scrollY / max);
}
/* ── video mode ─────────────────────────────────────────────────────── */
// Maps global progress β†’ [segment index, local t within segment].
// Scroll is distributed over segments proportionally to their duration.
function segmentAt(p) {
var t = p * duration;
for (var i = 0; i < TIMELINE.length; i++) {
if (t <= TIMELINE[i].end || i === TIMELINE.length - 1) {
var seg = TIMELINE[i];
return { seg: seg, i: i, local: clamp01((t - seg.start) / (seg.end - seg.start)) };
}
}
}
function renderVideo(p) {
var t = p * duration;
// Never seek to the exact end (some browsers show a black frame there).
if (video.readyState >= 1) video.currentTime = Math.min(t, duration - 0.05);
var pos = segmentAt(p);
var isDive = pos.seg.name.indexOf("dive_") === 0;
var sceneIdx = isDive ? Math.floor(pos.i / 2) : Math.floor((pos.i + 1) / 2);
// Cards live inside dive segments: in after 10%, out by 80%.
for (var i = 0; i < N; i++) {
var card = cards[i];
var o = 0, y = 30;
if (isDive && Math.floor(pos.i / 2) === i) {
var lt = pos.local;
var fadeIn = i === 0 && p < 0.002 ? 1 : clamp01(lt / 0.12);
var fadeOut = 1 - clamp01((lt - 0.72) / 0.16);
o = Math.min(fadeIn, fadeOut);
y = 30 * (1 - fadeIn) - 60 * (1 - fadeOut);
}
card.style.opacity = o.toFixed(4);
card.style.transform = "translate(0, calc(-50% + " + y.toFixed(1) + "px))";
card.classList.toggle("is-live", o > 0.5);
}
updateChrome(p, sceneIdx);
}
/* ── still-image fallback mode ──────────────────────────────────────── */
var ZOOM_THROUGH = 3.1;
var UNDER = 0.68;
var CF = 0.22;
function renderStills(p) {
var seg = 1 / N;
for (var i = 0; i < N; i++) {
var scene = scenes[i];
var img = scene.firstElementChild;
var isLast = i === N - 1;
var t = (p - i * seg) / seg;
if (t <= -CF || t >= 1 + (isLast ? 1 : 0.0001)) {
if (scene.style.opacity !== "0") scene.style.opacity = "0";
continue;
}
var scale, opacity, blur = 0;
if (t < 0) {
var k = easeOutCubic(clamp01((t + CF) / CF));
scale = UNDER + (1 - UNDER) * k;
opacity = k;
} else {
var tt = clamp01(isLast ? Math.min(t, 0.72) : t);
scale = Math.exp(Math.log(ZOOM_THROUGH) * easeInCubic(tt));
if (!isLast && t > 1 - CF) {
var k2 = clamp01((t - (1 - CF)) / CF);
opacity = 1 - k2;
blur = 6 * k2;
} else {
opacity = 1;
}
}
scene.style.opacity = opacity.toFixed(4);
img.style.transform = "scale(" + scale.toFixed(5) + ")";
scene.style.filter = blur > 0.05 ? "blur(" + blur.toFixed(2) + "px)" : "none";
}
for (var j = 0; j < N; j++) {
var card = cards[j];
var t2 = (p - j * seg) / seg;
var o = 0, y = 30;
if (t2 >= -0.02 && t2 <= 0.86) {
var fadeIn = j === 0 && p < 0.002 ? 1 : clamp01(t2 / 0.04);
var fadeOut = 1 - clamp01((t2 - 0.72) / 0.14);
o = Math.min(fadeIn, fadeOut);
y = 30 * (1 - fadeIn) - 60 * (1 - fadeOut);
}
card.style.opacity = o.toFixed(4);
card.style.transform = "translate(0, calc(-50% + " + y.toFixed(1) + "px))";
card.classList.toggle("is-live", o > 0.5);
}
updateChrome(p, Math.min(N - 1, Math.floor(p * N)));
}
/* ── shared chrome (rail, visibility) ───────────────────────────────── */
function updateChrome(p, sceneIdx) {
railFill.style.height = (p * 100).toFixed(2) + "%";
dots.forEach(function (d, i) { d.classList.toggle("is-active", i === sceneIdx); });
rail.classList.toggle("is-visible", p > 0.01 && p < 0.995);
world.style.visibility = p >= 1 && window.scrollY > scrollLen ? "hidden" : "visible";
}
function render(p) {
if (videoMode) renderVideo(p); else renderStills(p);
}
function tick() {
current += (target - current) * LERP;
if (Math.abs(target - current) < 0.00005) current = target;
render(current);
requestAnimationFrame(tick);
}
/* ── boot: use the flight video when available ──────────────────────── */
function enableVideoMode() {
duration = TIMELINE[TIMELINE.length - 1].end;
videoMode = true;
world.classList.add("world--video");
render(current);
}
if (video && TIMELINE) {
// Big/hi-dpi screens get the 1440p build when the host serves it;
// on a 404 (e.g. running from the repo, which only ships 1080p) the
// engine drops down to flight.mp4, then to the stills fallback.
var sources = ["assets/flight.mp4"];
var effectiveWidth = window.screen.width * (window.devicePixelRatio || 1);
if (effectiveWidth >= 2200) sources.unshift("assets/flight_1440.mp4");
var tryNext = function () {
var src = sources.shift();
if (!src) { videoMode = false; return; }
video.src = src;
video.load();
};
video.addEventListener("loadedmetadata", enableVideoMode);
video.addEventListener("error", tryNext);
tryNext();
}
/* ── nav deep-links into the flight ─────────────────────────────────── */
var jumpMap = { "#world-ledger": 1, "#world-reviewer": 2, "#world-arena": 3, "#world-frontier": 4 };
document.querySelectorAll('a[href^="#world-"]').forEach(function (a) {
a.addEventListener("click", function (e) {
var idx = jumpMap[a.getAttribute("href")];
if (idx === undefined) return;
e.preventDefault();
var frac;
if (videoMode) {
// Aim at 25% into the scene's dive segment.
var seg = TIMELINE[idx * 2];
frac = (seg.start + (seg.end - seg.start) * 0.25) / duration;
} else {
frac = idx / N + 0.35 / N;
}
window.scrollTo({ top: frac * scrollLen, behavior: reducedMotion ? "auto" : "smooth" });
});
});
window.addEventListener("scroll", readScroll, { passive: true });
window.addEventListener("resize", layout);
layout();
requestAnimationFrame(tick);
})();