From 80f25c3e4240597939cb93ec857d5d60764db55f Mon Sep 17 00:00:00 2001 From: kts of kettek Date: Wed, 14 Feb 2024 08:51:33 -0800 Subject: [PATCH] Implement undo system for placing pixels --- frontend/src/App.svelte | 15 +++-- frontend/src/sections/Editor2D.svelte | 42 ++++++++++-- frontend/src/types/canvas.ts | 6 ++ frontend/src/types/file.ts | 52 ++++++++++++++- frontend/src/types/undo.ts | 95 +++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 frontend/src/types/undo.ts diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 0e9740e..ff8b83e 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -6,7 +6,7 @@ import FloatingPanel from './components/FloatingPanel.svelte' import { Palette, PaletteEntry, defaultPalette } from './types/palette' - import type { LoadedFile } from './types/file.ts' + import { LoadedFile } from './types/file.ts' import "carbon-components-svelte/css/all.css" import { Tabs, Tab, TabContent, Theme, Button, Modal, Truncate } from "carbon-components-svelte" @@ -38,6 +38,7 @@ let focusedFileIndex: number = -1 let focusedFile: LoadedFile = null $: focusedFile = files[focusedFileIndex] ?? null + $: console.log(focusedFile) function selectFile(file: LoadedFile, index: number) { focusedFileIndex = index @@ -46,12 +47,7 @@ function engageImport() { if (importValid) { - files = [...files, { - filepath: importFilepath, - title: importFilepath, - data: importFile, - canvas: importCanvas, - }] + files = [...files, new LoadedFile({filepath: importFilepath, title: importFilepath, canvas: importCanvas, data: importFile})] console.log(files) } showImport = false @@ -73,6 +69,11 @@ + +
Edit
+ focusedFile?.undo()} disabled={!focusedFile?.canUndo()}/> + focusedFile?.redo()} disabled={!focusedFile?.canRedo()}/> +
Windows
showPreview = true}/> diff --git a/frontend/src/sections/Editor2D.svelte b/frontend/src/sections/Editor2D.svelte index 3de45f8..064512a 100644 --- a/frontend/src/sections/Editor2D.svelte +++ b/frontend/src/sections/Editor2D.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte' import type { data } from '../../wailsjs/go/models.ts' - import type { LoadedFile } from '../types/file' + import { PixelPlaceUndoable, type LoadedFile } from '../types/file' export let file: LoadedFile export let animation: data.Animation @@ -27,6 +27,8 @@ let overlayDirty: boolean = true let canvasDirty: boolean = true + + let traversedPixels: Set = new Set() // check resizes canvases, etc. function check() { @@ -65,6 +67,14 @@ if (!ctx) return ctx.clearRect(0, 0, rootCanvas.width, rootCanvas.height) ctx.drawImage(canvas, 0, 0) + + // Draw the actual canvas image. + ctx.save() + ctx.imageSmoothingEnabled = false + ctx.scale(zoom, zoom) + ctx.drawImage(file.canvas.canvas, offsetX, offsetY) + ctx.restore() + ctx.drawImage(overlayCanvas, 0, 0) } @@ -103,9 +113,6 @@ } ctx.fill() } - - // TODO: Draw the current layer of the current frame. - ctx.drawImage(file.canvas.canvas, offsetX, offsetY) ctx.restore() } @@ -164,6 +171,17 @@ buttons.add(e.button) x = e.clientX y = e.clientY + + if (e.button === 0) { + console.log('0 click') + traversedPixels.clear() + file.capture() + let p = file.canvas.getPixel(mousePixelX, mousePixelY) + if (p !== -1) { + file.push(new PixelPlaceUndoable(mousePixelX, mousePixelY, p, 1)) + traversedPixels.add(mousePixelX+mousePixelY*file.canvas.width) + } + } }) node.addEventListener('wheel', (e: WheelEvent) => { @@ -221,6 +239,14 @@ if (buttons.has(0)) { console.log('0') + + if (!traversedPixels.has(mousePixelX+mousePixelY*file.canvas.width)) { + traversedPixels.add(mousePixelX+mousePixelY*file.canvas.width) + let p = file.canvas.getPixel(mousePixelX, mousePixelY) + if (p !== -1) { + file.push(new PixelPlaceUndoable(mousePixelX, mousePixelY, p, 1)) + } + } } if (buttons.has(1)) { offsetX += dx / zoom @@ -235,7 +261,15 @@ }) window.addEventListener('mouseup', (e: MouseEvent) => { + if (buttons.size === 0) return + + if (e.button === 0) { + file.release() + console.log('release') + } + buttons.delete(e.button) + }) } diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts index b0d77e0..dbbdd13 100644 --- a/frontend/src/types/canvas.ts +++ b/frontend/src/types/canvas.ts @@ -46,6 +46,12 @@ export class Canvas { this.imageData.data[i * 4 + 3] = (color >> 24) & 0xFF } } + getPixel(x: number, y: number): number { + if (x < 0 || x >= this.width || y < 0 || y >= this.height) { + return -1 + } + return this.pixels[y * this.width + x] + } setPixel(x: number, y: number, index: number) { this.pixels[y * this.width + x] = index let color = this.palette[index] diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts index 9399327..885ca0d 100644 --- a/frontend/src/types/file.ts +++ b/frontend/src/types/file.ts @@ -1,9 +1,59 @@ import type { data } from '../../wailsjs/go/models.ts' import type { Canvas } from './canvas.ts' +import { UndoableStack, type Undoable } from './undo.ts' -export class LoadedFile { +export interface LoadedFileOptions { filepath: string title: string canvas: Canvas data: data.StackistFileV1 +} + +export class LoadedFile extends UndoableStack { + filepath: string + title: string + canvas: Canvas + data: data.StackistFileV1 + + constructor(options: LoadedFileOptions) { + super() + this.setTarget(this) + this.filepath = options.filepath + this.title = options.title + this.canvas = options.canvas + this.data = options.data + } + + undo() { + super.undo() + this.canvas.refreshCanvas() + } + redo() { + super.redo() + this.canvas.refreshCanvas() + } + push(item: Undoable) { + super.push(item) + this.canvas.refreshCanvas() + } +} + +export class PixelPlaceUndoable implements Undoable { + x: number + y: number + oldIndex: number + newIndex: number + constructor(x: number, y: number, oldIndex: number, newIndex: number) { + this.x = x + this.y = y + this.oldIndex = oldIndex + this.newIndex = newIndex + } + apply(file: LoadedFile) { + console.log('apply', this.x, this.y, this.newIndex) + file.canvas.setPixel(this.x, this.y, this.newIndex) + } + unapply(file: LoadedFile) { + file.canvas.setPixel(this.x, this.y, this.oldIndex) + } } \ No newline at end of file diff --git a/frontend/src/types/undo.ts b/frontend/src/types/undo.ts new file mode 100644 index 0000000..ea93d19 --- /dev/null +++ b/frontend/src/types/undo.ts @@ -0,0 +1,95 @@ +export class UndoableStack { + private target: T; + private stack: Undoable[] = []; + private stackIndex: number = 0; + + private captureStack: Undoable[] = []; + private capturing: boolean = false; + + setTarget(target: T) { + this.target = target; + } + + public push(item: Undoable) { + if (this.capturing) { + this.captureStack.push(item); + item.apply(this.target); + return; + } + + this.stack.splice(this.stackIndex, this.stack.length - this.stackIndex, item); + item.apply(this.target); + this.stackIndex++; + } + + public pop(): Undoable { + if (this.stack.length === 0) { + return null; + } + this.stack[this.stack.length - 1].unapply(this.target); + return this.stack.pop(); + } + + public undo() { + if (this.stackIndex === 0) { + return; + } + this.stack[--this.stackIndex].unapply(this.target); + } + + public redo() { + if (this.stackIndex === this.stack.length) { + return; + } + this.stack[this.stackIndex++].apply(this.target); + } + + public canUndo() { + return this.stackIndex > 0; + } + + public canRedo() { + return this.stackIndex < this.stack.length; + } + + public capture() { + this.captureStack = []; + this.capturing = true; + } + public release() { + this.capturing = false; + this.stack.splice(this.stackIndex, this.stack.length - this.stackIndex, new UndoableGroup(this.captureStack)); + this.stackIndex++; + console.log(this.stack, this.stackIndex) + this.captureStack = []; + } +} + +export interface Undoable { + apply(t: T): void; + unapply(t: T): void; +} + +export class UndoableGroup { + private items: Undoable[]; + + constructor(items: Undoable[]) { + this.items = items; + } + + add(item: Undoable) { + this.items.push(item); + } + + apply(t: T) { + for (let item of this.items) { + item.apply(t); + } + } + + unapply(t: T) { + for (let i = this.items.length - 1; i >= 0; i--) { + this.items[i].unapply(t); + } + } +}