create a website called "algorithmic garden" that uses webxr or xr.js plus three.js to display a list of .glb models that can overlayed over whatever a cellphone camera sees (AR). There should be pinch to zoom and rotate commands on the phone interface.
e34da73 verified | document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize Three.js scene | |
| let scene, camera, renderer, controls; | |
| let currentModel = null; | |
| let isARSupported = false; | |
| let isInARMode = false; | |
| // Model gallery data | |
| const modelData = [ | |
| { | |
| id: 'bamboo', | |
| title: 'Bamboo Cluster', | |
| description: 'A peaceful cluster of bamboo stalks', | |
| thumbnail: 'https://static.photos/nature/640x360/1', | |
| path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/bamboo.glb' | |
| }, | |
| { | |
| id: 'bonsai', | |
| title: 'Zen Bonsai', | |
| description: 'A meticulously crafted bonsai tree', | |
| thumbnail: 'https://static.photos/nature/640x360/2', | |
| path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/bonsai.glb' | |
| }, | |
| { | |
| id: 'fern', | |
| title: 'Lush Fern', | |
| description: 'A vibrant green fern plant', | |
| thumbnail: 'https://static.photos/nature/640x360/3', | |
| path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/fern.glb' | |
| }, | |
| { | |
| id: 'lotus', | |
| title: 'Floating Lotus', | |
| description: 'A beautiful water lotus flower', | |
| thumbnail: 'https://static.photos/nature/640x360/4', | |
| path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/lotus.glb' | |
| }, | |
| { | |
| id: 'palm', | |
| title: 'Tropical Palm', | |
| description: 'A tall tropical palm tree', | |
| thumbnail: 'https://static.photos/nature/640x360/5', | |
| path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/palm.glb' | |
| }, | |
| { | |
| id: 'sakura', | |
| title: 'Cherry Blossom', | |
| description: 'A delicate cherry blossom tree', | |
| thumbnail: 'https://static.photos/nature/640x360/6', | |
| path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/sakura.glb' | |
| } | |
| ]; | |
| // Check for WebXR support | |
| function checkXRSupport() { | |
| if ('xr' in navigator) { | |
| navigator.xr.isSessionSupported('immersive-ar').then((supported) => { | |
| isARSupported = supported; | |
| document.getElementById('xr-button').style.display = supported ? 'block' : 'none'; | |
| }); | |
| } | |
| } | |
| // Initialize Three.js scene | |
| function initScene() { | |
| const canvas = document.getElementById('ar-viewport'); | |
| // Scene | |
| scene = new THREE.Scene(); | |
| // Camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 1.6, 3); | |
| // Renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: canvas, | |
| antialias: true, | |
| alpha: true | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.xr.enabled = true; | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); | |
| directionalLight.position.set(0, 10, 5); | |
| scene.add(directionalLight); | |
| // Controls (for non-AR mode) | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.25; | |
| // Load default model | |
| loadModel(modelData[0].path); | |
| // Check XR support | |
| checkXRSupport(); | |
| // Start animation loop | |
| animate(); | |
| // Hide loading screen | |
| setTimeout(() => { | |
| document.getElementById('loading').style.display = 'none'; | |
| }, 1500); | |
| } | |
| // Load 3D model | |
| function loadModel(path) { | |
| const loader = new THREE.GLTFLoader(); | |
| if (currentModel) { | |
| scene.remove(currentModel); | |
| } | |
| loader.load(path, (gltf) => { | |
| currentModel = gltf.scene; | |
| currentModel.scale.set(0.5, 0.5, 0.5); | |
| currentModel.position.set(0, 0, 0); | |
| scene.add(currentModel); | |
| }, undefined, (error) => { | |
| console.error('Error loading model:', error); | |
| }); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (!isInARMode) { | |
| controls.update(); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // Initialize AR session | |
| function startARSession() { | |
| if (!isARSupported) return; | |
| const sessionInit = { optionalFeatures: ['dom-overlay', 'dom-overlay-for-handheld-ar'] }; | |
| navigator.xr.requestSession('immersive-ar', sessionInit).then((session) => { | |
| isInARMode = true; | |
| document.getElementById('ar-controls').classList.remove('hidden'); | |
| renderer.xr.setSession(session); | |
| // Add reticle for placing objects | |
| const reticle = new THREE.Mesh( | |
| new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2), | |
| new THREE.MeshBasicMaterial({ color: 0xffffff }) | |
| ); | |
| reticle.matrixAutoUpdate = false; | |
| reticle.visible = false; | |
| scene.add(reticle); | |
| // Handle session end | |
| session.addEventListener('end', () => { | |
| isInARMode = false; | |
| document.getElementById('ar-controls').classList.add('hidden'); | |
| currentModel.position.set(0, 0, 0); | |
| }); | |
| // Handle select events (placing objects) | |
| session.addEventListener('select', () => { | |
| if (currentModel && reticle.visible) { | |
| currentModel.position.setFromMatrixPosition(reticle.matrix); | |
| } | |
| }); | |
| }); | |
| } | |
| // Event listeners | |
| document.getElementById('xr-button').addEventListener('click', startARSession); | |
| document.getElementById('rotate-btn').addEventListener('click', () => { | |
| if (currentModel) { | |
| currentModel.rotation.y += Math.PI / 4; | |
| } | |
| }); | |
| document.getElementById('scale-btn').addEventListener('touchstart', (e) => { | |
| if (e.touches.length === 2 && currentModel) { | |
| const touch1 = e.touches[0]; | |
| const touch2 = e.touches[1]; | |
| const dist1 = Math.hypot( | |
| touch2.pageX - touch1.pageX, | |
| touch2.pageY - touch1.pageY | |
| ); | |
| function handleMove(e) { | |
| const touch1 = e.touches[0]; | |
| const touch2 = e.touches[1]; | |
| const dist2 = Math.hypot( | |
| touch2.pageX - touch1.pageX, | |
| touch2.pageY - touch1.pageY | |
| ); | |
| const scale = dist2 / dist1; | |
| currentModel.scale.set(scale, scale, scale); | |
| } | |
| function handleEnd() { | |
| document.removeEventListener('touchmove', handleMove); | |
| document.removeEventListener('touchend', handleEnd); | |
| } | |
| document.addEventListener('touchmove', handleMove); | |
| document.addEventListener('touchend', handleEnd); | |
| } | |
| }); | |
| document.getElementById('place-btn').addEventListener('click', () => { | |
| // In AR mode, this would place the object at the reticle position | |
| if (isInARMode && currentModel) { | |
| currentModel.position.set(0, 0, -1); | |
| } | |
| }); | |
| // Populate model gallery | |
| function populateModelGallery() { | |
| const gallery = document.getElementById('model-gallery'); | |
| modelData.forEach((model) => { | |
| const card = document.createElement('div'); | |
| card.className = 'model-card bg-white dark:bg-gray-800 rounded-xl overflow-hidden shadow-md cursor-pointer transition-all'; | |
| card.innerHTML = ` | |
| <div class="h-48 overflow-hidden"> | |
| <img src="${model.thumbnail}" alt="${model.title}" class="w-full h-full object-cover"> | |
| </div> | |
| <div class="p-4"> | |
| <h3 class="font-bold text-lg mb-1">${model.title}</h3> | |
| <p class="text-gray-600 dark:text-gray-300 text-sm mb-3">${model.description}</p> | |
| <button class="bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-1 rounded-full text-sm transition-colors"> | |
| View in AR | |
| </button> | |
| </div> | |
| `; | |
| card.addEventListener('click', () => loadModel(model.path)); | |
| gallery.appendChild(card); | |
| }); | |
| } | |
| // Initialize everything | |
| populateModelGallery(); | |
| initScene(); | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| }); |