feat: mobile support for drag/touch

main
flop 4 days ago
parent 2b60c30f7f
commit 8efc44d1c2
  1. 21
      ts-editor/index.html
  2. 87
      ts-editor/src/browser.ts
  3. 8
      ts-editor/style.css

@ -103,27 +103,6 @@
<div class="empty-state">No sections yet.<br />Use <b>Add section</b> to get started.</div>
</div>
</div>
<nav id="mob-tabs">
<a class="mob-tab" href="#left">
<svg viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="12" rx="1" />
<line x1="3" y1="19" x2="8" y2="19" />
<line x1="12" y1="19" x2="21" y2="19" />
</svg>
Preview
</a>
<a class="mob-tab" href="#right">
<svg viewBox="0 0 24 24">
<line x1="8" y1="6" x2="21" y2="6" />
<line x1="8" y1="12" x2="21" y2="12" />
<line x1="8" y1="18" x2="21" y2="18" />
<line x1="3" y1="6" x2="3.01" y2="6" />
<line x1="3" y1="12" x2="3.01" y2="12" />
<line x1="3" y1="18" x2="3.01" y2="18" />
</svg>
Sections
</a>
</nav>
</div>
<!-- confirm dialog -->

@ -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();
};

@ -681,4 +681,10 @@ select.meta-val {
background: var(--bg-active)
}
#mob-add-section { display: none; }
#mob-add-section { display: none; }
canvas.pixel-editor {
touch-action: none; /* belt-and-suspenders alongside preventDefault */
-webkit-user-select: none;
user-select: none;
}
Loading…
Cancel
Save