Add spraypaint tool

main
kts of kettek 2024-02-23 23:32:39 -08:00
parent 3b7428e6df
commit a16f0703b7
4 changed files with 90 additions and 4 deletions

View File

@ -14,10 +14,10 @@
import { OverflowMenu, OverflowMenuItem } from "carbon-components-svelte"
import { Close, Erase, PaintBrushAlt, RainDrop, Redo, Select_01, Undo, Scale, Eyedropper, Move, MagicWand } from "carbon-icons-svelte"
import { Close, Erase, PaintBrushAlt, RainDrop, Redo, Select_01, Undo, Scale, Eyedropper, Move, MagicWand, SprayPaint } from "carbon-icons-svelte"
import StackPreview from './sections/StackPreview.svelte'
import type { Canvas } from './types/canvas'
import { BrushTool, EraserTool, FillTool, PickerTool, SelectionTool, MagicWandTool, type BrushType, type Tool, MoveTool } from './types/tools'
import { BrushTool, EraserTool, FillTool, PickerTool, SelectionTool, MagicWandTool, type BrushType, type Tool, MoveTool, SprayTool } from './types/tools'
import BrushSize from './components/BrushSize.svelte'
import Shortcut from './components/Shortcut.svelte'
import Shortcuts from './components/Shortcuts.svelte'
@ -110,12 +110,15 @@
let toolFill = new FillTool()
let toolErase = new EraserTool()
let toolBrush = new BrushTool()
let toolSpray = new SprayTool()
let toolPicker = new PickerTool()
let toolMove = new MoveTool()
let currentTool: Tool = toolBrush
let previousTool: Tool = null
let brushSize: number = 1
let brushType: BrushType = 'circle'
let sprayRadius: number = 16
let sprayDensity: number = 2
function swapTool(tool: Tool) {
previousTool = currentTool
@ -261,6 +264,7 @@
<Button isSelected={currentTool === toolMagicWand} kind="ghost" size="small" icon={MagicWand} iconDescription="magic selection" tooltipPosition="right" on:click={()=>swapTool(toolMagicWand)}></Button>
<hr/>
<Button isSelected={currentTool === toolBrush} kind="ghost" size="small" icon={PaintBrushAlt} iconDescription="paint" tooltipPosition="right" on:click={()=>swapTool(toolBrush)}></Button>
<Button isSelected={currentTool === toolSpray} kind="ghost" size="small" icon={SprayPaint} iconDescription="spray" tooltipPosition="right" on:click={()=>swapTool(toolSpray)}></Button>
<Button isSelected={currentTool === toolPicker} kind="ghost" size="small" icon={Eyedropper} iconDescription="pick" tooltipPosition="right" on:click={()=>swapTool(toolPicker)}></Button>
<Button isSelected={currentTool === toolErase} kind="ghost" size="small" icon={Erase} iconDescription="erase" tooltipPosition="right" on:click={()=>swapTool(toolErase)}></Button>
<Button isSelected={currentTool === toolFill} kind="ghost" size="small" icon={RainDrop} iconDescription="fill" tooltipPosition="right" on:click={()=>swapTool(toolFill)}></Button>
@ -282,6 +286,7 @@
<Shortcut global cmd='fill' keys={['f']} on:trigger={()=>swapTool(toolFill)} />
<Shortcut global cmd='picker' keys={['i']} on:trigger={()=>swapTool(toolPicker)} />
<Shortcut global cmd='erase' keys={['e']} on:trigger={()=>swapTool(toolErase)} />
<Shortcut global cmd='erase' keys={['p']} on:trigger={()=>swapTool(toolSpray)} />
<Shortcut global cmd='copy' keys={['ctrl+c']} on:trigger={()=>engageCopy()} />
<Shortcut global cmd='cut' keys={['ctrl+x']} on:trigger={()=>engageDelete(true)} />
<Shortcut global cmd='delete' keys={['delete']} on:trigger={()=>engageDelete(false)} />
@ -293,6 +298,9 @@
{#if currentTool === toolBrush || currentTool === toolErase}
<BrushSize bind:brushSize bind:brushType/>
<NumberInput size="sm" min={1} max={100} step={1} bind:value={brushSize}/>
{:else if currentTool === toolSpray}
radius:&nbsp; <NumberInput size="sm" min={1} max={100} step={1} bind:value={sprayRadius}/>
density:&nbsp; <NumberInput size="sm" min={1} max={100} step={1} bind:value={sprayDensity}/>
{/if}
</menu>
<Tabs bind:selected={focusedFileIndex}>
@ -320,6 +328,8 @@
bind:currentTool={currentTool}
brushSize={brushSize}
brushType={brushType}
sprayDensity={sprayDensity}
sprayRadius={sprayRadius}
showCheckerboard={showCheckerboard}
checkerboardSize={checkerboardSize}
checkerboardColor1={checkerboardColor1}

View File

@ -4,7 +4,7 @@
import type { data } from '../../wailsjs/go/models.ts'
import type { LoadedFile } from '../types/file'
import { FilledCircle, FilledSquare, type PixelPosition } from '../types/shapes'
import { BrushTool, EraserTool, FillTool, PickerTool, MoveTool, type BrushType, type Tool, SelectionTool } from '../types/tools'
import { BrushTool, EraserTool, FillTool, PickerTool, MoveTool, type BrushType, type Tool, SelectionTool, SprayTool } from '../types/tools'
import { Button, NumberInput, OverflowMenu, OverflowMenuItem, Slider } from 'carbon-components-svelte';
import { ZoomIn, ZoomOut } from 'carbon-icons-svelte';
@ -46,6 +46,8 @@
export let secondaryColorIndex: number
export let brushSize: number
export let brushType: BrushType
export let sprayRadius: number
export let sprayDensity: number
let rootCanvas: HTMLCanvasElement
let overlayCanvas: HTMLCanvasElement = document.createElement('canvas')
@ -314,6 +316,8 @@
currentTool.pointerDown({file, brushSize, brushType, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: e.button, shift: e.shiftKey, control: e.ctrlKey })
} else if (currentTool instanceof EraserTool) {
currentTool.pointerDown({file, brushSize, brushType}, {x: mousePixelX, y: mousePixelY, id: e.button, shift: e.shiftKey, control: e.ctrlKey })
} else if (currentTool instanceof SprayTool) {
currentTool.pointerDown({file, radius: sprayRadius, density: sprayDensity, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: e.button, shift: e.shiftKey, control: e.ctrlKey })
} else if (currentTool instanceof FillTool) {
currentTool.pointerDown({file, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: e.button, shift: e.shiftKey, control: e.ctrlKey })
} else if (currentTool instanceof PickerTool) {
@ -377,6 +381,8 @@
currentTool.pointerMove({file, brushSize, brushType, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: 0 })
} else if (currentTool instanceof EraserTool) {
currentTool.pointerMove({file, brushSize, brushType}, {x: mousePixelX, y: mousePixelY, id: 0 })
} else if (currentTool instanceof SprayTool) {
currentTool.pointerMove({file, radius: sprayRadius, density: sprayDensity, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: e.button, shift: e.shiftKey, control: e.ctrlKey })
} else if (currentTool instanceof FillTool) {
currentTool.pointerMove({file, colorIndex: primaryColorIndex}, {x: mousePixelX, y: mousePixelY, id: 0 })
} else if (currentTool instanceof PickerTool) {

View File

@ -29,5 +29,25 @@ export function FilledSquare(x: number, y: number, size: number, index: number):
}
}
return pixels
}
export function RandomSpray(x: number, y: number, radius: number, density: number, index: number): PixelPosition[] {
let pixels: PixelPosition[] = []
for (let i = 0; i < density; i++) {
// Get dx/dy in a circle.
let r = Math.sqrt(Math.random()) * radius
let theta = Math.random() * 2 * Math.PI
let dx = Math.floor(r * Math.cos(theta))
let dy = Math.floor(r * Math.sin(theta))
// Get dx/dy in a square.
//let dx = Math.floor(Math.random() * radius * 2) - radius
//let dy = Math.floor(Math.random() * radius * 2) - radius
pixels.push({x: x + dx, y: y + dy, index})
}
return pixels
}

View File

@ -1,6 +1,6 @@
import { PixelPlaceUndoable, type LoadedFile, PixelsPlaceUndoable, SelectionClearUndoable, SelectionSetUndoable, SelectionMoveUndoable } from "./file"
import { Preview } from "./preview"
import { FilledCircle, FilledSquare, type PixelPosition } from "./shapes"
import { FilledCircle, FilledSquare, RandomSpray, type PixelPosition } from "./shapes"
export interface ToolContext {
file: LoadedFile
@ -29,6 +29,12 @@ export interface BrushToolContext {
colorIndex: number
}
export interface SprayToolContext {
radius: number
density: number
colorIndex: number
}
export interface EraserToolContext {
brushSize: number
brushType: BrushType
@ -142,6 +148,50 @@ export class EraserTool extends BrushTool {
}
}
export class SprayTool implements Tool {
private active: boolean
private lastX: number
private lastY: number
isActive(): boolean {
return this.active
}
pointerDown(ctx: ToolContext & SprayToolContext, ptr: Pointer) {
this.active = true
this.lastX = ptr.x
this.lastY = ptr.y
ctx.file.capture()
let pixels = RandomSpray(ptr.x, ptr.y, ctx.radius, ctx.density, ctx.colorIndex)
pixels = pixels.filter(p => ctx.file.selection.isPixelMarked(p.x, p.y))
ctx.file.push(new PixelsPlaceUndoable(pixels))
}
pointerMove(ctx: ToolContext & SprayToolContext, ptr: Pointer) {
let dx = this.lastX - ptr.x
let dy = this.lastY - ptr.y
this.lastX = ptr.x
this.lastY = ptr.y
let angle = Math.atan2(dy, dx)
let dist = Math.sqrt(dx*dx + dy*dy)
let steps = Math.ceil(dist)
for (let i = 0; i < steps; i++) {
let x = Math.floor(this.lastX + Math.cos(angle) * i)
let y = Math.floor(this.lastY + Math.sin(angle) * i)
if (x < 0 || y < 0 || x >= ctx.file.canvas.width || y >= ctx.file.canvas.height) continue
let pixels = RandomSpray(x, y, ctx.radius, ctx.density, ctx.colorIndex)
pixels = pixels.filter(p => ctx.file.selection.isPixelMarked(p.x, p.y))
ctx.file.push(new PixelsPlaceUndoable(pixels))
}
}
pointerUp(ctx: ToolContext, ptr: Pointer): void {
ctx.file.release()
this.active = false
}
}
export class FillTool implements Tool {
private active: boolean
isActive(): boolean {