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 { 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 @@
<Theme bind:theme/>
<main>
<menu>
<menu class="mainMenu">
<OverflowMenu size="sm">
<div slot="menu">File</div>
<OverflowMenuItem text="New"/>
@ -71,8 +71,12 @@
</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()}/>
<OverflowMenuItem on:click={() => focusedFile?.undo()} disabled={!focusedFile?.canUndo()}>
Undo &nbsp; <Undo/>
</OverflowMenuItem>
<OverflowMenuItem on:click={() => focusedFile?.redo()} disabled={!focusedFile?.canRedo()}>
Redo &nbsp; <Redo/>
</OverflowMenuItem>
</OverflowMenu>
<OverflowMenu size="sm">
<div slot="menu">Windows</div>
@ -83,6 +87,11 @@
<section class='left'>
<PaletteSection bind:palette bind:primaryColorIndex bind:secondaryColorIndex file={focusedFile} />
</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'>
<Tabs>
{#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);

View File

@ -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<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.
function check() {
@ -155,7 +162,9 @@
offsetY = canvas.height-30
}
}
let currentTool: Tool = new BrushTool()
function canvasMousedown(node) {
let buttons: Set<number> = 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)

View File

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