<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arix Signature Christmas Tree</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
background-color: #020504;
color: #F2D06B;
font-family: 'Cinzel', serif;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
#root { width: 100vw; height: 100vh; }
/* Loading Overlay */
#loader {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: #020504; display: flex; justify-content: center; align-items: center;
z-index: 9999; color: #FFD700; transition: opacity 0.5s; pointer-events: none;
}
</style>
<!-- 1. Load Babel for browser-side compilation -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- 2. Define Import Map with Stable Public CDNs (esm.sh) -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"three": "https://esm.sh/three@0.160.0",
"@react-three/fiber": "https://esm.sh/@react-three/fiber@8.15.16?external=react,react-dom,three",
"@react-three/drei": "https://esm.sh/@react-three/drei@9.99.0?external=react,react-dom,three,@react-three/fiber",
"@react-three/postprocessing": "https://esm.sh/@react-three/postprocessing@2.16.0?external=react,react-dom,three,@react-three/fiber",
"@mediapipe/tasks-vision": "https://esm.sh/@mediapipe/tasks-vision@0.10.8",
"uuid": "https://esm.sh/uuid@9.0.1"
}
}
</script>
</head>
<body>
<div id="loader">INITIALIZING ARIX EXPERIENCE...</div>
<div id="root"></div>
<!-- 3. MAIN APPLICATION LOGIC (EMBEDDED) -->
<script type="text/babel" data-type="module" data-presets="react,typescript">
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import { EffectComposer, Bloom, Vignette, Noise } from '@react-three/postprocessing';
import * as THREE from 'three';
import { FilesetResolver, GestureRecognizer } from '@mediapipe/tasks-vision';
// --- MATH & UTILS ---
const TREE_HEIGHT = 12;
const TREE_RADIUS = 4.5;
const SCATTER_RADIUS = 25;
const randomInSphere = (radius) => {
const u = Math.random();
const v = Math.random();
const theta = 2 * Math.PI * u;
const phi = Math.acos(2 * v - 1);
const r = Math.cbrt(Math.random()) * radius;
const sinPhi = Math.sin(phi);
return new THREE.Vector3(
r * sinPhi * Math.cos(theta),
r * sinPhi * Math.sin(theta),
r * Math.cos(phi)
);
};
const pointInCone = (h, r) => {
const y = Math.random() * h;
const rAtY = (r * (h - y)) / h;
const angle = Math.random() * Math.PI * 2;
const rad = Math.sqrt(Math.random()) * rAtY;
return new THREE.Vector3(
rad * Math.cos(angle),
y - h / 2,
rad * Math.sin(angle)
);
};
const pointOnConeSurface = (h, r, t) => {
const y = t * h - h / 2;
const rAtY = (r * (1 - t));
const angle = Math.random() * Math.PI * 2;
return new THREE.Vector3(
rAtY * Math.cos(angle),
y,
rAtY * Math.sin(angle)
);
};
const getSpiralPos = (i, count, h, r) => {
const t = i / count;
const y = t * h - h / 2;
const rAtY = (r * (1 - t)) + 0.2;
const loops = 8;
const angle = t * Math.PI * 2 * loops;
return new THREE.Vector3(
rAtY * Math.cos(angle),
y,
rAtY * Math.sin(angle)
);
};
// --- SHADERS ---
const FoliageMaterial = {
uniforms: {
uTime: { value: 0 },
uProgress: { value: 0 },
uColor: { value: new THREE.Color('#0B3B24') },
uHighlight: { value: new THREE.Color('#4F7A5E') },
uGold: { value: new THREE.Color('#FFD700') }
},
vertexShader: `
uniform float uTime;
uniform float uProgress;
attribute vec3 aScatterPos;
attribute vec3 aTreePos;
attribute float aRandom;
varying float vAlpha;
varying vec3 vColor;
float easeOutCubic(float x) {
return 1.0 - pow(1.0 - x, 3.0);
}
void main() {
float t = easeOutCubic(uProgress);
vec3 pos = mix(aScatterPos, aTreePos, t);
float wind = sin(uTime * 2.0 + pos.y * 0.5 + pos.x) * 0.05 * t;
pos.x += wind;
pos.z += wind;
if (t < 0.5) {
pos.y += sin(uTime + aRandom * 10.0) * 0.1;
}
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
gl_PointSize = (8.0 * aRandom + 3.0) * (10.0 / -mvPosition.z);
vAlpha = 0.6 + 0.4 * sin(uTime * 3.0 + aRandom * 100.0);
}
`,
fragmentShader: `
uniform vec3 uColor;
uniform vec3 uHighlight;
uniform vec3 uGold;
varying float vAlpha;
varying vec3 vColor;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard;
float strength = 1.0 - (dist * 2.0);
strength = pow(strength, 1.5);
vec3 finalColor = mix(uColor, uGold, strength * 0.5);
gl_FragColor = vec4(finalColor, vAlpha * strength);
}
`
};
// --- COMPONENTS ---
const GestureController = ({ setTreeState, syncData }) => {
useEffect(() => {
let recognizer;
let video;
let lastVideoTime = -1;
let frameId;
const setup = async () => {
try {
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.8/wasm"
);
recognizer = await GestureRecognizer.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/1/gesture_recognizer.task",
delegate: "GPU"
},
runningMode: "VIDEO",
numHands: 1
});
video = document.createElement("video");
video.style.display = "none";
document.body.appendChild(video);
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await new Promise((resolve) => {
video.onloadedmetadata = () => {
video.play();
resolve(true);
}
});
const loop = () => {
if (video && video.currentTime !== lastVideoTime) {
lastVideoTime = video.currentTime;
const result = recognizer.recognizeForVideo(video, Date.now());
if (result.gestures.length > 0 && result.landmarks.length > 0) {
const gesture = result.gestures[0][0];
const landmarks = result.landmarks[0];
const handX = 1.0 - landmarks[9].x; // Mirror X
const handY = landmarks[9].y;
syncData.hasHand = true;
syncData.handX = THREE.MathUtils.lerp(syncData.handX, handX, 0.1);
syncData.handY = THREE.MathUtils.lerp(syncData.handY, handY, 0.1);
if (gesture.categoryName === "Open_Palm") {
setTreeState(false);
} else if (gesture.categoryName === "Closed_Fist") {
setTreeState(true);
}
} else {
syncData.hasHand = false;
}
}
frameId = requestAnimationFrame(loop);
};
loop();
} catch (e) {
console.warn("Camera/MediaPipe skipped or failed:", e);
}
};
setup();
return () => {
cancelAnimationFrame(frameId);
if (video && video.srcObject) {
video.srcObject.getTracks().forEach(t => t.stop());
video.remove();
}
};
}, [setTreeState, syncData]);
return null;
};
const Foliage = ({ syncData }) => {
const count = 12000;
const meshRef = useRef(null);
const { positions, scatterPositions, randoms } = useMemo(() => {
const pos = new Float32Array(count * 3);
const scatter = new Float32Array(count * 3);
const rand = new Float32Array(count);
for (let i = 0; i < count; i++) {
const treeP = pointInCone(TREE_HEIGHT, TREE_RADIUS);
pos[i * 3] = treeP.x;
pos[i * 3 + 1] = treeP.y;
pos[i * 3 + 2] = treeP.z;
const scatterP = randomInSphere(SCATTER_RADIUS);
scatter[i * 3] = scatterP.x;
scatter[i * 3 + 1] = scatterP.y;
scatter[i * 3 + 2] = scatterP.z;
rand[i] = Math.random();
}
return { positions: pos, scatterPositions: scatter, randoms: rand };
}, []);
useFrame((state) => {
if (meshRef.current) {
const material = meshRef.current.material;
material.uniforms.uTime.value = state.clock.elapsedTime;
material.uniforms.uProgress.value = syncData.value;
}
});
return (
<points ref={meshRef}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" count={count} array={positions} itemSize={3} />
<bufferAttribute attach="attributes-aTreePos" count={count} array={positions} itemSize={3} />
<bufferAttribute attach="attributes-aScatterPos" count={count} array={scatterPositions} itemSize={3} />
<bufferAttribute attach="attributes-aRandom" count={count} array={randoms} itemSize={1} />
</bufferGeometry>
<shaderMaterial
attach="material"
args={[FoliageMaterial]}
transparent
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
};
const MorphingInstances = ({ count, geometry, material, getTreePos, syncData, scale = 1 }) => {
const meshRef = useRef(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
const data = useMemo(() => {
return new Array(count).fill(0).map((_, i) => {
const treePos = getTreePos(i);
const normalizedHeight = (treePos.y + TREE_HEIGHT / 2) / TREE_HEIGHT;
const heightScaleFactor = 1.0 - normalizedHeight * 0.5;
const scatterPos = randomInSphere(SCATTER_RADIUS * 0.8);
const rot = new THREE.Euler(Math.random()*Math.PI, Math.random()*Math.PI, 0);
return {
treePos,
scatterPos,
rot,
scale: scale * heightScaleFactor * (0.8 + Math.random() * 0.4)
};
});
}, [count, scale, getTreePos]);
useFrame((state) => {
if (!meshRef.current) return;
const time = state.clock.elapsedTime;
const progress = syncData.value;
const easeProgress = 1.0 - Math.pow(1.0 - progress, 3.0);
data.forEach((item, i) => {
// Use scatter pos if progress is 0, tree pos if 1, with interpolation
const x = THREE.MathUtils.lerp(item.scatterPos.x, item.treePos.x, easeProgress);
const y = THREE.MathUtils.lerp(item.scatterPos.y, item.treePos.y, easeProgress);
const z = THREE.MathUtils.lerp(item.scatterPos.z, item.treePos.z, easeProgress);
dummy.position.set(x, y, z);
dummy.position.y += Math.sin(time + i * 10) * 0.1;
dummy.rotation.copy(item.rot);
dummy.rotation.x += time * 0.2;
dummy.rotation.y += time * 0.1;
dummy.scale.setScalar(item.scale);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
});
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[geometry, material, count]} castShadow receiveShadow>
</instancedMesh>
);
};
const Ornaments = ({ syncData }) => {
const sphereGeo = useMemo(() => new THREE.SphereGeometry(1, 16, 16), []);
const boxGeo = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
const starGeo = useMemo(() => {
const pts = [];
for (let i = 0; i < 10; i++) {
const dist = i % 2 === 0 ? 1 : 0.5;
const ang = (i / 10) * Math.PI * 2;
pts.push(new THREE.Vector2(Math.cos(ang) * dist, Math.sin(ang) * dist));
}
const shape = new THREE.Shape(pts);
const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.2, bevelEnabled: true, bevelThickness: 0.1, bevelSize: 0.05, bevelSegments: 1 });
geo.center();
return geo;
}, []);
const goldMaterial = useMemo(() => new THREE.MeshStandardMaterial({
color: "#FFD700", metalness: 1, roughness: 0.05, emissive: "#C5A059", emissiveIntensity: 0.8
}), []);
const starMaterial = useMemo(() => new THREE.MeshStandardMaterial({
color: "#FFD700", metalness: 1, roughness: 0.1, emissive: "#FFD700", emissiveIntensity: 3.0
}), []);
const pearlMaterial = useMemo(() => new THREE.MeshStandardMaterial({
color: "#F5F5F0", metalness: 0.1, roughness: 0.1
}), []);
const redGemMaterial = useMemo(() => new THREE.MeshPhysicalMaterial({
color: "#8A0B28", metalness: 0.1, roughness: 0.0, transmission: 0.6, thickness: 1
}), []);
const giftGreenMat = useMemo(() => new THREE.MeshStandardMaterial({ color: "#0F3B25", metalness: 0.2, roughness: 0.5 }), []);
const giftBrownMat = useMemo(() => new THREE.MeshStandardMaterial({ color: "#C4A484", metalness: 0.1, roughness: 0.6 }), []);
const giftPearlMat = useMemo(() => new THREE.MeshStandardMaterial({ color: "#FFF5EE", metalness: 0.3, roughness: 0.2 }), []);
const getGiftPos = () => {
const r = 2.5 + Math.random() * 4.0;
const a = Math.random() * Math.PI * 2;
const yOffset = Math.random() * 1.5;
return new THREE.Vector3(r * Math.cos(a), -TREE_HEIGHT/2 + 0.6 + yOffset, r * Math.sin(a));
};
const getSurfaceDistributedPos = (rScale = 1.0) => {
const u = Math.random();
const t = 1 - Math.sqrt(u);
return pointOnConeSurface(TREE_HEIGHT, TREE_RADIUS * rScale, t);
};
return (
<group>
<MorphingInstances count={300} geometry={sphereGeo} material={pearlMaterial} scale={0.12} syncData={syncData} get