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);
+ }
+ }
+}