← back to Model Arena
data/artifacts/02a362bd0b06/grok.html
627 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon Realm</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a12;cursor:crosshair}
canvas{display:block;width:100%;height:100%}
#hint{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);color:rgba(255,220,180,0.7);font:14px/1.4 Georgia,serif;letter-spacing:0.12em;text-transform:uppercase;pointer-events:none;text-shadow:0 0 12px rgba(255,140,60,0.5);transition:opacity 1.5s;z-index:10}
#hint.hide{opacity:0}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hint">Click to summon dragonfire</div>
<script>
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
let W,H,dpr;
function resize(){
dpr=Math.min(window.devicePixelRatio||1,2);
W=window.innerWidth;H=window.innerHeight;
canvas.width=W*dpr;canvas.height=H*dpr;
ctx.setTransform(dpr,0,0,dpr,0,0);
}
resize();
window.addEventListener('resize',resize);
// ─── Utilities ───
const rand=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const lerp=(a,b,t)=>a+(b-a)*t;
// ─── Stars ───
const stars=Array.from({length:180},()=>({
x:Math.random(),y:Math.random()*0.7,
r:rand(0.4,1.8),ph:Math.random()*Math.PI*2,
sp:rand(0.5,2.5),b:rand(0.4,1)
}));
// ─── Snow ───
const snow=Array.from({length:220},()=>({
x:Math.random(),y:Math.random(),
r:rand(0.8,3.2),sp:rand(0.15,0.7),
drift:rand(-0.15,0.15),ph:Math.random()*Math.PI*2,
a:rand(0.35,0.9)
}));
// ─── Fire particles ───
let flames=[];
// ─── Mountains (layered silhouettes) ───
function genMountains(baseY,amp,freq,rough,seed){
const pts=[];
let x=0;
let y=baseY;
let s=seed;
while(x<=1.05){
s=Math.sin(s*127.1+311.7)*43758.5453;
const n=s-Math.floor(s);
y=baseY-Math.abs(Math.sin(x*freq*Math.PI*2+seed))*amp
-n*rough*amp
-Math.sin(x*freq*3.7+seed*2)*amp*0.25;
pts.push({x,y});
x+=0.012+n*0.008;
}
return pts;
}
const mtnFar=genMountains(0.58,0.14,1.8,0.35,1.1);
const mtnMid=genMountains(0.68,0.20,2.4,0.45,2.7);
const mtnNear=genMountains(0.82,0.22,3.1,0.55,4.3);
function drawMountains(pts,fill,glow){
ctx.beginPath();
ctx.moveTo(0,H);
for(const p of pts)ctx.lineTo(p.x*W,p.y*H);
ctx.lineTo(W,H);
ctx.closePath();
if(glow){
ctx.shadowColor=glow;
ctx.shadowBlur=30;
}
ctx.fillStyle=fill;
ctx.fill();
ctx.shadowBlur=0;
}
// ─── Aurora ribbons ───
function drawAurora(t){
ctx.save();
ctx.globalCompositeOperation='screen';
for(let i=0;i<4;i++){
const y0=H*0.08+i*H*0.06;
const grad=ctx.createLinearGradient(0,y0,W,y0+H*0.15);
const hue=160+i*25+Math.sin(t*0.3+i)*20;
grad.addColorStop(0,`hsla(${hue},80%,55%,0)`);
grad.addColorStop(0.3,`hsla(${hue},90%,60%,${0.08+Math.sin(t*0.5+i)*0.04})`);
grad.addColorStop(0.55,`hsla(${hue+30},85%,65%,${0.12+Math.sin(t*0.4+i*1.3)*0.05})`);
grad.addColorStop(0.8,`hsla(${hue+50},80%,55%,0.06)`);
grad.addColorStop(1,`hsla(${hue},70%,50%,0)`);
ctx.fillStyle=grad;
ctx.beginPath();
ctx.moveTo(0,y0);
for(let x=0;x<=W;x+=8){
const nx=x/W;
const y=y0
+Math.sin(nx*4+t*0.4+i)*H*0.04
+Math.sin(nx*9+t*0.7+i*2)*H*0.02
+Math.sin(nx*2.5-t*0.25)*H*0.03;
ctx.lineTo(x,y);
}
for(let x=W;x>=0;x-=8){
const nx=x/W;
const y=y0+H*0.12
+Math.sin(nx*3.5+t*0.35+i)*H*0.05
+Math.cos(nx*7+t*0.55)*H*0.025;
ctx.lineTo(x,y);
}
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
// ─── Moon ───
function drawMoon(t){
const mx=W*0.78,my=H*0.16,r=Math.min(W,H)*0.055;
// glow
const g=ctx.createRadialGradient(mx,my,r*0.3,mx,my,r*4);
g.addColorStop(0,'rgba(255,240,210,0.25)');
g.addColorStop(0.4,'rgba(200,210,255,0.08)');
g.addColorStop(1,'rgba(180,200,255,0)');
ctx.fillStyle=g;
ctx.beginPath();ctx.arc(mx,my,r*4,0,Math.PI*2);ctx.fill();
// body
const mg=ctx.createRadialGradient(mx-r*0.3,my-r*0.3,r*0.1,mx,my,r);
mg.addColorStop(0,'#fff8e8');
mg.addColorStop(0.6,'#e8e0d0');
mg.addColorStop(1,'#c8c0b0');
ctx.fillStyle=mg;
ctx.beginPath();ctx.arc(mx,my,r,0,Math.PI*2);ctx.fill();
// craters
ctx.fillStyle='rgba(160,150,140,0.25)';
[[0.3,-0.2,0.18],[ -0.25,0.25,0.12],[0.1,0.35,0.1],[-0.35,-0.15,0.08]].forEach(([cx,cy,cr])=>{
ctx.beginPath();ctx.arc(mx+cx*r,my+cy*r,cr*r,0,Math.PI*2);ctx.fill();
});
}
// ─── Dragon ───
const dragon={
x:0.2,y:0.38,
vx:0.00035,phase:0,
wing:0,breathing:0,breathT:0,
scale:1,dir:1
};
function dragonWorld(){
return {
x:dragon.x*W,
y:dragon.y*H+Math.sin(dragon.phase)*H*0.03,
s:Math.min(W,H)*0.0011*dragon.scale
};
}
function drawDragonBody(t){
const {x,y,s}=dragonWorld();
const wingAng=Math.sin(dragon.wing)*0.7;
const wingAng2=Math.sin(dragon.wing+0.4)*0.5;
ctx.save();
ctx.translate(x,y);
ctx.scale(dragon.dir*s,s);
// tail
ctx.strokeStyle='#1a3a2a';
ctx.lineWidth=14;
ctx.lineCap='round';
ctx.beginPath();
ctx.moveTo(-40,10);
for(let i=1;i<=12;i++){
const tx=-40-i*22;
const ty=10+Math.sin(dragon.phase*2+i*0.55)*18*i*0.12+i*i*0.15;
ctx.lineTo(tx,ty);
}
ctx.stroke();
// tail spikes
ctx.fillStyle='#2a5a3a';
for(let i=2;i<=11;i+=2){
const tx=-40-i*22;
const ty=10+Math.sin(dragon.phase*2+i*0.55)*18*i*0.12+i*i*0.15;
const nx=-22,ny=Math.sin(dragon.phase*2+(i+1)*0.55)*18*(i+1)*0.12+(i+1)*(i+1)*0.15
-(Math.sin(dragon.phase*2+i*0.55)*18*i*0.12+i*i*0.15);
ctx.beginPath();
ctx.moveTo(tx,ty-8);
ctx.lineTo(tx-8,ty-28);
ctx.lineTo(tx+nx*0.3,ty+ny*0.3);
ctx.fill();
}
// tail tip flame-ish glow when breathing
if(dragon.breathing>0){
const tipX=-40-12*22,tipY=10+Math.sin(dragon.phase*2+12*0.55)*18*12*0.12+144*0.15;
ctx.fillStyle=`rgba(255,120,40,${dragon.breathing*0.3})`;
ctx.beginPath();ctx.arc(tipX,tipY,20,0,Math.PI*2);ctx.fill();
}
// back wing (far)
ctx.save();
ctx.translate(-10,-5);
ctx.rotate(-wingAng2*0.8-0.3);
drawWing(-1);
ctx.restore();
// body
const bodyGrad=ctx.createLinearGradient(0,-40,0,40);
bodyGrad.addColorStop(0,'#3d8b5e');
bodyGrad.addColorStop(0.4,'#2a6b45');
bodyGrad.addColorStop(1,'#1a3a28');
ctx.fillStyle=bodyGrad;
ctx.beginPath();
ctx.ellipse(10,5,55,28, -0.15,0,Math.PI*2);
ctx.fill();
// belly
ctx.fillStyle='#4a7a55';
ctx.beginPath();
ctx.ellipse(15,14,40,16,-0.15,0,Math.PI*2);
ctx.fill();
// scales hint
ctx.strokeStyle='rgba(20,60,35,0.35)';
ctx.lineWidth=1.5;
for(let i=-2;i<=3;i++){
ctx.beginPath();
ctx.arc(i*14,0,10,0.2,Math.PI-0.2);
ctx.stroke();
}
// neck
ctx.fillStyle='#2d7048';
ctx.beginPath();
ctx.moveTo(50,-5);
ctx.quadraticCurveTo(85,-35,105,-28);
ctx.quadraticCurveTo(115,-20,110,-5);
ctx.quadraticCurveTo(90,10,55,15);
ctx.closePath();
ctx.fill();
// head
ctx.fillStyle='#348050';
ctx.beginPath();
ctx.ellipse(125,-22,28,18,-0.2,0,Math.PI*2);
ctx.fill();
// snout
ctx.beginPath();
ctx.moveTo(140,-28);
ctx.quadraticCurveTo(175,-30,180,-18);
ctx.quadraticCurveTo(175,-8,145,-10);
ctx.closePath();
ctx.fill();
// jaw
const jawOpen=dragon.breathing*0.35;
ctx.fillStyle='#2a6040';
ctx.beginPath();
ctx.moveTo(145,-12);
ctx.quadraticCurveTo(170,-8+jawOpen*40,178,-14+jawOpen*30);
ctx.quadraticCurveTo(165,0+jawOpen*20,140,-5);
ctx.closePath();
ctx.fill();
// horns
ctx.fillStyle='#1a3020';
ctx.beginPath();
ctx.moveTo(115,-35);
ctx.quadraticCurveTo(108,-60,100,-72);
ctx.quadraticCurveTo(112,-55,122,-36);
ctx.fill();
ctx.beginPath();
ctx.moveTo(128,-38);
ctx.quadraticCurveTo(130,-65,125,-80);
ctx.quadraticCurveTo(138,-60,135,-36);
ctx.fill();
// eye
ctx.fillStyle='#1a1008';
ctx.beginPath();ctx.ellipse(135,-26,6,5,0,0,Math.PI*2);ctx.fill();
const eyeGlow=0.5+dragon.breathing*0.5;
ctx.fillStyle=`rgba(255,200,50,${eyeGlow})`;
ctx.shadowColor='#ffaa00';
ctx.shadowBlur=8+dragon.breathing*12;
ctx.beginPath();ctx.ellipse(136,-26,3.5,4,0,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
// nostril
ctx.fillStyle='#1a3020';
ctx.beginPath();ctx.arc(168,-22,3,0,Math.PI*2);ctx.fill();
// front wing
ctx.save();
ctx.translate(5,-10);
ctx.rotate(-wingAng-0.2);
drawWing(1);
ctx.restore();
// legs
ctx.strokeStyle='#1a3a28';
ctx.lineWidth=8;
ctx.lineCap='round';
// rear
ctx.beginPath();
ctx.moveTo(-15,20);
ctx.quadraticCurveTo(-20,45,-10,55+Math.sin(dragon.phase*1.5)*4);
ctx.stroke();
// front
ctx.beginPath();
ctx.moveTo(30,22);
ctx.quadraticCurveTo(40,48,35,58+Math.sin(dragon.phase*1.5+1)*4);
ctx.stroke();
// spine ridges
ctx.fillStyle='#1e4a32';
for(let i=-1;i<=3;i++){
ctx.beginPath();
ctx.moveTo(i*16-5,-18);
ctx.lineTo(i*16,-38-Math.abs(i)*2);
ctx.lineTo(i*16+8,-16);
ctx.fill();
}
ctx.restore();
// fire breath origin in world coords for particles
return {
fx:x+dragon.dir*s*(180),
fy:y+s*(-18+jawOpen*10),
dir:dragon.dir
};
}
function drawWing(side){
// membrane
const wg=ctx.createLinearGradient(0,0,0,120);
wg.addColorStop(0,'rgba(60,120,80,0.85)');
wg.addColorStop(0.5,'rgba(30,70,50,0.75)');
wg.addColorStop(1,'rgba(15,40,30,0.5)');
ctx.fillStyle=wg;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.quadraticCurveTo(40*side,-30,90*side,-10);
ctx.quadraticCurveTo(120*side,30,100*side,90);
ctx.quadraticCurveTo(60*side,70,30*side,100);
ctx.quadraticCurveTo(20*side,50,0,10);
ctx.closePath();
ctx.fill();
// bone structure
ctx.strokeStyle='rgba(20,50,30,0.7)';
ctx.lineWidth=3;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(90*side,-10);
ctx.moveTo(0,0);
ctx.lineTo(100*side,90);
ctx.moveTo(0,0);
ctx.lineTo(30*side,100);
ctx.moveTo(40*side,-5);
ctx.lineTo(70*side,50);
ctx.stroke();
// membrane glow edge
ctx.strokeStyle='rgba(100,200,140,0.25)';
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(90*side,-10);
ctx.quadraticCurveTo(120*side,30,100*side,90);
ctx.quadraticCurveTo(60*side,70,30*side,100);
ctx.stroke();
}
// ─── Fire system ───
function spawnFire(fx,fy,dir){
const n=18+Math.floor(Math.random()*12);
for(let i=0;i<n;i++){
const speed=rand(3,11);
const spread=rand(-0.35,0.35);
flames.push({
x:fx,y:fy,
vx:dir*speed*Math.cos(spread)+rand(-0.5,0.5),
vy:speed*Math.sin(spread)*0.6+rand(-1.5,0.5),
life:1,
decay:rand(0.012,0.028),
r:rand(4,16),
hue:rand(15,45),
type:Math.random()<0.3?1:0
});
}
// smoke
for(let i=0;i<4;i++){
flames.push({
x:fx,y:fy,
vx:dir*rand(1,3),
vy:rand(-2,-0.5),
life:1,
decay:rand(0.008,0.015),
r:rand(10,25),
hue:0,
type:2
});
}
}
function updateFlames(){
for(let i=flames.length-1;i>=0;i--){
const f=flames[i];
f.x+=f.vx;
f.y+=f.vy;
f.vy-=0.04;
f.vx*=0.99;
f.life-=f.decay;
f.r*=0.985;
if(f.life<=0||f.r<0.5)flames.splice(i,1);
}
}
function drawFlames(){
ctx.save();
ctx.globalCompositeOperation='lighter';
for(const f of flames){
if(f.type===2){
ctx.globalCompositeOperation='source-over';
ctx.fillStyle=`rgba(60,55,50,${f.life*0.2})`;
ctx.beginPath();ctx.arc(f.x,f.y,f.r*(2-f.life),0,Math.PI*2);ctx.fill();
ctx.globalCompositeOperation='lighter';
continue;
}
const a=f.life;
const g=ctx.createRadialGradient(f.x,f.y,0,f.x,f.y,f.r);
if(f.type===1){
g.addColorStop(0,`rgba(255,255,220,${a})`);
g.addColorStop(0.3,`rgba(255,200,50,${a*0.8})`);
g.addColorStop(0.7,`rgba(255,100,20,${a*0.4})`);
g.addColorStop(1,`rgba(255,40,0,0)`);
}else{
g.addColorStop(0,`rgba(255,240,180,${a*0.9})`);
g.addColorStop(0.25,`hsla(${f.hue},100%,55%,${a*0.7})`);
g.addColorStop(0.6,`hsla(${f.hue-10},100%,40%,${a*0.35})`);
g.addColorStop(1,`hsla(${f.hue},100%,30%,0)`);
}
ctx.fillStyle=g;
ctx.beginPath();ctx.arc(f.x,f.y,f.r,0,Math.PI*2);ctx.fill();
}
ctx.restore();
}
// ─── Ground mist ───
function drawMist(t){
ctx.save();
ctx.globalCompositeOperation='screen';
for(let i=0;i<5;i++){
const gy=H*0.75+i*H*0.05;
const gg=ctx.createLinearGradient(0,gy-40,0,gy+60);
gg.addColorStop(0,'rgba(100,140,180,0)');
gg.addColorStop(0.5,`rgba(120,160,200,${0.04+Math.sin(t*0.3+i)*0.02})`);
gg.addColorStop(1,'rgba(80,120,160,0)');
ctx.fillStyle=gg;
ctx.beginPath();
ctx.moveTo(0,gy);
for(let x=0;x<=W;x+=20){
ctx.lineTo(x,gy+Math.sin(x*0.01+t*0.4+i*1.5)*15+Math.sin(x*0.003-t*0.2)*25);
}
ctx.lineTo(W,H);ctx.lineTo(0,H);ctx.closePath();
ctx.fill();
}
ctx.restore();
}
// ─── Lighting overlay ───
let fireLight=0;
function drawLighting(t,mouth){
// ambient night vignette
const vig=ctx.createRadialGradient(W*0.5,H*0.3,H*0.1,W*0.5,H*0.4,H*0.9);
vig.addColorStop(0,'rgba(0,0,0,0)');
vig.addColorStop(0.7,'rgba(0,0,10,0.15)');
vig.addColorStop(1,'rgba(0,0,20,0.55)');
ctx.fillStyle=vig;
ctx.fillRect(0,0,W,H);
// fire dynamic light
if(fireLight>0.01&&mouth){
const lg=ctx.createRadialGradient(mouth.fx,mouth.fy,10,mouth.fx,mouth.fy,280*fireLight);
lg.addColorStop(0,`rgba(255,180,60,${0.2*fireLight})`);
lg.addColorStop(0.3,`rgba(255,100,20,${0.1*fireLight})`);
lg.addColorStop(0.7,`rgba(255,50,0,${0.04*fireLight})`);
lg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=lg;
ctx.globalCompositeOperation='screen';
ctx.fillRect(0,0,W,H);
ctx.globalCompositeOperation='source-over';
}
}
// ─── Interaction ───
let hintHidden=false;
canvas.addEventListener('click',(e)=>{
dragon.breathing=1;
dragon.breathT=0;
if(!hintHidden){
document.getElementById('hint').classList.add('hide');
hintHidden=true;
}
});
canvas.addEventListener('touchstart',(e)=>{
e.preventDefault();
dragon.breathing=1;
dragon.breathT=0;
if(!hintHidden){
document.getElementById('hint').classList.add('hide');
hintHidden=true;
}
},{passive:false});
// ─── Main loop ───
let t0=performance.now();
function frame(now){
const t=(now-t0)/1000;
const dt=Math.min((now-(frame.prev||now))/1000,0.05);
frame.prev=now;
// sky
const sky=ctx.createLinearGradient(0,0,0,H);
sky.addColorStop(0,'#06061a');
sky.addColorStop(0.3,'#0c1030');
sky.addColorStop(0.55,'#152045');
sky.addColorStop(0.75,'#1a2840');
sky.addColorStop(1,'#0e1a28');
ctx.fillStyle=sky;
ctx.fillRect(0,0,W,H);
// stars
for(const s of stars){
const tw=0.5+0.5*Math.sin(t*s.sp+s.ph);
ctx.fillStyle=`rgba(220,230,255,${s.b*tw})`;
ctx.beginPath();
ctx.arc(s.x*W,s.y*H,s.r*(0.7+tw*0.3),0,Math.PI*2);
ctx.fill();
}
drawAurora(t);
drawMoon(t);
// far mountains
drawMountains(mtnFar,'#12182a','rgba(80,120,180,0.15)');
// mid
drawMountains(mtnMid,'#0e1520','rgba(60,100,140,0.1)');
// snow behind dragon
for(const p of snow){
if(p.r>2)continue;
p.y+=p.sp*dt*0.25;
p.x+=p.drift*dt*0.15+Math.sin(t*1.5+p.ph)*0.0003;
if(p.y>1){p.y=-0.02;p.x=Math.random()}
if(p.x<0)p.x+=1;if(p.x>1)p.x-=1;
ctx.fillStyle=`rgba(220,230,255,${p.a*0.5})`;
ctx.beginPath();ctx.arc(p.x*W,p.y*H,p.r,0,Math.PI*2);ctx.fill();
}
// dragon update
dragon.phase+=dt*1.4;
dragon.wing+=dt*5.5;
dragon.x+=dragon.vx*60*dt;
if(dragon.x>1.25){dragon.x=-0.25;dragon.y=rand(0.28,0.48)}
// gentle bob already in dragonWorld
if(dragon.breathing>0){
dragon.breathT+=dt;
dragon.breathing=Math.max(0,1-dragon.breathT/1.8);
fireLight=lerp(fireLight,dragon.breathing,0.15);
if(dragon.breathT<1.4){
const m=dragonWorld();
const jawOpen=dragon.breathing*0.35;
spawnFire(
m.x+dragon.dir*m.s*180,
m.y+m.s*(-18),
dragon.dir
);
}
}else{
fireLight=lerp(fireLight,0,0.08);
}
updateFlames();
const mouth=drawDragonBody(t);
drawFlames();
// near mountains
drawMountains(mtnNear,'#0a1018',null);
// foreground snow
for(const p of snow){
if(p.r<=2)continue;
p.y+=p.sp*dt*0.35;
p.x+=p.drift*dt*0.2+Math.sin(t*1.2+p.ph)*0.0004;
if(p.y>1){p.y=-0.02;p.x=Math.random()}
if(p.x<0)p.x+=1;if(p.x>1)p.x-=1;
ctx.fillStyle=`rgba(230,235,255,${p.a})`;
ctx.beginPath();ctx.arc(p.x*W,p.y*H,p.r,0,Math.PI*2);ctx.fill();
// sparkle
if(p.r>2.5&&Math.sin(t*3+p.ph)>0.7){
ctx.fillStyle=`rgba(255,255,255,${p.a*0.6})`;
ctx.fillRect(p.x*W-0.5,p.y*H-p.r*1.5,1,p.r*3);
ctx.fillRect(p.x*W-p.r*1.5,p.y*H-0.5,p.r*3,1);
}
}
drawMist(t);
drawLighting(t,mouth);
// snowy ground highlight on near peaks
ctx.strokeStyle='rgba(180,200,220,0.12)';
ctx.lineWidth=2;
ctx.beginPath();
let first=true;
for(const p of mtnNear){
if(first){ctx.moveTo(p.x*W,p.y*H);first=false}
else ctx.lineTo(p.x*W,p.y*H);
}
ctx.stroke();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>