Add drag/move preview functionality

main
kts of kettek 2024-02-17 12:49:11 -08:00
parent 3091ba6856
commit 169091237c
6 changed files with 124 additions and 0 deletions

View File

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

View File

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

View File

@ -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<LoadedFile> {
title: string
canvas: Canvas
selection: SelectionArea
preview: Preview
data: data.StackistFileV1
constructor(options: LoadedFileOptions) {
@ -24,6 +26,7 @@ export class LoadedFile extends UndoableStack<LoadedFile> {
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)
}

View File

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

View File

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

View File

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