Implement undo system for placing pixels
parent
26ca39450f
commit
80f25c3e42
|
@ -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}/>
|
||||
|
|
|
@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue