← 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> 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 · Scroll to zoom · <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>