Add spraypaint tool
parent
3b7428e6df
commit
a16f0703b7
|
@ -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: <NumberInput size="sm" min={1} max={100} step={1} bind:value={sprayRadius}/>
|
||||
density: <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}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue