@pixerate/editor-react 0.68.1 → 0.70.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.
@@ -1,4 +1,4 @@
1
- import { PaintEditorController, PaintEditorState, PaintEditorOptions, PaintTool, SliderControl } from '@pixerate/editor/paint-editor';
1
+ import { PaintEditorController, PaintEditorState, PaintDirectorOptions, PaintPresence, PaintDirector, PaintEditorOptions, PaintTool, PaintView, SliderControl } from '@pixerate/editor/paint-editor';
2
2
  import React__default from 'react';
3
3
 
4
4
  interface UsePaintEditorReturn {
@@ -13,6 +13,15 @@ interface UsePaintEditorReturn {
13
13
  declare function usePaintEditor(options?: PaintEditorOptions | PaintEditorController): UsePaintEditorReturn;
14
14
  /** Subscribes to a controller's state. */
15
15
  declare function usePaintEditorState(controller: PaintEditorController): PaintEditorState;
16
+ /**
17
+ * A `PaintPresence` and `PaintDirector` for a paint editor: pass `presence` to `PaintEditor` (or
18
+ * `PaintCanvas`) and `director` to `createPaintEditorToolProvider`, so agent strokes are painted
19
+ * live by a cursor on the canvas. Options are read on the first render.
20
+ */
21
+ declare function usePaintDirector(controller: PaintEditorController, options?: Omit<PaintDirectorOptions, 'clock'>): {
22
+ presence: PaintPresence;
23
+ director: PaintDirector;
24
+ };
16
25
 
17
26
  interface PaintCanvasProps {
18
27
  controller: PaintEditorController;
@@ -24,6 +33,8 @@ interface PaintCanvasProps {
24
33
  fitOnMount?: boolean;
25
34
  /** Called with a refusal (hidden or locked layer). */
26
35
  onError?: (message: string | null) => void;
36
+ /** Agent cursors to draw over the canvas (see `PaintDirector` and `usePaintDirector`). */
37
+ presence?: PaintPresence;
27
38
  className?: string;
28
39
  }
29
40
  /**
@@ -34,6 +45,22 @@ interface PaintCanvasProps {
34
45
  * rotation. Guides and floating reference images are drawn on top.
35
46
  */
36
47
  declare const PaintCanvas: React__default.FC<PaintCanvasProps>;
48
+ /**
49
+ * Agent cursors and notes over the canvas. Positions are document pixels, placed through the view
50
+ * (zoom, pan, rotation, mirroring), while the cursors themselves stay upright and unscaled.
51
+ */
52
+ declare const PaintPresenceLayer: React__default.FC<{
53
+ presence: PaintPresence;
54
+ view: PaintView;
55
+ canvas: {
56
+ width: number;
57
+ height: number;
58
+ };
59
+ viewport: {
60
+ width: number;
61
+ height: number;
62
+ };
63
+ }>;
37
64
 
38
65
  interface PaintToolbarProps {
39
66
  controller: PaintEditorController;
@@ -60,8 +87,10 @@ interface BrushPanelProps {
60
87
  /**
61
88
  * The brush library (thumbnails rendered from a test stroke on the current paper, grouped by
62
89
  * category) and the active brush's settings: size, opacity, flow, hardness, spacing, roundness,
63
- * angle, paper grain, position and pressure smoothing, dynamics and rotation mode. Save the edited
64
- * brush, import and export libraries as JSON.
90
+ * angle, paper grain, position and pressure smoothing, dynamics, rotation mode and blend mode;
91
+ * then the engine's own controls (`brushEngineControls`): blender strength and blend distance,
92
+ * smudge strength and length, pick-up and mix ratios, RGB / pigment mixing, dirty brush,
93
+ * multi-colour and the brush load. Save the edited brush, import and export libraries as JSON.
65
94
  */
66
95
  declare const BrushPanel: React__default.FC<BrushPanelProps>;
67
96
 
@@ -87,7 +116,8 @@ interface ColorPanelProps {
87
116
  /**
88
117
  * Colour: a hue wheel (painter's RYB or RGB) with the chosen harmony marked on it, saturation and
89
118
  * value, the paint and second colours, harmony chips, swatches, and a mixing pad to paint on,
90
- * smear with a palette knife (pigment mixing) and pick from.
119
+ * smear with a palette knife (pigment mixing) and pick from (with a sample size), mix the paint
120
+ * and second colours in measured parts on, and keep by name.
91
121
  */
92
122
  declare const ColorPanel: React__default.FC<ColorPanelProps>;
93
123
 
@@ -108,6 +138,8 @@ interface PaintEditorProps {
108
138
  fileName?: string;
109
139
  /** Height of the editor (default 720 px). */
110
140
  height?: number | string;
141
+ /** Agent cursors to draw over the canvas (see `usePaintDirector`). */
142
+ presence?: PaintPresence;
111
143
  className?: string;
112
144
  }
113
145
  /**
@@ -127,4 +159,4 @@ declare const PaintSlider: React__default.FC<{
127
159
  onCommit?: (v: number) => void;
128
160
  }>;
129
161
 
130
- export { BrushPanel, type BrushPanelProps, ColorPanel, type ColorPanelProps, PaintCanvas, type PaintCanvasProps, PaintEditor, type PaintEditorProps, PaintLayersPanel, type PaintLayersPanelProps, PaintSlider, PaintToolbar, type PaintToolbarProps, PaperPanel, type PaperPanelProps, type UsePaintEditorReturn, usePaintEditor, usePaintEditorState };
162
+ export { BrushPanel, type BrushPanelProps, ColorPanel, type ColorPanelProps, PaintCanvas, type PaintCanvasProps, PaintEditor, type PaintEditorProps, PaintLayersPanel, type PaintLayersPanelProps, PaintPresenceLayer, PaintSlider, PaintToolbar, type PaintToolbarProps, PaperPanel, type PaperPanelProps, type UsePaintEditorReturn, usePaintDirector, usePaintEditor, usePaintEditorState };
@@ -1,6 +1,6 @@
1
1
  // src/paint-editor/usePaintEditor.ts
2
2
  import { useMemo, useState, useSyncExternalStore } from "react";
3
- import { PaintEditorController } from "@pixerate/editor/paint-editor";
3
+ import { PaintDirector, PaintEditorController, PaintPresence } from "@pixerate/editor/paint-editor";
4
4
  function usePaintEditor(options = {}) {
5
5
  const [controller] = useState(() => options instanceof PaintEditorController ? options : new PaintEditorController(options));
6
6
  const state = usePaintEditorState(controller);
@@ -13,19 +13,27 @@ function usePaintEditorState(controller) {
13
13
  () => controller.getState()
14
14
  );
15
15
  }
16
+ function usePaintDirector(controller, options = {}) {
17
+ const [value] = useState(() => {
18
+ const presence = new PaintPresence();
19
+ return { presence, director: new PaintDirector(controller, presence, options) };
20
+ });
21
+ return value;
22
+ }
16
23
 
17
24
  // src/paint-editor/PaintCanvas.tsx
18
- import { useEffect, useMemo as useMemo2, useRef, useState as useState2 } from "react";
25
+ import { useEffect, useMemo as useMemo2, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
19
26
  import {
20
27
  PaintCanvasPresenter,
21
28
  PaintGesture,
29
+ documentToScreen,
22
30
  fitZoom,
23
31
  guideOverlay,
24
32
  handlePaintShortcut,
25
33
  viewCssTransform
26
34
  } from "@pixerate/editor/paint-editor";
27
35
  import { jsx, jsxs } from "react/jsx-runtime";
28
- var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount = true, onError, className }) => {
36
+ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount = true, onError, presence, className }) => {
29
37
  const state = usePaintEditorState(controller);
30
38
  const viewport = useRef(null);
31
39
  const canvas = useRef(null);
@@ -140,6 +148,7 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
140
148
  predicted.length > 0 && /* @__PURE__ */ jsx("polyline", { points: predicted.map((p) => `${p.x},${p.y}`).join(" "), fill: "none", stroke: state.color, strokeOpacity: 0.5, strokeWidth: Math.max(1, state.brush.tip.size / 2), strokeLinecap: "round" })
141
149
  ] }),
142
150
  state.guides.references.filter((r) => r.visible).map((r) => /* @__PURE__ */ jsx(ReferencePanel, { controller, id: r.id }, r.id)),
151
+ presence && /* @__PURE__ */ jsx(PaintPresenceLayer, { presence, view: state.view, canvas: doc, viewport: size }),
143
152
  /* @__PURE__ */ jsxs("div", { className: "absolute bottom-1 right-2 text-[10px] text-slate-500 pointer-events-none", "data-testid": "paint-view-status", children: [
144
153
  Math.round(state.view.zoom * 100),
145
154
  "% \xB7 ",
@@ -151,6 +160,16 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
151
160
  }
152
161
  );
153
162
  };
163
+ var PaintPresenceLayer = ({ presence, view, canvas, viewport }) => {
164
+ const { actors } = useSyncExternalStore2(presence.subscribe, presence.getState, presence.getState);
165
+ return /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", "data-paint-presence": "", "aria-hidden": "true", children: actors.map((a) => {
166
+ const p = documentToScreen(view, canvas, viewport, a.x, a.y);
167
+ return /* @__PURE__ */ jsxs("div", { className: "absolute", style: { left: p.x, top: p.y }, "data-actor": a.id, children: [
168
+ /* @__PURE__ */ jsx("svg", { width: "18", height: "20", viewBox: "0 0 18 20", className: "drop-shadow-md", style: { transform: a.pressed ? "scale(0.85)" : void 0, transformOrigin: "0 0" }, children: /* @__PURE__ */ jsx("path", { d: "M0 0 L0 15 L4.5 10.8 L7.6 17 L10.2 15.8 L7.2 9.7 L13.5 9.4 Z", fill: a.color, stroke: "white", strokeWidth: "1.4", strokeLinejoin: "round" }) }),
169
+ /* @__PURE__ */ jsx("span", { className: "absolute left-4 top-4 w-max max-w-[240px] rounded-md px-2 py-0.5 text-[11px] font-semibold text-white shadow", style: { background: a.color }, children: a.message ?? a.name })
170
+ ] }, a.id);
171
+ }) });
172
+ };
154
173
  var ReferencePanel = ({ controller, id }) => {
155
174
  const state = usePaintEditorState(controller);
156
175
  const ref = state.guides.references.find((r) => r.id === id);
@@ -458,8 +477,19 @@ var PaintToolbar = ({ controller, tool, onToolChange, fileName = "painting", cla
458
477
 
459
478
  // src/paint-editor/BrushPanel.tsx
460
479
  import { useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
461
- import { BRUSH_ROTATIONS, BRUSH_SLIDERS, downloadBytes as downloadBytes2, patchPath, readPath } from "@pixerate/editor/paint-editor";
462
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
480
+ import {
481
+ BLEND_MENU_GROUPS,
482
+ BRUSH_ROTATIONS,
483
+ BRUSH_SLIDERS,
484
+ brushEngineControls,
485
+ brushLoadColor,
486
+ downloadBytes as downloadBytes2,
487
+ multiColorSliders,
488
+ patchBrushValue,
489
+ patchPath,
490
+ readPath
491
+ } from "@pixerate/editor/paint-editor";
492
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
463
493
  var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, className }) => {
464
494
  const state = usePaintEditorState(controller);
465
495
  const [error, attempt] = useAttempt();
@@ -473,6 +503,10 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
473
503
  // eslint-disable-next-line react-hooks/exhaustive-deps
474
504
  [controller, brushes, paper, thumbnailWidth, thumbnailHeight]
475
505
  );
506
+ const engine = brushEngineControls(brush);
507
+ const mc = brush.behavior.multicolor;
508
+ const carried = brushLoadColor(state.mixerLoad);
509
+ const setColors = (colors) => controller.setBrush({ behavior: { multicolor: { colors } } });
476
510
  const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "brush";
477
511
  return /* @__PURE__ */ jsxs4("div", { className: `${panel} ${className ?? ""}`, "data-testid": "brush-panel", children: [
478
512
  /* @__PURE__ */ jsx4("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ jsxs4(
@@ -504,6 +538,68 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
504
538
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2", children: [
505
539
  /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
506
540
  " Pressure \u2192 opacity"
541
+ ] }),
542
+ brush.engine === "dab" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
543
+ /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Blend" }),
544
+ /* @__PURE__ */ jsx4("select", { "aria-label": "Brush blend", value: brush.tip.blend, onChange: (e) => controller.setBrush({ tip: { blend: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ jsx4("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx4("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label)) })
545
+ ] })
546
+ ] }),
547
+ engine && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-engine", children: [
548
+ /* @__PURE__ */ jsx4("span", { className: "font-medium text-slate-300", children: engine.title }),
549
+ engine.sliders.map((c) => /* @__PURE__ */ jsx4(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path)),
550
+ engine.mixingPath && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
551
+ /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Mixing" }),
552
+ /* @__PURE__ */ jsxs4(
553
+ "select",
554
+ {
555
+ "aria-label": "Brush mixing",
556
+ value: engine.mixingPath === "blender.mixing" ? brush.blender.mixing : engine.mixingPath === "smudge.mixing" ? brush.smudge.mixing : brush.behavior.mixing,
557
+ onChange: (e) => controller.setBrush(patchBrushValue(brush, engine.mixingPath, e.target.value)),
558
+ className: "rounded bg-slate-800 px-1 py-0.5",
559
+ children: [
560
+ /* @__PURE__ */ jsx4("option", { value: "pigment", children: "Pigment" }),
561
+ /* @__PURE__ */ jsx4("option", { value: "rgb", children: "RGB" })
562
+ ]
563
+ }
564
+ )
565
+ ] }),
566
+ engine.dirty && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2", title: "Keep the colour the brush picked up for the next stroke (needs Pick-up above 0)", children: [
567
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", "aria-label": "Dirty brush", checked: brush.behavior.dirty, onChange: (e) => controller.setBrush({ behavior: { dirty: e.target.checked } }) }),
568
+ " Dirty brush"
569
+ ] }),
570
+ engine.multicolor && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1.5", "data-testid": "multicolor", children: [
571
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
572
+ /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Multi-colour" }),
573
+ /* @__PURE__ */ jsxs4("select", { "aria-label": "Multi-colour", value: mc.mode, onChange: (e) => controller.setBrush({ behavior: { multicolor: { mode: e.target.value } } }), className: "rounded bg-slate-800 px-1 py-0.5", children: [
574
+ /* @__PURE__ */ jsx4("option", { value: "off", children: "Off" }),
575
+ /* @__PURE__ */ jsx4("option", { value: "tip", children: "Across the tip" }),
576
+ /* @__PURE__ */ jsx4("option", { value: "dab", children: "Per dab" })
577
+ ] })
578
+ ] }),
579
+ mc.mode !== "off" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
580
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", "aria-label": "Multi-colour colours", children: [
581
+ mc.colors.map((col, i) => /* @__PURE__ */ jsxs4("span", { className: "flex items-center", children: [
582
+ /* @__PURE__ */ jsx4("input", { type: "color", "aria-label": `Multi-colour ${i + 1}`, value: col, onChange: (e) => setColors(mc.colors.map((x, j) => j === i ? e.target.value : x)), className: "h-5 w-6 rounded bg-transparent" }),
583
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": `Remove multi-colour ${i + 1}`, className: "px-0.5 text-slate-500 hover:text-white", onClick: () => setColors(mc.colors.filter((_, j) => j !== i)), children: "\xD7" })
584
+ ] }, i)),
585
+ mc.colors.length < 8 && /* @__PURE__ */ jsx4("button", { type: "button", className: button, "aria-label": "Add multi-colour", onClick: () => setColors([...mc.colors.length ? mc.colors : [state.color, state.secondaryColor], state.color].slice(0, 8)), children: "+" }),
586
+ mc.colors.length < 2 && /* @__PURE__ */ jsx4("span", { className: "text-[10px] text-slate-500", children: "Uses the paint and second colours" })
587
+ ] }),
588
+ mc.mode === "dab" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
589
+ /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Order" }),
590
+ /* @__PURE__ */ jsxs4("select", { "aria-label": "Colour order", value: mc.order, onChange: (e) => controller.setBrush({ behavior: { multicolor: { order: e.target.value } } }), className: "rounded bg-slate-800 px-1 py-0.5", children: [
591
+ /* @__PURE__ */ jsx4("option", { value: "sequence", children: "In order" }),
592
+ /* @__PURE__ */ jsx4("option", { value: "random", children: "Random" })
593
+ ] })
594
+ ] }),
595
+ multiColorSliders(mc.mode).map((c) => /* @__PURE__ */ jsx4(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path))
596
+ ] })
597
+ ] }),
598
+ engine.load && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
599
+ /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Carries" }),
600
+ /* @__PURE__ */ jsx4("span", { "aria-label": "Brush load", title: carried ?? "Clean", className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: carried ?? "transparent" } }),
601
+ brush.engine === "mixer" && /* @__PURE__ */ jsx4("button", { type: "button", className: button, onClick: () => controller.loadMixerBrush(), children: "Load" }),
602
+ /* @__PURE__ */ jsx4("button", { type: "button", className: button, onClick: () => controller.cleanMixerBrush(brush.engine === "mixer"), children: "Clean" })
507
603
  ] })
508
604
  ] }),
509
605
  /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", children: [
@@ -579,7 +675,9 @@ import {
579
675
  HARMONY_KINDS,
580
676
  MIXING_PAD_HEIGHT,
581
677
  MIXING_PAD_WIDTH,
678
+ PAD_SAMPLE_SIZES,
582
679
  hsvToRgb,
680
+ padSampleRadius,
583
681
  hueToWheel,
584
682
  parseHexColor,
585
683
  renderColorWheel,
@@ -595,6 +693,9 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
595
693
  const [wheel, setWheel] = useState5("ryb");
596
694
  const [kind, setKind] = useState5("complementary");
597
695
  const [padTool, setPadTool] = useState5("paint");
696
+ const [sample, setSample] = useState5(3);
697
+ const [parts, setParts] = useState5([1, 1]);
698
+ const [padName, setPadName] = useState5("");
598
699
  const padPoints = useRef5(null);
599
700
  const ring = useMemo5(() => renderColorWheel(wheelSize, wheel), [wheelSize, wheel]);
600
701
  const color = state.color;
@@ -706,7 +807,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
706
807
  onPointerDown: (e) => {
707
808
  const p = padPoint(e);
708
809
  if (padTool === "pick") {
709
- attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true }));
810
+ attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true, radius: padSampleRadius(sample) }));
710
811
  return;
711
812
  }
712
813
  padPoints.current = [p];
@@ -720,7 +821,44 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
720
821
  },
721
822
  children: /* @__PURE__ */ jsx6(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${MIXING_PAD_WIDTH} / ${MIXING_PAD_HEIGHT}` } })
722
823
  }
723
- )
824
+ ),
825
+ /* @__PURE__ */ jsx6("div", { className: "flex flex-wrap items-center gap-1", children: /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1", children: [
826
+ /* @__PURE__ */ jsx6("span", { className: "text-slate-400", children: "Sample" }),
827
+ /* @__PURE__ */ jsx6("select", { "aria-label": "Pick sample size", value: sample, onChange: (e) => setSample(Number(e.target.value)), className: "rounded bg-slate-800 px-1 py-0.5", children: PAD_SAMPLE_SIZES.map((n) => /* @__PURE__ */ jsx6("option", { value: n, children: n === 1 ? "1 px" : `${n}\xD7${n}` }, n)) })
828
+ ] }) }),
829
+ /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-1", children: [
830
+ /* @__PURE__ */ jsx6("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: color } }),
831
+ /* @__PURE__ */ jsx6("input", { type: "number", "aria-label": "Paint colour parts", min: 0, max: 20, value: parts[0], onChange: (e) => setParts([Number(e.target.value), parts[1]]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
832
+ /* @__PURE__ */ jsx6("span", { className: "text-slate-500", children: ":" }),
833
+ /* @__PURE__ */ jsx6("input", { type: "number", "aria-label": "Second colour parts", min: 0, max: 20, value: parts[1], onChange: (e) => setParts([parts[0], Number(e.target.value)]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
834
+ /* @__PURE__ */ jsx6("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: state.secondaryColor } }),
835
+ /* @__PURE__ */ jsx6(
836
+ "button",
837
+ {
838
+ type: "button",
839
+ className: button,
840
+ title: "Mix the paint and second colours in these parts on the pad, and use the mix",
841
+ onClick: () => attempt(
842
+ () => controller.mixOnPad(
843
+ [
844
+ { color, parts: parts[0] },
845
+ { color: state.secondaryColor, parts: parts[1] }
846
+ ],
847
+ { setColor: true }
848
+ )
849
+ ),
850
+ children: "Mix"
851
+ }
852
+ )
853
+ ] }),
854
+ /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-1", children: [
855
+ /* @__PURE__ */ jsx6("input", { value: padName, placeholder: "Pad name", "aria-label": "Pad name", onChange: (e) => setPadName(e.target.value), className: "w-24 rounded bg-slate-800 px-1.5 py-0.5" }),
856
+ /* @__PURE__ */ jsx6("button", { type: "button", className: button, onClick: () => attempt(() => (controller.saveMixingPad(padName || void 0), setPadName(""))), children: "Keep pad" })
857
+ ] }),
858
+ state.savedMixingPads.length > 0 && /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-0.5", "aria-label": "Kept pads", children: state.savedMixingPads.map((p) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-1", children: [
859
+ /* @__PURE__ */ jsx6("button", { type: "button", className: `${button} flex-1 text-left`, onClick: () => attempt(() => controller.loadMixingPad(p.id)), children: p.name }),
860
+ /* @__PURE__ */ jsx6("button", { type: "button", className: button, "aria-label": `Remove ${p.name}`, onClick: () => attempt(() => controller.removeMixingPad(p.id)), children: "\xD7" })
861
+ ] }, p.id)) })
724
862
  ] }),
725
863
  /* @__PURE__ */ jsx6(ErrorLine, { error })
726
864
  ] });
@@ -728,7 +866,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
728
866
 
729
867
  // src/paint-editor/PaintLayersPanel.tsx
730
868
  import { useState as useState6 } from "react";
731
- import { BLEND_MENU_GROUPS } from "@pixerate/editor/paint-editor";
869
+ import { BLEND_MENU_GROUPS as BLEND_MENU_GROUPS2 } from "@pixerate/editor/paint-editor";
732
870
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
733
871
  function siblingIndex(layers, l) {
734
872
  const sibs = layers.filter((x) => x.parentId === l.parentId).reverse();
@@ -825,7 +963,7 @@ var PaintLayersPanel = ({ controller, className }) => {
825
963
  /* @__PURE__ */ jsx7("span", { className: "w-14 text-slate-400", children: "Blend" }),
826
964
  /* @__PURE__ */ jsxs7("select", { "aria-label": "Layer blend", value: active.blend, onChange: (e) => attempt(() => controller.updateLayer(active.id, { blend: e.target.value })), className: "flex-1 rounded bg-slate-800 px-1 py-0.5", children: [
827
965
  active.type === "group" && /* @__PURE__ */ jsx7("option", { value: "pass-through", children: "Pass through" }),
828
- BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ jsx7("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx7("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label))
966
+ BLEND_MENU_GROUPS2.map((g) => /* @__PURE__ */ jsx7("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx7("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label))
829
967
  ] })
830
968
  ] }),
831
969
  /* @__PURE__ */ jsxs7("div", { className: "flex gap-1", children: [
@@ -840,7 +978,7 @@ var PaintLayersPanel = ({ controller, className }) => {
840
978
  // src/paint-editor/PaintEditor.tsx
841
979
  import { useState as useState7 } from "react";
842
980
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
843
- var PaintEditor = ({ controller, fileName, height = 720, className }) => {
981
+ var PaintEditor = ({ controller, fileName, height = 720, presence, className }) => {
844
982
  const [tool, setTool] = useState7("brush");
845
983
  const [tab, setTab] = useState7("color");
846
984
  const [error, setError] = useState7(null);
@@ -849,7 +987,7 @@ var PaintEditor = ({ controller, fileName, height = 720, className }) => {
849
987
  /* @__PURE__ */ jsxs8("div", { className: "flex min-h-0 flex-1 gap-2", children: [
850
988
  /* @__PURE__ */ jsx8(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
851
989
  /* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-1 flex-col", children: [
852
- /* @__PURE__ */ jsx8(PaintCanvas, { controller, tool, onError: setError, className: "flex-1 rounded-lg" }),
990
+ /* @__PURE__ */ jsx8(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
853
991
  error && /* @__PURE__ */ jsx8("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
854
992
  ] }),
855
993
  /* @__PURE__ */ jsxs8("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
@@ -867,9 +1005,11 @@ export {
867
1005
  PaintCanvas,
868
1006
  PaintEditor,
869
1007
  PaintLayersPanel,
1008
+ PaintPresenceLayer,
870
1009
  PaintSlider,
871
1010
  PaintToolbar,
872
1011
  PaperPanel,
1012
+ usePaintDirector,
873
1013
  usePaintEditor,
874
1014
  usePaintEditorState
875
1015
  };