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