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