@pixerate/editor-react 0.69.0 → 0.71.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.
@@ -477,8 +477,19 @@ var PaintToolbar = ({ controller, tool, onToolChange, fileName = "painting", cla
477
477
 
478
478
  // src/paint-editor/BrushPanel.tsx
479
479
  import { useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
480
- import { BRUSH_ROTATIONS, BRUSH_SLIDERS, downloadBytes as downloadBytes2, patchPath, readPath } from "@pixerate/editor/paint-editor";
481
- 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";
482
493
  var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, className }) => {
483
494
  const state = usePaintEditorState(controller);
484
495
  const [error, attempt] = useAttempt();
@@ -492,6 +503,10 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
492
503
  // eslint-disable-next-line react-hooks/exhaustive-deps
493
504
  [controller, brushes, paper, thumbnailWidth, thumbnailHeight]
494
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 } } });
495
510
  const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "brush";
496
511
  return /* @__PURE__ */ jsxs4("div", { className: `${panel} ${className ?? ""}`, "data-testid": "brush-panel", children: [
497
512
  /* @__PURE__ */ jsx4("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ jsxs4(
@@ -523,6 +538,68 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
523
538
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2", children: [
524
539
  /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
525
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" })
526
603
  ] })
527
604
  ] }),
528
605
  /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", children: [
@@ -598,7 +675,9 @@ import {
598
675
  HARMONY_KINDS,
599
676
  MIXING_PAD_HEIGHT,
600
677
  MIXING_PAD_WIDTH,
678
+ PAD_SAMPLE_SIZES,
601
679
  hsvToRgb,
680
+ padSampleRadius,
602
681
  hueToWheel,
603
682
  parseHexColor,
604
683
  renderColorWheel,
@@ -614,6 +693,9 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
614
693
  const [wheel, setWheel] = useState5("ryb");
615
694
  const [kind, setKind] = useState5("complementary");
616
695
  const [padTool, setPadTool] = useState5("paint");
696
+ const [sample, setSample] = useState5(3);
697
+ const [parts, setParts] = useState5([1, 1]);
698
+ const [padName, setPadName] = useState5("");
617
699
  const padPoints = useRef5(null);
618
700
  const ring = useMemo5(() => renderColorWheel(wheelSize, wheel), [wheelSize, wheel]);
619
701
  const color = state.color;
@@ -725,7 +807,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
725
807
  onPointerDown: (e) => {
726
808
  const p = padPoint(e);
727
809
  if (padTool === "pick") {
728
- attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true }));
810
+ attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true, radius: padSampleRadius(sample) }));
729
811
  return;
730
812
  }
731
813
  padPoints.current = [p];
@@ -739,7 +821,44 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
739
821
  },
740
822
  children: /* @__PURE__ */ jsx6(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${MIXING_PAD_WIDTH} / ${MIXING_PAD_HEIGHT}` } })
741
823
  }
742
- )
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)) })
743
862
  ] }),
744
863
  /* @__PURE__ */ jsx6(ErrorLine, { error })
745
864
  ] });
@@ -747,7 +866,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
747
866
 
748
867
  // src/paint-editor/PaintLayersPanel.tsx
749
868
  import { useState as useState6 } from "react";
750
- import { BLEND_MENU_GROUPS } from "@pixerate/editor/paint-editor";
869
+ import { BLEND_MENU_GROUPS as BLEND_MENU_GROUPS2 } from "@pixerate/editor/paint-editor";
751
870
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
752
871
  function siblingIndex(layers, l) {
753
872
  const sibs = layers.filter((x) => x.parentId === l.parentId).reverse();
@@ -844,7 +963,7 @@ var PaintLayersPanel = ({ controller, className }) => {
844
963
  /* @__PURE__ */ jsx7("span", { className: "w-14 text-slate-400", children: "Blend" }),
845
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: [
846
965
  active.type === "group" && /* @__PURE__ */ jsx7("option", { value: "pass-through", children: "Pass through" }),
847
- 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))
848
967
  ] })
849
968
  ] }),
850
969
  /* @__PURE__ */ jsxs7("div", { className: "flex gap-1", children: [