@pixerate/editor 0.246.0 → 0.248.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 (50) hide show
  1. package/dist/animation-editor/index.cjs +59 -59
  2. package/dist/animation-editor/index.js +3 -3
  3. package/dist/{brushstrokeRender-ZDHMXJAE.cjs → brushstrokeRender-DTDPFBLC.cjs} +5 -5
  4. package/dist/{brushstrokeRender-AKUI3SEJ.js → brushstrokeRender-QLCXROJK.js} +2 -2
  5. package/dist/canvas/index.cjs +8 -8
  6. package/dist/canvas/index.js +1 -1
  7. package/dist/{chunk-DQWPMGH2.js → chunk-5GLYUSNT.js} +1 -1
  8. package/dist/{chunk-Q2UYUWAZ.cjs → chunk-6G6ZDDDB.cjs} +390 -27
  9. package/dist/chunk-7T7JWTVE.cjs +192 -0
  10. package/dist/{chunk-UVPD55UZ.cjs → chunk-IBDU6EGZ.cjs} +35 -35
  11. package/dist/{chunk-FK5LTDOO.cjs → chunk-L6REDLNB.cjs} +2 -2
  12. package/dist/{chunk-LDPLEZ5T.js → chunk-NZ3O3A5C.js} +1 -1
  13. package/dist/{chunk-N6WUB3Y5.js → chunk-Q2HA52V4.js} +1 -1
  14. package/dist/chunk-SC2RZ5EF.js +192 -0
  15. package/dist/{chunk-5MFDD7SP.cjs → chunk-TPWB4Y6L.cjs} +1 -1
  16. package/dist/{chunk-VDAWA546.js → chunk-VNBUDVD4.js} +388 -25
  17. package/dist/{chunk-XTWXBQD4.cjs → chunk-WVS5KQDH.cjs} +442 -87
  18. package/dist/{chunk-A23PSOBB.js → chunk-XNRBMYNB.js} +1 -1
  19. package/dist/{chunk-CEPKXLBS.cjs → chunk-XWTRATIM.cjs} +23 -23
  20. package/dist/{chunk-4PMCMWQS.js → chunk-ZFSLEOSQ.js} +357 -2
  21. package/dist/image-editor/effect-tools.d.cts +37 -4
  22. package/dist/image-editor/effect-tools.d.ts +37 -4
  23. package/dist/image-editor/index.cjs +1059 -1552
  24. package/dist/image-editor/index.d.cts +1 -1
  25. package/dist/image-editor/index.d.ts +1 -1
  26. package/dist/image-editor/index.js +275 -768
  27. package/dist/paint-editor/controller.d.cts +34 -1
  28. package/dist/paint-editor/controller.d.ts +34 -1
  29. package/dist/paint-editor/index.cjs +426 -257
  30. package/dist/paint-editor/index.js +184 -15
  31. package/dist/paint-editor/procedural.d.cts +82 -5
  32. package/dist/paint-editor/procedural.d.ts +82 -5
  33. package/dist/paint-editor/tools.d.cts +1 -1
  34. package/dist/paint-editor/tools.d.ts +1 -1
  35. package/dist/paint-editor/ui.d.cts +15 -2
  36. package/dist/paint-editor/ui.d.ts +15 -2
  37. package/dist/{image-editor/layers/effects-ui.d.ts → raster/effects/ui.d.cts} +32 -5
  38. package/dist/{image-editor/layers/effects-ui.d.cts → raster/effects/ui.d.ts} +32 -5
  39. package/dist/raster/index.cjs +13 -1
  40. package/dist/raster/index.d.cts +1 -0
  41. package/dist/raster/index.d.ts +1 -0
  42. package/dist/raster/index.js +12 -0
  43. package/dist/vector/index.cjs +962 -962
  44. package/dist/vector/index.js +4 -4
  45. package/package.json +1 -1
  46. package/skills/pixerate-paint-editor/SKILL.md +18 -27
  47. package/skills/pixerate-paint-editor/references/gradient-paint.md +23 -0
  48. package/skills/pixerate-paint-editor/references/layer-effects.md +28 -0
  49. package/skills/pixerate-paint-editor/references/light-paint.md +10 -2
  50. package/skills/pixerate-paint-editor/references/particle-brushes.md +66 -0
@@ -2,21 +2,30 @@ import {
2
2
  toVideoFfmpeg
3
3
  } from "../chunk-L3JFOTBQ.js";
4
4
  import {
5
+ createLayerEffectTools,
5
6
  deserializeLayers,
6
7
  exportPsd,
7
8
  resolveExternalAssets,
8
9
  serializeLayers,
9
10
  serializeLayersExternal
10
- } from "../chunk-4PMCMWQS.js";
11
+ } from "../chunk-ZFSLEOSQ.js";
11
12
  import "../chunk-3CI7FRMK.js";
12
13
  import "../chunk-ZNA3IQZF.js";
13
14
  import {
15
+ LAYER_EFFECT_KINDS,
16
+ LayerEffectError,
14
17
  RasterCompositor,
18
+ defaultLayerEffect,
15
19
  findLayer,
16
20
  flattenLayers,
17
21
  insertLayer,
22
+ isLayerEffectKind,
18
23
  isLayerLocked,
19
- mapLayer
24
+ layerEffectLabel,
25
+ mapLayer,
26
+ mergeLayerEffect,
27
+ newEffectId,
28
+ validateLayerEffects
20
29
  } from "../chunk-UOOK7HTX.js";
21
30
  import "../chunk-OHKOFKBY.js";
22
31
  import "../chunk-ZIF4UJJ6.js";
@@ -55,7 +64,7 @@ import {
55
64
  realPresenceClock,
56
65
  tweenPresenceCursor
57
66
  } from "../chunk-IVSWRVB4.js";
58
- import "../chunk-N6WUB3Y5.js";
67
+ import "../chunk-Q2HA52V4.js";
59
68
  import {
60
69
  BRUSH_ID,
61
70
  BRUSH_ROTATIONS,
@@ -72,11 +81,14 @@ import {
72
81
  DEFAULT_PARTICLE,
73
82
  DEFAULT_STICKER,
74
83
  DEFAULT_WET_BRUSH,
84
+ DOT_ROTATIONS,
85
+ DOT_TEXTURE_ORDERS,
75
86
  DryStroke,
76
87
  EFFECT_MAP_REPEATS,
77
88
  EFFECT_STRIP_HEIGHT,
78
89
  IDENTITY_COPY,
79
90
  IMPASTO_TOOLS,
91
+ MAX_DOT_TEXTURES,
80
92
  MULTI_COLOR_MODES,
81
93
  PAINT_BRUSH_CATEGORIES,
82
94
  PAINT_BRUSH_ENGINES,
@@ -87,10 +99,12 @@ import {
87
99
  ParticleStroke,
88
100
  PenDabWalker,
89
101
  PenSmoother,
102
+ RIBBON_PAIRS,
90
103
  STAMP_KINDS,
91
104
  StickerSpray,
92
105
  WET_MEDIA,
93
106
  WET_TOOLS,
107
+ addPolygonArea,
94
108
  brushTakesPaint,
95
109
  brushToJSON,
96
110
  clonePaper,
@@ -133,7 +147,7 @@ import {
133
147
  tiltToOrientation,
134
148
  toStrokePoint,
135
149
  usesPaintExchange
136
- } from "../chunk-VDAWA546.js";
150
+ } from "../chunk-VNBUDVD4.js";
137
151
  import "../chunk-M6DNZNVY.js";
138
152
  import "../chunk-WEIK3UYB.js";
139
153
  import "../chunk-PWBZH3FK.js";
@@ -3500,7 +3514,9 @@ function liveStrokeBounds(stroke) {
3500
3514
  if (q.y > y1) y1 = q.y;
3501
3515
  }
3502
3516
  }
3503
- const pad = stroke.brush.tip.size * (stroke.brush.engine === "particle" ? 2 : 1) + 2;
3517
+ const particle = stroke.brush.engine === "particle" ? stroke.brush.particle : null;
3518
+ const wave = particle ? Math.max(particle.waveAmplitude ?? 0, particle.kind === "fuzzy" ? 0.12 : 0) : 0;
3519
+ const pad = stroke.brush.tip.size * (particle ? 2 + wave : 1) + 2;
3504
3520
  return { x: Math.floor(x0 - pad), y: Math.floor(y0 - pad), width: Math.ceil(x1 - x0 + 2 * pad), height: Math.ceil(y1 - y0 + 2 * pad) };
3505
3521
  }
3506
3522
 
@@ -4032,6 +4048,14 @@ function effectLayersToJSON(map, keep) {
4032
4048
  }
4033
4049
 
4034
4050
  // src/paint-editor/controller.ts
4051
+ function effectGuard(fn) {
4052
+ try {
4053
+ return fn();
4054
+ } catch (e) {
4055
+ if (e instanceof LayerEffectError) throw new PaintEditorError(e.message);
4056
+ throw e;
4057
+ }
4058
+ }
4035
4059
  var PaintEditorError = class extends Error {
4036
4060
  constructor(message) {
4037
4061
  super(message);
@@ -4557,6 +4581,7 @@ var PaintEditorController = class _PaintEditorController {
4557
4581
  active: l.id === this.doc.activeLayerId,
4558
4582
  bounds,
4559
4583
  ...this.doc.liveStrokes?.[l.id] ? { liveStrokes: this.doc.liveStrokes[l.id].length } : {},
4584
+ ...l.effects?.items.length ? { effects: l.effects.items.length } : {},
4560
4585
  parentId,
4561
4586
  depth
4562
4587
  };
@@ -4630,6 +4655,94 @@ var PaintEditorController = class _PaintEditorController {
4630
4655
  this.commit("Layer properties", { ...this.doc, layers: mapLayer(this.doc.layers, id, () => next) });
4631
4656
  return this.describeLayer(next);
4632
4657
  }
4658
+ // --- Layer effects (layer styles) ---------------------------------------------
4659
+ /** A layer's live effects (an empty, enabled list when it has none). */
4660
+ getLayerEffects(id) {
4661
+ return this.requireLayer(id).effects ?? { enabled: true, items: [] };
4662
+ }
4663
+ /**
4664
+ * Adds a live effect of `kind` (outer and inner glow, drop and inner shadow, stroke, bevel &
4665
+ * emboss, satin, colour / gradient / pattern overlay) with Photoshop's defaults and `settings`
4666
+ * applied, at `index` in the layer's list (default the end). The effect is drawn from the
4667
+ * layer's pixels when it is composited, so it follows later painting. One undo step.
4668
+ */
4669
+ addLayerEffect(id, kind, settings = {}, index) {
4670
+ if (!isLayerEffectKind(kind)) throw new PaintEditorError(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
4671
+ const current = this.effectsLayer(id, "add effects").effects ?? { enabled: true, items: [] };
4672
+ const ids = new Set(current.items.map((e) => e.id));
4673
+ let effectId = newEffectId();
4674
+ while (ids.has(effectId)) effectId = newEffectId();
4675
+ const { id: _id, kind: _kind, ...rest } = settings;
4676
+ const effect = effectGuard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
4677
+ const items = [...current.items];
4678
+ const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
4679
+ items.splice(at, 0, effect);
4680
+ this.writeLayerEffects(id, { ...current, items }, `Add ${layerEffectLabel(kind)}`, true);
4681
+ return { effect, effects: this.getLayerEffects(id) };
4682
+ }
4683
+ /**
4684
+ * Changes one effect: only the given settings (nested objects replace whole; `null` removes an
4685
+ * optional contour or texture). One undo step, or with `commitToHistory` false folded into the
4686
+ * last change of this effect (a slider drag). Returns the layer's effects.
4687
+ */
4688
+ updateLayerEffect(id, effectId, changes, commitToHistory = true) {
4689
+ const current = this.effectsLayer(id, "change its effects").effects;
4690
+ const i = this.requireEffect(id, effectId);
4691
+ const items = [...current.items];
4692
+ items[i] = effectGuard(() => mergeLayerEffect(items[i], changes));
4693
+ this.writeLayerEffects(id, { ...current, items }, `Change ${layerEffectLabel(items[i].kind)}`, commitToHistory);
4694
+ return this.getLayerEffects(id);
4695
+ }
4696
+ /** Removes one effect (one undo step). Returns the layer's remaining effects. */
4697
+ removeLayerEffect(id, effectId) {
4698
+ const current = this.effectsLayer(id, "change its effects").effects;
4699
+ const i = this.requireEffect(id, effectId);
4700
+ this.writeLayerEffects(id, { ...current, items: current.items.filter((e) => e.id !== effectId) }, `Remove ${layerEffectLabel(current.items[i].kind)}`, true);
4701
+ return this.getLayerEffects(id);
4702
+ }
4703
+ /** Turns all of a layer's effects on or off and, when given, sets Scale Effects (0.01..10). One undo step. */
4704
+ setLayerEffectsEnabled(id, enabled, scale, commitToHistory = true) {
4705
+ const current = this.effectsLayer(id, "change its effects").effects ?? { enabled: true, items: [] };
4706
+ if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) throw new PaintEditorError(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
4707
+ const next = { ...current, enabled };
4708
+ if (scale !== void 0) {
4709
+ if (scale === 1) delete next.scale;
4710
+ else next.scale = scale;
4711
+ }
4712
+ this.writeLayerEffects(id, next, "Layer effects", commitToHistory);
4713
+ return this.getLayerEffects(id);
4714
+ }
4715
+ /** Replaces a layer's effects, validated (`null` removes them). One undo step. Returns the effects. */
4716
+ setLayerEffects(id, effects, commitToHistory = true) {
4717
+ this.effectsLayer(id, "change its effects");
4718
+ if (effects) effectGuard(() => validateLayerEffects(effects));
4719
+ this.writeLayerEffects(id, effects, "Layer effects", commitToHistory);
4720
+ return this.getLayerEffects(id);
4721
+ }
4722
+ /** Document patterns for pattern effects besides the built-in ones: none (paint documents bring none). */
4723
+ getDocumentPatterns() {
4724
+ return [];
4725
+ }
4726
+ effectsLayer(id, action) {
4727
+ this.requireIdle("changing layer effects");
4728
+ const l = this.requireLayer(id);
4729
+ if (isLayerLocked(l, "all")) throw new PaintEditorError(`Layer "${l.name}" is fully locked, so it can't ${action}; unlock it first (locks {all: false}).`);
4730
+ return l;
4731
+ }
4732
+ requireEffect(id, effectId) {
4733
+ const l = this.requireLayer(id);
4734
+ const items = l.effects?.items ?? [];
4735
+ const i = items.findIndex((e) => e.id === effectId);
4736
+ if (i === -1) throw new PaintEditorError(`Layer "${l.name}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
4737
+ return i;
4738
+ }
4739
+ /** Writes a layer's effects (dropped when empty, enabled and unscaled), as an undo step or folded into the last one. */
4740
+ writeLayerEffects(id, effects, label, commitToHistory) {
4741
+ const next = { ...this.requireLayer(id) };
4742
+ if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) next.effects = effects;
4743
+ else delete next.effects;
4744
+ this.commitOrAmend(label, { ...this.doc, layers: mapLayer(this.doc.layers, id, () => next) }, commitToHistory);
4745
+ }
4633
4746
  removeLayer(id) {
4634
4747
  this.requireIdle("removing a layer");
4635
4748
  const l = this.requireLayer(id);
@@ -9001,17 +9114,39 @@ var PARTICLE_BRUSH = {
9001
9114
  type: "object",
9002
9115
  description: "Particle engine settings (procedural brushes). Missing fields keep their current values.",
9003
9116
  properties: {
9004
- kind: { type: "string", enum: [...PARTICLE_KINDS], description: "string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade." },
9005
- render: { type: "string", enum: [...PARTICLE_RENDERS], description: "dot: a mark at each step; line: each particle draws a continuous line; sequence: beads spaced along each path." },
9117
+ kind: {
9118
+ type: "string",
9119
+ enum: [...PARTICLE_KINDS],
9120
+ description: "string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade; elastic: strands whose links keep a rest length, stretching when the pen is fast and snapping back; fuzzy: winding strings, each strand waving out of step with the others (always waves by at least 0.12 of the tip size)."
9121
+ },
9122
+ render: {
9123
+ type: "string",
9124
+ enum: [...PARTICLE_RENDERS],
9125
+ description: "dot: a mark at each step (round, or dotTextures); line: each particle draws a continuous line; sequence: beads spaced along each path (round, or dotTextures); ribbon: the band between neighbouring particles' paths filled (ribbonFill), with the paths drawn as its edges at the mark size (flame, silk, smoke)."
9126
+ },
9006
9127
  count: { type: "integer", minimum: 1, maximum: 64, description: "Particles (flow: how many are thrown per tip of travel)." },
9007
- size: { type: "number", minimum: 0, maximum: 1, description: "Mark size as a fraction of the tip size." },
9008
- spread: { type: "number", minimum: 0, maximum: 1, description: "How far apart the particles start, as a fraction of the tip size." },
9009
- stiffness: { type: "number", minimum: 0, maximum: 1, description: "Spring and string stiffness: stiff follows closely, soft swings wide." },
9128
+ size: { type: "number", minimum: 0, maximum: 1, description: "Mark size as a fraction of the tip size (ribbon: the width of its edges)." },
9129
+ spread: { type: "number", minimum: 0, maximum: 1, description: "How far apart the particles start, as a fraction of the tip size (ribbon: how wide it is)." },
9130
+ stiffness: { type: "number", minimum: 0, maximum: 1, description: "Spring, string and elastic stiffness: stiff follows closely (elastic: snaps back faster), soft swings wide." },
9010
9131
  damping: { type: "number", minimum: 0, maximum: 1, description: "How quickly motion dies down." },
9011
9132
  speed: { type: "number", minimum: 0, maximum: 1, description: "Orbit and flow speed." },
9012
9133
  gravity: { type: "number", minimum: -1, maximum: 1, description: "Pull downwards (negative: upwards)." },
9013
9134
  life: { type: "number", minimum: 0, maximum: 1, description: "Flow particles: how long they live." },
9014
- interval: { type: "number", minimum: 0, maximum: 1, description: "Sequence: spacing of the beads." }
9135
+ interval: { type: "number", minimum: 0, maximum: 1, description: "Sequence: spacing of the beads." },
9136
+ ribbonFill: { type: "number", minimum: 0, maximum: 1, description: "Ribbon: opacity of the fill between the paths, relative to their edges (default 0.5; 0 draws only the edges)." },
9137
+ ribbonPairs: { type: "string", enum: [...RIBBON_PAIRS], description: "Ribbon: adjacent (default) joins each particle to its neighbour around the pen; first-last makes one band between the outermost two." },
9138
+ waveAmplitude: { type: "number", minimum: 0, maximum: 1, description: "Wave: how far each particle's drawn place swings sideways to the stroke, as a fraction of the tip size (default 0: no wave). Works with every kind and render." },
9139
+ waveLength: { type: "number", minimum: 4, maximum: 2e3, description: "Wave: wavelength in px of pen travel (default 60)." },
9140
+ waveSpeed: { type: "number", minimum: 0, maximum: 1, description: "Wave: how fast the waves run, by stroke time (0 default: they stand still on the path; 1: about four a second, so slow strokes wave more). Strokes without times run at a nominal 0.5 px/ms." },
9141
+ wavePhaseJitter: { type: "number", minimum: 0, maximum: 1, description: "Wave: how far out of step the particles' waves are (default 0: all in step, a wavy band; 1: random phases, tangled strands)." },
9142
+ dotTextures: {
9143
+ type: "array",
9144
+ maxItems: MAX_DOT_TEXTURES,
9145
+ items: { type: "string", enum: [...STAMP_KINDS], description: "A stamp." },
9146
+ description: `Dot and sequence renders: up to ${MAX_DOT_TEXTURES} sticker stamps (list_stickers) drawn instead of round marks; repeats are allowed (they weight the order). Empty (default): round marks.`
9147
+ },
9148
+ dotTextureOrder: { type: "string", enum: [...DOT_TEXTURE_ORDERS], description: "Textured dots: sequence (default) uses the stamps in turn; random picks one per dot (seeded, so replays are exact)." },
9149
+ dotRotation: { type: "string", enum: [...DOT_ROTATIONS], description: "Textured dots: follow turns each with its particle's motion; random (default) turns each at random; fixed uses the tip angle." }
9015
9150
  },
9016
9151
  additionalProperties: false
9017
9152
  };
@@ -9431,7 +9566,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
9431
9566
  add({
9432
9567
  name: "list_brushes",
9433
9568
  title: "List brushes",
9434
- description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, glow wash, highlighter; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
9569
+ description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, light ribbon, flame ribbon, neon strand, smoke ribbon, bokeh dots, twinkle sparks, glow wash, highlighter; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
9435
9570
  inputSchema: EMPTY3,
9436
9571
  annotations: { title: "List brushes", ...READ6 },
9437
9572
  handler: () => {
@@ -9943,6 +10078,19 @@ function createPaintEditorToolProvider(controller, options = {}) {
9943
10078
  tools.push(...createPaintReplayTools(c));
9944
10079
  tools.push(...createPaintStickerTools(c));
9945
10080
  tools.push(...createPigmentTools());
10081
+ tools.push(
10082
+ ...createLayerEffectTools(
10083
+ {
10084
+ requireLayer: (id) => void c.getLayerEffects(id),
10085
+ getLayerEffects: (id) => c.getLayerEffects(id),
10086
+ addLayerEffect: (id, kind, settings, index) => c.addLayerEffect(id, kind, settings, index),
10087
+ updateLayerEffect: (id, effectId, changes) => c.updateLayerEffect(id, effectId, changes),
10088
+ removeLayerEffect: (id, effectId) => c.removeLayerEffect(id, effectId),
10089
+ setLayerEffectsEnabled: (id, enabled, scale) => c.setLayerEffectsEnabled(id, enabled, scale)
10090
+ },
10091
+ { layers: "a pixel, stroke, sticker or group layer", error: (m) => new PaintEditorError(m) }
10092
+ )
10093
+ );
9946
10094
  const exclude = new Set(options.exclude ?? []);
9947
10095
  const description = "Natural-media painting: paper and grain, dry and wet brushes, the brush library, strokes as data, pigment mixing, stencils, guides, layers and export.";
9948
10096
  return withPaintPlayback({
@@ -10413,7 +10561,12 @@ function engineControls(brush) {
10413
10561
  { path: "particle.speed", label: "Speed", min: 0, max: 1, step: 0.01 },
10414
10562
  { path: "particle.gravity", label: "Gravity", min: -1, max: 1, step: 0.01 },
10415
10563
  { path: "particle.life", label: "Life", min: 0, max: 1, step: 0.01 },
10416
- { path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 }
10564
+ { path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 },
10565
+ { path: "particle.ribbonFill", label: "Ribbon fill", min: 0, max: 1, step: 0.01 },
10566
+ { path: "particle.waveAmplitude", label: "Wave", min: 0, max: 1, step: 0.01 },
10567
+ { path: "particle.waveLength", label: "Wavelength", min: 4, max: 400, step: 1 },
10568
+ { path: "particle.waveSpeed", label: "Wave speed", min: 0, max: 1, step: 0.01 },
10569
+ { path: "particle.wavePhaseJitter", label: "Wave scatter", min: 0, max: 1, step: 0.01 }
10417
10570
  ],
10418
10571
  mixingPath: null,
10419
10572
  dirty: false,
@@ -10628,13 +10781,20 @@ var LIGHT_SLIDERS = [
10628
10781
  { path: "elevation", label: "Light height", min: 5, max: 90, step: 1 },
10629
10782
  { path: "relief", label: "Relief", min: 0, max: 1, step: 0.01 }
10630
10783
  ];
10631
- var PARTICLE_KIND_LABELS = { string: "Strings", spring: "Springs", orbit: "Orbits", flow: "Flow" };
10632
- var PARTICLE_RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads" };
10784
+ var PARTICLE_KIND_LABELS = { string: "Strings", spring: "Springs", orbit: "Orbits", flow: "Flow", elastic: "Elastic", fuzzy: "Fuzzy" };
10785
+ var PARTICLE_RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads", ribbon: "Ribbon" };
10786
+ var RIBBON_PAIR_LABELS = { adjacent: "Neighbours", "first-last": "Outermost" };
10787
+ var DOT_TEXTURE_ORDER_LABELS = { sequence: "In turn", random: "Random" };
10788
+ var DOT_ROTATION_LABELS = { follow: "Follow motion", random: "Random", fixed: "Tip angle" };
10633
10789
  var STAMP_LABELS = { dot: "Dot", star: "Star", heart: "Heart", leaf: "Leaf", flower: "Flower", confetti: "Confetti", splat: "Splat", sparkle: "Sparkle", ring: "Ring" };
10634
10790
  function toggleStamp(stamps, stamp) {
10635
10791
  const next = stamps.includes(stamp) ? stamps.filter((s) => s !== stamp) : [...stamps, stamp];
10636
10792
  return next.length ? next : [stamp];
10637
10793
  }
10794
+ function toggleDotTexture(textures, stamp) {
10795
+ if (textures.includes(stamp)) return textures.filter((s) => s !== stamp);
10796
+ return textures.length < MAX_DOT_TEXTURES ? [...textures, stamp] : [...textures];
10797
+ }
10638
10798
  var STROKE_SLIDERS = [
10639
10799
  { path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
10640
10800
  { path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
@@ -10957,6 +11117,10 @@ export {
10957
11117
  DEFAULT_VIEW,
10958
11118
  DEFAULT_WET_BRUSH,
10959
11119
  DEFAULT_WET_PARAMS,
11120
+ DOT_ROTATIONS,
11121
+ DOT_ROTATION_LABELS,
11122
+ DOT_TEXTURE_ORDERS,
11123
+ DOT_TEXTURE_ORDER_LABELS,
10960
11124
  DryStroke,
10961
11125
  EFFECT_MAP_REPEATS,
10962
11126
  EFFECT_STRIP_HEIGHT,
@@ -10974,6 +11138,7 @@ export {
10974
11138
  MAX_BRUSH_SIZE,
10975
11139
  MAX_CANVAS_SIDE,
10976
11140
  MAX_COORDINATE,
11141
+ MAX_DOT_TEXTURES,
10977
11142
  MAX_IMAGE_SIDE,
10978
11143
  MAX_LIVE_EFFECT_SOURCES,
10979
11144
  MAX_ZOOM,
@@ -11033,6 +11198,8 @@ export {
11033
11198
  PenDabWalker,
11034
11199
  PenSmoother,
11035
11200
  PerspectiveSnapper,
11201
+ RIBBON_PAIRS,
11202
+ RIBBON_PAIR_LABELS,
11036
11203
  SNAP_DECIDE_DISTANCE,
11037
11204
  STAMP_KINDS,
11038
11205
  STAMP_LABELS,
@@ -11061,6 +11228,7 @@ export {
11061
11228
  WET_TOOL_LABELS,
11062
11229
  WetStrokeDriver,
11063
11230
  addPaintGradientStop,
11231
+ addPolygonArea,
11064
11232
  affineCopy,
11065
11233
  applyAffine,
11066
11234
  backlightOf,
@@ -11238,6 +11406,7 @@ export {
11238
11406
  timeLapseVideo,
11239
11407
  timeLapseZip,
11240
11408
  toStrokePoint,
11409
+ toggleDotTexture,
11241
11410
  toggleStamp,
11242
11411
  translateStencil,
11243
11412
  updatePaintGradientStop,
@@ -6,8 +6,12 @@
6
6
  * rules. `string`: strands of masses on springs, dragged behind the pen like hair or wet string;
7
7
  * `spring`: each particle tied to the pen by a spring, overshooting and wobbling; `orbit`:
8
8
  * particles circling the pen (spirograph curls); `flow`: particles thrown off the pen and carried
9
- * by a swirling flow field until they fade. Each is drawn as `dot`s, a continuous `line`, or a
10
- * `sequence` of beads spaced along its path.
9
+ * by a swirling flow field until they fade; `elastic`: strands whose masses keep a rest length,
10
+ * stretching when the pen is fast and snapping back; `fuzzy`: strings that wind, each strand
11
+ * waving out of step with the others. Each is drawn as `dot`s (round, or stamped with up to four
12
+ * textures), a continuous `line`, a `sequence` of beads spaced along its path, or a `ribbon`:
13
+ * the area between neighbouring particles' paths filled, with their paths as its edges. Any kind
14
+ * can wave: each particle's drawn place moves sideways on a sine along the pen's path.
11
15
  * - **Bristle brushes** (`engine: 'bristle'`): a tuft of bristles, each its own thin line. Pressure
12
16
  * splays the tuft, tilt flattens it, and bristles lag behind the tip by their stiffness, so they
13
17
  * bend into curves at turns; each bristle carries its own paint and runs dry at its own pace, so
@@ -19,11 +23,12 @@
19
23
  */
20
24
  import { CoverageMap } from '../paint/brush.cjs';
21
25
  import { type PaintGradientSource } from '../paint/gradient-paint.cjs';
22
- import type { RasterRect } from '../raster/surface.cjs';
26
+ import { type RasterRect } from '../raster/surface.cjs';
23
27
  import type { PaintBrush } from './brushes.cjs';
24
28
  import { type EffectPaintPixels } from './effect-paint.cjs';
25
29
  import { PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine.cjs';
26
30
  import { type PaperGrain } from './paper.cjs';
31
+ import { type StampKind } from './stickers.cjs';
27
32
  export interface ProceduralStrokeOptions {
28
33
  grain: PaperGrain | null;
29
34
  paperDepth: number;
@@ -71,13 +76,36 @@ declare abstract class MarkStroke implements CoverageStroke {
71
76
  protected abstract advance(copy: number, d: PenDab, rects: RasterRect[]): void;
72
77
  /** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
73
78
  protected mark(x: number, y: number, r: number, alpha: number, opacity: number, pressure: number, rects: RasterRect[]): void;
79
+ /**
80
+ * Stamps a textured mark: `kind` from the sticker stamps over a square of half-size `r`, turned
81
+ * `rotation` radians clockwise, with flow `alpha` towards `opacity`. Small marks are supersampled.
82
+ */
83
+ protected stamp(x: number, y: number, r: number, kind: StampKind, rotation: number, seed: number, alpha: number, opacity: number, pressure: number, rects: RasterRect[]): void;
84
+ /** A scratch buffer of `n` values (reused; valid until the next mark). */
85
+ protected scratch(n: number): Float32Array;
86
+ /**
87
+ * Lays coverage `vals` over `rect` towards `opacity`: paper grain, then effect paint or the
88
+ * colour of a mark at (x, y) (gradient paint), into the stroke's coverage.
89
+ */
90
+ protected lay(rect: RasterRect, vals: Float32Array, opacity: number, pressure: number, x: number, y: number, rects: RasterRect[]): void;
74
91
  /** Marks along a segment, spaced by a fraction of the radius. */
75
92
  protected segment(x0: number, y0: number, x1: number, y1: number, r: number, alpha: number, opacity: number, pressure: number, rects: RasterRect[]): void;
76
93
  }
77
- export type ParticleKind = 'string' | 'spring' | 'orbit' | 'flow';
94
+ export type ParticleKind = 'string' | 'spring' | 'orbit' | 'flow' | 'elastic' | 'fuzzy';
78
95
  export declare const PARTICLE_KINDS: readonly ParticleKind[];
79
- export type ParticleRender = 'dot' | 'line' | 'sequence';
96
+ export type ParticleRender = 'dot' | 'line' | 'sequence' | 'ribbon';
80
97
  export declare const PARTICLE_RENDERS: readonly ParticleRender[];
98
+ /** Ribbon: which particles' paths are joined (`adjacent`: each to its neighbour; `first-last`: one band between the outermost two). */
99
+ export type RibbonPairs = 'adjacent' | 'first-last';
100
+ export declare const RIBBON_PAIRS: readonly RibbonPairs[];
101
+ /** Textured dots: the stamps in turn, or one picked at random (seeded) per dot. */
102
+ export type DotTextureOrder = 'sequence' | 'random';
103
+ export declare const DOT_TEXTURE_ORDERS: readonly DotTextureOrder[];
104
+ /** Textured dots: turned with the particle's motion, at random, or at the tip angle. */
105
+ export type DotRotation = 'follow' | 'random' | 'fixed';
106
+ export declare const DOT_ROTATIONS: readonly DotRotation[];
107
+ /** Textured dots: at most this many stamps. */
108
+ export declare const MAX_DOT_TEXTURES = 4;
81
109
  /** Particle engine settings (each 0..1 unless noted). */
82
110
  export interface ParticleSettings {
83
111
  kind: ParticleKind;
@@ -100,17 +128,66 @@ export interface ParticleSettings {
100
128
  life: number;
101
129
  /** Sequence: spacing of the beads along each path. */
102
130
  interval: number;
131
+ /** Ribbon: opacity of the fill between the paths, relative to their edges. */
132
+ ribbonFill: number;
133
+ /** Ribbon: which paths are joined. */
134
+ ribbonPairs: RibbonPairs;
135
+ /** Wave: how far each particle's drawn place swings sideways, as a fraction of the tip size (0: no wave). */
136
+ waveAmplitude: number;
137
+ /** Wave: wavelength in px of pen travel, 4..2000. */
138
+ waveLength: number;
139
+ /** Wave: how fast the waves run (0: they stand still on the path; 1: about four a second while the pen moves). */
140
+ waveSpeed: number;
141
+ /** Wave: how far out of step the particles' waves are (0: all in step; 1: random phases). */
142
+ wavePhaseJitter: number;
143
+ /** Dots and beads: up to four sticker stamps drawn instead of round marks (empty: round). */
144
+ dotTextures: StampKind[];
145
+ dotTextureOrder: DotTextureOrder;
146
+ dotRotation: DotRotation;
103
147
  }
104
148
  export declare const DEFAULT_PARTICLE: Readonly<ParticleSettings>;
105
149
  export declare function normalizeParticle(raw?: Partial<ParticleSettings>, base?: ParticleSettings): ParticleSettings;
150
+ /**
151
+ * Adds the area polygon `pts` (x, y pairs, even-odd) covers in each pixel of `rect`, times `weight`, to `out`,
152
+ * exactly across each of `RIBBON_SUB` sub-rows per row. Points on an edge two polygons share
153
+ * count for exactly one of them, so polygons that tile a region add up to it with no seams.
154
+ */
155
+ export declare function addPolygonArea(pts: readonly number[], rect: RasterRect, out: Float32Array, weight?: number): void;
106
156
  /** A particle-brush stroke. */
107
157
  export declare class ParticleStroke extends MarkStroke {
108
158
  private readonly sets;
159
+ /** Ribbon pairs per copy: particles in their order around the pen (non-flow kinds). */
160
+ private readonly orders;
109
161
  private readonly s;
110
162
  private spawn;
163
+ /** Textured dots drawn so far, per copy (sequence order, random picks). */
164
+ private readonly dots;
165
+ /** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
166
+ private readonly filled;
167
+ private pts;
111
168
  constructor(brush: PaintBrush, options: ProceduralStrokeOptions);
169
+ /** Strand kinds: a chain of masses behind each particle. */
170
+ private get strands();
171
+ private newParticle;
112
172
  private init;
173
+ /** The sideways wave of particle `p` at pen dab `d` (px along the normal to the pen's direction). */
174
+ private wave;
113
175
  protected advance(copy: number, d: PenDab, rects: RasterRect[]): void;
176
+ /** A dot (or bead): round, or a textured stamp turned by the particle's motion (mx, my), at random or to the tip angle. */
177
+ private dot;
178
+ /**
179
+ * Ribbon fill for one step: the band between each pair of joined particles, from where they
180
+ * were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
181
+ * share edges exactly, so the fill has no seams; it is telescoped into the coverage
182
+ * (`telescope`), so it does not build up at the joins.
183
+ */
184
+ private ribbon;
185
+ /**
186
+ * Turns the area newly filled over `rect` into coverage to lay towards the fill's ceiling: the
187
+ * share of what was left unfilled, so after any number of bands (in any grouping) a pixel's
188
+ * coverage is the ceiling times the area filled there. False when nothing was added.
189
+ */
190
+ private telescope;
114
191
  }
115
192
  /** Bristle engine settings (each 0..1 unless noted). */
116
193
  export interface BristleSettings {
@@ -6,8 +6,12 @@
6
6
  * rules. `string`: strands of masses on springs, dragged behind the pen like hair or wet string;
7
7
  * `spring`: each particle tied to the pen by a spring, overshooting and wobbling; `orbit`:
8
8
  * particles circling the pen (spirograph curls); `flow`: particles thrown off the pen and carried
9
- * by a swirling flow field until they fade. Each is drawn as `dot`s, a continuous `line`, or a
10
- * `sequence` of beads spaced along its path.
9
+ * by a swirling flow field until they fade; `elastic`: strands whose masses keep a rest length,
10
+ * stretching when the pen is fast and snapping back; `fuzzy`: strings that wind, each strand
11
+ * waving out of step with the others. Each is drawn as `dot`s (round, or stamped with up to four
12
+ * textures), a continuous `line`, a `sequence` of beads spaced along its path, or a `ribbon`:
13
+ * the area between neighbouring particles' paths filled, with their paths as its edges. Any kind
14
+ * can wave: each particle's drawn place moves sideways on a sine along the pen's path.
11
15
  * - **Bristle brushes** (`engine: 'bristle'`): a tuft of bristles, each its own thin line. Pressure
12
16
  * splays the tuft, tilt flattens it, and bristles lag behind the tip by their stiffness, so they
13
17
  * bend into curves at turns; each bristle carries its own paint and runs dry at its own pace, so
@@ -19,11 +23,12 @@
19
23
  */
20
24
  import { CoverageMap } from '../paint/brush.js';
21
25
  import { type PaintGradientSource } from '../paint/gradient-paint.js';
22
- import type { RasterRect } from '../raster/surface.js';
26
+ import { type RasterRect } from '../raster/surface.js';
23
27
  import type { PaintBrush } from './brushes.js';
24
28
  import { type EffectPaintPixels } from './effect-paint.js';
25
29
  import { PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine.js';
26
30
  import { type PaperGrain } from './paper.js';
31
+ import { type StampKind } from './stickers.js';
27
32
  export interface ProceduralStrokeOptions {
28
33
  grain: PaperGrain | null;
29
34
  paperDepth: number;
@@ -71,13 +76,36 @@ declare abstract class MarkStroke implements CoverageStroke {
71
76
  protected abstract advance(copy: number, d: PenDab, rects: RasterRect[]): void;
72
77
  /** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
73
78
  protected mark(x: number, y: number, r: number, alpha: number, opacity: number, pressure: number, rects: RasterRect[]): void;
79
+ /**
80
+ * Stamps a textured mark: `kind` from the sticker stamps over a square of half-size `r`, turned
81
+ * `rotation` radians clockwise, with flow `alpha` towards `opacity`. Small marks are supersampled.
82
+ */
83
+ protected stamp(x: number, y: number, r: number, kind: StampKind, rotation: number, seed: number, alpha: number, opacity: number, pressure: number, rects: RasterRect[]): void;
84
+ /** A scratch buffer of `n` values (reused; valid until the next mark). */
85
+ protected scratch(n: number): Float32Array;
86
+ /**
87
+ * Lays coverage `vals` over `rect` towards `opacity`: paper grain, then effect paint or the
88
+ * colour of a mark at (x, y) (gradient paint), into the stroke's coverage.
89
+ */
90
+ protected lay(rect: RasterRect, vals: Float32Array, opacity: number, pressure: number, x: number, y: number, rects: RasterRect[]): void;
74
91
  /** Marks along a segment, spaced by a fraction of the radius. */
75
92
  protected segment(x0: number, y0: number, x1: number, y1: number, r: number, alpha: number, opacity: number, pressure: number, rects: RasterRect[]): void;
76
93
  }
77
- export type ParticleKind = 'string' | 'spring' | 'orbit' | 'flow';
94
+ export type ParticleKind = 'string' | 'spring' | 'orbit' | 'flow' | 'elastic' | 'fuzzy';
78
95
  export declare const PARTICLE_KINDS: readonly ParticleKind[];
79
- export type ParticleRender = 'dot' | 'line' | 'sequence';
96
+ export type ParticleRender = 'dot' | 'line' | 'sequence' | 'ribbon';
80
97
  export declare const PARTICLE_RENDERS: readonly ParticleRender[];
98
+ /** Ribbon: which particles' paths are joined (`adjacent`: each to its neighbour; `first-last`: one band between the outermost two). */
99
+ export type RibbonPairs = 'adjacent' | 'first-last';
100
+ export declare const RIBBON_PAIRS: readonly RibbonPairs[];
101
+ /** Textured dots: the stamps in turn, or one picked at random (seeded) per dot. */
102
+ export type DotTextureOrder = 'sequence' | 'random';
103
+ export declare const DOT_TEXTURE_ORDERS: readonly DotTextureOrder[];
104
+ /** Textured dots: turned with the particle's motion, at random, or at the tip angle. */
105
+ export type DotRotation = 'follow' | 'random' | 'fixed';
106
+ export declare const DOT_ROTATIONS: readonly DotRotation[];
107
+ /** Textured dots: at most this many stamps. */
108
+ export declare const MAX_DOT_TEXTURES = 4;
81
109
  /** Particle engine settings (each 0..1 unless noted). */
82
110
  export interface ParticleSettings {
83
111
  kind: ParticleKind;
@@ -100,17 +128,66 @@ export interface ParticleSettings {
100
128
  life: number;
101
129
  /** Sequence: spacing of the beads along each path. */
102
130
  interval: number;
131
+ /** Ribbon: opacity of the fill between the paths, relative to their edges. */
132
+ ribbonFill: number;
133
+ /** Ribbon: which paths are joined. */
134
+ ribbonPairs: RibbonPairs;
135
+ /** Wave: how far each particle's drawn place swings sideways, as a fraction of the tip size (0: no wave). */
136
+ waveAmplitude: number;
137
+ /** Wave: wavelength in px of pen travel, 4..2000. */
138
+ waveLength: number;
139
+ /** Wave: how fast the waves run (0: they stand still on the path; 1: about four a second while the pen moves). */
140
+ waveSpeed: number;
141
+ /** Wave: how far out of step the particles' waves are (0: all in step; 1: random phases). */
142
+ wavePhaseJitter: number;
143
+ /** Dots and beads: up to four sticker stamps drawn instead of round marks (empty: round). */
144
+ dotTextures: StampKind[];
145
+ dotTextureOrder: DotTextureOrder;
146
+ dotRotation: DotRotation;
103
147
  }
104
148
  export declare const DEFAULT_PARTICLE: Readonly<ParticleSettings>;
105
149
  export declare function normalizeParticle(raw?: Partial<ParticleSettings>, base?: ParticleSettings): ParticleSettings;
150
+ /**
151
+ * Adds the area polygon `pts` (x, y pairs, even-odd) covers in each pixel of `rect`, times `weight`, to `out`,
152
+ * exactly across each of `RIBBON_SUB` sub-rows per row. Points on an edge two polygons share
153
+ * count for exactly one of them, so polygons that tile a region add up to it with no seams.
154
+ */
155
+ export declare function addPolygonArea(pts: readonly number[], rect: RasterRect, out: Float32Array, weight?: number): void;
106
156
  /** A particle-brush stroke. */
107
157
  export declare class ParticleStroke extends MarkStroke {
108
158
  private readonly sets;
159
+ /** Ribbon pairs per copy: particles in their order around the pen (non-flow kinds). */
160
+ private readonly orders;
109
161
  private readonly s;
110
162
  private spawn;
163
+ /** Textured dots drawn so far, per copy (sequence order, random picks). */
164
+ private readonly dots;
165
+ /** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
166
+ private readonly filled;
167
+ private pts;
111
168
  constructor(brush: PaintBrush, options: ProceduralStrokeOptions);
169
+ /** Strand kinds: a chain of masses behind each particle. */
170
+ private get strands();
171
+ private newParticle;
112
172
  private init;
173
+ /** The sideways wave of particle `p` at pen dab `d` (px along the normal to the pen's direction). */
174
+ private wave;
113
175
  protected advance(copy: number, d: PenDab, rects: RasterRect[]): void;
176
+ /** A dot (or bead): round, or a textured stamp turned by the particle's motion (mx, my), at random or to the tip angle. */
177
+ private dot;
178
+ /**
179
+ * Ribbon fill for one step: the band between each pair of joined particles, from where they
180
+ * were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
181
+ * share edges exactly, so the fill has no seams; it is telescoped into the coverage
182
+ * (`telescope`), so it does not build up at the joins.
183
+ */
184
+ private ribbon;
185
+ /**
186
+ * Turns the area newly filled over `rect` into coverage to lay towards the fill's ceiling: the
187
+ * share of what was left unfilled, so after any number of bands (in any grouping) a pixel's
188
+ * coverage is the ceiling times the area filled there. False when nothing was added.
189
+ */
190
+ private telescope;
114
191
  }
115
192
  /** Bristle engine settings (each 0..1 unless noted). */
116
193
  export interface BristleSettings {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { AgentToolProvider } from '../tools/registry.cjs';
8
8
  import { type PaintOrigin } from './hosted.cjs';
9
- import type { PaintEditorController } from './controller.cjs';
9
+ import { type PaintEditorController } from './controller.cjs';
10
10
  import type { PaintDirector } from './presence.cjs';
11
11
  import type { PresenceActorInfo } from '../animation/presence.cjs';
12
12
  export interface PaintEditorToolProviderOptions {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { AgentToolProvider } from '../tools/registry.js';
8
8
  import { type PaintOrigin } from './hosted.js';
9
- import type { PaintEditorController } from './controller.js';
9
+ import { type PaintEditorController } from './controller.js';
10
10
  import type { PaintDirector } from './presence.js';
11
11
  import type { PresenceActorInfo } from '../animation/presence.js';
12
12
  export interface PaintEditorToolProviderOptions {