Initial commit of project structure
This commit is contained in:
137
product-scroll-poc/script.js
Normal file
137
product-scroll-poc/script.js
Normal file
@@ -0,0 +1,137 @@
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user