Add WIP copy paste logic

main
kts of kettek 2024-02-17 15:31:05 -08:00
parent 169091237c
commit 7cce4f2fbd
4 changed files with 171 additions and 1 deletions

View File

@ -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 @@
<Shortcut global cmd='fill' keys={['f']} on:trigger={()=>swapTool(toolFill)} />
<Shortcut global cmd='picker' keys={['i']} on:trigger={()=>swapTool(toolPicker)} />
<Shortcut global cmd='erase' keys={['e']} on:trigger={()=>swapTool(toolErase)} />
<Shortcut global cmd='copy' keys={['ctrl+c']} on:trigger={()=>engageCopy()} />
<Shortcut global cmd='paste' keys={['ctrl+v']} on:trigger={()=>engagePaste()} />
</Shortcuts>
</menu>
<section class='middle'>

View File

@ -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}
}
}

View File

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

View File

@ -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