← back to Model Arena

data/artifacts/1d4944290382/claude-code.html

390 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tiny Isometric City Builder</title>
<style>
  :root{
    --bg1:#1b2440; --bg2:#0e1424; --panel:#141d33; --ink:#e8eefc;
    --muted:#8ea1c9; --accent:#66d0a5;
  }
  *{box-sizing:border-box;}
  html,body{height:100%;margin:0;}
  body{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    background:radial-gradient(1200px 700px at 50% -10%,var(--bg1),var(--bg2));
    color:var(--ink);
    display:flex;flex-direction:column;align-items:center;
    min-height:100%;padding:16px;
  }
  h1{
    font-size:20px;font-weight:700;letter-spacing:.3px;margin:2px 0 2px;
    display:flex;align-items:center;gap:8px;
  }
  h1 .dot{width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent);}
  .hint{color:var(--muted);font-size:13px;margin:0 0 12px;text-align:center;max-width:640px;line-height:1.4;}
  .stage{
    background:linear-gradient(180deg,#0f1830,#0b1122);
    border:1px solid rgba(255,255,255,.06);
    border-radius:16px;
    box-shadow:0 20px 60px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.04);
    padding:10px;
  }
  canvas{display:block;border-radius:10px;cursor:pointer;touch-action:none;}
  .controls{
    display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:center;
    margin-top:14px;
  }
  button{
    font:inherit;font-weight:600;color:var(--ink);
    background:linear-gradient(180deg,#26365e,#1b284a);
    border:1px solid rgba(255,255,255,.12);
    padding:9px 16px;border-radius:10px;cursor:pointer;
    transition:transform .06s ease, filter .12s ease;
  }
  button:hover{filter:brightness(1.15);}
  button:active{transform:translateY(1px);}
  .counts{
    display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:14px;
  }
  .chip{
    display:flex;align-items:center;gap:8px;
    background:var(--panel);border:1px solid rgba(255,255,255,.08);
    padding:7px 12px;border-radius:999px;font-size:14px;
  }
  .sw{width:14px;height:14px;border-radius:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.35);}
  .chip b{font-variant-numeric:tabular-nums;}
  .foot{color:var(--muted);font-size:12px;margin-top:14px;}
</style>
</head>
<body>
  <h1><span class="dot"></span> Tiny Isometric City Builder</h1>
  <p class="hint">Click any tile to cycle it: <b>grass → house → tree → road → shop → grass</b>. Use <b>Rotate&nbsp;View</b> to spin the camera. Build your little town!</p>

  <div class="stage">
    <canvas id="city" width="720" height="540"></canvas>
  </div>

  <div class="controls">
    <button id="rotate">⟳ Rotate View</button>
    <button id="clear">🗑 Clear</button>
    <button id="fill">🎲 Randomize</button>
  </div>

  <div class="counts" id="counts"></div>
  <div class="foot">Canvas · no libraries · self-contained</div>

<script>
(function(){
  "use strict";
  const canvas = document.getElementById('city');
  const ctx = canvas.getContext('2d');
  const countsEl = document.getElementById('counts');

  const N = 10;                 // grid size
  const TW = 60, TH = 30;       // tile width/height (iso diamond)
  const originX = canvas.width/2;
  const originY = 70;

  // Tile types
  const EMPTY=0, HOUSE=1, TREE=2, ROAD=3, SHOP=4;
  const CYCLE = [EMPTY,HOUSE,TREE,ROAD,SHOP];
  const TYPES = {
    [EMPTY]:{name:'Grass', color:'#5fa855'},
    [HOUSE]:{name:'House', color:'#d9a05b'},
    [TREE] :{name:'Tree',  color:'#3f9d5c'},
    [ROAD] :{name:'Road',  color:'#6a6f7a'},
    [SHOP] :{name:'Shop',  color:'#4bb3c4'}
  };

  // grid[col][row]
  const grid = [];
  for(let c=0;c<N;c++){grid[c]=[];for(let r=0;r<N;r++)grid[c][r]=EMPTY;}

  let rotation = 0;   // 0..3
  let hover = null;   // {c,r}

  // ---- rotation transforms (logical -> display) ----
  function toDisplay(c,r){
    switch(rotation){
      case 0: return [c, r];
      case 1: return [N-1-r, c];
      case 2: return [N-1-c, N-1-r];
      case 3: return [r, N-1-c];
    }
  }
  // display -> logical (inverse)
  function toLogical(dc,dr){
    switch(rotation){
      case 0: return [dc, dr];
      case 1: return [dr, N-1-dc];
      case 2: return [N-1-dc, N-1-dr];
      case 3: return [N-1-dr, dc];
    }
  }

  // display coords -> screen (top vertex of ground diamond)
  function project(dc,dr){
    return [
      originX + (dc - dr) * (TW/2),
      originY + (dc + dr) * (TH/2)
    ];
  }

  // ---- color helpers ----
  function shade(hex, amt){
    const n = parseInt(hex.slice(1),16);
    let R=(n>>16)&255, G=(n>>8)&255, B=n&255;
    R=Math.max(0,Math.min(255,R+amt));
    G=Math.max(0,Math.min(255,G+amt));
    B=Math.max(0,Math.min(255,B+amt));
    return 'rgb('+R+','+G+','+B+')';
  }

  function diamond(sx,sy){
    return [
      [sx, sy],                 // top
      [sx+TW/2, sy+TH/2],       // right
      [sx, sy+TH],              // bottom
      [sx-TW/2, sy+TH/2]        // left
    ];
  }
  function poly(pts){
    ctx.beginPath();
    ctx.moveTo(pts[0][0],pts[0][1]);
    for(let i=1;i<pts.length;i++)ctx.lineTo(pts[i][0],pts[i][1]);
    ctx.closePath();
  }

  // draw the flat ground tile
  function drawGround(sx,sy,base,highlight){
    const d = diamond(sx,sy);
    poly(d);
    ctx.fillStyle = highlight ? shade(base,35) : base;
    ctx.fill();
    ctx.lineWidth=1;
    ctx.strokeStyle='rgba(0,0,0,.18)';
    ctx.stroke();
  }

  // draw a raised iso cube (top at height h above ground), returns top diamond
  function drawCube(sx,sy,h,color){
    const tw=TW/2, th=TH/2;
    // top diamond corners (raised)
    const t_top=[sx, sy-h];
    const t_right=[sx+tw, sy-h+th];
    const t_bottom=[sx, sy-h+TH];
    const t_left=[sx-tw, sy-h+th];
    // bottom (ground) corners we need: bottom, right, left
    const g_right=[sx+tw, sy+th];
    const g_bottom=[sx, sy+TH];
    const g_left=[sx-tw, sy+th];

    // left face (front-left)
    poly([t_left,t_bottom,g_bottom,g_left]);
    ctx.fillStyle=shade(color,-40); ctx.fill();
    ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.stroke();
    // right face (front-right)
    poly([t_bottom,t_right,g_right,g_bottom]);
    ctx.fillStyle=shade(color,-70); ctx.fill();
    ctx.stroke();
    // top face
    poly([t_top,t_right,t_bottom,t_left]);
    ctx.fillStyle=shade(color,20); ctx.fill();
    ctx.stroke();

    return {top:t_top,right:t_right,bottom:t_bottom,left:t_left};
  }

  // pitched roof on top of a cube's top diamond
  function drawRoof(topD, roofH, color){
    const apex=[topD.top[0], topD.top[1]-roofH+ (topD.bottom[1]-topD.top[1])/2 ];
    // apex centered above the top diamond center
    const cx=(topD.left[0]+topD.right[0])/2;
    const cy=(topD.top[1]+topD.bottom[1])/2;
    const peak=[cx, cy-roofH];
    // front-left roof face
    poly([peak, topD.left, topD.bottom]);
    ctx.fillStyle=shade(color,10); ctx.fill();
    ctx.strokeStyle='rgba(0,0,0,.28)'; ctx.stroke();
    // front-right roof face
    poly([peak, topD.bottom, topD.right]);
    ctx.fillStyle=shade(color,-35); ctx.fill();
    ctx.stroke();
  }

  function drawRoadTop(sx,sy){
    const d=diamond(sx,sy);
    poly(d);
    ctx.fillStyle='#5a5f69'; ctx.fill();
    ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.stroke();
    // dashed center line along the diamond's long axis
    ctx.save();
    ctx.setLineDash([5,5]);
    ctx.strokeStyle='rgba(255,225,120,.85)';
    ctx.lineWidth=2;
    ctx.beginPath();
    ctx.moveTo(sx-TW/2*0.55, sy+TH/2-TH*0.05);
    ctx.lineTo(sx+TW/2*0.55, sy+TH/2-TH*0.05+0);
    // horizontal-ish line across tile
    ctx.moveTo(d[3][0]+8, d[3][1]);
    ctx.lineTo(d[1][0]-8, d[1][1]);
    ctx.stroke();
    ctx.restore();
  }

  // draw a single logical tile at screen pos
  function drawTile(c,r,sx,sy,isHover){
    const t = grid[c][r];
    if(t===EMPTY){
      drawGround(sx,sy,TYPES[EMPTY].color,isHover);
      return;
    }
    // grass base under everything except road
    if(t!==ROAD) drawGround(sx,sy,shade(TYPES[EMPTY].color,-8),isHover);

    if(t===ROAD){
      drawRoadTop(sx,sy);
      if(isHover){poly(diamond(sx,sy));ctx.fillStyle='rgba(255,255,255,.10)';ctx.fill();}
    }
    else if(t===HOUSE){
      const top=drawCube(sx,sy,26,TYPES[HOUSE].color);
      drawRoof(top,18,'#c0413a');
    }
    else if(t===SHOP){
      const top=drawCube(sx,sy,40,TYPES[SHOP].color);
      // flat parapet + little sign
      drawRoof(top,10,'#e8613f');
      ctx.fillStyle='rgba(255,255,255,.9)';
      ctx.beginPath();
      ctx.arc(sx, sy-40+ (top.bottom[1]-top.top[1])/2, 3, 0, Math.PI*2);
      ctx.fill();
    }
    else if(t===TREE){
      // trunk
      drawCube(sx,sy,10,'#7a5230');
      // foliage: rounded green blob using stacked cube + circle top
      const top=drawCube(sx,sy-10,24,'#3f9d5c');
      const cx=(top.left[0]+top.right[0])/2;
      const cy=(top.top[1]+top.bottom[1])/2;
      ctx.beginPath();
      ctx.fillStyle=shade('#3f9d5c',22);
      ctx.arc(cx, cy-4, TW*0.28, 0, Math.PI*2);
      ctx.fill();
      ctx.strokeStyle='rgba(0,0,0,.15)'; ctx.stroke();
    }

    if(isHover){
      poly(diamond(sx,sy));
      ctx.lineWidth=2;
      ctx.strokeStyle='rgba(255,255,255,.55)';
      ctx.stroke();
      ctx.lineWidth=1;
    }
  }

  function render(){
    ctx.clearRect(0,0,canvas.width,canvas.height);

    // build a draw list of logical tiles with their display + screen coords
    const list=[];
    for(let c=0;c<N;c++){
      for(let r=0;r<N;r++){
        const [dc,dr]=toDisplay(c,r);
        const [sx,sy]=project(dc,dr);
        list.push({c,r,dc,dr,sx,sy,depth:dc+dr});
      }
    }
    // painter's algorithm: back (small depth) first
    list.sort((a,b)=> a.depth-b.depth || a.dc-b.dc);

    for(const it of list){
      const isHover = hover && hover.c===it.c && hover.r===it.r;
      drawTile(it.c,it.r,it.sx,it.sy,isHover);
    }
    updateCounts();
  }

  function updateCounts(){
    const counts={};
    for(const k of CYCLE) counts[k]=0;
    for(let c=0;c<N;c++)for(let r=0;r<N;r++)counts[grid[c][r]]++;
    countsEl.innerHTML='';
    for(const k of CYCLE){
      const el=document.createElement('div');
      el.className='chip';
      el.innerHTML='<span class="sw" style="background:'+TYPES[k].color+'"></span>'+
                   TYPES[k].name+' <b>'+counts[k]+'</b>';
      countsEl.appendChild(el);
    }
  }

  // ---- picking: screen -> logical tile (ground plane) ----
  function pick(mx,my){
    const a=(mx-originX)/(TW/2);
    const b=(my-originY)/(TH/2);
    let dc=Math.floor((a+b)/2);
    let dr=Math.floor((b-a)/2);
    if(dc<0||dc>=N||dr<0||dr>=N) return null;
    const [c,r]=toLogical(dc,dr);
    if(c<0||c>=N||r<0||r>=N) return null;
    return {c,r};
  }

  function relPos(e){
    const rect=canvas.getBoundingClientRect();
    const sx=canvas.width/rect.width, sy=canvas.height/rect.height;
    const p=(e.touches&&e.touches[0])||e;
    return [ (p.clientX-rect.left)*sx, (p.clientY-rect.top)*sy ];
  }

  canvas.addEventListener('mousemove',e=>{
    const [mx,my]=relPos(e);
    const t=pick(mx,my);
    const changed = (!!t!==!!hover) || (t&&hover&&(t.c!==hover.c||t.r!==hover.r));
    hover=t;
    if(changed) render();
  });
  canvas.addEventListener('mouseleave',()=>{ if(hover){hover=null;render();} });

  function place(e){
    e.preventDefault();
    const [mx,my]=relPos(e);
    const t=pick(mx,my);
    if(!t) return;
    const cur=grid[t.c][t.r];
    const idx=CYCLE.indexOf(cur);
    grid[t.c][t.r]=CYCLE[(idx+1)%CYCLE.length];
    hover=t;
    render();
  }
  canvas.addEventListener('click',place);
  canvas.addEventListener('touchstart',place,{passive:false});

  document.getElementById('rotate').addEventListener('click',()=>{
    rotation=(rotation+1)%4;
    render();
  });
  document.getElementById('clear').addEventListener('click',()=>{
    for(let c=0;c<N;c++)for(let r=0;r<N;r++)grid[c][r]=EMPTY;
    render();
  });
  document.getElementById('fill').addEventListener('click',()=>{
    for(let c=0;c<N;c++)for(let r=0;r<N;r++){
      const roll=Math.random();
      grid[c][r]= roll<0.45?EMPTY : roll<0.6?HOUSE : roll<0.75?TREE : roll<0.9?ROAD : SHOP;
    }
    render();
  });

  // seed a tiny starter town
  grid[4][4]=HOUSE; grid[5][4]=HOUSE; grid[4][5]=SHOP;
  grid[5][5]=ROAD;  grid[6][5]=ROAD;  grid[5][6]=ROAD;
  grid[3][4]=TREE;  grid[6][6]=TREE;  grid[3][6]=TREE;

  render();
})();
</script>
</body>
</html>