Category: three.js

  • three.js with mero.live

    <style>#media-3d-canvas {
           position: fixed;
           top: 0;
           left: 0;
           width: 100vw;
           height: 100vh;
           background-color: #000;
           z-index: 1; /* Make sure this is behind your Alpine UI */
       }
       #media-vault {
           display: none;
       }
       .page-container {
           position: relative;
           z-index: 2; /* Keeps Alpine loader/UI on top */
           pointer-events: none; /* Allows clicks to pass through to the 3D gallery */
       }
       .page-container * {
           pointer-events: auto; /* Re-enables clicking for UI elements */
       }</style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js"></script>
    <script>
       window.addEventListener('load', function() {
           console.log("Gallery: Waiting for Alpine...");
    
           let alpineCheck = setInterval(() => {
               const el = document.querySelector('[x-data]');
               if (window.Alpine && el) {
                   // Support both Alpine v2 and v3
                   const data = el.__x ? el.__x.data : (window.Alpine.$data ? window.Alpine.$data(el) : null);
    
                   if (data && data.vids && data.vids.length > 0) {
                       console.log("Gallery: Data found, starting 3D...");
                       clearInterval(alpineCheck);
                       init3DGallery(data);
                   }
               }
           }, 100);
    
           function init3DGallery(alpineData) {
               const container = document.getElementById('media-3d-canvas');
               const vault = document.getElementById('media-vault');
    
               let scene, camera, renderer, raycaster, mouse;
               let planes = [];
               let velocity = new THREE.Vector3(0, 0, 0);
               let targetVel = new THREE.Vector3(0, 0, 0);
               let autoVel = new THREE.Vector3(0, 0, -0.2);
    
               let isDragging = false, interactionStarted = false;
               let lastPointer = { x: 0, y: 0 };
               let mediaIndexCounter = 0;
    
               const isMobile = /Android|iPhone/i.test(navigator.userAgent);
               const ACTIVE_COUNT = isMobile ? 10 : 20;
               const TUNNEL_LENGTH = 1600;
               const SPREAD = 700;
    
               function init() {
                   scene = new THREE.Scene();
                   scene.background = new THREE.Color(0x000000);
                   scene.fog = new THREE.Fog(0x000000, 100, TUNNEL_LENGTH * 0.9);
    
                   camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 3000);
                   camera.position.z = 500;
    
                   renderer = new THREE.WebGLRenderer({ antialias: true });
                   renderer.setSize(window.innerWidth, window.innerHeight);
                   renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
                   container.appendChild(renderer.domElement);
    
                   raycaster = new THREE.Raycaster();
                   mouse = new THREE.Vector2();
    
                   for (let i = 0; i < ACTIVE_COUNT; i++) { createPlane(i); }
    
                   const startVideos = () => {
                       if (interactionStarted) return;
                       interactionStarted = true;
                       planes.forEach(p => { if(p.userData.video) p.userData.video.play().catch(()=>{}) });
                   };
    
                   container.addEventListener('mousedown', e => {
                       isDragging = true;
                       lastPointer.x = e.clientX;
                       lastPointer.y = e.clientY;
                       startVideos();
                   });
                   window.addEventListener('mouseup', () => isDragging = false);
                   window.addEventListener('mousemove', onMouseMove);
    
                   container.addEventListener('touchstart', e => {
                       isDragging = true;
                       lastPointer.x = e.touches[0].clientX;
                       lastPointer.y = e.touches[0].clientY;
                       startVideos();
                   }, {passive: false});
    
                   container.addEventListener('touchmove', e => {
                       if(!isDragging) return;
                       const dX = e.touches[0].clientX - lastPointer.x;
                       const dY = e.touches[0].clientY - lastPointer.y;
                       targetVel.x -= dX * 0.2; targetVel.y += dY * 0.2;
                       lastPointer.x = e.touches[0].clientX; lastPointer.y = e.touches[0].clientY;
                   }, {passive: false});
    
                   container.addEventListener('touchend', () => isDragging = false);
                   container.addEventListener('click', onSelect);
                   window.addEventListener('resize', onResize);
    
                   animate();
               }
    
               function createPlane(index) {
                   // Using BufferGeometry for compatibility with r149
                   const geo = new THREE.PlaneBufferGeometry(1, 1);
                   // Start with a grey color so we can see them even if video is loading
                   const mat = new THREE.MeshBasicMaterial({ color: 0x222222, side: THREE.DoubleSide });
                   const mesh = new THREE.Mesh(geo, mat);
                   const zPos = 500 - (index * (TUNNEL_LENGTH / ACTIVE_COUNT));
                   mesh.position.set((Math.random()-0.5)*SPREAD, (Math.random()-0.5)*SPREAD, zPos);
                   scene.add(mesh);
                   planes.push(mesh);
                   recyclePlane(mesh);
               }
    
               function recyclePlane(mesh) {
                   const vids = alpineData.vids;
                   const videoData = vids[mediaIndexCounter % vids.length];
                   const originalIndex = mediaIndexCounter % vids.length;
                   mediaIndexCounter++;
    
                   if (mesh.userData.video) {
                       mesh.userData.video.pause();
                       mesh.userData.video.src = "";
                       mesh.userData.video.remove();
                   }
    
                   const v = document.createElement('video');
    
                   // PATH LOGIC FIX:
                   // Converts "/thumbs/123.webp" to "/videos/123.mp4"
                   let videoSrc = videoData.thumb
                       .replace('/thumbs/', '/videos/')
                       .replace('.webp', '.mp4');
    
                   v.src = videoSrc;
                   v.muted = true;
                   v.loop = true;
                   v.playsInline = true;
                   v.crossOrigin = "anonymous";
                   vault.appendChild(v);
    
                   if (interactionStarted) v.play().catch(()=>{});
    
                   const tex = new THREE.VideoTexture(v);
                   mesh.material.map = tex;
                   mesh.material.color.set(0xffffff); // Set back to white once texture is assigned
                   mesh.userData.video = v;
                   mesh.userData.index = originalIndex;
                   mesh.userData.baseX = (Math.random() - 0.5) * SPREAD;
                   mesh.userData.baseY = (Math.random() - 0.5) * SPREAD;
    
                   const unit = window.innerWidth / (isMobile ? 2.5 : 5);
                   mesh.scale.set(unit * 0.56, unit, 1);
               }
    
               function animate() {
                   requestAnimationFrame(animate);
                   if (!isDragging) camera.position.add(autoVel);
    
                   velocity.lerp(targetVel, 0.1);
                   camera.position.add(velocity);
                   targetVel.multiplyScalar(0.9);
    
                   const camZ = camera.position.z;
                   planes.forEach(mesh => {
                       // Infinite Loop
                       if (mesh.position.z > camZ + 200) {
                           mesh.position.z -= TUNNEL_LENGTH;
                           recyclePlane(mesh);
                       } else if (mesh.position.z < camZ - (TUNNEL_LENGTH - 200)) {
                           mesh.position.z += TUNNEL_LENGTH;
                           recyclePlane(mesh);
                       }
    
                       // Autoplay Center logic
                       const distToCam = Math.abs(mesh.position.z - (camZ - 350));
                       if (distToCam < 120) {
                           if (mesh.userData.video?.paused && interactionStarted) mesh.userData.video.play().catch(()=>{});
                           // Scale up the center video
                           const scaleFactor = isMobile ? 1.8 : 1.5;
                           mesh.scale.lerp(new THREE.Vector3((window.innerWidth/4)*0.56*scaleFactor, (window.innerWidth/4)*scaleFactor, 1), 0.1);
                       } else {
                           if (!mesh.userData.video?.paused) mesh.userData.video?.pause();
                       }
                   });
                   renderer.render(scene, camera);
               }
    
               function onMouseMove(e) {
                   if (!isDragging) return;
                   targetVel.x -= (e.clientX - lastPointer.x) * 0.1;
                   targetVel.y += (e.clientY - lastPointer.y) * 0.1;
                   lastPointer.x = e.clientX; lastPointer.y = e.clientY;
               }
    
               function onSelect(e) {
                   mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
                   mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
                   raycaster.setFromCamera(mouse, camera);
                   const hits = raycaster.intersectObjects(planes);
                   if (hits.length > 0) {
                       const idx = hits[0].object.userData.index;
                       // Calls your Alpine openVideo function
                       if(alpineData.openVideo) alpineData.openVideo(idx);
                   }
               }
    
               function onResize() {
                   camera.aspect = window.innerWidth / window.innerHeight;
                   camera.updateProjectionMatrix();
                   renderer.setSize(window.innerWidth, window.innerHeight);
               }
    
               init();
           }
       });
       </script>