@pixerate/editor 0.262.1 → 0.263.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 (68) hide show
  1. package/dist/animation-editor/index.cjs +60 -60
  2. package/dist/animation-editor/index.js +4 -4
  3. package/dist/brushes/index.cjs +52 -52
  4. package/dist/brushes/index.js +4 -4
  5. package/dist/{brushstrokeRender-N6UPZSQS.cjs → brushstrokeRender-2GBME4XD.cjs} +13 -13
  6. package/dist/{brushstrokeRender-25Q5BEY6.js → brushstrokeRender-S5DABANG.js} +3 -3
  7. package/dist/canvas/index.cjs +15 -14
  8. package/dist/canvas/index.js +3 -2
  9. package/dist/canvas/stickers.d.cts +5 -1
  10. package/dist/canvas/stickers.d.ts +5 -1
  11. package/dist/{chunk-6NZL24W4.js → chunk-34HIZXFX.js} +1 -1
  12. package/dist/{chunk-UVZ7RQOE.cjs → chunk-4E6BGHWN.cjs} +1 -1
  13. package/dist/{chunk-J5UQHWVL.js → chunk-6JL2D5SG.js} +27 -15
  14. package/dist/{chunk-QAR2N7HZ.js → chunk-6XX7E7O2.js} +1 -1
  15. package/dist/{chunk-3TQW5MPW.cjs → chunk-73QFSPDG.cjs} +23 -23
  16. package/dist/{chunk-APX63ILU.js → chunk-ACFSFUQH.js} +31 -0
  17. package/dist/{chunk-X2EXYY4J.js → chunk-CNLO6E2K.js} +2 -2
  18. package/dist/{chunk-F7CPSRLN.js → chunk-DOZ6F2K2.js} +12 -4
  19. package/dist/{chunk-CQVTGBQI.js → chunk-JGI4PZWO.js} +1 -1
  20. package/dist/{chunk-EHALWBBD.js → chunk-LG6MEEV7.js} +1 -1
  21. package/dist/{chunk-7AUQ2UJ3.cjs → chunk-LWHUU6A6.cjs} +10 -10
  22. package/dist/{chunk-JSDND7DF.cjs → chunk-OTN6VB5S.cjs} +32 -1
  23. package/dist/{chunk-5IW65VQ6.js → chunk-P5343XZO.js} +1 -1
  24. package/dist/{chunk-AVTPZO4Z.cjs → chunk-PE6YJMQT.cjs} +37 -25
  25. package/dist/{chunk-D4TE5JNW.js → chunk-QW7LKWKE.js} +1 -1
  26. package/dist/{chunk-YLUIFXSL.cjs → chunk-TIHW7CUY.cjs} +35 -35
  27. package/dist/{chunk-PYTJBVW5.cjs → chunk-TILEUVRO.cjs} +31 -31
  28. package/dist/{chunk-RVGP2XLF.cjs → chunk-XORJ7UZO.cjs} +21 -13
  29. package/dist/{chunk-43NT242D.cjs → chunk-XQW6R7VW.cjs} +86 -86
  30. package/dist/{chunk-TZOX7KWV.cjs → chunk-ZBACQHW3.cjs} +8 -8
  31. package/dist/image-editor/index.cjs +789 -788
  32. package/dist/image-editor/index.js +7 -6
  33. package/dist/image-editor/stickers.d.cts +7 -2
  34. package/dist/image-editor/stickers.d.ts +7 -2
  35. package/dist/paint-editor/controller.d.cts +7 -2
  36. package/dist/paint-editor/controller.d.ts +7 -2
  37. package/dist/paint-editor/index.cjs +574 -521
  38. package/dist/paint-editor/index.js +97 -44
  39. package/dist/paint-editor/sticker-art.d.cts +20 -10
  40. package/dist/paint-editor/sticker-art.d.ts +20 -10
  41. package/dist/paint-editor/stickers.d.cts +8 -1
  42. package/dist/paint-editor/stickers.d.ts +8 -1
  43. package/dist/paint-editor/stroke.d.cts +2 -0
  44. package/dist/paint-editor/stroke.d.ts +2 -0
  45. package/dist/stickers/browser.d.cts +10 -3
  46. package/dist/stickers/browser.d.ts +10 -3
  47. package/dist/stickers/combine.d.cts +2 -0
  48. package/dist/stickers/combine.d.ts +2 -0
  49. package/dist/stickers/conformance.d.cts +1 -1
  50. package/dist/stickers/conformance.d.ts +1 -1
  51. package/dist/stickers/errors.d.cts +1 -1
  52. package/dist/stickers/errors.d.ts +1 -1
  53. package/dist/stickers/fake.d.cts +4 -2
  54. package/dist/stickers/fake.d.ts +4 -2
  55. package/dist/stickers/helpers.d.cts +16 -2
  56. package/dist/stickers/helpers.d.ts +16 -2
  57. package/dist/stickers/index.cjs +116 -29
  58. package/dist/stickers/index.js +106 -19
  59. package/dist/stickers/tools.d.cts +9 -2
  60. package/dist/stickers/tools.d.ts +9 -2
  61. package/dist/stickers/types.d.cts +32 -5
  62. package/dist/stickers/types.d.ts +32 -5
  63. package/dist/vector/index.cjs +972 -971
  64. package/dist/vector/index.js +7 -6
  65. package/dist/vector/stickers.d.cts +7 -2
  66. package/dist/vector/stickers.d.ts +7 -2
  67. package/package.json +1 -1
  68. package/skills/pixerate-stickers/SKILL.md +20 -9
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  StickerBrowser,
3
3
  editorStickers
4
- } from "../chunk-F7CPSRLN.js";
4
+ } from "../chunk-DOZ6F2K2.js";
5
5
  import {
6
6
  BRUSH_PATCH_PROPERTIES,
7
7
  BRUSH_SLIDERS,
@@ -11,11 +11,11 @@ import {
11
11
  mergeBrushPatch,
12
12
  multiColorSliders,
13
13
  patchBrushValue
14
- } from "../chunk-X2EXYY4J.js";
14
+ } from "../chunk-CNLO6E2K.js";
15
15
  import {
16
16
  createStickerTools,
17
17
  describeStickerRef
18
- } from "../chunk-J5UQHWVL.js";
18
+ } from "../chunk-6JL2D5SG.js";
19
19
  import {
20
20
  toVideoFfmpeg
21
21
  } from "../chunk-MWUZCEFT.js";
@@ -30,7 +30,7 @@ import {
30
30
  resolveExternalAssets,
31
31
  serializeLayers,
32
32
  serializeLayersExternal
33
- } from "../chunk-D4TE5JNW.js";
33
+ } from "../chunk-QW7LKWKE.js";
34
34
  import "../chunk-WJ4WQJCC.js";
35
35
  import {
36
36
  LAYER_EFFECT_KINDS,
@@ -88,7 +88,7 @@ import {
88
88
  realPresenceClock,
89
89
  tweenPresenceCursor
90
90
  } from "../chunk-IVSWRVB4.js";
91
- import "../chunk-6NZL24W4.js";
91
+ import "../chunk-34HIZXFX.js";
92
92
  import {
93
93
  BRUSH_ID,
94
94
  BRUSH_PRESETS,
@@ -143,15 +143,16 @@ import {
143
143
  parseBrushLibrary,
144
144
  resolveBrush,
145
145
  usesPaintExchange
146
- } from "../chunk-5IW65VQ6.js";
146
+ } from "../chunk-P5343XZO.js";
147
147
  import {
148
148
  STAMP_KINDS,
149
149
  StickersSlot,
150
150
  editorStampOf,
151
151
  normalizeStickerRef,
152
152
  stampCoverage,
153
- stickerIdFor
154
- } from "../chunk-APX63ILU.js";
153
+ stickerIdFor,
154
+ stickerVariantFor
155
+ } from "../chunk-ACFSFUQH.js";
155
156
  import "../chunk-M6DNZNVY.js";
156
157
  import {
157
158
  bezierPathToSvg,
@@ -2538,49 +2539,77 @@ var defaultStickerRasterizer = async (asset, size) => {
2538
2539
  const img = ctx.getImageData(0, 0, w, h);
2539
2540
  return { width: w, height: h, data: img.data };
2540
2541
  };
2541
- var StickerArtCache = class {
2542
+ var StickerArtCache = class _StickerArtCache {
2542
2543
  entries = /* @__PURE__ */ new Map();
2543
- /** The art for `ref` when it has loaded (editor stamps never need any). */
2544
- get = (ref2) => {
2545
- const e = this.entries.get(ref2);
2544
+ /** Descriptions by ref (their variants pick the art), shared by the loads that need them. */
2545
+ descriptions = /* @__PURE__ */ new Map();
2546
+ variants = /* @__PURE__ */ new Map();
2547
+ static key(ref2, variant = "light") {
2548
+ return variant === "dark" ? `${ref2}#dark` : ref2;
2549
+ }
2550
+ /** The art for `ref` in `variant` (default `light`) when it has loaded (editor stamps never need any). */
2551
+ get = (ref2, variant) => {
2552
+ const e = this.entries.get(_StickerArtCache.key(ref2, variant));
2546
2553
  return e?.status === "ready" ? e.art : void 0;
2547
2554
  };
2555
+ /** The variant to draw `ref` in on a dark or light background: `dark` once the sticker is known to have it. */
2556
+ variantFor(ref2, dark) {
2557
+ return dark && this.variants.get(ref2)?.includes("dark") ? "dark" : "light";
2558
+ }
2548
2559
  /** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
2549
- status(ref2) {
2560
+ status(ref2, variant) {
2550
2561
  if (editorStampOf(ref2)) return { status: "ready" };
2551
- const e = this.entries.get(ref2);
2552
- if (!e) return { status: "none" };
2562
+ const e = this.entries.get(_StickerArtCache.key(ref2, variant));
2563
+ if (!e) return this.descriptions.has(ref2) && !this.variants.has(ref2) ? { status: "loading" } : { status: "none" };
2553
2564
  return e.status === "failed" ? { status: "failed", error: e.error } : { status: e.status };
2554
2565
  }
2555
2566
  /**
2556
- * Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
2557
- * reject with the closest known ones; failures are kept so they are reported, and retried on
2558
- * the next call.
2567
+ * Loads and rasterises `ref` from `provider` once per variant (later calls share the load): the
2568
+ * dark variant when `dark` and the sticker has one, else the light. Unknown refs reject with the
2569
+ * closest known ones; failures are kept so they are reported, and retried on the next call.
2559
2570
  */
2560
- ensure(provider, ref2, rasterize) {
2561
- if (editorStampOf(ref2)) return Promise.resolve(null);
2562
- const e = this.entries.get(ref2);
2571
+ async ensure(provider, ref2, rasterize, dark = false) {
2572
+ if (editorStampOf(ref2)) return null;
2573
+ let described = this.descriptions.get(ref2);
2574
+ if (!described) {
2575
+ described = describeStickerRef(provider, ref2);
2576
+ this.descriptions.set(ref2, described);
2577
+ }
2578
+ let d;
2579
+ try {
2580
+ d = await described;
2581
+ } catch (err) {
2582
+ this.descriptions.delete(ref2);
2583
+ this.entries.set(_StickerArtCache.key(ref2), { status: "failed", error: err instanceof Error ? err.message : String(err) });
2584
+ throw err;
2585
+ }
2586
+ this.variants.set(ref2, d.variants ?? ["light"]);
2587
+ return this.load(provider, ref2, d, stickerVariantFor(d, dark), rasterize);
2588
+ }
2589
+ load(provider, ref2, d, variant, rasterize) {
2590
+ const key = _StickerArtCache.key(ref2, variant);
2591
+ const e = this.entries.get(key);
2563
2592
  if (e?.status === "ready") return Promise.resolve(e.art);
2564
2593
  if (e?.status === "loading") return e.promise;
2565
2594
  const promise = (async () => {
2566
- const d = await describeStickerRef(provider, ref2);
2567
2595
  const id = stickerIdFor(provider, ref2);
2568
2596
  const max = provider.capabilities.maxSize ?? STICKER_ART_SIZE;
2569
2597
  const size = Math.min(STICKER_ART_SIZE, max);
2570
- const asset = await provider.load(id, d.formats.includes("png") ? { format: "png", size } : { format: d.formats[0] ?? "svg", size });
2598
+ const v = variant === "dark" ? { variant } : {};
2599
+ const asset = await provider.load(id, d.formats.includes("png") ? { format: "png", size, ...v } : { format: d.formats[0] ?? "svg", size, ...v });
2571
2600
  const px = await rasterize(asset, size);
2572
2601
  if (!(px.width > 0 && px.height > 0) || px.data.length !== px.width * px.height * 4) throw new StickerError("load_failed", `The art for "${ref2}" came back empty or the wrong size.`);
2573
2602
  return { ...px, tintable: d.tintable };
2574
2603
  })();
2575
- this.entries.set(ref2, { status: "loading", promise });
2604
+ this.entries.set(key, { status: "loading", promise });
2576
2605
  return promise.then(
2577
2606
  (art) => {
2578
- this.entries.set(ref2, { status: "ready", art });
2607
+ this.entries.set(key, { status: "ready", art });
2579
2608
  return art;
2580
2609
  },
2581
2610
  (err) => {
2582
2611
  const message = err instanceof Error ? err.message : String(err);
2583
- this.entries.set(ref2, { status: "failed", error: message });
2612
+ this.entries.set(key, { status: "failed", error: message });
2584
2613
  throw err;
2585
2614
  }
2586
2615
  );
@@ -2588,6 +2617,8 @@ var StickerArtCache = class {
2588
2617
  /** Forgets every load (a new provider). */
2589
2618
  clear() {
2590
2619
  this.entries.clear();
2620
+ this.descriptions.clear();
2621
+ this.variants.clear();
2591
2622
  }
2592
2623
  };
2593
2624
  function sampleArt(art, x, y, out) {
@@ -2645,7 +2676,7 @@ function stickerStamp(s) {
2645
2676
  function drawSticker(medium, s, art) {
2646
2677
  const kind = stickerStamp(s);
2647
2678
  if (!kind) {
2648
- const a2 = art?.(s.ref);
2679
+ const a2 = art?.(s.ref, s.variant);
2649
2680
  return a2 ? drawArtSticker(medium, s, a2) : stickerRect(s);
2650
2681
  }
2651
2682
  const rect = stickerRect(s);
@@ -2792,7 +2823,8 @@ var StickerSpray = class {
2792
2823
  }
2793
2824
  const ref2 = st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "editor:dot";
2794
2825
  const stamp = stickerStamp({ ref: ref2 });
2795
- if (!stamp && !this.init.art?.(ref2)) {
2826
+ const variant = stamp ? "light" : this.init.variant?.(ref2) ?? "light";
2827
+ if (!stamp && !this.init.art?.(ref2, variant)) {
2796
2828
  this.skipped.add(ref2);
2797
2829
  this.next++;
2798
2830
  return;
@@ -2807,7 +2839,8 @@ var StickerSpray = class {
2807
2839
  rotation: Math.round((-d.angle * 180 / Math.PI + (rand01(seed, i, 59) * 2 - 1) * 180 * st.rotationJitter) * 10) / 10,
2808
2840
  color: st.colorJitter > 0 ? jitterColor(base.color, st.colorJitter, seed, i) : base.color,
2809
2841
  opacity: Math.round(Math.min(1, d.opacity * this.init.brush.tip.opacity * base.alpha) * 1e3) / 1e3,
2810
- seed: Math.floor(rand01(seed, i, 65) * 1e9)
2842
+ seed: Math.floor(rand01(seed, i, 65) * 1e9),
2843
+ ...variant === "dark" ? { variant } : {}
2811
2844
  };
2812
2845
  this.stickers.push(sticker);
2813
2846
  rects.push(drawSticker(this.medium, sticker, this.init.art));
@@ -2914,7 +2947,7 @@ function stickerAt(stickers, x, y, art) {
2914
2947
  if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
2915
2948
  continue;
2916
2949
  }
2917
- const pic = art?.(s.ref);
2950
+ const pic = art?.(s.ref, s.variant);
2918
2951
  if (!pic) return s;
2919
2952
  const scale = Math.max(pic.width, pic.height) / 2;
2920
2953
  if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
@@ -2965,7 +2998,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
2965
2998
  };
2966
2999
  if (this.wetDriver) {
2967
3000
  } else if (init.sticker) {
2968
- this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect, art: init.sticker.art });
3001
+ this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect, art: init.sticker.art, variant: init.sticker.variant });
2969
3002
  } else if (init.thick) {
2970
3003
  const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
2971
3004
  this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect });
@@ -7465,6 +7498,8 @@ var PaintEditorController = class _PaintEditorController {
7465
7498
  this.colorScheme = next;
7466
7499
  const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
7467
7500
  if (hex2) this.color = hex2;
7501
+ const refs = brushStickerRefs(this.brush);
7502
+ if (refs.some((r) => !editorStampOf(r))) void this.prepareStickers(refs).catch(() => this.emit([]));
7468
7503
  this.emit([]);
7469
7504
  }
7470
7505
  /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
@@ -7748,7 +7783,7 @@ var PaintEditorController = class _PaintEditorController {
7748
7783
  thick: brush.engine === "impasto" ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
7749
7784
  // Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
7750
7785
  light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
7751
- sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
7786
+ sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
7752
7787
  stampArt: this.stickerArt.get,
7753
7788
  paint: effect ? null : this.strokeGradient(init, brush, color),
7754
7789
  effect
@@ -8598,8 +8633,8 @@ var PaintEditorController = class _PaintEditorController {
8598
8633
  try {
8599
8634
  const ref2 = normalizeStickerRef(x.ref ?? x.stamp);
8600
8635
  const stamp = editorStampOf(ref2);
8601
- const { stamp: _old, ...rest } = x;
8602
- return [{ ...rest, ref: ref2, ...stamp ? { stamp } : {} }];
8636
+ const { stamp: _old, variant, ...rest } = x;
8637
+ return [{ ...rest, ref: ref2, ...stamp ? { stamp } : {}, ...variant === "dark" && !stamp ? { variant } : {} }];
8603
8638
  } catch {
8604
8639
  return [];
8605
8640
  }
@@ -9056,22 +9091,32 @@ var PaintEditorController = class _PaintEditorController {
9056
9091
  }
9057
9092
  /**
9058
9093
  * Loads the art of `refs` (provider stickers; editor stamps need none) so sprays and placements
9059
- * can draw them. Rejects with a `PaintEditorError` naming an unknown ref and the closest known
9094
+ * can draw them: their dark variants when the colour scheme is dark and they have one. Rejects with a `PaintEditorError` naming an unknown ref and the closest known
9060
9095
  * ones, or a sticker whose art failed. Emits when art arrives, so views redraw.
9061
9096
  */
9062
9097
  async prepareStickers(refs) {
9063
9098
  const list = [...new Set(refs.map((r) => this.readStickerRef(r)))].filter((r) => !editorStampOf(r));
9064
9099
  try {
9065
- await Promise.all(list.map((r) => this.stickerArt.ensure(this.stickers.provider, r, this.rasterizeSticker)));
9100
+ await Promise.all(list.map((r) => this.stickerArt.ensure(this.stickers.provider, r, this.rasterizeSticker, this.colorScheme === "dark")));
9066
9101
  } catch (e) {
9067
9102
  throw new PaintEditorError(e instanceof Error ? e.message : String(e));
9068
9103
  } finally {
9069
9104
  if (list.length) this.emit([]);
9070
9105
  }
9071
9106
  }
9072
- /** Whether a sticker's art is ready to draw: `ready` (always for editor stamps), `loading`, `failed` (with why) or `none`. */
9107
+ /** Whether a sticker's art is ready to draw in the variant the colour scheme picks: `ready` (always for editor stamps), `loading`, `failed` (with why) or `none`. */
9073
9108
  stickerArtStatus(ref2) {
9074
- return this.stickerArt.status(this.readStickerRef(ref2));
9109
+ const r = this.readStickerRef(ref2);
9110
+ return this.stickerArt.status(r, this.stickerArt.variantFor(r, this.colorScheme === "dark"));
9111
+ }
9112
+ /**
9113
+ * A sticker whose ref is new (placed or restamped) in the variant the colour scheme picks: `dark`
9114
+ * on a dark scheme when its sticker has one (known once its art was asked for).
9115
+ */
9116
+ withStickerVariant(s) {
9117
+ const { variant: _old, ...rest } = s;
9118
+ const v = editorStampOf(s.ref) ? "light" : this.stickerArt.variantFor(s.ref, this.colorScheme === "dark");
9119
+ return v === "dark" ? { ...rest, variant: v } : rest;
9075
9120
  }
9076
9121
  readStickerRef(ref2) {
9077
9122
  try {
@@ -9082,10 +9127,17 @@ var PaintEditorController = class _PaintEditorController {
9082
9127
  }
9083
9128
  /** Refuses to redraw stickers whose art has not loaded (they would vanish), naming them and what to call. */
9084
9129
  requireStickerArt(stickers) {
9085
- const missing = [...new Set(stickers.filter((s) => !stickerStamp(s) && !this.stickerArt.get(s.ref)).map((s) => s.ref))];
9130
+ const missing = stickers.filter((s) => !stickerStamp(s) && !this.stickerArt.get(s.ref, s.variant));
9086
9131
  if (missing.length) {
9087
- for (const r of missing) void this.stickerArt.ensure(this.stickers.provider, r, this.rasterizeSticker).then(() => this.emit([]), () => this.emit([]));
9088
- throw new PaintEditorError(`The art for ${missing.map((r) => `"${r}"`).join(", ")} has not loaded yet; it is loading now (prepareStickers), so try again when it has.`);
9132
+ const dark = this.colorScheme === "dark";
9133
+ const loads = /* @__PURE__ */ new Map();
9134
+ for (const s of missing) {
9135
+ loads.set(`${s.ref}#${s.variant ?? "light"}`, [s.ref, s.variant === "dark"]);
9136
+ if (dark) loads.set(`${s.ref}#dark`, [s.ref, true]);
9137
+ }
9138
+ for (const [r, d] of loads.values()) void this.stickerArt.ensure(this.stickers.provider, r, this.rasterizeSticker, d).then(() => this.emit([]), () => this.emit([]));
9139
+ const refs = [...new Set(missing.map((s) => s.ref))];
9140
+ throw new PaintEditorError(`The art for ${refs.map((r) => `"${r}"`).join(", ")} has not loaded yet; it is loading now (prepareStickers), so try again when it has.`);
9089
9141
  }
9090
9142
  }
9091
9143
  /** A layer's stickers, bottom first. */
@@ -9148,7 +9200,8 @@ var PaintEditorController = class _PaintEditorController {
9148
9200
  if (isLayerLocked(layer, "pixels")) throw new PaintEditorError(`Layer "${layer.name}" has its pixels locked; unlock it first.`);
9149
9201
  const i = stickers.findIndex((s) => s.id === id);
9150
9202
  if (i < 0) throw new PaintEditorError(`Layer "${layer.name}" has no sticker "${id}". Call list_stickers for the ids.`);
9151
- const next = { ...stickers[i], ...this.readStickerUpdate(update) };
9203
+ const u = this.readStickerUpdate(update);
9204
+ const next = u.ref !== void 0 ? this.withStickerVariant({ ...stickers[i], ...u }) : { ...stickers[i], ...u };
9152
9205
  const list = stickers.map((s, j) => j === i ? next : s);
9153
9206
  const dirty = [stickerRect(stickers[i]), stickerRect(next)];
9154
9207
  const base = this.previewBase() ?? this.doc;
@@ -9172,7 +9225,7 @@ var PaintEditorController = class _PaintEditorController {
9172
9225
  const i = stickers.findIndex((s) => s.id === id);
9173
9226
  if (i < 0) throw new PaintEditorError(`Layer "${layer.name}" has no sticker "${id}". Call list_stickers for the ids.`);
9174
9227
  const u = this.readStickerUpdate(update);
9175
- const next = { ...stickers[i], ...u };
9228
+ const next = u.ref !== void 0 ? this.withStickerVariant({ ...stickers[i], ...u }) : { ...stickers[i], ...u };
9176
9229
  this.commitStickers("Edit sticker", layer, stickers.map((s, j) => j === i ? next : s), [stickerRect(stickers[i]), stickerRect(next)]);
9177
9230
  return { ...next };
9178
9231
  }
@@ -9201,7 +9254,7 @@ var PaintEditorController = class _PaintEditorController {
9201
9254
  }
9202
9255
  if (raw.seed !== void 0 && !Number.isInteger(raw.seed)) throw new PaintEditorError(`Sticker ${i}: seed must be a whole number.`);
9203
9256
  const id = n++;
9204
- return { id: `st${id}`, size: 48, rotation: 0, color: this.color, opacity: 1, seed: raw.seed ?? mix32(id * 2654435761 >>> 0) & 2147483647, ...u };
9257
+ return this.withStickerVariant({ id: `st${id}`, size: 48, rotation: 0, color: this.color, opacity: 1, seed: raw.seed ?? mix32(id * 2654435761 >>> 0) & 2147483647, ...u });
9205
9258
  });
9206
9259
  this.commitStickers("Add stickers", layer, [...existing ?? [], ...added], existing?.length ? added.map(stickerRect) : void 0);
9207
9260
  return added.map((s) => ({ ...s }));
@@ -6,8 +6,11 @@
6
6
  * PNG art (`data:` URLs) is decoded here, in any environment. SVG and `https:` images need a
7
7
  * rasteriser: the browser's (`createImageBitmap` and `OffscreenCanvas`) by default, or the
8
8
  * host's (`PaintEditorOptions.rasterizeSticker`).
9
+ *
10
+ * A sticker with a dark variant is kept in each variant asked for: `ensure(…, dark)` loads the dark
11
+ * art when the sticker has it, and sprayed stickers record the variant they were drawn in.
9
12
  */
10
- import type { StickerAsset, StickerProvider, StickerRef } from '../stickers/types.cjs';
13
+ import type { StickerAsset, StickerProvider, StickerRef, StickerVariant } from '../stickers/types.cjs';
11
14
  /** Rasterised sticker art (straight RGBA) and whether to recolour it. */
12
15
  export interface StickerArt {
13
16
  width: number;
@@ -16,8 +19,8 @@ export interface StickerArt {
16
19
  /** Flat art: drawn in the sticker's colour, keeping its alpha. */
17
20
  tintable: boolean;
18
21
  }
19
- /** The art for a ref, when it has loaded. */
20
- export type StickerArtLookup = (ref: StickerRef) => StickerArt | undefined;
22
+ /** The art for a ref in a variant (default `light`), when it has loaded. */
23
+ export type StickerArtLookup = (ref: StickerRef, variant?: StickerVariant) => StickerArt | undefined;
21
24
  /** Turns a sticker asset into RGBA at about `size` px on its longest side. */
22
25
  export type StickerRasterizer = (asset: StickerAsset, size: number) => Promise<{
23
26
  width: number;
@@ -32,22 +35,29 @@ export declare const STICKER_ART_SIZE = 256;
32
35
  * that adds a rasteriser.
33
36
  */
34
37
  export declare const defaultStickerRasterizer: StickerRasterizer;
35
- /** Loaded sticker art by ref, with the state of each load. */
38
+ /** Loaded sticker art by ref and variant, with the state of each load. */
36
39
  export declare class StickerArtCache {
37
40
  private readonly entries;
38
- /** The art for `ref` when it has loaded (editor stamps never need any). */
41
+ /** Descriptions by ref (their variants pick the art), shared by the loads that need them. */
42
+ private readonly descriptions;
43
+ private readonly variants;
44
+ private static key;
45
+ /** The art for `ref` in `variant` (default `light`) when it has loaded (editor stamps never need any). */
39
46
  get: StickerArtLookup;
47
+ /** The variant to draw `ref` in on a dark or light background: `dark` once the sticker is known to have it. */
48
+ variantFor(ref: StickerRef, dark: boolean): StickerVariant;
40
49
  /** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
41
- status(ref: StickerRef): {
50
+ status(ref: StickerRef, variant?: StickerVariant): {
42
51
  status: 'ready' | 'loading' | 'failed' | 'none';
43
52
  error?: string;
44
53
  };
45
54
  /**
46
- * Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
47
- * reject with the closest known ones; failures are kept so they are reported, and retried on
48
- * the next call.
55
+ * Loads and rasterises `ref` from `provider` once per variant (later calls share the load): the
56
+ * dark variant when `dark` and the sticker has one, else the light. Unknown refs reject with the
57
+ * closest known ones; failures are kept so they are reported, and retried on the next call.
49
58
  */
50
- ensure(provider: StickerProvider, ref: StickerRef, rasterize: StickerRasterizer): Promise<StickerArt | null>;
59
+ ensure(provider: StickerProvider, ref: StickerRef, rasterize: StickerRasterizer, dark?: boolean): Promise<StickerArt | null>;
60
+ private load;
51
61
  /** Forgets every load (a new provider). */
52
62
  clear(): void;
53
63
  }
@@ -6,8 +6,11 @@
6
6
  * PNG art (`data:` URLs) is decoded here, in any environment. SVG and `https:` images need a
7
7
  * rasteriser: the browser's (`createImageBitmap` and `OffscreenCanvas`) by default, or the
8
8
  * host's (`PaintEditorOptions.rasterizeSticker`).
9
+ *
10
+ * A sticker with a dark variant is kept in each variant asked for: `ensure(…, dark)` loads the dark
11
+ * art when the sticker has it, and sprayed stickers record the variant they were drawn in.
9
12
  */
10
- import type { StickerAsset, StickerProvider, StickerRef } from '../stickers/types.js';
13
+ import type { StickerAsset, StickerProvider, StickerRef, StickerVariant } from '../stickers/types.js';
11
14
  /** Rasterised sticker art (straight RGBA) and whether to recolour it. */
12
15
  export interface StickerArt {
13
16
  width: number;
@@ -16,8 +19,8 @@ export interface StickerArt {
16
19
  /** Flat art: drawn in the sticker's colour, keeping its alpha. */
17
20
  tintable: boolean;
18
21
  }
19
- /** The art for a ref, when it has loaded. */
20
- export type StickerArtLookup = (ref: StickerRef) => StickerArt | undefined;
22
+ /** The art for a ref in a variant (default `light`), when it has loaded. */
23
+ export type StickerArtLookup = (ref: StickerRef, variant?: StickerVariant) => StickerArt | undefined;
21
24
  /** Turns a sticker asset into RGBA at about `size` px on its longest side. */
22
25
  export type StickerRasterizer = (asset: StickerAsset, size: number) => Promise<{
23
26
  width: number;
@@ -32,22 +35,29 @@ export declare const STICKER_ART_SIZE = 256;
32
35
  * that adds a rasteriser.
33
36
  */
34
37
  export declare const defaultStickerRasterizer: StickerRasterizer;
35
- /** Loaded sticker art by ref, with the state of each load. */
38
+ /** Loaded sticker art by ref and variant, with the state of each load. */
36
39
  export declare class StickerArtCache {
37
40
  private readonly entries;
38
- /** The art for `ref` when it has loaded (editor stamps never need any). */
41
+ /** Descriptions by ref (their variants pick the art), shared by the loads that need them. */
42
+ private readonly descriptions;
43
+ private readonly variants;
44
+ private static key;
45
+ /** The art for `ref` in `variant` (default `light`) when it has loaded (editor stamps never need any). */
39
46
  get: StickerArtLookup;
47
+ /** The variant to draw `ref` in on a dark or light background: `dark` once the sticker is known to have it. */
48
+ variantFor(ref: StickerRef, dark: boolean): StickerVariant;
40
49
  /** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
41
- status(ref: StickerRef): {
50
+ status(ref: StickerRef, variant?: StickerVariant): {
42
51
  status: 'ready' | 'loading' | 'failed' | 'none';
43
52
  error?: string;
44
53
  };
45
54
  /**
46
- * Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
47
- * reject with the closest known ones; failures are kept so they are reported, and retried on
48
- * the next call.
55
+ * Loads and rasterises `ref` from `provider` once per variant (later calls share the load): the
56
+ * dark variant when `dark` and the sticker has one, else the light. Unknown refs reject with the
57
+ * closest known ones; failures are kept so they are reported, and retried on the next call.
49
58
  */
50
- ensure(provider: StickerProvider, ref: StickerRef, rasterize: StickerRasterizer): Promise<StickerArt | null>;
59
+ ensure(provider: StickerProvider, ref: StickerRef, rasterize: StickerRasterizer, dark?: boolean): Promise<StickerArt | null>;
60
+ private load;
51
61
  /** Forgets every load (a new provider). */
52
62
  clear(): void;
53
63
  }
@@ -15,7 +15,7 @@ import { type PaintGradientSource } from '../paint/gradient-paint.cjs';
15
15
  import type { PaintBrush } from './brushes.cjs';
16
16
  import { type PenPoint, type StrokeCopy } from './engine.cjs';
17
17
  import { type StampKind } from '../stickers/stamps.cjs';
18
- import type { StickerRef } from '../stickers/types.cjs';
18
+ import type { StickerRef, StickerVariant } from '../stickers/types.cjs';
19
19
  import { type StickerArtLookup } from './sticker-art.cjs';
20
20
  export { STAMP_KINDS, stampCoverage, type StampKind } from '../stickers/stamps.cjs';
21
21
  export { DEFAULT_STICKER, MAX_STICKER_STAMPS, normalizeStickerSettings, type StickerSettings } from './mark-settings.cjs';
@@ -39,6 +39,11 @@ export interface Sticker {
39
39
  opacity: number;
40
40
  /** Texture seed. */
41
41
  seed: number;
42
+ /**
43
+ * `dark` when it is drawn in its sticker's dark variant (sprayed or placed while the editor's
44
+ * colour scheme was dark and the sticker has one); absent for the light art.
45
+ */
46
+ variant?: StickerVariant;
42
47
  }
43
48
  /** The pixel rectangle a sticker covers. */
44
49
  export declare function stickerRect(s: Sticker): RasterRect;
@@ -69,6 +74,8 @@ export interface StickerSprayInit {
69
74
  effect?: EffectPaintPixels | null;
70
75
  /** Loaded art for provider stickers; stickers without art yet are skipped (see `skipped`). */
71
76
  art?: StickerArtLookup;
77
+ /** The variant to spray a provider sticker in (default `light`); recorded on each sticker. */
78
+ variant?: (ref: StickerRef) => StickerVariant;
72
79
  }
73
80
  /** A sticker-spray stroke: drops stickers along the path and draws them as it goes. */
74
81
  export declare class StickerSpray {
@@ -15,7 +15,7 @@ import { type PaintGradientSource } from '../paint/gradient-paint.js';
15
15
  import type { PaintBrush } from './brushes.js';
16
16
  import { type PenPoint, type StrokeCopy } from './engine.js';
17
17
  import { type StampKind } from '../stickers/stamps.js';
18
- import type { StickerRef } from '../stickers/types.js';
18
+ import type { StickerRef, StickerVariant } from '../stickers/types.js';
19
19
  import { type StickerArtLookup } from './sticker-art.js';
20
20
  export { STAMP_KINDS, stampCoverage, type StampKind } from '../stickers/stamps.js';
21
21
  export { DEFAULT_STICKER, MAX_STICKER_STAMPS, normalizeStickerSettings, type StickerSettings } from './mark-settings.js';
@@ -39,6 +39,11 @@ export interface Sticker {
39
39
  opacity: number;
40
40
  /** Texture seed. */
41
41
  seed: number;
42
+ /**
43
+ * `dark` when it is drawn in its sticker's dark variant (sprayed or placed while the editor's
44
+ * colour scheme was dark and the sticker has one); absent for the light art.
45
+ */
46
+ variant?: StickerVariant;
42
47
  }
43
48
  /** The pixel rectangle a sticker covers. */
44
49
  export declare function stickerRect(s: Sticker): RasterRect;
@@ -69,6 +74,8 @@ export interface StickerSprayInit {
69
74
  effect?: EffectPaintPixels | null;
70
75
  /** Loaded art for provider stickers; stickers without art yet are skipped (see `skipped`). */
71
76
  art?: StickerArtLookup;
77
+ /** The variant to spray a provider sticker in (default `light`); recorded on each sticker. */
78
+ variant?: (ref: StickerRef) => StickerVariant;
72
79
  }
73
80
  /** A sticker-spray stroke: drops stickers along the path and draws them as it goes. */
74
81
  export declare class StickerSpray {
@@ -21,6 +21,7 @@ import type { WetStrokeDriver } from './wet.cjs';
21
21
  import { type ThickChannels } from './thick.cjs';
22
22
  import { type Sticker } from './stickers.cjs';
23
23
  import type { StickerArtLookup } from './sticker-art.cjs';
24
+ import type { StickerRef, StickerVariant } from '../stickers/types.cjs';
24
25
  export interface StrokeSessionHost {
25
26
  /** Shows the layer's live surface (no history); `rects` are where it changed. */
26
27
  update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;
@@ -78,6 +79,7 @@ export interface StrokeSessionInit {
78
79
  sticker?: {
79
80
  nextId: number;
80
81
  art?: StickerArtLookup;
82
+ variant?: (ref: StickerRef) => StickerVariant;
81
83
  };
82
84
  /** Loaded sticker art for stamp tips and textured particle dots of stickers from the sticker library. */
83
85
  stampArt?: StickerArtLookup;
@@ -21,6 +21,7 @@ import type { WetStrokeDriver } from './wet.js';
21
21
  import { type ThickChannels } from './thick.js';
22
22
  import { type Sticker } from './stickers.js';
23
23
  import type { StickerArtLookup } from './sticker-art.js';
24
+ import type { StickerRef, StickerVariant } from '../stickers/types.js';
24
25
  export interface StrokeSessionHost {
25
26
  /** Shows the layer's live surface (no history); `rects` are where it changed. */
26
27
  update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;
@@ -78,6 +79,7 @@ export interface StrokeSessionInit {
78
79
  sticker?: {
79
80
  nextId: number;
80
81
  art?: StickerArtLookup;
82
+ variant?: (ref: StickerRef) => StickerVariant;
81
83
  };
82
84
  /** Loaded sticker art for stamp tips and textured particle dots of stickers from the sticker library. */
83
85
  stampArt?: StickerArtLookup;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * `StickerBrowser`: the state behind a sticker picker, shared by the React and Svelte pickers. It
3
- * searches or lists the provider a page at a time (search words, category), keeps the pages it
4
- * has loaded and its categories, and tells listeners when anything changes. UI-agnostic.
3
+ * searches or lists the provider a page at a time (search words, category, collection), keeps the
4
+ * pages it has loaded, its categories and collections, and tells listeners when anything changes.
5
+ * UI-agnostic.
5
6
  */
6
- import type { StickerProvider, StickerRef, StickerSummary } from './types.cjs';
7
+ import type { StickerCollection, StickerProvider, StickerRef, StickerSummary } from './types.cjs';
7
8
  export interface StickerBrowserItem extends StickerSummary {
8
9
  ref: StickerRef;
9
10
  }
@@ -11,6 +12,8 @@ export interface StickerBrowserState {
11
12
  query: string;
12
13
  /** `''` for every category. */
13
14
  category: string;
15
+ /** `''` for every collection. */
16
+ collection: string;
14
17
  items: StickerBrowserItem[];
15
18
  total: number;
16
19
  /** More pages to load (`more()`). */
@@ -22,6 +25,8 @@ export interface StickerBrowserState {
22
25
  name: string;
23
26
  count: number;
24
27
  }>;
28
+ /** The provider's collections (empty when it has none). */
29
+ collections: StickerCollection[];
25
30
  /** The provider's name. */
26
31
  provider: string;
27
32
  }
@@ -42,6 +47,8 @@ export declare class StickerBrowser {
42
47
  setQuery(query: string): Promise<void>;
43
48
  /** Shows one category (`''` for all) from the first page. */
44
49
  setCategory(category: string): Promise<void>;
50
+ /** Shows one collection (`''` for all) from the first page. */
51
+ setCollection(collection: string): Promise<void>;
45
52
  /** Loads the next page onto the list. */
46
53
  more(): Promise<void>;
47
54
  private page;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * `StickerBrowser`: the state behind a sticker picker, shared by the React and Svelte pickers. It
3
- * searches or lists the provider a page at a time (search words, category), keeps the pages it
4
- * has loaded and its categories, and tells listeners when anything changes. UI-agnostic.
3
+ * searches or lists the provider a page at a time (search words, category, collection), keeps the
4
+ * pages it has loaded, its categories and collections, and tells listeners when anything changes.
5
+ * UI-agnostic.
5
6
  */
6
- import type { StickerProvider, StickerRef, StickerSummary } from './types.js';
7
+ import type { StickerCollection, StickerProvider, StickerRef, StickerSummary } from './types.js';
7
8
  export interface StickerBrowserItem extends StickerSummary {
8
9
  ref: StickerRef;
9
10
  }
@@ -11,6 +12,8 @@ export interface StickerBrowserState {
11
12
  query: string;
12
13
  /** `''` for every category. */
13
14
  category: string;
15
+ /** `''` for every collection. */
16
+ collection: string;
14
17
  items: StickerBrowserItem[];
15
18
  total: number;
16
19
  /** More pages to load (`more()`). */
@@ -22,6 +25,8 @@ export interface StickerBrowserState {
22
25
  name: string;
23
26
  count: number;
24
27
  }>;
28
+ /** The provider's collections (empty when it has none). */
29
+ collections: StickerCollection[];
25
30
  /** The provider's name. */
26
31
  provider: string;
27
32
  }
@@ -42,6 +47,8 @@ export declare class StickerBrowser {
42
47
  setQuery(query: string): Promise<void>;
43
48
  /** Shows one category (`''` for all) from the first page. */
44
49
  setCategory(category: string): Promise<void>;
50
+ /** Shows one collection (`''` for all) from the first page. */
51
+ setCollection(collection: string): Promise<void>;
45
52
  /** Loads the next page onto the list. */
46
53
  more(): Promise<void>;
47
54
  private page;
@@ -3,6 +3,8 @@ import type { StickerProvider } from './types.cjs';
3
3
  * One provider over several, in order. Its ids are refs (`provider:id`, `namespaced`), so the
4
4
  * same id from two libraries stays two stickers. Pages run through the providers in turn; the
5
5
  * cursor is `<provider index>|<that provider's cursor>`. Capabilities are what any of them offers.
6
+ * Collections are namespaced the same way (`provider:collection`), so a picker can tell a
7
+ * provider's collections apart from another provider's.
6
8
  */
7
9
  export declare function combineStickerProviders(providers: readonly StickerProvider[], options?: {
8
10
  id?: string;
@@ -3,6 +3,8 @@ import type { StickerProvider } from './types.js';
3
3
  * One provider over several, in order. Its ids are refs (`provider:id`, `namespaced`), so the
4
4
  * same id from two libraries stays two stickers. Pages run through the providers in turn; the
5
5
  * cursor is `<provider index>|<that provider's cursor>`. Capabilities are what any of them offers.
6
+ * Collections are namespaced the same way (`provider:collection`), so a picker can tell a
7
+ * provider's collections apart from another provider's.
6
8
  */
7
9
  export declare function combineStickerProviders(providers: readonly StickerProvider[], options?: {
8
10
  id?: string;