From a16f0703b7fd5ef7f84c97d24f19726bdcb982b2 Mon Sep 17 00:00:00 2001 From: kts of kettek Date: Fri, 23 Feb 2024 23:32:39 -0800 Subject: [PATCH] Add spraypaint tool --- frontend/src/App.svelte | 14 ++++++-- frontend/src/sections/Editor2D.svelte | 8 ++++- frontend/src/types/shapes.ts | 20 +++++++++++ frontend/src/types/tools.ts | 52 ++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) 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 {