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 FloatingPanel from './components/FloatingPanel.svelte'
import { Palette, PaletteEntry, defaultPalette } from './types/palette' 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 "carbon-components-svelte/css/all.css"
import { Tabs, Tab, TabContent, Theme, Button, Modal, Truncate } from "carbon-components-svelte" import { Tabs, Tab, TabContent, Theme, Button, Modal, Truncate } from "carbon-components-svelte"
@ -38,6 +38,7 @@
let focusedFileIndex: number = -1 let focusedFileIndex: number = -1
let focusedFile: LoadedFile = null let focusedFile: LoadedFile = null
$: focusedFile = files[focusedFileIndex] ?? null $: focusedFile = files[focusedFileIndex] ?? null
$: console.log(focusedFile)
function selectFile(file: LoadedFile, index: number) { function selectFile(file: LoadedFile, index: number) {
focusedFileIndex = index focusedFileIndex = index
@ -46,12 +47,7 @@
function engageImport() { function engageImport() {
if (importValid) { if (importValid) {
files = [...files, { files = [...files, new LoadedFile({filepath: importFilepath, title: importFilepath, canvas: importCanvas, data: importFile})]
filepath: importFilepath,
title: importFilepath,
data: importFile,
canvas: importCanvas,
}]
console.log(files) console.log(files)
} }
showImport = false showImport = false
@ -73,6 +69,11 @@
<OverflowMenuItem text="Save As..."/> <OverflowMenuItem text="Save As..."/>
<OverflowMenuItem hasDivider danger text="Quit"/> <OverflowMenuItem hasDivider danger text="Quit"/>
</OverflowMenu> </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"> <OverflowMenu size="sm">
<div slot="menu">Windows</div> <div slot="menu">Windows</div>
<OverflowMenuItem text="Preview" on:click={() => showPreview = true}/> <OverflowMenuItem text="Preview" on:click={() => showPreview = true}/>

View File

@ -2,7 +2,7 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import type { data } from '../../wailsjs/go/models.ts' 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 file: LoadedFile
export let animation: data.Animation export let animation: data.Animation
@ -28,6 +28,8 @@
let overlayDirty: boolean = true let overlayDirty: boolean = true
let canvasDirty: boolean = true let canvasDirty: boolean = true
let traversedPixels: Set<number> = new Set()
// check resizes canvases, etc. // check resizes canvases, etc.
function check() { function check() {
let ctx = rootCanvas.getContext('2d') let ctx = rootCanvas.getContext('2d')
@ -65,6 +67,14 @@
if (!ctx) return if (!ctx) return
ctx.clearRect(0, 0, rootCanvas.width, rootCanvas.height) ctx.clearRect(0, 0, rootCanvas.width, rootCanvas.height)
ctx.drawImage(canvas, 0, 0) 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) ctx.drawImage(overlayCanvas, 0, 0)
} }
@ -103,9 +113,6 @@
} }
ctx.fill() ctx.fill()
} }
// TODO: Draw the current layer of the current frame.
ctx.drawImage(file.canvas.canvas, offsetX, offsetY)
ctx.restore() ctx.restore()
} }
@ -164,6 +171,17 @@
buttons.add(e.button) buttons.add(e.button)
x = e.clientX x = e.clientX
y = e.clientY 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) => { node.addEventListener('wheel', (e: WheelEvent) => {
@ -221,6 +239,14 @@
if (buttons.has(0)) { if (buttons.has(0)) {
console.log('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)) { if (buttons.has(1)) {
offsetX += dx / zoom offsetX += dx / zoom
@ -235,7 +261,15 @@
}) })
window.addEventListener('mouseup', (e: MouseEvent) => { window.addEventListener('mouseup', (e: MouseEvent) => {
if (buttons.size === 0) return
if (e.button === 0) {
file.release()
console.log('release')
}
buttons.delete(e.button) buttons.delete(e.button)
}) })
} }

View File

@ -46,6 +46,12 @@ export class Canvas {
this.imageData.data[i * 4 + 3] = (color >> 24) & 0xFF 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) { setPixel(x: number, y: number, index: number) {
this.pixels[y * this.width + x] = index this.pixels[y * this.width + x] = index
let color = this.palette[index] let color = this.palette[index]

View File

@ -1,9 +1,59 @@
import type { data } from '../../wailsjs/go/models.ts' import type { data } from '../../wailsjs/go/models.ts'
import type { Canvas } from './canvas.ts' import type { Canvas } from './canvas.ts'
import { UndoableStack, type Undoable } from './undo.ts'
export class LoadedFile { export interface LoadedFileOptions {
filepath: string filepath: string
title: string title: string
canvas: Canvas canvas: Canvas
data: data.StackistFileV1 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);
}
}
}