From 169091237ce6f9f660507feb20d293bb2b35f23e Mon Sep 17 00:00:00 2001 From: kts of kettek Date: Sat, 17 Feb 2024 12:49:11 -0800 Subject: [PATCH] Add drag/move preview functionality --- frontend/src/sections/Editor2D.svelte | 7 ++++ frontend/src/types/canvas.ts | 33 ++++++++++++++++ frontend/src/types/file.ts | 3 ++ frontend/src/types/preview.ts | 57 +++++++++++++++++++++++++++ frontend/src/types/selection.ts | 15 +++++++ frontend/src/types/tools.ts | 9 +++++ 6 files changed, 124 insertions(+) create mode 100644 frontend/src/types/preview.ts diff --git a/frontend/src/sections/Editor2D.svelte b/frontend/src/sections/Editor2D.svelte index 8e43e44..f671dbf 100644 --- a/frontend/src/sections/Editor2D.svelte +++ b/frontend/src/sections/Editor2D.svelte @@ -147,6 +147,13 @@ for (let i = 0; i < shape.length; i++) { ctx.fillRect(offsetX*zoom+(mousePixelX+shape[i].x)*zoom, offsetY*zoom+(mousePixelY+shape[i].y)*zoom, zoom, zoom) } + } else if (currentTool instanceof MoveTool && currentTool.isActive()) { + ctx.save() + ctx.imageSmoothingEnabled = false + ctx.scale(zoom, zoom) + let {x, y} = currentTool.previewPosition() + ctx.drawImage(currentTool.preview.canvas, offsetX+x, offsetY+y) + ctx.restore() } // Draw our overlay with difference composition so visibility is better. diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts index bde347e..154de37 100644 --- a/frontend/src/types/canvas.ts +++ b/frontend/src/types/canvas.ts @@ -1,3 +1,5 @@ +import type { PixelPosition } from "./shapes" + export class Canvas { width: number height: number @@ -102,4 +104,35 @@ export class Canvas { return index } + + // Returns the an ImageData containing the canvas contents clipped to the provided pixel mask. + getImageDataFromMask(mask: PixelPosition[]): {imageData: ImageData, x: number, y: number, w: number, h: number} { + // Get minimum x position from mask. + let minX = 9999999 + let minY = 9999999 + let maxX = -9999999 + let maxY = -9999999 + for (let pixel of mask) { + minX = Math.min(minX, pixel.x) + minY = Math.min(minY, pixel.y) + maxX = Math.max(maxX, pixel.x) + maxY = Math.max(maxY, pixel.y) + } + let width = maxX - minX + 1 + let height = maxY - minY + 1 + + let imageData = new ImageData(width, height) + + for (let pixel of mask) { + let p = this.getPixel(pixel.x, pixel.y) + if (p !== -1) { + let color = this.palette[p] + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 0] = color & 0xFF + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 1] = (color >> 8) & 0xFF + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 2] = (color >> 16) & 0xFF + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 3] = (color >> 24) & 0xFF + } + } + return {imageData, x: minX, y: minY, w: width, h: height} + } } \ No newline at end of file diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts index 3f82ae1..33473fc 100644 --- a/frontend/src/types/file.ts +++ b/frontend/src/types/file.ts @@ -1,5 +1,6 @@ import type { data } from '../../wailsjs/go/models.ts' import type { Canvas } from './canvas' +import { Preview } from './preview' import { SelectionArea } from './selection' import { UndoableStack, type Undoable } from './undo' @@ -15,6 +16,7 @@ export class LoadedFile extends UndoableStack { title: string canvas: Canvas selection: SelectionArea + preview: Preview data: data.StackistFileV1 constructor(options: LoadedFileOptions) { @@ -24,6 +26,7 @@ export class LoadedFile extends UndoableStack { this.filepath = options.filepath this.title = options.title this.canvas = options.canvas + this.preview = new Preview() this.selection = new SelectionArea(options.canvas.width, options.canvas.height, 1) } diff --git a/frontend/src/types/preview.ts b/frontend/src/types/preview.ts new file mode 100644 index 0000000..fcbf748 --- /dev/null +++ b/frontend/src/types/preview.ts @@ -0,0 +1,57 @@ +import type { Canvas } from "./canvas" +import type { SelectionArea } from "./selection" + +// Preview is a helper class that provides a preview HTMLCanvasElement that is built from a Selection and a Canvas. +export class Preview { + public canvas: HTMLCanvasElement // A canvas sized to the minimum bounding box of the selection. + // x and y should be used to position the Preview's canvas relative to the top-left of the Canvas. + public x: number // x is the x offet from the left-most pixel in the selection. + public y: number // y is the y offset from the top-most pixel in the selection. + + constructor() { + this.canvas = document.createElement('canvas') + this.x = 0 + this.y = 0 + } + + fromSelectionAndCanvas(selection: SelectionArea, canvas: Canvas) { + // Get our selection mask. FIXME: We can probably just use the selection's pixelMaskCanvasPixels... + let mask = selection.getMask() + // Get minimum x position from mask. + let minX = 9999999 + let minY = 9999999 + let maxX = -9999999 + let maxY = -9999999 + for (let pixel of mask) { + minX = Math.min(minX, pixel.x) + minY = Math.min(minY, pixel.y) + maxX = Math.max(maxX, pixel.x) + maxY = Math.max(maxY, pixel.y) + } + let width = maxX - minX + 1 + let height = maxY - minY + 1 + + // Copy over the pixels from the canvas's image data to a preview image data. + let imageData = new ImageData(width, height) + for (let pixel of mask) { + let p = canvas.getPixel(pixel.x, pixel.y) + if (p !== -1) { + let {r, g, b, a} = canvas.getPaletteAsRGBA(p) + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 0] = r + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 1] = g + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 2] = b + imageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 3] = a + } + } + + // Store for use in other calculations. + this.x = minX + this.y = minY + + // Draw it! + this.canvas.width = width + this.canvas.height = height + let ctx = this.canvas.getContext('2d') + ctx.putImageData(imageData, 0, 0) + } +} \ No newline at end of file diff --git a/frontend/src/types/selection.ts b/frontend/src/types/selection.ts index e5377f1..fe074b0 100644 --- a/frontend/src/types/selection.ts +++ b/frontend/src/types/selection.ts @@ -1,3 +1,5 @@ +import type { PixelPosition } from "./shapes" + export class SelectionArea { public marchingCanvas: HTMLCanvasElement private marchStep: number = 0 @@ -199,4 +201,17 @@ export class SelectionArea { this.pixelMaskCanvasPixels.data[i + 3] = 0 } } + + // getMask returns an array of PixelPositions that correspond to non-zero alpha pixels in the selection. + getMask(): PixelPosition[] { + let pixels: PixelPosition[] = [] + for (let y = 0; y < this.pixelMaskCanvas.height; y++) { + for (let x = 0; x < this.pixelMaskCanvas.width; x++) { + if (this.pixelMaskCanvasPixels.data[(y * this.pixelMaskCanvas.width + x) * 4 + 3] !== 0) { + pixels.push({x, y, index: 0}) + } + } + } + return pixels + } } \ No newline at end of file diff --git a/frontend/src/types/tools.ts b/frontend/src/types/tools.ts index 7a0487f..9acd7d5 100644 --- a/frontend/src/types/tools.ts +++ b/frontend/src/types/tools.ts @@ -1,4 +1,5 @@ import { PixelPlaceUndoable, type LoadedFile, PixelsPlaceUndoable, SelectionClearUndoable, SelectionSetUndoable, SelectionMoveUndoable } from "./file" +import { Preview } from "./preview" import { FilledCircle, FilledSquare, type PixelPosition } from "./shapes" export interface ToolContext { @@ -287,10 +288,15 @@ export class MoveTool implements Tool { private startY: number private endX: number private endY: number + public preview: Preview isActive(): boolean { return this.active } + + previewPosition(): ({x: number, y: number}) { + return {x: this.endX - this.startX + this.preview.x, y: this.endY - this.startY + this.preview.y} + } shift(ctx: ToolContext, ptr: Pointer) { this.startX = 0 @@ -300,11 +306,14 @@ export class MoveTool implements Tool { this.pointerUp(ctx, ptr) } pointerDown(ctx: ToolContext, ptr: Pointer) { + this.preview = new Preview() + this.preview.fromSelectionAndCanvas(ctx.file.selection, ctx.file.canvas) this.startX = this.endX = ptr.x this.startY = this.endY = ptr.y this.active = true } pointerMove(ctx: ToolContext, ptr: Pointer) { + this.preview.fromSelectionAndCanvas(ctx.file.selection, ctx.file.canvas) this.endX = ptr.x this.endY = ptr.y }