diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index c7ddd4f..235467e 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -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 @@
+
@@ -282,6 +286,7 @@
swapTool(toolFill)} />
swapTool(toolPicker)} />
swapTool(toolErase)} />
+ swapTool(toolSpray)} />
engageCopy()} />
engageDelete(true)} />
engageDelete(false)} />
@@ -293,6 +298,9 @@
{#if currentTool === toolBrush || currentTool === toolErase}
+ {:else if currentTool === toolSpray}
+ radius:
+ density:
{/if}
@@ -320,6 +328,8 @@
bind:currentTool={currentTool}
brushSize={brushSize}
brushType={brushType}
+ sprayDensity={sprayDensity}
+ sprayRadius={sprayRadius}
showCheckerboard={showCheckerboard}
checkerboardSize={checkerboardSize}
checkerboardColor1={checkerboardColor1}
diff --git a/frontend/src/sections/Editor2D.svelte b/frontend/src/sections/Editor2D.svelte
index 034d95e..2ce0847 100644
--- a/frontend/src/sections/Editor2D.svelte
+++ b/frontend/src/sections/Editor2D.svelte
@@ -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) {
diff --git a/frontend/src/types/shapes.ts b/frontend/src/types/shapes.ts
index d99e616..1c5e29f 100644
--- a/frontend/src/types/shapes.ts
+++ b/frontend/src/types/shapes.ts
@@ -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
}
\ No newline at end of file
diff --git a/frontend/src/types/tools.ts b/frontend/src/types/tools.ts
index 0defa03..c38654f 100644
--- a/frontend/src/types/tools.ts
+++ b/frontend/src/types/tools.ts
@@ -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 {