From 8efc44d1c2c0837c5c95d3f86814cdb71e16f9b8 Mon Sep 17 00:00:00 2001 From: flop Date: Sat, 6 Jun 2026 12:18:48 +0200 Subject: [PATCH] feat: mobile support for drag/touch --- ts-editor/index.html | 21 ---------- ts-editor/src/browser.ts | 87 ++++++++++++++++++++++++---------------- ts-editor/style.css | 8 +++- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/ts-editor/index.html b/ts-editor/index.html index 8a2373c..d8c8633 100644 --- a/ts-editor/index.html +++ b/ts-editor/index.html @@ -103,27 +103,6 @@
No sections yet.
Use Add section to get started.
- diff --git a/ts-editor/src/browser.ts b/ts-editor/src/browser.ts index 685760a..e50c1d1 100644 --- a/ts-editor/src/browser.ts +++ b/ts-editor/src/browser.ts @@ -518,82 +518,99 @@ const PixelCanvas: m.Component<{ img: MonoFormatPixelImage; onpaint: () => void vnode.state.drawing = false; vnode.state.drawValue = 1; }, - view({ attrs: { img, onpaint }, state }) { - function pixelFromEvent(e: MouseEvent): { x: number; y: number } | null { - const canvas = e.currentTarget as HTMLCanvasElement; + view({ attrs: { img, onpaint }, state: s }) { + function pixelFromCoords(canvas: HTMLCanvasElement, clientX: number, clientY: number) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; - const x = Math.floor((e.clientX - rect.left) * scaleX / PIXEL_SCALE); - const y = Math.floor((e.clientY - rect.top) * scaleY / PIXEL_SCALE); + const x = Math.floor((clientX - rect.left) * scaleX / PIXEL_SCALE); + const y = Math.floor((clientY - rect.top) * scaleY / PIXEL_SCALE); if (x < 0 || x >= img.width || y < 0 || y >= img.height) return null; return { x, y }; } - function paint(e: MouseEvent) { - const p = pixelFromEvent(e); + function paintAt(canvas: HTMLCanvasElement, clientX: number, clientY: number) { + const p = pixelFromCoords(canvas, clientX, clientY); if (!p) return; - img.pixels[getPixelIndex(img, p.x, p.y)] = state.drawValue; + img.pixels[getPixelIndex(img, p.x, p.y)] = s.drawValue; onpaint(); - drawPixelCanvas(e.currentTarget as HTMLCanvasElement, img); + drawPixelCanvas(canvas, img); + } + + function paint(e: MouseEvent) { + paintAt(e.currentTarget as HTMLCanvasElement, e.clientX, e.clientY); + } + + function attachTouch(canvas: HTMLCanvasElement) { + canvas.addEventListener("touchstart", (e: TouchEvent) => { + e.preventDefault(); + s.drawing = true; + const touch = e.changedTouches[0]; + const p = pixelFromCoords(canvas, touch.clientX, touch.clientY); + if (p) s.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1; + paintAt(canvas, touch.clientX, touch.clientY); + }, { passive: false }); + + canvas.addEventListener("touchmove", (e: TouchEvent) => { + e.preventDefault(); + if (!s.drawing) return; + const touch = e.changedTouches[0]; + paintAt(canvas, touch.clientX, touch.clientY); + }, { passive: false }); + + canvas.addEventListener("touchend", (e) => { e.preventDefault(); s.drawing = false; }, { passive: false }); + canvas.addEventListener("touchcancel", (e) => { e.preventDefault(); s.drawing = false; }, { passive: false }); } return m("canvas.pixel-editor", { width: img.width * PIXEL_SCALE, height: img.height * PIXEL_SCALE, - oncreate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img), + oncreate: ({ dom }: m.VnodeDOM) => { + const canvas = dom as HTMLCanvasElement; + drawPixelCanvas(canvas, img); + attachTouch(canvas); + }, onupdate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img), + onmousedown: (e: MouseEvent) => { - state.drawing = true; - const p = pixelFromEvent(e); - if (p) state.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1; + s.drawing = true; + const p = pixelFromCoords(e.currentTarget as HTMLCanvasElement, e.clientX, e.clientY); + if (p) s.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1; paint(e); }, - onmousemove: (e: MouseEvent) => { if (state.drawing) paint(e); }, - onmouseup: () => { state.drawing = false; }, - onmouseleave: () => { state.drawing = false; }, + onmousemove: (e: MouseEvent) => { if (s.drawing) paint(e); }, + onmouseup: () => { s.drawing = false; }, + onmouseleave: () => { s.drawing = false; }, + ondragover: (e: DragEvent) => { e.preventDefault(); e.dataTransfer!.dropEffect = "copy"; }, - ondrop: (e: DragEvent) => { e.preventDefault(); const file = e.dataTransfer?.files[0]; if (!file || !file.type.startsWith("image/")) return; - const url = URL.createObjectURL(file); const htmlImg = new Image(); htmlImg.onload = () => { - // Draw the dropped image into an offscreen canvas to read pixel data const offscreen = document.createElement("canvas"); - offscreen.width = htmlImg.width; - offscreen.height = htmlImg.height; const ctx = offscreen.getContext("2d")!; - ctx.drawImage(htmlImg, 0, 0); - - const imageData = ctx.getImageData(0, 0, htmlImg.width, htmlImg.height); - - // Convert to your pixel format — adjust this to match your img structure const scale = Math.min(1, 120 / htmlImg.width, 60 / htmlImg.height); const w = Math.round(htmlImg.width * scale); const h = Math.round(htmlImg.height * scale); - offscreen.width = w; offscreen.height = h; - ctx.drawImage(htmlImg, 0, 0, w, h); // scales the image down while drawing + ctx.drawImage(htmlImg, 0, 0, w, h); + const imageData = ctx.getImageData(0, 0, w, h); img.width = w; img.height = h; - // then read imageData from the scaled offscreen canvas as before img.pixels = new Uint8Array(w * h).map((_, i) => { - const r = imageData.data[i * 4] || 0; - const g = imageData.data[i * 4 + 1] || 0; - const b = imageData.data[i * 4 + 2] || 0; - const a = imageData.data[i * 4 + 3] || 0; - // transparent = 0, dark pixels = 1, light pixels = 0 + const r = imageData.data[i * 4]; + const g = imageData.data[i * 4 + 1]; + const b = imageData.data[i * 4 + 2]; + const a = imageData.data[i * 4 + 3]; return a > 128 && (r * 299 + g * 587 + b * 114) < 128_000 ? 0 : 1; }); - URL.revokeObjectURL(url); m.redraw(); }; diff --git a/ts-editor/style.css b/ts-editor/style.css index 8a2c032..adebd99 100644 --- a/ts-editor/style.css +++ b/ts-editor/style.css @@ -681,4 +681,10 @@ select.meta-val { background: var(--bg-active) } -#mob-add-section { display: none; } \ No newline at end of file +#mob-add-section { display: none; } + +canvas.pixel-editor { + touch-action: none; /* belt-and-suspenders alongside preventDefault */ + -webkit-user-select: none; + user-select: none; +} \ No newline at end of file