@pixerate/editor 0.155.0 → 0.156.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 (42) hide show
  1. package/dist/animation-editor/index.cjs +58 -58
  2. package/dist/animation-editor/index.js +4 -4
  3. package/dist/{brushstrokeRender-YAZVQUXY.cjs → brushstrokeRender-5WANQIG4.cjs} +10 -10
  4. package/dist/{brushstrokeRender-EUDQTDYS.js → brushstrokeRender-Q4LI22QV.js} +3 -3
  5. package/dist/canvas/index.cjs +8 -8
  6. package/dist/canvas/index.js +1 -1
  7. package/dist/{chunk-AGZ47XY2.js → chunk-2N2QPO3H.js} +6 -2
  8. package/dist/{chunk-NMXPOKF3.js → chunk-45XXVHVG.js} +1 -1
  9. package/dist/{chunk-FLOGD3QB.js → chunk-6UPGM3JB.js} +2 -2
  10. package/dist/{chunk-6FUAHCVI.cjs → chunk-7NW46N7I.cjs} +16 -16
  11. package/dist/{chunk-ORDXMBD4.js → chunk-A5UCR2TO.js} +10 -3
  12. package/dist/{chunk-PI45JMP7.cjs → chunk-CD7PLU77.cjs} +81 -81
  13. package/dist/{chunk-LRRCMRSL.cjs → chunk-EBDCRTZI.cjs} +20 -16
  14. package/dist/{chunk-OLCYQLFE.js → chunk-EL5PAHNP.js} +1 -1
  15. package/dist/{chunk-PUC43JPT.cjs → chunk-EYVVIZJ3.cjs} +1 -1
  16. package/dist/{chunk-BXMIBMVG.cjs → chunk-JGKCC5OF.cjs} +65 -58
  17. package/dist/{chunk-LIXLD433.cjs → chunk-MHEXLG3M.cjs} +35 -35
  18. package/dist/{chunk-RHXW4TA5.cjs → chunk-QMI5HZWA.cjs} +7 -7
  19. package/dist/{chunk-UILRJQFD.js → chunk-WNAAWLZ5.js} +1 -1
  20. package/dist/{chunk-Y64YPIFT.js → chunk-ZQWLWDZI.js} +1 -1
  21. package/dist/image-editor/index.cjs +686 -686
  22. package/dist/image-editor/index.js +5 -5
  23. package/dist/paint/grain.d.cts +7 -0
  24. package/dist/paint/grain.d.ts +7 -0
  25. package/dist/paint/index.cjs +3 -3
  26. package/dist/paint/index.js +2 -2
  27. package/dist/paint-editor/controller.d.cts +30 -0
  28. package/dist/paint-editor/controller.d.ts +30 -0
  29. package/dist/paint-editor/document.d.cts +8 -0
  30. package/dist/paint-editor/document.d.ts +8 -0
  31. package/dist/paint-editor/index.cjs +403 -256
  32. package/dist/paint-editor/index.js +158 -11
  33. package/dist/paint-editor/paper.d.cts +8 -0
  34. package/dist/paint-editor/paper.d.ts +8 -0
  35. package/dist/paint-editor/stencils.d.cts +2 -0
  36. package/dist/paint-editor/stencils.d.ts +2 -0
  37. package/dist/paint-editor/wet.d.cts +1 -0
  38. package/dist/paint-editor/wet.d.ts +1 -0
  39. package/dist/vector/index.cjs +950 -950
  40. package/dist/vector/index.js +4 -4
  41. package/package.json +1 -1
  42. package/skills/pixerate-paint-editor/SKILL.md +9 -1
@@ -7,7 +7,7 @@ import {
7
7
  resolveExternalAssets,
8
8
  serializeLayers,
9
9
  serializeLayersExternal
10
- } from "../chunk-Y64YPIFT.js";
10
+ } from "../chunk-ZQWLWDZI.js";
11
11
  import "../chunk-3CI7FRMK.js";
12
12
  import {
13
13
  EffectRendererPool
@@ -54,7 +54,7 @@ import {
54
54
  realPresenceClock,
55
55
  tweenPresenceCursor
56
56
  } from "../chunk-O2ANJFUF.js";
57
- import "../chunk-FLOGD3QB.js";
57
+ import "../chunk-6UPGM3JB.js";
58
58
  import {
59
59
  BRUSH_ID,
60
60
  BRUSH_ROTATIONS,
@@ -119,7 +119,7 @@ import {
119
119
  tiltToOrientation,
120
120
  toStrokePoint,
121
121
  usesPaintExchange
122
- } from "../chunk-ORDXMBD4.js";
122
+ } from "../chunk-A5UCR2TO.js";
123
123
  import "../chunk-M6DNZNVY.js";
124
124
  import "../chunk-WEIK3UYB.js";
125
125
  import "../chunk-PWBZH3FK.js";
@@ -189,7 +189,7 @@ import {
189
189
  seedFromString,
190
190
  solidPaint,
191
191
  updatePaintGradientStop
192
- } from "../chunk-AGZ47XY2.js";
192
+ } from "../chunk-2N2QPO3H.js";
193
193
  import "../chunk-ODBT55Q7.js";
194
194
  import {
195
195
  BLEND_MODES,
@@ -1357,6 +1357,17 @@ function stencilMask(stencils, width, height) {
1357
1357
  }
1358
1358
  return { width, height, data };
1359
1359
  }
1360
+ function translateStencil(s, dx, dy) {
1361
+ const p = (q) => q ? { x: q.x + dx, y: q.y + dy } : void 0;
1362
+ return {
1363
+ ...s,
1364
+ ...s.from ? { from: p(s.from) } : {},
1365
+ ...s.to ? { to: p(s.to) } : {},
1366
+ ...s.center ? { center: p(s.center) } : {},
1367
+ ...s.rect ? { rect: { ...s.rect, x: s.rect.x + dx, y: s.rect.y + dy } } : {},
1368
+ ...s.points ? { points: s.points.map((q) => p(q)) } : {}
1369
+ };
1370
+ }
1360
1371
  function scaleStencil(s, k) {
1361
1372
  const p = (q) => q ? { x: q.x * k, y: q.y * k } : void 0;
1362
1373
  return {
@@ -1507,7 +1518,7 @@ var TRACING_NODE_ID = "__tracing";
1507
1518
  var PAPER_EFFECT_NODE_ID = "__paper_effect";
1508
1519
  function paperKey(p, light) {
1509
1520
  const metal = p.metallic ?? 0;
1510
- return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry, metal, metal > 0 && light ? [light.azimuth, light.elevation] : null]);
1521
+ return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry, p.offset ?? null, metal, metal > 0 && light ? [light.azimuth, light.elevation] : null]);
1511
1522
  }
1512
1523
  var paperNodes = /* @__PURE__ */ new WeakMap();
1513
1524
  function paperNode(paper, light) {
@@ -1522,7 +1533,7 @@ function paperNode(paper, light) {
1522
1533
  var grainCache = /* @__PURE__ */ new Map();
1523
1534
  var tileCache = /* @__PURE__ */ new Map();
1524
1535
  function paperGrain(paper) {
1525
- const key = JSON.stringify([paper.pattern, paper.seed, paper.scale, paper.tiling, paper.dry]);
1536
+ const key = JSON.stringify([paper.pattern, paper.seed, paper.scale, paper.tiling, paper.dry, paper.offset ?? null]);
1526
1537
  let g = grainCache.get(key);
1527
1538
  if (!g) {
1528
1539
  g = new PaperGrain(paper, "dry");
@@ -1612,7 +1623,7 @@ function downsampleRgba(data, width, height, tw, th) {
1612
1623
  // src/paint-editor/serialize.ts
1613
1624
  var PAINT_FORMAT = "pixerate-paint";
1614
1625
  var PAINT_SCHEMA_VERSION = 1;
1615
- var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches", "mixingPad", "wet", "wetEvents", "paintEvents"]);
1626
+ var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches", "mixingPad", "wet", "wetEvents", "paintEvents", "origin"]);
1616
1627
  function documentExtensions(raw) {
1617
1628
  const out = {};
1618
1629
  for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;
@@ -1636,6 +1647,7 @@ function header(state, options) {
1636
1647
  if (options.wet !== void 0) out.wet = JSON.parse(JSON.stringify(options.wet));
1637
1648
  if (state.wetEvents?.length) out.wetEvents = JSON.parse(JSON.stringify(state.wetEvents));
1638
1649
  if (state.paintEvents?.length) out.paintEvents = JSON.parse(JSON.stringify(state.paintEvents));
1650
+ if (state.origin && (state.origin.x || state.origin.y)) out.origin = { ...state.origin };
1639
1651
  return out;
1640
1652
  }
1641
1653
  function serializePaintDocument(state, options = {}) {
@@ -1688,6 +1700,8 @@ function deserializePaintDocument(raw) {
1688
1700
  if (wetEvents.length) state.wetEvents = wetEvents;
1689
1701
  const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "clear", "flatten", "stickers"].includes(e.kind)) : [];
1690
1702
  if (paintEvents.length) state.paintEvents = paintEvents;
1703
+ const o = r.origin;
1704
+ if (o && typeof o === "object" && Number.isFinite(o.x) && Number.isFinite(o.y) && (o.x || o.y)) state.origin = { x: Math.round(o.x), y: Math.round(o.y) };
1691
1705
  return { state, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, mixingPad: r.mixingPad, wet: r.wet, problems };
1692
1706
  }
1693
1707
  async function deserializePaintDocumentAsync(raw, store) {
@@ -2138,7 +2152,10 @@ var PaintWetLayers = class {
2138
2152
  /** The world (made on first use, at the canvas size). */
2139
2153
  ensureWorld() {
2140
2154
  const { width, height } = this.host.canvas();
2141
- if (this.world && (this.world.width !== width || this.world.height !== height)) throw new Error("The wet world is the wrong size; dry the paint before resizing the canvas.");
2155
+ if (this.world && (this.world.width !== width || this.world.height !== height)) {
2156
+ if (this.world.laneIds().length) throw new Error("The wet world is the wrong size; dry the paint before resizing the canvas.");
2157
+ this.dropWorld();
2158
+ }
2142
2159
  if (!this.world) {
2143
2160
  this.world = new WetWorld(width, height, wetPaperInput(this.host.paper()), { kernel: this.options.kernel, threads: this.options.threads });
2144
2161
  this.world.setTilt(this.tilt);
@@ -2401,9 +2418,16 @@ var PaintWetLayers = class {
2401
2418
  if (this.world) for (const id of this.world.laneIds()) this.world.removeLane(id);
2402
2419
  return;
2403
2420
  }
2421
+ const { width, height } = this.host.canvas();
2422
+ if (this.world && (this.world.width !== width || this.world.height !== height)) this.dropWorld();
2404
2423
  this.ensureWorld().restore(snap);
2405
2424
  this.syncDisplays();
2406
2425
  }
2426
+ dropWorld() {
2427
+ this.world?.destroy();
2428
+ this.world = null;
2429
+ this.driver = null;
2430
+ }
2407
2431
  /** Forgets everything wet (a new canvas, a resize, opening a document). */
2408
2432
  reset() {
2409
2433
  this.world?.destroy();
@@ -3594,6 +3618,7 @@ var PaintEditorController = class _PaintEditorController {
3594
3618
  const paper = this.doc.paper;
3595
3619
  this.doc = e.before;
3596
3620
  this.docRevision++;
3621
+ if (e.guides) this.restoreGuides(e.guides.before);
3597
3622
  if (e.wet) {
3598
3623
  if (paper !== this.doc.paper) this.wet.paperChanged();
3599
3624
  this.wet.restore(e.wet.lost ? null : e.wet.before);
@@ -3612,6 +3637,7 @@ var PaintEditorController = class _PaintEditorController {
3612
3637
  const paper = this.doc.paper;
3613
3638
  this.doc = e.after;
3614
3639
  this.docRevision++;
3640
+ if (e.guides) this.restoreGuides(e.guides.after);
3615
3641
  if (e.wet) {
3616
3642
  this.wet.restore(e.wet.lost ? null : e.wet.after);
3617
3643
  if (paper !== this.doc.paper) this.wet.paperChanged();
@@ -3619,6 +3645,10 @@ var PaintEditorController = class _PaintEditorController {
3619
3645
  this.emit();
3620
3646
  return e.label;
3621
3647
  }
3648
+ restoreGuides(guides) {
3649
+ if (guides.tracing !== this.guides.tracing) this.tracingVersion++;
3650
+ this.guides = guides;
3651
+ }
3622
3652
  /** Labels of the undo steps, oldest first. */
3623
3653
  historyLabels() {
3624
3654
  return { undo: this.undoStack.map((e) => e.label), redo: this.redoStack.map((e) => e.label).reverse() };
@@ -3639,7 +3669,7 @@ var PaintEditorController = class _PaintEditorController {
3639
3669
  width: options.width ?? this.doc.canvas.width,
3640
3670
  height: options.height ?? this.doc.canvas.height,
3641
3671
  dpi: options.dpi ?? this.doc.canvas.dpi,
3642
- paper: options.paper === void 0 ? this.doc.paper : this.resolvePaper(options.paper)
3672
+ paper: options.paper === void 0 ? { ...this.doc.paper, offset: void 0 } : this.resolvePaper(options.paper)
3643
3673
  });
3644
3674
  this.extensions = {};
3645
3675
  this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
@@ -3666,6 +3696,54 @@ var PaintEditorController = class _PaintEditorController {
3666
3696
  this.commit("Canvas size", this.withWetEvents({ ...this.doc, canvas }));
3667
3697
  return canvas;
3668
3698
  }
3699
+ /**
3700
+ * Grows the canvas by `left`, `top`, `right` and `bottom` pixels without moving anything
3701
+ * relative to anything else: the layers (with their masks, relief and stickers), the paper grain,
3702
+ * the effect paper, guides, stencils, the tracing image and the recorded strokes and events all
3703
+ * move by (left, top) in document pixels, `document.origin` moves by (−left, −top), and the view
3704
+ * pans so the picture stays still on screen.
3705
+ *
3706
+ * Wet paint dries first (the wet simulation is canvas-sized), as with `setCanvas`; one undo step
3707
+ * (undo brings back the old size, origin and guides, and the wet paint). Returns the new canvas,
3708
+ * where it sits in the previous document's pixels, and the origin.
3709
+ */
3710
+ extendCanvas(by) {
3711
+ this.requireIdle("extending the canvas");
3712
+ const side = (k) => {
3713
+ const v = by[k] ?? 0;
3714
+ if (!Number.isInteger(v) || v < 0) throw new PaintEditorError(`${k} is ${JSON.stringify(by[k])}; use a whole number of pixels to add, 0 or more.`);
3715
+ return v;
3716
+ };
3717
+ const left = side("left");
3718
+ const top = side("top");
3719
+ const right = side("right");
3720
+ const bottom = side("bottom");
3721
+ if (!(left || top || right || bottom)) throw new PaintEditorError("Nothing to extend: give left, top, right or bottom (pixels to add on that side) above 0.");
3722
+ const was = this.doc.canvas;
3723
+ const width = was.width + left + right;
3724
+ const height = was.height + top + bottom;
3725
+ if (width > MAX_CANVAS_SIDE || height > MAX_CANVAS_SIDE) {
3726
+ throw new PaintEditorError(`The canvas would be ${width}\xD7${height}; the limit is ${MAX_CANVAS_SIDE} px per side. Extend by less (it is ${was.width}\xD7${was.height} now).`);
3727
+ }
3728
+ if (this.wet.world) {
3729
+ this.requireWetIdle("extending the canvas");
3730
+ this.dryLayersFirst(this.wet.wetLayerIds());
3731
+ const settings = [...this.wet.settings];
3732
+ this.wet.reset();
3733
+ for (const [id, s] of settings) this.wet.settings.set(id, s.flow.center ? { ...s, flow: { ...s.flow, center: { x: s.flow.center.x + left, y: s.flow.center.y + top } } } : s);
3734
+ }
3735
+ const next = { ...shiftDocument(this.withWetEvents(this.doc), left, top), canvas: { ...was, width, height } };
3736
+ const m = viewMatrix(this.view, was, { width: 0, height: 0 });
3737
+ const dx = (right - left) / 2;
3738
+ const dy = (bottom - top) / 2;
3739
+ this.view = { ...this.view, pan: { x: this.view.pan.x + m[0] * dx + m[1] * dy, y: this.view.pan.y + m[2] * dx + m[3] * dy } };
3740
+ const guidesBefore = this.guides;
3741
+ if (left || top) this.restoreGuides(shiftGuides(this.guides, left, top));
3742
+ this.commit("Extend canvas", next);
3743
+ if (this.guides !== guidesBefore) this.undoStack[this.undoStack.length - 1].guides = { before: guidesBefore, after: this.guides };
3744
+ const origin = { ...next.origin ?? { x: 0, y: 0 } };
3745
+ return { canvas: { ...next.canvas }, rect: { x: 0 - left, y: 0 - top, width, height }, origin };
3746
+ }
3669
3747
  /** Changes the paper: a library preset (`preset`), and / or any of its settings (one undo step). */
3670
3748
  setPaper(update) {
3671
3749
  this.requireIdle("changing the paper");
@@ -3677,7 +3755,7 @@ var PaintEditorController = class _PaintEditorController {
3677
3755
  }
3678
3756
  const { preset: _p, ...rest } = update;
3679
3757
  if (rest.color !== void 0) parseColor(rest.color, "paper color");
3680
- const paper = normalizePaper({ ...rest, preset: base.preset }, base);
3758
+ const paper = normalizePaper({ ...rest, preset: base.preset, offset: rest.offset ?? this.doc.paper.offset }, base);
3681
3759
  const changedKeys = Object.keys(rest).filter((k) => k !== "name");
3682
3760
  if (changedKeys.length && update.preset === void 0) paper.preset = base.preset;
3683
3761
  if (this.wet.world) this.beginWetOp();
@@ -5839,6 +5917,60 @@ var PaintEditorController = class _PaintEditorController {
5839
5917
  }
5840
5918
  };
5841
5919
  var MAX_IMAGE_SIDE = 8192;
5920
+ var movePoint = (p, dx, dy) => ({ x: p.x + dx, y: p.y + dy });
5921
+ var moveRect = (r, dx, dy) => ({ ...r, x: r.x + dx, y: r.y + dy });
5922
+ function shiftDocument(doc, dx, dy) {
5923
+ if (!dx && !dy) return doc;
5924
+ let layers = doc.layers;
5925
+ for (const l of flattenLayers(doc.layers)) if (l.type === "pixel") layers = mapLayer(layers, l.id, () => ({ ...l, offset: movePoint(l.offset, dx, dy) }));
5926
+ const m = [1, 0, 0, 1, dx, dy];
5927
+ const moveStickers = (list) => list.map((s) => ({ ...s, x: Math.round((s.x + dx) * 100) / 100, y: Math.round((s.y + dy) * 100) / 100 }));
5928
+ const out = {
5929
+ ...doc,
5930
+ layers,
5931
+ paper: { ...doc.paper, offset: movePoint(doc.paper.offset ?? { x: 0, y: 0 }, dx, dy) },
5932
+ origin: movePoint(doc.origin ?? { x: 0, y: 0 }, -dx, -dy),
5933
+ strokes: doc.strokes.map((s) => {
5934
+ const moved = varyStroke(s, m, {});
5935
+ return s.stencils ? { ...moved, stencils: s.stencils.map((x) => translateStencil(x, dx, dy)) } : moved;
5936
+ })
5937
+ };
5938
+ if (doc.stickers) out.stickers = Object.fromEntries(Object.entries(doc.stickers).map(([id, list]) => [id, moveStickers(list)]));
5939
+ if (doc.paintEvents) {
5940
+ out.paintEvents = doc.paintEvents.map((e) => {
5941
+ const list = e.kind === "stickers" ? e.data?.stickers : void 0;
5942
+ return list ? { ...e, data: { ...e.data, stickers: moveStickers(list) } } : e;
5943
+ });
5944
+ }
5945
+ if (doc.wetEvents) {
5946
+ out.wetEvents = doc.wetEvents.map((e) => {
5947
+ const d = e.data;
5948
+ if (!d) return e;
5949
+ if (e.kind === "dry" && typeof d.x === "number") return { ...e, data: moveRect(d, dx, dy) };
5950
+ if (e.kind === "wet-paper" && d.rect) return { ...e, data: { ...d, rect: moveRect(d.rect, dx, dy) } };
5951
+ if (e.kind === "flow" && d.center) return { ...e, data: { ...d, center: movePoint(d.center, dx, dy) } };
5952
+ return e;
5953
+ });
5954
+ }
5955
+ if (doc.paperEffect) {
5956
+ const src = doc.paperEffect.image;
5957
+ const width = src.width + dx;
5958
+ const height = src.height + dy;
5959
+ const data = new Uint8ClampedArray(width * height * 4);
5960
+ for (let y = 0; y < src.height; y++) data.set(src.data.subarray(y * src.width * 4, (y + 1) * src.width * 4), ((y + dy) * width + dx) * 4);
5961
+ out.paperEffect = { ...doc.paperEffect, image: { width, height, data }, version: nextPaperEffectVersion() };
5962
+ }
5963
+ return out;
5964
+ }
5965
+ function shiftGuides(g, dx, dy) {
5966
+ return {
5967
+ ...g,
5968
+ symmetry: { ...g.symmetry, center: movePoint(g.symmetry.center, dx, dy) },
5969
+ perspective: { ...g.perspective, points: g.perspective.points.map((p) => movePoint(p, dx, dy)) },
5970
+ tracing: g.tracing ? { ...g.tracing, rect: moveRect(g.tracing.rect, dx, dy) } : null,
5971
+ stencils: g.stencils.map((s) => translateStencil(s, dx, dy))
5972
+ };
5973
+ }
5842
5974
  function resampleInto(src, rect, width, height) {
5843
5975
  const out = new Uint8ClampedArray(width * height * 4);
5844
5976
  const x0 = Math.max(0, Math.floor(rect.x));
@@ -7167,7 +7299,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
7167
7299
  add({
7168
7300
  name: "get_canvas_info",
7169
7301
  title: "Get the canvas",
7170
- description: "Reads the canvas: size in pixels and dpi, the paper (preset, colour, grain depth), the active layer, the active brush id, the paint colour, the view (zoom, rotation, mirror), the hand, how many strokes are recorded, the undo / redo labels, and the blend modes. Call it first.",
7302
+ description: "Reads the canvas: size in pixels and dpi, its origin (where (0, 0) is relative to the canvas it was extended from), the paper (preset, colour, grain depth), the active layer, the active brush id, the paint colour, the view (zoom, rotation, mirror), the hand, how many strokes are recorded, the undo / redo labels, and the blend modes. Call it first.",
7171
7303
  inputSchema: EMPTY3,
7172
7304
  annotations: { title: "Get the canvas", ...READ6 },
7173
7305
  handler: () => {
@@ -7177,6 +7309,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
7177
7309
  width: d.canvas.width,
7178
7310
  height: d.canvas.height,
7179
7311
  dpi: d.canvas.dpi,
7312
+ origin: { ...d.origin ?? { x: 0, y: 0 } },
7180
7313
  paper: { preset: d.paper.preset, name: d.paper.name, color: d.paper.color, depth: d.paper.depth },
7181
7314
  activeLayerId: d.activeLayerId,
7182
7315
  layerCount: c.listLayers().length,
@@ -7230,6 +7363,19 @@ function createPaintEditorToolProvider(controller, options = {}) {
7230
7363
  annotations: { title: "Resize the canvas", ...SET4 },
7231
7364
  handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
7232
7365
  });
7366
+ const EXTEND_SIDE = (side) => ({ type: "integer", minimum: 0, maximum: MAX_CANVAS_SIDE - 1, description: `Pixels to add on the ${side} (default 0).` });
7367
+ add({
7368
+ name: "extend_canvas",
7369
+ title: "Extend the canvas",
7370
+ description: "Grows the canvas by adding pixels on any side, keeping everything painted. Adding on the left or top moves the whole picture (layers, paper grain, guides, stencils, recorded strokes) right or down by that much in document pixels, so coordinates you read before change by (+left, +top). Wet paint dries first. One undo step. Returns the canvas, rect (the new canvas in the previous coordinates: x -left, y -top) and origin.",
7371
+ inputSchema: {
7372
+ type: "object",
7373
+ properties: { left: EXTEND_SIDE("left"), top: EXTEND_SIDE("top"), right: EXTEND_SIDE("right"), bottom: EXTEND_SIDE("bottom") },
7374
+ additionalProperties: false
7375
+ },
7376
+ annotations: { title: "Extend the canvas", ...WRITE5 },
7377
+ handler: (a) => c.extendCanvas({ left: a.left, top: a.top, right: a.right, bottom: a.bottom })
7378
+ });
7233
7379
  add({
7234
7380
  name: "list_papers",
7235
7381
  title: "List papers",
@@ -8904,6 +9050,7 @@ export {
8904
9050
  timeLapseZip,
8905
9051
  toStrokePoint,
8906
9052
  toggleStamp,
9053
+ translateStencil,
8907
9054
  updatePaintGradientStop,
8908
9055
  usesPaintExchange,
8909
9056
  variationAffine,
@@ -40,6 +40,14 @@ export interface PaperSettings {
40
40
  roughness: number;
41
41
  /** Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light, 0..1 (default 0). */
42
42
  metallic?: number;
43
+ /**
44
+ * Where the grain's (0, 0) sits in document pixels (missing = the top-left). `extendCanvas` moves
45
+ * it with the paint, so the texture stays under what was painted on it.
46
+ */
47
+ offset?: {
48
+ x: number;
49
+ y: number;
50
+ };
43
51
  }
44
52
  type PaperPreset = PaperSettings;
45
53
  /** The built-in paper library: generated papers in families. */
@@ -40,6 +40,14 @@ export interface PaperSettings {
40
40
  roughness: number;
41
41
  /** Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light, 0..1 (default 0). */
42
42
  metallic?: number;
43
+ /**
44
+ * Where the grain's (0, 0) sits in document pixels (missing = the top-left). `extendCanvas` moves
45
+ * it with the paint, so the texture stays under what was painted on it.
46
+ */
47
+ offset?: {
48
+ x: number;
49
+ y: number;
50
+ };
43
51
  }
44
52
  type PaperPreset = PaperSettings;
45
53
  /** The built-in paper library: generated papers in families. */
@@ -76,5 +76,7 @@ export declare function stencilMask(stencils: readonly Stencil[], width: number,
76
76
  height: number;
77
77
  data: Uint8Array;
78
78
  } | null;
79
+ /** A stencil moved by (dx, dy) document pixels (for a canvas extended to the left or top). */
80
+ export declare function translateStencil(s: Stencil, dx: number, dy: number): Stencil;
79
81
  /** A stencil scaled about the origin by `k` (for repainting at a larger size). */
80
82
  export declare function scaleStencil(s: Stencil, k: number): Stencil;
@@ -76,5 +76,7 @@ export declare function stencilMask(stencils: readonly Stencil[], width: number,
76
76
  height: number;
77
77
  data: Uint8Array;
78
78
  } | null;
79
+ /** A stencil moved by (dx, dy) document pixels (for a canvas extended to the left or top). */
80
+ export declare function translateStencil(s: Stencil, dx: number, dy: number): Stencil;
79
81
  /** A stencil scaled about the origin by `k` (for repainting at a larger size). */
80
82
  export declare function scaleStencil(s: Stencil, k: number): Stencil;
@@ -266,6 +266,7 @@ export declare class PaintWetLayers {
266
266
  snapshot(): WetSnapshot | null;
267
267
  /** Puts a snapshot back (`null`: no wet layers). Returns the layer surfaces it implies. */
268
268
  restore(snap: WetSnapshot | null): void;
269
+ private dropWorld;
269
270
  /** Forgets everything wet (a new canvas, a resize, opening a document). */
270
271
  reset(): void;
271
272
  /** Water overlay tiles (RGBA) over a document rectangle, for the view. */
@@ -266,6 +266,7 @@ export declare class PaintWetLayers {
266
266
  snapshot(): WetSnapshot | null;
267
267
  /** Puts a snapshot back (`null`: no wet layers). Returns the layer surfaces it implies. */
268
268
  restore(snap: WetSnapshot | null): void;
269
+ private dropWorld;
269
270
  /** Forgets everything wet (a new canvas, a resize, opening a document). */
270
271
  reset(): void;
271
272
  /** Water overlay tiles (RGBA) over a document rectangle, for the view. */