Implement undo system for placing pixels
parent
26ca39450f
commit
80f25c3e42
|
|
@ -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}/>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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