From 8db3bead13441a5882d8689dfc6a9f09c82375d9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Mar 2026 16:47:22 +0100 Subject: feat: masks, properties refactor, shaders, storage migrations, and more --- apps/web/src/lib/graphics/stroke.ts | 102 ++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 apps/web/src/lib/graphics/stroke.ts (limited to 'apps/web/src/lib/graphics/stroke.ts') diff --git a/apps/web/src/lib/graphics/stroke.ts b/apps/web/src/lib/graphics/stroke.ts new file mode 100644 index 00000000..98f9f43f --- /dev/null +++ b/apps/web/src/lib/graphics/stroke.ts @@ -0,0 +1,102 @@ +import type { GraphicStrokeAlign } from "./definitions/shared"; + +type GraphicRenderContext = + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D; + +function createTempCanvas({ + width, + height, +}: { + width: number; + height: number; +}): OffscreenCanvas | HTMLCanvasElement { + try { + return new OffscreenCanvas(width, height); + } catch { + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + return canvas; + } +} + +function applyStroke({ + ctx, + path, + strokeWidth, + strokeColor, +}: { + ctx: GraphicRenderContext; + path: Path2D; + strokeWidth: number; + strokeColor: string; +}) { + ctx.strokeStyle = strokeColor; + ctx.lineWidth = strokeWidth; + ctx.stroke(path); +} + +export function applyAlignedStroke({ + ctx, + path, + strokeWidth, + strokeAlign, + strokeColor, +}: { + ctx: GraphicRenderContext; + path: Path2D; + strokeWidth: number; + strokeAlign: GraphicStrokeAlign; + strokeColor: string; +}): void { + if (strokeWidth <= 0) { + return; + } + + if (strokeAlign === "inside") { + ctx.save(); + ctx.clip(path); + applyStroke({ + ctx, + path, + strokeWidth: strokeWidth * 2, + strokeColor, + }); + ctx.restore(); + return; + } + + if (strokeAlign === "outside") { + const strokeCanvas = createTempCanvas({ + width: ctx.canvas.width, + height: ctx.canvas.height, + }); + const strokeCtx = strokeCanvas.getContext("2d") as GraphicRenderContext | null; + if (!strokeCtx) { + return; + } + + applyStroke({ + ctx: strokeCtx, + path, + strokeWidth: strokeWidth * 2, + strokeColor, + }); + + // Keep only the outer half of the doubled stroke so alpha fills do not + // leave a visible inner stroke behind. + strokeCtx.globalCompositeOperation = "destination-out"; + strokeCtx.fill(path); + + ctx.drawImage(strokeCanvas, 0, 0); + return; + } + + applyStroke({ + ctx, + path, + strokeWidth, + strokeColor, + }); +} -- cgit v1.3.1