hotschpotsh/product-scroll-poc/script.js

286 lines
8.3 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 = 4.0; // Increased speed for faster loading
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
}
});
}
// --- 3D Parallax Implementation ---
async function initParallax() {
// Wait for generic window load to ensure Three.js is ready
if (typeof THREE === 'undefined') {
console.warn("Three.js not loaded yet. Retrying...");
requestAnimationFrame(initParallax);
return;
}
const parallaxCanvas = document.querySelector("#parallax-canvas");
if (!parallaxCanvas) return;
// SCENE SETUP
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: parallaxCanvas, alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// TEXTURE LOADER
const textureLoader = new THREE.TextureLoader();
// Load textures
// Note: Assuming these files exist in the same directory/public folder
const [originalTexture, depthTexture] = await Promise.all([
new Promise(resolve => textureLoader.load('workshop.jpg', resolve)),
new Promise(resolve => textureLoader.load('workshop_depth.png', resolve))
]);
// GEOMETRY & MATERIAL
const geometry = new THREE.PlaneGeometry(16, 9, 128, 128); // Increased segments for smoother displacement
const material = new THREE.ShaderMaterial({
uniforms: {
tImage: { value: originalTexture },
tDepth: { value: depthTexture },
uDepthScale: { value: 3.0 }, // Exaggerated depth
uMouse: { value: new THREE.Vector2(0, 0) },
uScroll: { value: 0 }
},
vertexShader: `
varying vec2 vUv;
varying float vDisplacement;
uniform sampler2D tDepth;
uniform float uDepthScale;
uniform vec2 uMouse;
void main() {
vUv = uv;
float depth = texture2D(tDepth, uv).r;
vDisplacement = depth;
vec3 newPosition = position;
// Displace along Z
newPosition.z += depth * uDepthScale;
// Mouse Parallax (Simulate perspective shift)
// Closer objects (light depth) move more than far objects
newPosition.x += (uMouse.x * depth * 0.5);
newPosition.y += (uMouse.y * depth * 0.5);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform sampler2D tImage;
void main() {
gl_FragColor = texture2D(tImage, vUv);
}
`,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
camera.position.z = 5;
// MOUSE INTERACTION
window.addEventListener("mousemove", (e) => {
const x = (e.clientX / window.innerWidth) * 2 - 1;
const y = -(e.clientY / window.innerHeight) * 2 + 1;
// Smooth lerp could be better, but direct set for responsiveness
gsap.to(material.uniforms.uMouse.value, {
x: x * 0.5, // Sensitivity
y: y * 0.5,
duration: 1,
ease: "power2.out"
});
});
// RESIZE HANDLER
function handleResize() {
const videoAspect = 16 / 9;
const windowAspect = window.innerWidth / window.innerHeight;
camera.aspect = windowAspect;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
// Cover logic
if (windowAspect < videoAspect) {
mesh.scale.set(videoAspect / windowAspect, 1, 1);
} else {
mesh.scale.set(1, windowAspect / videoAspect, 1);
}
}
window.addEventListener('resize', handleResize);
handleResize(); // Initial call
// SCROLL ANIMATION (GSAP)
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".parallax-section",
start: "top top",
end: "bottom bottom",
scrub: true
}
});
tl.to(camera.position, {
z: 3.5,
ease: "none"
}, 0);
// Fade out to reveal product
tl.to(".product-reveal", { opacity: 1, duration: 0.2 }, 0.9);
tl.to(parallaxCanvas, { opacity: 0, duration: 0.2 }, 0.95);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
// Start
initParallax();