diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 0a53832..44ac770 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -6,15 +6,15 @@ import FloatingPanel from './components/FloatingPanel.svelte' import { Palette, PaletteEntry, defaultPalette } from './types/palette' - import { LoadedFile } from './types/file.ts' + import { LoadedFile } from './types/file' 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, ButtonSet } from "carbon-components-svelte" import { ComposedModal } from "carbon-components-svelte" import { OverflowMenu, OverflowMenuItem } from "carbon-components-svelte" - import { Close } from "carbon-icons-svelte" + import { Close, Erase, PaintBrushAlt, Redo, Select_01, Undo } from "carbon-icons-svelte" import StackPreview from './sections/StackPreview.svelte' import type { Canvas } from './types/canvas' @@ -59,7 +59,7 @@
- +
File
@@ -71,8 +71,12 @@
Edit
- focusedFile?.undo()} disabled={!focusedFile?.canUndo()}/> - focusedFile?.redo()} disabled={!focusedFile?.canRedo()}/> + focusedFile?.undo()} disabled={!focusedFile?.canUndo()}> + Undo   + + focusedFile?.redo()} disabled={!focusedFile?.canRedo()}> + Redo   +
Windows
@@ -83,6 +87,11 @@
+ + + + +
{#each files as file, index} @@ -130,18 +139,18 @@ display: grid; grid-template-rows: auto minmax(0, 1fr); } - menu { + .mainMenu { display: flex; flex-direction: row; justify-content: flex-start; } - :global(menu > button) { + :global(.mainMenu > button) { width: 4rem !important; color: var(--cds-text-02, #c6c6c6); } .content { display: grid; - grid-template-columns: 1fr 4fr; + grid-template-columns: 1fr auto 4fr; grid-template-rows: minmax(0, 1fr); } .left { @@ -149,6 +158,11 @@ flex-direction: row; align-items: flex-start; } + .toolbar { + display: flex; + flex-direction: column; + justify-content: flex-start; + } .middle { display: grid; grid-template-rows: auto minmax(0, 1fr); diff --git a/frontend/src/sections/Editor2D.svelte b/frontend/src/sections/Editor2D.svelte index a496446..c67e1b1 100644 --- a/frontend/src/sections/Editor2D.svelte +++ b/frontend/src/sections/Editor2D.svelte @@ -2,7 +2,9 @@ import { onMount } from 'svelte' import type { data } from '../../wailsjs/go/models.ts' - import { PixelPlaceUndoable, type LoadedFile } from '../types/file' + import type { LoadedFile } from '../types/file' + import type { PixelPosition } from '../types/shapes' + import { BrushTool, type Tool } from '../types/tools' export let file: LoadedFile export let animation: data.Animation @@ -32,6 +34,11 @@ let canvasDirty: boolean = true let traversedPixels: Set = new Set() + function addTraversedPixels(pixels: PixelPosition[]) { + for (let p of pixels) { + traversedPixels.add(p.x+p.y*file.canvas.width) + } + } // check resizes canvases, etc. function check() { @@ -155,7 +162,9 @@ offsetY = canvas.height-30 } } - + + let currentTool: Tool = new BrushTool() + function canvasMousedown(node) { let buttons: Set = new Set() let x: number = 0 @@ -176,13 +185,8 @@ 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, primaryColorIndex)) - traversedPixels.add(mousePixelX+mousePixelY*file.canvas.width) + if (currentTool instanceof BrushTool) { + currentTool.pointerDown({file, brushSize: 3, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: e.button }) } } }) @@ -241,13 +245,9 @@ y = e.clientY 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, primaryColorIndex)) + if (currentTool.isActive()) { + if (currentTool instanceof BrushTool) { + currentTool.pointerMove({file, brushSize: 3, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: 0 }) } } } @@ -267,8 +267,9 @@ if (buttons.size === 0) return if (e.button === 0) { - file.release() - console.log('release') + if (currentTool.isActive()) { + currentTool.pointerUp({file}, {x: mousePixelX, y: mousePixelY, id: 0 }) + } } buttons.delete(e.button) diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts index 885ca0d..6193b93 100644 --- a/frontend/src/types/file.ts +++ b/frontend/src/types/file.ts @@ -1,6 +1,6 @@ import type { data } from '../../wailsjs/go/models.ts' -import type { Canvas } from './canvas.ts' -import { UndoableStack, type Undoable } from './undo.ts' +import type { Canvas } from './canvas' +import { UndoableStack, type Undoable } from './undo' export interface LoadedFileOptions { filepath: string @@ -50,10 +50,40 @@ export class PixelPlaceUndoable implements Undoable { 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) } +} + +export class PixelsPlaceUndoable implements Undoable { + private oldPixels: { x: number, y: number, index: number }[] + private hasOldPixels: boolean + private pixels: { x: number, y: number, index: number }[] + constructor(pixels: {x: number, y: number, index: number}[]) { + this.hasOldPixels = false + this.oldPixels = [] + this.pixels = pixels + } + apply(file: LoadedFile) { + if (!this.hasOldPixels) { + for (let pixel of this.pixels) { + let p = file.canvas.getPixel(pixel.x, pixel.y) + this.oldPixels.push({x: pixel.x, y: pixel.y, index: p}) + } + this.hasOldPixels = true + } + for (let pixel of this.pixels) { + file.canvas.setPixel(pixel.x, pixel.y, pixel.index) + } + } + unapply(file: LoadedFile) { + if (!this.hasOldPixels) { + throw new Error('no old pixels') + } + for (let pixel of this.oldPixels) { + file.canvas.setPixel(pixel.x, pixel.y, pixel.index) + } + } } \ No newline at end of file diff --git a/frontend/src/types/shapes.ts b/frontend/src/types/shapes.ts new file mode 100644 index 0000000..c502499 --- /dev/null +++ b/frontend/src/types/shapes.ts @@ -0,0 +1,19 @@ +export interface PixelPosition { + x: number + y: number + index: number +} + +export function FilledCircle(x: number, y: number, radius: number, index: number): PixelPosition[] { + let pixels: PixelPosition[] = [] + + for (let dx = -radius; dx <= radius; dx++) { + for (let dy = -radius; dy <= radius; dy++) { + if (dx * dx + dy * dy <= radius * radius) { + pixels.push({x: x + dx, y: y + dy, index}) + } + } + } + + return pixels +} \ No newline at end of file diff --git a/frontend/src/types/tools.ts b/frontend/src/types/tools.ts new file mode 100644 index 0000000..4e37126 --- /dev/null +++ b/frontend/src/types/tools.ts @@ -0,0 +1,78 @@ +import { PixelPlaceUndoable, type LoadedFile, PixelsPlaceUndoable } from "./file" +import { FilledCircle } from "./shapes" + +export interface ToolContext { + file: LoadedFile +} + +interface Pointer { + x: number + y: number + id: number +} + +export interface Tool { + isActive(): boolean + pointerDown(ctx: ToolContext, ptr: Pointer): void + pointerMove(ctx: ToolContext, ptr: Pointer): void + pointerUp(ctx: ToolContext, ptr: Pointer): void +} + +export interface BrushToolContext { + brushSize: number + colorIndex: number +} + +export class BrushTool implements Tool { + private active: boolean + isActive(): boolean { + return this.active + } + + pointerDown(ctx: ToolContext & BrushToolContext, ptr: Pointer) { + this.active = true + ctx.file.capture() + if (ctx.brushSize == 1) { + let p = ctx.file.canvas.getPixel(ptr.x, ptr.y) + if (p !== -1) { + ctx.file.push(new PixelPlaceUndoable(ptr.x, ptr.y, p, ctx.colorIndex)) + } + } else if (ctx.brushSize == 2) { + for (let x1 = 0; x1 < 2; x1++) { + for (let y1 = 0; y1 < 2; y1++) { + let p = ctx.file.canvas.getPixel(ptr.x+x1, ptr.y+y1) + if (p !== -1) { + ctx.file.push(new PixelPlaceUndoable(ptr.x+x1, ptr.y+y1, p, ctx.colorIndex)) + } + } + } + } else { + let shape = FilledCircle(ptr.x, ptr.y, ctx.brushSize-2, ctx.colorIndex) + ctx.file.push(new PixelsPlaceUndoable(shape)) + } + } + pointerMove(ctx: ToolContext & BrushToolContext, ptr: Pointer) { + if (ctx.brushSize == 1) { + let p = ctx.file.canvas.getPixel(ptr.x, ptr.y) + if (p !== -1) { + ctx.file.push(new PixelPlaceUndoable(ptr.x, ptr.y, p, ctx.colorIndex)) + } + } else if (ctx.brushSize == 2) { + for (let x1 = 0; x1 < 2; x1++) { + for (let y1 = 0; y1 < 2; y1++) { + let p = ctx.file.canvas.getPixel(ptr.x+x1, ptr.y+y1) + if (p !== -1) { + ctx.file.push(new PixelPlaceUndoable(ptr.x+x1, ptr.y+y1, p, ctx.colorIndex)) + } + } + } + } else { + let shape = FilledCircle(ptr.x, ptr.y, ctx.brushSize-2, ctx.colorIndex) + ctx.file.push(new PixelsPlaceUndoable(shape)) + } + } + pointerUp(ctx: ToolContext, ptr: Pointer) { + ctx.file.release() + this.active = false + } +} \ No newline at end of file