patchhub / pixel-paint Public
Draw pixel art in your browser
pixel-paint / index.html
7488 bytes · raw
| 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="UTF-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | <title>Pixel Art</title> |
| 7 | <style> |
| 8 | :root { |
| 9 | --cell: min(16px, calc((100vw - 48px) / 32), calc((100vh - 220px) / 32)); |
| 10 | } |
| 11 | * { box-sizing: border-box; } |
| 12 | body { |
| 13 | margin: 0; |
| 14 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; |
| 15 | background: #2b2d42; |
| 16 | color: #edf2f4; |
| 17 | display: flex; |
| 18 | flex-direction: column; |
| 19 | align-items: center; |
| 20 | min-height: 100vh; |
| 21 | padding: 16px; |
| 22 | } |
| 23 | h1 { margin: 0 0 12px; font-size: 1.4rem; letter-spacing: 0.05em; } |
| 24 | |
| 25 | .toolbar { |
| 26 | display: flex; |
| 27 | flex-wrap: wrap; |
| 28 | gap: 8px; |
| 29 | align-items: center; |
| 30 | justify-content: center; |
| 31 | margin-bottom: 12px; |
| 32 | } |
| 33 | .palette { |
| 34 | display: flex; |
| 35 | flex-wrap: wrap; |
| 36 | gap: 4px; |
| 37 | justify-content: center; |
| 38 | max-width: 520px; |
| 39 | margin-bottom: 12px; |
| 40 | } |
| 41 | .swatch { |
| 42 | width: 28px; |
| 43 | height: 28px; |
| 44 | border: 2px solid #8d99ae; |
| 45 | border-radius: 4px; |
| 46 | cursor: pointer; |
| 47 | padding: 0; |
| 48 | } |
| 49 | .swatch.selected { |
| 50 | border-color: #fff; |
| 51 | box-shadow: 0 0 0 2px #ef233c; |
| 52 | transform: scale(1.1); |
| 53 | } |
| 54 | button.tool { |
| 55 | font: inherit; |
| 56 | padding: 6px 14px; |
| 57 | border: 2px solid #8d99ae; |
| 58 | border-radius: 6px; |
| 59 | background: #3d405b; |
| 60 | color: #edf2f4; |
| 61 | cursor: pointer; |
| 62 | } |
| 63 | button.tool:hover { background: #4a4e6d; } |
| 64 | button.tool.active { border-color: #fff; background: #ef233c; } |
| 65 | label.custom { |
| 66 | display: flex; |
| 67 | align-items: center; |
| 68 | gap: 6px; |
| 69 | font-size: 0.9rem; |
| 70 | } |
| 71 | input[type="color"] { |
| 72 | width: 36px; |
| 73 | height: 30px; |
| 74 | border: none; |
| 75 | padding: 0; |
| 76 | background: none; |
| 77 | cursor: pointer; |
| 78 | } |
| 79 | .current { |
| 80 | display: flex; |
| 81 | align-items: center; |
| 82 | gap: 6px; |
| 83 | font-size: 0.9rem; |
| 84 | } |
| 85 | .current-box { |
| 86 | width: 24px; |
| 87 | height: 24px; |
| 88 | border: 2px solid #fff; |
| 89 | border-radius: 4px; |
| 90 | } |
| 91 | .checker { |
| 92 | background-color: #fff; |
| 93 | background-image: |
| 94 | linear-gradient(45deg, #ccc 25%, transparent 25%), |
| 95 | linear-gradient(-45deg, #ccc 25%, transparent 25%), |
| 96 | linear-gradient(45deg, transparent 75%, #ccc 75%), |
| 97 | linear-gradient(-45deg, transparent 75%, #ccc 75%); |
| 98 | background-size: 8px 8px; |
| 99 | background-position: 0 0, 0 4px, 4px -4px, -4px 0; |
| 100 | } |
| 101 | |
| 102 | #grid { |
| 103 | display: grid; |
| 104 | grid-template-columns: repeat(32, var(--cell)); |
| 105 | grid-template-rows: repeat(32, var(--cell)); |
| 106 | border: 2px solid #8d99ae; |
| 107 | background: #fff; |
| 108 | cursor: crosshair; |
| 109 | touch-action: none; |
| 110 | user-select: none; |
| 111 | } |
| 112 | #grid.show-lines .cell { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.12); } |
| 113 | .cell { width: var(--cell); height: var(--cell); } |
| 114 | .hint { margin-top: 10px; font-size: 0.8rem; color: #8d99ae; text-align: center; } |
| 115 | </style> |
| 116 | </head> |
| 117 | <body> |
| 118 | <h1>Pixel Art</h1> |
| 119 | |
| 120 | <div class="palette" id="palette"></div> |
| 121 | |
| 122 | <div class="toolbar"> |
| 123 | <div class="current"> |
| 124 | Color: <span class="current-box" id="currentBox"></span> |
| 125 | </div> |
| 126 | <label class="custom"> |
| 127 | Custom: <input type="color" id="customColor" value="#ff8800"> |
| 128 | </label> |
| 129 | <button class="tool" id="brushBtn" type="button">✏️ Brush</button> |
| 130 | <button class="tool" id="eraserBtn" type="button">🧽 Eraser</button> |
| 131 | <button class="tool" id="linesBtn" type="button">▦ Grid lines</button> |
| 132 | <button class="tool" id="clearBtn" type="button">🗑️ Clear</button> |
| 133 | </div> |
| 134 | |
| 135 | <div id="grid" class="show-lines"></div> |
| 136 | |
| 137 | <p class="hint">Click or drag to paint. Right-click (or hold the Eraser) to erase.</p> |
| 138 | |
| 139 | <script> |
| 140 | (function () { |
| 141 | const SIZE = 32; |
| 142 | const ERASE = null; |
| 143 | |
| 144 | const COLORS = [ |
| 145 | "#000000", "#545454", "#a8a8a8", "#ffffff", |
| 146 | "#7f0000", "#ff0000", "#ff7f7f", "#ff8800", |
| 147 | "#ffd800", "#fff5a0", "#4c3a1e", "#a0652c", |
| 148 | "#006400", "#00c800", "#a0ff7f", "#00807f", |
| 149 | "#00ffff", "#0026ff", "#7fb2ff", "#4800ff", |
| 150 | "#b200ff", "#ff00dc", "#ff9ee9", "#ffd2b0" |
| 151 | ]; |
| 152 | |
| 153 | const gridEl = document.getElementById("grid"); |
| 154 | const paletteEl = document.getElementById("palette"); |
| 155 | const currentBox = document.getElementById("currentBox"); |
| 156 | const customColor = document.getElementById("customColor"); |
| 157 | const brushBtn = document.getElementById("brushBtn"); |
| 158 | const eraserBtn = document.getElementById("eraserBtn"); |
| 159 | const linesBtn = document.getElementById("linesBtn"); |
| 160 | const clearBtn = document.getElementById("clearBtn"); |
| 161 | |
| 162 | let color = COLORS[0]; |
| 163 | let erasing = false; |
| 164 | let painting = false; |
| 165 | let paintValue = color; |
| 166 | const cells = []; |
| 167 | |
| 168 | // Build the grid |
| 169 | for (let i = 0; i < SIZE * SIZE; i++) { |
| 170 | const cell = document.createElement("div"); |
| 171 | cell.className = "cell"; |
| 172 | cell.dataset.index = i; |
| 173 | gridEl.appendChild(cell); |
| 174 | cells.push(cell); |
| 175 | } |
| 176 | |
| 177 | // Build the palette |
| 178 | const swatches = COLORS.map(function (c) { |
| 179 | const s = document.createElement("button"); |
| 180 | s.type = "button"; |
| 181 | s.className = "swatch"; |
| 182 | s.style.background = c; |
| 183 | s.title = c; |
| 184 | s.addEventListener("click", function () { selectColor(c); }); |
| 185 | paletteEl.appendChild(s); |
| 186 | return s; |
| 187 | }); |
| 188 | |
| 189 | function updateUI() { |
| 190 | swatches.forEach(function (s, i) { |
| 191 | s.classList.toggle("selected", !erasing && COLORS[i] === color); |
| 192 | }); |
| 193 | brushBtn.classList.toggle("active", !erasing); |
| 194 | eraserBtn.classList.toggle("active", erasing); |
| 195 | if (erasing) { |
| 196 | currentBox.style.background = ""; |
| 197 | currentBox.classList.add("checker"); |
| 198 | } else { |
| 199 | currentBox.classList.remove("checker"); |
| 200 | currentBox.style.background = color; |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | function selectColor(c) { |
| 205 | color = c; |
| 206 | erasing = false; |
| 207 | updateUI(); |
| 208 | } |
| 209 | |
| 210 | function paintCell(cell) { |
| 211 | if (!cell || !cell.classList.contains("cell")) return; |
| 212 | cell.style.background = paintValue === ERASE ? "" : paintValue; |
| 213 | } |
| 214 | |
| 215 | function cellAt(x, y) { |
| 216 | const el = document.elementFromPoint(x, y); |
| 217 | return el && el.parentNode === gridEl ? el : null; |
| 218 | } |
| 219 | |
| 220 | // Painting with mouse, pen or touch |
| 221 | gridEl.addEventListener("pointerdown", function (e) { |
| 222 | e.preventDefault(); |
| 223 | painting = true; |
| 224 | // Right mouse button always erases |
| 225 | paintValue = (erasing || e.button === 2) ? ERASE : color; |
| 226 | paintCell(cellAt(e.clientX, e.clientY)); |
| 227 | }); |
| 228 | |
| 229 | window.addEventListener("pointermove", function (e) { |
| 230 | if (!painting) return; |
| 231 | // Use coalesced events for smoother strokes on fast drags |
| 232 | const events = e.getCoalescedEvents ? e.getCoalescedEvents() : [e]; |
| 233 | (events.length ? events : [e]).forEach(function (ev) { |
| 234 | paintCell(cellAt(ev.clientX, ev.clientY)); |
| 235 | }); |
| 236 | }); |
| 237 | |
| 238 | function stop() { painting = false; } |
| 239 | window.addEventListener("pointerup", stop); |
| 240 | window.addEventListener("pointercancel", stop); |
| 241 | window.addEventListener("blur", stop); |
| 242 | |
| 243 | gridEl.addEventListener("contextmenu", function (e) { e.preventDefault(); }); |
| 244 | |
| 245 | // Tools |
| 246 | customColor.addEventListener("input", function () { selectColor(customColor.value); }); |
| 247 | brushBtn.addEventListener("click", function () { erasing = false; updateUI(); }); |
| 248 | eraserBtn.addEventListener("click", function () { erasing = true; updateUI(); }); |
| 249 | linesBtn.addEventListener("click", function () { |
| 250 | gridEl.classList.toggle("show-lines"); |
| 251 | linesBtn.classList.toggle("active", gridEl.classList.contains("show-lines")); |
| 252 | }); |
| 253 | clearBtn.addEventListener("click", function () { |
| 254 | if (confirm("Clear the whole drawing?")) { |
| 255 | cells.forEach(function (c) { c.style.background = ""; }); |
| 256 | } |
| 257 | }); |
| 258 | |
| 259 | linesBtn.classList.add("active"); |
| 260 | updateUI(); |
| 261 | })(); |
| 262 | </script> |
| 263 | </body> |
| 264 | </html> |