<!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} getTreePos={(i) => getSpiralPos(i, 300, TREE_HEIGHT, TREE_RADIUS)} />

      <MorphingInstances count={160} geometry={sphereGeo} material={goldMaterial} scale={0.35} syncData={syncData} getTreePos={() => getSurfaceDistributedPos(0.9)} />

      <MorphingInstances count={50} geometry={sphereGeo} material={redGemMaterial} scale={0.18} syncData={syncData} getTreePos={() => getSurfaceDistributedPos(0.85)} />

      <MorphingInstances count={25} geometry={boxGeo} material={giftGreenMat} scale={1.0} syncData={syncData} getTreePos={getGiftPos} />

      <MorphingInstances count={25} geometry={boxGeo} material={giftBrownMat} scale={0.9} syncData={syncData} getTreePos={getGiftPos} />

      <MorphingInstances count={25} geometry={boxGeo} material={giftPearlMat} scale={1.1} syncData={syncData} getTreePos={getGiftPos} />

      <TopStar syncData={syncData} geometry={starGeo} material={starMaterial} />

    </group>

  );

};


const TopStar = ({ syncData, geometry, material }) => {

  const mesh = useRef(null);

  const startPos = useMemo(() => randomInSphere(SCATTER_RADIUS), []);

  const endPos = useMemo(() => new THREE.Vector3(0, TREE_HEIGHT/2 + 0.5, 0), []);


  useFrame((state) => {

    if(!mesh.current) return;

    const time = state.clock.elapsedTime;

    const progress = syncData.value;

    const easeProgress = 1.0 - Math.pow(1.0 - progress, 3.0);

    

    mesh.current.position.lerpVectors(startPos, endPos, easeProgress);

    mesh.current.rotation.y = time * 0.5;

    mesh.current.rotation.z = Math.sin(time) * 0.1;

    const s = 1.0 + Math.sin(time * 2) * 0.1;

    mesh.current.scale.setScalar(s);

  });


  return <mesh ref={mesh} geometry={geometry} material={material} castShadow />;

};


const Overlay = ({ state, toggleState }) => {

  return (

    <div style={{

      position: 'absolute', top: 0, left: 0, width: '100%', height: '100%',

      pointerEvents: 'none', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', zIndex: 10

    }}>

      <div style={{ padding: '40px', width: '100%', display: 'flex', justifyContent: window.innerWidth < 768 ? 'flex-start' : 'center' }}>

        <h1 style={{

          margin: 0, fontSize: 'clamp(2rem, 5vw, 4rem)', letterSpacing: '0.1em', textTransform: 'uppercase',

          textShadow: '0 0 20px rgba(255, 215, 0, 0.5)', background: 'linear-gradient(to bottom, #FFD700, #C5A059)',

          WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent'

        }}>

          Merry Christmas

        </h1>

      </div>

      <div style={{ padding: '40px', display: 'flex', justifyContent: 'center', pointerEvents: 'auto', marginBottom: '40px' }}>

        <button onClick={toggleState} style={{

            background: 'transparent', border: '1px solid #FFD700', color: '#FFD700', padding: '15px 40px',

            fontSize: '1rem', fontFamily: 'Cinzel, serif', cursor: 'pointer', transition: 'all 0.3s ease',

            textTransform: 'uppercase', letterSpacing: '2px', backdropFilter: 'blur(5px)'

          }}

          onMouseEnter={(e) => { e.currentTarget.style.background = '#FFD700'; e.currentTarget.style.color = '#020504'; e.currentTarget.style.boxShadow = '0 0 30px rgba(255, 215, 0, 0.4)'; }}

          onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#FFD700'; e.currentTarget.style.boxShadow = 'none'; }}

        >

          {state ? 'Scatter' : 'Reform Tree'}

        </button>

      </div>

      <div style={{ position: 'absolute', bottom: '20px', width: '100%', textAlign: 'center', color: '#E8E8E8', fontSize: '0.9rem', opacity: 0.8 }}>

        @cider, 2025

      </div>

    </div>

  );

};


const Background = () => {

  return (

    <mesh scale={100}>

      <sphereGeometry />

      <meshBasicMaterial color="#020504" side={THREE.BackSide} />

    </mesh>

  );

};


const Scene = ({ isTree, syncData }) => {

  const controlsRef = useRef(null);


  useFrame((state, delta) => {

    const target = isTree ? 1 : 0;

    syncData.value = THREE.MathUtils.damp(syncData.value, target, 1.5, delta);


    if (controlsRef.current) {

      if (syncData.hasHand) {

        controlsRef.current.autoRotate = false;

        const targetAzimuth = (syncData.handX - 0.5) * Math.PI; 

        const targetPolar = Math.PI/4 + syncData.handY * (Math.PI/1.5 - Math.PI/4);

        

        const currentAzimuth = controlsRef.current.getAzimuthalAngle();

        const currentPolar = controlsRef.current.getPolarAngle();

        

        const nextAzimuth = THREE.MathUtils.lerp(currentAzimuth, targetAzimuth, 0.05);

        const nextPolar = THREE.MathUtils.lerp(currentPolar, targetPolar, 0.05);

        

        controlsRef.current.setAzimuthalAngle(nextAzimuth);

        controlsRef.current.setPolarAngle(nextPolar);

        

      } else {

        controlsRef.current.autoRotate = isTree;

      }

      controlsRef.current.update();

    }

  });


  return (

    <>

      <PerspectiveCamera makeDefault position={[0, 0, 20]} fov={45} />

      <OrbitControls ref={controlsRef} enablePan={false} minPolarAngle={Math.PI / 4} maxPolarAngle={Math.PI / 1.5} minDistance={10} maxDistance={40} autoRotate={isTree} autoRotateSpeed={0.5} />

      

      <ambientLight intensity={0.8} />

      <directionalLight position={[10, 10, 5]} intensity={4} color="#FFD700" castShadow />

      <spotLight position={[-10, 20, -5]} intensity={5} color="#4fb" angle={0.5} penumbra={1} />

      <spotLight position={[0, 10, -20]} intensity={8} color="#FFD700" distance={50} />


      <group position={[0, -2, 0]}>

         <Foliage syncData={syncData} />

         <Ornaments syncData={syncData} />

      </group>


      <Background />


      <EffectComposer disableNormalPass>

        <Bloom luminanceThreshold={0.5} mipmapBlur intensity={1.5} radius={0.6} />

        <Noise opacity={0.05} />

        <Vignette eskil={false} offset={0.1} darkness={1.1} />

      </EffectComposer>

    </>

  );

};


function App() {

  const [isTree, setIsTree] = useState(false);

  const syncData = useMemo(() => ({ value: 0, handX: 0.5, handY: 0.5, hasHand: false }), []);


  useEffect(() => {

    // Hide loader when React mounts

    const loader = document.getElementById('loader');

    if(loader) loader.style.opacity = '0';

  }, []);


  return (

    <>

      <GestureController setTreeState={setIsTree} syncData={syncData} />

      <Canvas shadows dpr={[1, 2]} gl={{ antialias: false, toneMapping: THREE.ReinhardToneMapping, toneMappingExposure: 2.5 }}>

        <Scene isTree={isTree} syncData={syncData} />

      </Canvas>

      <Overlay state={isTree} toggleState={() => setIsTree(!isTree)} />

    </>

  );

}


const rootElement = document.getElementById('root');

const root = createRoot(rootElement);

root.render(<App />);

    </script>

</body>

</html>