286 lines
8.3 KiB
JavaScript
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();
|