Implement undo system for placing pixels

main
kts of kettek 2024-02-14 08:51:33 -08:00
parent 26ca39450f
commit 80f25c3e42
5 changed files with 198 additions and 12 deletions

View File

@ -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 @@
<OverflowMenuItem text="Save As..."/>
<OverflowMenuItem hasDivider danger text="Quit"/>
</OverflowMenu>
<OverflowMenu size="sm">
<div slot="menu">Edit</div>
<OverflowMenuItem text="Undo" on:click={() => focusedFile?.undo()} disabled={!focusedFile?.canUndo()}/>
<OverflowMenuItem text="Redo" on:click={() => focusedFile?.redo()} disabled={!focusedFile?.canRedo()}/>
</OverflowMenu>
<OverflowMenu size="sm">
<div slot="menu">Windows</div>
<OverflowMenuItem text="Preview" on:click={() => showPreview = true}/>

View File

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

View File

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

View File

@ -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<LoadedFile> {
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<LoadedFile>) {
super.push(item)
this.canvas.refreshCanvas()
}
}
export class PixelPlaceUndoable implements Undoable<LoadedFile> {
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)
}
}

View File

@ -0,0 +1,95 @@
export class UndoableStack<T> {
private target: T;
private stack: Undoable<T>[] = [];
private stackIndex: number = 0;
private captureStack: Undoable<T>[] = [];
private capturing: boolean = false;
setTarget(target: T) {
this.target = target;
}
public push(item: Undoable<T>) {
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<T> {
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<T> {
apply(t: T): void;
unapply(t: T): void;
}
export class UndoableGroup<T> {
private items: Undoable<T>[];
constructor(items: Undoable<T>[]) {
this.items = items;
}
add(item: Undoable<T>) {
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);
}
}
}