PatchHub

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>