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();