Add drag/move preview functionality
parent
3091ba6856
commit
169091237c
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue