diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/lib/graphics/stroke.ts | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/lib/graphics/stroke.ts')
| -rw-r--r-- | apps/web/src/lib/graphics/stroke.ts | 102 |
1 files changed, 102 insertions, 0 deletions
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, + }); +} |
