console.clear(); gsap.registerPlugin(Flip, ScrollTrigger); let ctx, renderer, scene, camera, mesh, canvasEl; /* makeGradientNoiseTexture() This becomes our texture for the cube. */ function makeGradientNoiseTexture() { const c = document.createElement("canvas"); c.width = c.height = 256; const g = c.getContext("2d"); g.fillStyle = (() => { const grd = g.createLinearGradient(0, 0, 230, 384); grd.addColorStop(0, "#fec5fb"); // color A grd.addColorStop(1, "#00bae2"); // color B return grd; })(); g.fillRect(0, 0, 256, 256); // Subtle grain for texture. for (let i = 0; i < 4000; i++) { const x = Math.floor(gsap.utils.random(0, 256)); const y = Math.floor(gsap.utils.random(0, 256)); const a = gsap.utils.random(0.02, 0.1); g.fillStyle = `rgba(0,0,0,${a})`; g.fillRect(x, y, 3, 3); } const tex = new THREE.CanvasTexture(c); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4; return tex; } /* initThree(canvas) Sets up a very small Three.js scene that renders into the supplied canvas. */ function initThree(canvas) { renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); renderer.outputColorSpace = THREE.SRGBColorSpace; scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100); camera.position.set(0, 0, 3); // Square cube with our gradient noise texture const mat = new THREE.MeshBasicMaterial({ map: makeGradientNoiseTexture() }); mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), mat); scene.add(mesh); // Render on the GSAP ticker so ScrollTrigger timing stays in sync gsap.ticker.add(render); onResize(); buildTimeline(); } /* render() the scene. */ function render() { if (!renderer) return; renderer.render(scene, camera); } /* onResize() */ function onResize() { if (!renderer || !canvasEl) return; const r = canvasEl.getBoundingClientRect(); const dpr = Math.min(window.devicePixelRatio || 1, 2); renderer.setPixelRatio(1); renderer.setSize( Math.max(1, r.width * dpr), Math.max(1, r.height * dpr), false ); camera.aspect = (r.width || 1) / (r.height || 1); camera.updateProjectionMatrix(); } /* buildTimeline() Flip moves the canvas between your marker targets while we rotate the cube in the same timeline. */ function buildTimeline() { ctx && ctx.revert(); // clean teardown for resizes ctx = gsap.context(() => { const s2 = Flip.getState(".second .marker"); const s3 = Flip.getState(".third .marker"); const tl = gsap.timeline({ scrollTrigger: { start: 0, end: "max", scrub: 2 } }); // Hop to second marker and rotate the cube tl.add(Flip.fit(canvasEl, s2, { duration: 1, ease: "none" }), 0) .to( mesh.rotation, { x: `+=${Math.PI}`, y: `+=${Math.PI}`, duration: 1, ease: "none" }, "<" ) // A little breathing room between hops .addLabel("mid", "+=0.5") // Hop to third marker and rotate again .add(Flip.fit(canvasEl, s3, { duration: 1, ease: "none" }), "mid") .to( mesh.rotation, { x: `+=${Math.PI}`, y: `+=${Math.PI}`, duration: 1, ease: "none" }, "<" ); }); } /* build() Creates the canvas in the starting container and kicks off the scene. */ function build() { ctx && ctx.revert(); const start = document.querySelector(".container.initial"); if (!canvasEl) { canvasEl = document.createElement("canvas"); canvasEl.className = "box"; start.appendChild(canvasEl); initThree(canvasEl); } else { buildTimeline(); } } // Boot it up build(); // Keep things laid out correctly when the layout changes addEventListener("resize", () => { onResize(); buildTimeline(); });