@pixerate/editor-react 0.74.0 → 0.75.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.
@@ -29,6 +29,8 @@ __export(paint_editor_exports, {
29
29
  PaintSlider: () => PaintSlider,
30
30
  PaintToolbar: () => PaintToolbar,
31
31
  PaperPanel: () => PaperPanel,
32
+ StickerPanel: () => StickerPanel,
33
+ WetPanel: () => WetPanel,
32
34
  usePaintDirector: () => usePaintDirector,
33
35
  usePaintEditor: () => usePaintEditor,
34
36
  usePaintEditorState: () => usePaintEditorState
@@ -66,6 +68,7 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
66
68
  const state = usePaintEditorState(controller);
67
69
  const viewport = (0, import_react2.useRef)(null);
68
70
  const canvas = (0, import_react2.useRef)(null);
71
+ const water = (0, import_react2.useRef)(null);
69
72
  const [size, setSize] = (0, import_react2.useState)({ width: 800, height: 600 });
70
73
  const measured = (0, import_react2.useRef)(false);
71
74
  const [predicted, setPredicted] = (0, import_react2.useState)([]);
@@ -105,6 +108,11 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
105
108
  if (ctx && typeof ctx.createImageData === "function") presenter.render(ctx);
106
109
  else controller.takeDirty();
107
110
  }, [state, presenter, controller]);
111
+ (0, import_react2.useEffect)(() => (0, import_paint_editor2.startWetClock)(controller), [controller]);
112
+ (0, import_react2.useEffect)(() => {
113
+ const ctx = water.current?.getContext("2d");
114
+ if (ctx && typeof ctx.createImageData === "function") (0, import_paint_editor2.drawWaterOverlay)(controller, ctx);
115
+ }, [state, controller]);
108
116
  (0, import_react2.useEffect)(() => {
109
117
  const el = viewport.current;
110
118
  if (!el) return;
@@ -159,6 +167,17 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
159
167
  style: { transform, transformOrigin: "0 0", imageRendering: state.view.zoom > 2 ? "pixelated" : "auto" }
160
168
  }
161
169
  ),
170
+ state.wet.overlay && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
171
+ "canvas",
172
+ {
173
+ ref: water,
174
+ width: doc.width,
175
+ height: doc.height,
176
+ "data-testid": "paint-water-overlay",
177
+ className: "absolute left-0 top-0 pointer-events-none opacity-60",
178
+ style: { transform, transformOrigin: "0 0" }
179
+ }
180
+ ),
162
181
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "absolute left-0 top-0 pointer-events-none overflow-visible", width: doc.width, height: doc.height, style: { transform, transformOrigin: "0 0" }, "aria-hidden": "true", children: [
163
182
  lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
164
183
  "line",
@@ -167,8 +186,8 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
167
186
  y1: l.from.y,
168
187
  x2: l.to.x,
169
188
  y2: l.to.y,
170
- stroke: l.kind === "symmetry" ? "#22d3ee" : "#f472b6",
171
- strokeOpacity: l.kind === "symmetry" ? 0.8 : 0.35,
189
+ stroke: l.kind === "symmetry" ? "#22d3ee" : l.kind === "stencil" ? "#fbbf24" : "#f472b6",
190
+ strokeOpacity: l.kind === "symmetry" || l.kind === "stencil" ? 0.8 : 0.35,
172
191
  strokeWidth: 1 / state.view.zoom,
173
192
  strokeDasharray: l.kind === "symmetry" ? `${6 / state.view.zoom} ${4 / state.view.zoom}` : void 0
174
193
  },
@@ -352,6 +371,8 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
352
371
  var PaintToolbar = ({ controller, tool, onToolChange, fileName = "painting", className }) => {
353
372
  const state = usePaintEditorState(controller);
354
373
  const [error, attempt] = useAttempt();
374
+ const [busy, setBusy] = (0, import_react4.useState)(null);
375
+ const [note, setNote] = (0, import_react4.useState)(null);
355
376
  const openRef = (0, import_react4.useRef)(null);
356
377
  const tracingRef = (0, import_react4.useRef)(null);
357
378
  const refRef = (0, import_react4.useRef)(null);
@@ -448,6 +469,12 @@ var PaintToolbar = ({ controller, tool, onToolChange, fileName = "painting", cla
448
469
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => controller.clearTracingImage(), children: "Remove tracing" })
449
470
  ] }),
450
471
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => refRef.current?.click(), children: "Reference\u2026" }),
472
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "flex items-center gap-1", role: "group", "aria-label": "Stencils", children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, title: "A ruler: strokes that start near it follow it", onClick: () => attempt(() => controller.addStencil((0, import_paint_editor3.quickStencil)("ruler", canvas))), children: "Ruler" }),
474
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, title: "A circle guide: strokes that start near it follow it", onClick: () => attempt(() => controller.addStencil((0, import_paint_editor3.quickStencil)("circle", canvas))), children: "Circle" }),
475
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, title: "An ellipse mask: paint lands only inside it", onClick: () => attempt(() => controller.addStencil((0, import_paint_editor3.quickStencil)("ellipse-mask", canvas))), children: "Mask" }),
476
+ state.guides.stencils.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.removeStencils()), children: "Clear stencils" })
477
+ ] }),
451
478
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, title: "Which hand holds the pen (palm rejection)", onClick: () => controller.setHand(state.hand === "right" ? "left" : "right"), children: state.hand === "right" ? "Right-handed" : "Left-handed" }),
452
479
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-1 h-4 w-px bg-slate-700" }),
453
480
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (0, import_paint_editor3.downloadBytes)(JSON.stringify(controller.toJSON()), `${fileName}.paint.json`, "application/json")), children: "Save" }),
@@ -455,6 +482,45 @@ var PaintToolbar = ({ controller, tool, onToolChange, fileName = "painting", cla
455
482
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => exportImage("image/png"), children: "PNG" }),
456
483
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => exportImage("image/jpeg"), children: "JPEG" }),
457
484
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (0, import_paint_editor3.downloadBytes)(controller.exportPsd().bytes, `${fileName}.psd`, "image/vnd.adobe.photoshop")), children: "PSD" }),
485
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
486
+ "button",
487
+ {
488
+ type: "button",
489
+ className: button,
490
+ disabled: busy !== null,
491
+ title: "Repaint the painting from its strokes as a time-lapse: frames, a video project and the ffmpeg command, zipped",
492
+ onClick: () => attempt(async () => {
493
+ setBusy("Repainting\u2026");
494
+ try {
495
+ const tl = await controller.timeLapse({ onProgress: (p) => setBusy(`Repainting\u2026 ${Math.round(p * 100)}%`) });
496
+ (0, import_paint_editor3.downloadBytes)(await (0, import_paint_editor3.timeLapseZip)(tl), `${fileName}-timelapse.zip`, "application/zip");
497
+ setNote(`Time-lapse: ${tl.frames.length} frames, ${(tl.durationMs / 1e3).toFixed(1)} s (run render.sh in the zip for the MP4).`);
498
+ } finally {
499
+ setBusy(null);
500
+ }
501
+ }),
502
+ children: "Time-lapse"
503
+ }
504
+ ),
505
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
506
+ "button",
507
+ {
508
+ type: "button",
509
+ className: button,
510
+ disabled: busy !== null,
511
+ title: "Repaint the painting from its strokes at twice the size (more detail, not upscaled)",
512
+ onClick: () => attempt(async () => {
513
+ setBusy("Repainting at 2\xD7\u2026");
514
+ try {
515
+ (0, import_paint_editor3.downloadBytes)(await controller.exportHighResPng({ scale: 2 }), `${fileName}@2x.png`, "image/png");
516
+ } finally {
517
+ setBusy(null);
518
+ }
519
+ }),
520
+ children: "PNG 2\xD7"
521
+ }
522
+ ),
523
+ (busy ?? note) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[11px] text-slate-400", "data-testid": "paint-replay-status", children: busy ?? note }),
458
524
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
459
525
  "input",
460
526
  {
@@ -564,6 +630,39 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
564
630
  ] }),
565
631
  engine && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-engine", children: [
566
632
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium text-slate-300", children: engine.title }),
633
+ engine.wetTool && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
634
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Wet tool" }),
635
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Wet tool", value: brush.wet.tool, onChange: (e) => controller.setBrush({ wet: { tool: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.WET_TOOLS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: t, children: import_paint_editor4.WET_TOOL_LABELS[t] }, t)) })
636
+ ] }),
637
+ engine.wetTool && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
638
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Medium" }),
639
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Wet medium", value: brush.wet.medium, onChange: (e) => controller.setBrush({ wet: { medium: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.WET_MEDIA.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: m, children: import_paint_editor4.WET_MEDIUM_LABELS[m] }, m)) })
640
+ ] }),
641
+ engine.particleMenus && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
642
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
643
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Particles" }),
644
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Particle kind", value: brush.particle.kind, onChange: (e) => controller.setBrush({ particle: { kind: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.PARTICLE_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: k, children: import_paint_editor4.PARTICLE_KIND_LABELS[k] }, k)) })
645
+ ] }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
647
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Drawn as" }),
648
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Particle render", value: brush.particle.render, onChange: (e) => controller.setBrush({ particle: { render: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.PARTICLE_RENDERS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: k, children: import_paint_editor4.PARTICLE_RENDER_LABELS[k] }, k)) })
649
+ ] })
650
+ ] }),
651
+ engine.stickerStamps && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Sticker stamps", children: import_paint_editor4.STAMP_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
652
+ "button",
653
+ {
654
+ type: "button",
655
+ "aria-pressed": brush.sticker.stamps.includes(k),
656
+ className: `px-1.5 py-0.5 rounded text-[10px] ${brush.sticker.stamps.includes(k) ? "bg-indigo-600 text-white" : "bg-slate-800 text-slate-300"}`,
657
+ onClick: () => controller.setBrush({ sticker: { stamps: (0, import_paint_editor4.toggleStamp)(brush.sticker.stamps, k) } }),
658
+ children: import_paint_editor4.STAMP_LABELS[k]
659
+ },
660
+ k
661
+ )) }),
662
+ engine.impastoTool && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
663
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Tool" }),
664
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Impasto tool", value: brush.impasto.tool, onChange: (e) => controller.setBrush({ impasto: { tool: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.IMPASTO_TOOLS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: t, children: import_paint_editor4.IMPASTO_TOOL_LABELS[t] }, t)) })
665
+ ] }),
567
666
  engine.sliders.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor4.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor4.patchPath)(brush, c.path, v)) }, c.path)),
568
667
  engine.mixingPath && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
569
668
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Mixing" }),
@@ -683,6 +782,11 @@ var PaperPanel = ({ controller, className }) => {
683
782
  " Mirror tiling"
684
783
  ] }),
685
784
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "self-start px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700", onClick: () => attempt(() => controller.setPaper({ seed: paper.seed + 1 })), children: "New sheet" }),
785
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "light-panel", children: [
786
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium text-slate-300", children: "Light (thick paint, metallic paper)" }),
787
+ import_paint_editor5.LIGHT_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor5.readPath)(controller.getLighting(), c.path), onChange: (v) => attempt(() => controller.setLighting({ [c.path]: v })) }, c.path)),
788
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "self-start px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700", onClick: () => attempt(() => controller.flattenRelief()), children: "Flatten relief" })
789
+ ] }),
686
790
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ErrorLine, { error })
687
791
  ] });
688
792
  };
@@ -979,26 +1083,117 @@ var PaintLayersPanel = ({ controller, className }) => {
979
1083
  ] });
980
1084
  };
981
1085
 
982
- // src/paint-editor/PaintEditor.tsx
983
- var import_react9 = require("react");
1086
+ // src/paint-editor/WetPanel.tsx
1087
+ var import_paint_editor8 = require("@pixerate/editor/paint-editor");
984
1088
  var import_jsx_runtime8 = require("react/jsx-runtime");
1089
+ var FLOW_LABELS = { none: "None", linear: "One way", swirl: "Swirl", radial: "Outwards", turbulent: "Eddies" };
1090
+ var WetPanel = ({ controller, className }) => {
1091
+ const state = usePaintEditorState(controller);
1092
+ const [error, attempt] = useAttempt();
1093
+ const wet = state.wet;
1094
+ const layerId = state.document.activeLayerId;
1095
+ const info = layerId ? controller.wetLayerInfo(layerId) : null;
1096
+ const backend = controller.wetState();
1097
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "wet-panel", children: [
1098
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center justify-between", children: [
1099
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: wet.wet ? "text-sky-300" : "text-slate-400", "data-testid": "wet-status", children: wet.wet ? wet.paused ? "Wet (paused)" : "Wet" : "Dry" }),
1100
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "text-[10px] text-slate-500", children: [
1101
+ "step ",
1102
+ wet.clock
1103
+ ] })
1104
+ ] }),
1105
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1106
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, disabled: !wet.layerIds.length, onClick: () => attempt(async () => (await controller.wetIdle(), controller.dryPaint())), children: "Dry now" }),
1107
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wet.paused), "aria-pressed": wet.paused, onClick: () => controller.setWetPaused(!wet.paused), children: wet.paused ? "Resume" : "Pause" }),
1108
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wet.overlay), "aria-pressed": wet.overlay, onClick: () => controller.setWaterOverlay(!wet.overlay), children: "Water" }),
1109
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wet.instantDry), "aria-pressed": wet.instantDry, title: "Each wet stroke dries as soon as it ends", onClick: () => controller.setInstantDry(!wet.instantDry), children: "Instant dry" }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, disabled: !layerId, onClick: () => attempt(async () => (await controller.wetIdle(), controller.wetPaper({}))), children: "Wet paper" })
1111
+ ] }),
1112
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintSlider, { control: import_paint_editor8.WET_SPEED_SLIDER, value: wet.speed, onChange: (v) => controller.setWetSpeed(v) }),
1113
+ import_paint_editor8.WET_TILT_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor8.readPath)(wet.tilt, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setCanvasTilt({ [c.path]: v }))) }, c.path)),
1114
+ info && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "wet-layer", children: [
1115
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-medium text-slate-300", children: "This layer" }),
1116
+ import_paint_editor8.WET_PARAM_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor8.readPath)(info.params, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setWetParams(layerId, { [c.path]: v }))) }, c.path)),
1117
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1118
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-slate-400", children: "Flow map" }),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("select", { "aria-label": "Flow map", value: info.flow.kind, onChange: (e) => attempt(async () => (await controller.wetIdle(), controller.setFlowMap(layerId, { kind: e.target.value }))), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor8.WET_FLOW_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: k, children: FLOW_LABELS[k] }, k)) })
1120
+ ] }),
1121
+ info.flow.kind !== "none" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1122
+ PaintSlider,
1123
+ {
1124
+ control: { path: "strength", label: "Flow strength", min: 0, max: 1, step: 0.01 },
1125
+ value: info.flow.strength,
1126
+ onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setFlowMap(layerId, { strength: v })))
1127
+ }
1128
+ )
1129
+ ] }),
1130
+ backend.message && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-[10px] text-amber-300", "data-testid": "wet-message", children: backend.message }),
1131
+ backend.backend && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-[10px] text-slate-500", children: backend.backend.kind === "wasm-threads" ? `WebAssembly, ${backend.backend.threads} threads` : backend.backend.kind === "wasm" ? "WebAssembly, one thread" : "JavaScript" }),
1132
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorLine, { error })
1133
+ ] });
1134
+ };
1135
+
1136
+ // src/paint-editor/StickerPanel.tsx
1137
+ var import_react9 = require("react");
1138
+ var import_paint_editor9 = require("@pixerate/editor/paint-editor");
1139
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1140
+ var StickerPanel = ({ controller, className }) => {
1141
+ const state = usePaintEditorState(controller);
1142
+ const [error, attempt] = useAttempt();
1143
+ const [picked, setPicked] = (0, import_react9.useState)(null);
1144
+ const layerId = state.document.activeLayerId;
1145
+ const stickers = layerId ? state.document.stickers?.[layerId] ?? [] : [];
1146
+ if (!stickers.length) return null;
1147
+ const sel = stickers.find((s) => s.id === picked) ?? null;
1148
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "sticker-panel", children: [
1149
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "font-medium text-slate-300", children: [
1150
+ "Stickers (",
1151
+ stickers.length,
1152
+ ")"
1153
+ ] }),
1154
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex max-h-24 flex-wrap gap-1 overflow-y-auto", role: "listbox", "aria-label": "Stickers", children: stickers.map((s) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", role: "option", "aria-selected": s.id === picked, title: `${import_paint_editor9.STAMP_LABELS[s.stamp]} ${s.id}`, className: `h-5 w-5 rounded ring-1 ${s.id === picked ? "ring-indigo-400" : "ring-slate-700"}`, style: { background: s.color }, onClick: () => setPicked(s.id) }, s.id)) }),
1155
+ sel && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "sticker-edit", children: [
1156
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1157
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-slate-400", children: "Stamp" }),
1158
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("select", { "aria-label": "Sticker stamp", value: sel.stamp, onChange: (e) => attempt(() => controller.updateSticker(layerId, sel.id, { stamp: e.target.value })), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor9.STAMP_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: k, children: import_paint_editor9.STAMP_LABELS[k] }, k)) })
1159
+ ] }),
1160
+ import_paint_editor9.STICKER_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor9.readPath)(sel, c.path), onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { [c.path]: v })) }, c.path)),
1161
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between", children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-slate-400", children: "Colour" }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CommitColorInput, { label: "Sticker colour", value: sel.color, onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { color: v })), className: "h-6 w-10 rounded bg-transparent" })
1164
+ ] }),
1165
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1166
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "front")), children: "To front" }),
1167
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "back")), children: "To back" }),
1168
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (controller.removeStickers(layerId, [sel.id]), setPicked(null))), children: "Delete" })
1169
+ ] })
1170
+ ] }),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorLine, { error })
1172
+ ] });
1173
+ };
1174
+
1175
+ // src/paint-editor/PaintEditor.tsx
1176
+ var import_react10 = require("react");
1177
+ var import_jsx_runtime10 = require("react/jsx-runtime");
985
1178
  var PaintEditor = ({ controller, fileName, height = 720, presence, className }) => {
986
- const [tool, setTool] = (0, import_react9.useState)("brush");
987
- const [tab, setTab] = (0, import_react9.useState)("color");
988
- const [error, setError] = (0, import_react9.useState)(null);
989
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className ?? ""}`, style: { height }, "data-testid": "paint-editor", children: [
990
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintToolbar, { controller, tool, onToolChange: setTool, fileName }),
991
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-h-0 flex-1 gap-2", children: [
992
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
993
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [
994
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
995
- error && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
1179
+ const [tool, setTool] = (0, import_react10.useState)("brush");
1180
+ const [tab, setTab] = (0, import_react10.useState)("color");
1181
+ const [error, setError] = (0, import_react10.useState)(null);
1182
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: `flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className ?? ""}`, style: { height }, "data-testid": "paint-editor", children: [
1183
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintToolbar, { controller, tool, onToolChange: setTool, fileName }),
1184
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-h-0 flex-1 gap-2", children: [
1185
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
1186
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [
1187
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
1188
+ error && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
996
1189
  ] }),
997
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
998
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex gap-1", role: "tablist", "aria-label": "Paint panels", children: ["color", "paper", "layers"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === t, className: chip(tab === t), onClick: () => setTab(t), children: t === "color" ? "Colour" : t === "paper" ? "Paper" : "Layers" }, t)) }),
999
- tab === "color" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ColorPanel, { controller }),
1000
- tab === "paper" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaperPanel, { controller }),
1001
- tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintLayersPanel, { controller })
1190
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
1191
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex gap-1", role: "tablist", "aria-label": "Paint panels", children: ["color", "wet", "paper", "layers"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === t, className: chip(tab === t), onClick: () => setTab(t), children: t === "color" ? "Colour" : t === "wet" ? "Wet" : t === "paper" ? "Paper" : "Layers" }, t)) }),
1192
+ tab === "color" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ColorPanel, { controller }),
1193
+ tab === "wet" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(WetPanel, { controller }),
1194
+ tab === "paper" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaperPanel, { controller }),
1195
+ tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintLayersPanel, { controller }),
1196
+ tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StickerPanel, { controller })
1002
1197
  ] })
1003
1198
  ] })
1004
1199
  ] });
@@ -1014,6 +1209,8 @@ var PaintEditor = ({ controller, fileName, height = 720, presence, className })
1014
1209
  PaintSlider,
1015
1210
  PaintToolbar,
1016
1211
  PaperPanel,
1212
+ StickerPanel,
1213
+ WetPanel,
1017
1214
  usePaintDirector,
1018
1215
  usePaintEditor,
1019
1216
  usePaintEditorState