Implement tool abstraction + brush

main
kts of kettek 2024-02-14 13:38:46 -08:00
parent c7dc5594ec
commit 8ffce21840
5 changed files with 172 additions and 30 deletions

View File

@ -6,15 +6,15 @@
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 { LoadedFile } from './types/file.ts' import { LoadedFile } from './types/file'
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, ButtonSet } from "carbon-components-svelte"
import { ComposedModal } from "carbon-components-svelte" import { ComposedModal } from "carbon-components-svelte"
import { OverflowMenu, OverflowMenuItem } 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 StackPreview from './sections/StackPreview.svelte'
import type { Canvas } from './types/canvas' import type { Canvas } from './types/canvas'
@ -59,7 +59,7 @@
<Theme bind:theme/> <Theme bind:theme/>
<main> <main>
<menu> <menu class="mainMenu">
<OverflowMenu size="sm"> <OverflowMenu size="sm">
<div slot="menu">File</div> <div slot="menu">File</div>
<OverflowMenuItem text="New"/> <OverflowMenuItem text="New"/>
@ -71,8 +71,12 @@
</OverflowMenu> </OverflowMenu>
<OverflowMenu size="sm"> <OverflowMenu size="sm">
<div slot="menu">Edit</div> <div slot="menu">Edit</div>
<OverflowMenuItem text="Undo" on:click={() => focusedFile?.undo()} disabled={!focusedFile?.canUndo()}/> <OverflowMenuItem on:click={() => focusedFile?.undo()} disabled={!focusedFile?.canUndo()}>
<OverflowMenuItem text="Redo" on:click={() => focusedFile?.redo()} disabled={!focusedFile?.canRedo()}/> Undo &nbsp; <Undo/>
</OverflowMenuItem>
<OverflowMenuItem on:click={() => focusedFile?.redo()} disabled={!focusedFile?.canRedo()}>
Redo &nbsp; <Redo/>
</OverflowMenuItem>
</OverflowMenu> </OverflowMenu>
<OverflowMenu size="sm"> <OverflowMenu size="sm">
<div slot="menu">Windows</div> <div slot="menu">Windows</div>
@ -83,6 +87,11 @@
<section class='left'> <section class='left'>
<PaletteSection bind:palette bind:primaryColorIndex bind:secondaryColorIndex file={focusedFile} /> <PaletteSection bind:palette bind:primaryColorIndex bind:secondaryColorIndex file={focusedFile} />
</section> </section>
<menu class='toolbar'>
<Button kind="ghost" size="small" icon={Select_01} iconDescription="selection"></Button>
<Button kind="ghost" size="small" icon={PaintBrushAlt} iconDescription="paint"></Button>
<Button kind="ghost" size="small" icon={Erase} iconDescription="erase"></Button>
</menu>
<section class='middle'> <section class='middle'>
<Tabs> <Tabs>
{#each files as file, index} {#each files as file, index}
@ -130,18 +139,18 @@
display: grid; display: grid;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
} }
menu { .mainMenu {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
} }
:global(menu > button) { :global(.mainMenu > button) {
width: 4rem !important; width: 4rem !important;
color: var(--cds-text-02, #c6c6c6); color: var(--cds-text-02, #c6c6c6);
} }
.content { .content {
display: grid; display: grid;
grid-template-columns: 1fr 4fr; grid-template-columns: 1fr auto 4fr;
grid-template-rows: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
} }
.left { .left {
@ -149,6 +158,11 @@
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
} }
.toolbar {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.middle { .middle {
display: grid; display: grid;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);

View File

@ -2,7 +2,9 @@
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 { 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 file: LoadedFile
export let animation: data.Animation export let animation: data.Animation
@ -32,6 +34,11 @@
let canvasDirty: boolean = true let canvasDirty: boolean = true
let traversedPixels: Set<number> = new Set() let traversedPixels: Set<number> = new Set()
function addTraversedPixels(pixels: PixelPosition[]) {
for (let p of pixels) {
traversedPixels.add(p.x+p.y*file.canvas.width)
}
}
// check resizes canvases, etc. // check resizes canvases, etc.
function check() { function check() {
@ -156,6 +163,8 @@
} }
} }
let currentTool: Tool = new BrushTool()
function canvasMousedown(node) { function canvasMousedown(node) {
let buttons: Set<number> = new Set() let buttons: Set<number> = new Set()
let x: number = 0 let x: number = 0
@ -176,13 +185,8 @@
y = e.clientY y = e.clientY
if (e.button === 0) { if (e.button === 0) {
console.log('0 click') if (currentTool instanceof BrushTool) {
traversedPixels.clear() currentTool.pointerDown({file, brushSize: 3, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: e.button })
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)
} }
} }
}) })
@ -241,13 +245,9 @@
y = e.clientY y = e.clientY
if (buttons.has(0)) { if (buttons.has(0)) {
console.log('0') if (currentTool.isActive()) {
if (currentTool instanceof BrushTool) {
if (!traversedPixels.has(mousePixelX+mousePixelY*file.canvas.width)) { currentTool.pointerMove({file, brushSize: 3, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: 0 })
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))
} }
} }
} }
@ -267,8 +267,9 @@
if (buttons.size === 0) return if (buttons.size === 0) return
if (e.button === 0) { if (e.button === 0) {
file.release() if (currentTool.isActive()) {
console.log('release') currentTool.pointerUp({file}, {x: mousePixelX, y: mousePixelY, id: 0 })
}
} }
buttons.delete(e.button) buttons.delete(e.button)

View File

@ -1,6 +1,6 @@
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'
import { UndoableStack, type Undoable } from './undo.ts' import { UndoableStack, type Undoable } from './undo'
export interface LoadedFileOptions { export interface LoadedFileOptions {
filepath: string filepath: string
@ -50,10 +50,40 @@ export class PixelPlaceUndoable implements Undoable<LoadedFile> {
this.newIndex = newIndex this.newIndex = newIndex
} }
apply(file: LoadedFile) { apply(file: LoadedFile) {
console.log('apply', this.x, this.y, this.newIndex)
file.canvas.setPixel(this.x, this.y, this.newIndex) file.canvas.setPixel(this.x, this.y, this.newIndex)
} }
unapply(file: LoadedFile) { unapply(file: LoadedFile) {
file.canvas.setPixel(this.x, this.y, this.oldIndex) file.canvas.setPixel(this.x, this.y, this.oldIndex)
} }
} }
export class PixelsPlaceUndoable implements Undoable<LoadedFile> {
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)
}
}
}

View File

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

View File

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