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>