gsap.registerPlugin(ScrollTrigger); const canvas = document.querySelector("#cup-canvas"); const context = canvas.getContext("2d"); const video = document.querySelector("#cup-video"); const status = document.querySelector("#buffer-status"); const frames = []; let isBuffered = false; // 1. Canvas Setup function resizeCanvas() { if (video.videoWidth) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; } else { canvas.width = 1000; canvas.height = 1000; } } // 2. Buffering Logic (Capturing frames into memory) video.addEventListener("loadedmetadata", () => { resizeCanvas(); startBuffering(); }); async function startBuffering() { video.currentTime = 0; video.playbackRate = 1; // Standard speed for better capture quality await video.play(); function capture() { if (video.paused || video.ended) { finishBuffering(); return; } // Draw video frame to worker canvas and store as ImageBitmap createImageBitmap(video).then(bitmap => { frames.push(bitmap); // Draw first frame immediately if (frames.length === 1) renderFrame(0); requestAnimationFrame(capture); }); } requestAnimationFrame(capture); } function finishBuffering() { isBuffered = true; status.style.opacity = "0"; video.pause(); initScrollAnimation(); } function renderFrame(index) { if (!frames[index]) return; context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(frames[index], 0, 0, canvas.width, canvas.height); } // 3. Optimized Scroll Animation function initScrollAnimation() { const totalFrames = frames.length - 1; const scrollObj = { frame: 0 }; // GSAP Timeline for the whole experience const tl = gsap.timeline({ scrollTrigger: { trigger: ".scroll-container", start: "top top", end: "bottom bottom", scrub: 0.5, // Slight lag-behind for smoothness } }); // Frame scrubbing tl.to(scrollObj, { frame: totalFrames, ease: "none", onUpdate: () => renderFrame(Math.floor(scrollObj.frame)) }, 0); // Intro Fade gsap.to(".intro-content", { opacity: 0, y: -50, scrollTrigger: { trigger: ".intro-section", start: "top top", end: "bottom center", scrub: true } }); // Sub-animations for depth tl.to(canvas, { scale: 0.95, rotateY: 5, ease: "sine.inOut" }, 0); // Content text reveal const sections = document.querySelectorAll(".content-section"); sections.forEach((section) => { const text = section.querySelector(".text-block"); gsap.fromTo(text, { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 1, scrollTrigger: { trigger: section, start: "top 70%", end: "top 30%", toggleActions: "play reverse play reverse", } } ); }); // Exit gsap.to(".product-stage", { opacity: 0, scrollTrigger: { trigger: ".outro-section", start: "top center", end: "bottom bottom", scrub: true } }); }