@pixerate/editor 0.93.0 → 0.95.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 (43) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/{chunk-ZOOEX3WT.cjs → chunk-CRMAXIUV.cjs} +369 -7
  3. package/dist/chunk-CRMAXIUV.cjs.map +1 -0
  4. package/dist/chunk-E2ILRYTJ.js +3695 -0
  5. package/dist/chunk-E2ILRYTJ.js.map +1 -0
  6. package/dist/{chunk-D4ZCU72J.js → chunk-HEW6KDNI.js} +366 -4
  7. package/dist/chunk-HEW6KDNI.js.map +1 -0
  8. package/dist/{chunk-ISDCJHTO.js → chunk-IB53LVGV.js} +238 -285
  9. package/dist/chunk-IB53LVGV.js.map +1 -0
  10. package/dist/chunk-T6H3JDGA.cjs +3695 -0
  11. package/dist/chunk-T6H3JDGA.cjs.map +1 -0
  12. package/dist/{chunk-4F3YDIWW.cjs → chunk-ZZOTHJ4K.cjs} +264 -311
  13. package/dist/chunk-ZZOTHJ4K.cjs.map +1 -0
  14. package/dist/image-editor/index.cjs +1252 -588
  15. package/dist/image-editor/index.cjs.map +1 -1
  16. package/dist/image-editor/index.d.cts +164 -34
  17. package/dist/image-editor/index.d.ts +164 -34
  18. package/dist/image-editor/index.js +913 -249
  19. package/dist/image-editor/index.js.map +1 -1
  20. package/dist/paint/index.cjs +22 -3
  21. package/dist/paint/index.cjs.map +1 -1
  22. package/dist/paint/index.d.cts +117 -2
  23. package/dist/paint/index.d.ts +117 -2
  24. package/dist/paint/index.js +32 -13
  25. package/dist/raster/index.cjs +83 -3
  26. package/dist/raster/index.cjs.map +1 -1
  27. package/dist/raster/index.d.cts +99 -348
  28. package/dist/raster/index.d.ts +99 -348
  29. package/dist/raster/index.js +83 -3
  30. package/dist/{surface-fJhkmNQn.d.cts → surface-CgAiXVI3.d.cts} +23 -11
  31. package/dist/{surface-fJhkmNQn.d.ts → surface-CgAiXVI3.d.ts} +23 -11
  32. package/dist/zlib-B3LIoxF-.d.cts +796 -0
  33. package/dist/zlib-eiTEEj1z.d.ts +796 -0
  34. package/package.json +1 -1
  35. package/skills/pixerate-image-editor/SKILL.md +82 -7
  36. package/dist/chunk-4F3YDIWW.cjs.map +0 -1
  37. package/dist/chunk-D4ZCU72J.js.map +0 -1
  38. package/dist/chunk-H543GD6T.cjs +0 -1696
  39. package/dist/chunk-H543GD6T.cjs.map +0 -1
  40. package/dist/chunk-ISDCJHTO.js.map +0 -1
  41. package/dist/chunk-P5TPUNBZ.js +0 -1696
  42. package/dist/chunk-P5TPUNBZ.js.map +0 -1
  43. package/dist/chunk-ZOOEX3WT.cjs.map +0 -1
@@ -5,17 +5,25 @@ import {
5
5
  createCaptureTool
6
6
  } from "../chunk-REOJJIY3.js";
7
7
  import {
8
+ DEFAULT_GLOBAL_LIGHT,
8
9
  DIRECT_GAUSSIAN_MAX_SIGMA,
10
+ LAYER_EFFECT_KINDS,
11
+ LayerEffectError,
9
12
  RasterCompositor,
10
13
  ZlibError,
11
14
  adler32,
12
15
  base64ToBytes,
13
16
  blendTile,
17
+ builtinPattern,
18
+ builtinPatternIds,
14
19
  bytesToBase64,
15
20
  changedTiles,
21
+ curveLut,
16
22
  decodePng,
23
+ defaultLayerEffect,
17
24
  deflate,
18
25
  deflateRaw,
26
+ effectsMargin,
19
27
  encodePng,
20
28
  findLayer,
21
29
  flattenLayers,
@@ -23,44 +31,52 @@ import {
23
31
  inflate,
24
32
  inflateRaw,
25
33
  insertLayer,
34
+ isLayerEffectKind,
26
35
  isLayerLocked,
27
36
  isPng,
37
+ layerEffectLabel,
28
38
  mapLayer,
39
+ mergeLayerEffect,
40
+ newEffectId,
41
+ normalizeLayerEffects,
29
42
  parseDataUrl,
30
- pngDataUrl
31
- } from "../chunk-P5TPUNBZ.js";
43
+ pngDataUrl,
44
+ validateLayerEffects
45
+ } from "../chunk-E2ILRYTJ.js";
32
46
  import {
33
47
  CoverageMap,
34
48
  DEFAULT_BRUSH,
49
+ DEFAULT_MIXER,
35
50
  DabWalker,
51
+ EMPTY_MIXER_LOAD,
36
52
  MAX_BRUSH_SIZE,
37
- PIGMENT_BANDS,
38
- PIGMENT_LAMBDA_MIN,
39
- PIGMENT_LAMBDA_STEP,
40
- PIGMENT_WAVELENGTHS,
53
+ MIXER_DRAIN,
54
+ MIXER_PICKUP_RATE,
55
+ MIXING_MODELS,
56
+ MixerStroke,
41
57
  StrokeCoverage,
42
58
  applyStrokeToTile,
59
+ cleanMixer,
43
60
  coverageTileCoord,
44
- createPigment,
45
61
  createPigmentTools,
46
62
  dabCoverage,
47
63
  dabRect,
48
- lerpPigment,
49
- lerpPigmentInto,
50
- mixPigments,
64
+ loadMixer,
51
65
  normalizeBrush,
66
+ normalizeMixer,
52
67
  rasterizeDab,
53
- reflectanceToRgb,
54
- rgbToReflectance,
55
- setPigmentColor,
56
68
  smoothPoints,
57
69
  tipFalloff,
58
70
  tipReach,
59
71
  tipShape,
60
72
  walkDabs
61
- } from "../chunk-ISDCJHTO.js";
73
+ } from "../chunk-IB53LVGV.js";
62
74
  import {
63
75
  BLEND_MODES,
76
+ PIGMENT_BANDS,
77
+ PIGMENT_LAMBDA_MIN,
78
+ PIGMENT_LAMBDA_STEP,
79
+ PIGMENT_WAVELENGTHS,
64
80
  SurfaceDraft,
65
81
  TILE,
66
82
  TILE_SHIFT,
@@ -73,16 +89,25 @@ import {
73
89
  canvasCompositeOperation,
74
90
  collectTiles,
75
91
  contentBounds,
92
+ createPigment,
76
93
  emptySurface,
77
94
  fillRect,
78
95
  finishTile,
79
96
  isBlendMode,
80
97
  isSeparableBlendMode,
98
+ lerpPigment,
99
+ lerpPigmentInto,
100
+ mixPigment8,
101
+ mixPigmentRgb,
102
+ mixPigments,
81
103
  normalizeBlendMode,
82
104
  packRgba,
83
105
  readPixel,
84
106
  readRegion,
107
+ reflectanceToRgb,
108
+ rgbToReflectance,
85
109
  setLum,
110
+ setPigmentColor,
86
111
  solidSurface,
87
112
  surfaceBytes,
88
113
  surfaceFromPixels,
@@ -94,7 +119,7 @@ import {
94
119
  translateSurface,
95
120
  uniformTile,
96
121
  writeRegion
97
- } from "../chunk-D4ZCU72J.js";
122
+ } from "../chunk-HEW6KDNI.js";
98
123
 
99
124
  // src/image-editor/color.ts
100
125
  var LEGACY_PROCESS_VERSION = 1;
@@ -1496,59 +1521,6 @@ function normalizeAdjustmentLayers(raw) {
1496
1521
  }
1497
1522
  return out;
1498
1523
  }
1499
- function curveLut(points, size = ADJUSTMENT_LUT_SIZE) {
1500
- const pts = points.filter((p) => Number.isFinite(p.input) && Number.isFinite(p.output)).map((p) => [p.input, p.output]).sort((a, b) => a[0] - b[0]);
1501
- const dedup = [];
1502
- for (const p of pts) {
1503
- if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;
1504
- dedup.push(p);
1505
- }
1506
- const out = new Float32Array(size);
1507
- const n = dedup.length;
1508
- if (n < 2) {
1509
- for (let k = 0; k < size; k++) out[k] = k / (size - 1);
1510
- return out;
1511
- }
1512
- const x = dedup.map((p) => p[0]);
1513
- const y = dedup.map((p) => p[1]);
1514
- const m2 = new Float64Array(n);
1515
- if (n > 2) {
1516
- const c = new Float64Array(n);
1517
- const d = new Float64Array(n);
1518
- for (let i = 1; i < n - 1; i++) {
1519
- const h0 = x[i] - x[i - 1];
1520
- const h1 = x[i + 1] - x[i];
1521
- const a = h0 / 6;
1522
- const b = (h0 + h1) / 3;
1523
- const cc = h1 / 6;
1524
- const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;
1525
- const denom = b - a * c[i - 1];
1526
- c[i] = cc / denom;
1527
- d[i] = (r - a * d[i - 1]) / denom;
1528
- }
1529
- for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];
1530
- }
1531
- let seg = 0;
1532
- for (let k = 0; k < size; k++) {
1533
- const xv = k / (size - 1);
1534
- if (xv <= x[0]) {
1535
- out[k] = clamp01(y[0]);
1536
- continue;
1537
- }
1538
- if (xv >= x[n - 1]) {
1539
- out[k] = clamp01(y[n - 1]);
1540
- continue;
1541
- }
1542
- while (seg < n - 2 && xv > x[seg + 1]) seg++;
1543
- const x0 = x[seg], x1 = x[seg + 1];
1544
- const h = x1 - x0;
1545
- const a = (x1 - xv) / h;
1546
- const b = (xv - x0) / h;
1547
- const v = a * y[seg] + b * y[seg + 1] + ((a * a * a - a) * m2[seg] + (b * b * b - b) * m2[seg + 1]) * (h * h) / 6;
1548
- out[k] = clamp01(v);
1549
- }
1550
- return out;
1551
- }
1552
1524
  function curveLutFromLevels(points) {
1553
1525
  return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));
1554
1526
  }
@@ -1958,6 +1930,10 @@ function applyLayer(c, layer, op, coverage = 1) {
1958
1930
  }
1959
1931
  const cb0 = c[0], cb1 = c[1], cb2 = c[2];
1960
1932
  op(c);
1933
+ if (blend === "pigment") {
1934
+ mixPigmentRgb([clamp01(cb0), clamp01(cb1), clamp01(cb2)], [clamp01(c[0]), clamp01(c[1]), clamp01(c[2])], alpha, c);
1935
+ return;
1936
+ }
1961
1937
  const b = blend === "normal" ? c : blendRgb(blend, [cb0, cb1, cb2], c);
1962
1938
  c[0] = (1 - alpha) * cb0 + alpha * b[0];
1963
1939
  c[1] = (1 - alpha) * cb1 + alpha * b[1];
@@ -2700,7 +2676,7 @@ function developTarget(layers, activeLayerId) {
2700
2676
  }
2701
2677
  return pixels.find((l) => l.source === "document") ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;
2702
2678
  }
2703
- var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks;
2679
+ var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks && !l.effects;
2704
2680
  function legacyView(stack, width, height, activeLayerId = null) {
2705
2681
  const { layers } = stack;
2706
2682
  const target = developTarget(layers, activeLayerId);
@@ -2740,7 +2716,7 @@ function isLegacyShape(stack) {
2740
2716
  const bg = layers[0];
2741
2717
  if (!bg || bg.type !== "pixel" || bg.source !== "document") return false;
2742
2718
  if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;
2743
- if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks) return false;
2719
+ if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks || bg.effects) return false;
2744
2720
  if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;
2745
2721
  const filters = bg.filters ?? [];
2746
2722
  if (filters.length < 1 || filters.length > 2) return false;
@@ -2756,7 +2732,7 @@ function isLegacyShape(stack) {
2756
2732
  let adjustments = 0;
2757
2733
  for (; i < layers.length && layers[i].type === "adjustment"; i++) {
2758
2734
  const l = layers[i];
2759
- if (l.clipped || l.locks || l.mask?.source) return false;
2735
+ if (l.clipped || l.locks || l.mask?.source || l.effects) return false;
2760
2736
  adjustments++;
2761
2737
  }
2762
2738
  for (; i < layers.length; i++) {
@@ -3262,7 +3238,9 @@ var ImageEditorRenderer = class {
3262
3238
  return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
3263
3239
  },
3264
3240
  rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),
3265
- version: `${host?.version() ?? ""}:${this.assetVersion}`
3241
+ version: `${host?.version() ?? ""}:${this.assetVersion}`,
3242
+ globalLight: host?.globalLight?.(),
3243
+ pattern: host?.pattern ? (id) => host.pattern(id) : void 0
3266
3244
  };
3267
3245
  }
3268
3246
  /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */
@@ -4535,8 +4513,29 @@ function baseFields(layer, ctx) {
4535
4513
  if (layer.clipped) out.clipped = true;
4536
4514
  if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
4537
4515
  if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
4516
+ if (layer.effects) out.effects = serializeEffects(layer.effects);
4538
4517
  return out;
4539
4518
  }
4519
+ function serializeEffects(effects) {
4520
+ const out = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };
4521
+ if (effects.scale !== void 0 && effects.scale !== 1) out.scale = effects.scale;
4522
+ if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };
4523
+ return out;
4524
+ }
4525
+ function deserializeEffects(raw) {
4526
+ if (!raw || typeof raw !== "object") return void 0;
4527
+ const r = raw;
4528
+ let psd;
4529
+ const p = r.psd;
4530
+ if (p && typeof p.key === "string" && typeof p.data === "string") {
4531
+ try {
4532
+ psd = { key: p.key, data: base64ToBytes(p.data) };
4533
+ } catch {
4534
+ psd = void 0;
4535
+ }
4536
+ }
4537
+ return normalizeLayerEffects({ ...r, psd });
4538
+ }
4540
4539
  function serializeMask(mask, hint, ctx) {
4541
4540
  const out = { defaultValue: mask.defaultValue };
4542
4541
  if (!mask.source) {
@@ -4739,6 +4738,8 @@ function deserializeLayer(raw, ctx) {
4739
4738
  for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
4740
4739
  if (Object.keys(locks).length) base.locks = locks;
4741
4740
  }
4741
+ const effects = deserializeEffects(r.effects);
4742
+ if (effects && r.type !== "adjustment") base.effects = effects;
4742
4743
  switch (r.type) {
4743
4744
  case "pixel": {
4744
4745
  const layer = {
@@ -5021,6 +5022,77 @@ function removeLayerFilter(layers, id, filterId) {
5021
5022
  else delete next.filters;
5022
5023
  return mapLayer(layers, id, () => next);
5023
5024
  }
5025
+ function requireEffectLayer(layers, id, action) {
5026
+ const layer = requireLayer(layers, id).layer;
5027
+ if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
5028
+ requireUnlocked(layer, "all", action);
5029
+ return layer;
5030
+ }
5031
+ function layerEffectsOf(layer) {
5032
+ return layer.effects ?? { enabled: true, items: [] };
5033
+ }
5034
+ function guard(fn) {
5035
+ try {
5036
+ return fn();
5037
+ } catch (e) {
5038
+ if (e instanceof LayerEffectError) fail(e.message);
5039
+ throw e;
5040
+ }
5041
+ }
5042
+ function setLayerEffects(layers, id, effects) {
5043
+ const layer = requireEffectLayer(layers, id, "change its effects");
5044
+ const next = { ...layer };
5045
+ if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
5046
+ guard(() => validateLayerEffects(effects));
5047
+ next.effects = effects;
5048
+ } else delete next.effects;
5049
+ return mapLayer(layers, id, () => next);
5050
+ }
5051
+ function addLayerEffect(layers, id, kind, params = {}, index) {
5052
+ if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
5053
+ const layer = requireEffectLayer(layers, id, "add effects");
5054
+ const current = layerEffectsOf(layer);
5055
+ const ids = new Set(current.items.map((e) => e.id));
5056
+ let effectId = newEffectId();
5057
+ while (ids.has(effectId)) effectId = newEffectId();
5058
+ const { id: _ignored, kind: _kind, ...rest } = params;
5059
+ const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
5060
+ const items = [...current.items];
5061
+ const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
5062
+ items.splice(at, 0, effect);
5063
+ return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
5064
+ }
5065
+ function requireEffect(layer, effectId) {
5066
+ const items = layer.effects?.items ?? [];
5067
+ const i = items.findIndex((e) => e.id === effectId);
5068
+ if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
5069
+ return i;
5070
+ }
5071
+ function updateLayerEffect(layers, id, effectId, changes) {
5072
+ const layer = requireEffectLayer(layers, id, "change its effects");
5073
+ const i = requireEffect(layer, effectId);
5074
+ const current = layer.effects;
5075
+ const items = [...current.items];
5076
+ items[i] = guard(() => mergeLayerEffect(items[i], changes));
5077
+ return setLayerEffects(layers, id, { ...current, items });
5078
+ }
5079
+ function removeLayerEffect(layers, id, effectId) {
5080
+ const layer = requireEffectLayer(layers, id, "change its effects");
5081
+ requireEffect(layer, effectId);
5082
+ const current = layer.effects;
5083
+ return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
5084
+ }
5085
+ function setLayerEffectsEnabled(layers, id, enabled, scale) {
5086
+ const layer = requireEffectLayer(layers, id, "change its effects");
5087
+ if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
5088
+ const current = layerEffectsOf(layer);
5089
+ const next = { ...current, enabled };
5090
+ if (scale !== void 0) {
5091
+ if (scale === 1) delete next.scale;
5092
+ else next.scale = scale;
5093
+ }
5094
+ return setLayerEffects(layers, id, next);
5095
+ }
5024
5096
  function parseBlend(value, allowPassThrough = false) {
5025
5097
  if (value === void 0) return void 0;
5026
5098
  if (value === "pass-through" && allowPassThrough) return value;
@@ -7127,22 +7199,53 @@ var PaintStrokeSession = class {
7127
7199
  constructor(controller, target, stroke, apply) {
7128
7200
  this.controller = controller;
7129
7201
  this.target = target;
7130
- this.stroke = stroke;
7131
7202
  this.apply = apply;
7132
7203
  this.before = controller._captureForStroke();
7133
7204
  this.base = target.surface;
7134
7205
  this.live = new Map(this.base.tiles);
7206
+ if (stroke instanceof StrokeCoverage) {
7207
+ this.stroke = stroke;
7208
+ this.mixer = null;
7209
+ } else {
7210
+ this.stroke = null;
7211
+ this.mixer = stroke.create(this.mixerPixels());
7212
+ this.mixerInit = stroke;
7213
+ }
7135
7214
  }
7136
7215
  controller;
7137
7216
  target;
7138
- stroke;
7139
7217
  apply;
7140
7218
  before;
7141
7219
  base;
7142
7220
  live;
7143
7221
  touched = /* @__PURE__ */ new Set();
7222
+ stroke;
7223
+ mixer;
7224
+ mixerInit = null;
7225
+ /** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */
7226
+ fresh = /* @__PURE__ */ new Set();
7144
7227
  last = null;
7145
7228
  closed = false;
7229
+ /** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */
7230
+ mixerPixels() {
7231
+ const a = this.apply;
7232
+ return {
7233
+ offset: a.offset,
7234
+ lockTransparency: a.lockTransparency,
7235
+ selection: a.selection,
7236
+ readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? (this.base.defaultValue ? defaultTile(this.base) : null),
7237
+ writeTile: (tx, ty) => {
7238
+ const key = tileKey(tx, ty);
7239
+ if (!this.touched.has(key)) {
7240
+ const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
7241
+ this.live.set(key, { data: new Uint8ClampedArray(src), uniform: null });
7242
+ this.touched.add(key);
7243
+ }
7244
+ this.fresh.add(key);
7245
+ return this.live.get(key).data;
7246
+ }
7247
+ };
7248
+ }
7146
7249
  get layerId() {
7147
7250
  return this.target.id;
7148
7251
  }
@@ -7150,11 +7253,18 @@ var PaintStrokeSession = class {
7150
7253
  push(points) {
7151
7254
  if (this.closed || !points.length) return;
7152
7255
  this.last = points[points.length - 1];
7153
- this.update(this.stroke.push(points));
7256
+ this.update(this.mixer ? this.mixer.push(points) : this.stroke.push(points));
7154
7257
  }
7155
7258
  update(rects) {
7156
7259
  if (!rects.length) return;
7157
7260
  const ch = this.base.channels;
7261
+ if (this.mixer) {
7262
+ for (const key of this.fresh) this.live.set(key, { data: this.live.get(key).data, uniform: null });
7263
+ this.fresh.clear();
7264
+ this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
7265
+ return;
7266
+ }
7267
+ const stroke = this.stroke;
7158
7268
  const regions = /* @__PURE__ */ new Map();
7159
7269
  for (const r of rects) {
7160
7270
  for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {
@@ -7167,7 +7277,7 @@ var PaintStrokeSession = class {
7167
7277
  const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
7168
7278
  const prev = this.touched.has(key) ? this.live.get(key)?.data : void 0;
7169
7279
  this.touched.add(key);
7170
- const data = applyStrokeToTile(this.stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});
7280
+ const data = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});
7171
7281
  this.live.set(key, { data, uniform: null });
7172
7282
  }
7173
7283
  this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
@@ -7175,7 +7285,8 @@ var PaintStrokeSession = class {
7175
7285
  /** Finishes the stroke (one undo step) and returns what changed. */
7176
7286
  end() {
7177
7287
  if (this.closed) throw new LayerOperationError("This stroke has already ended.");
7178
- this.update(this.stroke.finish(this.last ?? void 0));
7288
+ const last = this.last ?? void 0;
7289
+ this.update(this.mixer ? this.mixer.finish(last) : this.stroke.finish(last));
7179
7290
  this.closed = true;
7180
7291
  const ch = this.base.channels;
7181
7292
  const tiles = new Map(this.base.tiles);
@@ -7189,9 +7300,12 @@ var PaintStrokeSession = class {
7189
7300
  if (!sameTile(prev, tile)) changed = true;
7190
7301
  }
7191
7302
  const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
7303
+ const mixerLoad = this.mixer ? this.mixerInit.end(this.mixer.load) : void 0;
7192
7304
  this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);
7193
- const b = this.stroke.coverage.bounds;
7194
- return { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: this.stroke.dabCount };
7305
+ const b = this.mixer ? this.mixer.bounds : this.stroke.coverage.bounds;
7306
+ const result = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke).dabCount };
7307
+ if (mixerLoad) result.mixerLoad = mixerLoad;
7308
+ return result;
7195
7309
  }
7196
7310
  /** Abandons the stroke and restores the layer. */
7197
7311
  cancel() {
@@ -7305,7 +7419,8 @@ var ImageEditorController = class {
7305
7419
  this.renderer.layerSources = {
7306
7420
  surfaceOf: (layer) => this.surfaceOf(layer),
7307
7421
  sourceIsOpaque: () => this.sourceIsOpaque(),
7308
- version: () => this.sourceVersion()
7422
+ version: () => this.sourceVersion(),
7423
+ globalLight: () => this.state.globalLight
7309
7424
  };
7310
7425
  this.state = {
7311
7426
  sourceUrl: typeof options.image === "string" ? options.image : null,
@@ -7326,7 +7441,7 @@ var ImageEditorController = class {
7326
7441
  activeLayerId: null,
7327
7442
  activeTarget: "pixels",
7328
7443
  outputOpacity: 1,
7329
- paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff" },
7444
+ paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff", mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },
7330
7445
  fit: options.fit ?? "margin"
7331
7446
  };
7332
7447
  if (options.image instanceof Object && "width" in options.image) {
@@ -7459,6 +7574,7 @@ var ImageEditorController = class {
7459
7574
  }
7460
7575
  };
7461
7576
  if (s.legacyOutputBlur !== void 0) entry.legacyOutputBlur = s.legacyOutputBlur;
7577
+ if (s.globalLight) entry.globalLight = { ...s.globalLight };
7462
7578
  return entry;
7463
7579
  }
7464
7580
  restoreHistoryEntry(entry) {
@@ -7471,6 +7587,8 @@ var ImageEditorController = class {
7471
7587
  s.outputOpacity = entry.outputOpacity;
7472
7588
  if (entry.legacyOutputBlur !== void 0) s.legacyOutputBlur = entry.legacyOutputBlur;
7473
7589
  else delete s.legacyOutputBlur;
7590
+ if (entry.globalLight) s.globalLight = { ...entry.globalLight };
7591
+ else delete s.globalLight;
7474
7592
  s.selection = entry.selection;
7475
7593
  if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
7476
7594
  this.imageElement = entry.source.element;
@@ -7692,7 +7810,7 @@ var ImageEditorController = class {
7692
7810
  }
7693
7811
  compositorSources() {
7694
7812
  const { width, height } = this.naturalSize;
7695
- return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion() };
7813
+ return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion(), globalLight: this.state.globalLight };
7696
7814
  }
7697
7815
  sourceIds = /* @__PURE__ */ new WeakMap();
7698
7816
  nextSourceId = 1;
@@ -8104,6 +8222,55 @@ var ImageEditorController = class {
8104
8222
  this.commitLayers(next);
8105
8223
  return findLayer(next, id).layer.filters ?? [];
8106
8224
  }
8225
+ // --- Layer effects ---
8226
+ /** A layer's effects (an empty, enabled list when it has none). */
8227
+ getLayerEffects(id) {
8228
+ return layerEffectsOf(requireLayer(this.state.layers, id).layer);
8229
+ }
8230
+ /**
8231
+ * Adds an effect of `kind` with Photoshop's defaults and `params` applied (one undo step), at
8232
+ * `index` in the layer's effect list (default the end). Returns the new effect and the effects.
8233
+ */
8234
+ addLayerEffect(id, kind, params = {}, index) {
8235
+ const res = addLayerEffect(this.state.layers, id, kind, params, index);
8236
+ this.commitLayers(res.layers);
8237
+ return { effect: res.effect, effects: this.getLayerEffects(id) };
8238
+ }
8239
+ /** Changes one effect (one undo step, or none with `commitToHistory = false` during a drag). Returns the effects. */
8240
+ updateLayerEffect(id, effectId, changes, commitToHistory = true) {
8241
+ this.commitLayers(updateLayerEffect(this.state.layers, id, effectId, changes), void 0, commitToHistory);
8242
+ return this.getLayerEffects(id);
8243
+ }
8244
+ /** Removes one effect (one undo step). Returns the remaining effects. */
8245
+ removeLayerEffect(id, effectId) {
8246
+ this.commitLayers(removeLayerEffect(this.state.layers, id, effectId));
8247
+ return this.getLayerEffects(id);
8248
+ }
8249
+ /** Turns a layer's effects on or off as a whole and, when given, sets Scale Effects (one undo step). */
8250
+ setLayerEffectsEnabled(id, enabled, scale, commitToHistory = true) {
8251
+ this.commitLayers(setLayerEffectsEnabled(this.state.layers, id, enabled, scale), void 0, commitToHistory);
8252
+ return this.getLayerEffects(id);
8253
+ }
8254
+ /** Replaces a layer's effects (one undo step; `null` removes them). Returns the effects. */
8255
+ setLayerEffects(id, effects, commitToHistory = true) {
8256
+ this.commitLayers(setLayerEffects(this.state.layers, id, effects), void 0, commitToHistory);
8257
+ return this.getLayerEffects(id);
8258
+ }
8259
+ /** The light effects with "use global light" follow. */
8260
+ getGlobalLight() {
8261
+ return { ...this.state.globalLight ?? DEFAULT_GLOBAL_LIGHT };
8262
+ }
8263
+ /** Sets the global light (one undo step). `angle` -360..360°, `altitude` 0..90°. Returns it. */
8264
+ setGlobalLight(light, commitToHistory = true) {
8265
+ const next = { ...this.getGlobalLight(), ...light };
8266
+ if (!(Number.isFinite(next.angle) && next.angle >= -360 && next.angle <= 360)) throw new LayerOperationError(`angle must be from -360 to 360 degrees, got ${next.angle}.`);
8267
+ if (!(Number.isFinite(next.altitude) && next.altitude >= 0 && next.altitude <= 90)) throw new LayerOperationError(`altitude must be from 0 to 90 degrees, got ${next.altitude}.`);
8268
+ if (commitToHistory) this.recordHistory();
8269
+ if (next.angle === DEFAULT_GLOBAL_LIGHT.angle && next.altitude === DEFAULT_GLOBAL_LIGHT.altitude) delete this.state.globalLight;
8270
+ else this.state.globalLight = next;
8271
+ this.notify();
8272
+ return this.getGlobalLight();
8273
+ }
8107
8274
  /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
8108
8275
  async compositeToSurface(layers, rect) {
8109
8276
  const { width, height } = this.naturalSize;
@@ -8133,7 +8300,8 @@ var ImageEditorController = class {
8133
8300
  }
8134
8301
  /**
8135
8302
  * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,
8136
- * opacity and blend; masks, fill and filters are baked), as one undo step. Returns the merged id.
8303
+ * opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.
8304
+ * Returns the merged id.
8137
8305
  */
8138
8306
  async mergeDown(id) {
8139
8307
  const loc = requireLayer(this.state.layers, id);
@@ -8145,7 +8313,9 @@ var ImageEditorController = class {
8145
8313
  if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
8146
8314
  requireUnlocked(upper, "all", "merge it");
8147
8315
  requireUnlocked(lower, "pixels", "merge into it");
8148
- const rect = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8316
+ let rect = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8317
+ const reach = Math.max(effectsMargin(lower.effects), upper.type === "adjustment" ? 0 : effectsMargin(upper.effects));
8318
+ if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };
8149
8319
  const base = { ...lower, blend: "normal", opacity: 1 };
8150
8320
  delete base.clipped;
8151
8321
  const surface = await this.compositeToSurface([base, upper], rect);
@@ -8154,6 +8324,7 @@ var ImageEditorController = class {
8154
8324
  delete merged.fillOpacity;
8155
8325
  delete merged.filters;
8156
8326
  delete merged.source;
8327
+ delete merged.effects;
8157
8328
  let next = mapLayer(this.state.layers, id, () => null);
8158
8329
  next = mapLayer(next, lower.id, () => merged);
8159
8330
  this.commitLayers(next, () => {
@@ -8261,12 +8432,38 @@ var ImageEditorController = class {
8261
8432
  const next = {
8262
8433
  brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,
8263
8434
  foreground: options.foreground !== void 0 ? toHex2(parseColor(options.foreground)) : cur.foreground,
8264
- background: options.background !== void 0 ? toHex2(parseColor(options.background)) : cur.background
8435
+ background: options.background !== void 0 ? toHex2(parseColor(options.background)) : cur.background,
8436
+ mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,
8437
+ mixerLoad: cur.mixerLoad
8265
8438
  };
8266
8439
  this.state.paint = next;
8267
8440
  this.notify();
8268
8441
  return next;
8269
8442
  }
8443
+ /**
8444
+ * Loads the Mixer Brush's reservoir full with `color` (default the foreground); the picked-up
8445
+ * colour is kept. View state, no undo. Returns the brush load.
8446
+ */
8447
+ loadMixerBrush(color) {
8448
+ const c = parseColor(color ?? this.state.paint.foreground);
8449
+ const load = loadMixer(this.state.paint.mixerLoad, [c[0] / 255, c[1] / 255, c[2] / 255]);
8450
+ this.state.paint = { ...this.state.paint, mixerLoad: load };
8451
+ this.notify();
8452
+ return load;
8453
+ }
8454
+ /** Cleans the Mixer Brush: empties the pickup well (the reservoir is kept). Returns the brush load. */
8455
+ cleanMixerBrush() {
8456
+ const load = cleanMixer(this.state.paint.mixerLoad);
8457
+ this.state.paint = { ...this.state.paint, mixerLoad: load };
8458
+ this.notify();
8459
+ return load;
8460
+ }
8461
+ /** Empties the Mixer Brush entirely (no reservoir, nothing picked up). Returns the brush load. */
8462
+ emptyMixerBrush() {
8463
+ this.state.paint = { ...this.state.paint, mixerLoad: EMPTY_MIXER_LOAD };
8464
+ this.notify();
8465
+ return EMPTY_MIXER_LOAD;
8466
+ }
8270
8467
  /** D: black foreground, white background. */
8271
8468
  resetPaintColors() {
8272
8469
  this.setPaintOptions({ foreground: "#000000", background: "#ffffff" });
@@ -8311,7 +8508,11 @@ var ImageEditorController = class {
8311
8508
  */
8312
8509
  beginStroke(init = {}) {
8313
8510
  const tool = init.tool ?? "brush";
8511
+ if (tool !== "brush" && tool !== "eraser" && tool !== "mixer") throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use brush, eraser or mixer.`);
8314
8512
  const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : "paint");
8513
+ if (tool === "mixer" && target.channel === "mask") {
8514
+ throw new LayerOperationError("The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.");
8515
+ }
8315
8516
  const brush = normalizeBrush({ ...this.state.paint.brush, ...init.brush ?? {} });
8316
8517
  const color = parseColor(init.color ?? this.state.paint.foreground);
8317
8518
  const background = parseColor(init.background ?? this.state.paint.background);
@@ -8329,8 +8530,26 @@ var ImageEditorController = class {
8329
8530
  selection: this.state.selection?.bounds ? this.state.selection : null,
8330
8531
  offset: target.offset
8331
8532
  };
8533
+ if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);
8332
8534
  return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);
8333
8535
  }
8536
+ /** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */
8537
+ mixerStroke(init, brush) {
8538
+ const settings = normalizeMixer({ ...this.state.paint.mixer, ...init.mixer ?? {} });
8539
+ let load = this.state.paint.mixerLoad;
8540
+ if (init.color !== void 0 || settings.loadEachStroke) {
8541
+ const c = parseColor(init.color ?? this.state.paint.foreground);
8542
+ load = loadMixer(load, [c[0] / 255, c[1] / 255, c[2] / 255]);
8543
+ }
8544
+ return {
8545
+ create: (pixels) => new MixerStroke(brush, settings, load, pixels),
8546
+ end: (after) => {
8547
+ const next = settings.cleanEachStroke ? cleanMixer(after) : after;
8548
+ this.state.paint = { ...this.state.paint, mixerLoad: next };
8549
+ return next;
8550
+ }
8551
+ };
8552
+ }
8334
8553
  /** @internal Shows a stroke's live surface (no history). */
8335
8554
  _strokeUpdate(layerId, channel, surface, rects) {
8336
8555
  this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
@@ -8354,7 +8573,7 @@ var ImageEditorController = class {
8354
8573
  }
8355
8574
  /**
8356
8575
  * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional
8357
- * pressure, through the brush or eraser. One undo step. Returns the layer, channel, the changed
8576
+ * pressure, through the brush, eraser or Mixer Brush. One undo step. Returns the layer, channel, the changed
8358
8577
  * document bounds and the number of dabs.
8359
8578
  */
8360
8579
  paintStroke(init) {
@@ -9363,6 +9582,11 @@ var ImageEditorController = class {
9363
9582
  return baked;
9364
9583
  };
9365
9584
  const prepare = async (layer) => {
9585
+ const p = await prepareLayer(layer);
9586
+ if (p?.blend === "pigment") add("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
9587
+ return p;
9588
+ };
9589
+ const prepareLayer = async (layer) => {
9366
9590
  if (layer.type === "group") {
9367
9591
  const children = [];
9368
9592
  for (const c of layer.children) {
@@ -9534,6 +9758,9 @@ var ImageEditorController = class {
9534
9758
  if (typeof data.legacyOutputBlur === "number" && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;
9535
9759
  else delete s.legacyOutputBlur;
9536
9760
  s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;
9761
+ const light = data.globalLight;
9762
+ if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };
9763
+ else delete s.globalLight;
9537
9764
  if (!this.imageElement && typeof data.width === "number" && typeof data.height === "number" && data.width > 0 && data.height > 0) {
9538
9765
  s.imageDimensions = { width: data.width, height: data.height };
9539
9766
  }
@@ -9547,6 +9774,7 @@ var ImageEditorController = class {
9547
9774
  s.crop = data.crop ? { ...data.crop } : null;
9548
9775
  s.transform = { ...data.transform };
9549
9776
  s.activeLayerId = null;
9777
+ delete s.globalLight;
9550
9778
  }
9551
9779
  s.activeTarget = "pixels";
9552
9780
  this.viewKey = [];
@@ -9556,7 +9784,7 @@ var ImageEditorController = class {
9556
9784
  serializeState() {
9557
9785
  const { width, height } = this.naturalSize;
9558
9786
  const view = legacyView(this.stack(), width, height, null);
9559
- if (view.exact) {
9787
+ if (view.exact && !this.state.globalLight) {
9560
9788
  return {
9561
9789
  // States without adjustment layers keep schema 1, so they round-trip unchanged.
9562
9790
  version: view.adjustments.layers?.length ? 2 : 1,
@@ -9583,6 +9811,7 @@ var ImageEditorController = class {
9583
9811
  opacity: this.state.outputOpacity
9584
9812
  };
9585
9813
  if (this.state.legacyOutputBlur !== void 0) out.legacyOutputBlur = this.state.legacyOutputBlur;
9814
+ if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };
9586
9815
  return out;
9587
9816
  }
9588
9817
  };
@@ -10820,6 +11049,7 @@ function describeLayer(layer, activeId, boundsOf) {
10820
11049
  clipped: Boolean(layer.clipped),
10821
11050
  locks: layer.locks ? { ...layer.locks } : {},
10822
11051
  mask: maskSummary(layer.mask),
11052
+ effects: layer.effects ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) } : null,
10823
11053
  active: layer.id === activeId
10824
11054
  };
10825
11055
  switch (layer.type) {
@@ -10879,7 +11109,7 @@ function createImageLayerTools(controller) {
10879
11109
  const listLayers = {
10880
11110
  name: "list_layers",
10881
11111
  title: "List image layers",
10882
- description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
11112
+ description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
10883
11113
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
10884
11114
  annotations: { title: "List image layers", ...READ2 },
10885
11115
  handler: () => {
@@ -11315,9 +11545,491 @@ function createImageLayerTools(controller) {
11315
11545
  ];
11316
11546
  }
11317
11547
 
11318
- // src/image-editor/paint-tools.ts
11548
+ // src/image-editor/effect-tools.ts
11549
+ var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11319
11550
  var WRITE3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
11551
+ var WRITE_IDEMPOTENT3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11320
11552
  var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11553
+ var LAYER_ID3 = { type: "string", description: "Layer id from list_layers (a pixel, annotation or group layer)." };
11554
+ var EFFECT_ID = { type: "string", description: "Effect id from list_layer_effects or add_layer_effect." };
11555
+ var BLEND_IDS = BLEND_MODES.map((m) => m.id);
11556
+ var DEFS = {
11557
+ color: { type: "string", pattern: "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$", description: "Colour as #rrggbb." },
11558
+ contour: {
11559
+ type: "object",
11560
+ description: "A transfer curve over the effect coverage: points { input, output } from 0 to 1 (at least 2), sorted by input. Linear is [{0,0},{1,1}].",
11561
+ properties: {
11562
+ name: { type: "string", description: 'Display name (e.g. "Cone", "Gaussian").' },
11563
+ points: {
11564
+ type: "array",
11565
+ minItems: 2,
11566
+ description: "Curve points.",
11567
+ items: {
11568
+ type: "object",
11569
+ properties: {
11570
+ input: { type: "number", minimum: 0, maximum: 1, description: "Coverage in (0..1)." },
11571
+ output: { type: "number", minimum: 0, maximum: 1, description: "Coverage out (0..1)." },
11572
+ corner: { type: "boolean", description: "Sharp corner instead of a smooth point (default false)." }
11573
+ },
11574
+ required: ["input", "output"],
11575
+ additionalProperties: false
11576
+ }
11577
+ }
11578
+ },
11579
+ required: ["name", "points"],
11580
+ additionalProperties: false
11581
+ },
11582
+ gradient: {
11583
+ type: "object",
11584
+ description: "A gradient: colour stops, optional opacity stops and its layout.",
11585
+ properties: {
11586
+ stops: {
11587
+ type: "array",
11588
+ minItems: 1,
11589
+ description: "Colour stops { position 0..1, color #rrggbb }.",
11590
+ items: {
11591
+ type: "object",
11592
+ properties: { position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." }, color: { $ref: "#/$defs/color" } },
11593
+ required: ["position", "color"],
11594
+ additionalProperties: false
11595
+ }
11596
+ },
11597
+ opacityStops: {
11598
+ type: "array",
11599
+ description: "Opacity stops { position 0..1, opacity 0..1 } (default opaque).",
11600
+ items: {
11601
+ type: "object",
11602
+ properties: {
11603
+ position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." },
11604
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1." }
11605
+ },
11606
+ required: ["position", "opacity"],
11607
+ additionalProperties: false
11608
+ }
11609
+ },
11610
+ style: { type: "string", enum: ["linear", "radial", "angle", "reflected", "diamond"], description: "Geometry." },
11611
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, 90 = bottom to top." },
11612
+ scale: { type: "number", minimum: 0.1, maximum: 1.5, description: "1 = 100 %." },
11613
+ reverse: { type: "boolean", description: "Swap the ends." },
11614
+ align: { type: "boolean", description: "Lay out over the layer bounds (true) or the canvas." },
11615
+ offset: {
11616
+ type: "object",
11617
+ description: "Centre offset as fractions of the bounds.",
11618
+ properties: { x: { type: "number", minimum: -1, maximum: 1, description: "Fraction of the width." }, y: { type: "number", minimum: -1, maximum: 1, description: "Fraction of the height." } },
11619
+ required: ["x", "y"],
11620
+ additionalProperties: false
11621
+ }
11622
+ },
11623
+ required: ["stops", "style", "angle", "scale", "reverse", "align", "offset"],
11624
+ additionalProperties: false
11625
+ },
11626
+ phase: {
11627
+ type: "object",
11628
+ description: "Pattern origin offset in px.",
11629
+ properties: { x: { type: "number", description: "px." }, y: { type: "number", description: "px." } },
11630
+ required: ["x", "y"],
11631
+ additionalProperties: false
11632
+ },
11633
+ fill: {
11634
+ description: "What a stroke or glow paints with. Glows take color or gradient only.",
11635
+ anyOf: [
11636
+ {
11637
+ type: "object",
11638
+ properties: { type: { const: "color", description: "Solid colour." }, color: { $ref: "#/$defs/color" } },
11639
+ required: ["type", "color"],
11640
+ additionalProperties: false
11641
+ },
11642
+ {
11643
+ type: "object",
11644
+ properties: { type: { const: "gradient", description: "Gradient." }, gradient: { $ref: "#/$defs/gradient" } },
11645
+ required: ["type", "gradient"],
11646
+ additionalProperties: false
11647
+ },
11648
+ {
11649
+ type: "object",
11650
+ properties: {
11651
+ type: { const: "pattern", description: "Pattern (strokes only)." },
11652
+ patternId: { type: "string", description: "Pattern id from list_layer_effects.patterns." },
11653
+ name: { type: "string", description: "Display name." },
11654
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 %." },
11655
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Rotation in degrees (default 0)." },
11656
+ link: { type: "boolean", description: "Tile from the layer (default true) or the canvas origin." },
11657
+ phase: { $ref: "#/$defs/phase" }
11658
+ },
11659
+ required: ["type", "patternId", "scale"],
11660
+ additionalProperties: false
11661
+ }
11662
+ ]
11663
+ }
11664
+ };
11665
+ var nullable = (schema, description) => ({ anyOf: [schema, { type: "null" }], description });
11666
+ function settingsSchema(forUpdate) {
11667
+ const opt = (schema, description) => forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description };
11668
+ return {
11669
+ type: "object",
11670
+ description: forUpdate ? "Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect." : "Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.",
11671
+ properties: {
11672
+ enabled: { type: "boolean", description: "All kinds: false keeps the effect but hides it (default true)." },
11673
+ blend: { type: "string", enum: BLEND_IDS, description: "All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal)." },
11674
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1)." },
11675
+ color: { $ref: "#/$defs/color", description: "drop-shadow, inner-shadow, satin, color-overlay: #rrggbb." },
11676
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Shadows, satin, bevel-emboss: light direction in degrees, counter-clockwise from 3 o'clock (default 120; satin 19)." },
11677
+ useGlobalLight: { type: "boolean", description: "Shadows, bevel-emboss: follow the document global light instead of angle (default true)." },
11678
+ distance: { type: "number", minimum: 0, maximum: 3e4, description: "Shadows, satin: offset in px (default 5; satin 11)." },
11679
+ spread: { type: "number", minimum: 0, maximum: 1, description: "drop-shadow, outer-glow: 0..1 of the size made solid (default 0)." },
11680
+ choke: { type: "number", minimum: 0, maximum: 1, description: "inner-shadow, inner-glow: 0..1 of the size made solid (default 0)." },
11681
+ size: { type: "number", minimum: 0, maximum: 250, description: "Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3)." },
11682
+ contour: opt({ $ref: "#/$defs/contour" }, "Shadows, glows, satin: coverage contour (default linear)."),
11683
+ antiAlias: { type: "boolean", description: "Shadows, glows, satin: smooth the contour (default false; satin true)." },
11684
+ noise: { type: "number", minimum: 0, maximum: 1, description: "Shadows, glows: speckle 0..1 (default 0)." },
11685
+ knocksOut: { type: "boolean", description: "drop-shadow: the layer hides the shadow beneath its see-through fill (default true)." },
11686
+ fill: { $ref: "#/$defs/fill", description: "outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern)." },
11687
+ technique: { type: "string", enum: ["softer", "precise", "smooth", "chisel-hard", "chisel-soft"], description: "Glows: softer | precise (default softer); bevel-emboss: smooth | chisel-hard | chisel-soft (default smooth)." },
11688
+ range: { type: "number", minimum: 0.01, maximum: 1, description: "Glows: the part of the glow the contour spans (default 0.5)." },
11689
+ jitter: { type: "number", minimum: 0, maximum: 1, description: "Glows: gradient jitter 0..1 (stored, not rendered; default 0)." },
11690
+ source: { type: "string", enum: ["edge", "center"], description: "inner-glow: glow from the edge (default) or the centre." },
11691
+ style: { type: "string", enum: ["inner-bevel", "outer-bevel", "emboss", "pillow-emboss", "stroke-emboss"], description: "bevel-emboss: style (default inner-bevel)." },
11692
+ depth: { type: "number", minimum: 0, maximum: 10, description: "bevel-emboss: depth, 1 = 100 % (default 1)." },
11693
+ direction: { type: "string", enum: ["up", "down"], description: "bevel-emboss: raised (up, default) or sunk (down)." },
11694
+ soften: { type: "number", minimum: 0, maximum: 16, description: "bevel-emboss: blur of the height in px (default 0)." },
11695
+ altitude: { type: "number", minimum: 0, maximum: 90, description: "bevel-emboss: light altitude in degrees (default 30)." },
11696
+ glossContour: opt({ $ref: "#/$defs/contour" }, "bevel-emboss: gloss contour over the shading (default linear)."),
11697
+ highlightColor: { $ref: "#/$defs/color", description: "bevel-emboss: highlight colour (default #ffffff)." },
11698
+ highlightBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: highlight mode (default screen)." },
11699
+ highlightOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: highlight opacity (default 0.75)." },
11700
+ shadowColor: { $ref: "#/$defs/color", description: "bevel-emboss: shadow colour (default #000000)." },
11701
+ shadowBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: shadow mode (default multiply)." },
11702
+ shadowOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: shadow opacity (default 0.75)." },
11703
+ contourElement: opt(
11704
+ {
11705
+ type: "object",
11706
+ properties: {
11707
+ contour: { $ref: "#/$defs/contour" },
11708
+ range: { type: "number", minimum: 0, maximum: 1, description: "Stored range (default 0.5)." },
11709
+ antiAlias: { type: "boolean", description: "Smooth the contour." }
11710
+ },
11711
+ required: ["contour", "range", "antiAlias"],
11712
+ additionalProperties: false
11713
+ },
11714
+ "bevel-emboss: the Contour element, shaping the bevel's height profile (default off)."
11715
+ ),
11716
+ texture: opt(
11717
+ {
11718
+ type: "object",
11719
+ properties: {
11720
+ patternId: { type: "string", description: "Pattern id from list_layer_effects.patterns." },
11721
+ name: { type: "string", description: "Display name." },
11722
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 %." },
11723
+ depth: { type: "number", minimum: -10, maximum: 10, description: "1 = +100 %; negative carves the pattern in." },
11724
+ invert: { type: "boolean", description: "Invert the pattern." },
11725
+ link: { type: "boolean", description: "Tile from the layer (default true)." },
11726
+ phase: { $ref: "#/$defs/phase" }
11727
+ },
11728
+ required: ["patternId", "scale", "depth", "invert"],
11729
+ additionalProperties: false
11730
+ },
11731
+ "bevel-emboss: the Texture element, a pattern's luminance as extra height (default off)."
11732
+ ),
11733
+ invert: { type: "boolean", description: "satin: invert (default true)." },
11734
+ gradient: { $ref: "#/$defs/gradient", description: "gradient-overlay: the gradient (default black to white, 90\xB0)." },
11735
+ dither: { type: "boolean", description: "gradient-overlay: dither (stored; default false)." },
11736
+ patternId: { type: "string", description: "pattern-overlay: pattern id from list_layer_effects.patterns (default checker)." },
11737
+ name: { type: "string", description: "pattern-overlay: pattern display name." },
11738
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "pattern-overlay: 1 = 100 % (default 1)." },
11739
+ link: { type: "boolean", description: "pattern-overlay: tile from the layer (default true) or the canvas origin." },
11740
+ phase: { $ref: "#/$defs/phase", description: "pattern-overlay: origin offset in px (default 0, 0)." },
11741
+ position: { type: "string", enum: ["outside", "inside", "center"], description: "stroke: where the stroke sits on the edge (default outside)." }
11742
+ },
11743
+ additionalProperties: false
11744
+ };
11745
+ }
11746
+ function patternList() {
11747
+ return builtinPatternIds().map((id) => {
11748
+ const p = builtinPattern(id);
11749
+ return { id, name: p.name, width: p.width, height: p.height, builtin: true };
11750
+ });
11751
+ }
11752
+ function describeLayerEffects(effects) {
11753
+ return {
11754
+ enabled: effects.enabled,
11755
+ scale: effects.scale ?? 1,
11756
+ margin: effectsMargin(effects),
11757
+ fromPsd: Boolean(effects.psd),
11758
+ effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) }))
11759
+ };
11760
+ }
11761
+ function createImageEffectTools(controller) {
11762
+ const layerArg = (id) => {
11763
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a layer id from list_layers).");
11764
+ requireLayer(controller.getLayers(), id);
11765
+ return id;
11766
+ };
11767
+ const result = (layerId, extra = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });
11768
+ const list = {
11769
+ name: "list_layer_effects",
11770
+ title: "List layer effects",
11771
+ description: "Returns a layer's effects: the master switch (enabled), Scale Effects (scale, 1 = 100 %), margin (px the effects reach beyond the shape), whether they came from a PSD, and every effect with its id, kind, label and all settings. Also returns the document global light and the pattern ids pattern effects can use.",
11772
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, required: ["layerId"], additionalProperties: false },
11773
+ annotations: { title: "List layer effects", ...READ3 },
11774
+ handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(), kinds: [...LAYER_EFFECT_KINDS] })
11775
+ };
11776
+ const add = {
11777
+ name: "add_layer_effect",
11778
+ title: "Add a layer effect",
11779
+ description: "Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
11780
+ inputSchema: {
11781
+ type: "object",
11782
+ $defs: DEFS,
11783
+ properties: {
11784
+ layerId: LAYER_ID3,
11785
+ kind: { type: "string", enum: [...LAYER_EFFECT_KINDS], description: "Effect kind." },
11786
+ settings: settingsSchema(false),
11787
+ index: { type: "integer", minimum: 0, description: "Position in the layer's effect list (default the end)." }
11788
+ },
11789
+ required: ["layerId", "kind"],
11790
+ additionalProperties: false
11791
+ },
11792
+ annotations: { title: "Add a layer effect", ...WRITE3 },
11793
+ handler: ({ layerId, kind, settings, index }) => {
11794
+ const id = layerArg(layerId);
11795
+ if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(", ")}.`);
11796
+ const { effect } = controller.addLayerEffect(id, kind, settings ?? {}, index);
11797
+ return result(id, { effectId: effect.id });
11798
+ }
11799
+ };
11800
+ const update = {
11801
+ name: "update_layer_effect",
11802
+ title: "Change a layer effect",
11803
+ description: "Changes settings of one effect (only the given ones; fill, gradient, contour and texture objects are replaced whole, null removes an optional contour or texture). The kind cannot change. Validates everything before changing anything. One undo step. Returns the layer's effects.",
11804
+ inputSchema: {
11805
+ type: "object",
11806
+ $defs: DEFS,
11807
+ properties: { layerId: LAYER_ID3, effectId: EFFECT_ID, settings: settingsSchema(true) },
11808
+ required: ["layerId", "effectId", "settings"],
11809
+ additionalProperties: false
11810
+ },
11811
+ annotations: { title: "Change a layer effect", ...WRITE_IDEMPOTENT3 },
11812
+ handler: ({ layerId, effectId, settings }) => {
11813
+ const id = layerArg(layerId);
11814
+ const changes = settings ?? {};
11815
+ if (!Object.keys(changes).length) throw new LayerOperationError("Give at least one setting to change.");
11816
+ controller.updateLayerEffect(id, effectId, changes);
11817
+ return result(id);
11818
+ }
11819
+ };
11820
+ const remove = {
11821
+ name: "remove_layer_effect",
11822
+ title: "Remove a layer effect",
11823
+ description: "Removes one effect from a layer (to hide it but keep its settings, set enabled false with update_layer_effect instead). One undo step. Returns the layer's remaining effects.",
11824
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID3, effectId: EFFECT_ID }, required: ["layerId", "effectId"], additionalProperties: false },
11825
+ annotations: { title: "Remove a layer effect", ...DESTRUCTIVE2 },
11826
+ handler: ({ layerId, effectId }) => {
11827
+ const id = layerArg(layerId);
11828
+ controller.removeLayerEffect(id, effectId);
11829
+ return result(id);
11830
+ }
11831
+ };
11832
+ const setEnabled = {
11833
+ name: "set_layer_effects_enabled",
11834
+ title: "Switch layer effects on or off",
11835
+ description: "Turns all of a layer's effects on or off at once (the master switch; each effect keeps its own enabled flag) and optionally sets Scale Effects, which multiplies every size, distance and pattern scale. One undo step. Returns the layer's effects.",
11836
+ inputSchema: {
11837
+ type: "object",
11838
+ properties: {
11839
+ layerId: LAYER_ID3,
11840
+ enabled: { type: "boolean", description: "true renders the enabled effects; false hides them all." },
11841
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "Scale Effects, 1 = 100 % (unchanged when left out)." }
11842
+ },
11843
+ required: ["layerId", "enabled"],
11844
+ additionalProperties: false
11845
+ },
11846
+ annotations: { title: "Switch layer effects on or off", ...WRITE_IDEMPOTENT3 },
11847
+ handler: ({ layerId, enabled, scale }) => {
11848
+ const id = layerArg(layerId);
11849
+ controller.setLayerEffectsEnabled(id, enabled, scale);
11850
+ return result(id);
11851
+ }
11852
+ };
11853
+ const light = {
11854
+ name: "set_global_light",
11855
+ title: "Set the global light",
11856
+ description: "Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120\xB0, altitude 30\xB0). One undo step. Returns the light.",
11857
+ inputSchema: {
11858
+ type: "object",
11859
+ properties: {
11860
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out)." },
11861
+ altitude: { type: "number", minimum: 0, maximum: 90, description: "Degrees above the canvas, for bevels (unchanged when left out)." }
11862
+ },
11863
+ additionalProperties: false
11864
+ },
11865
+ annotations: { title: "Set the global light", ...WRITE_IDEMPOTENT3 },
11866
+ handler: ({ angle, altitude }) => {
11867
+ if (angle === void 0 && altitude === void 0) throw new LayerOperationError("Give angle, altitude or both.");
11868
+ const next = {};
11869
+ if (angle !== void 0) next.angle = angle;
11870
+ if (altitude !== void 0) next.altitude = altitude;
11871
+ return { globalLight: controller.setGlobalLight(next) };
11872
+ }
11873
+ };
11874
+ return [list, add, update, remove, setEnabled, light];
11875
+ }
11876
+
11877
+ // src/image-editor/layers/paint-ui.ts
11878
+ var plain2 = { kind: "plain" };
11879
+ var BRUSH_OPTION_CONTROLS = [
11880
+ { id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain2, unit: "px" },
11881
+ { id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
11882
+ { id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
11883
+ { id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
11884
+ { id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
11885
+ ];
11886
+ var MIXER_OPTION_CONTROLS = [
11887
+ { id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
11888
+ { id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
11889
+ { id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
11890
+ ];
11891
+ var MIXER_MIXING_OPTIONS = [
11892
+ { id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
11893
+ { id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
11894
+ ];
11895
+ var PAINT_TOOLS = [
11896
+ { id: "move", label: "Move", shortcut: "V" },
11897
+ { id: "brush", label: "Brush", shortcut: "B" },
11898
+ { id: "mixer", label: "Mixer", shortcut: "Shift+B" },
11899
+ { id: "eraser", label: "Eraser", shortcut: "E" }
11900
+ ];
11901
+ var isPaintTool = (tool) => tool === "move" || tool === "brush" || tool === "mixer" || tool === "eraser";
11902
+ function rgbUnitToHex(c) {
11903
+ return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
11904
+ }
11905
+ function sizeStep(size) {
11906
+ return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
11907
+ }
11908
+ var isTyping = (t) => {
11909
+ const el = t;
11910
+ return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
11911
+ };
11912
+ function handlePaintShortcut(controller, e) {
11913
+ if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
11914
+ const brush = controller.getState().paint.brush;
11915
+ const set = (b) => controller.setPaintOptions({ brush: b });
11916
+ const key = e.key;
11917
+ switch (key.toLowerCase()) {
11918
+ case "b":
11919
+ controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
11920
+ return true;
11921
+ case "e":
11922
+ controller.setTool("eraser");
11923
+ return true;
11924
+ case "v":
11925
+ controller.setTool("move");
11926
+ return true;
11927
+ case "d":
11928
+ controller.resetPaintColors();
11929
+ return true;
11930
+ case "x":
11931
+ controller.swapPaintColors();
11932
+ return true;
11933
+ }
11934
+ if (key === "[" || key === "{") {
11935
+ if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
11936
+ else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
11937
+ return true;
11938
+ }
11939
+ if (key === "]" || key === "}") {
11940
+ if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
11941
+ else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
11942
+ return true;
11943
+ }
11944
+ if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
11945
+ set({ opacity: key === "0" ? 1 : Number(key) / 10 });
11946
+ return true;
11947
+ }
11948
+ return false;
11949
+ }
11950
+ function pointerPressure(e) {
11951
+ if (e.pointerType === "pen" && typeof e.pressure === "number" && e.pressure > 0) return Math.min(1, e.pressure);
11952
+ return 1;
11953
+ }
11954
+ var PaintGesture = class {
11955
+ constructor(controller) {
11956
+ this.controller = controller;
11957
+ }
11958
+ controller;
11959
+ stroke = null;
11960
+ move = null;
11961
+ /** The last refusal (e.g. "layer is locked"), for the editor to show. */
11962
+ error = null;
11963
+ get active() {
11964
+ return Boolean(this.stroke || this.move);
11965
+ }
11966
+ down(sample) {
11967
+ this.error = null;
11968
+ const tool = this.controller.getState().activeTool;
11969
+ try {
11970
+ if (tool === "move") {
11971
+ const layer = this.controller.getActiveLayer();
11972
+ if (!layer) throw new Error("Select a layer to move in the Layers panel.");
11973
+ if (layer.type !== "pixel") throw new Error(`Only pixel layers move; "${layer.name}" is a ${layer.type} layer.`);
11974
+ if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer "${layer.name}" has its position locked.`);
11975
+ this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };
11976
+ return null;
11977
+ }
11978
+ if (tool === "brush" || tool === "eraser" || tool === "mixer") {
11979
+ this.stroke = this.controller.beginStroke({ tool });
11980
+ this.stroke.push([sample]);
11981
+ return null;
11982
+ }
11983
+ } catch (err) {
11984
+ this.error = err instanceof Error ? err.message : String(err);
11985
+ this.stroke = null;
11986
+ this.move = null;
11987
+ }
11988
+ return this.error;
11989
+ }
11990
+ /** Feeds pointer samples (all coalesced events of one move, in order). */
11991
+ moveTo(samples) {
11992
+ if (!samples.length) return;
11993
+ if (this.stroke) {
11994
+ this.stroke.push(samples);
11995
+ return;
11996
+ }
11997
+ const m = this.move;
11998
+ if (m) {
11999
+ const last = samples[samples.length - 1];
12000
+ const x = m.origin.x + Math.round(last.x - m.start.x);
12001
+ const y = m.origin.y + Math.round(last.y - m.start.y);
12002
+ try {
12003
+ this.controller.offsetLayer(m.id, { x, y }, !m.moved);
12004
+ m.moved = true;
12005
+ } catch (err) {
12006
+ this.error = err instanceof Error ? err.message : String(err);
12007
+ this.move = null;
12008
+ }
12009
+ }
12010
+ }
12011
+ up() {
12012
+ if (this.stroke) {
12013
+ try {
12014
+ this.stroke.end();
12015
+ } catch (err) {
12016
+ this.error = err instanceof Error ? err.message : String(err);
12017
+ }
12018
+ }
12019
+ this.stroke = null;
12020
+ this.move = null;
12021
+ }
12022
+ cancel() {
12023
+ this.stroke?.cancel();
12024
+ this.stroke = null;
12025
+ this.move = null;
12026
+ }
12027
+ };
12028
+
12029
+ // src/image-editor/paint-tools.ts
12030
+ var READ4 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
12031
+ var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
12032
+ var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11321
12033
  var TARGET = {
11322
12034
  layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
11323
12035
  channel: { type: "string", enum: ["pixels", "mask"], description: "pixels (default for a pixel layer) or the layer's mask: black hides, white reveals." }
@@ -11336,10 +12048,30 @@ var BRUSH = {
11336
12048
  smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Point smoothing 0..0.95 (default 0)." },
11337
12049
  pressureSize: { type: "boolean", description: "Point pressure scales the size (default true)." },
11338
12050
  pressureOpacity: { type: "boolean", description: "Point pressure scales the opacity (default false)." },
11339
- blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "How paint combines with the layer (default normal)." }
12051
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "How paint combines with the layer (default normal). pigment mixes it in like real paint (blue into yellow gives green) instead of laying it over." }
11340
12052
  },
11341
12053
  additionalProperties: false
11342
12054
  };
12055
+ var MIXER = {
12056
+ type: "object",
12057
+ description: "Mixer Brush settings (tool mixer only); missing fields use the current mixer options (wet 0.5, load 0.5, mix 0.5, loadEachStroke true, cleanEachStroke true, mixing rgb).",
12058
+ properties: {
12059
+ wet: { type: "number", minimum: 0, maximum: 1, description: "How much canvas colour each dab picks up, 0 (dry: paints only the reservoir) .. 1 (default 0.5)." },
12060
+ load: { type: "number", minimum: 0, maximum: 1, description: "How much paint the reservoir holds, 0 .. 1 (default 0.5); below 1 it drains per dab, so long strokes run dry." },
12061
+ mix: { type: "number", minimum: 0, maximum: 1, description: "How far each dab's paint moves from the reservoir towards the picked-up colour, 0 .. 1 (default 0.5)." },
12062
+ loadEachStroke: { type: "boolean", description: "Refill the reservoir with `color` (or the foreground) before the stroke (default true). Off, the brush keeps what is left of its last load." },
12063
+ cleanEachStroke: { type: "boolean", description: "Empty the pickup well after the stroke (default true). Off, the next stroke starts with the colour this one picked up." },
12064
+ mixing: { type: "string", enum: [...MIXING_MODELS], description: "rgb (default): RGB averages. pigment: mixes like paint (Kubelka-Munk), so blue dragged through yellow makes green." }
12065
+ },
12066
+ additionalProperties: false
12067
+ };
12068
+ function mixerLoadResult(load) {
12069
+ return {
12070
+ reservoir: load.reservoir ? rgbUnitToHex(load.reservoir) : null,
12071
+ amount: Math.round(load.amount * 1e3) / 1e3,
12072
+ pickup: load.pickup ? rgbUnitToHex(load.pickup) : null
12073
+ };
12074
+ }
11343
12075
  function readTarget(args) {
11344
12076
  return { layerId: args.layerId, channel: args.channel };
11345
12077
  }
@@ -11347,11 +12079,11 @@ function createImagePaintTools(controller) {
11347
12079
  const paintStroke = {
11348
12080
  name: "paint_stroke",
11349
12081
  title: "Paint a brush stroke",
11350
- description: "Paints one stroke with the brush (or removes pixels with the eraser) through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask. The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count.",
12082
+ description: "Paints one stroke with the brush, smears and blends paint with the Mixer Brush (tool mixer: its reservoir mixes with colour picked up from the layer under each dab, in RGB or pigment mixing), or removes pixels with the eraser, through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask (the mixer paints pixels only). The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count, and for the mixer its mixerLoad after the stroke ({reservoir, amount, pickup}).",
11351
12083
  inputSchema: {
11352
12084
  type: "object",
11353
12085
  properties: {
11354
- tool: { type: "string", enum: ["brush", "eraser"], description: "brush (default) or eraser." },
12086
+ tool: { type: "string", enum: ["brush", "eraser", "mixer"], description: "brush (default), eraser, or mixer (the Mixer Brush: picks up and blends the layer colour under each dab)." },
11355
12087
  points: {
11356
12088
  type: "array",
11357
12089
  minItems: 1,
@@ -11368,21 +12100,66 @@ function createImagePaintTools(controller) {
11368
12100
  additionalProperties: false
11369
12101
  }
11370
12102
  },
11371
- color: { type: "string", description: "Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals." },
12103
+ color: {
12104
+ type: "string",
12105
+ description: "Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals. For the mixer, the colour its reservoir is loaded with before the stroke (given, it loads even with loadEachStroke false)."
12106
+ },
11372
12107
  brush: BRUSH,
12108
+ mixer: MIXER,
11373
12109
  ...TARGET
11374
12110
  },
11375
12111
  required: ["points"],
11376
12112
  additionalProperties: false
11377
12113
  },
11378
- annotations: { title: "Paint a brush stroke", ...WRITE3 },
11379
- handler: (args) => controller.paintStroke({
11380
- tool: args.tool,
11381
- points: args.points,
11382
- color: args.color,
11383
- brush: args.brush,
11384
- ...readTarget(args)
11385
- })
12114
+ annotations: { title: "Paint a brush stroke", ...WRITE4 },
12115
+ handler: (args) => {
12116
+ const tool = args.tool;
12117
+ if (args.mixer !== void 0 && tool !== "mixer") throw new LayerOperationError('mixer settings apply to the Mixer Brush only; pass tool "mixer", or drop mixer.');
12118
+ const result = controller.paintStroke({
12119
+ tool,
12120
+ points: args.points,
12121
+ color: args.color,
12122
+ brush: args.brush,
12123
+ mixer: args.mixer,
12124
+ ...readTarget(args)
12125
+ });
12126
+ return result.mixerLoad ? { ...result, mixerLoad: mixerLoadResult(result.mixerLoad) } : result;
12127
+ }
12128
+ };
12129
+ const getMixer = {
12130
+ name: "get_mixer_brush",
12131
+ title: "Get the Mixer Brush",
12132
+ description: "Reads the Mixer Brush: its settings (wet, load, mix, loadEachStroke, cleanEachStroke, mixing) and what it carries now (reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null). Call it before a paint_stroke with tool mixer and loadEachStroke false, to know the brush's starting paint.",
12133
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
12134
+ annotations: { title: "Get the Mixer Brush", ...READ4 },
12135
+ handler: () => {
12136
+ const { mixer, mixerLoad } = controller.getState().paint;
12137
+ return { settings: { ...mixer }, load: mixerLoadResult(mixerLoad) };
12138
+ }
12139
+ };
12140
+ const loadMixerTool = {
12141
+ name: "load_mixer_brush",
12142
+ title: "Load the Mixer Brush",
12143
+ description: "Fills the Mixer Brush reservoir with a colour (amount 1); the picked-up colour is kept. View state, not an undo step. Use it with loadEachStroke false to carry one load across several paint_stroke calls. Returns the load {reservoir, amount, pickup}.",
12144
+ inputSchema: {
12145
+ type: "object",
12146
+ properties: { color: { type: "string", description: "Paint colour #rrggbb (default the foreground)." } },
12147
+ additionalProperties: false
12148
+ },
12149
+ annotations: { title: "Load the Mixer Brush", ...WRITE4, idempotentHint: true },
12150
+ handler: (args) => mixerLoadResult(controller.loadMixerBrush(args.color))
12151
+ };
12152
+ const cleanMixerTool = {
12153
+ name: "clean_mixer_brush",
12154
+ title: "Clean the Mixer Brush",
12155
+ description: "Cleans the Mixer Brush: empties the pickup well so the next stroke starts without canvas colour; with emptyReservoir also drops the loaded paint (the brush then only smears). View state, not an undo step. Returns the load {reservoir, amount, pickup}.",
12156
+ inputSchema: {
12157
+ type: "object",
12158
+ properties: { emptyReservoir: { type: "boolean", description: "Also drop the reservoir (default false)." } },
12159
+ additionalProperties: false
12160
+ },
12161
+ annotations: { title: "Clean the Mixer Brush", ...WRITE4, idempotentHint: true },
12162
+ handler: (args) => mixerLoadResult(args.emptyReservoir === true ? controller.emptyMixerBrush() : controller.cleanMixerBrush())
11386
12163
  };
11387
12164
  const fill = {
11388
12165
  name: "fill_pixels",
@@ -11393,13 +12170,13 @@ function createImagePaintTools(controller) {
11393
12170
  properties: {
11394
12171
  color: { type: "string", description: "Fill colour #rrggbb or #rrggbbaa (default the foreground)." },
11395
12172
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Fill opacity 0..1 (default 1)." },
11396
- blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
12173
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal). pigment mixes the colour into the pixels like paint, opacity being the share of new paint." },
11397
12174
  useSelection: { type: "boolean", description: "Limit to the selection when there is one (default true)." },
11398
12175
  ...TARGET
11399
12176
  },
11400
12177
  additionalProperties: false
11401
12178
  },
11402
- annotations: { title: "Fill pixels", ...WRITE3 },
12179
+ annotations: { title: "Fill pixels", ...WRITE4 },
11403
12180
  handler: (args) => controller.fillPixels({
11404
12181
  color: args.color,
11405
12182
  opacity: args.opacity,
@@ -11421,15 +12198,15 @@ function createImagePaintTools(controller) {
11421
12198
  },
11422
12199
  additionalProperties: false
11423
12200
  },
11424
- annotations: { title: "Clear pixels", ...DESTRUCTIVE2 },
12201
+ annotations: { title: "Clear pixels", ...DESTRUCTIVE3 },
11425
12202
  handler: (args) => controller.clearPixels({ opacity: args.opacity, useSelection: args.useSelection, ...readTarget(args) })
11426
12203
  };
11427
- return [paintStroke, fill, clear];
12204
+ return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];
11428
12205
  }
11429
12206
 
11430
12207
  // src/image-editor/psd-tools.ts
11431
- var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11432
- var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
12208
+ var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
12209
+ var DESTRUCTIVE4 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11433
12210
  var DATA = {
11434
12211
  type: "string",
11435
12212
  minLength: 1,
@@ -11449,7 +12226,7 @@ function createImagePsdTools(controller) {
11449
12226
  title: "Inspect a PSD",
11450
12227
  description: "Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.",
11451
12228
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11452
- annotations: { title: "Inspect a PSD", ...READ3 },
12229
+ annotations: { title: "Inspect a PSD", ...READ5 },
11453
12230
  handler: ({ data }) => {
11454
12231
  const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
11455
12232
  return {
@@ -11467,7 +12244,7 @@ function createImagePsdTools(controller) {
11467
12244
  title: "Open a PSD as the document",
11468
12245
  description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility and locks map directly; type, smart-object, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
11469
12246
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11470
- annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE3 },
12247
+ annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
11471
12248
  handler: ({ data }) => {
11472
12249
  const res = controller.loadPsd(psdBytesFromBase64(data));
11473
12250
  const s = controller.getState();
@@ -11486,7 +12263,7 @@ function createImagePsdTools(controller) {
11486
12263
  title: "Export the document as a PSD",
11487
12264
  description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers and masks, in the source frame (crop and rotation are not applied). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
11488
12265
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
11489
- annotations: { title: "Export the document as a PSD", ...READ3 },
12266
+ annotations: { title: "Export the document as a PSD", ...READ5 },
11490
12267
  handler: async () => {
11491
12268
  const res = await controller.toPsd();
11492
12269
  return {
@@ -11740,6 +12517,7 @@ function createImageEditorToolProvider(controller, options = {}) {
11740
12517
  ...createImageSelectionTools(controller),
11741
12518
  ...createImageEditTools(controller),
11742
12519
  ...createImageLayerTools(controller),
12520
+ ...createImageEffectTools(controller),
11743
12521
  ...createImagePaintTools(controller),
11744
12522
  ...createPigmentTools(),
11745
12523
  ...createImagePsdTools(controller)
@@ -12183,145 +12961,6 @@ async function runLayerMenuAction(controller, action) {
12183
12961
  }
12184
12962
  }
12185
12963
 
12186
- // src/image-editor/layers/paint-ui.ts
12187
- var plain2 = { kind: "plain" };
12188
- var BRUSH_OPTION_CONTROLS = [
12189
- { id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain2, unit: "px" },
12190
- { id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
12191
- { id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
12192
- { id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
12193
- { id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
12194
- ];
12195
- var PAINT_TOOLS = [
12196
- { id: "move", label: "Move", shortcut: "V" },
12197
- { id: "brush", label: "Brush", shortcut: "B" },
12198
- { id: "eraser", label: "Eraser", shortcut: "E" }
12199
- ];
12200
- var isPaintTool = (tool) => tool === "move" || tool === "brush" || tool === "eraser";
12201
- function sizeStep(size) {
12202
- return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
12203
- }
12204
- var isTyping = (t) => {
12205
- const el = t;
12206
- return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
12207
- };
12208
- function handlePaintShortcut(controller, e) {
12209
- if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
12210
- const brush = controller.getState().paint.brush;
12211
- const set = (b) => controller.setPaintOptions({ brush: b });
12212
- const key = e.key;
12213
- switch (key.toLowerCase()) {
12214
- case "b":
12215
- controller.setTool("brush");
12216
- return true;
12217
- case "e":
12218
- controller.setTool("eraser");
12219
- return true;
12220
- case "v":
12221
- controller.setTool("move");
12222
- return true;
12223
- case "d":
12224
- controller.resetPaintColors();
12225
- return true;
12226
- case "x":
12227
- controller.swapPaintColors();
12228
- return true;
12229
- }
12230
- if (key === "[" || key === "{") {
12231
- if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
12232
- else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
12233
- return true;
12234
- }
12235
- if (key === "]" || key === "}") {
12236
- if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
12237
- else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
12238
- return true;
12239
- }
12240
- if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
12241
- set({ opacity: key === "0" ? 1 : Number(key) / 10 });
12242
- return true;
12243
- }
12244
- return false;
12245
- }
12246
- function pointerPressure(e) {
12247
- if (e.pointerType === "pen" && typeof e.pressure === "number" && e.pressure > 0) return Math.min(1, e.pressure);
12248
- return 1;
12249
- }
12250
- var PaintGesture = class {
12251
- constructor(controller) {
12252
- this.controller = controller;
12253
- }
12254
- controller;
12255
- stroke = null;
12256
- move = null;
12257
- /** The last refusal (e.g. "layer is locked"), for the editor to show. */
12258
- error = null;
12259
- get active() {
12260
- return Boolean(this.stroke || this.move);
12261
- }
12262
- down(sample) {
12263
- this.error = null;
12264
- const tool = this.controller.getState().activeTool;
12265
- try {
12266
- if (tool === "move") {
12267
- const layer = this.controller.getActiveLayer();
12268
- if (!layer) throw new Error("Select a layer to move in the Layers panel.");
12269
- if (layer.type !== "pixel") throw new Error(`Only pixel layers move; "${layer.name}" is a ${layer.type} layer.`);
12270
- if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer "${layer.name}" has its position locked.`);
12271
- this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };
12272
- return null;
12273
- }
12274
- if (tool === "brush" || tool === "eraser") {
12275
- this.stroke = this.controller.beginStroke({ tool });
12276
- this.stroke.push([sample]);
12277
- return null;
12278
- }
12279
- } catch (err) {
12280
- this.error = err instanceof Error ? err.message : String(err);
12281
- this.stroke = null;
12282
- this.move = null;
12283
- }
12284
- return this.error;
12285
- }
12286
- /** Feeds pointer samples (all coalesced events of one move, in order). */
12287
- moveTo(samples) {
12288
- if (!samples.length) return;
12289
- if (this.stroke) {
12290
- this.stroke.push(samples);
12291
- return;
12292
- }
12293
- const m = this.move;
12294
- if (m) {
12295
- const last = samples[samples.length - 1];
12296
- const x = m.origin.x + Math.round(last.x - m.start.x);
12297
- const y = m.origin.y + Math.round(last.y - m.start.y);
12298
- try {
12299
- this.controller.offsetLayer(m.id, { x, y }, !m.moved);
12300
- m.moved = true;
12301
- } catch (err) {
12302
- this.error = err instanceof Error ? err.message : String(err);
12303
- this.move = null;
12304
- }
12305
- }
12306
- }
12307
- up() {
12308
- if (this.stroke) {
12309
- try {
12310
- this.stroke.end();
12311
- } catch (err) {
12312
- this.error = err instanceof Error ? err.message : String(err);
12313
- }
12314
- }
12315
- this.stroke = null;
12316
- this.move = null;
12317
- }
12318
- cancel() {
12319
- this.stroke?.cancel();
12320
- this.stroke = null;
12321
- this.move = null;
12322
- }
12323
- };
12324
-
12325
12964
  // src/image-editor/psd/ui.ts
12326
12965
  var PSD_FILE_ACCEPT = ".psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop";
12327
12966
  function blobBytes(file) {
@@ -12372,6 +13011,7 @@ export {
12372
13011
  DEFAULT_ADJUSTMENTS,
12373
13012
  DEFAULT_BRUSH,
12374
13013
  DEFAULT_DEPTH_MASK,
13014
+ DEFAULT_MIXER,
12375
13015
  DEFAULT_REFINE_EDGE_PARAMS,
12376
13016
  DEFAULT_SELECTION_TOOL_OPTIONS,
12377
13017
  DEFAULT_TRANSFORM,
@@ -12379,6 +13019,7 @@ export {
12379
13019
  DIRECT_GAUSSIAN_MAX_SIGMA,
12380
13020
  DabWalker,
12381
13021
  EMPTY_DISTANCE,
13022
+ EMPTY_MIXER_LOAD,
12382
13023
  GUIDED_FILTER_EPS,
12383
13024
  HISTOGRAM_BINS,
12384
13025
  HUE_RANGE_LABELS,
@@ -12398,6 +13039,12 @@ export {
12398
13039
  MAX_KELVIN,
12399
13040
  MIN_KELVIN,
12400
13041
  MIRED_PER_TEMPERATURE_UNIT,
13042
+ MIXER_DRAIN,
13043
+ MIXER_MIXING_OPTIONS,
13044
+ MIXER_OPTION_CONTROLS,
13045
+ MIXER_PICKUP_RATE,
13046
+ MIXING_MODELS,
13047
+ MixerStroke,
12401
13048
  PAINT_TOOLS,
12402
13049
  PIGMENT_BANDS,
12403
13050
  PIGMENT_LAMBDA_MIN,
@@ -12427,6 +13074,7 @@ export {
12427
13074
  XYZ_D50_TO_SRGB,
12428
13075
  ZlibError,
12429
13076
  activeAdjustmentLayers,
13077
+ addLayerEffect,
12430
13078
  addLayerFilter,
12431
13079
  addToHistogram,
12432
13080
  adjustmentKindLabel,
@@ -12472,6 +13120,7 @@ export {
12472
13120
  changedTiles,
12473
13121
  clampControlValue,
12474
13122
  clampCropToImage,
13123
+ cleanMixer,
12475
13124
  collectTiles,
12476
13125
  colorBalanceChannel,
12477
13126
  colorRange,
@@ -12490,6 +13139,7 @@ export {
12490
13139
  createImageCaptureSurface,
12491
13140
  createImageEditTools,
12492
13141
  createImageEditorToolProvider,
13142
+ createImageEffectTools,
12493
13143
  createImageLayerTools,
12494
13144
  createImagePaintTools,
12495
13145
  createImagePsdTools,
@@ -12516,7 +13166,9 @@ export {
12516
13166
  depthMaskAlpha,
12517
13167
  depthRasterMask,
12518
13168
  describeLayer,
13169
+ describeLayerEffects,
12519
13170
  describeSelection,
13171
+ deserializeEffects,
12520
13172
  deserializeLayer,
12521
13173
  deserializeLayers,
12522
13174
  deserializeSelection,
@@ -12601,6 +13253,7 @@ export {
12601
13253
  isSpatialFilter,
12602
13254
  kelvinToTemperature,
12603
13255
  layerBadge,
13256
+ layerEffectsOf,
12604
13257
  layerLabel,
12605
13258
  layerMaskCoverage,
12606
13259
  layerMaskFromSelection,
@@ -12620,13 +13273,17 @@ export {
12620
13273
  linearSrgbToXy,
12621
13274
  linearToSrgb,
12622
13275
  loadImageEditorDocument,
13276
+ loadMixer,
12623
13277
  magicWand,
12624
13278
  mapCropHandle,
12625
13279
  mapLayer,
12626
13280
  maskSummary,
12627
13281
  migrateSerializedImageEditorState,
12628
13282
  migrateToLayeredState,
13283
+ mixPigment8,
13284
+ mixPigmentRgb,
12629
13285
  mixPigments,
13286
+ mixerLoadResult,
12630
13287
  modifySelection,
12631
13288
  moveCurvePoint,
12632
13289
  moveLayer,
@@ -12641,6 +13298,7 @@ export {
12641
13298
  normalizeBrush,
12642
13299
  normalizeFilter,
12643
13300
  normalizeLayerMask,
13301
+ normalizeMixer,
12644
13302
  normalizeProcessVersion,
12645
13303
  normalizeRect,
12646
13304
  normalizeRefineEdgeParams,
@@ -12686,6 +13344,7 @@ export {
12686
13344
  removeAlpha,
12687
13345
  removeCurvePoint,
12688
13346
  removeLayer,
13347
+ removeLayerEffect,
12689
13348
  removeLayerFilter,
12690
13349
  renderLayeredBase,
12691
13350
  requireLayer,
@@ -12697,6 +13356,7 @@ export {
12697
13356
  rgbToGray,
12698
13357
  rgbToHsl,
12699
13358
  rgbToReflectance,
13359
+ rgbUnitToHex,
12700
13360
  runLayerMenuAction,
12701
13361
  sampleLut,
12702
13362
  sampleToneLut,
@@ -12708,11 +13368,14 @@ export {
12708
13368
  selectionToCoverage,
12709
13369
  selectionToPixels,
12710
13370
  selectionValueAt,
13371
+ serializeEffects,
12711
13372
  serializeLayer,
12712
13373
  serializeLayers,
12713
13374
  serializeLayersExternal,
12714
13375
  serializeSelection,
12715
13376
  setControlPath,
13377
+ setLayerEffects,
13378
+ setLayerEffectsEnabled,
12716
13379
  setLayerMask,
12717
13380
  setLayerProperties,
12718
13381
  setLum,
@@ -12746,6 +13409,7 @@ export {
12746
13409
  uniformTile,
12747
13410
  unionRect,
12748
13411
  unprojectCanvasPoint,
13412
+ updateLayerEffect,
12749
13413
  updateLayerFilter,
12750
13414
  updateLayerMaskProps,
12751
13415
  uvToXy,