1"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([["9716"],{833(e,n,s){s.r(n),s.d(n,{metadata:()=>r,default:()=>u,frontMatter:()=>l,contentTitle:()=>o,toc:()=>d,assets:()=>c});var r=JSON.parse('{"id":"api/canvas","title":"Canvas","description":"Canvas is the ergonomic entry point. It accepts either a canvas element or a","source":"@site/docs/api/canvas.mdx","sourceDirName":"api","slug":"/api/canvas","permalink":"/docs/api/canvas","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_position":1,"title":"Canvas"},"sidebar":"docsSidebar","previous":{"title":"Svelte","permalink":"/docs/guides/integration-svelte"},"next":{"title":"Brush","permalink":"/docs/api/brush"}}'),t=s(4848),a=s(8453),i=s(7014);let l={sidebar_position:1,title:"Canvas"},o="Canvas",c={},d=[{value:"Constructor Options",id:"constructor-options",level:2},{value:"Options",id:"options",level:3},{value:"Defaults",id:"defaults",level:2},{value:"Pressure",id:"pressure",level:2},{value:"Layer System",id:"layer-system",level:2},{value:"Layer Management",id:"layer-management",level:3},{value:"Blend Modes",id:"blend-modes",level:3},{value:"Layer Properties",id:"layer-properties",level:3},{value:"Direct Layer Manager Access",id:"direct-layer-manager-access",level:3},{value:"API Reference",id:"api-reference",level:2},{value:"<code>resize()</code>",id:"resize",level:3},{value:"<code>setDocumentSize(width, height)</code>",id:"setdocumentsizewidth-height",level:3},{value:"<code>undo()</code>, <code>redo()</code>, <code>clear()</code>",id:"undo-redo-clear",level:3},{value:"<code>history</code>",id:"history",level:3},{value:"<code>setSmooth(enabled)</code>, <code>setSpacing(enabled)</code>, <code>setEraser(enabled)</code>",id:"setsmoothenabled-setspacingenabled-seteraserenabled",level:3},{value:"<code>loadConfig(config)</code>",id:"loadconfigconfig",level:3},{value:"<code>loadImage(img)</code>",id:"loadimageimg",level:3},{value:"<code>destroy()</code>",id:"destroy",level:3},{value:"Document Persistence API",id:"document-persistence-api",level:2},{value:"<code>exportDocument(): DocumentData</code>",id:"exportdocument-documentdata",level:3},{value:"<code>importDocument(data: DocumentData): void</code>",id:"importdocumentdata-documentdata-void",level:3},{value:"<code>exportPNG(options?: { includeBackground?: boolean; backgroundColor?: string }): string</code>",id:"exportpngoptions--includebackground-boolean-backgroundcolor-string--string",level:3},{value:"Native Raster Commands",id:"native-raster-commands",level:2},{value:"<code>
1floodFill(x: number, y: number, color: string, tolerance?: number): void</code>",id:"floodfillx-number-y-number-color-string-tolerance-number-void",level:3},{value:"<code>drawRectangle(x, y, width, height, options?): void</code>",id:"drawrectanglex-y-width-height-options-void",level:3},{value:"<code>drawEllipse(cx, cy, rx, ry, options?): void</code>",id:"drawellipsecx-cy-rx-ry-options-void",level:3},{value:"<code>drawLine(x1, y1, x2, y2, options?): void</code>",id:"drawlinex1-y1-x2-y2-options-void",level:3},{value:"<code>drawText(text, x, y, options?): void</code>",id:"drawtexttext-x-y-options-void",level:3},{value:"<code>getColorAt(x: number, y: number, source?: "active" | "composite"): { r, g, b, a, hex }</code>",id:"getcoloratx-number-y-number-source-active--composite--r-g-b-a-hex-",level:3},{value:"Event System & State Snapshots",id:"event-system--state-snapshots",level:2},{value:"<code>on(event, handler)</code> / <code>off(event, handler)</code>",id:"onevent-handler--offevent-handler",level:3},{value:"<code>getSnapshot(): CanvasSnapshot</code>",id:"getsnapshot-canvassnapshot",level:3},{value:"History Navigation API",id:"history-navigation-api",level:2},{value:"<code>history.goTo(index: number): void</code>",id:"historygotoindex-number-void",level:3},{value:"<code>history.getStepSummaries(): HistoryStepSummary[]</code>",id:"historygetstepsummaries-historystepsummary",level:3}];function h(e){let n={code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"canvas",children:"Canvas"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"Canvas"})," is the ergonomic entry point. It accepts either a canvas element or a\nquery selector, creates a ",(0,t.jsx)(n.code,{children:"Brush"}),", configures the canvas for the current device\npixel ratio or an explicit document size, and binds pointer events."]}),"\n",(0,t.jsx)(n.p,{children:"The Canvas API is part of the stable 1.0.0 surface and is intended for\nproduction-ready drawing experiences."}),"\n",(0,t.jsx)(i.A,{title:"Canvas API behavior",description:"Draw here to see pointer input, pressure simulation, undo, redo, clear, and eraser mode working through the Canvas wrapper."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'import { Canvas } from "fuderu";\n\nconst painter = new Canvas({\n canvas: document.querySelector("canvas")!,\n document: {\n width: 1024,\n height: 768,\n },\n pressureSimulation: true,\n brush: {\n color: "#222222",\n size: 18,\n roundness: 0.8,\n },\n});\n'})}),"\n",(0,t.jsx)(n.h2,{id:"constructor-options",children:"Constructor Options"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"interface CanvasOptions {\n canvas: HTMLCanvasElement | string;\n /**\n * The logical drawing resolution.\n * This is the size of the internal pixel buffer, independent of how\n * the canvas element is sized on screen via CSS.\n * Defaults to the displayed canvas size multiplied by devicePixelRatio.\n */\n document?: {\n width: number;\n height: number;\n };\n brush?: BrushConfig;\n pressureSimulation?: boolean;\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"options",children:"Options"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Option"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"canvas"})}),(0,t.jsx)(n.td,{children:"The canvas element or a CSS selector string"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"document"})}),(0,t.jsxs)(n.td,{children:["Optional logical drawing dimensions (width/height in pixels). If not provided, defaults to the canvas element's display size multiplied by ",(0,t.jsx)(n.code,{children:"window.devicePixelRatio"})]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"brush"})}),(0,t.jsx)(n.td,{children:"Optional brush configuration"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"pressureSimulation"})}),(0,t.jsxs)(n.td,{children:["When ",(0,t.jsx)(n.code,{children:"true"}),", mouse/touch input uses simulated pressure based on movement speed. Defaults to ",(0,t.jsx)(n.code,{children:"false"}),"."]})]})]})]}),"\n",(0,t.jsx)(n.h2,{id:"defaults",children:"Defaults"}),"\n",(0,t.jsxs)(n.p,{children:["All options are optional except ",(0,t.jsx)(n.code,{children:"canvas"}),". Here are the default values:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'{\n // canvas: required (no default)\n document: undefined, // defaults to canvas size * devicePixelRatio\n brush: {\n size: 20,\n opacity: 1,\n flow: 1,\n color: "#000000",\n angle: 0,\n roundness: 1,\n spacing: 0.12,\n eraser: false,\n },\n pressureSimulation: false,\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"pressure",children:"Pressure"}),"\n",(0,t.jsxs)(n.p,{children:["Real pen pressure is used when the pointer is a pen and ",(0,t.jsx)(n.code,{children:"event.pressure"})," is\navailable. Mouse and touch input can use simulated pressure:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'const painter = new Canvas({\n canvas: "#paint",\n pressureSimulation: false,\n});\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Set ",(0,t.jsx)(n.code,{children:"pressureSimulation"})," to ",(0,t.jsx)(n.code,{children:"true"})," to enable simulated pressure based on movement\nspeed for mouse and touch input."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"lastPressure"})," stores the most recent pressure sent to the brush, which is handy\nfor UI meters without advancing the pressure simulator again."]}),"\n",(0,t.jsx)(n.h2,{id:"layer-system",children:"Layer System"}),"\n",(0,t.jsx)(n.p,{children:"Fuderu includes a professional layer system that allows you to create, manage,\nand composite multiple layers similar to professional digital art software."}),"\n",(0,t.jsx)(n.h3,{id:"layer-management",children:"Layer Management"}),"\n",(0,t.jsxs)(n.p,{children:["The Canvas instance includes a ",(0,t.jsx)(n.code,{children:"layers"})," property that provides access to the\nlayer management API:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'// Create a new layer (this automatically sets it as active)\nconst layer = painter.createLayer({\n name: "Line Art",\n opacity: 0.8,\n blendMode: "multiply",\n});\n\n// Get the active layer (which is now the new layer)\nconst activeLayer = painter.getActiveLayer();\n\n// Set a layer as active manually using its ID\npainter.setActiveLayer(layer.id);
1\n\n// Delete a layer by ID\npainter.deleteLayer("some-layer-id");\n\n// Duplicate a layer\nconst duplicatedLayer = painter.duplicateLayer(layer.id);\n\n// Get all layers\nconst allLayers = painter.getLayers();\n\n// Update layer properties by ID\npainter.updateLayer(layer.id, {\n opacity: 0.5,\n blendMode: "screen",\n visible: true,\n});\n'})}),"\n",(0,t.jsx)(n.h3,{id:"blend-modes",children:"Blend Modes"}),"\n",(0,t.jsx)(n.p,{children:"Layers support standard Canvas 2D blend modes for advanced compositing effects:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"source-over"})})," (default) - Normal layer stacking"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"multiply"})})," - Darkens the base layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"screen"})})," - Lightens the base layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"overlay"})})," - Combines multiply and screen"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"darken"})})," - Keeps the darker of the two layers"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"lighten"})})," - Keeps the lighter of the two layers"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"color-dodge"})})," - Brightens the base layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"color-burn"})})," - Darkens the base layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"hard-light"})})," - Intense overlay effect"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"soft-light"})})," - Soft overlay effect"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"difference"})})," - Subtracts the darker color from the lighter"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"exclusion"})})," - Similar to difference but lower contrast"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"hue"})})," - Applies the hue of the blend layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"saturation"})})," - Applies the saturation of the blend layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"color"})})," - Applies both hue and saturation"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"luminosity"})})," - Applies the luminosity of the blend layer"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Example using blend modes for a multiply blending effect:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'// Create a base color layer\nconst baseLayer = painter.createLayer({\n name: "Base Color",\n blendMode: "source-over",\n});\n\n// Draw your base colors on this layer\n// ...\n\n// Create a shading layer with multiply blend mode\nconst shadeLayer = painter.createLayer({\n name: "Shading",\n blendMode: "multiply",\n});\n\n// Draw shadows and shading on this layer\n// The multiply blend mode will darken the colors below\n'})}),"\n",(0,t.jsx)(n.h3,{id:"layer-properties",children:"Layer Properties"}),"\n",(0,t.jsx)(n.p,{children:"Each layer has the following properties:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"id"})})," - Unique identifier for the layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"name"})})," - Human-readable name for the layer"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"visible"})})," - Whether the layer is visible (default: true)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"opacity"})})," - Opacity of the layer (0-1, default: 1)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"blendMode"})})," - How the layer composites with layers below it"]}),"\n"]}
1),"\n",(0,t.jsx)(n.h3,{id:"direct-layer-manager-access",children:"Direct Layer Manager Access"}),"\n",(0,t.jsx)(n.p,{children:"For advanced layer operations, you can access the underlying `LayerManager` instance via the `layers` property on the Canvas:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// Move a layer to a specific index (0-based)\npainter.layers.moveLayer(layerId, targetIndex);\n\n// Resize all layers (called automatically when canvas size changes)\npainter.layers.resize(width, height);\n\n// Clear all layers\npainter.layers.clear();\n\n// Get readonly array of all layers\nconst allLayers = painter.layers.getAll();\n"})}),"\n",(0,t.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,t.jsx)(n.h3,{id:"resize",children:(0,t.jsx)(n.code,{children:"resize()"})}),"\n",(0,t.jsx)(n.p,{children:"Resize the internal drawing buffer to match the element's display size.\nAlso reinitialises the brush context so the brush's offscreen canvases\nmatch the new pixel buffer."}),"\n",(0,t.jsx)(n.h3,{id:"setdocumentsizewidth-height",children:(0,t.jsx)(n.code,{children:"setDocumentSize(width, height)"})}),"\n",(0,t.jsx)(n.p,{children:"Change the logical document size."}),"\n",(0,t.jsx)(n.p,{children:"This clears all artwork and resets the undo stack.\nIt does NOT change any CSS on the canvas element.\nSizing the element on screen is the caller's responsibility."}),"\n",(0,t.jsxs)(n.h3,{id:"undo-redo-clear",children:[(0,t.jsx)(n.code,{children:"undo()"}),", ",(0,t.jsx)(n.code,{children:"redo()"}),", ",(0,t.jsx)(n.code,{children:"clear()"})]}),"\n",(0,t.jsx)(n.p,{children:"Undo/redo the last user action or clear the canvas."}),"\n",(0,t.jsxs)(n.p,{children:["Starting in version 1.1.0, ",(0,t.jsx)(n.code,{children:"Canvas"})," manages a unified history stack via ",(0,t.jsx)(n.code,{children:"HistoryManager"}),". It supports undoing and redoing not just paint strokes, but also a variety of layer operations:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Drawing strokes"})," (",(0,t.jsx)(n.code,{children:"CanvasStateHistoryEntry"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Layer creations"})," (",(0,t.jsx)(n.code,{children:"LayerCreatedHistoryEntry"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Layer deletions"})," (",(0,t.jsx)(n.code,{children:"LayerDeletedHistoryEntry"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Layer reorderings"})," (",(0,t.jsx)(n.code,{children:"MoveLayerHistoryEntry"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Layer property changes"})," such as name, visibility, opacity, and blend modes (",(0,t.jsx)(n.code,{children:"LayerPropertyHistoryEntry"}),")"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Because history is managed at the high-level ",(0,t.jsx)(n.code,{children:"Canvas"})," wrapper, ",(0,t.jsx)(n.strong,{children:"switching the active layer no longer wipes the undo/redo stack"}),". Users can switch layers freely and still undo/redo their strokes across layers seamlessly."]}),"\n",(0,t.jsx)(n.h3,{id:"history",children:(0,t.jsx)(n.code,{children:"history"})}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"HistoryManager"})," instance on the canvas. It provides low-level control over the undo/redo stack:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// Check if undo or redo are available\nconst canUndo = painter.history.canUndo();\nconst canRedo = painter.history.canRedo();\n\n// Clear the history stack manually\npainter.history.clear();\n"})}),"\n",(0,t.jsxs)(n.h3,{id:"setsmoothenabled-setspacingenabled-seteraserenabled",children:[(0,t.jsx)(n.code,{children:"setSmooth(enabled)"}),", ",(0,t.jsx)(n.code,{children:"setSpacing(enabled)"}),", ",(0,t.jsx)(n.code,{children:"setEraser(enabled)"})]}),"\n",(0,t.jsx)(n.p,{children:"Toggle brush smoothing, spacing, and eraser mode."}),"\n",(0,t.jsx)(n.h3,{id:"loadconfigconfig",children:(0,t.jsx)(n.code,{children:"loadConfig(config)"})}),"\n",(0,t.jsx)(n.p,{children:"Update brush configuration."}),"\n",(0,t.jsx)(n.h3,{id:"loadimageimg",children:(0,t.jsx)(n.code,{children:"loadImage(img)"})}),"\n",(0,t.jsx)(n.p,{children:"Load an image for use as an image brush."}),"\n",(0,t.jsx)(n.h3,{id:"destroy",children:(0,t.jsx)(n.code,{children:"destroy()"})}),"\n",(0,t.jsx)(n.p,{children:"Clean up event listeners and references."}),"\n",(0,t.jsx)(n.h2,{id:"document-persistence-api",children:"Document Persistence API"}),"\n",(0,t.jsx)(n.p,{children:"Fuderu 1.3.0 introduces native document persistence for saving and restoring complete canvas compositions."}),"\n",(0,t.jsx)(n.h3,{id:"exportdocument-documentdata",children:(0,t.jsx)(n.code,{children:"exportDocument(): DocumentData"})}),"\n",(0,t.jsx)(n.p,{children:"Serializes the entire canvas document state\u2014including dimensions, version, active layer ID, and pixel data for every layer (as PNG data URLs)\u2014into a structured JSON object:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"const docData = painter.exportDocument();\n// Save docData as JSON string to localStorage, server, or file\n"})}),"\n",(0,t.jsx)(n.h3,{id:"importdocumentdata-documentdata-void",children:(0,t.jsx)(n.code,{children:"importDocument(data: DocumentData): void"})}),"\n",(0,t.jsxs)(n.p,{children:["Restores the complete canvas state from a serialized ",(0,t.jsx)(n.code,{children:"DocumentData"})," object:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"painter.importDocument(docData);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"exportpngoptions--includebackground-boolean-backgroundcolor-string--string",children:(0,t.jsx)(n.code,{children:"exportPNG(options?: { includeBackgroun
1d?: boolean; backgroundColor?: string }): string"})}),"\n",(0,t.jsx)(n.p,{children:"Renders a flattened composite image of all visible layers and returns a PNG Data URL:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'// Export transparent PNG composite\nconst pngDataUrl = painter.exportPNG();\n\n// Export with opaque background\nconst solidPng = painter.exportPNG({\n includeBackground: true,\n backgroundColor: "#ffffff",\n});\n'})}),"\n",(0,t.jsx)(n.h2,{id:"native-raster-commands",children:"Native Raster Commands"}),"\n",(0,t.jsx)(n.p,{children:"Fuderu provides built-in utilities for shape primitives, flood fill, text, and color sampling:"}),"\n",(0,t.jsx)(n.h3,{id:"floodfillx-number-y-number-color-string-tolerance-number-void",children:(0,t.jsx)(n.code,{children:"floodFill(x: number, y: number, color: string, tolerance?: number): void"})}),"\n",(0,t.jsxs)(n.p,{children:["Performs a fast scanline flood fill on the active layer starting at ",(0,t.jsx)(n.code,{children:"(x, y)"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'// Fill bounded region with blue (0-255 tolerance)\npainter.floodFill(150, 200, "#2563eb", 10);\n'})}),"\n",(0,t.jsx)(n.h3,{id:"drawrectanglex-y-width-height-options-void",children:(0,t.jsx)(n.code,{children:"drawRectangle(x, y, width, height, options?): void"})}),"\n",(0,t.jsx)(n.p,{children:"Draws a vector rectangle primitive onto the active layer:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'painter.drawRectangle(50, 50, 200, 150, {\n strokeColor: "#000000",\n strokeWidth: 4,\n fillColor: "#3b82f6",\n filled: true,\n});\n'})}),"\n",(0,t.jsx)(n.h3,{id:"drawellipsecx-cy-rx-ry-options-void",children:(0,t.jsx)(n.code,{children:"drawEllipse(cx, cy, rx, ry, options?): void"})}),"\n",(0,t.jsx)(n.p,{children:"Draws a vector ellipse primitive onto the active layer:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'painter.drawEllipse(150, 150, 80, 50, {\n strokeColor: "#ef4444",\n strokeWidth: 2,\n});\n'})}),"\n",(0,t.jsx)(n.h3,{id:"drawlinex1-y1-x2-y2-options-void",children:(0,t.jsx)(n.code,{children:"drawLine(x1, y1, x2, y2, options?): void"})}),"\n",(0,t.jsx)(n.p,{children:"Draws a straight line onto the active layer:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'painter.drawLine(0, 0, 400, 300, {\n strokeColor: "#10b981",\n strokeWidth: 6,\n lineCap: "round",\n});\n'})}),"\n",(0,t.jsx)(n.h3,{id:"drawtexttext-x-y-options-void",children:(0,t.jsx)(n.code,{children:"drawText(text, x, y, options?): void"})}),"\n",(0,t.jsx)(n.p,{children:"Renders text onto the active layer:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'painter.drawText("Hello Fuderu!", 100, 100, {\n fontSize: 32,\n fontFamily: "Inter, sans-serif",\n color: "#1e293b",\n});\n'})}),"\n",(0,t.jsx)(n.h3,{id:"getcoloratx-number-y-number-source-active--composite--r-g-b-a-hex-",children:(0,t.jsx)(n.code,{children:'getColorAt(x: number, y: number, source?: "active" | "composite"): { r, g, b, a, hex }'})}),"\n",(0,t.jsxs)(n.p,{children:["Samples pixel color at document coordinate ",(0,t.jsx)(n.code,{children:"(x, y)"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'const sample = painter.getColorAt(200, 150, "composite");\nconsole.log(sample.hex); // "#2563eb"\n'})}),"\n",(0,t.jsx)(n.h2,{id:"event-system--state-snapshots",children:"Event System & State Snapshots"}),"\n",(0,t.jsx)(n.p,{children:"Fuderu features a reactive event model for seamless state management and UI syncing:"}),"\n",(0,t.jsxs)(n.h3,{id:"onevent-handler--offevent-handler",children:[(0,t.jsx)(n.code,{children:"on(event, handler)"})," / ",(0,t.jsx)(n.code,{children:"off(event, handler)"})]}),"\n",(0,t.jsxs)(n.p,{children:["Subscribe to typed canvas events (",(0,t.jsx)(n.code,{children:"change"}),", ",(0,t.jsx)(n.code,{children:"stroke:start"}),", ",(0,t.jsx)(n.code,{children:"stroke:end"}),", ",(0,t.jsx)(n.code,{children:"layer:change"}),", ",(0,t.jsx)(n.code,{children:"history:change"}),"):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'const unsubscribe = painter.on("change", () => {\n console.log("Canvas state changed!");\n});\n'})}),"\n",(0,t.jsx)(n.h3,{id:"getsnapshot-canvassnapshot",children:(0,t.jsx)(n.code,{children:"getSnapshot(): CanvasSnapshot"})}),"\n",(0,t.jsxs)(n.p,{children:["Returns an immutable snapshot of canvas state suitable for React's ",(0,t.jsx)(n.code,{children:"useSyncExternalStore"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"const snapshot = painter.getSnapshot();\nconsole.log(snapshot.layerCount, snapshot.activeLayerId, snapshot.canUndo);\n"})}),"\n",(0,t.jsx)(n.h2,{id:"history-navigation-api",children:"History Navigation API"}),"\n",(0,t.jsx)(n.p,{children:"Navigate history timelines directly or inject custom undo patches:"}),"\n",(0,t.jsx)(n.h3,{id:"historygotoindex-number-void",children:(0,t.jsx)(n.code,{children:"history.goTo(index: number): void"})}),"\n",(0,t.jsx)(n.p,{children:"Jump directly to any point in the history timeline:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// Jump to step 3 in undo history\npainter.history.goTo(3);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"historygetstepsummaries-historystepsummary",children:(0,t.jsx)(n.code,{children:"history.getStepSummaries(): HistoryStepSummary[]"})}),"\n",(0,t.jsx)(n.p,{children:"Retrieve human-readable step summaries for building history timeline UI panels."})]})}function u(e={}){let{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},7014(e,n,s){s.d(n,{A:()=>x});var r=s(4848),t=s(6540),a=s(3889);
1let i="controlRow_Wuk7",l="controlLabel_YCf1",o="checkboxLabel_LjTe",c="rangeInput_Popc",d="selectInput_pGb6",h="status_CCSx",u="button_E7NR",p=[{label:"Star stamp",url:"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill='none' stroke='%23000000' stroke-width='6' d='M32 4l8 20 20 2-15 13 5 20-17-11-17 11 5-20-15-13 20-2z'/%3E%3C/svg%3E"},{label:"Leaf stamp",url:"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill='none' stroke='%23000000' stroke-width='6' d='M8 32c16-24 32-24 48 0-16 24-32 24-48 0zm20-8l12 12m0-12l-12 12'/%3E%3C/svg%3E"}];function x({title:e,description:n,brushColor:s="#0f766e",brushSize:m=18,spacing:g=.18,compact:j=!1,mode:v="intro"}){let y=(0,t.useRef)(null),b=(0,t.useRef)(null),f=(0,t.useRef)(null),w=(0,t.useRef)(null),C=(0,t.useRef)(null),S=(0,t.useRef)(null),N=(0,t.useRef)(null),k=(0,t.useRef)(null),D=(0,t.useRef)(null),L=(0,t.useRef)(null),[R,I]=(0,t.useState)(!1),[E,P]=(0,t.useState)(!1),[z,M]=(0,t.useState)(s),[A,T]=(0,t.useState)(m),[_,F]=(0,t.useState)(g),[H,B]=(0,t.useState)(!0),[O,U]=(0,t.useState)(!0),[J,G]=(0,t.useState)("source-over"),[q,W]=(0,t.useState)("none"),[K,Q]=(0,t.useState)("flow"),[X,V]=(0,t.useState)("No image loaded"),[Y,Z]=(0,t.useState)(!1),[$,ee]=(0,t.useState)(!1),[en,es]=(0,t.useState)(!1),[er,et]=(0,t.useState)(!1),[ea,ei]=(0,t.useState)(!1),[el,eo]=(0,t.useState)(!1),ec=(0,t.useMemo)(()=>({color:s,size:m,spacing:g,roundness:.8,opacity:.95,flow:.9}),[s,m,g]);(0,t.useEffect)(()=>{if(!y.current)return;let e=y.current.getBoundingClientRect(),n={width:Math.max(1,Math.round(e.width)),height:Math.max(1,Math.round(e.height))},s=new a.Hl({canvas:y.current,document:n,pressureSimulation:el,brush:ec});return b.current=s,P(!0),()=>{s.destroy(),b.current=null,P(!1)}},[ec]),(0,t.useEffect)(()=>{let e=b.current;e&&(e.pressureSimulation=el)},[el]),(0,t.useEffect)(()=>{let e=b.current;e&&(e.loadConfig({color:z,size:A,spacing:O?_:1,roundness:.8,opacity:.95,flow:.9,rotation:"image"===v?{mode:K,smoothing:.15}:void 0}),e.setSmooth(H),e.setSpacing(O),e.brush.blendMode=J,e.brush.filter=q,e.setEraser(R))},[z,A,_,H,O,J,q,K,R,v]);let ed=async e=>{if(b.current){V("Loading image...");try{await b.current.loadImage(e),V("Image loaded")}catch{V("Failed to load image")}}},eh=async(e,n,s,r,t)=>{if(b.current){if(e){let e=n();t&&e instanceof a.lC&&y.current&&(Z(!0),await e.loadPattern("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Crect width='32' height='32' fill='%23f8fafc'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%230f766e'/%3E%3C/svg%3E",y.current.width,y.current.height,z),Z(!1));let i=b.current.brush.useModule(e);s.current=e,r.current=i;return}r.current&&(b.current.brush.removeModule(r.current),s.current=null,r.current=null)}},eu="modules"===v,ep="image"===v;return(0,r.jsxs)("div",{className:j?"cardCompact_jfv9":"card_htZF",children:[(e||n)&&(0,r.jsxs)("div",{className:"header_z769",children:[e&&(0,r.jsx)("div",{className:"title_LlcE",children:e}),n&&(0,r.jsx)("p",{className:"description_bVFl",children:n})]}),(0,r.jsxs)("div",{className:"canvasWrapper_QpnX",children:[(0,r.jsx)("canvas",{ref:y,className:"canvas_fOpB"}),!E&&(0,r.jsx)("div",{className:"loading_FnIX",children:"Loading demo\u2026"})]}),!j&&(0,r.jsxs)("div",{className:"controls_HI0C",children:[(0,r.jsxs)("div",{className:i,children:[(0,r.jsx)("button",{className:u,onClick:()=>{b.current?.clear()},type:"button",children:"Clear"}),(0,r.jsx)("button",{className:u,onClick:()=>{b.current?.undo()},type:"button",children:"Undo"}),(0,r.jsx)("button",{className:u,onClick:()=>{b.current?.redo()},type:"button",children:"Redo"}),(0,r.jsx)("button",{className:u,onClick:()=>{let e=!R;I(e),b.current?.setEraser(e)},type:"button",children:R?"Brush":"Eraser"})]}),(0,r.jsxs)("div",{className:i,children:[(0,r.jsxs)("label",{className:l,children:["Color",(0,r.jsx)("input",{className:"colorInput_Q6d8",type:"color",value:z,onChange:e=>M(e.target.value)})]}),(0,r.jsxs)("label",{className:l,children:["Size",(0,r.jsx)("input",{className:c,type:"range",min:"4",max:"80",value:A,onChange:e=>T(Number(e.target.value))})]}),(0,r.jsxs)("label",{className:l,children:["Spacing",(0,r.jsx)("input",{className:c,type:"range",min:"0.1",max:"1",step:"0.05",value:_,onCh
1ange:e=>F(Number(e.target.value))})]})]}),ep&&(0,r.jsxs)("div",{className:i,children:[p.map(e=>(0,r.jsx)("button",{className:u,onClick:()=>void ed(e.url),type:"button",children:e.label},e.label)),(0,r.jsx)("button",{className:u,onClick:()=>{b.current?.brush.removeImage(),V("Image removed")},type:"button",children:"Remove image"}),(0,r.jsx)("span",{className:h,children:X})]}),ep&&(0,r.jsx)("div",{className:i,children:(0,r.jsxs)("label",{className:l,children:["Rotation",(0,r.jsxs)("select",{className:d,value:K,onChange:e=>Q(e.target.value),children:[(0,r.jsx)("option",{value:"fixed",children:"fixed"}),(0,r.jsx)("option",{value:"flow",children:"flow"}),(0,r.jsx)("option",{value:"random",children:"random"})]})]})}),"brush"===v&&(0,r.jsxs)("div",{className:i,children:[(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:H,onChange:e=>B(e.target.checked)}),"Smooth"]}),(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:O,onChange:e=>U(e.target.checked)}),"Spacing"]}),(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:el,onChange:e=>eo(e.target.checked)}),"Pressure sensitivity"]}),(0,r.jsxs)("label",{className:l,children:["Blend mode",(0,r.jsxs)("select",{className:d,value:J,onChange:e=>G(e.target.value),children:[(0,r.jsx)("option",{value:"source-over",children:"source-over"}),(0,r.jsx)("option",{value:"multiply",children:"multiply"}),(0,r.jsx)("option",{value:"screen",children:"screen"}),(0,r.jsx)("option",{value:"overlay",children:"overlay"})]})]}),(0,r.jsxs)("label",{className:l,children:["Filter",(0,r.jsxs)("select",{className:d,value:q,onChange:e=>W(e.target.value),children:[(0,r.jsx)("option",{value:"none",children:"none"}),(0,r.jsx)("option",{value:"blur(1px)",children:"blur"}),(0,r.jsx)("option",{value:"brightness(1.25)",children:"bright"}),(0,r.jsx)("option",{value:"contrast(1.2)",children:"contrast"})]})]})]}),eu&&(0,r.jsxs)("div",{className:i,children:[(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:$,onChange:async e=>{let n=e.target.checked;ee(n),await eh(n,()=>new a.p5({sizeJitter:.35,minDiameter:.5,angleJitter:.3,roundJitter:.25,minRoundness:.4}),f,w)}}),"Dynamic shape"]}),(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:en,onChange:async e=>{let n=e.target.checked;es(n),await eh(n,()=>new a.KO({opacityJitter:.25,minOpacityJitter:.4,flowJitter:.25,minFlowJitter:.35}),C,S)}}),"Dynamic transparency"]})]}),eu&&(0,r.jsxs)("div",{className:i,children:[(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:er,onChange:async e=>{let n=e.target.checked;et(n),await eh(n,()=>new a.W9({spreadRange:.25,count:5,countJitter:.4}),N,k)}}),"Spread"]}),(0,r.jsxs)("label",{className:o,children:[(0,r.jsx)("input",{type:"checkbox",checked:ea,onChange:async e=>{let n=e.target.checked;ei(n),await eh(n,()=>new a.lC({scale:.8,brightness:10,contrast:10,blendMode:"multiply"}),D,L,!0)}}),"Pattern"]}),Y&&(0,r.jsx)("span",{className:h,children:"Loading pattern\u2026"})]})]})]})}},8453(e,n,s){s.d(n,{R:()=>i,x:()=>l});var r=s(6540);let t={},a=r.createContext(t);function i(e){let n=r.useContext(a);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.