From 7cce4f2fbd9c630422fb819d458c3ca88e2ab182 Mon Sep 17 00:00:00 2001 From: kts of kettek Date: Sat, 17 Feb 2024 15:31:05 -0800 Subject: [PATCH] Add WIP copy paste logic --- frontend/src/App.svelte | 23 ++++++++++ frontend/src/types/canvas.ts | 57 +++++++++++++++++++++++ frontend/src/types/copypaste.ts | 80 +++++++++++++++++++++++++++++++++ frontend/src/types/selection.ts | 12 ++++- 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 frontend/src/types/copypaste.ts diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 1341e3a..7ae89da 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -21,6 +21,7 @@ import BrushSize from './components/BrushSize.svelte' import Shortcut from './components/Shortcut.svelte' import Shortcuts from './components/Shortcuts.svelte' + import { CopyPaste } from './types/copypaste' let theme: 'white'|'g10'|'g80'|'g90'|'g100' = 'g90' @@ -85,6 +86,26 @@ } showImport = false } + + function engageCopy() { + if (!focusedFile) return + CopyPaste.toLocal(focusedFile.canvas, focusedFile.selection) + } + function engagePaste() { + if (!focusedFile) return + let cp = CopyPaste.fromLocal() + let paletteDiff = cp.getPaletteLengthDifference(focusedFile.canvas.palette) + let missingColors = cp.getMissingPaletteColors(focusedFile.canvas.palette) + + // TODO: We need to do the following: + // 1. If the copying palette has more colors, we need to ask if we want to: + // a. Add the missing colors to the current palette + // b. Replace the current palette entries with the copying palette entries + // c. Remap the copying palette entries, but add the missing colors. + + console.log('paste results', paletteDiff, missingColors, cp) + } + function closeFile(index: number) { files = files.filter((_,i)=>i!==index) if (focusedFileIndex === index) { @@ -147,6 +168,8 @@ swapTool(toolFill)} /> swapTool(toolPicker)} /> swapTool(toolErase)} /> + engageCopy()} /> + engagePaste()} />
diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts index 154de37..e24c802 100644 --- a/frontend/src/types/canvas.ts +++ b/frontend/src/types/canvas.ts @@ -16,6 +16,23 @@ export class Canvas { this.canvas = document.createElement('canvas') this.imageData = new ImageData(width, height) } + + // fromData creates a new Canvas instance from the provided data. + static fromData({width, height, palette, pixels}: {width: number, height: number, palette: Uint32Array, pixels: Uint8Array}): Canvas { + let canvas = new Canvas(width, height) + canvas.palette = palette + canvas.pixels = pixels + canvas.imageData = new ImageData(width, height) + for (let i = 0; i < pixels.length; i++) { + let color = palette[pixels[i]] + canvas.imageData.data[i * 4 + 0] = color & 0xFF + canvas.imageData.data[i * 4 + 1] = (color >> 8) & 0xFF + canvas.imageData.data[i * 4 + 2] = (color >> 16) & 0xFF + canvas.imageData.data[i * 4 + 3] = (color >> 24) & 0xFF + } + return canvas + } + clear() { for (let i = 0; i < this.pixels.length; i++) { this.pixels[i] = 0 @@ -135,4 +152,44 @@ export class Canvas { } return {imageData, x: minX, y: minY, w: width, h: height} } + + // Clip the canvas to contain only the pixels in the provided mask. It returns the new left and top position that the canvas should be rendered to. + clipToMask(mask: PixelPosition[]): ({x: number, y: 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 newPixels = new Uint8Array(width * height) + let newImageData = new ImageData(width, height) + + for (let pixel of mask) { + let p = this.getPixel(pixel.x, pixel.y) + if (p !== -1) { + newPixels[(pixel.y - minY) * width + (pixel.x - minX)] = p + let color = this.palette[p] + newImageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 0] = color & 0xFF + newImageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 1] = (color >> 8) & 0xFF + newImageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 2] = (color >> 16) & 0xFF + newImageData.data[((pixel.y - minY) * width + (pixel.x - minX)) * 4 + 3] = (color >> 24) & 0xFF + } + } + + this.pixels = newPixels + this.imageData = newImageData + + this.width = width + this.height = height + + return {x: minX, y: minY} + } } \ No newline at end of file diff --git a/frontend/src/types/copypaste.ts b/frontend/src/types/copypaste.ts new file mode 100644 index 0000000..196b18e --- /dev/null +++ b/frontend/src/types/copypaste.ts @@ -0,0 +1,80 @@ +import { Canvas } from "./canvas" +import { SelectionArea } from "./selection" + +let currentCanvas: Canvas +let currentSelection: SelectionArea + +// CopyPaste contains a Canvas and SelectionArea that is used to copy and paste data within or between images. +export class CopyPaste { + canvas: Canvas + selection: SelectionArea + + constructor(canvas: Canvas, selection: SelectionArea) { + this.canvas = canvas + this.selection = selection + } + + // Returns the palette colors that are missing from another palette, if any. + getMissingPaletteColors(palette: Uint32Array): { r: number, g: number, b: number, a: number, index: number }[] { + let missingColors: { r: number, g: number, b: number, a: number, index: number }[] = [] + for (let i = 0; i < this.canvas.palette.length; i++) { + let color = this.canvas.palette[i] + if (!palette.includes(color)) { + missingColors.push({ + r: color & 0xFF, + g: (color >> 8) & 0xFF, + b: (color >> 16) & 0xFF, + a: (color >> 24) & 0xFF, + index: i, + }) + } + } + return missingColors + } + + // Returns the length difference between this palette and another palette. + getPaletteLengthDifference(palette: Uint32Array): number { + return palette.length - this.canvas.palette.length + } + + // toLocal writes the given canvas and selection to local variables. The canvas is clipped to the selection and the selection is thereby clipped to that result. + static toLocal(canvas: Canvas, selection: SelectionArea) { + if (!canvas || !selection) { + throw new Error('No copy data provided') + } + currentCanvas = Canvas.fromData({ + width: canvas.width, + height: canvas.height, + palette: canvas.palette, + pixels: canvas.pixels + }) + currentSelection = SelectionArea.fromData({ + width: selection.pixelMaskCanvasPixels.width, + height: selection.pixelMaskCanvasPixels.height, + pixels: selection.pixelMaskCanvasPixels.data + }) + // Clip the canvas to the selection and thereafter modify the selection to be relative to the canvas's new size. + let {x, y} = currentCanvas.clipToMask(selection.getMask()) + currentSelection.move(-x, -y) + currentSelection.resize(currentCanvas.width, currentCanvas.height, 1) + } + + // fromLocal creates a new CopyPaste instance from the local variables. + static fromLocal(): CopyPaste { + if (!currentCanvas || !currentSelection) { + throw new Error('No copy data available') + } + let canvas = Canvas.fromData({ + width: currentCanvas.width, + height: currentCanvas.height, + palette: currentCanvas.palette, + pixels: currentCanvas.pixels + }) + let selection = SelectionArea.fromData({ + width: currentSelection.pixelMaskCanvasPixels.width, + height: currentSelection.pixelMaskCanvasPixels.height, + pixels: currentSelection.pixelMaskCanvasPixels.data + }) + return new CopyPaste(canvas, selection) + } +} \ No newline at end of file diff --git a/frontend/src/types/selection.ts b/frontend/src/types/selection.ts index fe074b0..fa58a31 100644 --- a/frontend/src/types/selection.ts +++ b/frontend/src/types/selection.ts @@ -56,6 +56,16 @@ export class SelectionArea { this.redrawPixelMask = true this.refresh() } + + static fromData({ width, height, pixels }: { width: number, height: number, pixels: Uint8ClampedArray }): SelectionArea { + let selection = new SelectionArea(width, height, 1) + for (let i = 0; i < pixels.length; i++) { + selection.pixelMaskCanvasPixels.data[i] = pixels[i] + } + selection.redrawPixelMask = true + selection.refresh() + return selection + } public resize(width: number, height: number, zoom: number) { // Copy the old pixel mask pixels to a new one. @@ -102,7 +112,7 @@ export class SelectionArea { if (this.redrawPixelMask) { this.pixelMaskCanvas.getContext('2d').putImageData(this.pixelMaskCanvasPixels, 0, 0) - let ctx = this.canvas.getContext('2d') + let ctx = this.canvas.getContext('2d', { willReadFrequently: true }) if (!ctx) return ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) ctx.imageSmoothingEnabled = false