commit 9dacb29f9fecfb78c190fceeed61505cb7e2129f
parent 788937b6c2c84aac0a95170731f81ed0ca595580
Author: Hunter
Date: Mon, 20 Apr 2026 07:56:32 -0400
⌘+S to save painting to file; ⌘+O to open existing
Diffstat:
| M | index.html | | | 94 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 94 insertions(+), 0 deletions(-)
diff --git a/index.html b/index.html
@@ -447,6 +447,90 @@
}
}
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = 'image/*';
+ fileInput.style.display = 'none';
+ document.body.appendChild(fileInput);
+ fileInput.addEventListener('change', (e) => {
+ const f = e.target.files && e.target.files[0];
+ if (f) importImage(f);
+ fileInput.value = '';
+ });
+
+ function formatTimestamp(d) {
+ const p = (n) => String(n).padStart(2, '0');
+ return p(d.getFullYear() % 100) + '\u00b7' + p(d.getMonth() + 1) + '\u00b7' + p(d.getDate()) + '\u00b7' + p(d.getHours()) + '\u00b7' + p(d.getMinutes()) + '\u00b7' + p(d.getSeconds());
+ }
+
+ function exportPNG() {
+ if (chunks.size === 0) return;
+ let minCx = Infinity, minCy = Infinity, maxCx = -Infinity, maxCy = -Infinity;
+ for (const k of chunks.keys()) {
+ const [cx, cy] = k.split(',').map(Number);
+ if (cx < minCx) minCx = cx;
+ if (cy < minCy) minCy = cy;
+ if (cx > maxCx) maxCx = cx;
+ if (cy > maxCy) maxCy = cy;
+ }
+ const w = (maxCx - minCx + 1) * CHUNK;
+ const h = (maxCy - minCy + 1) * CHUNK;
+ const out = document.createElement('canvas');
+ out.width = w;
+ out.height = h;
+ const octx = out.getContext('2d');
+ octx.fillStyle = '#000';
+ octx.fillRect(0, 0, w, h);
+ for (const [k, c] of chunks) {
+ const [cx, cy] = k.split(',').map(Number);
+ octx.drawImage(c.canvas, (cx - minCx) * CHUNK, (cy - minCy) * CHUNK);
+ }
+ out.toBlob((blob) => {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'paint \u00b7 ' + formatTimestamp(new Date()) + '.png';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 'image/png');
+ dirty = false;
+ }
+
+ function importImage(file) {
+ if (dirty && !confirm('Importing will discard the current painting. Continue?')) return;
+ const img = new Image();
+ img.onload = () => {
+ chunks.clear();
+ const w = img.width, h = img.height;
+ const tmp = document.createElement('canvas');
+ tmp.width = w;
+ tmp.height = h;
+ const tctx = tmp.getContext('2d');
+ tctx.drawImage(img, 0, 0);
+ const ox = -Math.floor(w / 2);
+ const oy = -Math.floor(h / 2);
+ const cx0 = Math.floor(ox / CHUNK);
+ const cy0 = Math.floor(oy / CHUNK);
+ const cx1 = Math.floor((ox + w - 1) / CHUNK);
+ const cy1 = Math.floor((oy + h - 1) / CHUNK);
+ for (let cy = cy0; cy <= cy1; cy++) {
+ for (let cx = cx0; cx <= cx1; cx++) {
+ const c = getOrCreateChunk(cx, cy);
+ c.ctx.drawImage(tmp, ox - cx * CHUNK, oy - cy * CHUNK);
+ }
+ }
+ camX = ox - (cssW / zoom - w) / 2;
+ camY = oy - (cssH / zoom - h) / 2;
+ dirty = false;
+ URL.revokeObjectURL(img.src);
+ requestDraw();
+ };
+ img.onerror = () => URL.revokeObjectURL(img.src);
+ img.src = URL.createObjectURL(file);
+ }
+
window.addEventListener('resize', resize);
window.addEventListener('mouseover', (e) => {
@@ -576,6 +660,16 @@
}
window.addEventListener('keydown', (e) => {
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 's') {
+ e.preventDefault();
+ exportPNG();
+ return;
+ }
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'o') {
+ e.preventDefault();
+ fileInput.click();
+ return;
+ }
const k = e.key.toLowerCase();
const wasDown = keys[k];
keys[k] = true;