← back to Model Arena
data/artifacts/035b7009e366/claude-code.html
654 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Designer Wallcoverings — Pattern Runner</title>
<style>
:root{
/* ——— luxe-noir design tokens ——— */
--bg:#0d0d0f;
--surface:#17171c;
--surface-2:#1e1e25;
--ink:#f5f2ea;
--muted:#8a8578;
--accent:#c9a961;
--accent-deep:#7d6a45;
--line:rgba(201,169,97,.28);
--hairline:rgba(245,242,234,.10);
--shadow:0 16px 48px rgba(0,0,0,.55);
--glow:0 0 28px rgba(201,169,97,.35);
--ease:cubic-bezier(.22,1,.36,1);
--serif:'Didot','Bodoni MT','Playfair Display',Georgia,'Times New Roman',serif;
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',Arial,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans);}
/* ——— fixed luxury top bar ——— */
.topbar{
position:fixed;top:0;left:0;right:0;z-index:20;
height:84px;
display:flex;align-items:center;justify-content:space-between;
padding:0 clamp(18px,4vw,48px);
background:linear-gradient(180deg,rgba(13,13,15,.96) 0%,rgba(13,13,15,.72) 70%,rgba(13,13,15,0) 100%);
border-bottom:1px solid var(--hairline);
pointer-events:none;
}
.wordmark{
font-family:var(--serif);
text-transform:uppercase;
letter-spacing:.18em;
color:var(--ink);
line-height:1.02;
font-weight:500;
text-shadow:0 1px 0 rgba(0,0,0,.4);
}
.wordmark .l1{display:block;font-size:26px;}
.wordmark .l2{display:block;font-size:20px;color:var(--accent);letter-spacing:.205em;}
/* scoreboard — demoted refined sans */
.scores{display:flex;gap:clamp(14px,3vw,34px);align-items:flex-end;}
.score-item{text-align:right;}
.score-item .lab{
font-family:var(--sans);
font-size:11px;text-transform:uppercase;letter-spacing:.22em;
color:var(--muted);display:block;margin-bottom:3px;
}
.score-item .val{
font-family:var(--serif);
font-size:30px;letter-spacing:.04em;color:var(--ink);
font-variant-numeric:tabular-nums;font-weight:500;
}
.score-item.best .val{color:var(--accent);}
/* ——— canvas ——— */
#game{display:block;position:absolute;inset:0;width:100%;height:100%;}
/* ——— overlays ——— */
.overlay{
position:fixed;inset:0;z-index:30;
display:flex;align-items:center;justify-content:center;
background:radial-gradient(120% 80% at 50% 40%,rgba(23,23,28,.72),rgba(13,13,15,.94));
backdrop-filter:blur(3px);
padding:24px;
transition:opacity .5s var(--ease);
}
.overlay.hidden{opacity:0;pointer-events:none;}
.card{
width:min(560px,92vw);
background:linear-gradient(180deg,var(--surface),var(--surface-2));
border:1px solid var(--line);
border-radius:14px;
box-shadow:var(--shadow);
padding:clamp(32px,5vw,56px);
text-align:center;
animation:fadeUp .7s var(--ease) both;
}
@keyframes fadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}
.kicker{
font-family:var(--sans);font-size:12px;text-transform:uppercase;
letter-spacing:.28em;color:var(--accent);margin-bottom:20px;
}
.card h1{
font-family:var(--serif);font-weight:500;
font-size:clamp(38px,7vw,58px);line-height:1.02;letter-spacing:.01em;
color:var(--ink);margin-bottom:8px;
}
.card h1 .accent{color:var(--accent);font-style:italic;}
.card p{
font-family:var(--sans);font-size:16px;line-height:1.7;
color:var(--muted);max-width:40ch;margin:14px auto 4px;
}
.rule{width:56px;height:1px;background:var(--line);margin:26px auto;}
.result-row{display:flex;justify-content:center;gap:48px;margin:26px 0 6px;}
.result-row .lab{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);display:block;margin-bottom:6px;}
.result-row .num{font-family:var(--serif);font-size:44px;color:var(--ink);font-variant-numeric:tabular-nums;}
.result-row .num.gold{color:var(--accent);}
.btn{
margin-top:30px;
font-family:var(--sans);font-size:12.5px;text-transform:uppercase;letter-spacing:.24em;
color:var(--bg);background:var(--accent);
border:none;border-radius:999px;
padding:16px 40px;cursor:pointer;
transition:transform .2s var(--ease),box-shadow .3s var(--ease),background .3s var(--ease);
box-shadow:var(--glow);
}
.btn:hover{transform:translateY(-2px);background:#d8ba72;box-shadow:0 0 34px rgba(201,169,97,.5);}
.btn:active{transform:translateY(0);}
/* legend of collectible styles */
.legend{display:flex;flex-wrap:wrap;justify-content:center;gap:14px 22px;margin-top:30px;}
.legend .sw{display:flex;align-items:center;gap:9px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);}
.legend .dot{width:18px;height:18px;border-radius:4px;border:1px solid var(--hairline);flex:none;box-shadow:inset 0 0 0 1px rgba(0,0,0,.25);}
.hint{
position:fixed;left:0;right:0;bottom:22px;z-index:15;text-align:center;
font-family:var(--sans);font-size:11px;letter-spacing:.24em;text-transform:uppercase;
color:var(--muted);pointer-events:none;transition:opacity .4s var(--ease);
}
.hint.hidden{opacity:0;}
@media(max-width:520px){
.topbar{height:72px;}
.wordmark .l1{font-size:21px;}
.wordmark .l2{font-size:16px;}
.score-item .val{font-size:24px;}
}
</style>
</head>
<body>
<header class="topbar">
<div class="wordmark" aria-label="Designer Wallcoverings">
<span class="l1">Designer</span>
<span class="l2">Wallcoverings</span>
</div>
<div class="scores">
<div class="score-item"><span class="lab">Score</span><span class="val" id="scoreVal">0</span></div>
<div class="score-item best"><span class="lab">Best</span><span class="val" id="bestVal">0</span></div>
</div>
</header>
<canvas id="game"></canvas>
<div class="hint" id="hint">Tap · Space to Jump</div>
<!-- START -->
<div class="overlay" id="startOverlay">
<div class="card">
<div class="kicker">The House Atelier</div>
<h1>Pattern <span class="accent">Runner</span></h1>
<p>Roll the wall. Gather rare wallpaper bolts, dodge the paint hazards, and hold your line as the atelier quickens.</p>
<div class="legend" id="legendStart"></div>
<div class="rule"></div>
<button class="btn" id="startBtn">Begin the Run</button>
</div>
</div>
<!-- GAME OVER -->
<div class="overlay hidden" id="overOverlay">
<div class="card">
<div class="kicker" id="overKicker">The Run Concluded</div>
<h1>Curtain <span class="accent">Call</span></h1>
<div class="result-row">
<div><span class="lab">Score</span><span class="num gold" id="finalScore">0</span></div>
<div><span class="lab">Best</span><span class="num" id="finalBest">0</span></div>
</div>
<p id="overNote">A refined effort. The wall awaits your return.</p>
<div class="rule"></div>
<button class="btn" id="restartBtn">Run Again</button>
</div>
</div>
<script>
(function(){
"use strict";
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// ——— design tokens mirrored for canvas ———
const T = {
bg:'#0d0d0f', surface:'#17171c', ink:'#f5f2ea', muted:'#8a8578',
accent:'#c9a961', accentDeep:'#7d6a45'
};
// Wallpaper styles — visually distinct jewel motifs (color carries meaning)
const STYLES = [
{ name:'Damask', color:'#2f6b4f', accent:'#7bd6a2', motif:'damask' },
{ name:'Floral', color:'#a63a5b', accent:'#f2a6bd', motif:'floral' },
{ name:'Geometric', color:'#33507e', accent:'#8fb3ea', motif:'geo' },
{ name:'Stripe', color:'#b3792a', accent:'#f0c98a', motif:'stripe' },
{ name:'Toile', color:'#5e4785', accent:'#c3aef0', motif:'toile' }
];
// ——— sizing / DPR ———
let W=0,H=0,DPR=1, ground=0;
function resize(){
DPR = Math.min(window.devicePixelRatio||1, 2);
W = window.innerWidth; H = window.innerHeight;
canvas.width = Math.floor(W*DPR); canvas.height = Math.floor(H*DPR);
canvas.style.width = W+'px'; canvas.style.height = H+'px';
ctx.setTransform(DPR,0,0,DPR,0,0);
ground = H - Math.max(96, H*0.16);
}
window.addEventListener('resize', resize);
// ——— game state ———
const G = {
state:'start', // start | playing | over
speed:0, baseSpeed:340, speedGrow:8, // px/s
score:0, best:0, dist:0,
bgScroll:0, groundScroll:0,
spawnCd:0, styleIdx:0,
shake:0
};
G.best = parseInt(localStorage.getItem('dw_pattern_best')||'0',10)||0;
// Player — paint roller
const P = {
x:0, y:0, w:64, h:44,
vy:0, onGround:true, roll:0
};
const GRAV = 2400, JUMP = -880;
function resetPlayer(){
P.w = Math.max(52, Math.min(72, W*0.06));
P.h = P.w*0.7;
P.x = Math.max(70, W*0.16);
P.y = ground - P.h;
P.vy = 0; P.onGround = true; P.roll = 0;
}
let items = []; // collectibles + obstacles
let motes = []; // ambient gold dust
function startGame(){
resize();
G.state='playing'; G.speed=G.baseSpeed; G.score=0; G.dist=0;
G.spawnCd=0.6; G.styleIdx=0; G.shake=0;
items=[]; motes=[];
for(let i=0;i<26;i++){
motes.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.6+.4,s:Math.random()*10+4});
}
resetPlayer();
updateScore();
hide(startOverlay); hide(overOverlay);
hint.classList.remove('hidden');
setTimeout(()=>hint.classList.add('hidden'), 2600);
}
function endGame(){
G.state='over';
if(G.score > G.best){ G.best = G.score; localStorage.setItem('dw_pattern_best', String(G.best)); }
document.getElementById('finalScore').textContent = G.score;
document.getElementById('finalBest').textContent = G.best;
updateScore();
const note = document.getElementById('overNote');
const kick = document.getElementById('overKicker');
if(G.score>=G.best && G.score>0){ kick.textContent='A New House Record'; note.textContent='Exquisite. The finest run yet — the atelier is impressed.'; }
else if(G.score>=30){ note.textContent='A refined effort. The wall awaits your return.'; }
else { note.textContent='A promising start. Find your rhythm and roll on.'; }
show(overOverlay);
}
function jump(){
if(G.state!=='playing') return;
if(P.onGround){ P.vy = JUMP; P.onGround=false; hint.classList.add('hidden'); }
}
// ——— spawning ———
function spawn(){
// 62% collectible, 38% obstacle
const isItem = Math.random() < 0.62;
if(isItem){
const st = STYLES[(G.styleIdx++) % STYLES.length];
const elevated = Math.random()<0.45;
const size = 40;
items.push({
type:'roll', style:st, x:W+60,
y: elevated ? ground - P.h - (60+Math.random()*70) : ground - size - 8,
w:size, h:size*1.5, phase:Math.random()*6.28, taken:false
});
} else {
const kinds=['bucket','ladder','sign'];
const k=kinds[(Math.random()*kinds.length)|0];
let w,h;
if(k==='bucket'){ w=44; h=42; }
else if(k==='ladder'){ w=40; h=Math.min(120, ground*0.22+40); }
else { w=52; h=46; }
items.push({ type:'obstacle', kind:k, x:W+60, y:ground-h, w:w, h:h });
}
}
// ——— collision ———
function hit(a,b, pad){
pad = pad||0;
return a.x < b.x+b.w-pad && a.x+a.w > b.x+pad &&
a.y < b.y+b.h-pad && a.y+a.h > b.y+pad;
}
// ——— update ———
let last=0;
function frame(ts){
if(!last) last=ts;
let dt=(ts-last)/1000; last=ts;
if(dt>0.05) dt=0.05; // clamp after tab-away
if(G.state==='playing') update(dt);
render(dt);
requestAnimationFrame(frame);
}
function update(dt){
G.speed += G.speedGrow*dt;
G.dist += G.speed*dt;
G.bgScroll = (G.bgScroll + G.speed*0.28*dt) % 240;
G.groundScroll = (G.groundScroll + G.speed*dt) % 80;
// player physics
P.vy += GRAV*dt;
P.y += P.vy*dt;
if(P.y >= ground - P.h){ P.y = ground-P.h; P.vy=0; P.onGround=true; }
P.roll += (G.speed/ (P.w*0.5)) * dt; // rolling spin
// spawn cadence tied to speed
G.spawnCd -= dt;
if(G.spawnCd<=0){
spawn();
const gapBase = Math.max(0.62, 1.35 - G.dist/9000);
G.spawnCd = gapBase + Math.random()*0.55;
}
// move + resolve items
const px = {x:P.x+6, y:P.y+4, w:P.w-12, h:P.h-8};
for(let i=items.length-1;i>=0;i--){
const it=items[i];
it.x -= G.speed*dt;
if(it.type==='roll') it.phase += dt*2;
if(it.type==='roll' && !it.taken && hit(px,it,4)){
it.taken=true; it.pop=0; G.score += 1; updateScore(); G.shake=Math.min(G.shake+2,5);
} else if(it.type==='obstacle' && hit(px,it,7)){
G.shake=16; endGame(); return;
}
if(it.x + it.w < -80) items.splice(i,1);
if(it.taken){ it.pop=(it.pop||0)+dt*4; if(it.pop>1) items.splice(i,1); }
}
// ambient motes
for(const m of motes){ m.x -= (G.speed*0.12 + m.s)*dt; if(m.x<-4){ m.x=W+4; m.y=Math.random()*H; } }
if(G.shake>0) G.shake=Math.max(0,G.shake-dt*22);
}
// ——— render ———
function render(dt){
ctx.clearRect(0,0,W,H);
// shake
let ox=0,oy=0;
if(G.shake>0){ ox=(Math.random()-.5)*G.shake; oy=(Math.random()-.5)*G.shake; }
ctx.save(); ctx.translate(ox,oy);
drawBackground();
drawGround();
// ambient gold dust
for(const m of motes){
ctx.globalAlpha=0.22; ctx.fillStyle=T.accent;
ctx.beginPath(); ctx.arc(m.x,m.y,m.r,0,6.28); ctx.fill();
}
ctx.globalAlpha=1;
// items
for(const it of items){
if(it.type==='roll') drawRoll(it); else drawObstacle(it);
}
drawRoller();
ctx.restore();
}
function drawBackground(){
// deep vignette wall
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#141419'); g.addColorStop(0.62,'#0f0f13'); g.addColorStop(1,'#0b0b0e');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// subtle scrolling damask lattice on the wall (single accent, low opacity)
ctx.save();
ctx.globalAlpha=0.06; ctx.strokeStyle=T.accent; ctx.lineWidth=1;
const s=120, off=-G.bgScroll;
for(let x=off-s; x<W+s; x+=s){
for(let y=-s; y<ground+s; y+=s){
ctx.beginPath();
ctx.ellipse(x+s/2, y+s/2, s*0.30, s*0.44, 0, 0, 6.28);
ctx.stroke();
ctx.beginPath();
ctx.arc(x+s/2, y+s/2, 3, 0, 6.28); ctx.stroke();
}
}
ctx.restore();
// soft radial glow near player lane
const rg=ctx.createRadialGradient(P.x+40, ground-60, 20, P.x+40, ground-60, 360);
rg.addColorStop(0,'rgba(201,169,97,0.10)'); rg.addColorStop(1,'rgba(201,169,97,0)');
ctx.fillStyle=rg; ctx.fillRect(0,0,W,H);
}
function drawGround(){
// baseboard / floor
ctx.fillStyle='#08080a';
ctx.fillRect(0,ground,W,H-ground);
// hairline accent rail
ctx.strokeStyle='rgba(201,169,97,0.5)'; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(0,ground+0.5); ctx.lineTo(W,ground+0.5); ctx.stroke();
// molding shadow
const mg=ctx.createLinearGradient(0,ground,0,ground+26);
mg.addColorStop(0,'rgba(201,169,97,0.14)'); mg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=mg; ctx.fillRect(0,ground+1,W,26);
// scrolling parquet ticks
ctx.strokeStyle='rgba(245,242,234,0.05)'; ctx.lineWidth=1;
for(let x=-G.groundScroll; x<W; x+=80){
ctx.beginPath(); ctx.moveTo(x, ground+8); ctx.lineTo(x, H); ctx.stroke();
}
}
// Paint roller (player)
function drawRoller(){
const x=P.x, y=P.y, w=P.w, h=P.h;
const cy=y+h*0.62, cx=x+w*0.5, r=Math.min(w,h)*0.42;
// ground shadow
const airborne = ground-P.h - y;
const sc = Math.max(0.35, 1 - airborne/260);
ctx.save();
ctx.globalAlpha=0.38*sc; ctx.fillStyle='#000';
ctx.beginPath(); ctx.ellipse(cx, ground-4, r*1.15*sc, 6*sc, 0, 0, 6.28); ctx.fill();
ctx.restore();
// handle
ctx.strokeStyle=T.accent; ctx.lineWidth=4; ctx.lineCap='round';
ctx.beginPath();
ctx.moveTo(x+w*0.5, cy);
ctx.lineTo(x+w*0.5, y-6);
ctx.lineTo(x+w*0.86, y-16);
ctx.stroke();
ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(x+w*0.5, y-6); ctx.lineTo(x+w*0.72, y-11); ctx.stroke();
// roller cylinder (spinning)
ctx.save();
ctx.translate(cx,cy);
// body
const bg=ctx.createLinearGradient(-r,-r,r,r);
bg.addColorStop(0,'#e9e4d6'); bg.addColorStop(0.5,'#f5f2ea'); bg.addColorStop(1,'#cfc7b4');
ctx.fillStyle=bg;
ctx.beginPath(); ctx.arc(0,0,r,0,6.28); ctx.fill();
// rim
ctx.strokeStyle=T.accentDeep; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(0,0,r,0,6.28); ctx.stroke();
// spinning nap marks
ctx.rotate(P.roll);
ctx.strokeStyle='rgba(125,106,69,0.55)'; ctx.lineWidth=2;
for(let i=0;i<6;i++){
const a=i*(6.28/6);
ctx.beginPath();
ctx.moveTo(Math.cos(a)*r*0.35, Math.sin(a)*r*0.35);
ctx.lineTo(Math.cos(a)*r*0.9, Math.sin(a)*r*0.9);
ctx.stroke();
}
// gold hub
ctx.rotate(-P.roll);
ctx.fillStyle=T.accent;
ctx.beginPath(); ctx.arc(0,0,r*0.24,0,6.28); ctx.fill();
ctx.restore();
// fresh paint trail behind
ctx.globalAlpha=0.5; ctx.strokeStyle=T.accent; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(cx-r, cy+r*0.7); ctx.lineTo(cx-r-38, cy+r*0.7); ctx.stroke();
ctx.globalAlpha=1;
}
// Wallpaper bolt collectible — distinct motif per style
function drawRoll(it){
const bob = Math.sin(it.phase)*4;
const x=it.x, y=it.y+bob, w=it.w, h=it.h;
const pop = it.taken ? (it.pop||0) : 0;
ctx.save();
ctx.translate(x+w/2, y+h/2);
if(it.taken){ ctx.globalAlpha=Math.max(0,1-pop); ctx.scale(1+pop*0.7,1+pop*0.7); ctx.translate(0,-pop*30); }
ctx.translate(-w/2,-h/2);
// glow halo
ctx.save();
ctx.globalAlpha*=0.5;
const hg=ctx.createRadialGradient(w/2,h/2,2,w/2,h/2,w*0.95);
hg.addColorStop(0, it.style.accent+'55'); hg.addColorStop(1,'transparent');
ctx.fillStyle=hg; ctx.fillRect(-w*0.4,-h*0.4,w*1.8,h*1.8);
ctx.restore();
// bolt body (rounded rect)
roundRect(0,0,w,h,6);
const bg=ctx.createLinearGradient(0,0,w,h);
bg.addColorStop(0, shade(it.style.color,1.18)); bg.addColorStop(1, shade(it.style.color,0.82));
ctx.fillStyle=bg; ctx.fill();
ctx.strokeStyle='rgba(245,242,234,0.35)'; ctx.lineWidth=1; ctx.stroke();
// rolled edge (right)
ctx.fillStyle=shade(it.style.color,0.6);
roundRect(w-8,0,8,h,3); ctx.fill();
// motif
ctx.save();
ctx.beginPath(); roundRect(3,3,w-11,h-6,4); ctx.clip();
drawMotif(it.style, w, h);
ctx.restore();
// gold band label (no letters — color carries meaning)
ctx.fillStyle=T.accent;
ctx.fillRect(0,h*0.5-3,w-6,6);
ctx.restore();
}
function drawMotif(st, w, h){
ctx.strokeStyle=st.accent; ctx.fillStyle=st.accent; ctx.lineWidth=1.4;
const m=st.motif;
if(m==='stripe'){
for(let i=-h;i<w;i+=8){ ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i+h,h); ctx.stroke(); }
} else if(m==='geo'){
for(let gx=4;gx<w;gx+=12) for(let gy=4;gy<h;gy+=12){
ctx.beginPath(); ctx.moveTo(gx,gy-4); ctx.lineTo(gx+4,gy); ctx.lineTo(gx,gy+4); ctx.lineTo(gx-4,gy); ctx.closePath(); ctx.stroke();
}
} else if(m==='floral'){
for(let gx=8;gx<w;gx+=14) for(let gy=8;gy<h;gy+=16){
for(let p=0;p<5;p++){ const a=p*1.256; ctx.beginPath(); ctx.ellipse(gx+Math.cos(a)*3,gy+Math.sin(a)*3,2.4,1.3,a,0,6.28); ctx.fill(); }
}
} else if(m==='damask'){
for(let gy=6;gy<h;gy+=18) for(let gx=8;gx<w;gx+=16){
ctx.beginPath(); ctx.ellipse(gx,gy,4,7,0,0,6.28); ctx.stroke();
ctx.beginPath(); ctx.arc(gx,gy-9,2,0,6.28); ctx.stroke();
}
} else { // toile — fine cross-hatch scene dashes
for(let gy=5;gy<h;gy+=7){ for(let gx=3;gx<w;gx+=9){ ctx.beginPath(); ctx.moveTo(gx,gy); ctx.lineTo(gx+3,gy+2); ctx.stroke(); } }
}
}
// Obstacles — dark, ink forms with accent outline
function drawObstacle(it){
const x=it.x, y=it.y, w=it.w, h=it.h;
// shadow
ctx.globalAlpha=0.35; ctx.fillStyle='#000';
ctx.beginPath(); ctx.ellipse(x+w/2, ground-3, w*0.6, 5, 0, 0, 6.28); ctx.fill();
ctx.globalAlpha=1;
ctx.strokeStyle=T.accent; ctx.lineWidth=1.5;
if(it.kind==='bucket'){
// paint bucket (trapezoid) with drip
ctx.fillStyle='#1a1a20';
ctx.beginPath();
ctx.moveTo(x+4,y); ctx.lineTo(x+w-4,y); ctx.lineTo(x+w,y+h); ctx.lineTo(x,y+h); ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.fillStyle=T.accent;
ctx.fillRect(x+4,y,w-8,4);
// handle
ctx.beginPath(); ctx.arc(x+w/2,y,w*0.4,Math.PI,0); ctx.stroke();
} else if(it.kind==='ladder'){
ctx.fillStyle='#16161b';
ctx.strokeStyle='#2a2a33'; ctx.lineWidth=1; ctx.fillRect(x,y,w,h);
ctx.strokeStyle=T.accentDeep; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(x+6,y); ctx.lineTo(x+6,y+h); ctx.moveTo(x+w-6,y); ctx.lineTo(x+w-6,y+h); ctx.stroke();
ctx.lineWidth=2;
for(let ry=y+10; ry<y+h; ry+=16){ ctx.beginPath(); ctx.moveTo(x+6,ry); ctx.lineTo(x+w-6,ry); ctx.stroke(); }
} else { // wet-paint sign — sandwich board
ctx.fillStyle='#1a1a20';
ctx.beginPath();
ctx.moveTo(x+w/2,y); ctx.lineTo(x+w,y+h); ctx.lineTo(x,y+h); ctx.closePath();
ctx.fill();
ctx.strokeStyle=T.accent; ctx.lineWidth=1.5; ctx.stroke();
// accent chevrons (caution) — no letters
ctx.strokeStyle=T.accent; ctx.lineWidth=2.4;
for(let c=0;c<2;c++){
const cy=y+h*0.5+c*10;
ctx.beginPath(); ctx.moveTo(x+w*0.3,cy); ctx.lineTo(x+w*0.5,cy-6); ctx.lineTo(x+w*0.7,cy); ctx.stroke();
}
}
}
// helpers
function roundRect(x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }
function shade(hex,f){ const n=parseInt(hex.slice(1),16); let r=(n>>16)&255,g=(n>>8)&255,b=n&255; r=Math.min(255,r*f)|0; g=Math.min(255,g*f)|0; b=Math.min(255,b*f)|0; return 'rgb('+r+','+g+','+b+')'; }
function updateScore(){
document.getElementById('scoreVal').textContent = G.score;
document.getElementById('bestVal').textContent = G.best;
}
// overlays
const startOverlay=document.getElementById('startOverlay');
const overOverlay=document.getElementById('overOverlay');
const hint=document.getElementById('hint');
function show(el){ el.classList.remove('hidden'); }
function hide(el){ el.classList.add('hidden'); }
// legend swatches
(function buildLegend(){
const box=document.getElementById('legendStart');
STYLES.forEach(s=>{
const el=document.createElement('div'); el.className='sw';
el.innerHTML='<span class="dot" style="background:'+s.color+'"></span>'+s.name;
box.appendChild(el);
});
})();
// input
function onJumpKey(e){
if(e.code==='Space'||e.key===' '||e.key==='ArrowUp'){
e.preventDefault();
if(G.state==='playing') jump();
else if(G.state==='start') startGame();
else if(G.state==='over') startGame();
}
}
window.addEventListener('keydown', onJumpKey);
canvas.addEventListener('pointerdown', function(e){ e.preventDefault(); if(G.state==='playing') jump(); });
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', startGame);
// tapping the game-over/start card background (not button) also starts
overOverlay.addEventListener('pointerdown', function(e){ if(e.target===overOverlay) startGame(); });
// boot
resize();
updateScore();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>