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