Spaces:
Running
Running
Update index.html
Browse files- index.html +487 -17
index.html
CHANGED
|
@@ -1,19 +1,489 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <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>
|