singlecell's picture
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);
});
});