File size: 10,131 Bytes
43634b6
 
 
92558fd
 
 
 
 
b3981db
92558fd
 
 
 
 
 
 
43634b6
 
 
 
19fecc0
43634b6
de4aa16
43634b6
 
 
de4aa16
 
43634b6
 
 
 
 
 
 
 
 
 
 
 
 
b3981db
 
 
 
 
 
 
 
 
 
 
 
19fecc0
b3981db
de4aa16
 
 
 
 
 
 
b3981db
 
 
43634b6
 
 
 
 
 
 
 
de4aa16
 
43634b6
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tiny Sprite Maker</title>
<style>
body{font-family:system-ui;background:linear-gradient(160deg,#270C12 0%,#4B303A 55%,#270C12 100%);color:#F3E9DC;margin:0;padding:16px;min-height:100vh}
h1{font-size:24px;color:#E9C979;text-shadow:0 2px 8px rgba(0,0,0,.5);letter-spacing:.5px}
h3{color:#E9C979;margin:6px 0}
.card{background:rgba(38,16,22,.82);border:1px solid #B88A34;border-radius:14px;padding:14px;margin:12px 0;box-shadow:0 4px 14px rgba(0,0,0,.35)}
canvas{image-rendering:pixelated;background:#6E5A63;border:1px solid #652B2D;border-radius:8px}
button{background:#165341;color:#F3E9DC;border:1px solid #E9C979;border-radius:10px;font-weight:600;cursor:pointer}
button:active{background:#1f7a5f}
input,select{background:#4B303A;color:#F3E9DC;border:1px solid #B88A34;border-radius:8px}
a{color:#E9C979;font-weight:600}
img{image-rendering:pixelated}
label{margin-right:10px}
</style></head>
<body><h1>👾 Tiny Sprite Maker</h1>
<p>GBA-compact sprites from any picture: shrink it right down, lock it to a tiny palette, get a true-size PNG (often under 2 KB). Everything runs in your browser — no server, no GPU.</p>
<div class="card">
<input type="file" id="file" accept="image/*"><br>
<label>Sprite size: <select id="size"><option value="32x32">32×32</option><option value="32x48">32×48</option><option value="48x64">48×64</option><option value="64x64">64×64</option><option value="64x96">64×96</option><option value="96x144" selected>96×144 extra defined</option></select></label>
<label>Palette colours: <select id="cols"><option>8</option><option selected>16</option><option>32</option></select></label>
<button id="go">Make tiny sprite</button><p style="margin:6px 0 0;font-size:14px;color:#E9C979">Every sprite gets a <b>bold dark outline</b> with gentler inner lines and a brightness lift — defined, not murky — so it reads clearly on a small screen.</p>
</div>
<div class="card"><h3>True-size sprite</h3><canvas id="out" width="48" height="64"></canvas><br><a id="dl" download="tiny-sprite.png">Download sprite PNG</a>
<h3>Preview (zoomed)</h3><canvas id="prev" width="384" height="512"></canvas></div>
<div class="card"><h3>The standard: Sir Reginald, 64×96, 15 colours, dark scheme — now with his 4-frame walk, bold outline and brighter inside (watch him go)</h3>
<img src="data:image/gif;base64,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" width="192" alt="Sir Reginald walking sprite animation"></div>
<script>
function quantize(ctx,w,h,n){
  const img=ctx.getImageData(0,0,w,h), d=img.data, buckets={};
  for(let i=0;i<d.length;i+=4){ if(d[i+3]<128) continue;
    const key=(d[i]>>4)+','+(d[i+1]>>4)+','+(d[i+2]>>4);
    (buckets[key]=buckets[key]||[]).push([d[i],d[i+1],d[i+2]]); }
  const pal=Object.values(buckets).sort((a,b)=>b.length-a.length).slice(0,n).map(g=>{
    const m=[0,0,0]; g.forEach(p=>{m[0]+=p[0];m[1]+=p[1];m[2]+=p[2]}); return m.map(v=>Math.round(v/g.length)); });
  for(let i=0;i<d.length;i+=4){ if(d[i+3]<128) continue;
    let best=pal[0],bd=1e12; for(const p of pal){const dd=(d[i]-p[0])**2+(d[i+1]-p[1])**2+(d[i+2]-p[2])**2; if(dd<bd){bd=dd;best=p}}
    d[i]=best[0];d[i+1]=best[1];d[i+2]=best[2]; }
  ctx.putImageData(img,0,0);
}

function outline(ctx,w,h){
  const img=ctx.getImageData(0,0,w,h), d=img.data, src=new Uint8ClampedArray(d);
  const idx=(x,y)=>(y*w+x)*4;
  for(let y=0;y<h;y++)for(let x=0;x<w;x++){
    const i=idx(x,y); if(src[i+3]<128) continue;
    let edge=false;
    const nb=[[1,0],[-1,0],[0,1],[0,-1]];
    for(const q of nb){
      const nx=x+q[0], ny=y+q[1];
      if(nx<0||ny<0||nx>=w||ny>=h){ edge=true; break; }
      const j=idx(nx,ny);
      if(src[j+3]<128 || Math.abs(src[j]-src[i])+Math.abs(src[j+1]-src[i+1])+Math.abs(src[j+2]-src[i+2])>40){ edge=true; break; }
    }
    if(edge){
      const sil = (x===0||y===0||x===w-1||y===h-1);
      let hard = sil;
      if(!hard){ const nb2=[[1,0],[-1,0],[0,1],[0,-1]]; for(const q of nb2){ const nx=x+q[0], ny=y+q[1]; if(nx<0||ny<0||nx>=w||ny>=h||src[(ny*w+nx)*4+3]<128){ hard=true; break; } } }
      if(hard){ d[i]=14; d[i+1]=12; d[i+2]=18; }
      else { d[i]=Math.round(d[i]*0.45+20*0.55); d[i+1]=Math.round(d[i+1]*0.45+16*0.55); d[i+2]=Math.round(d[i+2]*0.45+24*0.55); }
    }
  }
  ctx.putImageData(img,0,0);
}
document.getElementById('go').onclick=()=>{
  const f=document.getElementById('file').files[0]; if(!f) return alert('Pick a picture first');
  const [w,h]=document.getElementById('size').value.split('x').map(Number);
  const n=+document.getElementById('cols').value;
  const url=URL.createObjectURL(f), im=new Image();
  im.onload=()=>{ const out=document.getElementById('out'); out.width=w; out.height=h;
    const ctx=out.getContext('2d');
    const s=Math.max(w/im.width,h/im.height), dw=im.width*s, dh=im.height*s;
    ctx.filter='brightness(1.14) contrast(1.06) saturate(1.25)'; ctx.drawImage(im,(w-dw)/2,(h-dh)/2,dw,dh); ctx.filter='none';
    quantize(ctx,w,h,n); outline(ctx,w,h);
    const prev=document.getElementById('prev'), pctx=prev.getContext('2d');
    pctx.imageSmoothingEnabled=false; pctx.clearRect(0,0,prev.width,prev.height);
    pctx.drawImage(out,0,0,prev.width,prev.height);
    document.getElementById('dl').href=out.toDataURL('image/png'); };
  im.src=url;
};
</script></body></html>