← back to Model Arena

data/artifacts/086d9b64d97d/claude-code.html

326 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aqua Vitae — The Fountain</title>
<style>
  :root{
    --bg:#0d0d0f; --surface:#17171c; --text:#f5f2ea; --muted:#8a8578;
    --accent:#c9a961; --accent-deep:#7d6a45;
  }
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{height:100%;overflow:hidden;background:var(--bg)}
  body{
    font-family:-apple-system,'Segoe UI','Helvetica Neue',Arial,sans-serif;
    color:var(--text);
  }
  #stage{position:fixed;inset:0;display:block;cursor:grab}
  #stage:active{cursor:grabbing}

  /* Ambient drifting glow behind everything */
  .ambient{
    position:fixed;inset:-20%;z-index:0;pointer-events:none;
    background:radial-gradient(60% 55% at 50% 62%, rgba(201,169,97,.10), transparent 60%),
               radial-gradient(50% 50% at 50% 40%, rgba(120,150,190,.08), transparent 65%);
    animation:drift 18s ease-in-out infinite alternate;
  }
  @keyframes drift{from{transform:translateY(-1.5%) scale(1)}to{transform:translateY(1.5%) scale(1.05)}}

  .overlay{position:fixed;inset:0;z-index:3;pointer-events:none}

  .titlecard{
    position:absolute;top:clamp(24px,6vh,64px);left:clamp(24px,5vw,64px);
    max-width:min(88vw,520px);
    animation:fadeUp .9s cubic-bezier(.22,1,.36,1) both;
  }
  .label{
    font-size:12px;letter-spacing:.15em;text-transform:uppercase;
    color:var(--muted);margin-bottom:16px;
    animation:fadeUp .9s cubic-bezier(.22,1,.36,1) .09s both;
  }
  .label .dot{color:var(--accent)}
  h1{
    font-family:'Didot','Bodoni MT','Playfair Display',Georgia,serif;
    font-weight:500;font-size:clamp(40px,7.4vw,67px);line-height:1.02;
    letter-spacing:.01em;
    animation:fadeUp .9s cubic-bezier(.22,1,.36,1) .18s both;
  }
  h1 em{font-style:italic;color:var(--accent)}
  .tagline{
    margin-top:20px;font-size:clamp(15px,2.2vw,21px);line-height:1.5;
    color:rgba(245,242,234,.72);max-width:42ch;font-weight:300;
    animation:fadeUp .9s cubic-bezier(.22,1,.36,1) .27s both;
  }
  .hint{
    position:absolute;bottom:clamp(20px,4vh,40px);left:50%;transform:translateX(-50%);
    font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:var(--muted);
    animation:fadeUp .9s cubic-bezier(.22,1,.36,1) .5s both;white-space:nowrap;
  }
  .hint b{color:var(--accent);font-weight:600}

  /* soft edge vignette */
  .vignette{
    position:fixed;inset:0;z-index:2;pointer-events:none;
    background:radial-gradient(120% 100% at 50% 46%, transparent 55%, rgba(0,0,0,.55) 100%);
  }
  @keyframes fadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}
  @media (max-width:640px){ .tagline{display:none} }
</style>
</head>
<body>
  <div class="ambient"></div>
  <canvas id="stage"></canvas>
  <div class="vignette"></div>
  <div class="overlay">
    <div class="titlecard">
      <div class="label"><span class="dot">◆</span>&nbsp;&nbsp;Interactive · Real-Time Water</div>
      <h1>Aqua <em>Vitae</em></h1>
      <p class="tagline">A tiered marble fountain rendered from a thousand droplets in motion — light, gravity, and stillness in a single breath.</p>
    </div>
    <div class="hint">Drag to orbit &nbsp;·&nbsp; Scroll to zoom &nbsp;·&nbsp; <b>Water never rests</b></div>
  </div>

<script>
(function(){
  "use strict";
  const canvas = document.getElementById('stage');
  const ctx = canvas.getContext('2d');
  let W=0,H=0,DPR=1,cx=0,cy=0;

  function resize(){
    DPR = Math.min(window.devicePixelRatio||1, 2);
    W = window.innerWidth; H = window.innerHeight;
    canvas.width = W*DPR; canvas.height = H*DPR;
    canvas.style.width = W+'px'; canvas.style.height = H+'px';
    ctx.setTransform(DPR,0,0,DPR,0,0);
    cx = W/2; cy = H*0.52;
  }
  window.addEventListener('resize', resize); resize();

  // ---------- Camera ----------
  let yaw = 0.6, pitch = -0.42, camDist = 940, focal = 820;
  let autoYaw = true, dragging=false, lastX=0, lastY=0;

  canvas.addEventListener('mousedown', e=>{dragging=true;autoYaw=false;lastX=e.clientX;lastY=e.clientY;});
  window.addEventListener('mouseup', ()=>dragging=false);
  window.addEventListener('mousemove', e=>{
    if(!dragging) return;
    yaw += (e.clientX-lastX)*0.006;
    pitch = Math.max(-1.15, Math.min(-0.05, pitch + (e.clientY-lastY)*0.004));
    lastX=e.clientX; lastY=e.clientY;
  });
  canvas.addEventListener('touchstart', e=>{dragging=true;autoYaw=false;const t=e.touches[0];lastX=t.clientX;lastY=t.clientY;},{passive:true});
  window.addEventListener('touchend', ()=>dragging=false);
  window.addEventListener('touchmove', e=>{
    if(!dragging)return; const t=e.touches[0];
    yaw += (t.clientX-lastX)*0.006;
    pitch = Math.max(-1.15, Math.min(-0.05, pitch + (t.clientY-lastY)*0.004));
    lastX=t.clientX; lastY=t.clientY;
  },{passive:true});
  canvas.addEventListener('wheel', e=>{
    e.preventDefault();
    camDist = Math.max(520, Math.min(1500, camDist + e.deltaY*0.7));
  },{passive:false});

  // ---------- Projection ----------
  function project(x,y,z){
    const cY=Math.cos(yaw), sY=Math.sin(yaw);
    let rx = x*cY - z*sY;
    let rz = x*sY + z*cY;
    const cP=Math.cos(pitch), sP=Math.sin(pitch);
    let ry = y*cP - rz*sP;
    let rz2 = y*sP + rz*cP;
    const depth = rz2 + camDist;
    const s = focal/depth;
    return {x: cx + rx*s, y: cy - ry*s, s, depth};
  }

  function circlePts(radius,y,n){
    const pts=[];
    for(let i=0;i<n;i++){const a=i/n*Math.PI*2; pts.push(project(Math.cos(a)*radius, y, Math.sin(a)*radius));}
    return pts;
  }
  function tracePath(pts){
    ctx.beginPath();
    for(let i=0;i<pts.length;i++){ const p=pts[i]; i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y); }
    ctx.closePath();
  }

  // ---------- Fountain geometry ----------
  const POOL_R = 230;
  const ringCount = 12;
  const emitters = []; // 0 = center, 1..ring
  emitters.push({type:'center'});
  for(let i=0;i<ringCount;i++) emitters.push({type:'ring', a:i/ringCount*Math.PI*2});

  const rand=(a,b)=>a+Math.random()*(b-a);
  const g = -560;

  // ---------- Particles ----------
  const N = 1400;
  const P = [];
  function emit(p){
    if(p.em===0){ // central column
      p.x=rand(-4,4); p.z=rand(-4,4); p.y=176+rand(0,6);
      const ang=Math.random()*Math.PI*2, spread=rand(0,24);
      p.vx=Math.cos(ang)*spread; p.vz=Math.sin(ang)*spread;
      p.vy=rand(330,395);
      p.size=rand(2.4,4.8); p.core=Math.random()<0.35;
    } else { // arcing ring jets
      const a=emitters[p.em].a, rr=72;
      p.x=Math.cos(a)*rr; p.z=Math.sin(a)*rr; p.y=176;
      const out=rand(80,102), tang=rand(-22,22), up=rand(268,318);
      p.vx=Math.cos(a)*out - Math.sin(a)*tang;
      p.vz=Math.sin(a)*out + Math.cos(a)*tang;
      p.vy=up;
      p.size=rand(2,3.4); p.core=Math.random()<0.2;
    }
    p.life=0; p.maxLife=rand(2.2,3.6);
  }
  for(let i=0;i<N;i++){
    const p={em: Math.random()<0.42 ? 0 : (1+((i)%ringCount))};
    emit(p);
    p.life=Math.random()*p.maxLife; // stagger
    p.y = 176 + Math.random()*120; p.vy = rand(-40,300);
    P.push(p);
  }

  // ---------- Ripples ----------
  const ripples=[];
  function addRipple(x,z){
    if(ripples.length>60) return;
    ripples.push({x,z,r:rand(4,10),maxR:rand(38,80),a:0.55});
  }

  // ---------- Update ----------
  function update(dt){
    for(let i=0;i<P.length;i++){
      const p=P[i];
      p.vy += g*dt;
      p.x += p.vx*dt; p.y += p.vy*dt; p.z += p.vz*dt; p.life += dt;
      if(p.y<=0 && p.vy<0){
        if(Math.random()<0.04){ const rr=Math.hypot(p.x,p.z); if(rr<POOL_R) addRipple(p.x,p.z); }
        emit(p);
      } else if(p.life>p.maxLife){
        emit(p);
      }
    }
    for(let i=ripples.length-1;i>=0;i--){
      const r=ripples[i];
      r.r += 34*dt; r.a -= 0.55*dt;
      if(r.a<=0 || r.r>r.maxR) ripples.splice(i,1);
    }
  }

  // ---------- Render helpers ----------
  function drawStructure(){
    const mainScale = focal/(camDist);
    // POOL (water surface at y=0)
    let pts = circlePts(POOL_R,0,64);
    let ax=0,ay=0; pts.forEach(p=>{ax+=p.x;ay+=p.y;}); ax/=pts.length; ay/=pts.length;
    let rgrad = ctx.createRadialGradient(ax,ay-8,12, ax,ay, POOL_R*mainScale*1.15);
    rgrad.addColorStop(0,'rgba(38,58,74,0.95)');
    rgrad.addColorStop(0.55,'rgba(20,32,44,0.95)');
    rgrad.addColorStop(1,'rgba(10,14,20,0.98)');
    tracePath(pts); ctx.fillStyle=rgrad; ctx.fill();
    // rim
    ctx.lineWidth=Math.max(1.5, 5*mainScale);
    ctx.strokeStyle='rgba(201,169,97,0.55)'; ctx.stroke();
    ctx.lineWidth=Math.max(1,2*mainScale);
    ctx.strokeStyle='rgba(245,242,234,0.10)';
    tracePath(circlePts(POOL_R-8,0,64)); ctx.stroke();

    // ripples on pool
    ctx.lineWidth=Math.max(0.8,1.6*mainScale);
    for(const r of ripples){
      const rp=[];
      for(let i=0;i<28;i++){const a=i/28*Math.PI*2; rp.push(project(r.x+Math.cos(a)*r.r, 0, r.z+Math.sin(a)*r.r));}
      tracePath(rp);
      ctx.strokeStyle='rgba(190,220,240,'+Math.max(0,r.a).toFixed(3)+')';
      ctx.stroke();
    }

    // Pedestal base
    fillTier(58, 0, 20, 'rgba(30,30,34,0.95)','rgba(201,169,97,0.35)', mainScale);
    fillTier(46, 20, 96,'rgba(24,24,28,0.95)','rgba(201,169,97,0.30)', mainScale);
    // Upper bowl (catches the arcs)
    let bowl = circlePts(104,150,48);
    let bx=0,by=0; bowl.forEach(p=>{bx+=p.x;by+=p.y;}); bx/=bowl.length; by/=bowl.length;
    let bg=ctx.createRadialGradient(bx,by-6,8,bx,by,104*mainScale*1.2);
    bg.addColorStop(0,'rgba(46,62,76,0.95)');
    bg.addColorStop(1,'rgba(16,22,30,0.96)');
    tracePath(bowl); ctx.fillStyle=bg; ctx.fill();
    ctx.lineWidth=Math.max(1.4,4*mainScale); ctx.strokeStyle='rgba(201,169,97,0.55)'; ctx.stroke();
    // stem to nozzle
    fillTier(16,150,176,'rgba(30,30,34,0.95)','rgba(201,169,97,0.30)', mainScale);
  }

  function fillTier(radius,y0,y1,fill,stroke,ms){
    const top=circlePts(radius,y1,40), bot=circlePts(radius,y0,40);
    // side wall as ribbon
    ctx.beginPath();
    for(let i=0;i<=top.length;i++){const p=top[i%top.length]; i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y);}
    for(let i=top.length;i>=0;i--){const p=bot[i%bot.length]; ctx.lineTo(p.x,p.y);}
    ctx.closePath(); ctx.fillStyle=fill; ctx.fill();
    tracePath(top); ctx.fillStyle=fill; ctx.fill();
    ctx.lineWidth=Math.max(1,3*ms); ctx.strokeStyle=stroke; ctx.stroke();
  }

  function drawParticles(){
    ctx.globalCompositeOperation='lighter';
    for(let i=0;i<P.length;i++){
      const p=P[i];
      const pr=project(p.x,p.y,p.z);
      if(pr.depth<=1) continue;
      const t=Math.max(0,Math.min(1,p.y/300));
      let a = 0.45;
      // fade near end of life
      const lifeLeft=(p.maxLife-p.life);
      if(lifeLeft<0.4) a*= lifeLeft/0.4;
      a *= Math.min(1, pr.s*1.2);
      if(a<=0.01) continue;
      const size=Math.max(0.6, p.size*pr.s);
      const rr = Math.round(140+ t*95);
      const gg = Math.round(185+ t*60);
      const bb = Math.round(220+ t*35);
      const grd=ctx.createRadialGradient(pr.x,pr.y,0,pr.x,pr.y,size*2.6);
      grd.addColorStop(0,'rgba('+rr+','+gg+','+bb+','+(a).toFixed(3)+')');
      grd.addColorStop(1,'rgba('+rr+','+gg+','+bb+',0)');
      ctx.fillStyle=grd;
      ctx.beginPath(); ctx.arc(pr.x,pr.y,size*2.6,0,Math.PI*2); ctx.fill();
      if(p.core){
        ctx.fillStyle='rgba(255,255,255,'+(a*0.9).toFixed(3)+')';
        ctx.beginPath(); ctx.arc(pr.x,pr.y,size*0.6,0,Math.PI*2); ctx.fill();
      }
    }
    ctx.globalCompositeOperation='source-over';
  }

  function drawBackground(){
    const bg=ctx.createLinearGradient(0,0,0,H);
    bg.addColorStop(0,'#101015');
    bg.addColorStop(0.5,'#0d0d0f');
    bg.addColorStop(1,'#070708');
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
  }

  // ---------- Loop ----------
  let last=performance.now();
  function frame(now){
    let dt=(now-last)/1000; last=now;
    if(dt>0.05) dt=0.05;
    if(autoYaw) yaw += dt*0.12;
    // step physics a couple substeps for stability
    update(dt*0.5); update(dt*0.5);

    drawBackground();
    drawStructure();
    drawParticles();
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>