← 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>