@pixerate/editor 0.94.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 (41) hide show
  1. package/THIRD_PARTY_NOTICES.md +1 -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-747CGOT2.js → chunk-E2ILRYTJ.js} +12 -2
  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-73L6TROJ.cjs → chunk-T6H3JDGA.cjs} +54 -44
  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 +705 -501
  15. package/dist/image-editor/index.cjs.map +1 -1
  16. package/dist/image-editor/index.d.cts +86 -22
  17. package/dist/image-editor/index.d.ts +86 -22
  18. package/dist/image-editor/index.js +383 -179
  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 +7 -3
  26. package/dist/raster/index.cjs.map +1 -1
  27. package/dist/raster/index.d.cts +4 -4
  28. package/dist/raster/index.d.ts +4 -4
  29. package/dist/raster/index.js +6 -2
  30. package/dist/{surface-CuBg2c6k.d.cts → surface-CgAiXVI3.d.cts} +23 -11
  31. package/dist/{surface-CuBg2c6k.d.ts → surface-CgAiXVI3.d.ts} +23 -11
  32. package/dist/{zlib-BJ6U7LuB.d.cts → zlib-B3LIoxF-.d.cts} +3 -2
  33. package/dist/{zlib-BjQkpMFi.d.ts → zlib-eiTEEj1z.d.ts} +3 -2
  34. package/package.json +1 -1
  35. package/skills/pixerate-image-editor/SKILL.md +26 -2
  36. package/dist/chunk-4F3YDIWW.cjs.map +0 -1
  37. package/dist/chunk-73L6TROJ.cjs.map +0 -1
  38. package/dist/chunk-747CGOT2.js.map +0 -1
  39. package/dist/chunk-D4ZCU72J.js.map +0 -1
  40. package/dist/chunk-ISDCJHTO.js.map +0 -1
  41. package/dist/chunk-ZOOEX3WT.cjs.map +0 -1
@@ -42,39 +42,41 @@ import {
42
42
  parseDataUrl,
43
43
  pngDataUrl,
44
44
  validateLayerEffects
45
- } from "../chunk-747CGOT2.js";
45
+ } from "../chunk-E2ILRYTJ.js";
46
46
  import {
47
47
  CoverageMap,
48
48
  DEFAULT_BRUSH,
49
+ DEFAULT_MIXER,
49
50
  DabWalker,
51
+ EMPTY_MIXER_LOAD,
50
52
  MAX_BRUSH_SIZE,
51
- PIGMENT_BANDS,
52
- PIGMENT_LAMBDA_MIN,
53
- PIGMENT_LAMBDA_STEP,
54
- PIGMENT_WAVELENGTHS,
53
+ MIXER_DRAIN,
54
+ MIXER_PICKUP_RATE,
55
+ MIXING_MODELS,
56
+ MixerStroke,
55
57
  StrokeCoverage,
56
58
  applyStrokeToTile,
59
+ cleanMixer,
57
60
  coverageTileCoord,
58
- createPigment,
59
61
  createPigmentTools,
60
62
  dabCoverage,
61
63
  dabRect,
62
- lerpPigment,
63
- lerpPigmentInto,
64
- mixPigments,
64
+ loadMixer,
65
65
  normalizeBrush,
66
+ normalizeMixer,
66
67
  rasterizeDab,
67
- reflectanceToRgb,
68
- rgbToReflectance,
69
- setPigmentColor,
70
68
  smoothPoints,
71
69
  tipFalloff,
72
70
  tipReach,
73
71
  tipShape,
74
72
  walkDabs
75
- } from "../chunk-ISDCJHTO.js";
73
+ } from "../chunk-IB53LVGV.js";
76
74
  import {
77
75
  BLEND_MODES,
76
+ PIGMENT_BANDS,
77
+ PIGMENT_LAMBDA_MIN,
78
+ PIGMENT_LAMBDA_STEP,
79
+ PIGMENT_WAVELENGTHS,
78
80
  SurfaceDraft,
79
81
  TILE,
80
82
  TILE_SHIFT,
@@ -87,16 +89,25 @@ import {
87
89
  canvasCompositeOperation,
88
90
  collectTiles,
89
91
  contentBounds,
92
+ createPigment,
90
93
  emptySurface,
91
94
  fillRect,
92
95
  finishTile,
93
96
  isBlendMode,
94
97
  isSeparableBlendMode,
98
+ lerpPigment,
99
+ lerpPigmentInto,
100
+ mixPigment8,
101
+ mixPigmentRgb,
102
+ mixPigments,
95
103
  normalizeBlendMode,
96
104
  packRgba,
97
105
  readPixel,
98
106
  readRegion,
107
+ reflectanceToRgb,
108
+ rgbToReflectance,
99
109
  setLum,
110
+ setPigmentColor,
100
111
  solidSurface,
101
112
  surfaceBytes,
102
113
  surfaceFromPixels,
@@ -108,7 +119,7 @@ import {
108
119
  translateSurface,
109
120
  uniformTile,
110
121
  writeRegion
111
- } from "../chunk-D4ZCU72J.js";
122
+ } from "../chunk-HEW6KDNI.js";
112
123
 
113
124
  // src/image-editor/color.ts
114
125
  var LEGACY_PROCESS_VERSION = 1;
@@ -1919,6 +1930,10 @@ function applyLayer(c, layer, op, coverage = 1) {
1919
1930
  }
1920
1931
  const cb0 = c[0], cb1 = c[1], cb2 = c[2];
1921
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
+ }
1922
1937
  const b = blend === "normal" ? c : blendRgb(blend, [cb0, cb1, cb2], c);
1923
1938
  c[0] = (1 - alpha) * cb0 + alpha * b[0];
1924
1939
  c[1] = (1 - alpha) * cb1 + alpha * b[1];
@@ -7184,22 +7199,53 @@ var PaintStrokeSession = class {
7184
7199
  constructor(controller, target, stroke, apply) {
7185
7200
  this.controller = controller;
7186
7201
  this.target = target;
7187
- this.stroke = stroke;
7188
7202
  this.apply = apply;
7189
7203
  this.before = controller._captureForStroke();
7190
7204
  this.base = target.surface;
7191
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
+ }
7192
7214
  }
7193
7215
  controller;
7194
7216
  target;
7195
- stroke;
7196
7217
  apply;
7197
7218
  before;
7198
7219
  base;
7199
7220
  live;
7200
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();
7201
7227
  last = null;
7202
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
+ }
7203
7249
  get layerId() {
7204
7250
  return this.target.id;
7205
7251
  }
@@ -7207,11 +7253,18 @@ var PaintStrokeSession = class {
7207
7253
  push(points) {
7208
7254
  if (this.closed || !points.length) return;
7209
7255
  this.last = points[points.length - 1];
7210
- this.update(this.stroke.push(points));
7256
+ this.update(this.mixer ? this.mixer.push(points) : this.stroke.push(points));
7211
7257
  }
7212
7258
  update(rects) {
7213
7259
  if (!rects.length) return;
7214
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;
7215
7268
  const regions = /* @__PURE__ */ new Map();
7216
7269
  for (const r of rects) {
7217
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 })) {
@@ -7224,7 +7277,7 @@ var PaintStrokeSession = class {
7224
7277
  const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
7225
7278
  const prev = this.touched.has(key) ? this.live.get(key)?.data : void 0;
7226
7279
  this.touched.add(key);
7227
- 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 } : {});
7228
7281
  this.live.set(key, { data, uniform: null });
7229
7282
  }
7230
7283
  this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
@@ -7232,7 +7285,8 @@ var PaintStrokeSession = class {
7232
7285
  /** Finishes the stroke (one undo step) and returns what changed. */
7233
7286
  end() {
7234
7287
  if (this.closed) throw new LayerOperationError("This stroke has already ended.");
7235
- 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));
7236
7290
  this.closed = true;
7237
7291
  const ch = this.base.channels;
7238
7292
  const tiles = new Map(this.base.tiles);
@@ -7246,9 +7300,12 @@ var PaintStrokeSession = class {
7246
7300
  if (!sameTile(prev, tile)) changed = true;
7247
7301
  }
7248
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;
7249
7304
  this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);
7250
- const b = this.stroke.coverage.bounds;
7251
- 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;
7252
7309
  }
7253
7310
  /** Abandons the stroke and restores the layer. */
7254
7311
  cancel() {
@@ -7384,7 +7441,7 @@ var ImageEditorController = class {
7384
7441
  activeLayerId: null,
7385
7442
  activeTarget: "pixels",
7386
7443
  outputOpacity: 1,
7387
- paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff" },
7444
+ paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff", mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },
7388
7445
  fit: options.fit ?? "margin"
7389
7446
  };
7390
7447
  if (options.image instanceof Object && "width" in options.image) {
@@ -8375,12 +8432,38 @@ var ImageEditorController = class {
8375
8432
  const next = {
8376
8433
  brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,
8377
8434
  foreground: options.foreground !== void 0 ? toHex2(parseColor(options.foreground)) : cur.foreground,
8378
- 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
8379
8438
  };
8380
8439
  this.state.paint = next;
8381
8440
  this.notify();
8382
8441
  return next;
8383
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
+ }
8384
8467
  /** D: black foreground, white background. */
8385
8468
  resetPaintColors() {
8386
8469
  this.setPaintOptions({ foreground: "#000000", background: "#ffffff" });
@@ -8425,7 +8508,11 @@ var ImageEditorController = class {
8425
8508
  */
8426
8509
  beginStroke(init = {}) {
8427
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.`);
8428
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
+ }
8429
8516
  const brush = normalizeBrush({ ...this.state.paint.brush, ...init.brush ?? {} });
8430
8517
  const color = parseColor(init.color ?? this.state.paint.foreground);
8431
8518
  const background = parseColor(init.background ?? this.state.paint.background);
@@ -8443,8 +8530,26 @@ var ImageEditorController = class {
8443
8530
  selection: this.state.selection?.bounds ? this.state.selection : null,
8444
8531
  offset: target.offset
8445
8532
  };
8533
+ if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);
8446
8534
  return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);
8447
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
+ }
8448
8553
  /** @internal Shows a stroke's live surface (no history). */
8449
8554
  _strokeUpdate(layerId, channel, surface, rects) {
8450
8555
  this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
@@ -8468,7 +8573,7 @@ var ImageEditorController = class {
8468
8573
  }
8469
8574
  /**
8470
8575
  * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional
8471
- * 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
8472
8577
  * document bounds and the number of dabs.
8473
8578
  */
8474
8579
  paintStroke(init) {
@@ -9477,6 +9582,11 @@ var ImageEditorController = class {
9477
9582
  return baked;
9478
9583
  };
9479
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) => {
9480
9590
  if (layer.type === "group") {
9481
9591
  const children = [];
9482
9592
  for (const c of layer.children) {
@@ -11764,7 +11874,160 @@ function createImageEffectTools(controller) {
11764
11874
  return [list, add, update, remove, setEnabled, light];
11765
11875
  }
11766
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
+
11767
12029
  // src/image-editor/paint-tools.ts
12030
+ var READ4 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11768
12031
  var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
11769
12032
  var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11770
12033
  var TARGET = {
@@ -11785,10 +12048,30 @@ var BRUSH = {
11785
12048
  smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Point smoothing 0..0.95 (default 0)." },
11786
12049
  pressureSize: { type: "boolean", description: "Point pressure scales the size (default true)." },
11787
12050
  pressureOpacity: { type: "boolean", description: "Point pressure scales the opacity (default false)." },
11788
- 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." }
11789
12052
  },
11790
12053
  additionalProperties: false
11791
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
+ }
11792
12075
  function readTarget(args) {
11793
12076
  return { layerId: args.layerId, channel: args.channel };
11794
12077
  }
@@ -11796,11 +12079,11 @@ function createImagePaintTools(controller) {
11796
12079
  const paintStroke = {
11797
12080
  name: "paint_stroke",
11798
12081
  title: "Paint a brush stroke",
11799
- 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}).",
11800
12083
  inputSchema: {
11801
12084
  type: "object",
11802
12085
  properties: {
11803
- 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)." },
11804
12087
  points: {
11805
12088
  type: "array",
11806
12089
  minItems: 1,
@@ -11817,21 +12100,66 @@ function createImagePaintTools(controller) {
11817
12100
  additionalProperties: false
11818
12101
  }
11819
12102
  },
11820
- 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
+ },
11821
12107
  brush: BRUSH,
12108
+ mixer: MIXER,
11822
12109
  ...TARGET
11823
12110
  },
11824
12111
  required: ["points"],
11825
12112
  additionalProperties: false
11826
12113
  },
11827
12114
  annotations: { title: "Paint a brush stroke", ...WRITE4 },
11828
- handler: (args) => controller.paintStroke({
11829
- tool: args.tool,
11830
- points: args.points,
11831
- color: args.color,
11832
- brush: args.brush,
11833
- ...readTarget(args)
11834
- })
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())
11835
12163
  };
11836
12164
  const fill = {
11837
12165
  name: "fill_pixels",
@@ -11842,7 +12170,7 @@ function createImagePaintTools(controller) {
11842
12170
  properties: {
11843
12171
  color: { type: "string", description: "Fill colour #rrggbb or #rrggbbaa (default the foreground)." },
11844
12172
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Fill opacity 0..1 (default 1)." },
11845
- 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." },
11846
12174
  useSelection: { type: "boolean", description: "Limit to the selection when there is one (default true)." },
11847
12175
  ...TARGET
11848
12176
  },
@@ -11873,11 +12201,11 @@ function createImagePaintTools(controller) {
11873
12201
  annotations: { title: "Clear pixels", ...DESTRUCTIVE3 },
11874
12202
  handler: (args) => controller.clearPixels({ opacity: args.opacity, useSelection: args.useSelection, ...readTarget(args) })
11875
12203
  };
11876
- return [paintStroke, fill, clear];
12204
+ return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];
11877
12205
  }
11878
12206
 
11879
12207
  // src/image-editor/psd-tools.ts
11880
- var READ4 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
12208
+ var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11881
12209
  var DESTRUCTIVE4 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11882
12210
  var DATA = {
11883
12211
  type: "string",
@@ -11898,7 +12226,7 @@ function createImagePsdTools(controller) {
11898
12226
  title: "Inspect a PSD",
11899
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.",
11900
12228
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11901
- annotations: { title: "Inspect a PSD", ...READ4 },
12229
+ annotations: { title: "Inspect a PSD", ...READ5 },
11902
12230
  handler: ({ data }) => {
11903
12231
  const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
11904
12232
  return {
@@ -11935,7 +12263,7 @@ function createImagePsdTools(controller) {
11935
12263
  title: "Export the document as a PSD",
11936
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.",
11937
12265
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
11938
- annotations: { title: "Export the document as a PSD", ...READ4 },
12266
+ annotations: { title: "Export the document as a PSD", ...READ5 },
11939
12267
  handler: async () => {
11940
12268
  const res = await controller.toPsd();
11941
12269
  return {
@@ -12633,145 +12961,6 @@ async function runLayerMenuAction(controller, action) {
12633
12961
  }
12634
12962
  }
12635
12963
 
12636
- // src/image-editor/layers/paint-ui.ts
12637
- var plain2 = { kind: "plain" };
12638
- var BRUSH_OPTION_CONTROLS = [
12639
- { id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain2, unit: "px" },
12640
- { id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
12641
- { id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
12642
- { id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
12643
- { id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
12644
- ];
12645
- var PAINT_TOOLS = [
12646
- { id: "move", label: "Move", shortcut: "V" },
12647
- { id: "brush", label: "Brush", shortcut: "B" },
12648
- { id: "eraser", label: "Eraser", shortcut: "E" }
12649
- ];
12650
- var isPaintTool = (tool) => tool === "move" || tool === "brush" || tool === "eraser";
12651
- function sizeStep(size) {
12652
- return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
12653
- }
12654
- var isTyping = (t) => {
12655
- const el = t;
12656
- return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
12657
- };
12658
- function handlePaintShortcut(controller, e) {
12659
- if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
12660
- const brush = controller.getState().paint.brush;
12661
- const set = (b) => controller.setPaintOptions({ brush: b });
12662
- const key = e.key;
12663
- switch (key.toLowerCase()) {
12664
- case "b":
12665
- controller.setTool("brush");
12666
- return true;
12667
- case "e":
12668
- controller.setTool("eraser");
12669
- return true;
12670
- case "v":
12671
- controller.setTool("move");
12672
- return true;
12673
- case "d":
12674
- controller.resetPaintColors();
12675
- return true;
12676
- case "x":
12677
- controller.swapPaintColors();
12678
- return true;
12679
- }
12680
- if (key === "[" || key === "{") {
12681
- if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
12682
- else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
12683
- return true;
12684
- }
12685
- if (key === "]" || key === "}") {
12686
- if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
12687
- else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
12688
- return true;
12689
- }
12690
- if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
12691
- set({ opacity: key === "0" ? 1 : Number(key) / 10 });
12692
- return true;
12693
- }
12694
- return false;
12695
- }
12696
- function pointerPressure(e) {
12697
- if (e.pointerType === "pen" && typeof e.pressure === "number" && e.pressure > 0) return Math.min(1, e.pressure);
12698
- return 1;
12699
- }
12700
- var PaintGesture = class {
12701
- constructor(controller) {
12702
- this.controller = controller;
12703
- }
12704
- controller;
12705
- stroke = null;
12706
- move = null;
12707
- /** The last refusal (e.g. "layer is locked"), for the editor to show. */
12708
- error = null;
12709
- get active() {
12710
- return Boolean(this.stroke || this.move);
12711
- }
12712
- down(sample) {
12713
- this.error = null;
12714
- const tool = this.controller.getState().activeTool;
12715
- try {
12716
- if (tool === "move") {
12717
- const layer = this.controller.getActiveLayer();
12718
- if (!layer) throw new Error("Select a layer to move in the Layers panel.");
12719
- if (layer.type !== "pixel") throw new Error(`Only pixel layers move; "${layer.name}" is a ${layer.type} layer.`);
12720
- if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer "${layer.name}" has its position locked.`);
12721
- this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };
12722
- return null;
12723
- }
12724
- if (tool === "brush" || tool === "eraser") {
12725
- this.stroke = this.controller.beginStroke({ tool });
12726
- this.stroke.push([sample]);
12727
- return null;
12728
- }
12729
- } catch (err) {
12730
- this.error = err instanceof Error ? err.message : String(err);
12731
- this.stroke = null;
12732
- this.move = null;
12733
- }
12734
- return this.error;
12735
- }
12736
- /** Feeds pointer samples (all coalesced events of one move, in order). */
12737
- moveTo(samples) {
12738
- if (!samples.length) return;
12739
- if (this.stroke) {
12740
- this.stroke.push(samples);
12741
- return;
12742
- }
12743
- const m = this.move;
12744
- if (m) {
12745
- const last = samples[samples.length - 1];
12746
- const x = m.origin.x + Math.round(last.x - m.start.x);
12747
- const y = m.origin.y + Math.round(last.y - m.start.y);
12748
- try {
12749
- this.controller.offsetLayer(m.id, { x, y }, !m.moved);
12750
- m.moved = true;
12751
- } catch (err) {
12752
- this.error = err instanceof Error ? err.message : String(err);
12753
- this.move = null;
12754
- }
12755
- }
12756
- }
12757
- up() {
12758
- if (this.stroke) {
12759
- try {
12760
- this.stroke.end();
12761
- } catch (err) {
12762
- this.error = err instanceof Error ? err.message : String(err);
12763
- }
12764
- }
12765
- this.stroke = null;
12766
- this.move = null;
12767
- }
12768
- cancel() {
12769
- this.stroke?.cancel();
12770
- this.stroke = null;
12771
- this.move = null;
12772
- }
12773
- };
12774
-
12775
12964
  // src/image-editor/psd/ui.ts
12776
12965
  var PSD_FILE_ACCEPT = ".psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop";
12777
12966
  function blobBytes(file) {
@@ -12822,6 +13011,7 @@ export {
12822
13011
  DEFAULT_ADJUSTMENTS,
12823
13012
  DEFAULT_BRUSH,
12824
13013
  DEFAULT_DEPTH_MASK,
13014
+ DEFAULT_MIXER,
12825
13015
  DEFAULT_REFINE_EDGE_PARAMS,
12826
13016
  DEFAULT_SELECTION_TOOL_OPTIONS,
12827
13017
  DEFAULT_TRANSFORM,
@@ -12829,6 +13019,7 @@ export {
12829
13019
  DIRECT_GAUSSIAN_MAX_SIGMA,
12830
13020
  DabWalker,
12831
13021
  EMPTY_DISTANCE,
13022
+ EMPTY_MIXER_LOAD,
12832
13023
  GUIDED_FILTER_EPS,
12833
13024
  HISTOGRAM_BINS,
12834
13025
  HUE_RANGE_LABELS,
@@ -12848,6 +13039,12 @@ export {
12848
13039
  MAX_KELVIN,
12849
13040
  MIN_KELVIN,
12850
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,
12851
13048
  PAINT_TOOLS,
12852
13049
  PIGMENT_BANDS,
12853
13050
  PIGMENT_LAMBDA_MIN,
@@ -12923,6 +13120,7 @@ export {
12923
13120
  changedTiles,
12924
13121
  clampControlValue,
12925
13122
  clampCropToImage,
13123
+ cleanMixer,
12926
13124
  collectTiles,
12927
13125
  colorBalanceChannel,
12928
13126
  colorRange,
@@ -13075,13 +13273,17 @@ export {
13075
13273
  linearSrgbToXy,
13076
13274
  linearToSrgb,
13077
13275
  loadImageEditorDocument,
13276
+ loadMixer,
13078
13277
  magicWand,
13079
13278
  mapCropHandle,
13080
13279
  mapLayer,
13081
13280
  maskSummary,
13082
13281
  migrateSerializedImageEditorState,
13083
13282
  migrateToLayeredState,
13283
+ mixPigment8,
13284
+ mixPigmentRgb,
13084
13285
  mixPigments,
13286
+ mixerLoadResult,
13085
13287
  modifySelection,
13086
13288
  moveCurvePoint,
13087
13289
  moveLayer,
@@ -13096,6 +13298,7 @@ export {
13096
13298
  normalizeBrush,
13097
13299
  normalizeFilter,
13098
13300
  normalizeLayerMask,
13301
+ normalizeMixer,
13099
13302
  normalizeProcessVersion,
13100
13303
  normalizeRect,
13101
13304
  normalizeRefineEdgeParams,
@@ -13153,6 +13356,7 @@ export {
13153
13356
  rgbToGray,
13154
13357
  rgbToHsl,
13155
13358
  rgbToReflectance,
13359
+ rgbUnitToHex,
13156
13360
  runLayerMenuAction,
13157
13361
  sampleLut,
13158
13362
  sampleToneLut,