@pixerate/editor 0.137.0 → 0.138.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.d.cts +3 -3
  3. package/dist/agent/index.d.ts +3 -3
  4. package/dist/agent/index.js +1 -1
  5. package/dist/animation-editor/index.cjs +57 -57
  6. package/dist/animation-editor/index.d.cts +3 -3
  7. package/dist/animation-editor/index.d.ts +3 -3
  8. package/dist/animation-editor/index.js +4 -4
  9. package/dist/{arrange-CKXK_hf7.d.cts → arrange-j_mLiFMO.d.cts} +1 -1
  10. package/dist/{arrange-BUREmgD9.d.ts → arrange-q5HQvnnB.d.ts} +1 -1
  11. package/dist/{bezier-CZ2AhjID.d.cts → bezier-CUaH8hcK.d.cts} +1 -1
  12. package/dist/{bezier-Dywv3Bw8.d.ts → bezier-DPmdGwgR.d.ts} +1 -1
  13. package/dist/{brushTypes-C7N3EBK7.d.cts → brushTypes-MX5UVs_m.d.cts} +27 -2
  14. package/dist/{brushTypes-C7N3EBK7.d.ts → brushTypes-MX5UVs_m.d.ts} +27 -2
  15. package/dist/{brushes-DJmUbIgz.d.ts → brushes-B1ABdDAs.d.ts} +2 -108
  16. package/dist/{brushes-CnvZSYYU.d.cts → brushes-DnIdJYxN.d.cts} +2 -108
  17. package/dist/{brushstrokeRender-IMFY3FF6.cjs → brushstrokeRender-PP6NTF6M.cjs} +52 -24
  18. package/dist/brushstrokeRender-PP6NTF6M.cjs.map +1 -0
  19. package/dist/{brushstrokeRender-2FAXA3XC.js → brushstrokeRender-QGEYJ4OZ.js} +40 -12
  20. package/dist/brushstrokeRender-QGEYJ4OZ.js.map +1 -0
  21. package/dist/{builtins-Cc-bH-3U.d.cts → builtins-BakDiKWf.d.cts} +3 -2
  22. package/dist/{builtins-ZU1GJnUA.d.ts → builtins-CE33vyv2.d.ts} +3 -2
  23. package/dist/canvas/index.cjs +16 -9
  24. package/dist/canvas/index.cjs.map +1 -1
  25. package/dist/canvas/index.d.cts +56 -11
  26. package/dist/canvas/index.d.ts +56 -11
  27. package/dist/canvas/index.js +10 -3
  28. package/dist/canvas/index.js.map +1 -1
  29. package/dist/{chunk-QBWEH3IM.cjs → chunk-3T5O225T.cjs} +391 -76
  30. package/dist/chunk-3T5O225T.cjs.map +1 -0
  31. package/dist/{chunk-PRSRYHQD.cjs → chunk-7MN75BXI.cjs} +36 -36
  32. package/dist/{chunk-PRSRYHQD.cjs.map → chunk-7MN75BXI.cjs.map} +1 -1
  33. package/dist/{chunk-EQQMGO42.js → chunk-C2J7GOMJ.js} +18 -5
  34. package/dist/chunk-C2J7GOMJ.js.map +1 -0
  35. package/dist/{chunk-2XS6LTUP.cjs → chunk-C46AZEBE.cjs} +104 -339
  36. package/dist/chunk-C46AZEBE.cjs.map +1 -0
  37. package/dist/{chunk-XN2NBLV5.cjs → chunk-CM3LJPR3.cjs} +82 -82
  38. package/dist/{chunk-XN2NBLV5.cjs.map → chunk-CM3LJPR3.cjs.map} +1 -1
  39. package/dist/{chunk-HUO3TI4C.js → chunk-CPSMARTX.js} +6 -241
  40. package/dist/chunk-CPSMARTX.js.map +1 -0
  41. package/dist/{chunk-KEENUAFN.cjs → chunk-DLLUOVQX.cjs} +18 -5
  42. package/dist/chunk-DLLUOVQX.cjs.map +1 -0
  43. package/dist/{chunk-LSXXAJG3.js → chunk-J7DSKTKB.js} +5 -4
  44. package/dist/chunk-J7DSKTKB.js.map +1 -0
  45. package/dist/{chunk-6S6362D2.js → chunk-OV7253FC.js} +2 -2
  46. package/dist/{chunk-5FSRTP6E.js → chunk-PQFVLOMR.js} +319 -4
  47. package/dist/chunk-PQFVLOMR.js.map +1 -0
  48. package/dist/{chunk-OIBIRXRG.js → chunk-PSPLRQL6.js} +2 -2
  49. package/dist/{chunk-SYT2TLQF.js → chunk-QBARQGSG.js} +2 -2
  50. package/dist/{chunk-5OOIUXNJ.cjs → chunk-SXOTFV3V.cjs} +6 -6
  51. package/dist/{chunk-5OOIUXNJ.cjs.map → chunk-SXOTFV3V.cjs.map} +1 -1
  52. package/dist/{chunk-DFSZO2B5.cjs → chunk-UFL5N4DK.cjs} +5 -4
  53. package/dist/chunk-UFL5N4DK.cjs.map +1 -0
  54. package/dist/{chunk-3D77LOBM.js → chunk-V45ZTW4R.js} +3 -3
  55. package/dist/{chunk-TLCTTQFF.cjs → chunk-VEHVXRJU.cjs} +17 -17
  56. package/dist/{chunk-TLCTTQFF.cjs.map → chunk-VEHVXRJU.cjs.map} +1 -1
  57. package/dist/commands/index.cjs +2 -2
  58. package/dist/commands/index.d.cts +3 -3
  59. package/dist/commands/index.d.ts +3 -3
  60. package/dist/commands/index.js +1 -1
  61. package/dist/{export-DwnZ_hvQ.d.ts → export-BwFRiItd.d.ts} +4 -4
  62. package/dist/{export-CyJdfP_Z.d.cts → export-DuN3dpCg.d.cts} +4 -4
  63. package/dist/{blending-NgF3qo3d.d.ts → grain-COgYMMHv.d.ts} +58 -1
  64. package/dist/{blending-D7jCmjO6.d.cts → grain-GCt_O2kv.d.cts} +58 -1
  65. package/dist/image-editor/index.cjs +695 -677
  66. package/dist/image-editor/index.cjs.map +1 -1
  67. package/dist/image-editor/index.d.cts +11 -11
  68. package/dist/image-editor/index.d.ts +11 -11
  69. package/dist/image-editor/index.js +23 -5
  70. package/dist/image-editor/index.js.map +1 -1
  71. package/dist/{opentype-a1KAW-qs.d.ts → opentype-T7bkbR2D.d.ts} +1 -1
  72. package/dist/{opentype-KR_jj6b_.d.cts → opentype-n99lqxEZ.d.cts} +1 -1
  73. package/dist/paint/index.cjs +21 -3
  74. package/dist/paint/index.cjs.map +1 -1
  75. package/dist/paint/index.d.cts +72 -7
  76. package/dist/paint/index.d.ts +72 -7
  77. package/dist/paint/index.js +20 -2
  78. package/dist/paint-editor/index.cjs +171 -171
  79. package/dist/paint-editor/index.cjs.map +1 -1
  80. package/dist/paint-editor/index.d.cts +75 -29
  81. package/dist/paint-editor/index.d.ts +75 -29
  82. package/dist/paint-editor/index.js +12 -12
  83. package/dist/{pathfinder-BHS4vQNm.d.cts → pathfinder-CQlldanv.d.cts} +2 -2
  84. package/dist/{pathfinder-BVvTIBMo.d.ts → pathfinder-D-V6JLSw.d.ts} +2 -2
  85. package/dist/{rand-Cw9VLrVC.d.cts → rand-D7nfeD1p.d.cts} +18 -2
  86. package/dist/{rand-8xbUb6ur.d.ts → rand-DJGxSq9d.d.ts} +18 -2
  87. package/dist/{svg-CSz3mzkw.d.ts → svg-CnDCpIy9.d.ts} +1 -1
  88. package/dist/{svg-DTb40ICU.d.cts → svg-DPJMUSNt.d.cts} +1 -1
  89. package/dist/vector/index.cjs +944 -944
  90. package/dist/vector/index.d.cts +10 -10
  91. package/dist/vector/index.d.ts +10 -10
  92. package/dist/vector/index.js +4 -4
  93. package/dist/{warp-DTOp0lON.d.cts → warp-C7rMWUgW.d.cts} +1 -1
  94. package/dist/{warp-pd7hrH4T.d.ts → warp-Doasjpyr.d.ts} +1 -1
  95. package/package.json +1 -1
  96. package/skills/pixerate-canvas/SKILL.md +12 -5
  97. package/dist/brushstrokeRender-2FAXA3XC.js.map +0 -1
  98. package/dist/brushstrokeRender-IMFY3FF6.cjs.map +0 -1
  99. package/dist/chunk-2XS6LTUP.cjs.map +0 -1
  100. package/dist/chunk-5FSRTP6E.js.map +0 -1
  101. package/dist/chunk-DFSZO2B5.cjs.map +0 -1
  102. package/dist/chunk-EQQMGO42.js.map +0 -1
  103. package/dist/chunk-HUO3TI4C.js.map +0 -1
  104. package/dist/chunk-KEENUAFN.cjs.map +0 -1
  105. package/dist/chunk-LSXXAJG3.js.map +0 -1
  106. package/dist/chunk-QBWEH3IM.cjs.map +0 -1
  107. /package/dist/{chunk-6S6362D2.js.map → chunk-OV7253FC.js.map} +0 -0
  108. /package/dist/{chunk-OIBIRXRG.js.map → chunk-PSPLRQL6.js.map} +0 -0
  109. /package/dist/{chunk-SYT2TLQF.js.map → chunk-QBARQGSG.js.map} +0 -0
  110. /package/dist/{chunk-3D77LOBM.js.map → chunk-V45ZTW4R.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-3T5O225T.cjs","../src/paint/rand.ts","../src/paint/gradient-paint.ts","../src/paint/brush.ts","../src/paint/grain.ts","../src/paint/path-stroke.ts","../src/paint/brushstroke.ts","../src/paint/exchange.ts","../src/paint/blending.ts","../src/paint/mixer.ts"],"names":["unit","i","finite"],"mappings":"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ACC,UAAA;AACvB,QAAA;AAEkB,QAAA;AACJ,QAAA;AACS,UAAA;AACG,UAAA;AACb,UAAA;AACY,YAAA;AACP,YAAA;AACI,YAAA;AACA,YAAA;AACpB,UAAA;AACe,UAAA;AACjB,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;ARi2DmC;AACA;AS/mEmB;AAC/C,EAAA;AACC,EAAA;AACD,EAAA;AACW,EAAA;AACC,EAAA;AACT,EAAA;AACV;AAGgF;AAC9B,EAAA;AACC,EAAA;AAC1C,EAAA;AACc,IAAA;AACE,IAAA;AACF,IAAA;AACM,IAAA;AACC,IAAA;AACH,IAAA;AACzB,EAAA;AACF;AAGmE;AACnC,EAAA;AAChC;AAGgG;AACzD,EAAA;AACN,EAAA;AACjC;AAGuD;AAC5B,EAAA;AAC3B;AAUyB;AAarB,EAAA;AAFS,IAAA;AAKmB,IAAA;AACR,IAAA;AACS,IAAA;AAC/B,EAAA;AARW,EAAA;AAVA,mBAAA;AACc,mBAAA;AACjB,EAAA;AACgC,mBAAA;AACR,mBAAA;AACf,EAAA;AACA,EAAA;AACgC,mBAAA;AAAA;AAc3B,EAAA;AACC,IAAA;AACvB,EAAA;AAEgD,EAAA;AACzB,IAAA;AACQ,IAAA;AACP,IAAA;AACxB,EAAA;AAEsC,EAAA;AACf,IAAA;AACU,IAAA;AACR,IAAA;AACD,IAAA;AACxB,EAAA;AAE4C,EAAA;AACb,IAAA;AACZ,IAAA;AACa,IAAA;AACb,IAAA;AACV,IAAA;AACT,EAAA;AAEsC,EAAA;AACV,IAAA;AACJ,IAAA;AACS,MAAA;AACD,MAAA;AACH,MAAA;AACJ,MAAA;AAEO,MAAA;AACP,MAAA;AACN,MAAA;AACU,MAAA;AACpB,MAAA;AACP,IAAA;AACO,IAAA;AACT,EAAA;AAE4D,EAAA;AAC1C,IAAA;AAEa,IAAA;AACD,IAAA;AACb,IAAA;AACgB,MAAA;AACJ,MAAA;AAC3B,IAAA;AACS,IAAA;AACX,EAAA;AACF;AAEuD;AAC/B,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;ATmlEmC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-3T5O225T.cjs","sourcesContent":[null,"// Portions ported from photocraft/crates/paint/src/rng.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Counter-based seeded randomness on 32-bit integer hashes: every value is a pure function of its\n * inputs, so strokes and papers regenerate identically on every machine. Shared by the paint\n * engine (`/paint`) and the paint editor (which re-exports it from `noise.ts`).\n */\n\n/** A 32-bit finaliser (murmur3's `fmix32`). */\nexport function mix32(h: number): number {\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return h >>> 0;\n}\n\n/** Hash of integer coordinates and a salt to [0, 1). */\nexport function hash2(x: number, y: number, salt: number): number {\n const h = mix32((Math.imul(x | 0, 0x27d4eb2d) ^ mix32((y | 0) + Math.imul(salt | 0, 0x165667b1))) >>> 0);\n return h / 4294967296;\n}\n\n/** Uniform in [0, 1) from a seed, an index and a stream (one stream per random quantity). */\nexport function rand01(seed: number, index: number, stream: number): number {\n return hash2(index, stream, seed);\n}\n\n/** Uniform in [-1, 1). */\nexport function randSigned(seed: number, index: number, stream: number): number {\n return rand01(seed, index, stream) * 2 - 1;\n}\n\n/** A seed from a string (FNV-1a, then mixed). */\nexport function seedFromString(text: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < text.length; i++) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return mix32(h >>> 0);\n}\n\nconst smooth = (t: number) => t * t * (3 - 2 * t);\n\n/** A periodic lattice of `px × py` random values, sampled with smooth interpolation. */\nclass Lattice {\n readonly values: Float32Array;\n constructor(readonly px: number, readonly py: number, salt: number) {\n this.values = new Float32Array(px * py);\n for (let j = 0; j < py; j++) for (let i = 0; i < px; i++) this.values[j * px + i] = hash2(i, j, salt);\n }\n\n /** Value noise at (x, y) in lattice cells; wraps every `px` / `py` cells. */\n at(x: number, y: number): number {\n const { px, py } = this;\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const tx = smooth(x - x0);\n const ty = smooth(y - y0);\n const i0 = ((x0 % px) + px) % px;\n const j0 = ((y0 % py) + py) % py;\n const i1 = (i0 + 1) % px;\n const j1 = (j0 + 1) % py;\n const v = this.values;\n const a = v[j0 * px + i0] + (v[j0 * px + i1] - v[j0 * px + i0]) * tx;\n const b = v[j1 * px + i0] + (v[j1 * px + i1] - v[j1 * px + i0]) * tx;\n return a + (b - a) * ty;\n }\n}\n\n/**\n * Tileable fractal value noise in 0..1 over a `size`-pixel square: `cellsX × cellsY` lattice cells\n * at the base octave (unequal counts give fibres and streaks), doubling per octave, so the result\n * repeats exactly every `size` pixels.\n */\nexport class TileableNoise {\n private readonly octaves: Lattice[] = [];\n\n constructor(readonly size: number, cellsX: number, cellsY: number, octaves: number, salt: number) {\n let cx = Math.max(1, Math.round(cellsX));\n let cy = Math.max(1, Math.round(cellsY));\n for (let o = 0; o < octaves; o++) {\n this.octaves.push(new Lattice(cx, cy, (salt + o * 7919) | 0));\n cx *= 2;\n cy *= 2;\n }\n }\n\n /** Fractal noise at pixel (x, y). */\n at(x: number, y: number): number {\n let sum = 0;\n let amp = 1;\n let norm = 0;\n const u = x / this.size;\n const v = y / this.size;\n for (const l of this.octaves) {\n sum += l.at(u * l.px, v * l.py) * amp;\n norm += amp;\n amp *= 0.5;\n }\n return norm > 0 ? sum / norm : 0.5;\n }\n}\n","/**\n * Gradient paint: a colour slot (the paint colour, a swatch, a stroke's colour) holds a\n * `PaintSource`, either one colour or a gradient that the brush samples dab by dab as the stroke\n * goes: by distance travelled, pen pressure, tilt, speed, direction, time, across the tip, or at\n * random. Shared by the paint editor and the image editor.\n *\n * - **Stops** have the vector and image gradients' shape (`offset`, `color`, `opacity`,\n * `midpoint`); opacity multiplies the brush's flow. `softness` (1, smooth, by default) turns\n * each blend into a harder step towards 0.\n * - **Space**: `pigment` (default) mixes neighbouring stops like paint (Kubelka-Munk, so blue to\n * yellow passes through green), `oklab` perceptually, `srgb` as plain numbers.\n * - **Mapping**: what moves along the gradient. `distance` (default: 300 px, mirrored) is the\n * \"drag it out\" paint: the colour changes as the stroke travels. It never stretches over the\n * whole stroke, whose length is unknown until the pen lifts.\n *\n * Everything is a pure function of the stroke's recorded samples and seed, so replays, time-lapses,\n * re-rendered exports and drying stay exact.\n */\n\nimport { mixPigment8 } from '../raster/blend';\nimport { rand01 } from './rand';\n\n/** sRGB 0..1. */\nexport type PaintRgb = [number, number, number];\n\n/** A gradient stop: the vector and image gradients' shape. */\nexport interface PaintGradientStop {\n /** 0..1 along the gradient. */\n offset: number;\n /**\n * #rrggbb, or `paint` / `secondary`: the editor's paint and second colours when the stroke\n * starts (a gradient between whatever two colours are chosen).\n */\n color: string;\n /** 0..1 (default 1): multiplies the brush's flow where this colour is laid. */\n opacity?: number;\n /** Where between this stop and the next (0.05..0.95 of the way) the colours are half mixed (default 0.5). */\n midpoint?: number;\n}\n\n/** How neighbouring stops mix. */\nexport type PaintGradientSpace = 'pigment' | 'oklab' | 'srgb';\nexport const PAINT_GRADIENT_SPACES: readonly PaintGradientSpace[] = ['pigment', 'oklab', 'srgb'];\n\n/** Past the end of the gradient: hold the last colour, start again, or run back. */\nexport type PaintGradientRepeat = 'clamp' | 'repeat' | 'mirror';\nexport const PAINT_GRADIENT_REPEATS: readonly PaintGradientRepeat[] = ['clamp', 'repeat', 'mirror'];\n\n/** What moves a stroke along its gradient. */\nexport type PaintGradientMapping =\n /** The stroke's travel: `length` px (or dabs, `unit` 'dabs') per run through the gradient. */\n | { by: 'distance'; length: number; repeat: PaintGradientRepeat; unit?: 'px' | 'dabs' }\n /** Pen pressure 0..1; tilt 0 (upright) .. 1 (flat); speed 0 .. 2.5 px/ms; direction of travel 0..360°. */\n | { by: 'pressure' }\n | { by: 'tilt' }\n | { by: 'velocity' }\n | { by: 'direction' }\n /** Time since the stroke began: `duration` ms per run through the gradient (needs timed samples). */\n | { by: 'time'; duration: number; repeat?: PaintGradientRepeat }\n /** Across the tip, left to right looking along the stroke: a brush loaded with several colours. */\n | { by: 'across-tip' }\n /** A random point of the gradient per dab (or per `every` dabs), seeded so replays match. */\n | { by: 'random'; every?: number };\n\nexport type PaintGradientMappingKind = PaintGradientMapping['by'];\nexport const PAINT_GRADIENT_MAPPINGS: readonly PaintGradientMappingKind[] = ['distance', 'pressure', 'tilt', 'velocity', 'direction', 'time', 'across-tip', 'random'];\n\nexport interface PaintColorSource {\n type: 'color';\n /** #rrggbb */\n color: string;\n}\n\nexport interface PaintGradientSource {\n type: 'gradient';\n /** 2..16 stops, sorted by offset. */\n stops: PaintGradientStop[];\n space: PaintGradientSpace;\n map: PaintGradientMapping;\n /** 0 (hard steps) .. 1 (smooth blends, default). */\n softness?: number;\n}\n\n/** What a colour slot holds: one colour or a gradient. */\nexport type PaintSource = PaintColorSource | PaintGradientSource;\n\n/** \"Drag it out\": 300 px of travel per run, mirrored back and forth. */\nexport const DEFAULT_PAINT_GRADIENT_MAPPING: Readonly<PaintGradientMapping> = { by: 'distance', length: 300, repeat: 'mirror', unit: 'px' };\n\n/** Speed (px/ms) at which `velocity` reaches the end of the gradient (the brush dynamics' full speed). */\nexport const PAINT_GRADIENT_FULL_SPEED = 2.5;\n\n/** Seed stream of `random` (the multi-colour brush's, so migrated brushes pick the same colours). */\nconst RANDOM_STREAM = 11;\n\nexport class PaintSourceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PaintSourceError';\n }\n}\n\nconst HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\n/** `#rgb` / `#rrggbb` → lowercase `#rrggbb`, or null. */\nexport function normalizeHexColor(v: unknown): string | null {\n if (typeof v !== 'string' || !HEX.test(v.trim())) return null;\n const h = v.trim().toLowerCase();\n return h.length === 4 ? `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}` : h;\n}\n\n/** `#rrggbb` → sRGB 0..1. */\nexport function hexToPaintRgb(hex: string): PaintRgb {\n const n = parseInt(hex.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst describe = (v: unknown) => (typeof v === 'string' ? `\"${v}\"` : JSON.stringify(v));\n\nfunction parseMapping(raw: unknown, at: string): PaintGradientMapping {\n if (raw === undefined) return { ...DEFAULT_PAINT_GRADIENT_MAPPING } as PaintGradientMapping;\n if (!raw || typeof raw !== 'object') throw new PaintSourceError(`${at} must be an object like { \"by\": \"distance\", \"length\": 300, \"repeat\": \"mirror\" }.`);\n const r = raw as Record<string, unknown>;\n const by = r.by as PaintGradientMappingKind;\n if (!PAINT_GRADIENT_MAPPINGS.includes(by)) throw new PaintSourceError(`${at}.by ${describe(r.by)} is not a mapping; use one of ${PAINT_GRADIENT_MAPPINGS.join(', ')}.`);\n const repeat = (key: string, def: PaintGradientRepeat): PaintGradientRepeat => {\n if (r[key] === undefined) return def;\n if (!PAINT_GRADIENT_REPEATS.includes(r[key] as PaintGradientRepeat)) throw new PaintSourceError(`${at}.${key} ${describe(r[key])} is not a repeat; use ${PAINT_GRADIENT_REPEATS.join(', ')}.`);\n return r[key] as PaintGradientRepeat;\n };\n const range = (key: string, lo: number, hi: number, def: number, unit: string): number => {\n if (r[key] === undefined) return def;\n if (!finite(r[key]) || (r[key] as number) < lo || (r[key] as number) > hi) throw new PaintSourceError(`${at}.${key} ${describe(r[key])} is out of range; use ${lo}..${hi} ${unit}.`);\n return r[key] as number;\n };\n switch (by) {\n case 'distance': {\n const unit = r.unit === undefined ? 'px' : r.unit;\n if (unit !== 'px' && unit !== 'dabs') throw new PaintSourceError(`${at}.unit ${describe(r.unit)} is not a unit; use px or dabs.`);\n return { by, length: range('length', 1, unit === 'px' ? 100000 : 10000, 300, unit), repeat: repeat('repeat', 'mirror'), unit };\n }\n case 'time':\n return { by, duration: range('duration', 1, 600000, 2000, 'ms'), repeat: repeat('repeat', 'clamp') };\n case 'random':\n return { by, every: Math.round(range('every', 1, 500, 1, 'dabs')) };\n default:\n return { by };\n }\n}\n\n/**\n * Reads a paint source strictly: what tools and APIs take. Throws `PaintSourceError` saying what\n * is wrong (unknown type, an unreadable colour, fewer than 2 or more than 16 stops, offsets\n * outside 0..1 or not sorted, a bad mapping) and how to fix it. Missing optional fields get their\n * defaults (space `pigment`, the `distance` mapping, softness 1).\n */\nexport function parsePaintSource(raw: unknown, at = 'paint'): PaintSource {\n if (typeof raw === 'string') {\n const color = normalizeHexColor(raw);\n if (!color) throw new PaintSourceError(`${at} ${describe(raw)} is not a colour; use #rrggbb or a gradient object.`);\n return { type: 'color', color };\n }\n if (!raw || typeof raw !== 'object') throw new PaintSourceError(`${at} must be { \"type\": \"color\", \"color\": \"#rrggbb\" } or { \"type\": \"gradient\", \"stops\": [...] }.`);\n const r = raw as Record<string, unknown>;\n if (r.type === 'color') {\n const color = normalizeHexColor(r.color);\n if (!color) throw new PaintSourceError(`${at}.color ${describe(r.color)} is not a colour; use #rrggbb.`);\n return { type: 'color', color };\n }\n if (r.type !== 'gradient') throw new PaintSourceError(`${at}.type ${describe(r.type)} is not a paint; use \"color\" or \"gradient\".`);\n if (!Array.isArray(r.stops) || r.stops.length < 2 || r.stops.length > 16) throw new PaintSourceError(`${at}.stops needs 2..16 stops like { \"offset\": 0, \"color\": \"#1d4ed8\" }; got ${Array.isArray(r.stops) ? r.stops.length : describe(r.stops)}.`);\n const stops: PaintGradientStop[] = r.stops.map((s, i) => {\n const sat = `${at}.stops[${i}]`;\n if (!s || typeof s !== 'object') throw new PaintSourceError(`${sat} must be { \"offset\": 0..1, \"color\": \"#rrggbb\" }.`);\n const o = s as Record<string, unknown>;\n if (!finite(o.offset) || o.offset < 0 || o.offset > 1) throw new PaintSourceError(`${sat}.offset ${describe(o.offset)} must be a number 0..1.`);\n const color = o.color === 'paint' || o.color === 'secondary' ? o.color : normalizeHexColor(o.color);\n if (!color) throw new PaintSourceError(`${sat}.color ${describe(o.color)} is not a colour; use #rrggbb, \"paint\" or \"secondary\".`);\n const stop: PaintGradientStop = { offset: o.offset, color };\n if (o.opacity !== undefined) {\n if (!finite(o.opacity) || o.opacity < 0 || o.opacity > 1) throw new PaintSourceError(`${sat}.opacity ${describe(o.opacity)} must be 0..1.`);\n if (o.opacity !== 1) stop.opacity = o.opacity;\n }\n if (o.midpoint !== undefined) {\n if (!finite(o.midpoint) || o.midpoint < 0.05 || o.midpoint > 0.95) throw new PaintSourceError(`${sat}.midpoint ${describe(o.midpoint)} must be 0.05..0.95.`);\n if (o.midpoint !== 0.5) stop.midpoint = o.midpoint;\n }\n return stop;\n });\n for (let i = 1; i < stops.length; i++) {\n if (stops[i].offset < stops[i - 1].offset) throw new PaintSourceError(`${at}.stops must be sorted by offset: stop ${i} (${stops[i].offset}) comes before stop ${i - 1} (${stops[i - 1].offset}).`);\n }\n const space = r.space === undefined ? 'pigment' : (r.space as PaintGradientSpace);\n if (!PAINT_GRADIENT_SPACES.includes(space)) throw new PaintSourceError(`${at}.space ${describe(r.space)} is not a space; use ${PAINT_GRADIENT_SPACES.join(', ')}.`);\n const out: PaintGradientSource = { type: 'gradient', stops, space, map: parseMapping(r.map, `${at}.map`) };\n if (r.softness !== undefined) {\n if (!finite(r.softness) || r.softness < 0 || r.softness > 1) throw new PaintSourceError(`${at}.softness ${describe(r.softness)} must be 0..1.`);\n if (r.softness !== 1) out.softness = r.softness;\n }\n return out;\n}\n\n/** Reads a paint source leniently (documents, presets): `null` when it is not one. */\nexport function readPaintSource(raw: unknown): PaintSource | null {\n try {\n return raw === undefined || raw === null ? null : parsePaintSource(raw);\n } catch {\n return null;\n }\n}\n\n/**\n * The gradient with its `paint` / `secondary` stops replaced by those colours (#rrggbb): what a\n * stroke records and samples.\n */\nexport function resolvePaintGradient(g: PaintGradientSource, colors: { paint: string; secondary: string }): PaintGradientSource {\n if (!g.stops.some((s) => s.color === 'paint' || s.color === 'secondary')) return g;\n return { ...g, stops: g.stops.map((s) => (s.color === 'paint' ? { ...s, color: colors.paint } : s.color === 'secondary' ? { ...s, color: colors.secondary } : s)) };\n}\n\n/** The gradient for a stroke scaled by `k` (a high-resolution re-render): pixel distances scale with it. */\nexport function scalePaintGradient(g: PaintGradientSource, k: number): PaintGradientSource {\n if (k === 1 || g.map.by !== 'distance' || g.map.unit === 'dabs') return g;\n return { ...g, map: { ...g.map, length: Math.min(100000, Math.max(1, g.map.length * k)) } };\n}\n\n/** A colour source. */\nexport function solidPaint(color: string): PaintColorSource {\n return { type: 'color', color: normalizeHexColor(color) ?? '#000000' };\n}\n\n/** Whether a source is a gradient. */\nexport function isGradientPaint(s: PaintSource | null | undefined): s is PaintGradientSource {\n return Boolean(s && s.type === 'gradient');\n}\n\n/** One colour that stands for the source (the colour, or a gradient's first stop): what a plain colour slot shows. */\nexport function paintSourceColor(s: PaintSource, colors: { paint: string; secondary: string } = { paint: '#000000', secondary: '#ffffff' }): string {\n if (s.type === 'color') return s.color;\n const c = s.stops[0].color;\n return c === 'paint' ? colors.paint : c === 'secondary' ? colors.secondary : c;\n}\n\n/** The source as a CSS `linear-gradient` (or colour) for colour wells and swatches. */\nexport function paintSourceCss(s: PaintSource, colors: { paint: string; secondary: string } = { paint: '#000000', secondary: '#ffffff' }): string {\n if (s.type === 'color') return s.color;\n const parts = resolvePaintGradient(s, colors).stops.map((st) => {\n const [r, g, b] = hexToPaintRgb(st.color).map((v) => Math.round(v * 255));\n return `rgba(${r}, ${g}, ${b}, ${st.opacity ?? 1}) ${Math.round(st.offset * 1000) / 10}%`;\n });\n return `linear-gradient(90deg, ${parts.join(', ')})`;\n}\n\n// --- Sampling -------------------------------------------------------------------\n\n/** Where `u` (0..1 of a segment) lands with the midpoint `m` (the vector gradients' remap). */\nfunction midpointRemap(u: number, m: number): number {\n if (Math.abs(m - 0.5) < 1e-6) return u;\n return u <= m ? (0.5 * u) / m : 0.5 + (0.5 * (u - m)) / (1 - m);\n}\n\n/** Softness 1 keeps the blend; towards 0 it becomes a step at ½ (the multi-colour brush's curve). */\nfunction soften(f: number, softness: number): number {\n if (softness >= 1) return f;\n const w = Math.max(1e-3, softness);\n return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));\n}\n\nconst q8 = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255);\nconst mixScratch = [0, 0, 0];\n\nfunction srgbToLinear(v: number): number {\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n}\nfunction linearToSrgb(v: number): number {\n return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\nfunction toOklab(c: PaintRgb): PaintRgb {\n const r = srgbToLinear(c[0]);\n const g = srgbToLinear(c[1]);\n const b = srgbToLinear(c[2]);\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];\n}\nfunction fromOklab(c: PaintRgb): PaintRgb {\n const l = (c[0] + 0.3963377774 * c[1] + 0.2158037573 * c[2]) ** 3;\n const m = (c[0] - 0.1055613458 * c[1] - 0.0638541728 * c[2]) ** 3;\n const s = (c[0] - 0.0894841775 * c[1] - 1.291485548 * c[2]) ** 3;\n const out: PaintRgb = [\n linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),\n linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),\n linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),\n ];\n // Rounded to 16 bits so last-bit differences between JavaScript engines' pow / cbrt never show.\n return out.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 65535) / 65535) as PaintRgb;\n}\n\ninterface RampStop {\n t: number;\n rgb: PaintRgb;\n lab: PaintRgb | null;\n alpha: number;\n mid: number;\n}\n\n/** A gradient's colour at a dab: sRGB 0..1 and the opacity to multiply the flow by. */\nexport interface PaintGradientSample {\n rgb: PaintRgb;\n alpha: number;\n}\n\n/** What a dab offers a mapping: all pure functions of the stroke's recorded samples and seed. */\nexport interface PaintDabParams {\n /** Path length (px) the stroke has travelled at this dab. */\n travel: number;\n /** The dab's index in its copy of the stroke (0, 1, 2 …). */\n index: number;\n /** 0..1 */\n pressure: number;\n /** 0 upright .. 1 flat. */\n lean: number;\n /** px/ms, or `null` without timed samples. */\n speed: number | null;\n /** Direction of travel, radians counter-clockwise with y up. */\n direction: number;\n /** ms since the stroke's first timed sample, or `null` without timed samples. */\n time: number | null;\n}\n\n/**\n * A gradient prepared for sampling. `dab` gives the colour of one dab; `across-tip` gradients give\n * a table across the tip instead (`tipTable`).\n */\nexport class PaintGradientSampler {\n private readonly stops: RampStop[];\n private readonly softness: number;\n /** Stops at exactly k/(n − 1): segments found by arithmetic, as the multi-colour brush did. */\n private readonly uniform: boolean;\n readonly acrossTip: boolean;\n\n constructor(\n readonly source: PaintGradientSource,\n private readonly seed = 0\n ) {\n this.stops = source.stops.map((s) => {\n // Unresolved `paint` / `secondary` stops (call `resolvePaintGradient` first) read as black and white.\n const rgb = s.color === 'paint' ? ([0, 0, 0] as PaintRgb) : s.color === 'secondary' ? ([1, 1, 1] as PaintRgb) : hexToPaintRgb(s.color);\n return { t: s.offset, rgb, lab: source.space === 'oklab' ? toOklab(rgb) : null, alpha: s.opacity ?? 1, mid: s.midpoint ?? 0.5 };\n });\n this.softness = source.softness ?? 1;\n const n = this.stops.length;\n this.uniform = this.stops.every((s, k) => s.t === k / (n - 1) && s.mid === 0.5);\n this.acrossTip = source.map.by === 'across-tip';\n }\n\n /** The two stops' mix `u` of the way (0..1), in the gradient's space. */\n private mix(a: RampStop, b: RampStop, u: number, out: PaintGradientSample): PaintGradientSample {\n const v = soften(u, this.softness);\n out.alpha = a.alpha + (b.alpha - a.alpha) * v;\n const ar = a.rgb;\n const br = b.rgb;\n if (v <= 0 || (ar[0] === br[0] && ar[1] === br[1] && ar[2] === br[2])) {\n out.rgb = [ar[0], ar[1], ar[2]];\n return out;\n }\n if (v >= 1) {\n out.rgb = [br[0], br[1], br[2]];\n return out;\n }\n if (this.source.space === 'pigment') {\n mixPigment8(q8(ar[0]), q8(ar[1]), q8(ar[2]), q8(br[0]), q8(br[1]), q8(br[2]), v, mixScratch);\n out.rgb = [mixScratch[0], mixScratch[1], mixScratch[2]];\n } else if (this.source.space === 'oklab') {\n const la = a.lab!;\n const lb = b.lab!;\n out.rgb = fromOklab([la[0] + (lb[0] - la[0]) * v, la[1] + (lb[1] - la[1]) * v, la[2] + (lb[2] - la[2]) * v]);\n } else out.rgb = [ar[0] + (br[0] - ar[0]) * v, ar[1] + (br[1] - ar[1]) * v, ar[2] + (br[2] - ar[2]) * v];\n return out;\n }\n\n /** The colour at `t` (clamped to 0..1). */\n sample(t: number, out: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 }): PaintGradientSample {\n const st = this.stops;\n const n = st.length;\n if (this.uniform) {\n const f = Math.min(1, Math.max(0, t)) * (n - 1);\n const i = Math.min(n - 2, Math.floor(f));\n return this.mix(st[i], st[i + 1], f - i, out);\n }\n if (!(t > st[0].t)) return this.mix(st[0], st[0], 0, out);\n if (t >= st[n - 1].t) return this.mix(st[n - 1], st[n - 1], 0, out);\n let i = 0;\n while (i < n - 2 && !(t < st[i + 1].t)) i++;\n const a = st[i];\n const b = st[i + 1];\n const h = b.t - a.t;\n return this.mix(a, b, h > 0 ? midpointRemap((t - a.t) / h, a.mid) : 1, out);\n }\n\n /** Sampled `travel / length` runs along, past the end as `repeat` says. */\n private along(travel: number, length: number, repeat: PaintGradientRepeat, out: PaintGradientSample): PaintGradientSample {\n const n = this.stops.length;\n if (repeat === 'repeat' && this.uniform) {\n // Whole segments: the multi-colour brush's `(index / length) % colours`, to the bit.\n const segs = n - 1;\n const f = (travel / (length / segs)) % segs;\n const i = Math.min(segs - 1, Math.floor(f));\n return this.mix(this.stops[i], this.stops[i + 1], f - i, out);\n }\n const x = travel / length;\n let t: number;\n if (repeat === 'clamp') t = x;\n else if (repeat === 'repeat') t = x - Math.floor(x);\n else {\n const m = x - 2 * Math.floor(x / 2);\n t = m <= 1 ? m : 2 - m;\n }\n return this.sample(t, out);\n }\n\n /** The colour of one dab (not for `across-tip`: see `tipTable`). */\n dab(d: PaintDabParams, out: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 }): PaintGradientSample {\n const map = this.source.map;\n switch (map.by) {\n case 'distance':\n return this.along(map.unit === 'dabs' ? d.index : d.travel, map.length, map.repeat, out);\n case 'pressure':\n return this.sample(d.pressure, out);\n case 'tilt':\n return this.sample(d.lean, out);\n case 'velocity':\n return this.sample(d.speed === null ? 0 : Math.min(1, d.speed / PAINT_GRADIENT_FULL_SPEED), out);\n case 'direction': {\n const turn = d.direction / (2 * Math.PI);\n return this.sample(turn - Math.floor(turn), out);\n }\n case 'time':\n return this.along(d.time ?? 0, map.duration, map.repeat ?? 'clamp', out);\n case 'random':\n return this.sample(rand01(this.seed, Math.floor(d.index / (map.every ?? 1)), RANDOM_STREAM), out);\n default:\n return this.sample(0.5, out);\n }\n }\n\n /** `steps` colours from the left edge of the tip to the right (`across-tip`). */\n tipTable(steps: number): PaintGradientSample[] {\n const out: PaintGradientSample[] = [];\n for (let j = 0; j < steps; j++) out.push(this.sample(j / (steps - 1)));\n return out;\n }\n}\n\n// --- JSON Schema (for agent tools) -------------------------------------------------\n\nconst STOP_SCHEMA = {\n type: 'object',\n description: 'A gradient stop.',\n properties: {\n offset: { type: 'number', minimum: 0, maximum: 1, description: 'Where the stop is, 0..1 along the gradient. Stops must be sorted by offset.' },\n color: { type: 'string', pattern: '^(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|paint|secondary)$', description: '#rrggbb, or \"paint\" / \"secondary\": the editor\\'s paint or second colour when the stroke starts.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: \"0..1 (default 1): multiplies the brush's flow where this colour is laid.\" },\n midpoint: { type: 'number', minimum: 0.05, maximum: 0.95, description: 'Where between this stop and the next the colours are half mixed, 0.05..0.95 of the way (default 0.5).' },\n },\n required: ['offset', 'color'],\n additionalProperties: false,\n} as const;\n\n/** JSON Schema of a gradient `PaintSource` (what tools accept for gradient paint). */\nexport const PAINT_GRADIENT_JSON_SCHEMA = {\n type: 'object',\n description:\n 'Gradient paint: the brush samples it dab by dab as the stroke goes. Example (blue to yellow over 300 px of stroke, and back): { \"type\": \"gradient\", \"stops\": [{ \"offset\": 0, \"color\": \"#1d4ed8\" }, { \"offset\": 1, \"color\": \"#facc15\" }], \"map\": { \"by\": \"distance\", \"length\": 300, \"repeat\": \"mirror\" } }.',\n properties: {\n type: { type: 'string', enum: ['gradient'], description: 'Always \"gradient\".' },\n stops: { type: 'array', minItems: 2, maxItems: 16, items: STOP_SCHEMA, description: '2..16 stops sorted by offset.' },\n space: { type: 'string', enum: [...PAINT_GRADIENT_SPACES], description: 'How neighbouring stops mix: pigment (default; like paint, blue to yellow passes green), oklab (perceptual) or srgb (plain numbers).' },\n map: {\n type: 'object',\n description: 'What moves the stroke along the gradient (default distance, 300 px, mirror).',\n properties: {\n by: {\n type: 'string',\n enum: [...PAINT_GRADIENT_MAPPINGS],\n description:\n 'distance: px (or dabs) of stroke travelled; pressure: pen pressure 0..1; tilt: pen lean, upright 0 .. flat 1; velocity: pen speed, 0 .. 2.5 px/ms; direction: direction of travel, once round per 360°; time: ms since the stroke began (needs timed points); across-tip: left to right across the tip, looking along the stroke (a brush loaded with several colours); random: a random point per dab (seeded, so replays match).',\n },\n length: { type: 'number', minimum: 1, maximum: 100000, description: 'distance: px of travel (or dabs, with unit \"dabs\") per run through the gradient (default 300).' },\n repeat: { type: 'string', enum: [...PAINT_GRADIENT_REPEATS], description: 'distance and time: past the end, clamp (hold the last colour), repeat (start again) or mirror (run back; default for distance, clamp for time).' },\n unit: { type: 'string', enum: ['px', 'dabs'], description: 'distance: px (default) or dabs.' },\n duration: { type: 'number', minimum: 1, maximum: 600000, description: 'time: ms per run through the gradient (default 2000).' },\n every: { type: 'integer', minimum: 1, maximum: 500, description: 'random: dabs per random pick (default 1).' },\n },\n required: ['by'],\n additionalProperties: false,\n },\n softness: { type: 'number', minimum: 0, maximum: 1, description: 'How softly neighbouring stops blend: 1 smooth (default) .. 0 hard steps.' },\n },\n required: ['type', 'stops'],\n additionalProperties: false,\n} as const;\n\n/** JSON Schema of a colour `PaintSource`. */\nexport const PAINT_COLOR_JSON_SCHEMA = {\n type: 'object',\n description: 'One colour: { \"type\": \"color\", \"color\": \"#rrggbb\" }.',\n properties: {\n type: { type: 'string', enum: ['color'], description: 'Always \"color\".' },\n color: { type: 'string', pattern: '^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$', description: '#rrggbb' },\n },\n required: ['type', 'color'],\n additionalProperties: false,\n} as const;\n\n/** JSON Schema of a `PaintSource`: a colour or gradient paint. */\nexport const PAINT_SOURCE_JSON_SCHEMA = {\n description: 'What to paint with: one colour, or gradient paint.',\n oneOf: [PAINT_COLOR_JSON_SCHEMA, PAINT_GRADIENT_JSON_SCHEMA],\n} as const;\n\n// --- Editing (for gradient editors in any framework) -------------------------------\n\nconst toHex8 = (rgb: readonly number[]) => '#' + rgb.slice(0, 3).map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('');\n\n/** Labels for the mapping picker. */\nexport const PAINT_GRADIENT_MAPPING_LABELS: Readonly<Record<PaintGradientMappingKind, string>> = {\n distance: 'Distance',\n pressure: 'Pressure',\n tilt: 'Tilt',\n velocity: 'Speed',\n direction: 'Direction',\n time: 'Time',\n 'across-tip': 'Across the tip',\n random: 'Random',\n};\nexport const PAINT_GRADIENT_SPACE_LABELS: Readonly<Record<PaintGradientSpace, string>> = { pigment: 'Pigment', oklab: 'Perceptual', srgb: 'RGB' };\nexport const PAINT_GRADIENT_REPEAT_LABELS: Readonly<Record<PaintGradientRepeat, string>> = { clamp: 'Hold', repeat: 'Repeat', mirror: 'Mirror' };\n\n/** The paint colour to the second colour, changing as the stroke is dragged out: where a gradient editor starts. */\nexport const DEFAULT_PAINT_GRADIENT: Readonly<PaintGradientSource> = {\n type: 'gradient',\n stops: [\n { offset: 0, color: 'paint' },\n { offset: 1, color: 'secondary' },\n ],\n space: 'pigment',\n map: { by: 'distance', length: 300, repeat: 'mirror', unit: 'px' },\n};\n\n/** A few starting gradients for gradient editors. */\nexport const PAINT_GRADIENT_PRESETS: readonly { name: string; paint: PaintGradientSource }[] = [\n { name: 'Paint to second colour', paint: DEFAULT_PAINT_GRADIENT },\n {\n name: 'Sky to sun',\n paint: { type: 'gradient', stops: [{ offset: 0, color: '#1d4ed8' }, { offset: 1, color: '#facc15' }], space: 'pigment', map: { by: 'distance', length: 300, repeat: 'mirror', unit: 'px' } },\n },\n {\n name: 'Sunset',\n paint: { type: 'gradient', stops: [{ offset: 0, color: '#7c3aed' }, { offset: 0.5, color: '#e11d48' }, { offset: 1, color: '#f59e0b' }], space: 'oklab', map: { by: 'distance', length: 400, repeat: 'mirror', unit: 'px' } },\n },\n {\n name: 'Rainbow',\n paint: {\n type: 'gradient',\n stops: ['#e11d48', '#f59e0b', '#facc15', '#22c55e', '#3b82f6', '#8b5cf6'].map((color, k) => ({ offset: k / 5, color })),\n space: 'oklab',\n map: { by: 'distance', length: 600, repeat: 'repeat', unit: 'px' },\n },\n },\n {\n name: 'Pressure: light to dark',\n paint: { type: 'gradient', stops: [{ offset: 0, color: 'secondary' }, { offset: 1, color: 'paint' }], space: 'pigment', map: { by: 'pressure' } },\n },\n];\n\n/** Adds a stop at `offset` with the gradient's colour there. Returns the gradient and the new stop's index. */\nexport function addPaintGradientStop(g: PaintGradientSource, offset: number, colors: { paint: string; secondary: string } = { paint: '#000000', secondary: '#ffffff' }): { paint: PaintGradientSource; index: number } {\n if (g.stops.length >= 16) return { paint: g, index: -1 };\n const t = Math.min(1, Math.max(0, offset));\n const s = new PaintGradientSampler({ ...resolvePaintGradient(g, colors), map: { by: 'pressure' } }).sample(t);\n const stop: PaintGradientStop = { offset: t, color: toHex8(s.rgb) };\n if (s.alpha < 1) stop.opacity = Math.round(s.alpha * 1000) / 1000;\n let index = g.stops.findIndex((x) => x.offset > t);\n if (index < 0) index = g.stops.length;\n const stops = [...g.stops.slice(0, index), stop, ...g.stops.slice(index)];\n return { paint: { ...g, stops }, index };\n}\n\n/** Removes stop `index` (a gradient keeps at least 2). */\nexport function removePaintGradientStop(g: PaintGradientSource, index: number): PaintGradientSource {\n if (g.stops.length <= 2 || index < 0 || index >= g.stops.length) return g;\n return { ...g, stops: g.stops.filter((_, i) => i !== index) };\n}\n\n/** Changes stop `index` (moving it keeps the stops sorted). Returns the gradient and where the stop is now. */\nexport function updatePaintGradientStop(g: PaintGradientSource, index: number, patch: Partial<PaintGradientStop>): { paint: PaintGradientSource; index: number } {\n const target = g.stops[index];\n if (!target) return { paint: g, index };\n const next: PaintGradientStop = { ...target, ...patch };\n next.offset = Math.min(1, Math.max(0, next.offset));\n if (next.opacity === undefined || next.opacity >= 1) delete next.opacity;\n else next.opacity = Math.max(0, next.opacity);\n if (next.midpoint === undefined || next.midpoint === 0.5) delete next.midpoint;\n else next.midpoint = Math.min(0.95, Math.max(0.05, next.midpoint));\n const rest = g.stops.filter((_, i) => i !== index);\n let at = rest.findIndex((x) => x.offset > next.offset);\n if (at < 0) at = rest.length;\n return { paint: { ...g, stops: [...rest.slice(0, at), next, ...rest.slice(at)] }, index: at };\n}\n\n/** The gradient with its stops reversed (midpoints measured from the other side). */\nexport function reversePaintGradient(g: PaintGradientSource): PaintGradientSource {\n const st = g.stops;\n const stops = st\n .map((s, i) => {\n const out: PaintGradientStop = { offset: 1 - s.offset, color: s.color };\n if (s.opacity !== undefined) out.opacity = s.opacity;\n // The midpoint of the segment that now follows this stop was the previous stop's.\n const prev = st[i - 1];\n if (prev?.midpoint !== undefined) out.midpoint = 1 - prev.midpoint;\n return out;\n })\n .reverse();\n return { ...g, stops };\n}\n\n/** The mapping for `by`, keeping what carries over from `current` (length, repeat, duration …). */\nexport function paintGradientMappingFor(by: PaintGradientMappingKind, current: PaintGradientMapping): PaintGradientMapping {\n const prev = current as Partial<Record<'length' | 'repeat' | 'unit' | 'duration' | 'every', unknown>>;\n switch (by) {\n case 'distance':\n return { by, length: typeof prev.length === 'number' ? prev.length : 300, repeat: (prev.repeat as PaintGradientRepeat) ?? 'mirror', unit: (prev.unit as 'px' | 'dabs') ?? 'px' };\n case 'time':\n return { by, duration: typeof prev.duration === 'number' ? prev.duration : 2000, repeat: (prev.repeat as PaintGradientRepeat) ?? 'clamp' };\n case 'random':\n return { by, every: typeof prev.every === 'number' ? prev.every : 1 };\n default:\n return { by };\n }\n}\n\n/** Everything a gradient-paint editor UI needs, in one object (framework bindings take it from their own entry). */\nexport const PAINT_GRADIENT_KIT = {\n PAINT_GRADIENT_MAPPINGS,\n PAINT_GRADIENT_MAPPING_LABELS,\n PAINT_GRADIENT_PRESETS,\n PAINT_GRADIENT_REPEATS,\n PAINT_GRADIENT_REPEAT_LABELS,\n PAINT_GRADIENT_SPACES,\n PAINT_GRADIENT_SPACE_LABELS,\n addPaintGradientStop,\n paintGradientMappingFor,\n paintSourceCss,\n removePaintGradientStop,\n reversePaintGradient,\n updatePaintGradientStop,\n} as const;\nexport type PaintGradientKit = typeof PAINT_GRADIENT_KIT;\n","// Portions ported from photocraft/crates/paint/src/lib.rs, dynamics.rs and render.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The brush engine (a subset of photocraft's `crates/paint`): a computed round tip with size,\n * hardness, angle, roundness and spacing; pressure to size and opacity; smoothing; and Photoshop's\n * opacity-versus-flow build-up. Strokes are data, so one replays the same way for undo, agents\n * and tests.\n *\n * - Points are walked into dabs at `spacing × diameter` (`walkDabs`).\n * - Each dab's coverage (`dabCoverage`, photocraft's measured Photoshop profile) builds up in a\n * stroke coverage map with flow towards the dab's opacity ceiling: `c ← c + v·(ceil − c)`.\n * - The stroke composites once at the stroke opacity over the layer as it was before the stroke,\n * so overlapping dabs never exceed the opacity.\n */\n\nimport { blendPixel, mixPigment8, mixPigmentRgb, type BlendMode } from '../raster/blend';\nimport { srgbDecode, srgbEncode } from '../raster/deep';\nimport type { RasterRect as CropBox } from '../raster/surface';\nimport { TILE, tileKey, tileCoord, type TileKey } from '../raster/surface';\nimport { PaintGradientSampler, type PaintDabParams, type PaintGradientSample, type PaintGradientSource, type PaintRgb } from './gradient-paint';\n\n/** One input sample. Missing pressure is a mouse (1). */\nexport interface StrokePoint {\n x: number;\n y: number;\n /** 0..1 (default 1). */\n pressure?: number;\n /** Milliseconds (optional; kept for replays). */\n t?: number;\n}\n\nexport interface BrushSettings {\n /** Diameter in pixels (1..2500, default 20). */\n size: number;\n /** 0 (soft) .. 1 (hard, default). */\n hardness: number;\n /** Dab spacing as a fraction of the diameter (0.01..10, default 0.1). */\n spacing: number;\n /** Tip angle in degrees (default 0). */\n angle: number;\n /** Tip roundness 0.01..1 (default 1). */\n roundness: number;\n /** Stroke opacity ceiling 0..1 (default 1). */\n opacity: number;\n /** Build-up per dab 0..1 (default 1). */\n flow: number;\n /** Input smoothing 0..0.95 (default 0). */\n smoothing: number;\n /** Pressure controls size (default true). */\n pressureSize: boolean;\n /** Pressure controls opacity (default false). */\n pressureOpacity: boolean;\n /** How the stroke paints onto the layer (default normal). */\n blend: BlendMode;\n}\n\nexport const DEFAULT_BRUSH: Readonly<BrushSettings> = {\n size: 20,\n hardness: 1,\n spacing: 0.1,\n angle: 0,\n roundness: 1,\n opacity: 1,\n flow: 1,\n smoothing: 0,\n pressureSize: true,\n pressureOpacity: false,\n blend: 'normal',\n};\n\nexport const MAX_BRUSH_SIZE = 2500;\n\n/** A brush with every field in range (missing fields from `DEFAULT_BRUSH`). */\nexport function normalizeBrush(raw: Partial<BrushSettings> = {}): BrushSettings {\n const num = (v: unknown, lo: number, hi: number, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d);\n return {\n size: num(raw.size, 1, MAX_BRUSH_SIZE, DEFAULT_BRUSH.size),\n hardness: num(raw.hardness, 0, 1, DEFAULT_BRUSH.hardness),\n spacing: num(raw.spacing, 0.01, 10, DEFAULT_BRUSH.spacing),\n angle: num(raw.angle, -360, 360, DEFAULT_BRUSH.angle),\n roundness: num(raw.roundness, 0.01, 1, DEFAULT_BRUSH.roundness),\n opacity: num(raw.opacity, 0, 1, DEFAULT_BRUSH.opacity),\n flow: num(raw.flow, 0, 1, DEFAULT_BRUSH.flow),\n smoothing: num(raw.smoothing, 0, 0.95, DEFAULT_BRUSH.smoothing),\n pressureSize: typeof raw.pressureSize === 'boolean' ? raw.pressureSize : DEFAULT_BRUSH.pressureSize,\n pressureOpacity: typeof raw.pressureOpacity === 'boolean' ? raw.pressureOpacity : DEFAULT_BRUSH.pressureOpacity,\n blend: raw.blend ?? 'normal',\n };\n}\n\n// ---------------------------------------------------------------------------\n// The round tip (photocraft's measured Photoshop profile)\n// ---------------------------------------------------------------------------\n\n/**\n * Photoshop's round tip, measured by photocraft: a solid core of radius `r0 · R`, then a Gaussian\n * shoulder `exp(-((d − r0) / s)²)` of width `s · R`, both by hardness (0, 0.1, … 1).\n */\nconst TIP_SHAPE: ReadonlyArray<[number, number]> = [\n [0.0, 0.664], [0.14, 0.58], [0.28, 0.5], [0.4, 0.44], [0.52, 0.36], [0.63, 0.29],\n [0.7, 0.24], [0.78, 0.19], [0.84, 0.15], [0.89, 0.11], [1.0, 0.0],\n];\n/** The anti-aliased edge: the shoulder never narrows below this many pixels. */\nconst TIP_EDGE_PX = 1.2;\nconst SMALL_TIP_RADIUS = 3;\nconst SUBPIXEL = [-0.375, -0.125, 0.125, 0.375];\n\n/** The core radius and shoulder width (pixels) of a round tip of `radius` and `hardness`. */\nexport function tipShape(radius: number, hardness: number): [number, number] {\n const h = (Number.isNaN(hardness) ? 0 : Math.min(1, Math.max(0, hardness))) * 10;\n const i = Math.min(9, Math.floor(h));\n const t = h - i;\n const a = TIP_SHAPE[i];\n const b = TIP_SHAPE[i + 1];\n const r0 = a[0] + (b[0] - a[0]) * t;\n const s = a[1] + (b[1] - a[1]) * t;\n const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;\n const edge = Math.max(0.25, Math.min(TIP_EDGE_PX, 0.3 * r));\n return [Math.max(0, Math.min(r0 * r, r - edge)), Math.max(1e-3, s * r, edge)];\n}\n\n/** `exp(-u)` as `(1 − u/64)^64` by six squarings: exactly 0 from u = 64. */\nfunction softExp(u: number): number {\n let g = Math.min(1, Math.max(0, 1 - u / 64));\n for (let k = 0; k < 6; k++) g *= g;\n return g;\n}\n\n/** Opacity of a round tip at distance `d` from its centre. */\nexport function tipFalloff(d: number, radius: number, hardness: number): number {\n const [r0, s] = tipShape(radius, hardness);\n if (d <= r0) return 1;\n const x = (d - r0) / s;\n return softExp(x * x);\n}\n\n/** How far from its centre a round tip still paints. */\nexport function tipReach(radius: number, hardness: number): number {\n const [r0, s] = tipShape(radius, hardness);\n return r0 + 2.6 * s;\n}\n\n/** Coverage of a round dab at distance `d` from its centre. */\nexport function dabCoverage(d: number, radius: number, hardness: number): number {\n if (Number.isNaN(d) || d >= tipReach(radius, hardness) + 0.5) return 0;\n return tipFalloff(d, radius, hardness);\n}\n\n// ---------------------------------------------------------------------------\n// Points → dabs\n// ---------------------------------------------------------------------------\n\nexport interface Dab {\n x: number;\n y: number;\n radius: number;\n /** Flow (per-dab build-up). */\n alpha: number;\n /** Opacity ceiling this dab builds up to. */\n opacity: number;\n /** Radians. */\n angle: number;\n roundness: number;\n /** What gradient paint maps by (travel, index, pressure, time …); set by the dab walkers. */\n params?: PaintDabParams;\n}\n\n/** Exponential smoothing of input points; the first and last point are kept. */\nexport function smoothPoints(points: readonly StrokePoint[], amount: number): StrokePoint[] {\n const a = 1 - Math.min(0.95, Math.max(0, amount));\n if (a >= 1 || points.length < 3) return points.slice();\n const out: StrokePoint[] = [];\n let cur: StrokePoint | null = null;\n for (const p of points) {\n const n: StrokePoint = cur ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;\n out.push(n);\n cur = n;\n }\n out[out.length - 1] = points[points.length - 1];\n return out;\n}\n\nconst pressureOf = (p: StrokePoint) => (typeof p.pressure === 'number' && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1);\n\n/**\n * Walks points into dabs (incrementally: feed points in any chunks and get the same dabs). The\n * first point is a dab; then one every `spacing × diameter` along the path.\n */\nexport class DabWalker {\n private last: StrokePoint | null = null;\n private nextAt = 0;\n private pendingFirst: StrokePoint | null = null;\n private index = 0;\n private travelled = 0;\n private dir = 0;\n private firstT: number | null = null;\n private lastSpeedT: number | null = null;\n private lastSpeedAt: StrokePoint | null = null;\n private speed: number | null = null;\n\n constructor(readonly brush: BrushSettings) {}\n\n private diameter(p: StrokePoint): number {\n return this.brush.pressureSize ? this.brush.size * pressureOf(p) : this.brush.size;\n }\n\n private stepLen(p: StrokePoint): number {\n return Math.max(0.5, Math.max(1, this.diameter(p)) * Math.max(0.01, this.brush.spacing));\n }\n\n private dab(p: StrokePoint, travel: number): Dab {\n const b = this.brush;\n const time = typeof p.t === 'number' && Number.isFinite(p.t) && this.firstT !== null ? p.t - this.firstT : null;\n return {\n x: p.x,\n y: p.y,\n radius: Math.max(0.5, this.diameter(p) / 2),\n alpha: Math.min(1, Math.max(0, b.flow)),\n opacity: b.pressureOpacity ? pressureOf(p) : 1,\n angle: (b.angle * Math.PI) / 180,\n roundness: Math.min(1, Math.max(0.01, b.roundness)),\n params: { travel, index: this.index++, pressure: pressureOf(p), lean: 0, speed: time === null ? null : (this.speed ?? 0), direction: this.dir, time },\n };\n }\n\n /** Tracks time and a smoothed speed from timed samples (for gradient paint). */\n private timeOf(p: StrokePoint): void {\n if (typeof p.t !== 'number' || !Number.isFinite(p.t)) return;\n if (this.firstT === null) this.firstT = p.t;\n const a = this.lastSpeedAt;\n if (a && this.lastSpeedT !== null && p.t > this.lastSpeedT) {\n const v = Math.hypot(p.x - a.x, p.y - a.y) / (p.t - this.lastSpeedT);\n this.speed = this.speed === null ? v : this.speed + (v - this.speed) * 0.35;\n }\n this.lastSpeedT = p.t;\n this.lastSpeedAt = p;\n }\n\n push(p: StrokePoint, out: Dab[]): void {\n const a = this.last;\n this.timeOf(p);\n if (!a) {\n this.last = p;\n this.pendingFirst = p;\n this.nextAt = this.stepLen(p);\n return;\n }\n const dx = p.x - a.x;\n const dy = p.y - a.y;\n const len = Math.hypot(dx, dy);\n if (len > 1e-9) this.dir = Math.atan2(-dy, dx);\n if (this.pendingFirst) {\n out.push(this.dab(this.pendingFirst, 0));\n this.pendingFirst = null;\n }\n if (len > 1e-9) {\n while (this.nextAt <= len + 1e-9) {\n const f = Math.min(1, this.nextAt / len);\n const q: StrokePoint = {\n x: a.x + dx * f,\n y: a.y + dy * f,\n pressure: pressureOf(a) + (pressureOf(p) - pressureOf(a)) * f,\n };\n if (typeof a.t === 'number' && typeof p.t === 'number') q.t = a.t + (p.t - a.t) * f;\n out.push(this.dab(q, this.travelled + this.nextAt));\n this.nextAt += this.stepLen(q);\n }\n this.nextAt -= len;\n this.travelled += len;\n }\n this.last = p;\n }\n\n finish(out: Dab[]): void {\n if (this.pendingFirst) {\n out.push(this.dab(this.pendingFirst, 0));\n this.pendingFirst = null;\n }\n }\n}\n\n/** All dabs of a point list (smoothing applied). */\nexport function walkDabs(points: readonly StrokePoint[], brush: BrushSettings): Dab[] {\n const w = new DabWalker(brush);\n const out: Dab[] = [];\n for (const p of smoothPoints(points, brush.smoothing)) w.push(p, out);\n w.finish(out);\n return out;\n}\n\n/** The pixel rectangle a dab can touch. */\nexport function dabRect(d: Dab, hardness: number): CropBox {\n // As `rasterizeDab`: the tip reaches `reach` across its minor axis and `reach / roundness` along it.\n const ro = Math.min(1, Math.max(0.5 / d.radius, d.roundness));\n const rm = Math.max(0.5, d.radius * ro);\n const r = Math.max(rm, tipReach(rm, hardness)) / ro + 1.5;\n const x0 = Math.floor(d.x - r);\n const y0 = Math.floor(d.y - r);\n return { x: x0, y: y0, width: Math.ceil(d.x + r) - x0, height: Math.ceil(d.y + r) - y0 };\n}\n\n/**\n * Rasterizes one dab over `rect` into `out` (rect.width × rect.height values, coverage × flow).\n * Small tips (radius below 3 px) take each pixel's covered area from a 4×4 grid.\n */\nexport function rasterizeDab(d: Dab, hardness: number, rect: CropBox, out: Float32Array): void {\n const w = rect.width;\n const h = rect.height;\n out.fill(0, 0, w * h);\n const r = d.radius;\n const ro = Math.min(1, Math.max(0.5 / r, d.roundness));\n const rm = Math.max(0.5, r * ro);\n const reach = Math.max(rm, tipReach(rm, hardness));\n const sn = Math.sin(d.angle);\n const cs = Math.cos(d.angle);\n const toTip = (dx: number, dy: number): [number, number] => {\n const ux = dx;\n const uy = -dy;\n return [ux * cs + uy * sn, -ux * sn + uy * cs];\n };\n const small = rm < SMALL_TIP_RADIUS;\n const reach2 = (reach + (small ? 0.75 : 0.5)) ** 2;\n for (let yy = 0; yy < h; yy++) {\n const dy = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < w; xx++) {\n const dx = rect.x + xx + 0.5 - d.x;\n const [u, v] = toTip(dx, dy);\n const d2 = (u * ro) ** 2 + v * v;\n if (d2 >= reach2) continue;\n let val: number;\n if (small) {\n let sum = 0;\n for (const sy of SUBPIXEL) {\n for (const sx of SUBPIXEL) {\n const [su, sv] = toTip(dx + sx, dy + sy);\n sum += tipFalloff(Math.sqrt((su * ro) ** 2 + sv * sv), rm, hardness);\n }\n }\n val = sum / 16;\n } else {\n val = dabCoverage(Math.sqrt(d2), rm, hardness);\n }\n if (val > 0) out[yy * w + xx] = val * d.alpha;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Stroke coverage\n// ---------------------------------------------------------------------------\n\nconst TILE_PIXELS = TILE * TILE;\n\n/** A stroke's accumulated coverage, in 256 px tiles over document space. */\nexport class CoverageMap {\n readonly tiles = new Map<TileKey, Float32Array>();\n /**\n * Gradient paint: the colour laid at each pixel (sRGB 0..1, 3 per pixel), when dabs carry their\n * own colours. Each dab blends its colour in by the share of coverage it adds. Missing: the\n * stroke's one colour (`StrokeApply.color`).\n */\n readonly colors = new Map<TileKey, Float32Array>();\n bounds: CropBox | null = null;\n private dirty = new Set<TileKey>();\n\n /** The colour at a pixel (gradient paint), or `null` when the stroke has one colour there. */\n colorAt(x: number, y: number, out: number[]): number[] | null {\n const tx = Math.floor(x / TILE);\n const ty = Math.floor(y / TILE);\n const t = this.colors.get(tileKey(tx, ty));\n if (!t) return null;\n const i = ((y - ty * TILE) * TILE + (x - tx * TILE)) * 3;\n out[0] = t[i];\n out[1] = t[i + 1];\n out[2] = t[i + 2];\n return out;\n }\n\n get(x: number, y: number): number {\n const tx = Math.floor(x / TILE);\n const ty = Math.floor(y / TILE);\n const t = this.tiles.get(tileKey(tx, ty));\n return t ? t[(y - ty * TILE) * TILE + (x - tx * TILE)] : 0;\n }\n\n /**\n * Builds up `vals` (over `rect`) with flow towards `ceil`: `c ← c + v·(ceil − c)`. With `color`\n * (one sRGB colour for the dab) or `field` (sRGB per pixel of `rect`, 3 each), the pixels' colours\n * move towards it by the share of coverage this dab adds (gradient paint).\n */\n accumulate(rect: CropBox, vals: Float32Array, ceil: number, color: readonly number[] | null = null, field: Float32Array | null = null): void {\n if (rect.width <= 0 || rect.height <= 0) return;\n const w = rect.width;\n const tx0 = Math.floor(rect.x / TILE);\n const ty0 = Math.floor(rect.y / TILE);\n const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);\n const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);\n let touched = false;\n for (let ty = ty0; ty <= ty1; ty++) {\n for (let tx = tx0; tx <= tx1; tx++) {\n const x0 = Math.max(rect.x, tx * TILE);\n const y0 = Math.max(rect.y, ty * TILE);\n const x1 = Math.min(rect.x + rect.width, tx * TILE + TILE);\n const y1 = Math.min(rect.y + rect.height, ty * TILE + TILE);\n let tile: Float32Array | undefined;\n for (let y = y0; y < y1; y++) {\n const row = (y - rect.y) * w;\n const trow = (y - ty * TILE) * TILE;\n for (let x = x0; x < x1; x++) {\n const v = vals[row + (x - rect.x)];\n if (v <= 0) continue;\n if (!tile) {\n const key = tileKey(tx, ty);\n tile = this.tiles.get(key);\n if (!tile) {\n tile = new Float32Array(TILE_PIXELS);\n this.tiles.set(key, tile);\n }\n this.dirty.add(key);\n touched = true;\n }\n const ti = trow + (x - tx * TILE);\n const c = tile[ti];\n if (ceil > c) {\n const next = c + v * (ceil - c);\n tile[ti] = next;\n if (color || field) {\n const key = tileKey(tx, ty);\n let ct = this.colors.get(key);\n if (!ct) {\n ct = new Float32Array(TILE_PIXELS * 3);\n this.colors.set(key, ct);\n }\n const w = c > 0 ? (next - c) / next : 1;\n const fi = (row + (x - rect.x)) * 3;\n const ci = ti * 3;\n for (let ch = 0; ch < 3; ch++) {\n const col = field ? field[fi + ch] : color![ch];\n ct[ci + ch] += (col - ct[ci + ch]) * w;\n }\n }\n }\n }\n }\n }\n }\n if (touched) this.bounds = unionRect(this.bounds, rect);\n }\n\n /** Tiles changed since the last call. */\n takeDirty(): TileKey[] {\n const out = [...this.dirty];\n this.dirty.clear();\n return out;\n }\n}\n\nfunction unionRect(a: CropBox | null, b: CropBox): CropBox {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\n/** Paints a stroke's dabs into a coverage map, incrementally. */\nexport class StrokeCoverage {\n readonly coverage = new CoverageMap();\n private walker: DabWalker;\n private smoothing: number;\n private smoothPos: StrokePoint | null = null;\n private buf = new Float32Array(0);\n private field = new Float32Array(0);\n private readonly sampler: PaintGradientSampler | null;\n private readonly tipTable: PaintGradientSample[] | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n dabCount = 0;\n\n /** `paint`: a gradient source (resolved: no `paint` / `secondary` stops) for gradient paint; `seed` for its `random` mapping. */\n constructor(\n readonly brush: BrushSettings,\n paint: PaintGradientSource | null = null,\n seed = 0\n ) {\n this.walker = new DabWalker(brush);\n this.smoothing = brush.smoothing;\n this.sampler = paint ? new PaintGradientSampler(paint, seed) : null;\n this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;\n }\n\n /** Feeds input points; returns the document rectangles the new dabs touched. */\n push(points: readonly StrokePoint[]): CropBox[] {\n const dabs: Dab[] = [];\n for (const p of points) this.walker.push(this.smooth(p), dabs);\n return this.paint(dabs);\n }\n\n /** Ends the stroke (a single click still paints one dab). */\n finish(last?: StrokePoint): CropBox[] {\n const dabs: Dab[] = [];\n if (last && this.smoothPos && (this.smoothPos.x !== last.x || this.smoothPos.y !== last.y)) this.walker.push(last, dabs);\n this.walker.finish(dabs);\n return this.paint(dabs);\n }\n\n private smooth(p: StrokePoint): StrokePoint {\n const a = 1 - Math.min(0.95, Math.max(0, this.smoothing));\n const cur = this.smoothPos;\n const n = cur && a < 1 ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;\n this.smoothPos = n;\n return n;\n }\n\n private paint(dabs: Dab[]): CropBox[] {\n const rects: CropBox[] = [];\n for (const d of dabs) {\n const rect = dabRect(d, this.brush.hardness);\n const n = rect.width * rect.height;\n if (this.buf.length < n) this.buf = new Float32Array(n);\n const vals = this.buf.subarray(0, n);\n rasterizeDab(d, this.brush.hardness, rect, vals);\n if (this.sampler) this.paintDab(d, rect, vals);\n else this.coverage.accumulate(rect, vals, d.opacity);\n rects.push(rect);\n this.dabCount++;\n }\n return rects;\n }\n\n private paintDab(d: Dab, rect: CropBox, vals: Float32Array): void {\n const scratch = { field: this.field, sample: this.sample };\n const { color, field } = gradientDabColor(this.sampler!, this.tipTable, d, d.params!, rect, vals, scratch);\n this.field = scratch.field;\n this.coverage.accumulate(rect, vals, d.opacity, color, field);\n }\n}\n\n/** Colours across the tip of an `across-tip` gradient. */\nexport const TIP_GRADIENT_STEPS = 32;\n\n/**\n * Gradient paint for one dab: the dab's colour (or, across the tip, a colour per pixel) and its\n * opacity, which multiplies the flow. Shared by every coverage stroke.\n */\nexport function gradientDabColor(\n sampler: PaintGradientSampler,\n tipTable: readonly PaintGradientSample[] | null,\n d: Dab,\n params: PaintDabParams,\n rect: CropBox,\n vals: Float32Array,\n scratch: { field: Float32Array; sample: PaintGradientSample }\n): { color: PaintRgb | null; field: Float32Array | null } {\n if (!tipTable) {\n const s = sampler.dab(params, scratch.sample);\n if (s.alpha < 1) for (let k = 0; k < vals.length; k++) vals[k] *= s.alpha;\n return { color: s.rgb, field: null };\n }\n const n = rect.width * rect.height;\n if (scratch.field.length < n * 3) scratch.field = new Float32Array(n * 3);\n const f = scratch.field;\n // Across the stroke: the normal to the direction of travel (document y points down).\n const nx = Math.sin(params.direction);\n const ny = Math.cos(params.direction);\n const r = Math.max(0.5, d.radius);\n const last = tipTable.length - 1;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const py = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < rect.width; xx++, k++) {\n const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;\n const e = tipTable[Math.min(last, Math.max(0, Math.round(((u + 1) / 2) * last)))];\n f[k * 3] = e.rgb[0];\n f[k * 3 + 1] = e.rgb[1];\n f[k * 3 + 2] = e.rgb[2];\n if (e.alpha < 1) vals[k] *= e.alpha;\n }\n }\n return { color: null, field: f };\n}\n\n/** How a stroke changes pixels. */\nexport interface StrokeApply {\n /** `paint` puts colour down; `erase` removes alpha (or, with transparency locked, paints `background`). */\n mode: 'paint' | 'erase';\n /** RGBA 0..255 (paint colour, or the mask grey in `r`). */\n color: readonly [number, number, number, number];\n /** RGBA 0..255 the eraser paints on a transparency-locked layer (and on masks). */\n background: readonly [number, number, number, number];\n opacity: number;\n blend: BlendMode;\n lockTransparency: boolean;\n /** Selection coverage (document space, canvas-sized), or `null`. */\n selection: { width: number; height: number; data: ArrayLike<number> } | null;\n /** Layer offset (target pixels are in layer space). */\n offset: { x: number; y: number };\n}\n\n/**\n * Composites the stroke's coverage over `tileData` (the target's tile (tx, ty) as it was before\n * the stroke, in layer space; RGBA or one byte for masks) and returns the new tile data. With\n * `into` (the tile's previous result) and `region` (document space), only `region` is recomputed\n * and the rest of `into` is kept.\n */\nexport function applyStrokeToTile(\n coverage: CoverageMap,\n tileData: Uint8ClampedArray,\n channels: 1 | 4,\n tx: number,\n ty: number,\n s: StrokeApply,\n options: { into?: Uint8ClampedArray; region?: CropBox } = {}\n): Uint8ClampedArray {\n const out = options.into ? new Uint8ClampedArray(options.into) : new Uint8ClampedArray(tileData);\n const ox = tx * TILE + s.offset.x;\n const oy = ty * TILE + s.offset.y;\n let x0 = 0;\n let y0 = 0;\n let x1 = TILE;\n let y1 = TILE;\n if (options.region) {\n const r = options.region;\n x0 = Math.max(0, Math.floor(r.x) - ox);\n y0 = Math.max(0, Math.floor(r.y) - oy);\n x1 = Math.min(TILE, Math.ceil(r.x + r.width) - ox);\n y1 = Math.min(TILE, Math.ceil(r.y + r.height) - oy);\n if (x1 <= x0 || y1 <= y0) return out;\n // Start the region from the pre-stroke pixels: coverage is cumulative.\n if (options.into) {\n for (let y = y0; y < y1; y++) out.set(tileData.subarray((y * TILE + x0) * channels, (y * TILE + x1) * channels), (y * TILE + x0) * channels);\n }\n }\n const sel = s.selection;\n const cb = [0, 0, 0, 0];\n const cs = [s.color[0] / 255, s.color[1] / 255, s.color[2] / 255, s.color[3] / 255];\n const bg = [s.background[0] / 255, s.background[1] / 255, s.background[2] / 255, s.background[3] / 255];\n // With a tile-aligned offset the layer tile is one coverage tile: read it directly.\n const aligned = s.offset.x % TILE === 0 && s.offset.y % TILE === 0;\n const covTile = aligned ? coverage.tiles.get(tileKey(Math.floor(ox / TILE), Math.floor(oy / TILE))) : undefined;\n if (aligned && !covTile) return out;\n // Gradient paint: each pixel's own colour (alpha stays the stroke's).\n const colored = s.mode === 'paint' && coverage.colors.size > 0;\n const own = [0, 0, 0, cs[3]];\n const ownBytes: [number, number, number, number] = [0, 0, 0, s.color[3]];\n for (let y = y0; y < y1; y++) {\n const dy = oy + y;\n for (let x = x0; x < x1; x++) {\n const dx = ox + x;\n const c = covTile ? covTile[y * TILE + x] : coverage.get(dx, dy);\n if (c <= 0) continue;\n const pixelColor = colored ? coverage.colorAt(dx, dy, own) : null;\n if (pixelColor) for (let ch = 0; ch < 3; ch++) ownBytes[ch] = Math.round(Math.min(1, Math.max(0, own[ch])) * 255);\n let k = c * s.opacity;\n if (sel) {\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) continue;\n k *= sel.data[dy * sel.width + dx] / 255;\n }\n if (k > 1) k = 1;\n if (k <= 0) continue;\n const p = y * TILE + x;\n if (channels === 1) {\n const target = s.mode === 'erase' ? s.background[0] : pixelColor ? Math.round(0.299 * ownBytes[0] + 0.587 * ownBytes[1] + 0.114 * ownBytes[2]) : s.color[0];\n out[p] = Math.round(out[p] + (target - out[p]) * k);\n continue;\n }\n const i = p * 4;\n if (s.mode === 'erase' && !s.lockTransparency) {\n out[i + 3] = Math.round(out[i + 3] * (1 - k));\n continue;\n }\n if (s.lockTransparency && out[i + 3] === 0) continue;\n const src = s.mode === 'erase' ? bg : pixelColor ? own : cs;\n if (s.blend === 'pigment') {\n // `blendPixel`'s paint mix on the 8-bit colours, with their spectra cached.\n const bytes = s.mode === 'erase' ? s.background : pixelColor ? ownBytes : s.color;\n const as = src[3] * k;\n if (!(as > 0)) continue;\n const ao = as + (out[i + 3] / 255) * (1 - as);\n mixPigment8(out[i], out[i + 1], out[i + 2], bytes[0], bytes[1], bytes[2], as / ao, cb);\n out[i] = Math.round(cb[0] * 255);\n out[i + 1] = Math.round(cb[1] * 255);\n out[i + 2] = Math.round(cb[2] * 255);\n if (!s.lockTransparency) out[i + 3] = Math.round(ao * 255);\n continue;\n }\n for (let ch = 0; ch < 4; ch++) cb[ch] = out[i + ch] / 255;\n const o = blendPixel(s.blend, cb, src, k);\n out[i] = Math.round(o[0] * 255);\n out[i + 1] = Math.round(o[1] * 255);\n out[i + 2] = Math.round(o[2] * 255);\n if (!s.lockTransparency) out[i + 3] = Math.round(o[3] * 255);\n }\n }\n return out;\n}\n\n/**\n * `applyStrokeToTile` at high bit depth: composites the stroke's coverage over deep RGBA\n * samples (`Uint16Array` sRGB or `Float32Array` linear light, see `raster/deep`) of the target's\n * tile (tx, ty) as it was before the stroke, and returns new samples of the same type. Blending\n * runs in float in the samples' own space (sRGB at 16 bits, linear light at 32 bits); the paint\n * colours are 8-bit sRGB and converted. With `options.channels` 1, a mask's coverage samples\n * (16-bit, or float 0..1) move towards the paint's grey, as `applyStrokeToTile` does at 8 bits.\n */\nexport function applyStrokeToDeepTile(\n coverage: CoverageMap,\n samples: Uint16Array | Float32Array,\n tx: number,\n ty: number,\n s: StrokeApply,\n options: { into?: Uint16Array | Float32Array; region?: CropBox; channels?: 1 | 4 } = {}\n): Uint16Array | Float32Array {\n const linear = samples instanceof Float32Array;\n const scale = linear ? 1 : 65535;\n const n = options.channels ?? 4;\n const out = (options.into ?? samples).slice() as Uint16Array | Float32Array;\n const ox = tx * TILE + s.offset.x;\n const oy = ty * TILE + s.offset.y;\n let x0 = 0;\n let y0 = 0;\n let x1 = TILE;\n let y1 = TILE;\n if (options.region) {\n const r = options.region;\n x0 = Math.max(0, Math.floor(r.x) - ox);\n y0 = Math.max(0, Math.floor(r.y) - oy);\n x1 = Math.min(TILE, Math.ceil(r.x + r.width) - ox);\n y1 = Math.min(TILE, Math.ceil(r.y + r.height) - oy);\n if (x1 <= x0 || y1 <= y0) return out;\n if (options.into) for (let y = y0; y < y1; y++) out.set(samples.subarray((y * TILE + x0) * n, (y * TILE + x1) * n), (y * TILE + x0) * n);\n }\n const grey = ((s.mode === 'erase' ? s.background[0] : s.color[0]) / 255) * scale;\n const colour = (c: readonly number[]) => {\n const v = [c[0] / 255, c[1] / 255, c[2] / 255, c[3] / 255];\n if (linear) for (let k = 0; k < 3; k++) v[k] = srgbDecode(v[k]);\n return v;\n };\n const src0 = colour(s.mode === 'erase' ? s.background : s.color);\n let src = src0;\n // Gradient paint: each pixel's own colour (alpha stays the stroke's).\n const colored = s.mode === 'paint' && coverage.colors.size > 0;\n const own = [0, 0, 0, src0[3]];\n const ownSrc = [0, 0, 0, src0[3]];\n const sel = s.selection;\n const cb = [0, 0, 0, 0];\n const mix = [0, 0, 0];\n const encCb = [0, 0, 0];\n const encSrc0 = linear ? [srgbEncode(src0[0]), srgbEncode(src0[1]), srgbEncode(src0[2])] : src0;\n const put = (i: number, v: number) => {\n out[i] = linear ? v : v <= 0 ? 0 : v >= 1 ? 65535 : Math.round(v * 65535);\n };\n for (let y = y0; y < y1; y++) {\n const dy = oy + y;\n for (let x = x0; x < x1; x++) {\n const dx = ox + x;\n const c = coverage.get(dx, dy);\n if (c <= 0) continue;\n const pixelColor = colored ? coverage.colorAt(dx, dy, own) : null;\n let k = c * s.opacity;\n if (sel) {\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) continue;\n k *= sel.data[dy * sel.width + dx] / 255;\n }\n if (k > 1) k = 1;\n if (k <= 0) continue;\n src = src0;\n if (pixelColor) {\n for (let ch = 0; ch < 3; ch++) ownSrc[ch] = linear ? srgbDecode(own[ch]) : own[ch];\n src = ownSrc;\n }\n if (n === 1) {\n // Mask coverage: towards the paint's grey (masks take the colour's grey value).\n const p = y * TILE + x;\n const g = pixelColor ? (0.299 * own[0] + 0.587 * own[1] + 0.114 * own[2]) * scale : grey;\n const v = out[p] + (g - out[p]) * k;\n out[p] = linear ? v : Math.round(v);\n continue;\n }\n const i = (y * TILE + x) * 4;\n if (s.mode === 'erase' && !s.lockTransparency) {\n put(i + 3, (out[i + 3] / scale) * (1 - k));\n // A fully erased pixel keeps no colour, so a cleared tile stores nothing.\n if (out[i + 3] === 0) out[i] = out[i + 1] = out[i + 2] = 0;\n continue;\n }\n if (s.lockTransparency && out[i + 3] === 0) continue;\n for (let ch = 0; ch < 4; ch++) cb[ch] = out[i + ch] / scale;\n if (s.blend === 'pigment') {\n const as = src[3] * k;\n if (!(as > 0)) continue;\n const ao = as + cb[3] * (1 - as);\n for (let ch = 0; ch < 3; ch++) encCb[ch] = linear ? srgbEncode(cb[ch]) : cb[ch];\n mixPigmentRgb(encCb, pixelColor ? own : encSrc0, as / ao, mix);\n for (let ch = 0; ch < 3; ch++) put(i + ch, linear ? srgbDecode(mix[ch]) : mix[ch]);\n if (!s.lockTransparency) put(i + 3, ao);\n continue;\n }\n const o = blendPixel(s.blend, cb, src, k);\n put(i, o[0]);\n put(i + 1, o[1]);\n put(i + 2, o[2]);\n if (!s.lockTransparency) put(i + 3, o[3]);\n }\n }\n return out;\n}\n\n/** Tile coordinates of coverage tile keys (for callers that track dirty tiles). */\nexport function coverageTileCoord(key: TileKey): { tx: number; ty: number } {\n return tileCoord(key);\n}\n","// Portions ported from photocraft/crates/paint/src/procedural.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Paper grain: generated height maps (0 = valley, 1 = peak) that tile across a surface, and how\n * much of a dry dab the grain catches. Shared by the paint editor's papers and standalone\n * brushstrokes (`renderBrushstroke` with `character`). Everything is generated from a pattern and a\n * seed, so no texture is bundled.\n */\n\nimport { hash2, TileableNoise } from './rand';\n\n/** Grain generators. Each is our own procedural pattern (no scanned textures). */\nexport type GrainPattern =\n | 'smooth'\n | 'cold-pressed'\n | 'hot-pressed'\n | 'rough'\n | 'vellum'\n | 'laid'\n | 'ingres'\n | 'sanded'\n | 'canvas'\n | 'linen'\n | 'gesso';\n\nexport const GRAIN_PATTERNS: readonly GrainPattern[] = ['smooth', 'cold-pressed', 'hot-pressed', 'rough', 'vellum', 'laid', 'ingres', 'sanded', 'canvas', 'linen', 'gesso'];\n\n/** How a height map is tuned for one kind of media. */\nexport interface GrainTuning {\n /** Height contrast around the middle, 0..3 (default 1). */\n contrast: number;\n /** Swap peaks and valleys (default false). */\n invert: boolean;\n}\n\nexport type PaperTiling = 'repeat' | 'mirror';\n\n/** What a `PaperGrain` reads from a paper (the paint editor's `PaperSettings` has it all). */\nexport interface PaperGrainSettings {\n pattern: GrainPattern;\n /** Grain seed: another seed gives another sheet of the same paper. */\n seed: number;\n /** Grain scale: 1 is the pattern's natural size, 2 twice as coarse. */\n scale: number;\n tiling: PaperTiling;\n dry: GrainTuning;\n wet: GrainTuning;\n}\n\n/** Side of the generated grain tile in pixels (at scale 1). */\nexport const GRAIN_TILE = 256;\n\n// ---------------------------------------------------------------------------\n// Height maps\n// ---------------------------------------------------------------------------\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/** One pattern's raw height (0..1) at pixel (x, y) of a `n`-pixel tile. */\nfunction patternHeights(pattern: GrainPattern, n: number, seed: number): Float32Array {\n const out = new Float32Array(n * n);\n const salt = Math.imul(seed | 0, 0x9e3779b1) + 17;\n if (pattern === 'smooth') return out.fill(1);\n const noise = (cx: number, cy: number, oct: number, s = 0) => new TileableNoise(n, cx, cy, oct, salt + s);\n const k = n / GRAIN_TILE;\n switch (pattern) {\n case 'cold-pressed': {\n const base = noise(20 * k, 20 * k, 4);\n const fine = noise(64 * k, 64 * k, 2, 3);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 1.8 + 0.5 + (fine.at(x, y) - 0.5) * 0.5 + (hash2(x, y, salt) - 0.5) * 0.12);\n break;\n }\n case 'hot-pressed': {\n const base = noise(40 * k, 40 * k, 3);\n for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 1.1 + 0.6 + (hash2(x, y, salt) - 0.5) * 0.08);\n break;\n }\n case 'rough': {\n const base = noise(10 * k, 10 * k, 5);\n const blot = noise(28 * k, 28 * k, 2, 5);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 2.4 + 0.5 + (blot.at(x, y) - 0.5) * 0.7 + (hash2(x, y, salt) - 0.5) * 0.15);\n break;\n }\n case 'vellum': {\n const base = noise(40 * k, 40 * k, 3);\n for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 1.6 + 0.55 + (hash2(x, y, salt) - 0.5) * 0.2);\n break;\n }\n case 'laid': {\n // Fine laid lines across, chain lines down every eighth of the tile, a little pulp noise.\n const pulp = noise(32 * k, 32 * k, 3);\n const chain = n / 8;\n for (let y = 0; y < n; y++) {\n const laid = 0.5 + 0.5 * Math.cos((2 * Math.PI * y) / 4);\n for (let x = 0; x < n; x++) {\n const c = Math.abs(((x + 0.5) % chain) - chain / 2) / (chain / 2);\n const chainLine = c > 0.94 ? -0.35 : 0;\n out[y * n + x] = clamp01(0.3 + 0.5 * laid + chainLine + (pulp.at(x, y) - 0.5) * 0.6);\n }\n }\n break;\n }\n case 'ingres': {\n // Long vertical fibres with grit.\n const fibres = noise(64 * k, 12 * k, 3);\n const base = noise(16 * k, 16 * k, 3, 7);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) {\n const g = hash2(x, y, salt ^ 0x55);\n out[y * n + x] = clamp01((fibres.at(x, y) - 0.5) * 1.5 + (base.at(x, y) - 0.5) * 0.7 + 0.55 + (g < 0.1 ? -0.3 : 0));\n }\n break;\n }\n case 'sanded': {\n // Fine grit: high-frequency noise and sharp single-pixel pits.\n const grit = noise(96 * k, 96 * k, 2);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) {\n const h = hash2(x, y, salt);\n out[y * n + x] = clamp01((grit.at(x, y) - 0.5) * 2.4 + 0.5 + (h - 0.5) * 0.7);\n }\n break;\n }\n case 'canvas':\n case 'linen': {\n // Over / under weave; linen has finer, irregular threads.\n const period = (pattern === 'canvas' ? 8 : 5) * k;\n // An even thread count keeps the over / under alternation seamless across the tile edge.\n const threads = Math.max(2, 2 * Math.round(n / period / 2));\n const p = n / threads;\n const slub = noise(threads, threads, 2, 11);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) {\n const fx = (x + 0.5) / p;\n const fy = (y + 0.5) / p;\n const cx = Math.floor(fx);\n const cy = Math.floor(fy);\n const warp = Math.sin(Math.PI * (fx - cx));\n const weft = Math.sin(Math.PI * (fy - cy));\n const top = (cx + cy) % 2 === 0 ? warp : weft;\n const irregular = pattern === 'linen' ? (slub.at(x, y) - 0.5) * 0.5 : 0;\n out[y * n + x] = clamp01(0.2 + 0.7 * top + irregular + (hash2(x, y, salt) - 0.5) * 0.15);\n }\n break;\n }\n case 'gesso': {\n // Brushed gesso: soft horizontal streaks over a fine tooth.\n const streaks = noise(2 * k, 28 * k, 3);\n const tooth = noise(64 * k, 64 * k, 2, 13);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) out[y * n + x] = clamp01((streaks.at(x, y) - 0.5) * 1.8 + (tooth.at(x, y) - 0.5) * 0.9 + 0.55);\n break;\n }\n }\n return out;\n}\n\nconst rawCache = new Map<string, Float32Array>();\n\n/** The raw height map of a pattern (`GRAIN_TILE` square, 0..1), generated once per pattern and seed. */\nexport function grainHeights(pattern: GrainPattern, seed: number, size = GRAIN_TILE): Float32Array {\n const key = `${pattern}:${seed}:${size}`;\n let h = rawCache.get(key);\n if (!h) {\n h = patternHeights(pattern, size, seed);\n if (rawCache.size > 32) rawCache.delete(rawCache.keys().next().value as string);\n rawCache.set(key, h);\n }\n return h;\n}\n\n/**\n * A paper's height field over the canvas (document pixels), tuned for one kind of media. Read it\n * with `at(x, y)`: bilinear, tiled by `paper.tiling`, at `paper.scale`.\n */\nexport class PaperGrain {\n readonly size = GRAIN_TILE;\n private readonly heights: Float32Array;\n private readonly scale: number;\n private readonly mirror: boolean;\n readonly flat: boolean;\n\n constructor(paper: PaperGrainSettings, media: 'dry' | 'wet' = 'dry') {\n const raw = grainHeights(paper.pattern, paper.seed);\n const t = paper[media];\n this.flat = paper.pattern === 'smooth';\n if (t.contrast === 1 && !t.invert) this.heights = raw;\n else {\n const out = new Float32Array(raw.length);\n for (let i = 0; i < raw.length; i++) {\n const v = clamp01((raw[i] - 0.5) * t.contrast + 0.5);\n out[i] = t.invert ? 1 - v : v;\n }\n this.heights = out;\n }\n this.scale = paper.scale;\n this.mirror = paper.tiling === 'mirror';\n }\n\n /** Tile cell index for a coordinate in grain pixels (mirrored on odd tiles when mirroring). */\n private wrap(v: number): number {\n const n = this.size;\n if (!this.mirror) return ((v % n) + n) % n;\n const p = ((v % (2 * n)) + 2 * n) % (2 * n);\n return p < n ? p : 2 * n - 1 - p;\n }\n\n /** Height 0..1 at document position (x, y). */\n at(x: number, y: number): number {\n const u = x / this.scale - 0.5;\n const v = y / this.scale - 0.5;\n const x0 = Math.floor(u);\n const y0 = Math.floor(v);\n const fx = u - x0;\n const fy = v - y0;\n const n = this.size;\n const h = this.heights;\n const a = this.wrap(x0);\n const b = this.wrap(x0 + 1);\n const c = this.wrap(y0) * n;\n const d = this.wrap(y0 + 1) * n;\n const top = h[c + a] + (h[c + b] - h[c + a]) * fx;\n const bot = h[d + a] + (h[d + b] - h[d + a]) * fx;\n return top + (bot - top) * fy;\n }\n}\n\n/**\n * How much of a dry dab lands at grain height `h` (0..1): the media catches the peaks first, and\n * pressure pushes it into the valleys. `depth` is the paper's grain depth times the brush's grain\n * response; at 0 everything lands (1).\n */\nexport function grainCatch(h: number, depth: number, pressure: number): number {\n if (depth <= 0) return 1;\n const t = depth * (0.75 - 0.65 * Math.min(1, Math.max(0, pressure)));\n const u = clamp01((h - t + 0.18) / 0.36);\n return 1 - depth + depth * u * u * (3 - 2 * u);\n}\n\n\n/**\n * How much of a finished standalone stroke stays at grain height `h` (0..1): `renderBrushstroke`\n * applies it once to the whole stroke's coverage. Unlike `grainCatch` (applied dab by dab while a\n * painter goes over the paper, and filling the valleys at full pressure), it keeps texture at full\n * pressure, which is all a mouse reports: the valleys lose up to `depth × (1.4 − 0.6 × pressure)`\n * of the paint, the peaks keep it all.\n */\nexport function strokeGrainCatch(h: number, depth: number, pressure: number): number {\n if (depth <= 0) return 1;\n const p = Math.min(1, Math.max(0, pressure));\n const amount = Math.min(1, depth * (1.4 - 0.6 * p));\n const u = clamp01((h - 0.2 - 0.25 * (1 - p)) / 0.5);\n return 1 - amount + amount * u * u * (3 - 2 * u);\n}\n","/**\n * A brush stroke along a path: polyline samples, each with an optional pressure and pen tilt, are\n * walked at the brush's spacing (pressure and tilt interpolated between samples) into dabs, built\n * up in a coverage map and composited onto a layer. This is what a vector editor calls to draw a\n * path with a natural-media brush (\"brushstrokes on vector paths\"): the path is the data, the\n * pixels are rendered from it.\n *\n * - Pressure scales size and / or opacity as the brush says.\n * - Tilt (`tiltX` / `tiltY` in degrees, or `altitude` / `azimuth` in radians, as Pointer Events give\n * them) widens the tip (`tilt.size`), flattens it along the lean (`tilt.roundness`), and with\n * `tilt.followAzimuth` turns the tip to the lean, as a pen laid on its side does.\n * - With `followPath` the tip turns with the path (a calligraphic nib along a curve).\n *\n * Deterministic and DOM-free: the same samples and brush give the same pixels everywhere.\n */\n\nimport { applyStrokeToTile, CoverageMap, dabRect, gradientDabColor, normalizeBrush, rasterizeDab, TIP_GRADIENT_STEPS, type BrushSettings, type Dab } from './brush';\nimport { hexToPaintRgb, PaintGradientSampler, resolvePaintGradient, type PaintGradientSample, type PaintSource } from './gradient-paint';\nimport { TiledSurface, finishTile, tileCoord, tileKey, tilesInRect, TILE, type RasterRect } from '../raster/surface';\nimport type { BlendMode } from '../raster/blend';\nimport { randSigned } from './rand';\nimport { strokeGrainCatch } from './grain';\n\n/** One sample of the path (document pixels). */\nexport interface PathSample {\n x: number;\n y: number;\n /** 0..1 (default 1). */\n pressure?: number;\n /** Pointer Events tilt in degrees (−90..90). */\n tiltX?: number;\n tiltY?: number;\n /** Radians above the surface (π/2 upright); overrides tiltX / tiltY. */\n altitude?: number;\n /** Radians clockwise from +x: where the pen leans (with altitude). */\n azimuth?: number;\n /** Milliseconds (gradient paint mapped by speed or time reads it). */\n t?: number;\n}\n\nexport interface PathStrokeOptions {\n brush: Partial<BrushSettings>;\n /** How tilt shapes the tip (each 0..1; default none). */\n tilt?: { size?: number; roundness?: number; followAzimuth?: boolean };\n /** Turn the tip with the direction of the path (added to the brush angle). */\n followPath?: boolean;\n /** Size at zero pressure as a fraction of the full size, when pressure controls size (default 0). */\n minSize?: number;\n /** Join the last sample back to the first. */\n closed?: boolean;\n /**\n * Per-dab jitter, as the paint editor's brush dynamics apply it: size, angle (fraction of ±180°),\n * opacity drop and scatter across the path (in diameters), each 0..1 (scatter 0..4). Hashed from\n * `seed` and the dab index, so the same stroke always jitters the same way. Default none.\n */\n jitter?: { seed: number; size?: number; angle?: number; opacity?: number; scatter?: number };\n /**\n * Paper grain the finished stroke catches (dry media; `strokeGrainCatch` at the stroke's mean\n * pressure): `at(x, y)` is the grain height 0..1 at a sample-space point, `depth` 0..1 how much\n * the valleys lose (paper depth × the brush's grain response). Default none.\n */\n grain?: { at(x: number, y: number): number; depth: number } | null;\n}\n\n/** Jitter streams (the paint editor's dab walker uses the same, so a preset jitters alike). */\nconst JITTER_SIZE = 1;\nconst JITTER_ANGLE = 2;\nconst JITTER_OPACITY = 3;\nconst JITTER_SCATTER = 4;\n\n/** A pen's lean from a sample: 0 upright .. 1 flat, and the direction it leans (radians, clockwise from +x). */\nfunction lean(s: PathSample): { amount: number; azimuth: number } | null {\n if (typeof s.altitude === 'number' && Number.isFinite(s.altitude)) {\n return { amount: 1 - Math.min(1, Math.max(0, s.altitude / (Math.PI / 2))), azimuth: s.azimuth ?? 0 };\n }\n if (typeof s.tiltX === 'number' || typeof s.tiltY === 'number') {\n const tx = ((s.tiltX ?? 0) * Math.PI) / 180;\n const ty = ((s.tiltY ?? 0) * Math.PI) / 180;\n if (tx === 0 && ty === 0) return { amount: 0, azimuth: 0 };\n const az = Math.atan2(Math.tan(ty), Math.tan(tx));\n const alt = Math.atan(1 / Math.hypot(Math.tan(tx), Math.tan(ty)));\n return { amount: 1 - alt / (Math.PI / 2), azimuth: az };\n }\n return null;\n}\n\n/** The dabs of a brush stroke along `samples`, with pressure and tilt interpolated between them. */\nexport function pathStrokeDabs(samples: readonly PathSample[], options: PathStrokeOptions): Dab[] {\n const brush = normalizeBrush(options.brush);\n const pts = options.closed && samples.length > 2 ? [...samples, samples[0]] : [...samples];\n if (!pts.length) return [];\n const tilt = options.tilt ?? {};\n const minSize = Math.min(1, Math.max(0, options.minSize ?? 0));\n let index = 0;\n const t0 = pts.find((s) => typeof s.t === 'number' && Number.isFinite(s.t))?.t ?? null;\n const dab = (x: number, y: number, p: number, l: { amount: number; azimuth: number } | null, dir: number, travel = 0, time: number | null = null, speed: number | null = null): Dab => {\n const pr = Math.min(1, Math.max(0, p));\n const lean0 = l?.amount ?? 0;\n const size = brush.size * (brush.pressureSize ? minSize + (1 - minSize) * pr : 1) * (1 + 1.5 * (tilt.size ?? 0) * lean0);\n let angle = (brush.angle * Math.PI) / 180;\n if (options.followPath) angle += dir;\n if (tilt.followAzimuth && l) angle = -l.azimuth;\n const roundness = Math.max(0.05, brush.roundness * (1 - 0.85 * (tilt.roundness ?? 0) * lean0));\n const op = brush.pressureOpacity ? pr : 1;\n const i = index++;\n const params = { travel, index: i, pressure: pr, lean: lean0, speed, direction: dir, time: time === null || t0 === null ? null : time - t0 };\n let diameter = size;\n let opacity = brush.opacity * op;\n const j = options.jitter;\n if (j) {\n if (j.size) diameter *= Math.max(0.05, 1 + j.size * randSigned(j.seed, i, JITTER_SIZE));\n if (j.angle) angle += j.angle * Math.PI * randSigned(j.seed, i, JITTER_ANGLE);\n if (j.opacity) opacity *= 1 - j.opacity * (randSigned(j.seed, i, JITTER_OPACITY) * 0.5 + 0.5);\n if (j.scatter) {\n // Across the path: the direction of travel turned a quarter (y points down).\n const off = j.scatter * diameter * randSigned(j.seed, i, JITTER_SCATTER);\n x += Math.sin(dir) * off;\n y += Math.cos(dir) * off;\n }\n }\n return { x, y, radius: Math.max(0.5, diameter / 2), alpha: brush.flow, opacity, angle, roundness, params };\n };\n const out: Dab[] = [];\n const pressure = (s: PathSample) => (typeof s.pressure === 'number' && Number.isFinite(s.pressure) ? s.pressure : 1);\n if (pts.length === 1) {\n out.push(dab(pts[0].x, pts[0].y, pressure(pts[0]), lean(pts[0]), 0));\n return out;\n }\n let carry = 0;\n let along = 0;\n const timeOf = (s: PathSample) => (typeof s.t === 'number' && Number.isFinite(s.t) ? s.t : null);\n out.push(dab(pts[0].x, pts[0].y, pressure(pts[0]), lean(pts[0]), Math.atan2(-(pts[1].y - pts[0].y), pts[1].x - pts[0].x), 0, timeOf(pts[0])));\n for (let i = 0; i + 1 < pts.length; i++) {\n const a = pts[i];\n const b = pts[i + 1];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy);\n if (len === 0) continue;\n // Dab angles are counter-clockwise with y up, as the shared rasterizer reads them.\n const dir = Math.atan2(-dy, dx);\n const la = lean(a);\n const lb = lean(b);\n const ta = timeOf(a);\n const tb = timeOf(b);\n const speed = ta !== null && tb !== null && tb > ta ? len / (tb - ta) : null;\n let t = carry;\n for (;;) {\n const f = t / len;\n if (f > 1) break;\n const p = pressure(a) + (pressure(b) - pressure(a)) * f;\n const l = la && lb ? { amount: la.amount + (lb.amount - la.amount) * f, azimuth: la.azimuth + (lb.azimuth - la.azimuth) * f } : (la ?? lb);\n const size = brush.size * (brush.pressureSize ? minSize + (1 - minSize) * p : 1);\n const step = Math.max(0.5, size * brush.spacing);\n if (t > 0 || i > 0) out.push(dab(a.x + dx * f, a.y + dy * f, p, l, dir, along + t, ta !== null && tb !== null ? ta + (tb - ta) * f : null, speed));\n t += step;\n }\n carry = t - len;\n along += len;\n }\n return out;\n}\n\n/**\n * Gradient paint for a path stroke: a `PaintSource` gradient sampled dab by dab (by distance along\n * the path, pressure, tilt, speed, direction, time, across the tip or at random). `paint` /\n * `secondary` stops resolve to `colors` (default: the stroke colour and white).\n */\nexport interface PathStrokePaint {\n paint: PaintSource;\n /** Seed of the `random` mapping (default 0). */\n seed?: number;\n colors?: { paint: string; secondary: string };\n}\n\n/** The coverage of a brush stroke along `samples` (build-up towards each dab's opacity) and its bounds. */\nexport function pathStrokeCoverage(samples: readonly PathSample[], options: PathStrokeOptions & { gradient?: PathStrokePaint | null }): { coverage: CoverageMap; bounds: RasterRect | null; dabs: number } {\n const brush = normalizeBrush(options.brush);\n const coverage = new CoverageMap();\n const dabs = pathStrokeDabs(samples, options);\n const g = options.gradient?.paint.type === 'gradient' ? options.gradient : null;\n const sampler = g ? new PaintGradientSampler(resolvePaintGradient(g.paint as Extract<PaintSource, { type: 'gradient' }>, g.colors ?? { paint: '#000000', secondary: '#ffffff' }), g.seed ?? 0) : null;\n const tipTable = sampler?.acrossTip ? sampler.tipTable(TIP_GRADIENT_STEPS) : null;\n const scratch: { field: Float32Array; sample: PaintGradientSample } = { field: new Float32Array(0), sample: { rgb: [0, 0, 0], alpha: 1 } };\n let buf = new Float32Array(0);\n let bounds: RasterRect | null = null;\n for (const d of dabs) {\n const rect = dabRect(d, brush.hardness);\n const n = rect.width * rect.height;\n if (n <= 0) continue;\n if (buf.length < n) buf = new Float32Array(n);\n const vals = buf.subarray(0, n);\n rasterizeDab(d, brush.hardness, rect, vals);\n if (sampler) {\n const { color, field } = gradientDabColor(sampler, tipTable, d, d.params!, rect, vals, scratch);\n coverage.accumulate(rect, vals, d.opacity, color, field);\n } else coverage.accumulate(rect, vals, d.opacity);\n if (!bounds) bounds = { ...rect };\n else {\n const x = Math.min(bounds.x, rect.x);\n const y = Math.min(bounds.y, rect.y);\n bounds = { x, y, width: Math.max(bounds.x + bounds.width, rect.x + rect.width) - x, height: Math.max(bounds.y + bounds.height, rect.y + rect.height) - y };\n }\n }\n const grain = options.grain;\n if (grain && grain.depth > 0 && dabs.length) {\n // Once over the finished stroke, so overlapping dabs cannot fill the valleys back in.\n const pressure = dabs.reduce((sum, d) => sum + (d.params?.pressure ?? 1), 0) / dabs.length;\n for (const [key, t] of coverage.tiles) {\n const { tx, ty } = tileCoord(key);\n for (let y = 0, k = 0; y < TILE; y++) {\n const gy = ty * TILE + y + 0.5;\n for (let x = 0; x < TILE; x++, k++) if (t[k] > 0) t[k] *= strokeGrainCatch(grain.at(tx * TILE + x + 0.5, gy), grain.depth, pressure);\n }\n }\n }\n return { coverage, bounds, dabs: dabs.length };\n}\n\nexport interface PaintPathOptions extends PathStrokeOptions {\n /** RGBA 0..255 of the paint (with gradient paint: its alpha, and the colour of `paint` stops). */\n color: readonly [number, number, number, number];\n /** Gradient paint (a `PaintSource`); a colour source replaces `color`'s RGB. */\n paint?: PathStrokePaint | null;\n /** `erase` removes paint instead (default `paint`). */\n mode?: 'paint' | 'erase';\n /** Blend mode (default the brush's). */\n blend?: BlendMode;\n /** Layer offset: layer pixel = document pixel − offset (default 0, 0). */\n offset?: { x: number; y: number };\n lockTransparency?: boolean;\n /** Where paint may land (canvas-sized coverage 0..255), or none. */\n selection?: { width: number; height: number; data: ArrayLike<number> } | null;\n}\n\n/**\n * Paints a brush stroke along `samples` onto an RGBA layer surface and returns the new surface\n * (the input is not changed; untouched tiles are shared) with the bounds it touched.\n */\nexport function paintPathStroke(surface: TiledSurface, samples: readonly PathSample[], options: PaintPathOptions): { surface: TiledSurface; bounds: RasterRect | null; dabs: number } {\n if (surface.channels !== 4) throw new Error('paintPathStroke paints RGBA surfaces (4 channels).');\n const brush = normalizeBrush(options.brush);\n const hex = (c: readonly number[]) => '#' + [0, 1, 2].map((i) => Math.round(Math.min(255, Math.max(0, c[i]))).toString(16).padStart(2, '0')).join('');\n let color = options.color;\n if (options.paint?.paint.type === 'color') {\n const rgb = hexToPaintRgb(options.paint.paint.color).map((v) => Math.round(v * 255));\n color = [rgb[0], rgb[1], rgb[2], color[3]];\n }\n const gradient = options.paint?.paint.type === 'gradient' ? { ...options.paint, colors: options.paint.colors ?? { paint: hex(color), secondary: '#ffffff' } } : null;\n const { coverage, bounds, dabs } = pathStrokeCoverage(samples, { ...options, gradient });\n if (!bounds) return { surface, bounds: null, dabs };\n const offset = options.offset ?? { x: 0, y: 0 };\n const apply = {\n mode: options.mode ?? 'paint',\n color,\n background: [0, 0, 0, 0] as const,\n opacity: brush.opacity,\n blend: options.blend ?? brush.blend,\n lockTransparency: options.lockTransparency ?? false,\n selection: options.selection ?? null,\n offset,\n };\n const tiles = new Map(surface.tiles);\n for (const { tx, ty } of tilesInRect({ x: bounds.x - offset.x, y: bounds.y - offset.y, width: bounds.width, height: bounds.height })) {\n const key = tileKey(tx, ty);\n const src = surface.tiles.get(key)?.data;\n const base = src && src.length === TILE * TILE * 4 ? src : new Uint8ClampedArray(TILE * TILE * 4);\n const t = finishTile(applyStrokeToTile(coverage, base, 4, tx, ty, apply), 4, surface.defaultValue);\n if (t) tiles.set(key, t);\n else tiles.delete(key);\n }\n return { surface: new TiledSurface(4, tiles, surface.defaultValue), bounds, dabs };\n}\n","/**\n * A brushstroke rendered on its own: a path (polyline samples with pressure, tilt and time, or\n * points plus pressure / tilt profiles along the length) and a dry brush in, an RGBA image and its\n * bounds out. The canvas renders brushed freehand strokes with it, and the vector editor\n * brushstrokes on vector paths; both keep the path as the data and the pixels as a cache.\n *\n * - Built on {@link paintPathStroke}: the same dabs, coverage and build-up as everywhere else.\n * - Rendered at `scale` image pixels per path unit (both the samples and the brush size scale), so\n * a stroke looks the same at any zoom with more or fewer pixels. Snap zooms to\n * {@link brushstrokeScaleBucket} so panning and small zoom changes reuse the same image.\n * - Dry media only. A wet brush needs an area to flow in (the paint editor's wet layer); mixer,\n * blender and smudge brushes need paint under them. Those are refused with an error.\n * - Deterministic and DOM-free: the image is plain RGBA bytes. Turn it into a PNG data URL with\n * {@link brushstrokeDataUrl}, or into a canvas through an injected factory\n * ({@link brushstrokeToCanvas}).\n */\n\nimport { normalizeBrush, type BrushSettings } from './brush';\nimport { paintPathStroke, type PathSample, type PathStrokeOptions } from './path-stroke';\nimport { scalePaintGradient, type PaintSource } from './gradient-paint';\nimport { PaperGrain, type GrainPattern } from './grain';\nimport { emptySurface, readRegion, type RasterRect } from '../raster/surface';\nimport { pngDataUrl } from '../raster/png';\nimport { BRUSHSTROKE_MAX_SCALE as MAX_SCALE, BRUSHSTROKE_MIN_SCALE as MIN_SCALE } from './brushstroke-scale';\n\nexport { brushstrokeScaleBucket, BRUSHSTROKE_MAX_SCALE, BRUSHSTROKE_MIN_SCALE } from './brushstroke-scale';\n\n/** One sample of a brushstroke: a {@link PathSample} plus an optional time stamp (milliseconds). */\nexport interface BrushstrokeSample extends PathSample {\n t?: number;\n}\n\n/**\n * A path plus pen profiles along it. Each profile is a list of values spread evenly over the\n * path's length (first at the start, last at the end) and interpolated in between; one value is\n * constant. Missing profiles leave the samples upright at full pressure.\n */\nexport interface BrushstrokeProfilePath {\n points: ReadonlyArray<{ x: number; y: number }>;\n /** Pressure 0..1 along the length. */\n pressure?: readonly number[];\n /** Pointer Events tilt in degrees (−90..90) along the length. */\n tiltX?: readonly number[];\n tiltY?: readonly number[];\n}\n\nexport type BrushstrokeInput = readonly BrushstrokeSample[] | BrushstrokeProfilePath;\n\n/**\n * A brush for a brushstroke: plain tip settings, or a paint-editor brush (`PaintBrush` matches this\n * shape structurally). Only dab-engine brushes render; `rotation: 'stroke'` turns the tip with the\n * path, and `dynamics` give the size at zero pressure and how tilt shapes the tip. With the\n * `character` option the brush's `grain` response and its size, angle, opacity and scatter jitter\n * apply too (seeded, so still deterministic). Velocity dynamics are never applied.\n */\nexport type BrushstrokeBrush =\n | Partial<BrushSettings>\n | {\n id?: string;\n name?: string;\n engine?: string;\n tip: Partial<BrushSettings>;\n rotation?: string;\n /** How strongly the mark follows the paper grain, 0..1 (with `character`). */\n grain?: number;\n dynamics?: { minSize?: number; tiltSize?: number; tiltRoundness?: number; sizeJitter?: number; angleJitter?: number; opacityJitter?: number; scatter?: number };\n };\n\n/** The paper a brushstroke's grain comes from (all optional). */\nexport interface BrushstrokePaper {\n /** Grain pattern (default `sanded`: a fine, even tooth). */\n pattern?: GrainPattern;\n /** Grain seed (default 1). */\n seed?: number;\n /** Grain scale in path units: 1 is the pattern's natural size (default 1). */\n scale?: number;\n /** Grain depth 0..1, multiplied by the brush's `grain` response (default 0.5). */\n depth?: number;\n}\n\n/**\n * A brush's character: its jitter and the paper grain it catches. Seeded, so a stroke always\n * renders the same bytes.\n */\nexport interface BrushstrokeCharacter {\n /** Jitter seed (default: a hash of the path, so each stroke differs but repeats exactly). */\n seed?: number;\n /** The paper (default: the built-in sanded grain); `null` for no grain. */\n paper?: BrushstrokePaper | null;\n /**\n * Where the path sits on the paper: paper point = `origin + path point × unit` (default origin\n * 0, 0 and unit 1). Pass the stroke's world position so neighbouring strokes share one sheet of\n * paper and the grain stays put on screen.\n */\n origin?: { x: number; y: number };\n unit?: number;\n}\n\n/**\n * What the stroke is painted with: a solid RGBA colour, or a `PaintSource` (one `#rrggbb` colour,\n * or gradient paint sampled dab by dab along the path: by distance, pressure, tilt, speed,\n * direction, time, across the tip or at random).\n */\nexport type BrushstrokePaint = { color: readonly [number, number, number, number] } | PaintSource;\n\nexport interface BrushstrokeOptions {\n /** Image pixels per path unit (default 1). Use {@link brushstrokeScaleBucket} for a zoom. */\n scale?: number;\n /** RGBA 0..255 (default opaque black). Shorthand for `paint: { color }`. */\n color?: readonly [number, number, number, number];\n /** What to paint with (wins over `color`; with gradient paint, `color` fills `paint` stops and gives the alpha). */\n paint?: BrushstrokePaint;\n /** Seed of gradient paint's `random` mapping (default 0). */\n seed?: number;\n /** Join the last sample back to the first. */\n closed?: boolean;\n /**\n * Render the brush's character: its seeded size / angle / opacity / scatter jitter and the paper\n * grain it catches (charcoal, pastel and pencil get their look from these). Default off: plain\n * dabs, as before.\n */\n character?: BrushstrokeCharacter;\n /**\n * The largest image to render, in pixels (default 16 777 216, 4096²). A stroke that would be\n * bigger at `scale` renders at a lower scale instead; the result's `scale` says which.\n */\n maxPixels?: number;\n}\n\n/** A rendered brushstroke. */\nexport interface BrushstrokeImage {\n /** Straight-alpha RGBA bytes, `width × height × 4` (the same layout as `ImageData`). */\n image: { width: number; height: number; data: Uint8ClampedArray };\n /** Where the image goes, in path units: draw `image` stretched over this rectangle. Null for an empty stroke. */\n bounds: { x: number; y: number; width: number; height: number } | null;\n /** Image pixels per path unit it was rendered at. */\n scale: number;\n /** How many dabs the stroke laid down. */\n dabs: number;\n}\n\nconst DEFAULT_MAX_PIXELS = 4096 * 4096;\n\nconst isProfilePath = (input: BrushstrokeInput): input is BrushstrokeProfilePath => !Array.isArray(input) && typeof input === 'object' && input !== null && 'points' in input;\n\n/** A profile's value at `f` (0..1 along the length), or undefined without one. */\nfunction profileAt(profile: readonly number[] | undefined, f: number): number | undefined {\n if (!profile || !profile.length) return undefined;\n if (profile.length === 1) return profile[0];\n const u = Math.min(1, Math.max(0, f)) * (profile.length - 1);\n const i = Math.min(profile.length - 2, Math.floor(u));\n return profile[i] + (profile[i + 1] - profile[i]) * (u - i);\n}\n\n/** The samples of a brushstroke input: profile paths are sampled at their points by arc length. */\nexport function brushstrokeSamples(input: BrushstrokeInput): BrushstrokeSample[] {\n if (!isProfilePath(input)) return input.map((s) => ({ ...s }));\n const pts = input.points;\n const lengths = [0];\n for (let i = 1; i < pts.length; i++) lengths.push(lengths[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y));\n const total = lengths.at(-1) ?? 0;\n return pts.map((p, i) => {\n const f = total > 0 ? lengths[i] / total : pts.length > 1 ? i / (pts.length - 1) : 0;\n const s: BrushstrokeSample = { x: p.x, y: p.y };\n const pressure = profileAt(input.pressure, f);\n const tiltX = profileAt(input.tiltX, f);\n const tiltY = profileAt(input.tiltY, f);\n if (pressure !== undefined) s.pressure = pressure;\n if (tiltX !== undefined) s.tiltX = tiltX;\n if (tiltY !== undefined) s.tiltY = tiltY;\n return s;\n });\n}\n\n/** Engines that cannot render a brushstroke on its own, and why. */\nconst REFUSED_ENGINES: Record<string, string> = {\n wet: 'is a wet brush, and wet paint needs an area to flow in (paint it on a wet layer in the paint editor)',\n mixer: 'is a Mixer Brush, which picks up paint from under the tip and has none on a standalone stroke',\n blender: 'is a blender, which only moves paint that is already there',\n smudge: 'is a smudge brush, which only moves paint that is already there',\n eraser: 'is an eraser, which has nothing to remove on a standalone stroke',\n};\n\n/**\n * The tip settings and path-stroke options of a brushstroke brush. Throws for brushes that are not\n * dry dab brushes (wet, mixer, blender, smudge, eraser, impasto and the procedural engines).\n */\nexport function brushstrokeStrokeOptions(brush: BrushstrokeBrush): PathStrokeOptions {\n if (!brush || typeof brush !== 'object') throw new Error('A brushstroke needs a brush: tip settings ({ size, hardness, … }) or a paint brush preset.');\n if (!('tip' in brush) || !brush.tip) return { brush: normalizeBrush(brush as Partial<BrushSettings>) };\n const engine = brush.engine ?? 'dab';\n if (engine !== 'dab') {\n const label = brush.name ?? brush.id ?? 'This brush';\n const why = REFUSED_ENGINES[engine] ?? `uses the '${engine}' engine, which renderBrushstroke does not draw`;\n throw new Error(`Brushstrokes render dry media only: '${label}' ${why}. Pick a dry brush (a dab-engine preset such as a pencil, pastel, charcoal, marker, ink or airbrush) or plain tip settings.`);\n }\n const d = brush.dynamics ?? {};\n const tilt = { size: d.tiltSize ?? 0, roundness: d.tiltRoundness ?? 0 };\n return {\n brush: normalizeBrush(brush.tip),\n followPath: brush.rotation === 'stroke',\n minSize: d.minSize ?? 0,\n ...(tilt.size || tilt.roundness ? { tilt } : {}),\n };\n}\n\n/** Whether a brush renders as a brushstroke (a dry dab brush, or plain tip settings). */\nexport function isBrushstrokeBrush(brush: BrushstrokeBrush): boolean {\n return !('tip' in brush) || !brush.tip || (brush.engine ?? 'dab') === 'dab';\n}\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\n\n/** The jitter and grain settings of a brush (zero for plain tip settings). */\nfunction brushCharacterOf(brush: BrushstrokeBrush): { grain: number; size: number; angle: number; opacity: number; scatter: number } {\n if (!('tip' in brush) || !brush.tip) return { grain: 0, size: 0, angle: 0, opacity: 0, scatter: 0 };\n const d = brush.dynamics ?? {};\n const u = (v: unknown, hi = 1) => (finite(v) ? Math.min(hi, Math.max(0, v)) : 0);\n return { grain: u(brush.grain), size: u(d.sizeJitter), angle: u(d.angleJitter), opacity: u(d.opacityJitter), scatter: u(d.scatter, 4) };\n}\n\nconst papers = new Map<string, PaperGrain>();\n\n/** The grain of a paper (built once per pattern, seed and scale). */\nfunction paperGrain(paper: BrushstrokePaper): PaperGrain {\n const pattern = paper.pattern ?? 'sanded';\n const seed = finite(paper.seed) ? Math.round(Math.abs(paper.seed)) : 1;\n const scale = finite(paper.scale) && paper.scale > 0 ? Math.min(8, Math.max(0.25, paper.scale)) : 1;\n const key = `${pattern}:${seed}:${scale}`;\n let g = papers.get(key);\n if (!g) {\n g = new PaperGrain({ pattern, seed, scale, tiling: 'mirror', dry: { contrast: 1, invert: false }, wet: { contrast: 1, invert: false } });\n if (papers.size > 16) papers.delete(papers.keys().next().value as string);\n papers.set(key, g);\n }\n return g;\n}\n\n/**\n * Renders a dry brushstroke along `input` with `brush` to an RGBA image. The same input, brush and\n * options always give the same bytes. Throws for a wet (or other non-dry) brush and for samples\n * that are not finite.\n */\nexport function renderBrushstroke(input: BrushstrokeInput, brush: BrushstrokeBrush, options: BrushstrokeOptions = {}): BrushstrokeImage {\n const strokeOptions = brushstrokeStrokeOptions(brush);\n const samples = brushstrokeSamples(input);\n for (const s of samples) if (!finite(s.x) || !finite(s.y)) throw new Error('Brushstroke samples need finite x and y.');\n const solid = options.paint && !('type' in options.paint) ? options.paint.color : null;\n const source = options.paint && 'type' in options.paint ? options.paint : null;\n const color = solid ?? options.color ?? [0, 0, 0, 255];\n let scale = finite(options.scale) && options.scale > 0 ? Math.min(MAX_SCALE, Math.max(MIN_SCALE, options.scale)) : 1;\n if (!samples.length) return { image: { width: 0, height: 0, data: new Uint8ClampedArray(0) }, bounds: null, scale, dabs: 0 };\n\n // Keep the image under maxPixels: estimate it from the samples and the widest the tip can get.\n const tip = strokeOptions.brush as BrushSettings;\n const reach = tip.size * (1 + 1.5 * (strokeOptions.tilt?.size ?? 0)) + 4;\n let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n for (const s of samples) {\n x0 = Math.min(x0, s.x); y0 = Math.min(y0, s.y); x1 = Math.max(x1, s.x); y1 = Math.max(y1, s.y);\n }\n const maxPixels = finite(options.maxPixels) && options.maxPixels > 0 ? options.maxPixels : DEFAULT_MAX_PIXELS;\n const area = (x1 - x0 + reach) * (y1 - y0 + reach);\n if (area * scale * scale > maxPixels) scale = Math.max(MIN_SCALE, Math.sqrt(maxPixels / area));\n\n const scaled = samples.map((s) => ({ ...s, x: s.x * scale, y: s.y * scale }));\n // Character: seeded jitter and paper grain, sampled where the path lies on the paper.\n let character: Pick<PathStrokeOptions, 'jitter' | 'grain'> = {};\n if (options.character) {\n const c = brushCharacterOf(brush);\n const ch = options.character;\n const seed = finite(ch.seed) ? ch.seed | 0 : hashString(JSON.stringify(input));\n const jitter = c.size || c.angle || c.opacity || c.scatter ? { seed, size: c.size, angle: c.angle, opacity: c.opacity, scatter: c.scatter } : undefined;\n const paper = ch.paper === null ? null : (ch.paper ?? {});\n const depth = paper ? Math.min(1, Math.max(0, (finite(paper.depth) ? paper.depth : 0.5) * c.grain)) : 0;\n let grain: PathStrokeOptions['grain'];\n if (paper && depth > 0) {\n const g = paperGrain(paper);\n const ox = ch.origin?.x ?? 0;\n const oy = ch.origin?.y ?? 0;\n const k = (finite(ch.unit) && ch.unit > 0 ? ch.unit : 1) / scale;\n grain = { depth, at: (x, y) => g.at(ox + x * k, oy + y * k) };\n }\n character = { ...(jitter ? { jitter } : {}), ...(grain ? { grain } : {}) };\n }\n const { surface, bounds, dabs } = paintPathStroke(emptySurface(4), scaled, {\n ...strokeOptions,\n ...character,\n brush: { ...tip, size: tip.size * scale },\n closed: options.closed ?? strokeOptions.closed,\n color,\n // Gradient lengths are in path units: scale them with the image.\n ...(source ? { paint: { paint: source.type === 'gradient' ? scalePaintGradient(source, scale) : source, seed: options.seed ?? 0 } } : {}),\n });\n if (!bounds) return { image: { width: 0, height: 0, data: new Uint8ClampedArray(0) }, bounds: null, scale, dabs };\n const rect: RasterRect = bounds;\n const data = readRegion(surface, rect);\n return {\n image: { width: rect.width, height: rect.height, data },\n bounds: { x: rect.x / scale, y: rect.y / scale, width: rect.width / scale, height: rect.height / scale },\n scale,\n dabs,\n };\n}\n\n/** A rendered brushstroke as a PNG data URL (for an SVG `<image>`, an `<img>` or an export). Empty strokes give ''. */\nexport function brushstrokeDataUrl(stroke: BrushstrokeImage): string {\n const { width, height, data } = stroke.image;\n return width && height ? pngDataUrl(width, height, data) : '';\n}\n\n/** The 2D-canvas surface {@link brushstrokeToCanvas} draws on (an `HTMLCanvasElement` or `OffscreenCanvas`). */\nexport interface BrushstrokeCanvasLike {\n getContext(type: '2d'): { createImageData(w: number, h: number): { data: Uint8ClampedArray }; putImageData(img: never, x: number, y: number): void } | null;\n}\n\n/**\n * A rendered brushstroke on a canvas made by `createCanvas(width, height)` (injected, so this\n * module stays DOM-free). Returns null for an empty stroke or a canvas without a 2D context.\n */\nexport function brushstrokeToCanvas<C extends BrushstrokeCanvasLike>(stroke: BrushstrokeImage, createCanvas: (width: number, height: number) => C): C | null {\n const { width, height, data } = stroke.image;\n if (!width || !height) return null;\n const canvas = createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n const img = ctx.createImageData(width, height);\n img.data.set(data);\n ctx.putImageData(img as never, 0, 0);\n return canvas;\n}\n\n// ---------------------------------------------------------------------------\n// Cache\n// ---------------------------------------------------------------------------\n\n/** FNV-1a over a string, as 8 hex digits. */\nfunction hashString(s: string, h = 0x811c9dc5): number {\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h;\n}\n\n/**\n * A key for a brushstroke: the path (samples or profiles), the brush, the colour and the scale.\n * Two calls with equal arguments give equal keys; any change to them changes the key.\n */\nexport function brushstrokeKey(input: BrushstrokeInput, brush: BrushstrokeBrush, options: BrushstrokeOptions = {}): string {\n const path = JSON.stringify(input);\n const { brush: tip, ...rest } = brushstrokeStrokeOptions(brush);\n const character = options.character ? [options.character, brushCharacterOf(brush)] : null;\n const rest2 = JSON.stringify([tip, rest, options.paint ?? options.color ?? null, options.seed ?? 0, options.closed ?? false, options.maxPixels ?? null, ...(character ? [character] : [])]);\n const scale = finite(options.scale) ? options.scale : 1;\n return `${hashString(path).toString(16)}:${path.length}:${hashString(rest2).toString(16)}:${scale}`;\n}\n\n/**\n * A small least-recently-used cache of rendered brushstrokes keyed by {@link brushstrokeKey}: path,\n * brush, colour and scale. Pass a bucketed scale ({@link brushstrokeScaleBucket}) so zooming inside\n * a bucket hits the cache, and a changed path, brush or bucket re-renders.\n */\nexport class BrushstrokeCache {\n private readonly entries = new Map<string, BrushstrokeImage>();\n /** Renders that missed the cache (for tests and stats). */\n misses = 0;\n\n constructor(readonly capacity = 64) {}\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The cached stroke for these arguments, rendering (and caching) it on a miss. */\n render(input: BrushstrokeInput, brush: BrushstrokeBrush, options: BrushstrokeOptions = {}): BrushstrokeImage {\n const key = brushstrokeKey(input, brush, options);\n const hit = this.entries.get(key);\n if (hit) {\n this.entries.delete(key);\n this.entries.set(key, hit);\n return hit;\n }\n this.misses++;\n const out = renderBrushstroke(input, brush, options);\n this.entries.set(key, out);\n while (this.entries.size > Math.max(1, this.capacity)) this.entries.delete(this.entries.keys().next().value as string);\n return out;\n }\n\n clear(): void {\n this.entries.clear();\n }\n}\n","// Portions ported from photocraft/crates/paint/src/mixer.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The pick-up / mix exchange between a loaded brush and the paint under it: the one mechanism\n * every wet-capable brush shares. The Mixer Brush (`MixerStroke`) and the paint editor's\n * wet-capable brushes run on it now; the wet layer (phase 3) plugs in as another `PaintMedium`.\n *\n * Two halves:\n *\n * - `PaintExchange` is what the brush carries: a **reservoir** of loaded paint (with an amount\n * that drains per dab) and a **well** of paint picked up from the canvas. Per dab it\n * 1. picks up: the well moves towards the colour under the dab by the pick-up rate times how\n * much of the footprint holds paint (`pickUp`);\n * 2. mixes the dab's paint: reservoir weight `amount × (1 − mix)`, well weight `mix`\n * (`paint`);\n * 3. drains the reservoir (`drain`).\n * Every mix is RGB or Kubelka-Munk pigment (`mixing`), so a blue brush through yellow goes\n * green.\n * - `PaintMedium` is what the brush touches: `sample` (the colour and contact under a footprint)\n * and `deposit` (lay paint at per-pixel amounts). `TileMedium` is the raster layer's pixels.\n * A wet layer implements the same two calls over its wet pigment and water (sampling only wet\n * paint, so pick-up lifts what is still wet), and nothing in `PaintExchange` changes.\n *\n * The ratios a user sets are `pickup` (pick-up ratio, 0..1) and `mix` (mix ratio, 0..1);\n * `exchangeRates` turns them into per-dab rates with the Mixer Brush's constants, so a Mixer\n * Brush's `wet` and `mix` are exactly these two ratios. Everything is data in, pixels out.\n */\n\nimport { blendPixel, mixPigment8, type BlendMode } from '../raster/blend';\nimport type { RasterRect } from '../raster/surface';\nimport { TILE } from '../raster/surface';\nimport { createPigment, lerpPigmentInto, setPigmentColor, type Pigment } from './pigment';\n\n/** How colours mix: straight sRGB averages, or like paint (Kubelka-Munk). */\nexport type MixingModel = 'rgb' | 'pigment';\n\nexport const MIXING_MODELS: readonly MixingModel[] = ['rgb', 'pigment'];\n\n/** An sRGB colour, 0..1 per channel. */\nexport type RgbTriple = [number, number, number];\n\n/** What a brush carries between dabs and strokes. Colours are straight sRGB, 0..1. */\nexport interface MixerLoad {\n /** Loaded paint, or `null` when the brush is unloaded. */\n readonly reservoir: readonly [number, number, number] | null;\n /** Reservoir left, 0..1. */\n readonly amount: number;\n /** Colour picked up from the canvas (the well), or `null` when the brush is clean. */\n readonly pickup: readonly [number, number, number] | null;\n}\n\nexport const EMPTY_MIXER_LOAD: MixerLoad = { reservoir: null, amount: 0, pickup: null };\n\n/** Fraction of the way the well moves towards the canvas colour per dab at a pick-up ratio of 1. */\nexport const MIXER_PICKUP_RATE = 0.25;\n/** Fraction of the reservoir that drains per dab at a load of 0. */\nexport const MIXER_DRAIN = 0.08;\n\n/** Per-dab rates of an exchange. */\nexport interface ExchangeRates {\n /** How far the well moves towards the canvas colour per dab at full contact (0: lifts nothing). */\n pickup: number;\n /** The well's weight in each dab's paint; the reservoir gets `amount × (1 − mix)`. */\n mix: number;\n /** Fraction of the reservoir lost per dab. */\n drain: number;\n mixing: MixingModel;\n}\n\n/**\n * Per-dab rates from the ratios a user sets: pick-up ratio `pickup` (0 dry .. 1), mix ratio\n * `mix` (0 .. 1, scaled by the pick-up ratio: a brush that lifts nothing has nothing to mix) and\n * `load` (1 never drains). The Mixer Brush passes its `wet`, `mix` and `load`.\n */\nexport function exchangeRates(pickup: number, mix: number, load: number, mixing: MixingModel): ExchangeRates {\n return { pickup: MIXER_PICKUP_RATE * pickup, mix: mix * pickup, drain: MIXER_DRAIN * (1 - load), mixing };\n}\n\n/** What a footprint holds: its average paint colour and how much of it holds paint (0..1). */\nexport interface PaintSample {\n rgb: RgbTriple;\n /** Paint-weighted share of the footprint (alpha × coverage over coverage). */\n contact: number;\n}\n\n/** The brush side of the exchange: reservoir, amount and well, mixed per dab. */\nexport class PaintExchange {\n private reservoir: RgbTriple | null;\n private amount: number;\n private well: RgbTriple | null;\n private readonly pig: { res: Pigment; well: Pigment; paint: Pigment; canvas: Pigment } | null;\n private readonly out: RgbTriple = [0, 0, 0];\n\n constructor(\n readonly rates: ExchangeRates,\n load: MixerLoad\n ) {\n this.reservoir = load.reservoir ? [load.reservoir[0], load.reservoir[1], load.reservoir[2]] : null;\n this.amount = this.reservoir ? Math.min(1, Math.max(0, load.amount)) : 0;\n this.well = load.pickup ? [load.pickup[0], load.pickup[1], load.pickup[2]] : null;\n this.pig =\n rates.mixing === 'pigment'\n ? { res: createPigment(this.reservoir ?? [0, 0, 0]), well: createPigment(this.well ?? [0, 0, 0]), paint: createPigment([0, 0, 0]), canvas: createPigment([0, 0, 0]) }\n : null;\n }\n\n /** What the brush carries now. */\n get load(): MixerLoad {\n return { reservoir: this.reservoir ? [...this.reservoir] : null, amount: this.amount, pickup: this.well ? [...this.well] : null };\n }\n\n /** The colour picked up so far, or `null`. */\n get pickup(): Readonly<RgbTriple> | null {\n return this.well;\n }\n\n /** The prepared pigment of the last `paint` colour (pigment mixing only), for `deposit`. */\n get paintPigment(): Pigment | null {\n return this.pig?.paint ?? null;\n }\n\n /** Recolours the reservoir without refilling it (a multi-coloured brush, per dab). */\n setReservoirColor(r: number, g: number, b: number): void {\n if (!this.reservoir) {\n this.reservoir = [r, g, b];\n this.amount = 1;\n } else {\n this.reservoir[0] = r;\n this.reservoir[1] = g;\n this.reservoir[2] = b;\n }\n if (this.pig) setPigmentColor(this.pig.res, r, g, b);\n }\n\n /** `a` mixed `t` of the way to `b` into `this.out`, by the mixing model. */\n private mix(a: RgbTriple, pa: Pigment | null, b: RgbTriple, pb: Pigment | null, t: number): RgbTriple {\n const o = this.out;\n if (pa && pb) lerpPigmentInto(pa, pb, t, o, 0);\n else for (let c = 0; c < 3; c++) o[c] = a[c] + (b[c] - a[c]) * t;\n return o;\n }\n\n /** `a` mixed `t` of the way to `b` (sRGB 0..1) by the mixing model, into `out`. */\n mixColors(a: readonly number[], b: readonly number[], t: number, out: number[]): number[] {\n if (this.pig) {\n setPigmentColor(this.pig.canvas, a[0], a[1], a[2]);\n setPigmentColor(this.pig.paint, b[0], b[1], b[2]);\n lerpPigmentInto(this.pig.canvas, this.pig.paint, t, out, 0);\n } else for (let c = 0; c < 3; c++) out[c] = a[c] + (b[c] - a[c]) * t;\n return out;\n }\n\n /** 1. Pick-up: the well moves towards the sampled colour (or takes it, when clean). */\n pickUp(sample: PaintSample | null): void {\n if (!sample || !(this.rates.pickup > 0)) return;\n const pig = this.pig;\n if (!this.well) this.well = [sample.rgb[0], sample.rgb[1], sample.rgb[2]];\n else {\n if (pig) setPigmentColor(pig.canvas, sample.rgb[0], sample.rgb[1], sample.rgb[2]);\n const o = this.mix(this.well, pig?.well ?? null, sample.rgb, pig?.canvas ?? null, this.rates.pickup * sample.contact);\n this.well = [o[0], o[1], o[2]];\n }\n if (pig) setPigmentColor(pig.well, this.well[0], this.well[1], this.well[2]);\n }\n\n /**\n * 2. The dab's paint: the reservoir mixed with the well, written to `out`. Returns the total\n * weight (≤ 0: nothing to lay down); deposit `min(1, weight)` of it. With pigment mixing,\n * `paintPigment` is the result's pigment afterwards.\n */\n paint(out: RgbTriple): number {\n const res = this.reservoir;\n const wr = res ? this.amount * (1 - this.rates.mix) : 0;\n const wp = this.well ? this.rates.mix : 0;\n const total = wr + wp;\n if (!(total > 1e-6)) return 0;\n const pig = this.pig;\n if (res && this.well) {\n const m = this.mix(res, pig?.res ?? null, this.well, pig?.well ?? null, wp / total);\n out[0] = m[0];\n out[1] = m[1];\n out[2] = m[2];\n } else {\n const c = res ?? this.well!;\n out[0] = c[0];\n out[1] = c[1];\n out[2] = c[2];\n }\n if (pig) setPigmentColor(pig.paint, out[0], out[1], out[2]);\n return total;\n }\n\n /** 3. The reservoir drains. */\n drain(): void {\n this.amount *= 1 - this.rates.drain;\n }\n}\n\n/**\n * A brush's working tile: 8-bit bytes, or (on a 16- or 32-bit document) floats on the same\n * 0..255 sRGB-encoded scale, which keep the fractions an 8-bit tile would round away.\n */\nexport type PaintPixels = Uint8ClampedArray | Float32Array;\n\n/** Writes `v` (0..1) into a working tile: rounded for bytes, kept exact (not below 0) for floats. */\nexport function put255(px: PaintPixels, i: number, v: number): void {\n if (px instanceof Float32Array) px[i] = v > 0 ? v * 255 : 0;\n else px[i] = Math.round(v * 255);\n}\n\n/** The pixels a brush reads and writes (RGBA tiles in layer space). */\nexport interface PaintTileTarget {\n /** The tile (TILE × TILE × 4) at (tx, ty) as it is now, or `null` when it is fully transparent. */\n readTile(tx: number, ty: number): PaintPixels | null;\n /** A writable tile at (tx, ty), returned by `readTile` from then on. */\n writeTile(tx: number, ty: number): PaintPixels;\n /** Layer offset: layer pixel = document pixel − offset. */\n offset: { x: number; y: number };\n /** Keep alpha and paint only where the layer has pixels. */\n lockTransparency: boolean;\n /** Selection coverage (document space, canvas-sized), or `null`. */\n selection: { width: number; height: number; data: ArrayLike<number> } | null;\n}\n\n/** The canvas side of the exchange: where a brush picks paint up and lays it down. */\nexport interface PaintMedium {\n /** The paint under a footprint (`cov` over `rect`, document space), or `null` where there is none. */\n sample(rect: RasterRect, cov: ArrayLike<number>): PaintSample | null;\n /**\n * Lays paint over the footprint at `dep × cov` per pixel. The colour is `paint` (with its\n * prepared pigment for pigment mixing), or per pixel from `field` (3 floats per pixel of\n * `rect`, sRGB 0..1) when given.\n */\n deposit(rect: RasterRect, cov: ArrayLike<number>, dep: number, paint: Readonly<RgbTriple>, paintPigment: Pigment | null, field?: Float32Array | null): void;\n}\n\nexport interface TileMediumOptions {\n /**\n * How deposits combine with the pixels. Missing: the exchange's mixing (source-over, or a\n * pigment mix with pigment mixing), as the Mixer Brush paints. Otherwise any blend mode, as a\n * brush tip's `blend`.\n */\n blend?: BlendMode;\n}\n\n/** A raster layer's pixels as a `PaintMedium` (through live tiles). */\nexport class TileMedium implements PaintMedium {\n // The tile last looked up by `locate` (and whether it came from `writeTile`).\n private tx = NaN;\n private ty = NaN;\n private tile: PaintPixels | null = null;\n private writable = false;\n private readonly canvasPig = createPigment([0, 0, 0]);\n private readonly paintPig = createPigment([0, 0, 0]);\n private paintKey = -1;\n private readonly cb = [0, 0, 0, 0];\n private readonly src = [0, 0, 0, 1];\n private readonly o: number[] = [0, 0, 0];\n\n constructor(\n readonly target: PaintTileTarget,\n private readonly options: TileMediumOptions = {}\n ) {}\n\n /** Forgets the cached tile (call when another writer may have replaced tiles). */\n reset(): void {\n this.tx = NaN;\n }\n\n /**\n * The tile holding document pixel (dx, dy) (`pixels`) and the pixel's byte index in it, or -1\n * when reading an empty tile.\n */\n locate(dx: number, dy: number, write: boolean): number {\n const t = this.target;\n const lx = dx - t.offset.x;\n const ly = dy - t.offset.y;\n const tx = Math.floor(lx / TILE);\n const ty = Math.floor(ly / TILE);\n if (tx !== this.tx || ty !== this.ty || (write && !this.writable)) {\n this.tx = tx;\n this.ty = ty;\n this.tile = write ? t.writeTile(tx, ty) : t.readTile(tx, ty);\n this.writable = write;\n }\n return this.tile ? ((ly - ty * TILE) * TILE + (lx - tx * TILE)) * 4 : -1;\n }\n\n /** The tile `locate` last found. */\n get pixels(): PaintPixels {\n return this.tile!;\n }\n\n /** Selection coverage at a document pixel, 0..1 (1 without a selection). */\n selectionAt(dx: number, dy: number): number {\n const sel = this.target.selection;\n if (!sel) return 1;\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) return 0;\n return sel.data[dy * sel.width + dx] / 255;\n }\n\n sample(rect: RasterRect, cov: ArrayLike<number>): PaintSample | null {\n const w = rect.width;\n this.tx = NaN;\n let ar = 0;\n let ag = 0;\n let ab = 0;\n let wsum = 0;\n let asum = 0;\n for (let yy = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const k = cov[yy * w + xx];\n if (k <= 0) continue;\n wsum += k;\n const i = this.locate(rect.x + xx, rect.y + yy, false);\n if (i < 0) continue;\n const px = this.tile!;\n const al = px[i + 3] / 255;\n if (al <= 0) continue;\n const ka = k * al;\n ar += (px[i] / 255) * ka;\n ag += (px[i + 1] / 255) * ka;\n ab += (px[i + 2] / 255) * ka;\n asum += ka;\n }\n }\n if (!(asum > 1e-6)) return null;\n return { rgb: [ar / asum, ag / asum, ab / asum], contact: asum / Math.max(1e-6, wsum) };\n }\n\n deposit(rect: RasterRect, cov: ArrayLike<number>, dep: number, paint: Readonly<RgbTriple>, paintPigment: Pigment | null, field: Float32Array | null = null): void {\n const t = this.target;\n const sel = t.selection;\n const w = rect.width;\n const o = this.o;\n const blend = this.options.blend;\n // Pigment where the exchange mixes pigment (no blend given) or the blend is `pigment`.\n const pigment = blend === undefined ? paintPigment !== null : blend === 'pigment';\n const general = blend !== undefined && blend !== 'normal' && blend !== 'pigment';\n // Pigment mode keeps the last canvas colour's spectrum: runs of equal pixels are common.\n let lastKey = -1;\n this.tx = NaN;\n for (let yy = 0; yy < rect.height; yy++) {\n const dy = rect.y + yy;\n for (let xx = 0; xx < w; xx++) {\n const p = yy * w + xx;\n let k = cov[p] * dep;\n if (k <= 0) continue;\n const dx = rect.x + xx;\n if (sel) {\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) continue;\n k *= sel.data[dy * sel.width + dx] / 255;\n if (k <= 0) continue;\n }\n if (k > 1) k = 1;\n // A locked layer keeps its transparent pixels (and leaves their tiles unwritten).\n if (t.lockTransparency) {\n const ri = this.locate(dx, dy, false);\n if (ri < 0 || this.tile![ri + 3] === 0) continue;\n }\n const i = this.locate(dx, dy, true);\n const px = this.tile!;\n const pr = field ? field[p * 3] : paint[0];\n const pg = field ? field[p * 3 + 1] : paint[1];\n const pb = field ? field[p * 3 + 2] : paint[2];\n if (general) {\n const cb = this.cb;\n for (let c = 0; c < 4; c++) cb[c] = px[i + c] / 255;\n const src = this.src;\n src[0] = pr;\n src[1] = pg;\n src[2] = pb;\n const r = blendPixel(blend!, cb, src, k);\n put255(px, i, r[0]);\n put255(px, i + 1, r[1]);\n put255(px, i + 2, r[2]);\n if (!t.lockTransparency) put255(px, i + 3, r[3]);\n continue;\n }\n const a = px[i + 3] / 255;\n const na = t.lockTransparency ? a : a + (1 - a) * k;\n // The new paint's share of the result (source-over); a locked layer mixes by `k` alone.\n const share = t.lockTransparency ? k : na > 0 ? k / na : 1;\n if (a <= 0 || share >= 1) {\n o[0] = pr;\n o[1] = pg;\n o[2] = pb;\n } else if (pigment && field) {\n mixPigment8(Math.round(px[i]), Math.round(px[i + 1]), Math.round(px[i + 2]), Math.round(pr * 255), Math.round(pg * 255), Math.round(pb * 255), share, o);\n } else if (pigment) {\n // Float tiles (16 / 32 bits) hold fractions the 8-bit key would merge: no cache there.\n const key = px instanceof Float32Array ? -2 : (px[i] << 16) | (px[i + 1] << 8) | px[i + 2];\n if (key !== lastKey || key === -2) {\n setPigmentColor(this.canvasPig, px[i] / 255, px[i + 1] / 255, px[i + 2] / 255);\n lastKey = key;\n }\n let pp = paintPigment;\n if (!pp) {\n // A pigment-blend tip over an RGB exchange: prepare the paint colour once per colour.\n const pkey = (Math.round(pr * 255) << 16) | (Math.round(pg * 255) << 8) | Math.round(pb * 255);\n if (pkey !== this.paintKey) {\n setPigmentColor(this.paintPig, pr, pg, pb);\n this.paintKey = pkey;\n }\n pp = this.paintPig;\n }\n lerpPigmentInto(this.canvasPig, pp, share, o, 0);\n } else {\n o[0] = px[i] / 255 + (pr - px[i] / 255) * share;\n o[1] = px[i + 1] / 255 + (pg - px[i + 1] / 255) * share;\n o[2] = px[i + 2] / 255 + (pb - px[i + 2] / 255) * share;\n }\n put255(px, i, o[0]);\n put255(px, i + 1, o[1]);\n put255(px, i + 2, o[2]);\n put255(px, i + 3, na);\n }\n }\n }\n}\n","/**\n * Blending tools on raster tiles, dab by dab (any dab walker can drive them):\n *\n * - **Blender** (`blenderDab`): every pixel under the tip moves towards the mix of the paint\n * within `distance` of it, so neighbouring colours run together. The mix is taken through the\n * pigment model: the neighbourhood's K/S spectra are averaged with Kubelka-Munk weights\n * (alpha × scattering), so blue next to yellow blends into green, not grey. `rgb` averages\n * straight colours instead.\n * - **Smudge** (`SmudgeTip`): a finger that carries a patch of paint. Each dab lays the patch it\n * carries over the pixels under the tip (`strength`) and picks up some of the paint that was\n * there (`1 − length`), so the paint's pattern is dragged along the stroke and fades with distance.\n * Optional pigment mixing for both steps.\n *\n * Both blend alpha too: blending next to bare canvas softens an edge, smudging drags paint out\n * into it. A transparency-locked layer keeps its alpha. Pure data in, pixels out.\n */\n\nimport { mixPigment8, pigment8 } from '../raster/blend';\nimport { PIGMENT_BANDS, lerpPigmentInto, type Pigment } from '../raster/pigment';\nimport type { RasterRect } from '../raster/surface';\nimport { put255, type MixingModel, type TileMedium } from './exchange';\n\n/** Blender options. */\nexport interface BlenderSettings {\n /** How far each pixel moves towards the neighbourhood mix per dab at full coverage, 0..1 (default 0.5). */\n strength: number;\n /** Blend distance: the neighbourhood's radius as a fraction of the tip diameter, 0..1 (default 0.35). */\n distance: number;\n /** `pigment` (default: Kubelka-Munk, blue next to yellow blends green) or `rgb`. */\n mixing: MixingModel;\n}\n\nexport const DEFAULT_BLENDER: Readonly<BlenderSettings> = { strength: 0.5, distance: 0.35, mixing: 'pigment' };\n\n/** Smudge options. */\nexport interface SmudgeSettings {\n /** How strongly the carried paint covers the pixels under the tip per dab, 0..1 (default 0.7). */\n strength: number;\n /** How far paint is dragged: the share of the carried patch kept per dab, 0..1 (default 0.85). */\n length: number;\n /** `rgb` (default) or `pigment` (smeared colours mix like paint). */\n mixing: MixingModel;\n}\n\nexport const DEFAULT_SMUDGE: Readonly<SmudgeSettings> = { strength: 0.7, length: 0.85, mixing: 'rgb' };\n\nconst unit = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);\n\nexport function normalizeBlender(raw: Partial<BlenderSettings> = {}, base: BlenderSettings = DEFAULT_BLENDER): BlenderSettings {\n return { strength: unit(raw.strength, base.strength), distance: unit(raw.distance, base.distance), mixing: raw.mixing === 'rgb' || raw.mixing === 'pigment' ? raw.mixing : base.mixing };\n}\n\nexport function normalizeSmudge(raw: Partial<SmudgeSettings> = {}, base: SmudgeSettings = DEFAULT_SMUDGE): SmudgeSettings {\n return { strength: unit(raw.strength, base.strength), length: unit(raw.length, base.length), mixing: raw.mixing === 'rgb' || raw.mixing === 'pigment' ? raw.mixing : base.mixing };\n}\n\n/** The neighbourhood radius in pixels of a blender dab of `radius`. */\nexport function blendRadius(settings: BlenderSettings, radius: number): number {\n return Math.max(1, Math.round(settings.distance * 2 * radius));\n}\n\n/** Reusable buffers for `blenderDab` (one per stroke). */\nexport interface BlenderScratch {\n sums: Float64Array;\n mix: Pigment;\n out: number[];\n}\n\nexport function createBlenderScratch(): BlenderScratch {\n return { sums: new Float64Array(0), mix: { rgb: [0, 0, 0], strength: 1, ks: new Float32Array(PIGMENT_BANDS), scatter: 1 }, out: [0, 0, 0] };\n}\n\nconst N = PIGMENT_BANDS;\n\n/**\n * One blender dab: `cov` is the tip's coverage over `rect` (document space, 0..1), `opacity` the\n * dab's (pressure) opacity, `radius` its radius in pixels. Reads the neighbourhood as it was\n * before the dab, then writes every covered pixel.\n */\nexport function blenderDab(medium: TileMedium, rect: RasterRect, cov: ArrayLike<number>, opacity: number, radius: number, settings: BlenderSettings, scratch: BlenderScratch): void {\n const R = blendRadius(settings, radius);\n const W = rect.width + 2 * R;\n const H = rect.height + 2 * R;\n const pigment = settings.mixing === 'pigment';\n // Channels per pixel: pigment: S·a·K/S per band, S·a, a, r·a, g·a, b·a; rgb: r·a, g·a, b·a, a.\n const C = pigment ? N + 2 : 4;\n const stride = (W + 1) * C;\n const size = stride * (H + 1);\n if (scratch.sums.length < size) scratch.sums = new Float64Array(size);\n const I = scratch.sums;\n I.fill(0, 0, size);\n medium.reset();\n // Summed-area table over the region (row 0 and column 0 stay zero).\n const row = new Float64Array(C);\n for (let y = 0; y < H; y++) {\n row.fill(0);\n const dy = rect.y - R + y;\n for (let x = 0; x < W; x++) {\n const dx = rect.x - R + x;\n const i = medium.locate(dx, dy, false);\n if (i >= 0) {\n const px = medium.pixels;\n const a = px[i + 3] / 255;\n if (a > 0) {\n if (pigment) {\n const p = pigment8(Math.round(px[i]), Math.round(px[i + 1]), Math.round(px[i + 2]));\n const sa = p.scatter * p.strength * a;\n for (let k = 0; k < N; k++) row[k] += sa * p.ks[k];\n row[N] += sa;\n row[N + 1] += a;\n } else {\n row[0] += (px[i] / 255) * a;\n row[1] += (px[i + 1] / 255) * a;\n row[2] += (px[i + 2] / 255) * a;\n row[3] += a;\n }\n }\n }\n const o = (y + 1) * stride + (x + 1) * C;\n const up = y * stride + (x + 1) * C;\n for (let c = 0; c < C; c++) I[o + c] = I[up + c] + row[c];\n }\n }\n const area = (2 * R + 1) * (2 * R + 1);\n const strength = settings.strength * opacity;\n const lock = medium.target.lockTransparency;\n const mixPig = scratch.mix;\n const out = scratch.out;\n const sum = (c: number, x1: number, y1: number, x2: number, y2: number) => I[y2 * stride + x2 * C + c] - I[y1 * stride + x2 * C + c] - I[y2 * stride + x1 * C + c] + I[y1 * stride + x1 * C + c];\n const w = rect.width;\n medium.reset();\n for (let yy = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const k = cov[yy * w + xx];\n if (k <= 0) continue;\n const dx = rect.x + xx;\n const dy = rect.y + yy;\n let t = strength * k * medium.selectionAt(dx, dy);\n if (t <= 0) continue;\n if (t > 1) t = 1;\n // The window [x − R, x + R] in region coordinates, as summed-area bounds.\n const x1 = xx;\n const y1 = yy;\n const x2 = xx + 2 * R + 1;\n const y2 = yy + 2 * R + 1;\n const asum = sum(pigment ? N + 1 : 3, x1, y1, x2, y2);\n const ri = medium.locate(dx, dy, false);\n const a = ri < 0 ? 0 : medium.pixels[ri + 3] / 255;\n if (lock && a <= 0) continue;\n const avgA = asum / area;\n const na = lock ? a : (1 - t) * a + t * avgA;\n if (asum <= 1e-9) {\n // Only bare canvas nearby: the paint thins out (edges soften).\n if (lock || a <= 0) continue;\n const i = medium.locate(dx, dy, true);\n put255(medium.pixels, i + 3, na);\n continue;\n }\n // The neighbourhood's share of the paint in the result.\n const share = lock ? t : na > 0 ? (t * avgA) / na : 1;\n const i = medium.locate(dx, dy, true);\n const px = medium.pixels;\n if (pigment) {\n const sa = sum(N, x1, y1, x2, y2);\n for (let c = 0; c < N; c++) mixPig.ks[c] = sum(c, x1, y1, x2, y2) / sa;\n mixPig.scatter = sa / asum;\n if (a <= 0 || share >= 1) lerpPigmentInto(mixPig, mixPig, 0, out, 0);\n else lerpPigmentInto(pigment8(Math.round(px[i]), Math.round(px[i + 1]), Math.round(px[i + 2])), mixPig, share, out, 0);\n } else {\n for (let c = 0; c < 3; c++) {\n const m = sum(c, x1, y1, x2, y2) / asum;\n out[c] = a <= 0 || share >= 1 ? m : px[i + c] / 255 + (m - px[i + c] / 255) * share;\n }\n }\n put255(px, i, out[0]);\n put255(px, i + 1, out[1]);\n put255(px, i + 2, out[2]);\n if (!lock) put255(px, i + 3, na);\n }\n }\n}\n\n/** A dab as the smudge needs it: centre, radius (px) and pressure opacity. */\nexport interface SmudgeDab {\n x: number;\n y: number;\n radius: number;\n opacity: number;\n}\n\n/**\n * A smudging finger: carries a patch of paint (straight RGBA, `size × size` cells over the tip,\n * in canvas orientation) from dab to dab. The first dab only picks the patch up.\n */\nexport class SmudgeTip {\n private readonly size: number;\n private readonly patch: Float32Array;\n private loaded = false;\n private readonly out = [0, 0, 0];\n\n constructor(\n readonly settings: SmudgeSettings,\n /** The largest tip diameter of the stroke, in pixels (the patch resolution). */\n diameter: number\n ) {\n this.size = Math.max(2, Math.min(512, Math.ceil(diameter)));\n this.patch = new Float32Array(this.size * this.size * 4);\n }\n\n private cell(d: SmudgeDab, px: number, py: number): number {\n const s = this.size;\n const r = Math.max(0.5, d.radius);\n const bx = Math.min(s - 1, Math.max(0, Math.floor((((px - d.x) / r + 1) / 2) * s)));\n const by = Math.min(s - 1, Math.max(0, Math.floor((((py - d.y) / r + 1) / 2) * s)));\n return (by * s + bx) * 4;\n }\n\n /** Fills the patch from the pixels under the dab. */\n private load(medium: TileMedium, d: SmudgeDab): void {\n const s = this.size;\n const r = Math.max(0.5, d.radius);\n medium.reset();\n for (let by = 0; by < s; by++) {\n for (let bx = 0; bx < s; bx++) {\n const dx = Math.floor(d.x + (((bx + 0.5) / s) * 2 - 1) * r);\n const dy = Math.floor(d.y + (((by + 0.5) / s) * 2 - 1) * r);\n const i = medium.locate(dx, dy, false);\n const c = (by * s + bx) * 4;\n if (i < 0) {\n this.patch.fill(0, c, c + 4);\n continue;\n }\n const px = medium.pixels;\n this.patch[c] = px[i] / 255;\n this.patch[c + 1] = px[i + 1] / 255;\n this.patch[c + 2] = px[i + 2] / 255;\n this.patch[c + 3] = px[i + 3] / 255;\n }\n }\n this.loaded = true;\n }\n\n /** `a` mixed `t` of the way to `b` (both 0..1) into `out`. */\n private mix(a0: number, a1: number, a2: number, b0: number, b1: number, b2: number, t: number): number[] {\n const o = this.out;\n if (this.settings.mixing === 'pigment') {\n mixPigment8(Math.round(a0 * 255), Math.round(a1 * 255), Math.round(a2 * 255), Math.round(b0 * 255), Math.round(b1 * 255), Math.round(b2 * 255), t, o);\n } else {\n o[0] = a0 + (b0 - a0) * t;\n o[1] = a1 + (b1 - a1) * t;\n o[2] = a2 + (b2 - a2) * t;\n }\n return o;\n }\n\n /** One dab: lays the carried patch over the tip's footprint (`cov` over `rect`), then picks up. */\n dab(medium: TileMedium, d: SmudgeDab, rect: RasterRect, cov: ArrayLike<number>): void {\n if (!this.loaded) {\n this.load(medium, d);\n return;\n }\n const { strength, length } = this.settings;\n const lock = medium.target.lockTransparency;\n const patch = this.patch;\n const w = rect.width;\n medium.reset();\n for (let yy = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const cv = cov[yy * w + xx];\n if (cv <= 0) continue;\n const dx = rect.x + xx;\n const dy = rect.y + yy;\n let k = strength * cv * d.opacity * medium.selectionAt(dx, dy);\n if (k <= 0) continue;\n if (k > 1) k = 1;\n const c = this.cell(d, dx + 0.5, dy + 0.5);\n const ba = patch[c + 3];\n const ri = medium.locate(dx, dy, false);\n const a = ri < 0 ? 0 : medium.pixels[ri + 3] / 255;\n if (lock && a <= 0) continue;\n // The pixel as it was: what the finger lays paint over, and what it picks up afterwards.\n const px0 = ri >= 0 ? medium.pixels : null;\n const o0 = px0 ? px0[ri] / 255 : 0;\n const o1 = px0 ? px0[ri + 1] / 255 : 0;\n const o2 = px0 ? px0[ri + 2] / 255 : 0;\n if (a > 0 || ba > 0) {\n const na = lock ? a : a + (ba - a) * k;\n const share = lock ? k * ba : na > 0 ? (k * ba) / na : 0;\n let r = o0;\n let g = o1;\n let b = o2;\n if (a <= 0 || share >= 1) {\n r = patch[c];\n g = patch[c + 1];\n b = patch[c + 2];\n } else if (share > 0) {\n const m = this.mix(o0, o1, o2, patch[c], patch[c + 1], patch[c + 2], share);\n r = m[0];\n g = m[1];\n b = m[2];\n }\n const i = medium.locate(dx, dy, true);\n const out = medium.pixels;\n put255(out, i, r);\n put255(out, i + 1, g);\n put255(out, i + 2, b);\n put255(out, i + 3, na);\n }\n // Pick up: the patch takes `1 − length` of the paint that was under it (alpha-weighted).\n const pick = (1 - length) * Math.min(1, cv * d.opacity);\n if (pick > 0) {\n const pa = ba + (a - ba) * pick;\n const s = pa > 0 ? (a * pick) / pa : 0;\n if (s > 0) {\n const m = ba <= 0 || s >= 1 ? [o0, o1, o2] : this.mix(patch[c], patch[c + 1], patch[c + 2], o0, o1, o2, s);\n patch[c] = m[0];\n patch[c + 1] = m[1];\n patch[c + 2] = m[2];\n }\n patch[c + 3] = pa;\n }\n }\n }\n }\n}\n","// Portions ported from photocraft/crates/paint/src/mixer.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The Mixer Brush: a reservoir of loaded paint plus a pickup well that takes up colour from the\n * canvas under each dab (photocraft's behavioural model):\n *\n * - Each dab samples the canvas under its footprint (coverage- and alpha-weighted). The pickup\n * colour `P` moves towards that sample by `PICKUP_RATE × wet` (scaled by the sample's coverage).\n * - The dab's paint mixes the reservoir `R` (weight `amount × (1 − mix·wet)`) with the pickup\n * (weight `mix·wet`) and lands at `flow × opacity` times the total weight, over the pixels as\n * they are now, so earlier dabs of the same stroke are picked up again (smearing).\n * - The reservoir drains per dab unless `load` is 1, so low-load strokes run dry. A dry brush\n * (`wet` 0) paints only reservoir colour; an empty, wet brush smears canvas colour.\n *\n * `mixing: 'pigment'` does all three mixes (pickup, paint and deposit) with the Kubelka-Munk model\n * of `./pigment`, so a blue brush dragged through yellow makes green instead of grey. Everything\n * is data in, pixels out: the same canvas, points, settings and load replay identically.\n */\n\nimport { DabWalker, dabRect, rasterizeDab, type BrushSettings, type Dab, type StrokePoint } from './brush';\nimport {\n EMPTY_MIXER_LOAD,\n exchangeRates,\n MIXER_DRAIN,\n MIXER_PICKUP_RATE,\n MIXING_MODELS,\n PaintExchange,\n TileMedium,\n type ExchangeRates,\n type MixerLoad,\n type MixingModel,\n type PaintTileTarget,\n type RgbTriple,\n} from './exchange';\nimport type { RasterRect as CropBox } from '../raster/surface';\n\n\nexport { EMPTY_MIXER_LOAD, MIXER_DRAIN, MIXER_PICKUP_RATE, MIXING_MODELS, type MixerLoad, type MixingModel };\n\n/** Mixer Brush options. */\nexport interface MixerSettings {\n /** How much colour the brush picks up from the canvas (the pick-up ratio), 0 (dry) .. 1 (default 0.5). */\n wet: number;\n /** How much paint the reservoir holds, 0 .. 1 (default 0.5): below 1 it drains per dab and the stroke runs dry. */\n load: number;\n /** How far each dab's paint moves from the reservoir towards the picked-up colour (the mix ratio), 0 .. 1 (default 0.5). */\n mix: number;\n /** Refill the reservoir with the paint colour at the start of each stroke (default true). */\n loadEachStroke: boolean;\n /** Empty the pickup well after each stroke (default true); off keeps the brush dirty for the next stroke. */\n cleanEachStroke: boolean;\n /** `rgb` (default) or `pigment` (blue and yellow make green). */\n mixing: MixingModel;\n}\n\nexport const DEFAULT_MIXER: Readonly<MixerSettings> = {\n wet: 0.5,\n load: 0.5,\n mix: 0.5,\n loadEachStroke: true,\n cleanEachStroke: true,\n mixing: 'rgb',\n};\n\n/** Mixer settings with every field in range (missing fields from `DEFAULT_MIXER`). */\nexport function normalizeMixer(raw: Partial<MixerSettings> = {}): MixerSettings {\n const unit = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);\n const bool = (v: unknown, d: boolean) => (typeof v === 'boolean' ? v : d);\n return {\n wet: unit(raw.wet, DEFAULT_MIXER.wet),\n load: unit(raw.load, DEFAULT_MIXER.load),\n mix: unit(raw.mix, DEFAULT_MIXER.mix),\n loadEachStroke: bool(raw.loadEachStroke, DEFAULT_MIXER.loadEachStroke),\n cleanEachStroke: bool(raw.cleanEachStroke, DEFAULT_MIXER.cleanEachStroke),\n mixing: raw.mixing === 'pigment' ? 'pigment' : 'rgb',\n };\n}\n\n/** The per-dab exchange rates of Mixer Brush settings (`wet` is the pick-up ratio, `mix` the mix ratio). */\nexport function mixerRates(settings: MixerSettings): ExchangeRates {\n return exchangeRates(settings.wet, settings.mix, settings.load, settings.mixing);\n}\n\n/** The brush loaded full with `color` (sRGB 0..1); the pickup well is kept. */\nexport function loadMixer(load: MixerLoad, color: readonly [number, number, number]): MixerLoad {\n const c = (v: number) => Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0));\n return { reservoir: [c(color[0]), c(color[1]), c(color[2])], amount: 1, pickup: load.pickup };\n}\n\n/** The brush with its pickup well emptied; the reservoir is kept. */\nexport function cleanMixer(load: MixerLoad): MixerLoad {\n return { reservoir: load.reservoir, amount: load.amount, pickup: null };\n}\n\n/** The pixels a mixer stroke reads and writes (RGBA tiles in layer space). */\nexport type MixerTarget = PaintTileTarget;\n\n/**\n * One Mixer Brush stroke. Feed points with `push` (any chunks give the same dabs), end with\n * `finish`; `load` is the brush load after the dabs so far. Each call returns the document\n * rectangles it touched. Runs on the shared pick-up / mix exchange (`./exchange`).\n */\nexport class MixerStroke {\n dabCount = 0;\n bounds: CropBox | null = null;\n private walker: DabWalker;\n private smoothPos: StrokePoint | null = null;\n private buf = new Float32Array(0);\n private readonly exchange: PaintExchange;\n private readonly medium: TileMedium;\n private readonly paintColor: RgbTriple = [0, 0, 0];\n\n constructor(\n readonly brush: BrushSettings,\n settings: Partial<MixerSettings>,\n load: MixerLoad,\n target: MixerTarget\n ) {\n this.walker = new DabWalker(brush);\n this.exchange = new PaintExchange(mixerRates(normalizeMixer(settings)), load);\n this.medium = new TileMedium(target);\n }\n\n /** The brush load after the dabs so far. */\n get load(): MixerLoad {\n return this.exchange.load;\n }\n\n push(points: readonly StrokePoint[]): CropBox[] {\n const dabs: Dab[] = [];\n for (const p of points) this.walker.push(this.smooth(p), dabs);\n return this.paint(dabs);\n }\n\n finish(last?: StrokePoint): CropBox[] {\n const dabs: Dab[] = [];\n if (last && this.smoothPos && (this.smoothPos.x !== last.x || this.smoothPos.y !== last.y)) this.walker.push(last, dabs);\n this.walker.finish(dabs);\n return this.paint(dabs);\n }\n\n private smooth(p: StrokePoint): StrokePoint {\n const a = 1 - Math.min(0.95, Math.max(0, this.brush.smoothing));\n const cur = this.smoothPos;\n const n = cur && a < 1 ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;\n this.smoothPos = n;\n return n;\n }\n\n private paint(dabs: Dab[]): CropBox[] {\n const rects: CropBox[] = [];\n for (const d of dabs) {\n const rect = dabRect(d, this.brush.hardness);\n const n = rect.width * rect.height;\n if (this.buf.length < n) this.buf = new Float32Array(n);\n const cov = this.buf.subarray(0, n);\n // Pure tip coverage: flow scales the deposit, not the sampling weights.\n rasterizeDab({ ...d, alpha: 1 }, this.brush.hardness, rect, cov);\n this.dab(d, rect, cov);\n rects.push(rect);\n this.bounds = union(this.bounds, rect);\n this.dabCount++;\n }\n return rects;\n }\n\n private dab(d: Dab, rect: CropBox, cov: Float32Array): void {\n const ex = this.exchange;\n // 1. Pick up from the canvas under the dab; 2. mix the dab's paint and lay it; 3. drain.\n ex.pickUp(this.medium.sample(rect, cov));\n const total = ex.paint(this.paintColor);\n if (total > 0) {\n const dep = Math.min(1, total) * this.brush.flow * this.brush.opacity * d.opacity;\n if (dep > 0) this.medium.deposit(rect, cov, dep, this.paintColor, ex.paintPigment);\n }\n ex.drain();\n }\n}\n\nfunction union(a: CropBox | null, b: CropBox): CropBox {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n"]}