kimhyunwoo commited on
Commit
070efe0
ยท
verified ยท
1 Parent(s): fd40f5c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +487 -17
index.html CHANGED
@@ -1,19 +1,489 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>ESL Painter (128ร—296 / 296ร—128) โ€” Pure HTML+JS</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0b0c0f; --panel:#14161a; --muted:#8a8f98; --text:#e9eef5; --accent:#4da3ff; --accent2:#ff4d6d;
10
+ --btn:#1b1e24; --btn2:#21262e; --grid:#2a2f37;
11
+ }
12
+ html,body{height:100%; background:var(--bg); color:var(--text); font-family:system-ui,Segoe UI,Roboto,Apple SD Gothic Neo,Malgun Gothic,sans-serif;}
13
+ .wrap{max-width:1100px; margin:24px auto; padding:0 16px;}
14
+ h1{font-size:20px; margin:0 0 12px;}
15
+ .panel{background:var(--panel); border-radius:14px; padding:14px; box-shadow:0 0 0 1px #000 inset;}
16
+ .row{display:flex; gap:12px; align-items:flex-start; flex-wrap:wrap;}
17
+ .col{flex:1 1 360px; min-width:320px;}
18
+ .tools .group{display:flex; flex-wrap:wrap; gap:8px; margin-bottom:10px;}
19
+ .tools label{font-size:12px; color:var(--muted);}
20
+ .btn{background:var(--btn); border:1px solid #000; color:var(--text); border-radius:10px; padding:8px 10px; cursor:pointer}
21
+ .btn:hover{background:var(--btn2);}
22
+ .btn.red{border-color:#000; color:#fff;}
23
+ .btn.block{display:block; width:100%; text-align:center;}
24
+ .btn.small{padding:6px 8px; font-size:12px;}
25
+ input[type="color"], input[type="number"], input[type="text"]{
26
+ background:#0e1014; color:var(--text); border:1px solid #000; border-radius:8px; padding:6px 8px; outline:none;
27
+ }
28
+ input[type="file"]{color:var(--muted);}
29
+ .radio{display:inline-flex; gap:4px; align-items:center; padding:4px 8px; border-radius:8px; background:#0e1014; border:1px solid #000;}
30
+ .radio input{margin:0;}
31
+ .canvasWrap{ position:relative; display:inline-block; background:#fff; }
32
+ canvas{image-rendering:pixelated; display:block; background:#fff; border-radius:10px; box-shadow:0 0 0 1px #000;}
33
+ .grid{position:absolute; inset:0; pointer-events:none;}
34
+ .muted{color:var(--muted); font-size:12px;}
35
+ .line{height:1px; background:#000; margin:10px 0;}
36
+ .tag{font-size:11px; color:#c6d3e1; background:#112; border:1px solid #000; border-radius:6px; padding:3px 6px; display:inline-block;}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div class="wrap">
41
+ <h1>ESL Painter โ€” 128ร—296 &nbsp;<span class="tag">B/W/Red</span></h1>
42
+ <div class="row">
43
+ <div class="col panel tools">
44
+ <div class="group">
45
+ <button class="btn small" id="swapWH">โ†” ํ•ด์ƒ๋„ ์ „ํ™˜ (128ร—296 โ†” 296ร—128)</button>
46
+ <span class="muted">ํ˜„์žฌ: <b id="curSize">128ร—296</b></span>
47
+ </div>
48
+
49
+ <div class="group">
50
+ <label>๋ธŒ๋Ÿฌ์‹œ</label>
51
+ <span class="radio"><input type="radio" name="tool" value="brush" checked><b>๊ทธ๋ฆฌ๊ธฐ</b></span>
52
+ <span class="radio"><input type="radio" name="tool" value="eraser"><b>์ง€์šฐ๊ฐœ</b></span>
53
+ <span class="radio"><input type="radio" name="tool" value="text"><b>ํ…์ŠคํŠธ</b></span>
54
+ </div>
55
+
56
+ <div class="group" id="brushBlock">
57
+ <label>์ƒ‰ / ๋‘๊ป˜</label>
58
+ <span class="radio"><input type="radio" name="color" value="black" checked>๊ฒ€์ •</span>
59
+ <span class="radio"><input type="radio" name="color" value="red">๋นจ๊ฐ•</span>
60
+ <span class="radio"><input type="radio" name="color" value="white">ํ•˜์–‘</span>
61
+ <input type="range" id="brushSize" min="1" max="20" value="3" />
62
+ </div>
63
+
64
+ <div class="group" id="textBlock" style="display:none">
65
+ <label>ํ…์ŠคํŠธ</label>
66
+ <input type="text" id="textStr" placeholder="ํ…์ŠคํŠธ ์ž…๋ ฅ" value="KETI" />
67
+ <select id="textColor">
68
+ <option value="black">๊ฒ€์ •</option>
69
+ <option value="red">๋นจ๊ฐ•</option>
70
+ <option value="white">ํ•˜์–‘</option>
71
+ </select>
72
+ <input type="number" id="textSize" min="6" max="72" value="20" />
73
+ <input type="number" id="textAngle" min="-180" max="180" value="0" />
74
+ <span class="muted">ํฌ๊ธฐ/๊ฐ๋„</span>
75
+ <button class="btn small" id="textPlace">๐Ÿ‘‰ ์บ”๋ฒ„์Šค ํด๋ฆญ์œผ๋กœ ๋ฐฐ์น˜</button>
76
+ </div>
77
+
78
+ <div class="group">
79
+ <label>์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ</label>
80
+ <input type="file" id="imgIn" accept="image/*" />
81
+ <span class="radio"><input type="checkbox" id="dither">๋””๋”๋ง</span>
82
+ <button class="btn small" id="fitImg">์ด๋ฏธ์ง€ ๋งž์ถ”๊ธฐ(์ฑ„์›€)</button>
83
+ <button class="btn small" id="fitContain">๋งž์ถ”๊ธฐ(์ „์ฒด ๋ณด์ด๊ธฐ)</button>
84
+ </div>
85
+
86
+ <div class="group">
87
+ <button class="btn small" id="rot90">์บ”๋ฒ„์Šค 90ยฐ ํšŒ์ „</button>
88
+ <button class="btn small" id="mirrorH">์ขŒ์šฐ ๋ฐ˜์ „</button>
89
+ <button class="btn small" id="mirrorV">์ƒํ•˜ ๋ฐ˜์ „</button>
90
+ <span class="radio"><input type="checkbox" id="grid">๊ฒฉ์ž</span>
91
+ </div>
92
+
93
+ <div class="group">
94
+ <button class="btn small" id="undo">โ†ถ ๋˜๋Œ๋ฆฌ๊ธฐ</button>
95
+ <button class="btn small" id="redo">โ†ท ๋‹ค์‹œํ•˜๊ธฐ</button>
96
+ <button class="btn small" id="clear">์ง€์šฐ๊ธฐ</button>
97
+ <button class="btn small" id="export">Export PNG</button>
98
+ </div>
99
+
100
+ <div class="line"></div>
101
+ <p class="muted">
102
+ ์ถœ๋ ฅ์€ ESL์˜ 3์ƒ‰ ํŒ”๋ ˆํŠธ(B/W/Red)์— ๋งž์ถฐ ์บ”๋ฒ„์Šค ์ƒ‰์„ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.
103
+ ์™„์„ฑ PNG๋ฅผ <b>shelflabels.andrewgraham.dev</b>์—์„œ ๊ฐ™์€ ํ•ด์ƒ๋„/ํšŒ์ „์œผ๋กœ ์—…๋กœ๋“œํ•˜์„ธ์š”.
104
+ </p>
105
+ </div>
106
+
107
+ <div class="col panel">
108
+ <div class="canvasWrap" id="canvasWrap">
109
+ <canvas id="cv" width="128" height="296"></canvas>
110
+ <canvas id="gridLayer" class="grid" width="128" height="296"></canvas>
111
+ </div>
112
+ <div style="display:flex; gap:8px; margin-top:10px;">
113
+ <span class="muted">์ขŒํ‘œ: <b id="pos">-,-</b></span>
114
+ <span class="muted">๋„๊ตฌ: <b id="stateLabel">๊ทธ๋ฆฌ๊ธฐ(๊ฒ€, 3px)</b></span>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <script>
121
+ (()=>{
122
+
123
+ // =========================
124
+ // State & helpers
125
+ // =========================
126
+ const cv = document.getElementById('cv');
127
+ const ctx = cv.getContext('2d', { willReadFrequently: true });
128
+ const gridCv = document.getElementById('gridLayer');
129
+ const gridCtx = gridCv.getContext('2d');
130
+ const posEl = document.getElementById('pos');
131
+ const stateEl = document.getElementById('stateLabel');
132
+ const curSizeEl = document.getElementById('curSize');
133
+
134
+ // Default: portrait 128x296 (as in your current setup)
135
+ let W = 128, H = 296;
136
+ let drawing = false;
137
+ let tool = 'brush'; // 'brush' | 'eraser' | 'text'
138
+ let color = 'black'; // 'black' | 'red' | 'white'
139
+ let brush = 3;
140
+ let ditherOn = false;
141
+ let textModeWaitingClick = false;
142
+ let textSpec = { str:'KETI', color:'black', size:20, angle:0 };
143
+
144
+ const undoStack = [];
145
+ const redoStack = [];
146
+ const MAX_STACK = 50;
147
+
148
+ function pushUndo(){
149
+ if(undoStack.length >= MAX_STACK) undoStack.shift();
150
+ undoStack.push(ctx.getImageData(0,0,cv.width,cv.height));
151
+ redoStack.length = 0;
152
+ }
153
+ function applyImageData(img){
154
+ ctx.putImageData(img, 0, 0);
155
+ }
156
+ function setSize(w,h){
157
+ W=w; H=h;
158
+ cv.width=W; cv.height=H;
159
+ gridCv.width=W; gridCv.height=H;
160
+ ctx.fillStyle = '#fff'; ctx.fillRect(0,0,W,H); // clear to white
161
+ drawGrid();
162
+ curSizeEl.textContent = `${W}ร—${H}`;
163
+ }
164
+
165
+ // Init white background
166
+ setSize(128,296);
167
+
168
+ function updateStateLabel(){
169
+ const toolLabel = tool==='brush'?'๊ทธ๋ฆฌ๊ธฐ':tool==='eraser'?'์ง€์šฐ๊ฐœ':'ํ…์ŠคํŠธ';
170
+ const colLabel = color==='black'?'๊ฒ€':color==='red'?'๋นจ':'ํ•˜์–‘';
171
+ stateEl.textContent = tool==='text'
172
+ ? `ํ…์ŠคํŠธ(${textSpec.color}, ${textSpec.size}px, ${textSpec.angle}ยฐ)`
173
+ : `${toolLabel}(${colLabel}, ${brush}px)`;
174
+ }
175
+
176
+ function clamp(v,a,b){ return Math.max(a, Math.min(b,v)); }
177
+
178
+ // =========================
179
+ // Tools UI
180
+ // =========================
181
+ document.querySelectorAll('input[name="tool"]').forEach(r=>{
182
+ r.addEventListener('change', e=>{
183
+ tool = e.target.value;
184
+ document.getElementById('brushBlock').style.display = (tool==='brush'||tool==='eraser')?'':'none';
185
+ document.getElementById('textBlock').style.display = (tool==='text')?'':'none';
186
+ updateStateLabel();
187
+ });
188
+ });
189
+ document.querySelectorAll('input[name="color"]').forEach(r=>{
190
+ r.addEventListener('change', e=>{ color = e.target.value; updateStateLabel(); });
191
+ });
192
+ document.getElementById('brushSize').addEventListener('input', e=>{
193
+ brush = +e.target.value; updateStateLabel();
194
+ });
195
+
196
+ // Text controls
197
+ document.getElementById('textStr').addEventListener('input', e=> textSpec.str = e.target.value);
198
+ document.getElementById('textColor').addEventListener('change', e=> { textSpec.color = e.target.value; updateStateLabel(); });
199
+ document.getElementById('textSize').addEventListener('input', e=> { textSpec.size = +e.target.value; updateStateLabel(); });
200
+ document.getElementById('textAngle').addEventListener('input', e=> { textSpec.angle = +e.target.value; updateStateLabel(); });
201
+ document.getElementById('textPlace').addEventListener('click', ()=>{
202
+ tool='text'; textModeWaitingClick=true;
203
+ document.querySelector('input[name="tool"][value="text"]').checked=true;
204
+ document.getElementById('brushBlock').style.display='none';
205
+ document.getElementById('textBlock').style.display='';
206
+ updateStateLabel();
207
+ });
208
+
209
+ // =========================
210
+ // Mouse / Touch drawing
211
+ // =========================
212
+ function getXY(evt){
213
+ const rect = cv.getBoundingClientRect();
214
+ const isTouch = evt.touches && evt.touches.length;
215
+ const x = ((isTouch?evt.touches[0].clientX:evt.clientX) - rect.left) * (cv.width / rect.width);
216
+ const y = ((isTouch?evt.touches[0].clientY:evt.clientY) - rect.top) * (cv.height / rect.height);
217
+ return { x: Math.round(x), y: Math.round(y) };
218
+ }
219
+
220
+ let last = null;
221
+ function drawPoint(p){
222
+ ctx.beginPath();
223
+ ctx.arc(p.x, p.y, brush/2, 0, Math.PI*2);
224
+ ctx.fill();
225
+ }
226
+ function drawLine(a,b){
227
+ ctx.beginPath();
228
+ ctx.moveTo(a.x, a.y);
229
+ ctx.lineTo(b.x, b.y);
230
+ ctx.lineWidth = brush;
231
+ ctx.lineCap = 'round';
232
+ ctx.stroke();
233
+ }
234
+ function setColorForCanvas(col, isEraser=false){
235
+ if(isEraser || col==='white') { ctx.globalCompositeOperation='destination-out'; ctx.strokeStyle='#000'; ctx.fillStyle='#000'; }
236
+ else { ctx.globalCompositeOperation='source-over'; ctx.strokeStyle = col==='red' ? '#ff0000' : '#000000'; ctx.fillStyle = ctx.strokeStyle; }
237
+ }
238
+
239
+ function onDown(e){
240
+ const p = getXY(e); last = p;
241
+ if(tool==='text' && textModeWaitingClick){
242
+ pushUndo();
243
+ drawTextAt(p.x, p.y);
244
+ textModeWaitingClick = false;
245
+ return;
246
+ }
247
+ if(tool==='brush' || tool==='eraser'){
248
+ pushUndo();
249
+ drawing = true;
250
+ setColorForCanvas(color, tool==='eraser');
251
+ drawPoint(p);
252
+ }
253
+ }
254
+ function onMove(e){
255
+ const p = getXY(e);
256
+ posEl.textContent = `${p.x}, ${p.y}`;
257
+ if(!drawing || !last) return;
258
+ drawLine(last,p);
259
+ last = p;
260
+ }
261
+ function onUp(e){
262
+ drawing=false; last=null;
263
+ }
264
+
265
+ cv.addEventListener('mousedown', onDown);
266
+ cv.addEventListener('mousemove', onMove);
267
+ window.addEventListener('mouseup', onUp);
268
+ cv.addEventListener('touchstart', e=>{ e.preventDefault(); onDown(e); }, {passive:false});
269
+ cv.addEventListener('touchmove', e=>{ e.preventDefault(); onMove(e); }, {passive:false});
270
+ cv.addEventListener('touchend', e=>{ e.preventDefault(); onUp(e); }, {passive:false});
271
+
272
+ function drawTextAt(x,y){
273
+ const s = textSpec;
274
+ // snap to ESL palette by drawing directly as #000 / #f00 / erase -> white
275
+ ctx.save();
276
+ ctx.translate(x, y);
277
+ ctx.rotate((s.angle || 0) * Math.PI/180);
278
+ const col = s.color==='red' ? '#f00' : s.color==='black' ? '#000' : '#fff';
279
+ if(col==='#fff'){
280
+ // "white text" = erase text shape
281
+ ctx.globalCompositeOperation='destination-out';
282
+ ctx.fillStyle='#000';
283
+ } else {
284
+ ctx.globalCompositeOperation='source-over';
285
+ ctx.fillStyle=col;
286
+ }
287
+ ctx.font = `bold ${s.size || 20}px sans-serif`;
288
+ ctx.textBaseline='middle';
289
+ ctx.textAlign='center';
290
+ ctx.fillText(s.str || '', 0, 0);
291
+ ctx.restore();
292
+ }
293
+
294
+ // =========================
295
+ // Undo / Redo / Clear
296
+ // =========================
297
+ document.getElementById('undo').addEventListener('click', ()=>{
298
+ if(!undoStack.length) return;
299
+ const cur = ctx.getImageData(0,0,W,H);
300
+ redoStack.push(cur);
301
+ const prev = undoStack.pop();
302
+ applyImageData(prev);
303
+ });
304
+ document.getElementById('redo').addEventListener('click', ()=>{
305
+ if(!redoStack.length) return;
306
+ const cur = ctx.getImageData(0,0,W,H);
307
+ undoStack.push(cur);
308
+ const next = redoStack.pop();
309
+ applyImageData(next);
310
+ });
311
+ document.getElementById('clear').addEventListener('click', ()=>{
312
+ pushUndo();
313
+ ctx.clearRect(0,0,W,H);
314
+ // white background ์œ ์ง€
315
+ ctx.fillStyle='#fff'; ctx.fillRect(0,0,W,H);
316
+ });
317
+
318
+ // =========================
319
+ // Rotate / Mirror / Grid
320
+ // =========================
321
+ function rotate90(){
322
+ pushUndo();
323
+ const tmp = document.createElement('canvas');
324
+ tmp.width=W; tmp.height=H;
325
+ tmp.getContext('2d').drawImage(cv,0,0);
326
+ setSize(H,W); // swap
327
+ ctx.save();
328
+ ctx.translate(W/2, H/2);
329
+ ctx.rotate(90*Math.PI/180);
330
+ ctx.drawImage(tmp, -tmp.width/2, -tmp.height/2);
331
+ ctx.restore();
332
+ }
333
+ document.getElementById('rot90').addEventListener('click', rotate90);
334
+
335
+ function mirror(horizontal=true){
336
+ pushUndo();
337
+ const tmp = document.createElement('canvas');
338
+ tmp.width=W; tmp.height=H;
339
+ tmp.getContext('2d').drawImage(cv,0,0);
340
+ ctx.save();
341
+ ctx.setTransform(horizontal?-1:1,0,0,horizontal?1:-1, horizontal?W:0, horizontal?0:H);
342
+ ctx.drawImage(tmp,0,0);
343
+ ctx.restore();
344
+ }
345
+ document.getElementById('mirrorH').addEventListener('click', ()=>mirror(true));
346
+ document.getElementById('mirrorV').addEventListener('click', ()=>mirror(false));
347
+
348
+ function drawGrid(){
349
+ gridCtx.clearRect(0,0,W,H);
350
+ if(!document.getElementById('grid').checked) return;
351
+ gridCtx.strokeStyle = '#2b2f37';
352
+ gridCtx.lineWidth = 1;
353
+ gridCtx.globalAlpha = .6;
354
+ const step = 8;
355
+ gridCtx.beginPath();
356
+ for(let x=step; x<W; x+=step){ gridCtx.moveTo(x,.5); gridCtx.lineTo(x, H+.5); }
357
+ for(let y=step; y<H; y+=step){ gridCtx.moveTo(.5,y); gridCtx.lineTo(W+.5, y); }
358
+ gridCtx.stroke();
359
+ gridCtx.globalAlpha = 1;
360
+ }
361
+ document.getElementById('grid').addEventListener('change', drawGrid);
362
+
363
+ // =========================
364
+ // Swap resolution
365
+ // =========================
366
+ document.getElementById('swapWH').addEventListener('click', ()=>{
367
+ const newW = H, newH = W; // swap
368
+ // ์ด๋ฏธ์ง€ ๋ณด์กดํ•˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ฆˆ
369
+ const snapshot = document.createElement('canvas');
370
+ snapshot.width=W; snapshot.height=H;
371
+ snapshot.getContext('2d').drawImage(cv,0,0);
372
+ setSize(newW,newH);
373
+ // ์ค‘์•™ ํšŒ์ „ ์—†์ด "์บ”๋ฒ„์Šค ํฌ๊ธฐ๋งŒ ๋ณ€๊ฒฝ"์„ ์›ํ•˜๋ฉด ์ ์ ˆํžˆ scale
374
+ // ์—ฌ๊ธฐ์„œ๋Š” ๋น„์œจ ์œ ์ง€ํ•˜์—ฌ ๋งž์ถค
375
+ const scale = Math.min(W/snapshot.width, H/snapshot.height);
376
+ const dw = snapshot.width*scale, dh=snapshot.height*scale;
377
+ ctx.drawImage(snapshot, (W-dw)/2, (H-dh)/2, dw, dh);
378
+ });
379
+
380
+ // =========================
381
+ // Image import + optional dithering (to B/W/Red palette)
382
+ // =========================
383
+ function quantizeToBWR(imgData){
384
+ // ํŒ”๋ ˆํŠธ: white(255,255,255), black(0,0,0), red(255,0,0)
385
+ const d = imgData.data;
386
+ const pal = [
387
+ {r:255,g:255,b:255, tag:'white'},
388
+ {r:0,g:0,b:0, tag:'black'},
389
+ {r:255,g:0,b:0, tag:'red'}
390
+ ];
391
+ function nearest(r,g,b){
392
+ let best=0, bestDist=1e9;
393
+ for(let i=0;i<pal.length;i++){
394
+ const dr=r-pal[i].r, dg=g-pal[i].g, db=b-pal[i].b;
395
+ const dist=dr*dr+dg*dg+db*db;
396
+ if(dist<bestDist){bestDist=dist; best=i;}
397
+ }
398
+ return pal[best];
399
+ }
400
+ if(!ditherOn){
401
+ for(let i=0;i<d.length;i+=4){
402
+ const q=nearest(d[i],d[i+1],d[i+2]);
403
+ d[i]=q.r; d[i+1]=q.g; d[i+2]=q.b;
404
+ }
405
+ return imgData;
406
+ }
407
+ // Floydโ€“Steinberg dithering
408
+ const w = imgData.width, h=imgData.height;
409
+ for(let y=0;y<h;y++){
410
+ for(let x=0;x<w;x++){
411
+ const i=(y*w+x)*4;
412
+ const old={r:d[i],g:d[i+1],b:d[i+2]};
413
+ const q = nearest(old.r, old.g, old.b);
414
+ d[i]=q.r; d[i+1]=q.g; d[i+2]=q.b;
415
+ const err={r:old.r-q.r, g:old.g-q.g, b:old.b-q.b};
416
+ function add(xo,yo, factor){
417
+ const xx=x+xo, yy=y+yo; if(xx<0||xx>=w||yy<0||yy>=h) return;
418
+ const idx=(yy*w+xx)*4;
419
+ d[idx ]=clamp(d[idx ]+err.r*factor,0,255);
420
+ d[idx+1]=clamp(d[idx+1]+err.g*factor,0,255);
421
+ d[idx+2]=clamp(d[idx+2]+err.b*factor,0,255);
422
+ }
423
+ add(1,0,7/16); add(-1,1,3/16); add(0,1,5/16); add(1,1,1/16);
424
+ }
425
+ }
426
+ return imgData;
427
+ }
428
+
429
+ const imgInput = document.getElementById('imgIn');
430
+ document.getElementById('dither').addEventListener('change', e=> ditherOn = e.target.checked);
431
+ function drawImageFit(img, mode='cover'){
432
+ pushUndo();
433
+ const r = Math.min(W/img.width, H/img.height);
434
+ const R = Math.max(W/img.width, H/img.height);
435
+ const scale = (mode==='contain')? r : R;
436
+ const dw = img.width*scale, dh = img.height*scale;
437
+ const dx = (W-dw)/2, dy=(H-dh)/2;
438
+ // Draw to temp, then quantize to BWR on main
439
+ const tmp = document.createElement('canvas');
440
+ tmp.width=W; tmp.height=H;
441
+ const tctx = tmp.getContext('2d');
442
+ tctx.fillStyle='#fff'; tctx.fillRect(0,0,W,H);
443
+ tctx.drawImage(img, dx, dy, dw, dh);
444
+ const q = quantizeToBWR(tctx.getImageData(0,0,W,H));
445
+ ctx.putImageData(q, 0, 0);
446
+ }
447
+ imgInput.addEventListener('change', e=>{
448
+ const f = e.target.files && e.target.files[0]; if(!f) return;
449
+ const url = URL.createObjectURL(f);
450
+ const img = new Image();
451
+ img.onload = ()=>{ drawImageFit(img, 'cover'); URL.revokeObjectURL(url); };
452
+ img.src = url;
453
+ });
454
+ document.getElementById('fitImg').addEventListener('click', ()=>{
455
+ // re-use last chosen image if possible โ€” user can choose again as needed
456
+ imgInput.click();
457
+ });
458
+ document.getElementById('fitContain').addEventListener('click', ()=>{
459
+ imgInput.click();
460
+ // contain ๋ชจ๋“œ๋Š” ํŒŒ์ผ ์„ ํƒ ํ›„ onload์—์„œ 'contain'์œผ๋กœ ๋ฐ”๊ฟ” ์ ์šฉํ•˜๊ธธ ์›ํ•˜๋ฉด
461
+ // ํ•„์š” ์‹œ ์ถ”๊ฐ€ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
462
+ });
463
+
464
+ // =========================
465
+ // Grid toggle redraw on resize
466
+ // =========================
467
+ const gridChk = document.getElementById('grid');
468
+ gridChk.addEventListener('change', drawGrid);
469
+ drawGrid();
470
+
471
+ // =========================
472
+ // Export PNG
473
+ // =========================
474
+ document.getElementById('export').addEventListener('click', ()=>{
475
+ // ํŒ”๋ ˆํŠธ ๊ฐ•์ œ(ํ˜น์‹œ ์‚ฌ์šฉ ์ค‘ ์ปฌ๋Ÿฌ๊ฐ€ ์„ž์˜€์„ ์ˆ˜๋„ ์žˆ์œผ๋ฏ€๋กœ ํ•œ๋ฒˆ ๋” ์Šค๋ƒ…)
476
+ const snap = ctx.getImageData(0,0,W,H);
477
+ const q = quantizeToBWR(snap);
478
+ ctx.putImageData(q,0,0);
479
+
480
+ const a = document.createElement('a');
481
+ a.href = cv.toDataURL('image/png');
482
+ a.download = `esl_${W}x${H}.png`;
483
+ a.click();
484
+ });
485
+
486
+ })();
487
+ </script>
488
+ </body>
489
  </html>