138 lines
3.6 KiB
JavaScript
138 lines
3.6 KiB
JavaScript
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
|
|
}
|
|
});
|
|
}
|