< style > #media-3d-canvas {
position: fixed ;
top: 0 ;
left: 0 ;
width: 100 vw ;
height: 100 vh ;
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 3 D 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>