singlecell/algorithmic-arboretum
0
1document.addEventListener('DOMContentLoaded', () => {2 // Initialize Three.js scene3 let scene, camera, renderer, controls;4 let currentModel = null;5 let isARSupported = false;6 let isInARMode = false;7 8 // Model gallery data9 const modelData = [10 {11 id: 'bamboo',12 title: 'Bamboo Cluster',13 description: 'A peaceful cluster of bamboo stalks',14 thumbnail: 'https://static.photos/nature/640x360/1',15 path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/bamboo.glb'16 },17 {18 id: 'bonsai',19 title: 'Zen Bonsai',20 description: 'A meticulously crafted bonsai tree',21 thumbnail: 'https://static.photos/nature/640x360/2',22 path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/bonsai.glb'23 },24 {25 id: 'fern',26 title: 'Lush Fern',27 description: 'A vibrant green fern plant',28 thumbnail: 'https://static.photos/nature/640x360/3',29 path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/fern.glb'30 },31 {32 id: 'lotus',33 title: 'Floating Lotus',34 description: 'A beautiful water lotus flower',35 thumbnail: 'https://static.photos/nature/640x360/4',36 path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/lotus.glb'37 },38 {39 id: 'palm',40 title: 'Tropical Palm',41 description: 'A tall tropical palm tree',42 thumbnail: 'https://static.photos/nature/640x360/5',43 path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/palm.glb'44 },45 {46 id: 'sakura',47 title: 'Cherry Blossom',48 description: 'A delicate cherry blossom tree',49 thumbnail: 'https://static.photos/nature/640x360/6',50 path: 'https://cdn.glitch.global/8e8b5d9a-7e4d-4a8b-9e8f-8f8e8f8e8f8e/sakura.glb'51 }52 ];53 54 // Check for WebXR support55 function checkXRSupport() {56 if ('xr' in navigator) {57 navigator.xr.isSessionSupported('immersive-ar').then((supported) => {58 isARSupported = supported;59 document.getElementById('xr-button').style.display = supported ? 'block' : 'none';60 });61 }62 }63 64 // Initialize Three.js scene65 function initScene() {66 const canvas = document.getElementById('ar-viewport');67 68 // Scene69 scene = new THREE.Scene();70 71 // Camera72 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);73 camera.position.set(0, 1.6, 3);74 75 // Renderer76 renderer = new THREE.WebGLRenderer({77 canvas: canvas,78 antialias: true,79 alpha: true80 });81 renderer.setSize(window.innerWidth, window.innerHeight);82 renderer.setPixelRatio(window.devicePixelRatio);83 renderer.xr.enabled = true;84 85 // Lighting86 const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);87 scene.add(ambientLight);88 89 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);90 directionalLight.position.set(0, 10, 5);91 scene.add(directionalLight);92 93 // Controls (for non-AR mode)94 controls = new THREE.OrbitControls(camera, renderer.domElement);95 controls.enableDamping = true;96 controls.dampingFactor = 0.25;97 98 // Load default model99 loadModel(modelData[0].path);100 101 // Check XR support102 checkXRSupport();103 104 // Start animation loop105 animate();106 107 // Hide loading screen108 setTimeout(() => {109 document.getElementById('loading').style.display = 'none';110 }, 1500);111 }112 113 // Load 3D model114 function loadModel(path) {115 const loader = new THREE.GLTFLoader();116 117 if (currentModel) {118 scene.remove(currentModel);119 }120 121 loader.load(path, (gltf) => {122 currentModel = gltf.scene;123 currentModel.scale.set(0.5, 0.5, 0.5);124 currentModel.position.set(0, 0, 0);125 scene.add(currentModel);126 }, undefined, (error) => {127 console.error('Error loading model:', error);128 });129 }130 131 // Animation loop132 function animate() {133 requestAnimationFrame(animate);134 135 if (!isInARMode) {136 controls.update();137 }138 139 renderer.render(scene, camera);140 }141 142 // Initialize AR session143 function startARSession() {144 if (!isARSupported) return;145 146 const sessionInit = { optionalFeatures: ['dom-overlay', 'dom-overlay-for-handheld-ar'] };147 148 navigator.xr.requestSession('immersive-ar', sessionInit).then((session) => {149 isInARMode = true;150 document.getElementById('ar-controls').classList.remove('hidden');151 152 renderer.xr.setSession(session);153 154 // Add reticle for placing objects155 const reticle = new THREE.Mesh(156 new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2),157 new THREE.MeshBasicMaterial({ color: 0xffffff })158 );159 reticle.matrixAutoUpdate = false;160 reticle.visible = false;161 scene.add(reticle);162 163 // Handle session end164 session.addEventListener('end', () => {165 isInARMode = false;166 document.getElementById('ar-controls').classList.add('hidden');167 currentModel.position.set(0, 0, 0);168 });169 170 // Handle select events (placing objects)171 session.addEventListener('select', () => {172 if (currentModel && reticle.visible) {173 currentModel.position.setFromMatrixPosition(reticle.matrix);174 }175 });176 });177 }178 179 // Event listeners180 document.getElementById('xr-button').addEventListener('click', startARSession);181 182 document.getElementById('rotate-btn').addEventListener('click', () => {183 if (currentModel) {184 currentModel.rotation.y += Math.PI / 4;185 }186 });187 188 document.getElementById('scale-btn').addEventListener('touchstart', (e) => {189 if (e.touches.length === 2 && currentModel) {190 const touch1 = e.touches[0];191 const touch2 = e.touches[1];192 const dist1 = Math.hypot(193 touch2.pageX - touch1.pageX,194 touch2.pageY - touch1.pageY195 );196 197 function handleMove(e) {198 const touch1 = e.touches[0];199 const touch2 = e.touches[1];200 const dist2 = Math.hypot(201 touch2.pageX - touch1.pageX,202 touch2.pageY - touch1.pageY203 );204 205 const scale = dist2 / dist1;206 currentModel.scale.set(scale, scale, scale);207 }208 209 function handleEnd() {210 document.removeEventListener('touchmove', handleMove);211 document.removeEventListener('touchend', handleEnd);212 }213 214 document.addEventListener('touchmove', handleMove);215 document.addEventListener('touchend', handleEnd);216 }217 });218 219 document.getElementById('place-btn').addEventListener('click', () => {220 // In AR mode, this would place the object at the reticle position221 if (isInARMode && currentModel) {222 currentModel.position.set(0, 0, -1);223 }224 });225 226 // Populate model gallery227 function populateModelGallery() {228 const gallery = document.getElementById('model-gallery');229 230 modelData.forEach((model) => {231 const card = document.createElement('div');232 card.className = 'model-card bg-white dark:bg-gray-800 rounded-xl overflow-hidden shadow-md cursor-pointer transition-all';233 card.innerHTML = `234 <div class="h-48 overflow-hidden">235 <img src="${model.thumbnail}" alt="${model.title}" class="w-full h-full object-cover">236 </div>237 <div class="p-4">238 <h3 class="font-bold text-lg mb-1">${model.title}</h3>239 <p class="text-gray-600 dark:text-gray-300 text-sm mb-3">${model.description}</p>240 <button class="bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-1 rounded-full text-sm transition-colors">241 View in AR242 </button>243 </div>244 `;245 246 card.addEventListener('click', () => loadModel(model.path));247 gallery.appendChild(card);248 });249 }250 251 // Initialize everything252 populateModelGallery();253 initScene();254 window.addEventListener('resize', () => {255 camera.aspect = window.innerWidth / window.innerHeight;256 camera.updateProjectionMatrix();257 renderer.setSize(window.innerWidth, window.innerHeight);258 });259});