Implement tool abstraction + brush
parent
c7dc5594ec
commit
8ffce21840
|
@ -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 <Undo/>
|
||||||
|
</OverflowMenuItem>
|
||||||
|
<OverflowMenuItem on:click={() => focusedFile?.redo()} disabled={!focusedFile?.canRedo()}>
|
||||||
|
Redo <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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue