@pixerate/editor 0.275.1 → 0.277.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 (81) hide show
  1. package/dist/animation-editor/index.cjs +62 -62
  2. package/dist/animation-editor/index.js +6 -6
  3. package/dist/brushes/index.cjs +84 -71
  4. package/dist/brushes/index.js +20 -7
  5. package/dist/brushes/patch.d.cts +4 -4
  6. package/dist/brushes/patch.d.ts +4 -4
  7. package/dist/brushes/profiles.d.cts +4 -0
  8. package/dist/brushes/profiles.d.ts +4 -0
  9. package/dist/brushes/schema.d.cts +2 -0
  10. package/dist/brushes/schema.d.ts +2 -0
  11. package/dist/brushes/types.d.cts +32 -1
  12. package/dist/brushes/types.d.ts +32 -1
  13. package/dist/{brushstrokeRender-2RQL46WB.cjs → brushstrokeRender-7HOMKZNO.cjs} +21 -21
  14. package/dist/{brushstrokeRender-WGY4DZHF.js → brushstrokeRender-EIK3625T.js} +5 -5
  15. package/dist/canvas/index.cjs +13 -13
  16. package/dist/canvas/index.js +1 -1
  17. package/dist/{chunk-T6KNJVBP.cjs → chunk-4NZTK7LM.cjs} +35 -35
  18. package/dist/{chunk-LMG57L5A.js → chunk-CTHWXW5M.js} +121 -8
  19. package/dist/{chunk-43XJC3PP.js → chunk-DWPFGWW5.js} +1 -1
  20. package/dist/{chunk-MSWLJI4G.js → chunk-FNIWHSLI.js} +2 -2
  21. package/dist/{chunk-JDXWLCXF.cjs → chunk-H3EME3S5.cjs} +2 -2
  22. package/dist/{chunk-Y2ISSVPH.js → chunk-HHX7ST6Z.js} +167 -9
  23. package/dist/{chunk-6QNDGMK7.cjs → chunk-HRUK5TFW.cjs} +187 -45
  24. package/dist/{chunk-NIBFKXMD.cjs → chunk-IFWLZZCW.cjs} +619 -2
  25. package/dist/{chunk-W6JDUXP3.js → chunk-J23JOHPR.js} +1 -1
  26. package/dist/{chunk-HVO6XCI4.js → chunk-JPB65KQ6.js} +1 -1
  27. package/dist/{chunk-Y354ETAE.cjs → chunk-JVFGK3DK.cjs} +69 -58
  28. package/dist/{chunk-ACTHSQOI.js → chunk-LDZOXESA.js} +618 -1
  29. package/dist/{chunk-IEEY5RJV.js → chunk-LZE2QZCT.js} +1 -1
  30. package/dist/{chunk-DHC4E2DU.cjs → chunk-MITQGHN3.cjs} +121 -8
  31. package/dist/{chunk-75SYKUOT.js → chunk-N4MCWQZ7.js} +1 -1
  32. package/dist/{chunk-G5TCMKX5.js → chunk-NJLPW4MM.js} +17 -6
  33. package/dist/{chunk-STLTOISW.cjs → chunk-NPPMKMCU.cjs} +17 -17
  34. package/dist/{chunk-OLZXXAER.cjs → chunk-PEQWARCP.cjs} +408 -250
  35. package/dist/{chunk-ZM7IKQ4X.js → chunk-PPDUCZZV.js} +152 -10
  36. package/dist/{chunk-RAPNRK4R.cjs → chunk-QBZEC2JJ.cjs} +88 -88
  37. package/dist/{chunk-RJGW3RQS.js → chunk-ROURHJYT.js} +1 -1
  38. package/dist/{chunk-62MFUHSI.cjs → chunk-UQXULAMQ.cjs} +16 -16
  39. package/dist/{chunk-YYZC2EQO.cjs → chunk-VNF7A2JA.cjs} +23 -23
  40. package/dist/{chunk-C6VB7CPY.cjs → chunk-WFSYZO5Z.cjs} +1 -1
  41. package/dist/image-editor/index.cjs +880 -852
  42. package/dist/image-editor/index.js +36 -8
  43. package/dist/paint/index.cjs +33 -5
  44. package/dist/paint/index.d.cts +1 -0
  45. package/dist/paint/index.d.ts +1 -0
  46. package/dist/paint/index.js +32 -4
  47. package/dist/paint/live-patterns.d.cts +149 -0
  48. package/dist/paint/live-patterns.d.ts +149 -0
  49. package/dist/paint/wet/world.d.cts +13 -0
  50. package/dist/paint/wet/world.d.ts +13 -0
  51. package/dist/paint-editor/brush-model.d.cts +21 -1
  52. package/dist/paint-editor/brush-model.d.ts +21 -1
  53. package/dist/paint-editor/controller.d.cts +102 -1
  54. package/dist/paint-editor/controller.d.ts +102 -1
  55. package/dist/paint-editor/document.d.cts +8 -1
  56. package/dist/paint-editor/document.d.ts +8 -1
  57. package/dist/paint-editor/ferro-tools.d.cts +10 -0
  58. package/dist/paint-editor/ferro-tools.d.ts +10 -0
  59. package/dist/paint-editor/ferro.d.cts +134 -0
  60. package/dist/paint-editor/ferro.d.ts +134 -0
  61. package/dist/paint-editor/index.cjs +2291 -904
  62. package/dist/paint-editor/index.d.cts +2 -0
  63. package/dist/paint-editor/index.d.ts +2 -0
  64. package/dist/paint-editor/index.js +1533 -146
  65. package/dist/paint-editor/stroke.d.cts +14 -0
  66. package/dist/paint-editor/stroke.d.ts +14 -0
  67. package/dist/paint-editor/thick.d.cts +15 -2
  68. package/dist/paint-editor/thick.d.ts +15 -2
  69. package/dist/paint-editor/ui.d.cts +7 -1
  70. package/dist/paint-editor/ui.d.ts +7 -1
  71. package/dist/paint-editor/wet-tools.d.cts +42 -0
  72. package/dist/paint-editor/wet-tools.d.ts +42 -0
  73. package/dist/paint-editor/wet.d.cts +22 -1
  74. package/dist/paint-editor/wet.d.ts +22 -1
  75. package/dist/vector/index.cjs +1002 -1002
  76. package/dist/vector/index.js +7 -7
  77. package/package.json +1 -1
  78. package/skills/pixerate-brushes/SKILL.md +1 -0
  79. package/skills/pixerate-paint-editor/SKILL.md +11 -29
  80. package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
  81. package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
@@ -35,7 +35,7 @@ import {
35
35
  traceField,
36
36
  traceVectorImage,
37
37
  vectorTraceContours
38
- } from "../chunk-75SYKUOT.js";
38
+ } from "../chunk-N4MCWQZ7.js";
39
39
  import {
40
40
  rasterizeCoverage
41
41
  } from "../chunk-ADMRNWR7.js";
@@ -66,7 +66,7 @@ import {
66
66
  reverseMorphContour,
67
67
  subpathToMorphContour,
68
68
  turnMorphContour
69
- } from "../chunk-43XJC3PP.js";
69
+ } from "../chunk-DWPFGWW5.js";
70
70
  import {
71
71
  DEFAULT_VECTOR_FONT,
72
72
  IDENTITY_MATRIX,
@@ -556,10 +556,10 @@ import {
556
556
  withVectorStrokes,
557
557
  withVectorTargetPaint,
558
558
  zigzagPaths
559
- } from "../chunk-Y2ISSVPH.js";
559
+ } from "../chunk-HHX7ST6Z.js";
560
560
  import {
561
561
  snapshotBrushLibrary
562
- } from "../chunk-ZM7IKQ4X.js";
562
+ } from "../chunk-PPDUCZZV.js";
563
563
  import "../chunk-JL6YH5WD.js";
564
564
  import {
565
565
  createProvenance
@@ -597,11 +597,11 @@ import {
597
597
  resolveEffectValues
598
598
  } from "../chunk-MV7CYFMJ.js";
599
599
  import "../chunk-V2ZCJRDM.js";
600
- import "../chunk-LMG57L5A.js";
601
- import "../chunk-W6JDUXP3.js";
600
+ import "../chunk-CTHWXW5M.js";
601
+ import "../chunk-J23JOHPR.js";
602
602
  import {
603
603
  GRAIN_PATTERNS
604
- } from "../chunk-ACTHSQOI.js";
604
+ } from "../chunk-LDZOXESA.js";
605
605
  import "../chunk-4D6KP5BV.js";
606
606
  import "../chunk-ODBT55Q7.js";
607
607
  import "../chunk-IRSTHLQQ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.275.1",
3
+ "version": "0.277.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@ built-in presets (read-only) and the user's brushes, shared by every editor that
24
24
  | Thermochromic | Heat-reactive ink (`hotColor` `#rrggbb`, `threshold` 0.05..0.95) that shifts colour where the paint editor's heat brush warms the canvas (preset `thermo-ink`). |
25
25
  | Iridescence | Structural thin-film interference (`thickness` 100..1000 nm, `pressureToThickness` 0..1, `ior` 1.2..2.0) whose hue shifts with light and view tilt (`scarab-lacquer`, `soap-film`, `pearl-glaze`). |
26
26
  | Ghost | On wet paint (`behaviour.surface: "wet"`): `linger` (0.5..60 s before the brush's own colour fades away) and `smear` (0..1, how much it lifts and drags the paint it crosses). Presets `ghost-wash`, `ghost-gouache`, `ghost-oil` and `invisible-ink`. |
27
+ | Ferro | Liquid magnetic ferrofluid (`amount` 0..1, capillary `wavelength` 4..64 px, `threshold` 0..1; presets `ferrofluid`, `ferro-ink`). |
27
28
  | Motion | Animation after painting (live effect paint, particle `waveSpeed`). |
28
29
  | Live patterns | `livePatterns`: Gray-Scott reaction-diffusion patterns (`pattern`: `spots`, `stripes`, `coral`, `maze`, `worms`, `feed`, `kill`, `scale`, `contrast`, `secondColor`, `relief`) that grow in wet paint (`behaviour.surface: 'wet'`) and freeze when it dries. Built-in presets: `turing-wash`, `coral-ink`. |
29
30
  | Colour | A `default` colour, a `palette` of up to 16, `own` (always its own colour). Each is a hex or `rgba()` colour, a light / dark pair `{ light, dark }`, a gradient or effect paint. A gradient default is always painted; a solid or light / dark default becomes the editor's colour when the brush is chosen (unless the user locked theirs), resolved for the colour scheme. Canvas strokes and vector brushstrokes take it too, when they are given no colour. Canvas and vector have the same lock (`canvas.setColorLocked`, `vector.setColorLocked`), and a canvas colour taken from a light / dark default follows the scheme like the paint editor's; vector strokes already made keep theirs. |
@@ -11,35 +11,10 @@ seed: its grain is a height map (peaks and valleys) that grain-responsive brushe
11
11
  is never part of a layer. Coordinates are document pixels, (0, 0) top-left, y down. See
12
12
  `pixerate-agent-tools` for errors, namespacing and batching.
13
13
 
14
- - **Brush colours.** Choosing a brush with a default colour (highlighter yellow, ballpoint
15
- blue, chalk light or dark for the colour scheme) makes it the paint colour, unless the colour is
16
- locked (`set_color` `locked`). Set the colour after choosing the brush, or lock it first.
17
- - **Brushes** come from the shared brush library (`pixerate-brushes`: `list_brush_presets`,
18
- `create_brush`, `preview_brush`…) and are made of facets: a tip (size, hardness, spacing, angle,
19
- roundness, opacity, flow, blend), dynamics (pressure, tilt, velocity, jitter, a **rotation**
20
- mode `fixed`, `stroke` or `barrel`), behaviour (paper **grain** response, pick-up, wet or thick
21
- surface), marks, light, motion and colour. `set_brush` chooses one and lays facet changes over
22
- it; `get_brush` reads it with the engine it paints with: `dab` (most media), `eraser`, `mixer` (the Mixer Brush: a reservoir of paint plus paint
23
- it picks up), `blender` (mixes the colours under and near the tip) or `smudge` (drags paint
24
- along the stroke), `wet` (watercolour, gouache, oil and the wet tools, below), `impasto`
25
- (thick paint with relief and material, below), `particle` (procedural particle brushes),
26
- `bristle` (a tuft of bristles) or `sticker` (sprays editable stickers).
27
- - **Wet media** (engine `wet`: `behaviour.surface` `wet`, and the wet tools; categories `watercolor` and `wet-tool`): a stroke lays water
28
- and pigment on the layer's **wet layer**, a fluid simulation that keeps running after the
29
- stroke: the water spreads until its edge is pinned by the paper, carries pigment out to the
30
- rim as it dries (edge darkening), drops pigment into the paper's valleys (granulation), pushes
31
- pigment back where new water flows in (blooms), bleeds between touching wet colours and runs
32
- downhill on a tilted canvas. Paint stays wet until it dries by itself (a few thousand steps)
33
- or `dry_paint` dries it; a dried wash is fixed, so the next wash **glazes** over it. A wet
34
- brush paints, or is a tool (`tool` `water`, `dryer`, `blow`, `blot`, `wet-blend`, `wet-drag`);
35
- `behaviour.wet` `water`, `pigment` (concentration), `body` (0 transparent watercolour .. 1
36
- opaque gouache) and `rewet`, and `behaviour.strength`, set it up. Every result says whether paint is still wet (`wet` / `stillWet`).
37
- - **Behaviour** (`behaviour.exchange`, dab and mixer brushes) is how a brush treats paint already on the
38
- canvas: a **pick-up ratio** (how much paint each dab lifts and carries), a **mix ratio** (how
39
- much of the carried paint goes into what it lays down), **dirty** (keep the carried colour into
40
- the next stroke) and **multi-colour** (`tip`: several colours side by side across the tip;
41
- `dab`: the colour changes along the stroke). A Mixer Brush's `mixer.wet` / `mixer.mix` are the
42
- same two ratios. `get_mixer_brush` reads what the brush carries; `clean_mixer_brush` wipes it.
14
+ - **Brush colours.** Choosing a brush with a default colour (highlighter yellow, ballpoint blue, chalk light or dark for the colour scheme) makes it the paint colour, unless the colour is locked (`set_color` `locked`). Set the colour after choosing the brush, or lock it first.
15
+ - **Brushes** come from the shared brush library (`pixerate-brushes`: `list_brush_presets`, `create_brush`, `preview_brush`…) and are made of facets: a tip (size, hardness, spacing, angle, roundness, opacity, flow, blend), dynamics (pressure, tilt, velocity, jitter, a **rotation** mode `fixed`, `stroke` or `barrel`), behaviour (paper **grain** response, pick-up, wet or thick surface), marks, light, ferrofluid, motion and colour. `set_brush` chooses one and lays facet changes over it; `get_brush` reads it with the engine it paints with: `dab` (most media), `eraser`, `mixer` (the Mixer Brush: a reservoir of paint plus paint it picks up), `blender` (mixes the colours under and near the tip) or `smudge` (drags paint along the stroke), `wet` (watercolour, gouache, oil and the wet tools, below), `impasto` (thick paint with relief and material, below), `particle` (procedural particle brushes), `bristle` (a tuft of bristles) or `sticker` (sprays editable stickers).
16
+ - **Wet media** (engine `wet`: `behaviour.surface` `wet`, and the wet tools; categories `watercolor` and `wet-tool`): a stroke lays water and pigment on the layer's **wet layer**, a fluid simulation that keeps running after the stroke: the water spreads until its edge is pinned by the paper, carries pigment out to the rim as it dries (edge darkening), drops pigment into the paper's valleys (granulation), pushes pigment back where new water flows in (blooms), bleeds between touching wet colours and runs downhill on a tilted canvas. Paint stays wet until it dries by itself (a few thousand steps) or `dry_paint` dries it; a dried wash is fixed, so the next wash **glazes** over it. A wet brush paints, or is a tool (`tool` `water`, `dryer`, `blow`, `blot`, `wet-blend`, `wet-drag`); `behaviour.wet` `water`, `pigment` (concentration), `body` (0 transparent watercolour .. 1 opaque gouache) and `rewet`, and `behaviour.strength`, set it up. Every result says whether paint is still wet (`wet` / `stillWet`).
17
+ - **Behaviour** (`behaviour.exchange`, dab and mixer brushes) is how a brush treats paint already on the canvas: a **pick-up ratio** (how much paint each dab lifts and carries), a **mix ratio** (how much of the carried paint goes into what it lays down), **dirty** (keep the carried colour into the next stroke) and **multi-colour** (`tip`: several colours side by side across the tip; `dab`: the colour changes along the stroke). A Mixer Brush's `mixer.wet` / `mixer.mix` are the same two ratios. `get_mixer_brush` reads what the brush carries; `clean_mixer_brush` wipes it.
43
18
  - **Grain**: a brush's grain response times the paper's `depth` decides how much light pressure
44
19
  skips the valleys. Pencil, pastel and charcoal are grainy; airbrush, marker and ink are not.
45
20
  A brush can also carry its own texture, `tip.grain` (a pattern such as `sanded` or `canvas`, or
@@ -126,6 +101,7 @@ in wet paint and freeze when it dries; presets `turing-wash`, `coral-ink`):
126
101
 
127
102
  Wet oil blends where the brush meets it; impasto adds relief lit by `set_lighting`, and `iridescence` (`scarab-lacquer`, `soap-film`, `pearl-glaze`) adds thin-film structural colour that shifts hue with light and `set_view_tilt`: load `pixerate-paint-oil-impasto` and read [references/oils.md](references/oils.md). **Light paint** (neon, glowing glass and watercolour: `light.amount`, the Light category; backlit paper): [references/light-paint.md](references/light-paint.md).
128
103
  **Smart inks** (`fluorescence` for UV/invisible ink under `set_uv_light`, `thermochromic` for heat-reactive ink with `paint_heat` / `advance_heat`; presets `uv-marker`, `thermo-ink`): [references/smart-inks.md](references/smart-inks.md).
104
+ **Ferrofluid paint** (`ferro.amount`, presets `ferrofluid` and `ferro-ink`, pulled by dipole magnets into lit Rosensweig spikes until cured): [references/ferrofluid.md](references/ferrofluid.md).
129
105
 
130
106
  ## Gradient paint
131
107
 
@@ -336,6 +312,12 @@ Listing, saving, sharing and previewing brushes are the brush library's tools (`
336
312
  | `advance_heat` | Step thermal diffusion and Newtonian cooling by `steps` (1..200, default 10) so warm patches spread and fade. Not an undo step. Returns `steps`, heat stats and `dried`. |
337
313
  | `clear_heat` | Cool the entire canvas heat field back to 0 at once so thermochromic paint returns to its cold colour. One undo step when warm. Returns `{ cleared }`. |
338
314
  | `get_heat` | Read the canvas heat stats (`max`, `mean`, `hotCells`, `bounds`), optional temperature `samples` at `points`, and `thermochromicLayerIds`. Changes nothing. |
315
+ | `add_magnet` | Place a dipole magnet at `x`, `y` (document px; `strength` -1..1 default 0.8, `radius` 4..2048 px default 80): liquid ferrofluid gathers toward it and rises into hexagonal Rosensweig spikes shaded by `set_lighting`. One undo step. Returns the magnet (`id`, `x`, `y`, `strength`, `radius`). |
316
+ | `update_magnet` | Move or tune a magnet by `id` (`x`, `y`, `strength`, `radius`): liquid ferrofluid gathers and spikes update at once. One undo step. Returns the magnet. |
317
+ | `remove_magnets` | Remove magnets by `ids` (or all when omitted): uncured ferrofluid relaxes, cured relief stays. One undo step. Returns `removed`. |
318
+ | `list_magnets` | List the canvas magnets (`id`, `x`, `y`, `strength`, `radius`). Changes nothing. |
319
+ | `get_ferrofluid` | Read a layer's liquid ferrofluid and live spikes (`ferro`, `bounds`, `peak`, `coverage`, `activeSpikeArea`, `maxSpikeHeight`, `magnetCount`, `wavelength`, `threshold`). Changes nothing. |
320
+ | `cure_ferrofluid` | Bake a layer's current magnetic spikes into solid impasto height and dark glossy material, clearing its liquid ferrofluid so magnet edits no longer change it. One undo step. Returns `layerId`, `cured`, `bounds`, `maxSpikeHeight`, `activeSpikeArea`. |
339
321
 
340
322
  ### Stickers
341
323
 
@@ -25,6 +25,7 @@ more; this is where each one went.
25
25
  | Thermochromic ink (shifts toward `hotColor` where `paint_heat` warms the canvas) | `thermochromic` (`hotColor` `#rrggbb`, `threshold` 0.05..0.95); `unset: ['thermochromic']` drops it |
26
26
  | Thin-film iridescence | `iridescence` (`thickness` 100..1000 nm, `pressureToThickness` 0..1, `ior` 1.2..2.0); `unset: ['iridescence']` drops it |
27
27
  | Ghost paint (linger, smear) | `ghost` (`linger`, `smear`); `unset: ['ghost']` drops it |
28
+ | Ferrofluid | `ferro` (`amount` 0..1 or numeric shorthand, `wavelength` 4..64 px, `threshold` 0..1); `unset: ['ferro']` drops it |
28
29
 
29
30
  ## Examples
30
31
 
@@ -0,0 +1,36 @@
1
+ # Ferrofluid paint
2
+
3
+ Ferrofluid paint is liquid magnetic paint (`ferro.amount` above 0 on any dab, particle, bristle,
4
+ Mixer Brush or impasto brush; presets `ferrofluid` and `ferro-ink`). Place dipole magnets on the
5
+ canvas (`add_magnet`), and the liquid paint gathers toward the magnets and rises into hexagonal
6
+ Rosensweig relief spikes along the magnetic field lines, shaded live by `set_lighting`. The paint
7
+ stays liquid and responds to magnet moves until `cure_ferrofluid` bakes the spikes into solid
8
+ impasto relief.
9
+
10
+ - **How it works.** Each layer keeps a liquid ferrofluid channel (`doc.ferro[layerId]`) alongside
11
+ its colour and impasto relief, and the document keeps dipole magnets (`doc.magnets`: `x`, `y`,
12
+ `strength` `-1..1`, `radius` `4..2048` px). At render time, wherever a pixel has liquid
13
+ ferrofluid (`ferro > 0`) and the analytical dipole field $|\mathbf{B}|$ exceeds the layer's
14
+ critical `threshold`, the surface rises into a hexagonal Rosensweig lattice of spikes spaced by
15
+ the capillary `wavelength` (4..64 px, default 14 px) with amplitude $\propto \sqrt{1 - B_c / |\mathbf{B}|} \times \text{ferro}$
16
+ and tilted along the horizontal component of $\mathbf{B}$.
17
+ - **Shading and material.** The spikes feed directly into the impasto lighting pass (`lightTile` /
18
+ `set_lighting`). Where a layer has no explicit impasto material of its own, liquid ferrofluid
19
+ gives it a dark glossy metallic carrier finish (`gloss` 0.88, `metallic` 0.65, `rim` 0.25) and
20
+ deepens the colour toward `#111827`, so grazing light (`set_lighting` `elevation` 20..35) catches
21
+ crisp specular highlights on every spike tip.
22
+ - **Gathering toward magnets.** Adding (`add_magnet`) or moving/tuning (`update_magnet`) a magnet
23
+ runs deterministic advection steps on every layer's liquid ferrofluid along $\nabla |\mathbf{B}|$,
24
+ so painted pools visibly pull toward nearby poles.
25
+ - **Moving vs curing.** While uncured, moving or removing magnets (`update_magnet`,
26
+ `remove_magnets`) immediately reshapes or relaxes the spikes. Call `cure_ferrofluid` when a
27
+ layer's spike crown is right: it bakes the current spike height and dark glossy material into
28
+ the layer's permanent impasto relief (`doc.thick[layerId]`) and clears `doc.ferro[layerId]` in
29
+ one undo step, so later magnet changes leave that layer untouched.
30
+ - **Brush facet.** Pass `ferro: 0.85` or `ferro: { amount: 0.85, wavelength: 14, threshold: 0.18 }`
31
+ to `set_brush` or per-stroke `paint_stroke` `brush`; `unset: ["ferro"]` turns it off.
32
+ - `ferrofluid`: thick, glossy dark ferrofluid pool (`wavelength` 14 px, `threshold` 0.18).
33
+ - `ferro-ink`: crisp calligraphic ferrofluid ink with finer spikes (`wavelength` 10 px,
34
+ `threshold` 0.15).
35
+ - **Refusals.** Wet brushes, sticker sprays and stroke layers refuse `ferro.amount > 0`, naming
36
+ the fix (`ferrofluid` / `ferro-ink` on a pixel layer).