@pixerate/editor 0.103.0 → 0.105.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.
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/{chunk-NQLQ2JOI.js → chunk-4X5MIVCE.js} +1155 -295
- package/dist/chunk-4X5MIVCE.js.map +1 -0
- package/dist/{chunk-VSMM7B3P.cjs → chunk-ZID226ZG.cjs} +1169 -309
- package/dist/chunk-ZID226ZG.cjs.map +1 -0
- package/dist/{compositor-CTMU-B1t.d.cts → compositor-Bc3PNP3C.d.cts} +1 -1
- package/dist/{compositor-B1ZrKR89.d.ts → compositor-BsYYllvR.d.ts} +1 -1
- package/dist/{export-X9rY79p0.d.cts → export-_m3Es4ru.d.cts} +19 -4
- package/dist/{export-BLpMyCYg.d.ts → export-iMrop3Jm.d.ts} +19 -4
- package/dist/image-editor/index.cjs +523 -539
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +16 -8
- package/dist/image-editor/index.d.ts +16 -8
- package/dist/image-editor/index.js +37 -53
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{mixer-DGcH6HLu.d.cts → mixer-C1M1Nua_.d.cts} +1 -1
- package/dist/{mixer-DG_9Yu2W.d.ts → mixer-Cu5a9Bzd.d.ts} +1 -1
- package/dist/paint/index.d.cts +2 -2
- package/dist/paint/index.d.ts +2 -2
- package/dist/paint-editor/index.cjs +1531 -128
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +461 -14
- package/dist/paint-editor/index.d.ts +461 -14
- package/dist/paint-editor/index.js +1479 -76
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/raster/index.d.cts +4 -4
- package/dist/raster/index.d.ts +4 -4
- package/dist/{surface-AfHCH7BR.d.cts → surface-BBJN9wxg.d.cts} +1 -1
- package/dist/{surface-AfHCH7BR.d.ts → surface-BBJN9wxg.d.ts} +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +14 -5
- package/skills/pixerate-paint-editor/SKILL.md +49 -8
- package/dist/chunk-NQLQ2JOI.js.map +0 -1
- package/dist/chunk-VSMM7B3P.cjs.map +0 -1
|
@@ -4,16 +4,19 @@ import {
|
|
|
4
4
|
resolveExternalAssets,
|
|
5
5
|
serializeLayers,
|
|
6
6
|
serializeLayersExternal
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-4X5MIVCE.js";
|
|
8
8
|
import {
|
|
9
9
|
RasterCompositor,
|
|
10
10
|
bytesToBase64,
|
|
11
|
+
decodePng,
|
|
11
12
|
encodePng,
|
|
12
13
|
findLayer,
|
|
13
14
|
flattenLayers,
|
|
14
15
|
insertLayer,
|
|
15
16
|
isLayerLocked,
|
|
16
|
-
mapLayer
|
|
17
|
+
mapLayer,
|
|
18
|
+
parseDataUrl,
|
|
19
|
+
pngDataUrl
|
|
17
20
|
} from "../chunk-67FRIOPZ.js";
|
|
18
21
|
import "../chunk-GX332W2C.js";
|
|
19
22
|
import {
|
|
@@ -43,6 +46,7 @@ import {
|
|
|
43
46
|
finishTile,
|
|
44
47
|
isBlendMode,
|
|
45
48
|
readPixel,
|
|
49
|
+
readRegion,
|
|
46
50
|
surfaceFromPixels,
|
|
47
51
|
tileKey,
|
|
48
52
|
tilesInRect
|
|
@@ -1010,6 +1014,8 @@ var PaintStrokeSession = class {
|
|
|
1010
1014
|
/** Adds raw pen samples (document pixels). */
|
|
1011
1015
|
push(points) {
|
|
1012
1016
|
if (this.closed || !points.length) return;
|
|
1017
|
+
const filter = this.init.inputFilter;
|
|
1018
|
+
if (filter) points = points.map(filter);
|
|
1013
1019
|
for (const p of points) this.points.push(p);
|
|
1014
1020
|
this.lastRaw = points[points.length - 1];
|
|
1015
1021
|
if (this.dry) this.update(this.dry.push(points));
|
|
@@ -1307,7 +1313,7 @@ function downsampleRgba(data, width, height, tw, th) {
|
|
|
1307
1313
|
// src/paint-editor/serialize.ts
|
|
1308
1314
|
var PAINT_FORMAT = "pixerate-paint";
|
|
1309
1315
|
var PAINT_SCHEMA_VERSION = 1;
|
|
1310
|
-
var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes"]);
|
|
1316
|
+
var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches"]);
|
|
1311
1317
|
function documentExtensions(raw) {
|
|
1312
1318
|
const out = {};
|
|
1313
1319
|
for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;
|
|
@@ -1325,6 +1331,8 @@ function header(state, options) {
|
|
|
1325
1331
|
activeLayerId: state.activeLayerId
|
|
1326
1332
|
};
|
|
1327
1333
|
if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));
|
|
1334
|
+
if (options.guides !== void 0) out.guides = JSON.parse(JSON.stringify(options.guides));
|
|
1335
|
+
if (options.swatches !== void 0) out.swatches = JSON.parse(JSON.stringify(options.swatches));
|
|
1328
1336
|
return out;
|
|
1329
1337
|
}
|
|
1330
1338
|
function serializePaintDocument(state, options = {}) {
|
|
@@ -1372,7 +1380,7 @@ function deserializePaintDocument(raw) {
|
|
|
1372
1380
|
const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);
|
|
1373
1381
|
const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
|
|
1374
1382
|
const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : flattenLayers(layers).filter((l) => l.type === "pixel").pop()?.id ?? null;
|
|
1375
|
-
return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), problems };
|
|
1383
|
+
return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, problems };
|
|
1376
1384
|
}
|
|
1377
1385
|
async function deserializePaintDocumentAsync(raw, store) {
|
|
1378
1386
|
if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
|
|
@@ -1501,6 +1509,28 @@ function defaultImageEncoder() {
|
|
|
1501
1509
|
return new Uint8Array(await blob.arrayBuffer());
|
|
1502
1510
|
};
|
|
1503
1511
|
}
|
|
1512
|
+
async function decodeImageBlob(blob) {
|
|
1513
|
+
const g = globalThis;
|
|
1514
|
+
if (typeof g.createImageBitmap !== "function" || typeof g.OffscreenCanvas !== "function") throw new Error("Decoding images needs a browser (createImageBitmap and OffscreenCanvas).");
|
|
1515
|
+
const bitmap = await g.createImageBitmap(blob);
|
|
1516
|
+
const canvas = new g.OffscreenCanvas(bitmap.width, bitmap.height);
|
|
1517
|
+
const ctx = canvas.getContext("2d");
|
|
1518
|
+
if (!ctx) throw new Error("No 2D context to decode with.");
|
|
1519
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
1520
|
+
const data = new Uint8ClampedArray(ctx.getImageData(0, 0, bitmap.width, bitmap.height).data);
|
|
1521
|
+
bitmap.close?.();
|
|
1522
|
+
return { width: bitmap.width, height: bitmap.height, data };
|
|
1523
|
+
}
|
|
1524
|
+
function downloadBytes(bytes, fileName, mimeType) {
|
|
1525
|
+
const g = globalThis;
|
|
1526
|
+
if (!g.document || !g.URL || !g.Blob) throw new Error("Downloading needs a browser.");
|
|
1527
|
+
const url = g.URL.createObjectURL(new g.Blob([bytes], { type: mimeType }));
|
|
1528
|
+
const a = g.document.createElement("a");
|
|
1529
|
+
a.href = url;
|
|
1530
|
+
a.download = fileName;
|
|
1531
|
+
a.click();
|
|
1532
|
+
setTimeout(() => g.URL.revokeObjectURL(url), 1e3);
|
|
1533
|
+
}
|
|
1504
1534
|
|
|
1505
1535
|
// src/paint-editor/thumbnails.ts
|
|
1506
1536
|
function thumbnailStroke(width, height) {
|
|
@@ -1562,6 +1592,411 @@ function renderBrushThumbnail(brush, options) {
|
|
|
1562
1592
|
return { width, height, data };
|
|
1563
1593
|
}
|
|
1564
1594
|
|
|
1595
|
+
// src/paint-editor/guides.ts
|
|
1596
|
+
var SYMMETRY_MODES = ["off", "vertical", "horizontal", "dual", "diagonal", "kaleidoscope"];
|
|
1597
|
+
function defaultGuides(canvas) {
|
|
1598
|
+
return {
|
|
1599
|
+
symmetry: { mode: "off", center: { x: canvas.width / 2, y: canvas.height / 2 }, rotation: 0, segments: 6, mirror: true },
|
|
1600
|
+
perspective: { enabled: false, points: [], snap: true },
|
|
1601
|
+
tracing: null,
|
|
1602
|
+
references: []
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
var MAX_COORDINATE = 1e6;
|
|
1606
|
+
var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1607
|
+
var coord = (v, d) => finite2(v) && Math.abs(v) <= MAX_COORDINATE ? v : d;
|
|
1608
|
+
function normalizeSymmetry(raw, base) {
|
|
1609
|
+
const r = raw;
|
|
1610
|
+
const c = r.center ?? {};
|
|
1611
|
+
return {
|
|
1612
|
+
mode: SYMMETRY_MODES.includes(r.mode) ? r.mode : base.mode,
|
|
1613
|
+
center: { x: coord(c.x, base.center.x), y: coord(c.y, base.center.y) },
|
|
1614
|
+
rotation: finite2(r.rotation) ? (r.rotation % 360 + 360) % 360 : base.rotation,
|
|
1615
|
+
segments: finite2(r.segments) ? Math.min(32, Math.max(2, Math.round(r.segments))) : base.segments,
|
|
1616
|
+
mirror: typeof r.mirror === "boolean" ? r.mirror : base.mirror
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function reflectAbout(c, a) {
|
|
1620
|
+
const dx = Math.cos(a);
|
|
1621
|
+
const dy = Math.sin(a);
|
|
1622
|
+
const m00 = 2 * dx * dx - 1;
|
|
1623
|
+
const m01 = 2 * dx * dy;
|
|
1624
|
+
const m11 = 2 * dy * dy - 1;
|
|
1625
|
+
return [m00, m01, m01, m11, c.x - (m00 * c.x + m01 * c.y), c.y - (m01 * c.x + m11 * c.y)];
|
|
1626
|
+
}
|
|
1627
|
+
function rotateAbout(c, a) {
|
|
1628
|
+
const cos = Math.cos(a);
|
|
1629
|
+
const sin = Math.sin(a);
|
|
1630
|
+
return [cos, -sin, sin, cos, c.x - (cos * c.x - sin * c.y), c.y - (sin * c.x + cos * c.y)];
|
|
1631
|
+
}
|
|
1632
|
+
var rad = (deg) => deg * Math.PI / 180;
|
|
1633
|
+
function symmetryAxisAngles(g) {
|
|
1634
|
+
if (g.mode === "off") return [];
|
|
1635
|
+
if (g.mode === "kaleidoscope") return Array.from({ length: g.segments }, (_, k) => rad(g.rotation) + k * Math.PI / g.segments);
|
|
1636
|
+
const initial = g.mode === "horizontal" ? 0 : g.mode === "diagonal" ? 45 : 90;
|
|
1637
|
+
const a = rad(initial + g.rotation);
|
|
1638
|
+
return g.mode === "dual" ? [a, a + Math.PI / 2] : [a];
|
|
1639
|
+
}
|
|
1640
|
+
function symmetryCopies(g) {
|
|
1641
|
+
const c = g.center;
|
|
1642
|
+
switch (g.mode) {
|
|
1643
|
+
case "off":
|
|
1644
|
+
return [];
|
|
1645
|
+
case "vertical":
|
|
1646
|
+
case "horizontal":
|
|
1647
|
+
case "diagonal":
|
|
1648
|
+
return symmetryAxisAngles(g).map((a) => reflectAbout(c, a));
|
|
1649
|
+
case "dual": {
|
|
1650
|
+
const [a, b] = symmetryAxisAngles(g);
|
|
1651
|
+
return [reflectAbout(c, a), reflectAbout(c, b), rotateAbout(c, Math.PI)];
|
|
1652
|
+
}
|
|
1653
|
+
case "kaleidoscope": {
|
|
1654
|
+
const out = [];
|
|
1655
|
+
const n = g.segments;
|
|
1656
|
+
for (let k = 1; k < n; k++) out.push(rotateAbout(c, 2 * Math.PI * k / n));
|
|
1657
|
+
if (g.mirror) {
|
|
1658
|
+
const first = reflectAbout(c, rad(g.rotation));
|
|
1659
|
+
for (let k = 0; k < n; k++) out.push(composeAffine(rotateAbout(c, 2 * Math.PI * k / n), first));
|
|
1660
|
+
}
|
|
1661
|
+
return out;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function symmetryAxisSegments(g, size) {
|
|
1666
|
+
const out = [];
|
|
1667
|
+
for (const a of symmetryAxisAngles(g)) {
|
|
1668
|
+
const d = [Math.cos(a), Math.sin(a)];
|
|
1669
|
+
const origin = [g.center.x, g.center.y];
|
|
1670
|
+
const extent = [size.width, size.height];
|
|
1671
|
+
let lo = -Infinity;
|
|
1672
|
+
let hi = Infinity;
|
|
1673
|
+
let ok = true;
|
|
1674
|
+
for (let i = 0; i < 2; i++) {
|
|
1675
|
+
if (Math.abs(d[i]) < 1e-12) {
|
|
1676
|
+
if (origin[i] < 0 || origin[i] > extent[i]) ok = false;
|
|
1677
|
+
} else {
|
|
1678
|
+
const t0 = -origin[i] / d[i];
|
|
1679
|
+
const t1 = (extent[i] - origin[i]) / d[i];
|
|
1680
|
+
lo = Math.max(lo, Math.min(t0, t1));
|
|
1681
|
+
hi = Math.min(hi, Math.max(t0, t1));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (!ok || !Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo) continue;
|
|
1685
|
+
out.push([{ x: g.center.x + lo * d[0], y: g.center.y + lo * d[1] }, { x: g.center.x + hi * d[0], y: g.center.y + hi * d[1] }]);
|
|
1686
|
+
}
|
|
1687
|
+
return out;
|
|
1688
|
+
}
|
|
1689
|
+
function normalizePerspective(raw, base) {
|
|
1690
|
+
const r = raw;
|
|
1691
|
+
let points = base.points;
|
|
1692
|
+
if (Array.isArray(r.points)) {
|
|
1693
|
+
if (r.points.length > 3) throw new Error(`A perspective guide has one to three vanishing points; got ${r.points.length}.`);
|
|
1694
|
+
points = r.points.map((p, i) => {
|
|
1695
|
+
const q = p ?? {};
|
|
1696
|
+
if (!finite2(q.x) || !finite2(q.y) || Math.abs(q.x) > MAX_COORDINATE || Math.abs(q.y) > MAX_COORDINATE) {
|
|
1697
|
+
throw new Error(`points[${i}] needs finite x and y within \xB1${MAX_COORDINATE} document pixels.`);
|
|
1698
|
+
}
|
|
1699
|
+
return { x: q.x, y: q.y };
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
const enabled = typeof r.enabled === "boolean" ? r.enabled : base.enabled;
|
|
1703
|
+
if (enabled && !points.length) throw new Error("An enabled perspective guide needs at least one vanishing point; pass points.");
|
|
1704
|
+
return { enabled, points, snap: typeof r.snap === "boolean" ? r.snap : base.snap };
|
|
1705
|
+
}
|
|
1706
|
+
function perspectiveDirections(g, p) {
|
|
1707
|
+
const out = [];
|
|
1708
|
+
for (const v of g.points) {
|
|
1709
|
+
const dx = v.x - p.x;
|
|
1710
|
+
const dy = v.y - p.y;
|
|
1711
|
+
const len = Math.hypot(dx, dy);
|
|
1712
|
+
if (len > 1e-6) out.push({ x: dx / len, y: dy / len });
|
|
1713
|
+
}
|
|
1714
|
+
if (g.points.length <= 2) out.push({ x: 0, y: 1 });
|
|
1715
|
+
if (g.points.length <= 1) out.push({ x: 1, y: 0 });
|
|
1716
|
+
return out;
|
|
1717
|
+
}
|
|
1718
|
+
var SNAP_DECIDE_DISTANCE = 6;
|
|
1719
|
+
var PerspectiveSnapper = class {
|
|
1720
|
+
constructor(guide) {
|
|
1721
|
+
this.guide = guide;
|
|
1722
|
+
}
|
|
1723
|
+
guide;
|
|
1724
|
+
start = null;
|
|
1725
|
+
dir = null;
|
|
1726
|
+
/** The line the stroke follows, once chosen. */
|
|
1727
|
+
get line() {
|
|
1728
|
+
return this.start && this.dir ? { from: this.start, direction: this.dir } : null;
|
|
1729
|
+
}
|
|
1730
|
+
snap(p) {
|
|
1731
|
+
if (!this.guide.enabled || !this.guide.snap || !this.guide.points.length) return p;
|
|
1732
|
+
if (!this.start) {
|
|
1733
|
+
this.start = { x: p.x, y: p.y };
|
|
1734
|
+
return p;
|
|
1735
|
+
}
|
|
1736
|
+
const s = this.start;
|
|
1737
|
+
if (!this.dir) {
|
|
1738
|
+
const dx = p.x - s.x;
|
|
1739
|
+
const dy = p.y - s.y;
|
|
1740
|
+
const len = Math.hypot(dx, dy);
|
|
1741
|
+
if (len < SNAP_DECIDE_DISTANCE) return { ...p, x: s.x, y: s.y };
|
|
1742
|
+
let best = null;
|
|
1743
|
+
let bestDot = -1;
|
|
1744
|
+
for (const d2 of perspectiveDirections(this.guide, s)) {
|
|
1745
|
+
const dot = Math.abs((dx * d2.x + dy * d2.y) / len);
|
|
1746
|
+
if (dot > bestDot) {
|
|
1747
|
+
bestDot = dot;
|
|
1748
|
+
best = d2;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
this.dir = best;
|
|
1752
|
+
if (!best) return p;
|
|
1753
|
+
}
|
|
1754
|
+
const d = this.dir;
|
|
1755
|
+
const t = (p.x - s.x) * d.x + (p.y - s.y) * d.y;
|
|
1756
|
+
return { ...p, x: s.x + d.x * t, y: s.y + d.y * t };
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
function perspectiveGuideLines(g, size, count = 12) {
|
|
1760
|
+
const out = [];
|
|
1761
|
+
const far = Math.hypot(size.width, size.height) * 4;
|
|
1762
|
+
for (const v of g.points) {
|
|
1763
|
+
const cx = size.width / 2 - v.x;
|
|
1764
|
+
const cy = size.height / 2 - v.y;
|
|
1765
|
+
const inside = v.x >= 0 && v.y >= 0 && v.x <= size.width && v.y <= size.height;
|
|
1766
|
+
const mid = Math.atan2(cy, cx);
|
|
1767
|
+
const spread = inside ? Math.PI : Math.min(Math.PI, 2 * Math.atan2(Math.hypot(size.width, size.height) / 2, Math.hypot(cx, cy)));
|
|
1768
|
+
for (let i = 0; i < count; i++) {
|
|
1769
|
+
const a = inside ? 2 * Math.PI * i / count : mid - spread / 2 + spread * (i + 0.5) / count;
|
|
1770
|
+
out.push([{ ...v }, { x: v.x + Math.cos(a) * far, y: v.y + Math.sin(a) * far }]);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return out;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/paint-editor/color.ts
|
|
1777
|
+
var HEX2 = /^#?([0-9a-f]{6})$/i;
|
|
1778
|
+
function parseHexColor(value, what = "color") {
|
|
1779
|
+
const m = typeof value === "string" ? HEX2.exec(value.trim()) : null;
|
|
1780
|
+
if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
1781
|
+
const n = parseInt(m[1], 16);
|
|
1782
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
1783
|
+
}
|
|
1784
|
+
function rgbToHexColor(r, g, b) {
|
|
1785
|
+
return "#" + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("");
|
|
1786
|
+
}
|
|
1787
|
+
function rgbToHsv(r, g, b) {
|
|
1788
|
+
const R = r / 255;
|
|
1789
|
+
const G = g / 255;
|
|
1790
|
+
const B = b / 255;
|
|
1791
|
+
const max = Math.max(R, G, B);
|
|
1792
|
+
const min = Math.min(R, G, B);
|
|
1793
|
+
const d = max - min;
|
|
1794
|
+
let h = 0;
|
|
1795
|
+
if (d > 0) {
|
|
1796
|
+
if (max === R) h = 60 * ((G - B) / d % 6);
|
|
1797
|
+
else if (max === G) h = 60 * ((B - R) / d + 2);
|
|
1798
|
+
else h = 60 * ((R - G) / d + 4);
|
|
1799
|
+
}
|
|
1800
|
+
if (h < 0) h += 360;
|
|
1801
|
+
return { h, s: max === 0 ? 0 : d / max, v: max };
|
|
1802
|
+
}
|
|
1803
|
+
function hsvToRgb(h, s, v) {
|
|
1804
|
+
const hh = (h % 360 + 360) % 360 / 60;
|
|
1805
|
+
const c = v * s;
|
|
1806
|
+
const x = c * (1 - Math.abs(hh % 2 - 1));
|
|
1807
|
+
const m = v - c;
|
|
1808
|
+
const [r, g, b] = hh < 1 ? [c, x, 0] : hh < 2 ? [x, c, 0] : hh < 3 ? [0, c, x] : hh < 4 ? [0, x, c] : hh < 5 ? [x, 0, c] : [c, 0, x];
|
|
1809
|
+
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
|
|
1810
|
+
}
|
|
1811
|
+
function rgbToHsl(r, g, b) {
|
|
1812
|
+
const { h } = rgbToHsv(r, g, b);
|
|
1813
|
+
const max = Math.max(r, g, b) / 255;
|
|
1814
|
+
const min = Math.min(r, g, b) / 255;
|
|
1815
|
+
const l = (max + min) / 2;
|
|
1816
|
+
const d = max - min;
|
|
1817
|
+
return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };
|
|
1818
|
+
}
|
|
1819
|
+
function hslToRgb(h, s, l) {
|
|
1820
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
1821
|
+
const v = l + c / 2;
|
|
1822
|
+
return hsvToRgb(h, v === 0 ? 0 : c / v, v);
|
|
1823
|
+
}
|
|
1824
|
+
var RYB_STOPS = [
|
|
1825
|
+
[0, 0],
|
|
1826
|
+
[60, 30],
|
|
1827
|
+
[120, 60],
|
|
1828
|
+
[180, 120],
|
|
1829
|
+
[240, 240],
|
|
1830
|
+
[300, 280],
|
|
1831
|
+
[360, 360]
|
|
1832
|
+
];
|
|
1833
|
+
function piecewise(v, from) {
|
|
1834
|
+
const x = (v % 360 + 360) % 360;
|
|
1835
|
+
const to = from === 0 ? 1 : 0;
|
|
1836
|
+
for (let i = 1; i < RYB_STOPS.length; i++) {
|
|
1837
|
+
const a = RYB_STOPS[i - 1];
|
|
1838
|
+
const b = RYB_STOPS[i];
|
|
1839
|
+
if (x <= b[from]) return a[to] + (x - a[from]) / (b[from] - a[from]) * (b[to] - a[to]);
|
|
1840
|
+
}
|
|
1841
|
+
return x;
|
|
1842
|
+
}
|
|
1843
|
+
function wheelToHue(angle, wheel) {
|
|
1844
|
+
return wheel === "rgb" ? (angle % 360 + 360) % 360 : piecewise(angle, 0);
|
|
1845
|
+
}
|
|
1846
|
+
function hueToWheel(hue, wheel) {
|
|
1847
|
+
return wheel === "rgb" ? (hue % 360 + 360) % 360 : piecewise(hue, 1);
|
|
1848
|
+
}
|
|
1849
|
+
var HARMONY_KINDS = ["complementary", "split-complementary", "analogous", "triadic", "tetradic", "square", "monochromatic"];
|
|
1850
|
+
function colorHarmony(base, kind, options = {}) {
|
|
1851
|
+
const wheel = options.wheel ?? "ryb";
|
|
1852
|
+
const [r, g, b] = parseHexColor(base, "base");
|
|
1853
|
+
const hsv = rgbToHsv(r, g, b);
|
|
1854
|
+
const start = hueToWheel(hsv.h, wheel);
|
|
1855
|
+
const at = (angle) => {
|
|
1856
|
+
const a = (angle % 360 + 360) % 360;
|
|
1857
|
+
const [rr, gg, bb] = hsvToRgb(wheelToHue(a, wheel), hsv.s, hsv.v);
|
|
1858
|
+
return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(a * 10) / 10 };
|
|
1859
|
+
};
|
|
1860
|
+
const spread = options.spread ?? 30;
|
|
1861
|
+
const offsets = {
|
|
1862
|
+
complementary: [0, 180],
|
|
1863
|
+
"split-complementary": [0, 180 - spread, 180 + spread],
|
|
1864
|
+
analogous: [0, -spread, spread],
|
|
1865
|
+
triadic: [0, 120, 240],
|
|
1866
|
+
tetradic: [0, 60, 180, 240],
|
|
1867
|
+
square: [0, 90, 180, 270]
|
|
1868
|
+
};
|
|
1869
|
+
if (kind === "monochromatic") {
|
|
1870
|
+
const hsl = rgbToHsl(r, g, b);
|
|
1871
|
+
const steps = [0, -0.3, -0.15, 0.15, 0.3];
|
|
1872
|
+
return steps.map((d) => {
|
|
1873
|
+
const [rr, gg, bb] = d === 0 ? [r, g, b] : hslToRgb(hsl.h, hsl.s, Math.min(0.97, Math.max(0.03, hsl.l + d)));
|
|
1874
|
+
return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(start * 10) / 10 };
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
return offsets[kind].map((o, i) => i === 0 ? { color: rgbToHexColor(r, g, b), angle: Math.round(start * 10) / 10 } : at(start + o));
|
|
1878
|
+
}
|
|
1879
|
+
var DEFAULT_SWATCHES = [
|
|
1880
|
+
{ color: "#f7f5ee", name: "Warm white" },
|
|
1881
|
+
{ color: "#f3d23a", name: "Lemon yellow" },
|
|
1882
|
+
{ color: "#c8912f", name: "Yellow ochre" },
|
|
1883
|
+
{ color: "#d23a2a", name: "Warm red" },
|
|
1884
|
+
{ color: "#9e1f3d", name: "Crimson" },
|
|
1885
|
+
{ color: "#8a4a2b", name: "Burnt earth" },
|
|
1886
|
+
{ color: "#2f3f9e", name: "Deep blue" },
|
|
1887
|
+
{ color: "#1d6f8f", name: "Cool blue" },
|
|
1888
|
+
{ color: "#4f6e2c", name: "Leaf green" },
|
|
1889
|
+
{ color: "#25221f", name: "Warm black" }
|
|
1890
|
+
];
|
|
1891
|
+
|
|
1892
|
+
// src/paint-editor/mixing-pad.ts
|
|
1893
|
+
var MIXING_PAD_WIDTH = 360;
|
|
1894
|
+
var MIXING_PAD_HEIGHT = 220;
|
|
1895
|
+
var PAD_PAINT = normalizePaintBrush({
|
|
1896
|
+
id: "pad-paint",
|
|
1897
|
+
name: "Pad paint",
|
|
1898
|
+
category: "mixer",
|
|
1899
|
+
engine: "mixer",
|
|
1900
|
+
tip: { size: 28, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 0.7, pressureSize: false, pressureOpacity: false },
|
|
1901
|
+
mixer: { wet: 0.45, load: 1, mix: 0.4, loadEachStroke: true, cleanEachStroke: true, mixing: "pigment" }
|
|
1902
|
+
});
|
|
1903
|
+
var PAD_KNIFE = normalizePaintBrush({
|
|
1904
|
+
id: "pad-knife",
|
|
1905
|
+
name: "Palette knife",
|
|
1906
|
+
category: "smudge",
|
|
1907
|
+
engine: "mixer",
|
|
1908
|
+
loadsColor: false,
|
|
1909
|
+
tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow: 0.85, pressureSize: false, pressureOpacity: false },
|
|
1910
|
+
mixer: { wet: 1, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: "pigment" }
|
|
1911
|
+
});
|
|
1912
|
+
var MixingPad = class {
|
|
1913
|
+
width = MIXING_PAD_WIDTH;
|
|
1914
|
+
height = MIXING_PAD_HEIGHT;
|
|
1915
|
+
surface = emptySurface(4);
|
|
1916
|
+
/** Increments on every change (for redrawing). */
|
|
1917
|
+
version = 0;
|
|
1918
|
+
/** Paints (`paint`, with `color`) or smears (`knife`) through `points` in pad pixels. */
|
|
1919
|
+
stroke(points, options) {
|
|
1920
|
+
if (!points.length) throw new Error("points is empty; give at least one {x, y} in pad pixels.");
|
|
1921
|
+
for (const [i, p] of points.entries()) {
|
|
1922
|
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y) || p.x < 0 || p.y < 0 || p.x > this.width || p.y > this.height) {
|
|
1923
|
+
throw new Error(`points[${i}] (${p.x}, ${p.y}) is off the ${this.width}\xD7${this.height} mixing pad.`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
let brush = options.tool === "knife" ? PAD_KNIFE : PAD_PAINT;
|
|
1927
|
+
if (options.size !== void 0) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);
|
|
1928
|
+
const [r, g, b] = hexToRgb8(options.color);
|
|
1929
|
+
let result = null;
|
|
1930
|
+
const session = new PaintStrokeSession(
|
|
1931
|
+
{ update: () => {
|
|
1932
|
+
}, commit: (_s, s) => result = s },
|
|
1933
|
+
{
|
|
1934
|
+
layerId: "mixing-pad",
|
|
1935
|
+
surface: this.surface,
|
|
1936
|
+
offset: { x: 0, y: 0 },
|
|
1937
|
+
lockTransparency: false,
|
|
1938
|
+
brush,
|
|
1939
|
+
color: [r, g, b, 255],
|
|
1940
|
+
background: [255, 255, 255, 255],
|
|
1941
|
+
grain: null,
|
|
1942
|
+
paperDepth: 0,
|
|
1943
|
+
seed: 1,
|
|
1944
|
+
colorHex: options.color,
|
|
1945
|
+
affineCopies: [],
|
|
1946
|
+
copies: [IDENTITY_COPY],
|
|
1947
|
+
mixerLoad: mixerLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
|
|
1948
|
+
}
|
|
1949
|
+
);
|
|
1950
|
+
session.push(points);
|
|
1951
|
+
const res = session.end();
|
|
1952
|
+
if (result) {
|
|
1953
|
+
this.surface = result;
|
|
1954
|
+
this.version++;
|
|
1955
|
+
}
|
|
1956
|
+
return { bounds: res.bounds };
|
|
1957
|
+
}
|
|
1958
|
+
/** The average colour within `radius` pixels of (x, y), or `null` where the pad is bare. */
|
|
1959
|
+
pick(x, y, radius = 3) {
|
|
1960
|
+
if (!(x >= 0 && y >= 0 && x < this.width && y < this.height)) throw new Error(`(${x}, ${y}) is off the ${this.width}\xD7${this.height} mixing pad.`);
|
|
1961
|
+
const r = Math.max(0, Math.round(radius));
|
|
1962
|
+
const x0 = Math.max(0, Math.floor(x) - r);
|
|
1963
|
+
const y0 = Math.max(0, Math.floor(y) - r);
|
|
1964
|
+
const x1 = Math.min(this.width, Math.floor(x) + r + 1);
|
|
1965
|
+
const y1 = Math.min(this.height, Math.floor(y) + r + 1);
|
|
1966
|
+
const px = readRegion(this.surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
1967
|
+
let R = 0;
|
|
1968
|
+
let G = 0;
|
|
1969
|
+
let B = 0;
|
|
1970
|
+
let A = 0;
|
|
1971
|
+
for (let i = 0; i < px.length; i += 4) {
|
|
1972
|
+
const a = px[i + 3];
|
|
1973
|
+
R += px[i] * a;
|
|
1974
|
+
G += px[i + 1] * a;
|
|
1975
|
+
B += px[i + 2] * a;
|
|
1976
|
+
A += a;
|
|
1977
|
+
}
|
|
1978
|
+
if (A === 0) return null;
|
|
1979
|
+
const hex = (v) => Math.round(v / A).toString(16).padStart(2, "0");
|
|
1980
|
+
return `#${hex(R)}${hex(G)}${hex(B)}`;
|
|
1981
|
+
}
|
|
1982
|
+
clear() {
|
|
1983
|
+
this.surface = emptySurface(4);
|
|
1984
|
+
this.version++;
|
|
1985
|
+
}
|
|
1986
|
+
/** The pad over white, `width × height` RGBA. */
|
|
1987
|
+
pixels() {
|
|
1988
|
+
const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
|
|
1989
|
+
for (let i = 0; i < px.length; i += 4) {
|
|
1990
|
+
const a = px[i + 3] / 255;
|
|
1991
|
+
px[i] = Math.round(px[i] * a + 255 * (1 - a));
|
|
1992
|
+
px[i + 1] = Math.round(px[i + 1] * a + 255 * (1 - a));
|
|
1993
|
+
px[i + 2] = Math.round(px[i + 2] * a + 255 * (1 - a));
|
|
1994
|
+
px[i + 3] = 255;
|
|
1995
|
+
}
|
|
1996
|
+
return px;
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
1565
2000
|
// src/paint-editor/controller.ts
|
|
1566
2001
|
var PaintEditorError = class extends Error {
|
|
1567
2002
|
constructor(message) {
|
|
@@ -1569,9 +2004,9 @@ var PaintEditorError = class extends Error {
|
|
|
1569
2004
|
this.name = "PaintEditorError";
|
|
1570
2005
|
}
|
|
1571
2006
|
};
|
|
1572
|
-
var
|
|
2007
|
+
var HEX3 = /^#[0-9a-f]{6}$/i;
|
|
1573
2008
|
function parseColor(value, what = "color") {
|
|
1574
|
-
if (typeof value !== "string" || !
|
|
2009
|
+
if (typeof value !== "string" || !HEX3.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
1575
2010
|
return value.trim().toLowerCase();
|
|
1576
2011
|
}
|
|
1577
2012
|
function rgbaOf(hex) {
|
|
@@ -1612,6 +2047,13 @@ var PaintEditorController = class {
|
|
|
1612
2047
|
dirty = "all";
|
|
1613
2048
|
seedCounter = 1;
|
|
1614
2049
|
strokeCounter = 1;
|
|
2050
|
+
guides;
|
|
2051
|
+
swatches = DEFAULT_SWATCHES.map((s) => ({ ...s }));
|
|
2052
|
+
pad = new MixingPad();
|
|
2053
|
+
tracingVersion = 1;
|
|
2054
|
+
tracingCache = null;
|
|
2055
|
+
imagePixels = /* @__PURE__ */ new Map();
|
|
2056
|
+
nextReferenceId = 1;
|
|
1615
2057
|
viewCompositor = createPaintCompositor();
|
|
1616
2058
|
exportCompositor = createPaintCompositor();
|
|
1617
2059
|
constructor(options = {}) {
|
|
@@ -1623,6 +2065,7 @@ var PaintEditorController = class {
|
|
|
1623
2065
|
this.color = options.color ? parseColor(options.color) : "#1f2937";
|
|
1624
2066
|
this.hand = options.hand === "left" ? "left" : "right";
|
|
1625
2067
|
this.encodeImage = options.encodeImage;
|
|
2068
|
+
this.guides = defaultGuides(this.doc.canvas);
|
|
1626
2069
|
}
|
|
1627
2070
|
// --- State and events ----------------------------------------------------------
|
|
1628
2071
|
subscribe(listener) {
|
|
@@ -1654,6 +2097,9 @@ var PaintEditorController = class {
|
|
|
1654
2097
|
canRedo: this.redoStack.length > 0 && !this.session,
|
|
1655
2098
|
undoLabel: last?.label ?? null,
|
|
1656
2099
|
redoLabel: next?.label ?? null,
|
|
2100
|
+
guides: this.guides,
|
|
2101
|
+
swatches: this.swatches,
|
|
2102
|
+
mixingPadVersion: this.pad.version,
|
|
1657
2103
|
stroking: Boolean(this.session),
|
|
1658
2104
|
version: this.version
|
|
1659
2105
|
};
|
|
@@ -1723,6 +2169,8 @@ var PaintEditorController = class {
|
|
|
1723
2169
|
paper: options.paper === void 0 ? this.doc.paper : this.resolvePaper(options.paper)
|
|
1724
2170
|
});
|
|
1725
2171
|
this.extensions = {};
|
|
2172
|
+
this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
|
|
2173
|
+
this.tracingVersion++;
|
|
1726
2174
|
this.commit("New canvas", next);
|
|
1727
2175
|
return next;
|
|
1728
2176
|
}
|
|
@@ -2145,7 +2593,7 @@ var PaintEditorController = class {
|
|
|
2145
2593
|
}
|
|
2146
2594
|
/** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
|
|
2147
2595
|
strokeCopies() {
|
|
2148
|
-
return
|
|
2596
|
+
return symmetryCopies(this.guides.symmetry);
|
|
2149
2597
|
}
|
|
2150
2598
|
sessionFor(doc, init, host) {
|
|
2151
2599
|
const layer = (() => {
|
|
@@ -2160,7 +2608,11 @@ var PaintEditorController = class {
|
|
|
2160
2608
|
return l;
|
|
2161
2609
|
})();
|
|
2162
2610
|
const brush = this.strokeBrush(init);
|
|
2163
|
-
|
|
2611
|
+
let color = init.color === void 0 ? this.color : parseColor(init.color);
|
|
2612
|
+
const tracing = this.guides.tracing;
|
|
2613
|
+
if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
|
|
2614
|
+
const perspective = this.guides.perspective;
|
|
2615
|
+
const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;
|
|
2164
2616
|
const rgba = rgbaOf(color);
|
|
2165
2617
|
const seed = init.seed ?? mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647;
|
|
2166
2618
|
const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
|
|
@@ -2178,6 +2630,7 @@ var PaintEditorController = class {
|
|
|
2178
2630
|
colorHex: color,
|
|
2179
2631
|
affineCopies: copies,
|
|
2180
2632
|
copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
|
|
2633
|
+
inputFilter: snapper ? (p) => snapper.snap(p) : void 0,
|
|
2181
2634
|
mixerLoad: brush.engine === "mixer" ? mixerLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]) : EMPTY_MIXER_LOAD
|
|
2182
2635
|
});
|
|
2183
2636
|
return session;
|
|
@@ -2222,7 +2675,7 @@ var PaintEditorController = class {
|
|
|
2222
2675
|
this.commit(`${brush.name} stroke`, { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke] });
|
|
2223
2676
|
}
|
|
2224
2677
|
};
|
|
2225
|
-
const session = this.sessionFor(this.doc, init, host);
|
|
2678
|
+
const session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);
|
|
2226
2679
|
this.session = session;
|
|
2227
2680
|
this.emit([]);
|
|
2228
2681
|
return session;
|
|
@@ -2254,7 +2707,7 @@ var PaintEditorController = class {
|
|
|
2254
2707
|
paintStroke(init) {
|
|
2255
2708
|
this.requireIdle("painting");
|
|
2256
2709
|
const points = this.checkPoints(init.points);
|
|
2257
|
-
const { doc, result, brush } = this.runStroke(this.doc, init, points);
|
|
2710
|
+
const { doc, result, brush } = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);
|
|
2258
2711
|
if (result.mixerLoad && brush.loadsColor) this.mixerLoad = result.mixerLoad;
|
|
2259
2712
|
if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);
|
|
2260
2713
|
else this.emit([]);
|
|
@@ -2339,7 +2792,20 @@ var PaintEditorController = class {
|
|
|
2339
2792
|
}
|
|
2340
2793
|
/** View-only layers between the paper and the paint (the painting guides add the tracing image). */
|
|
2341
2794
|
underlayLayers() {
|
|
2342
|
-
|
|
2795
|
+
const t = this.guides.tracing;
|
|
2796
|
+
if (!t || !t.visible) return [];
|
|
2797
|
+
return [this.tracingNode(t)];
|
|
2798
|
+
}
|
|
2799
|
+
/** The tracing image resampled onto the canvas (cached until it or the canvas changes). */
|
|
2800
|
+
tracingNode(t) {
|
|
2801
|
+
const { width, height } = this.doc.canvas;
|
|
2802
|
+
const key = this.tracingVersion * 100003 + width * 7 + height;
|
|
2803
|
+
if (this.tracingCache?.key === key && this.tracingCache.node.opacity === t.opacity) return this.tracingCache.node;
|
|
2804
|
+
const src = this.imagePixels.get(t.src) ?? this.decodeImage(t.src);
|
|
2805
|
+
const image = resampleInto(src, t.rect, width, height);
|
|
2806
|
+
const node = { type: "image", id: TRACING_NODE_ID, name: "Tracing", visible: true, opacity: t.opacity, blend: "normal", image, version: key + Math.round(t.opacity * 1e3) / 1e4 };
|
|
2807
|
+
this.tracingCache = { key, node };
|
|
2808
|
+
return node;
|
|
2343
2809
|
}
|
|
2344
2810
|
renderLayers(options, view = false) {
|
|
2345
2811
|
let layers = this.currentLayers();
|
|
@@ -2380,14 +2846,18 @@ var PaintEditorController = class {
|
|
|
2380
2846
|
// --- Save, load and export -------------------------------------------------------
|
|
2381
2847
|
/** The document as JSON with inline PNG assets. */
|
|
2382
2848
|
toJSON(options = {}) {
|
|
2383
|
-
return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions });
|
|
2849
|
+
return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
|
|
2384
2850
|
}
|
|
2385
2851
|
/** The document as JSON with each PNG stored in `store`. */
|
|
2386
2852
|
toJSONExternal(store, options = {}) {
|
|
2387
|
-
return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions });
|
|
2853
|
+
return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
|
|
2388
2854
|
}
|
|
2389
2855
|
adopt(loaded) {
|
|
2390
2856
|
this.extensions = loaded.extensions;
|
|
2857
|
+
this.readGuides(loaded.guides, loaded.state, loaded.problems);
|
|
2858
|
+
if (Array.isArray(loaded.swatches)) {
|
|
2859
|
+
this.swatches = loaded.swatches.filter((x) => Boolean(x) && typeof x.color === "string" && /^#[0-9a-f]{6}$/i.test(x.color)).map((x) => typeof x.name === "string" ? { color: x.color.toLowerCase(), name: x.name } : { color: x.color.toLowerCase() });
|
|
2860
|
+
}
|
|
2391
2861
|
for (const s of loaded.state.strokes) {
|
|
2392
2862
|
const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
|
|
2393
2863
|
if (n >= this.strokeCounter) this.strokeCounter = n + 1;
|
|
@@ -2476,20 +2946,630 @@ var PaintEditorController = class {
|
|
|
2476
2946
|
const img = this.renderPreview(options);
|
|
2477
2947
|
return { data: bytesToBase64(encodePng(img.width, img.height, img.data)), width: img.width, height: img.height };
|
|
2478
2948
|
}
|
|
2949
|
+
// --- Guides --------------------------------------------------------------------
|
|
2950
|
+
setGuides(next) {
|
|
2951
|
+
this.guides = next;
|
|
2952
|
+
this.emit("all");
|
|
2953
|
+
return next;
|
|
2954
|
+
}
|
|
2955
|
+
/** Mirror and kaleidoscope painting: every new stroke paints its copies too (one stroke, one undo step). */
|
|
2956
|
+
setSymmetry(update) {
|
|
2957
|
+
const symmetry = normalizeSymmetry(update, this.guides.symmetry);
|
|
2958
|
+
this.setGuides({ ...this.guides, symmetry });
|
|
2959
|
+
return symmetry;
|
|
2960
|
+
}
|
|
2961
|
+
/** Perspective guides with one to three vanishing points that pointer strokes snap to. */
|
|
2962
|
+
setPerspective(update) {
|
|
2963
|
+
let perspective;
|
|
2964
|
+
try {
|
|
2965
|
+
perspective = normalizePerspective(update, this.guides.perspective);
|
|
2966
|
+
} catch (e) {
|
|
2967
|
+
throw new PaintEditorError(e.message);
|
|
2968
|
+
}
|
|
2969
|
+
this.setGuides({ ...this.guides, perspective });
|
|
2970
|
+
return perspective;
|
|
2971
|
+
}
|
|
2972
|
+
/** An image from a PNG (base64 or data URL) or from RGBA pixels, stored as a PNG data URL. */
|
|
2973
|
+
importImage(input, what) {
|
|
2974
|
+
let pixels;
|
|
2975
|
+
if (input.pixels) {
|
|
2976
|
+
const { width, height, data } = input.pixels;
|
|
2977
|
+
if (!(width > 0 && height > 0 && Number.isInteger(width) && Number.isInteger(height)) || data.length !== width * height * 4) {
|
|
2978
|
+
throw new PaintEditorError(`${what}: pixels must be width \xD7 height \xD7 4 bytes of RGBA.`);
|
|
2979
|
+
}
|
|
2980
|
+
pixels = { width, height, data: new Uint8ClampedArray(data) };
|
|
2981
|
+
} else if (typeof input.png === "string" && input.png.trim()) {
|
|
2982
|
+
pixels = this.decodeImage(input.png.trim().startsWith("data:") ? input.png.trim() : `data:image/png;base64,${input.png.trim()}`, what);
|
|
2983
|
+
} else throw new PaintEditorError(`${what}: pass png (a PNG as base64 or a data URL) or pixels.`);
|
|
2984
|
+
if (pixels.width > MAX_IMAGE_SIDE || pixels.height > MAX_IMAGE_SIDE) throw new PaintEditorError(`${what} is ${pixels.width}\xD7${pixels.height}; the limit is ${MAX_IMAGE_SIDE} px per side.`);
|
|
2985
|
+
const src = pngDataUrl(pixels.width, pixels.height, pixels.data);
|
|
2986
|
+
this.imagePixels.set(src, pixels);
|
|
2987
|
+
return { src, pixels };
|
|
2988
|
+
}
|
|
2989
|
+
decodeImage(src, what = "Image") {
|
|
2990
|
+
const cached = this.imagePixels.get(src);
|
|
2991
|
+
if (cached) return cached;
|
|
2992
|
+
const parsed = parseDataUrl(src);
|
|
2993
|
+
if (!parsed) throw new PaintEditorError(`${what} is not a data URL or base64 PNG.`);
|
|
2994
|
+
try {
|
|
2995
|
+
const png = decodePng(parsed.bytes);
|
|
2996
|
+
const px = { width: png.width, height: png.height, data: png.data };
|
|
2997
|
+
this.imagePixels.set(src, px);
|
|
2998
|
+
return px;
|
|
2999
|
+
} catch (e) {
|
|
3000
|
+
throw new PaintEditorError(`${what} could not be decoded as a PNG (${e.message}); decode other formats in the browser and pass pixels.`);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Puts an image under the paint to trace over: view only, never in exports. Placed to fit the
|
|
3005
|
+
* canvas unless `rect` is given; `sampleColors` makes strokes take their colour from it.
|
|
3006
|
+
*/
|
|
3007
|
+
setTracingImage(options) {
|
|
3008
|
+
const { src, pixels } = this.importImage(options, "The tracing image");
|
|
3009
|
+
const { width, height } = this.doc.canvas;
|
|
3010
|
+
const k = Math.min(width / pixels.width, height / pixels.height);
|
|
3011
|
+
const fit = { x: (width - pixels.width * k) / 2, y: (height - pixels.height * k) / 2, width: pixels.width * k, height: pixels.height * k };
|
|
3012
|
+
const tracing = { src, width: pixels.width, height: pixels.height, rect: fit, opacity: 0.35, visible: true, sampleColors: false };
|
|
3013
|
+
return this.updateTracing({ ...options, rect: options.rect ?? fit }, tracing);
|
|
3014
|
+
}
|
|
3015
|
+
/** Changes the tracing image's opacity, visibility, placement or colour sampling. */
|
|
3016
|
+
updateTracing(update, base = this.guides.tracing) {
|
|
3017
|
+
if (!base) throw new PaintEditorError("There is no tracing image; set one with set_tracing_image first.");
|
|
3018
|
+
if (update.opacity !== void 0 && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);
|
|
3019
|
+
if (update.rect && !(update.rect.width > 0 && update.rect.height > 0)) throw new PaintEditorError("rect needs a width and height above 0.");
|
|
3020
|
+
const tracing = {
|
|
3021
|
+
...base,
|
|
3022
|
+
...update.opacity !== void 0 ? { opacity: update.opacity } : {},
|
|
3023
|
+
...update.visible !== void 0 ? { visible: update.visible } : {},
|
|
3024
|
+
...update.sampleColors !== void 0 ? { sampleColors: update.sampleColors } : {},
|
|
3025
|
+
...update.rect ? { rect: { ...update.rect } } : {}
|
|
3026
|
+
};
|
|
3027
|
+
if (tracing.src !== base.src || update.rect) this.tracingVersion++;
|
|
3028
|
+
if (base !== this.guides.tracing) this.tracingVersion++;
|
|
3029
|
+
this.setGuides({ ...this.guides, tracing });
|
|
3030
|
+
return tracing;
|
|
3031
|
+
}
|
|
3032
|
+
clearTracingImage() {
|
|
3033
|
+
this.tracingVersion++;
|
|
3034
|
+
this.setGuides({ ...this.guides, tracing: null });
|
|
3035
|
+
}
|
|
3036
|
+
/** The tracing image's colour at a document pixel, or `null` outside it or where it is clear. */
|
|
3037
|
+
sampleTracingColor(x, y) {
|
|
3038
|
+
const t = this.guides.tracing;
|
|
3039
|
+
if (!t) return null;
|
|
3040
|
+
const src = this.decodeImage(t.src);
|
|
3041
|
+
const u = Math.floor((x - t.rect.x) / t.rect.width * src.width);
|
|
3042
|
+
const v = Math.floor((y - t.rect.y) / t.rect.height * src.height);
|
|
3043
|
+
if (u < 0 || v < 0 || u >= src.width || v >= src.height) return null;
|
|
3044
|
+
const i = (v * src.width + u) * 4;
|
|
3045
|
+
if (src.data[i + 3] === 0) return null;
|
|
3046
|
+
return rgbToHex(src.data[i], src.data[i + 1], src.data[i + 2]);
|
|
3047
|
+
}
|
|
3048
|
+
/** Adds a floating reference image to look at and pick colours from. */
|
|
3049
|
+
addReferenceImage(options) {
|
|
3050
|
+
const { src, pixels } = this.importImage(options, "The reference image");
|
|
3051
|
+
const ref = {
|
|
3052
|
+
id: `ref${this.nextReferenceId++}`,
|
|
3053
|
+
name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,
|
|
3054
|
+
src,
|
|
3055
|
+
width: pixels.width,
|
|
3056
|
+
height: pixels.height,
|
|
3057
|
+
x: options.x ?? 16 + this.guides.references.length * 24,
|
|
3058
|
+
y: options.y ?? 16 + this.guides.references.length * 24,
|
|
3059
|
+
displayWidth: options.displayWidth ?? Math.min(240, pixels.width),
|
|
3060
|
+
visible: true
|
|
3061
|
+
};
|
|
3062
|
+
this.setGuides({ ...this.guides, references: [...this.guides.references, ref] });
|
|
3063
|
+
return ref;
|
|
3064
|
+
}
|
|
3065
|
+
requireReference(id) {
|
|
3066
|
+
const r = this.guides.references.find((x) => x.id === id);
|
|
3067
|
+
if (!r) throw new PaintEditorError(`Unknown reference image "${id}". Call list_reference_images for the ids.`);
|
|
3068
|
+
return r;
|
|
3069
|
+
}
|
|
3070
|
+
/** Moves, resizes, renames, shows or hides a reference image. */
|
|
3071
|
+
updateReferenceImage(id, update) {
|
|
3072
|
+
const r = this.requireReference(id);
|
|
3073
|
+
if (update.displayWidth !== void 0 && !(update.displayWidth >= 16 && update.displayWidth <= 4096)) throw new PaintEditorError(`displayWidth ${update.displayWidth} is out of range; use 16..4096.`);
|
|
3074
|
+
const next = { ...r, ...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== void 0)) };
|
|
3075
|
+
this.setGuides({ ...this.guides, references: this.guides.references.map((x) => x.id === id ? next : x) });
|
|
3076
|
+
return next;
|
|
3077
|
+
}
|
|
3078
|
+
removeReferenceImage(id) {
|
|
3079
|
+
this.requireReference(id);
|
|
3080
|
+
this.setGuides({ ...this.guides, references: this.guides.references.filter((x) => x.id !== id) });
|
|
3081
|
+
}
|
|
3082
|
+
/** A reference image's pixels (decoded once). */
|
|
3083
|
+
referencePixels(id) {
|
|
3084
|
+
return this.decodeImage(this.requireReference(id).src);
|
|
3085
|
+
}
|
|
3086
|
+
/** The colour of a reference image at its pixel (x, y). */
|
|
3087
|
+
pickReferenceColor(id, x, y) {
|
|
3088
|
+
const r = this.requireReference(id);
|
|
3089
|
+
const px = this.decodeImage(r.src);
|
|
3090
|
+
const u = Math.floor(x);
|
|
3091
|
+
const v = Math.floor(y);
|
|
3092
|
+
if (!(u >= 0 && v >= 0 && u < px.width && v < px.height)) throw new PaintEditorError(`(${x}, ${y}) is outside reference "${r.name}" (${px.width}\xD7${px.height}); use its own pixel coordinates.`);
|
|
3093
|
+
const i = (v * px.width + u) * 4;
|
|
3094
|
+
return rgbToHex(px.data[i], px.data[i + 1], px.data[i + 2]);
|
|
3095
|
+
}
|
|
3096
|
+
readGuides(raw, state, problems) {
|
|
3097
|
+
const base = defaultGuides(state.canvas);
|
|
3098
|
+
this.guides = base;
|
|
3099
|
+
this.tracingVersion++;
|
|
3100
|
+
if (!raw || typeof raw !== "object") return;
|
|
3101
|
+
const r = raw;
|
|
3102
|
+
const guides = { ...base };
|
|
3103
|
+
guides.symmetry = normalizeSymmetry(r.symmetry ?? {}, base.symmetry);
|
|
3104
|
+
try {
|
|
3105
|
+
guides.perspective = normalizePerspective(r.perspective ?? {}, base.perspective);
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
problems.push(`Perspective guide: ${e.message}`);
|
|
3108
|
+
}
|
|
3109
|
+
const t = r.tracing;
|
|
3110
|
+
if (t && typeof t.src === "string") {
|
|
3111
|
+
try {
|
|
3112
|
+
const px = this.decodeImage(t.src, "The tracing image");
|
|
3113
|
+
guides.tracing = {
|
|
3114
|
+
src: t.src,
|
|
3115
|
+
width: px.width,
|
|
3116
|
+
height: px.height,
|
|
3117
|
+
rect: t.rect && t.rect.width > 0 && t.rect.height > 0 ? { ...t.rect } : { x: 0, y: 0, width: state.canvas.width, height: state.canvas.height },
|
|
3118
|
+
opacity: typeof t.opacity === "number" ? Math.min(1, Math.max(0, t.opacity)) : 0.35,
|
|
3119
|
+
visible: t.visible !== false,
|
|
3120
|
+
sampleColors: t.sampleColors === true
|
|
3121
|
+
};
|
|
3122
|
+
} catch (e) {
|
|
3123
|
+
problems.push(e.message);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
if (Array.isArray(r.references)) {
|
|
3127
|
+
for (const ref of r.references) {
|
|
3128
|
+
if (!ref || typeof ref.src !== "string" || typeof ref.id !== "string") continue;
|
|
3129
|
+
try {
|
|
3130
|
+
const px = this.decodeImage(ref.src, `Reference "${ref.name}"`);
|
|
3131
|
+
guides.references.push({ id: ref.id, name: String(ref.name ?? ref.id), src: ref.src, width: px.width, height: px.height, x: Number(ref.x) || 0, y: Number(ref.y) || 0, displayWidth: Number(ref.displayWidth) || 240, visible: ref.visible !== false });
|
|
3132
|
+
const n = Number(/^ref(\d+)$/.exec(ref.id)?.[1] ?? 0);
|
|
3133
|
+
if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;
|
|
3134
|
+
} catch (e) {
|
|
3135
|
+
problems.push(e.message);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
this.guides = guides;
|
|
3140
|
+
}
|
|
3141
|
+
// --- Swatches and harmonies ------------------------------------------------------
|
|
3142
|
+
/** Colour harmonies around a colour (default the paint colour) on the painter's RYB wheel or the RGB wheel. */
|
|
3143
|
+
harmony(kind, options = {}) {
|
|
3144
|
+
try {
|
|
3145
|
+
return colorHarmony(options.color === void 0 ? this.color : parseColor(options.color), kind, options);
|
|
3146
|
+
} catch (e) {
|
|
3147
|
+
throw new PaintEditorError(e.message);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
addSwatch(color, name) {
|
|
3151
|
+
const c = parseColor(color);
|
|
3152
|
+
this.swatches = [...this.swatches, name?.trim() ? { color: c, name: name.trim() } : { color: c }];
|
|
3153
|
+
this.emit([]);
|
|
3154
|
+
return this.swatches;
|
|
3155
|
+
}
|
|
3156
|
+
removeSwatch(index) {
|
|
3157
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.swatches.length) throw new PaintEditorError(`index ${index} is out of range; there are ${this.swatches.length} swatches (0-based).`);
|
|
3158
|
+
this.swatches = this.swatches.filter((_, i) => i !== index);
|
|
3159
|
+
this.emit([]);
|
|
3160
|
+
return this.swatches;
|
|
3161
|
+
}
|
|
3162
|
+
setSwatches(swatches) {
|
|
3163
|
+
this.swatches = swatches.map((x) => x.name ? { color: parseColor(x.color), name: x.name } : { color: parseColor(x.color) });
|
|
3164
|
+
this.emit([]);
|
|
3165
|
+
return this.swatches;
|
|
3166
|
+
}
|
|
3167
|
+
// --- Mixing pad ------------------------------------------------------------------
|
|
3168
|
+
get mixingPad() {
|
|
3169
|
+
return this.pad;
|
|
3170
|
+
}
|
|
3171
|
+
/** Paints (`paint`) or smears with the palette knife (`knife`) on the mixing pad, in pad pixels. */
|
|
3172
|
+
mixingPadStroke(points, options = {}) {
|
|
3173
|
+
const color = options.color === void 0 ? this.color : parseColor(options.color);
|
|
3174
|
+
if (options.size !== void 0 && !(options.size >= 1 && options.size <= 200)) throw new PaintEditorError(`size ${options.size} is out of range; use 1..200 px.`);
|
|
3175
|
+
try {
|
|
3176
|
+
const r = this.pad.stroke(points, { tool: options.tool ?? "paint", color, size: options.size });
|
|
3177
|
+
this.emit([]);
|
|
3178
|
+
return r;
|
|
3179
|
+
} catch (e) {
|
|
3180
|
+
throw new PaintEditorError(e.message);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
/** Picks the mixed colour on the pad (averaged over `radius` px); with `setColor` it becomes the paint colour. */
|
|
3184
|
+
mixingPadPick(x, y, options = {}) {
|
|
3185
|
+
let c;
|
|
3186
|
+
try {
|
|
3187
|
+
c = this.pad.pick(x, y, options.radius);
|
|
3188
|
+
} catch (e) {
|
|
3189
|
+
throw new PaintEditorError(e.message);
|
|
3190
|
+
}
|
|
3191
|
+
if (c && options.setColor) this.setColor(c);
|
|
3192
|
+
return c;
|
|
3193
|
+
}
|
|
3194
|
+
clearMixingPad() {
|
|
3195
|
+
this.pad.clear();
|
|
3196
|
+
this.emit([]);
|
|
3197
|
+
}
|
|
2479
3198
|
/** A layer's tile surface (live while stroking), for thumbnails. */
|
|
2480
3199
|
layerSurface(id) {
|
|
2481
3200
|
const l = findLayer(this.currentLayers(), id)?.layer;
|
|
2482
3201
|
return isPixelLayer(l) ? l.surface : null;
|
|
2483
3202
|
}
|
|
2484
3203
|
};
|
|
3204
|
+
var MAX_IMAGE_SIDE = 8192;
|
|
3205
|
+
function resampleInto(src, rect, width, height) {
|
|
3206
|
+
const out = new Uint8ClampedArray(width * height * 4);
|
|
3207
|
+
const x0 = Math.max(0, Math.floor(rect.x));
|
|
3208
|
+
const y0 = Math.max(0, Math.floor(rect.y));
|
|
3209
|
+
const x1 = Math.min(width, Math.ceil(rect.x + rect.width));
|
|
3210
|
+
const y1 = Math.min(height, Math.ceil(rect.y + rect.height));
|
|
3211
|
+
const sx = src.width / rect.width;
|
|
3212
|
+
const sy = src.height / rect.height;
|
|
3213
|
+
const d = src.data;
|
|
3214
|
+
for (let y = y0; y < y1; y++) {
|
|
3215
|
+
const v = Math.min(src.height - 1, Math.max(0, (y + 0.5 - rect.y) * sy - 0.5));
|
|
3216
|
+
const j0 = Math.floor(v);
|
|
3217
|
+
const j1 = Math.min(src.height - 1, j0 + 1);
|
|
3218
|
+
const fy = v - j0;
|
|
3219
|
+
for (let x = x0; x < x1; x++) {
|
|
3220
|
+
const u = (x + 0.5 - rect.x) * sx - 0.5;
|
|
3221
|
+
if (u < -0.5 || u > src.width - 0.5 || (y + 0.5 - rect.y) * sy < 0 || (y + 0.5 - rect.y) * sy > src.height) continue;
|
|
3222
|
+
const uc = Math.min(src.width - 1, Math.max(0, u));
|
|
3223
|
+
const i0 = Math.floor(uc);
|
|
3224
|
+
const i1 = Math.min(src.width - 1, i0 + 1);
|
|
3225
|
+
const fx = uc - i0;
|
|
3226
|
+
const o = (y * width + x) * 4;
|
|
3227
|
+
for (let c = 0; c < 4; c++) {
|
|
3228
|
+
const a = d[(j0 * src.width + i0) * 4 + c] + (d[(j0 * src.width + i1) * 4 + c] - d[(j0 * src.width + i0) * 4 + c]) * fx;
|
|
3229
|
+
const b = d[(j1 * src.width + i0) * 4 + c] + (d[(j1 * src.width + i1) * 4 + c] - d[(j1 * src.width + i0) * 4 + c]) * fx;
|
|
3230
|
+
out[o + c] = a + (b - a) * fy;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
return { width, height, data: out };
|
|
3235
|
+
}
|
|
2485
3236
|
|
|
2486
|
-
// src/paint-editor/tools.ts
|
|
3237
|
+
// src/paint-editor/guide-tools.ts
|
|
2487
3238
|
var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
2488
3239
|
var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
2489
3240
|
var SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
2490
3241
|
var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
2491
3242
|
var EMPTY = { type: "object", properties: {}, additionalProperties: false };
|
|
2492
|
-
var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description:
|
|
3243
|
+
var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Colour #rrggbb." };
|
|
3244
|
+
var POINT = {
|
|
3245
|
+
type: "object",
|
|
3246
|
+
properties: { x: { type: "number", description: "x in document pixels." }, y: { type: "number", description: "y in document pixels." } },
|
|
3247
|
+
required: ["x", "y"],
|
|
3248
|
+
additionalProperties: false
|
|
3249
|
+
};
|
|
3250
|
+
var RECT = {
|
|
3251
|
+
type: "object",
|
|
3252
|
+
description: "Placement in document pixels.",
|
|
3253
|
+
properties: {
|
|
3254
|
+
x: { type: "number", description: "Left edge." },
|
|
3255
|
+
y: { type: "number", description: "Top edge." },
|
|
3256
|
+
width: { type: "number", exclusiveMinimum: 0, description: "Width." },
|
|
3257
|
+
height: { type: "number", exclusiveMinimum: 0, description: "Height." }
|
|
3258
|
+
},
|
|
3259
|
+
required: ["x", "y", "width", "height"],
|
|
3260
|
+
additionalProperties: false
|
|
3261
|
+
};
|
|
3262
|
+
var PNG = { type: "string", minLength: 1, description: "The image as a PNG, base64 or a data:image/png URL." };
|
|
3263
|
+
var tracingInfo = (t) => t ? { width: t.width, height: t.height, rect: t.rect, opacity: t.opacity, visible: t.visible, sampleColors: t.sampleColors } : null;
|
|
3264
|
+
var refInfo = ({ src: _s, ...r }) => r;
|
|
3265
|
+
function createPaintGuideTools(c) {
|
|
3266
|
+
const guides = () => c.getState().guides;
|
|
3267
|
+
return [
|
|
3268
|
+
{
|
|
3269
|
+
name: "get_guides",
|
|
3270
|
+
title: "Get the painting guides",
|
|
3271
|
+
description: "Reads the painting guides: symmetry (mode, centre, rotation, segments, mirror, and how many copies each stroke paints), the perspective guide (vanishing points, snap), the tracing image (size, placement, opacity, visibility, colour sampling) and the reference images.",
|
|
3272
|
+
inputSchema: EMPTY,
|
|
3273
|
+
annotations: { title: "Get the painting guides", ...READ },
|
|
3274
|
+
handler: () => {
|
|
3275
|
+
const g = guides();
|
|
3276
|
+
return { symmetry: { ...g.symmetry, copies: symmetryCopies(g.symmetry).length }, perspective: g.perspective, tracing: tracingInfo(g.tracing), references: g.references.map(refInfo) };
|
|
3277
|
+
}
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
name: "set_symmetry",
|
|
3281
|
+
title: "Set mirror or kaleidoscope painting",
|
|
3282
|
+
description: "Turns symmetry on or off. Every stroke after it also paints its copies, as one stroke and one undo step: vertical, horizontal or diagonal mirror (one copy), dual (four-way), or kaleidoscope with n segments (rotated copies, and with mirror the reflections between them). Editor state, saved with the document. Returns the guide and the number of copies.",
|
|
3283
|
+
inputSchema: {
|
|
3284
|
+
type: "object",
|
|
3285
|
+
properties: {
|
|
3286
|
+
mode: { type: "string", enum: [...SYMMETRY_MODES], description: "off, vertical (mirror left / right), horizontal (top / bottom), dual, diagonal or kaleidoscope." },
|
|
3287
|
+
center: { ...POINT, description: "Centre of the axes in document pixels (default the canvas centre)." },
|
|
3288
|
+
rotation: { type: "number", minimum: -360, maximum: 360, description: "Turn the axes, degrees clockwise." },
|
|
3289
|
+
segments: { type: "integer", minimum: 2, maximum: 32, description: "Kaleidoscope segments (default 6)." },
|
|
3290
|
+
mirror: { type: "boolean", description: "Kaleidoscope: also reflect alternate segments (default true)." }
|
|
3291
|
+
},
|
|
3292
|
+
additionalProperties: false
|
|
3293
|
+
},
|
|
3294
|
+
annotations: { title: "Set mirror or kaleidoscope painting", ...SET },
|
|
3295
|
+
handler: (a) => {
|
|
3296
|
+
const s = c.setSymmetry(a);
|
|
3297
|
+
return { ...s, copies: symmetryCopies(s).length };
|
|
3298
|
+
}
|
|
3299
|
+
},
|
|
3300
|
+
{
|
|
3301
|
+
name: "set_perspective_guide",
|
|
3302
|
+
title: "Set the perspective guide",
|
|
3303
|
+
description: "Sets one to three vanishing points (document pixels, may be off the canvas). With snap on, pen strokes snap to the line through their start towards a vanishing point (one-point also keeps horizontals and verticals, two-point verticals); for paint_stroke pass snapToPerspective. Editor state, saved with the document. Returns the guide.",
|
|
3304
|
+
inputSchema: {
|
|
3305
|
+
type: "object",
|
|
3306
|
+
properties: {
|
|
3307
|
+
enabled: { type: "boolean", description: "Show and use the guide." },
|
|
3308
|
+
points: { type: "array", minItems: 1, maxItems: 3, items: { ...POINT, description: "A vanishing point." }, description: "The vanishing points." },
|
|
3309
|
+
snap: { type: "boolean", description: "Snap strokes to the guide lines (default true)." }
|
|
3310
|
+
},
|
|
3311
|
+
additionalProperties: false
|
|
3312
|
+
},
|
|
3313
|
+
annotations: { title: "Set the perspective guide", ...SET },
|
|
3314
|
+
handler: (a) => c.setPerspective(a)
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
name: "set_tracing_image",
|
|
3318
|
+
title: "Set a tracing image",
|
|
3319
|
+
description: "Puts an image under the paint to trace over (view only: never exported), fitted to the canvas unless rect is given, at opacity (default 0.35). With sampleColors, strokes without a colour take the image colour where they start, so painting over it copies its colours. Replaces any tracing image. Returns its size and placement.",
|
|
3320
|
+
inputSchema: {
|
|
3321
|
+
type: "object",
|
|
3322
|
+
properties: {
|
|
3323
|
+
png: PNG,
|
|
3324
|
+
rect: RECT,
|
|
3325
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 0.35)." },
|
|
3326
|
+
sampleColors: { type: "boolean", description: "Strokes take their colour from the image (default false)." }
|
|
3327
|
+
},
|
|
3328
|
+
required: ["png"],
|
|
3329
|
+
additionalProperties: false
|
|
3330
|
+
},
|
|
3331
|
+
annotations: { title: "Set a tracing image", ...WRITE },
|
|
3332
|
+
handler: (a) => tracingInfo(c.setTracingImage(a))
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
name: "update_tracing_image",
|
|
3336
|
+
title: "Change the tracing image",
|
|
3337
|
+
description: "Changes the tracing image opacity, visibility, placement or colour sampling. Returns it.",
|
|
3338
|
+
inputSchema: {
|
|
3339
|
+
type: "object",
|
|
3340
|
+
properties: {
|
|
3341
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1." },
|
|
3342
|
+
visible: { type: "boolean", description: "Show or hide it." },
|
|
3343
|
+
sampleColors: { type: "boolean", description: "Strokes take their colour from it." },
|
|
3344
|
+
rect: RECT
|
|
3345
|
+
},
|
|
3346
|
+
additionalProperties: false
|
|
3347
|
+
},
|
|
3348
|
+
annotations: { title: "Change the tracing image", ...SET },
|
|
3349
|
+
handler: (a) => tracingInfo(c.updateTracing(a))
|
|
3350
|
+
},
|
|
3351
|
+
{
|
|
3352
|
+
name: "clear_tracing_image",
|
|
3353
|
+
title: "Remove the tracing image",
|
|
3354
|
+
description: "Removes the tracing image. Returns null.",
|
|
3355
|
+
inputSchema: EMPTY,
|
|
3356
|
+
annotations: { title: "Remove the tracing image", ...DESTRUCTIVE },
|
|
3357
|
+
handler: () => {
|
|
3358
|
+
c.clearTracingImage();
|
|
3359
|
+
return { tracing: null };
|
|
3360
|
+
}
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
name: "add_reference_image",
|
|
3364
|
+
title: "Add a reference image",
|
|
3365
|
+
description: "Adds a floating reference image (shown beside the canvas, never painted) to look at and pick colours from with pick_reference_color. Returns its id, size and placement.",
|
|
3366
|
+
inputSchema: {
|
|
3367
|
+
type: "object",
|
|
3368
|
+
properties: {
|
|
3369
|
+
png: PNG,
|
|
3370
|
+
name: { type: "string", minLength: 1, description: "Display name." },
|
|
3371
|
+
x: { type: "number", description: "Panel left in viewport pixels." },
|
|
3372
|
+
y: { type: "number", description: "Panel top in viewport pixels." },
|
|
3373
|
+
displayWidth: { type: "number", minimum: 16, maximum: 4096, description: "Panel width in screen pixels (default up to 240)." }
|
|
3374
|
+
},
|
|
3375
|
+
required: ["png"],
|
|
3376
|
+
additionalProperties: false
|
|
3377
|
+
},
|
|
3378
|
+
annotations: { title: "Add a reference image", ...WRITE },
|
|
3379
|
+
handler: (a) => refInfo(c.addReferenceImage(a))
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
name: "list_reference_images",
|
|
3383
|
+
title: "List reference images",
|
|
3384
|
+
description: "Lists the reference images: id, name, size in pixels, panel placement and visibility.",
|
|
3385
|
+
inputSchema: EMPTY,
|
|
3386
|
+
annotations: { title: "List reference images", ...READ },
|
|
3387
|
+
handler: () => ({ references: guides().references.map(refInfo) })
|
|
3388
|
+
},
|
|
3389
|
+
{
|
|
3390
|
+
name: "update_reference_image",
|
|
3391
|
+
title: "Change a reference image",
|
|
3392
|
+
description: "Moves, resizes, renames, shows or hides a reference image panel. Returns it.",
|
|
3393
|
+
inputSchema: {
|
|
3394
|
+
type: "object",
|
|
3395
|
+
properties: {
|
|
3396
|
+
id: { type: "string", description: "Reference id from list_reference_images." },
|
|
3397
|
+
name: { type: "string", minLength: 1, description: "Display name." },
|
|
3398
|
+
x: { type: "number", description: "Panel left in viewport pixels." },
|
|
3399
|
+
y: { type: "number", description: "Panel top in viewport pixels." },
|
|
3400
|
+
displayWidth: { type: "number", minimum: 16, maximum: 4096, description: "Panel width in screen pixels." },
|
|
3401
|
+
visible: { type: "boolean", description: "Show or hide it." }
|
|
3402
|
+
},
|
|
3403
|
+
required: ["id"],
|
|
3404
|
+
additionalProperties: false
|
|
3405
|
+
},
|
|
3406
|
+
annotations: { title: "Change a reference image", ...SET },
|
|
3407
|
+
handler: ({ id, ...u }) => refInfo(c.updateReferenceImage(id, u))
|
|
3408
|
+
},
|
|
3409
|
+
{
|
|
3410
|
+
name: "remove_reference_image",
|
|
3411
|
+
title: "Remove a reference image",
|
|
3412
|
+
description: "Removes a reference image. Returns the removed id.",
|
|
3413
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Reference id from list_reference_images." } }, required: ["id"], additionalProperties: false },
|
|
3414
|
+
annotations: { title: "Remove a reference image", ...DESTRUCTIVE },
|
|
3415
|
+
handler: ({ id }) => {
|
|
3416
|
+
c.removeReferenceImage(id);
|
|
3417
|
+
return { removed: id };
|
|
3418
|
+
}
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
name: "pick_reference_color",
|
|
3422
|
+
title: "Pick a colour from a reference",
|
|
3423
|
+
description: "Reads the colour of a reference image at its own pixel (x, y). With setColor it becomes the paint colour. Returns #rrggbb.",
|
|
3424
|
+
inputSchema: {
|
|
3425
|
+
type: "object",
|
|
3426
|
+
properties: {
|
|
3427
|
+
id: { type: "string", description: "Reference id from list_reference_images." },
|
|
3428
|
+
x: { type: "number", minimum: 0, description: "x in the image\u2019s pixels." },
|
|
3429
|
+
y: { type: "number", minimum: 0, description: "y in the image\u2019s pixels." },
|
|
3430
|
+
setColor: { type: "boolean", description: "Also make it the paint colour (default false)." }
|
|
3431
|
+
},
|
|
3432
|
+
required: ["id", "x", "y"],
|
|
3433
|
+
additionalProperties: false
|
|
3434
|
+
},
|
|
3435
|
+
annotations: { title: "Pick a colour from a reference", ...SET },
|
|
3436
|
+
handler: ({ id, x, y, setColor }) => {
|
|
3437
|
+
const color = c.pickReferenceColor(id, x, y);
|
|
3438
|
+
if (setColor) c.setColor(color);
|
|
3439
|
+
return { color };
|
|
3440
|
+
}
|
|
3441
|
+
},
|
|
3442
|
+
{
|
|
3443
|
+
name: "color_harmony",
|
|
3444
|
+
title: "Colour harmonies",
|
|
3445
|
+
description: "Returns the colours of a harmony around a colour (default the paint colour): complementary, split-complementary, analogous, triadic, tetradic, square or monochromatic, on the painter\u2019s red-yellow-blue wheel (default: red\u2019s complement is green) or the RGB wheel. Changes nothing. Returns the colours with their wheel angles, the base first.",
|
|
3446
|
+
inputSchema: {
|
|
3447
|
+
type: "object",
|
|
3448
|
+
properties: {
|
|
3449
|
+
kind: { type: "string", enum: [...HARMONY_KINDS], description: "The harmony." },
|
|
3450
|
+
color: { ...COLOR, description: "Base colour #rrggbb (default the paint colour)." },
|
|
3451
|
+
wheel: { type: "string", enum: ["ryb", "rgb"], description: "ryb (painter\u2019s wheel, default) or rgb." },
|
|
3452
|
+
spread: { type: "number", minimum: 5, maximum: 90, description: "Analogous and split-complementary spread in degrees (default 30)." }
|
|
3453
|
+
},
|
|
3454
|
+
required: ["kind"],
|
|
3455
|
+
additionalProperties: false
|
|
3456
|
+
},
|
|
3457
|
+
annotations: { title: "Colour harmonies", ...READ },
|
|
3458
|
+
handler: ({ kind, color, wheel, spread }) => ({ kind, wheel: wheel ?? "ryb", colors: c.harmony(kind, { color, wheel, spread }) })
|
|
3459
|
+
},
|
|
3460
|
+
{
|
|
3461
|
+
name: "list_swatches",
|
|
3462
|
+
title: "List swatches",
|
|
3463
|
+
description: "Lists the colour swatches (index, colour, name).",
|
|
3464
|
+
inputSchema: EMPTY,
|
|
3465
|
+
annotations: { title: "List swatches", ...READ },
|
|
3466
|
+
handler: () => ({ swatches: c.getState().swatches.map((s, index) => ({ index, ...s })) })
|
|
3467
|
+
},
|
|
3468
|
+
{
|
|
3469
|
+
name: "add_swatch",
|
|
3470
|
+
title: "Add a swatch",
|
|
3471
|
+
description: "Adds a colour to the swatches (saved with the document). Returns the swatches.",
|
|
3472
|
+
inputSchema: {
|
|
3473
|
+
type: "object",
|
|
3474
|
+
properties: { color: COLOR, name: { type: "string", minLength: 1, description: "Optional name." } },
|
|
3475
|
+
required: ["color"],
|
|
3476
|
+
additionalProperties: false
|
|
3477
|
+
},
|
|
3478
|
+
annotations: { title: "Add a swatch", ...WRITE },
|
|
3479
|
+
handler: ({ color, name }) => ({ swatches: c.addSwatch(color, name).map((s, index) => ({ index, ...s })) })
|
|
3480
|
+
},
|
|
3481
|
+
{
|
|
3482
|
+
name: "remove_swatch",
|
|
3483
|
+
title: "Remove a swatch",
|
|
3484
|
+
description: "Removes a swatch by index (from list_swatches). Returns the swatches.",
|
|
3485
|
+
inputSchema: { type: "object", properties: { index: { type: "integer", minimum: 0, description: "0-based index." } }, required: ["index"], additionalProperties: false },
|
|
3486
|
+
annotations: { title: "Remove a swatch", ...DESTRUCTIVE },
|
|
3487
|
+
handler: ({ index }) => ({ swatches: c.removeSwatch(index).map((s, i) => ({ index: i, ...s })) })
|
|
3488
|
+
},
|
|
3489
|
+
{
|
|
3490
|
+
name: "mixing_pad_stroke",
|
|
3491
|
+
title: "Paint on the mixing pad",
|
|
3492
|
+
description: "Paints on the mixing pad (a 360 \xD7 220 scratch canvas, not the painting): tool paint lays down a colour with a pigment Mixer Brush; tool knife smears what is there like a palette knife, mixing it as paint. Points are pad pixels. Not an undo step. Returns the changed bounds; then pick_mixing_pad_color.",
|
|
3493
|
+
inputSchema: {
|
|
3494
|
+
type: "object",
|
|
3495
|
+
properties: {
|
|
3496
|
+
points: {
|
|
3497
|
+
type: "array",
|
|
3498
|
+
minItems: 1,
|
|
3499
|
+
maxItems: 2e3,
|
|
3500
|
+
description: "The path in pad pixels (0..360, 0..220).",
|
|
3501
|
+
items: {
|
|
3502
|
+
type: "object",
|
|
3503
|
+
properties: { x: { type: "number", description: "Pad x." }, y: { type: "number", description: "Pad y." }, pressure: { type: "number", minimum: 0, maximum: 1, description: "Pressure (default 1)." } },
|
|
3504
|
+
required: ["x", "y"],
|
|
3505
|
+
additionalProperties: false
|
|
3506
|
+
}
|
|
3507
|
+
},
|
|
3508
|
+
tool: { type: "string", enum: ["paint", "knife"], description: "paint (default) or knife." },
|
|
3509
|
+
color: { ...COLOR, description: "Paint colour (default the paint colour; ignored by the knife)." },
|
|
3510
|
+
size: { type: "number", minimum: 1, maximum: 200, description: "Tool size in pad pixels (default 28 paint, 34 knife)." }
|
|
3511
|
+
},
|
|
3512
|
+
required: ["points"],
|
|
3513
|
+
additionalProperties: false
|
|
3514
|
+
},
|
|
3515
|
+
annotations: { title: "Paint on the mixing pad", ...WRITE },
|
|
3516
|
+
handler: ({ points, tool, color, size }) => c.mixingPadStroke(points, { tool, color, size })
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
name: "pick_mixing_pad_color",
|
|
3520
|
+
title: "Pick from the mixing pad",
|
|
3521
|
+
description: "Reads the mixed colour on the pad at (x, y), averaged over radius pixels (default 3); with setColor it becomes the paint colour. Returns #rrggbb, or null where the pad is bare.",
|
|
3522
|
+
inputSchema: {
|
|
3523
|
+
type: "object",
|
|
3524
|
+
properties: {
|
|
3525
|
+
x: { type: "number", minimum: 0, maximum: 360, description: "Pad x." },
|
|
3526
|
+
y: { type: "number", minimum: 0, maximum: 220, description: "Pad y." },
|
|
3527
|
+
radius: { type: "integer", minimum: 0, maximum: 20, description: "Averaging radius in pixels (default 3)." },
|
|
3528
|
+
setColor: { type: "boolean", description: "Also make it the paint colour (default false)." }
|
|
3529
|
+
},
|
|
3530
|
+
required: ["x", "y"],
|
|
3531
|
+
additionalProperties: false
|
|
3532
|
+
},
|
|
3533
|
+
annotations: { title: "Pick from the mixing pad", ...SET },
|
|
3534
|
+
handler: ({ x, y, radius, setColor }) => ({ color: c.mixingPadPick(x, y, { radius, setColor }) })
|
|
3535
|
+
},
|
|
3536
|
+
{
|
|
3537
|
+
name: "clear_mixing_pad",
|
|
3538
|
+
title: "Clear the mixing pad",
|
|
3539
|
+
description: "Wipes the mixing pad clean.",
|
|
3540
|
+
inputSchema: EMPTY,
|
|
3541
|
+
annotations: { title: "Clear the mixing pad", ...DESTRUCTIVE },
|
|
3542
|
+
handler: () => {
|
|
3543
|
+
c.clearMixingPad();
|
|
3544
|
+
return { cleared: true };
|
|
3545
|
+
}
|
|
3546
|
+
},
|
|
3547
|
+
{
|
|
3548
|
+
name: "render_mixing_pad",
|
|
3549
|
+
title: "Render the mixing pad",
|
|
3550
|
+
description: "Renders the mixing pad as a PNG (360 \xD7 220, over white). Changes nothing.",
|
|
3551
|
+
inputSchema: EMPTY,
|
|
3552
|
+
annotations: { title: "Render the mixing pad", ...READ },
|
|
3553
|
+
handler: () => {
|
|
3554
|
+
const pad = c.mixingPad;
|
|
3555
|
+
return {
|
|
3556
|
+
content: [
|
|
3557
|
+
{ type: "image", data: bytesToBase64(encodePng(pad.width, pad.height, pad.pixels())), mimeType: "image/png" },
|
|
3558
|
+
{ type: "text", text: `Mixing pad ${pad.width}\xD7${pad.height}` }
|
|
3559
|
+
]
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
];
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// src/paint-editor/tools.ts
|
|
3567
|
+
var READ2 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
3568
|
+
var WRITE2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
3569
|
+
var SET2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
3570
|
+
var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
3571
|
+
var EMPTY2 = { type: "object", properties: {}, additionalProperties: false };
|
|
3572
|
+
var COLOR2 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
|
|
2493
3573
|
var LAYER_ID = { type: "string", description: "Layer id from list_layers (default the active layer)." };
|
|
2494
3574
|
var TIP = {
|
|
2495
3575
|
type: "object",
|
|
@@ -2589,8 +3669,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2589
3669
|
name: "get_canvas_info",
|
|
2590
3670
|
title: "Get the canvas",
|
|
2591
3671
|
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.",
|
|
2592
|
-
inputSchema:
|
|
2593
|
-
annotations: { title: "Get the canvas", ...
|
|
3672
|
+
inputSchema: EMPTY2,
|
|
3673
|
+
annotations: { title: "Get the canvas", ...READ2 },
|
|
2594
3674
|
handler: () => {
|
|
2595
3675
|
const s = c.getState();
|
|
2596
3676
|
const d = s.document;
|
|
@@ -2627,7 +3707,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2627
3707
|
},
|
|
2628
3708
|
additionalProperties: false
|
|
2629
3709
|
},
|
|
2630
|
-
annotations: { title: "Start a new canvas", ...
|
|
3710
|
+
annotations: { title: "Start a new canvas", ...DESTRUCTIVE2 },
|
|
2631
3711
|
handler: (a) => {
|
|
2632
3712
|
const d = c.newCanvas({ width: a.width, height: a.height, dpi: a.dpi, paper: a.paper });
|
|
2633
3713
|
return { width: d.canvas.width, height: d.canvas.height, dpi: d.canvas.dpi, paper: d.paper.preset, layerId: d.activeLayerId };
|
|
@@ -2646,15 +3726,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2646
3726
|
},
|
|
2647
3727
|
additionalProperties: false
|
|
2648
3728
|
},
|
|
2649
|
-
annotations: { title: "Resize the canvas", ...
|
|
3729
|
+
annotations: { title: "Resize the canvas", ...SET2 },
|
|
2650
3730
|
handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
|
|
2651
3731
|
});
|
|
2652
3732
|
add({
|
|
2653
3733
|
name: "list_papers",
|
|
2654
3734
|
title: "List papers",
|
|
2655
3735
|
description: "Lists the built-in paper library (generated grain, no textures): id, name, family, grain pattern, colour, grain depth and the wet-media properties (absorption, permeability, roughness). Use an id with set_paper or new_canvas.",
|
|
2656
|
-
inputSchema:
|
|
2657
|
-
annotations: { title: "List papers", ...
|
|
3736
|
+
inputSchema: EMPTY2,
|
|
3737
|
+
annotations: { title: "List papers", ...READ2 },
|
|
2658
3738
|
handler: () => ({
|
|
2659
3739
|
papers: PAPER_LIBRARY.map((p) => ({ id: p.preset, name: p.name, family: p.family, pattern: p.pattern, color: p.color, depth: p.depth, absorption: p.absorption, permeability: p.permeability, roughness: p.roughness })),
|
|
2660
3740
|
families: [...PAPER_FAMILIES],
|
|
@@ -2665,8 +3745,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2665
3745
|
name: "get_paper",
|
|
2666
3746
|
title: "Get the paper",
|
|
2667
3747
|
description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness).",
|
|
2668
|
-
inputSchema:
|
|
2669
|
-
annotations: { title: "Get the paper", ...
|
|
3748
|
+
inputSchema: EMPTY2,
|
|
3749
|
+
annotations: { title: "Get the paper", ...READ2 },
|
|
2670
3750
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
2671
3751
|
});
|
|
2672
3752
|
const tuning2 = {
|
|
@@ -2685,7 +3765,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2685
3765
|
type: "object",
|
|
2686
3766
|
properties: {
|
|
2687
3767
|
preset: { type: "string", description: "Paper id from list_papers (its settings, then the other fields here on top)." },
|
|
2688
|
-
color: { ...
|
|
3768
|
+
color: { ...COLOR2, description: "Paper colour #rrggbb." },
|
|
2689
3769
|
depth: { type: "number", minimum: 0, maximum: 1, description: "Grain depth 0..1 (0: brushes ignore the grain)." },
|
|
2690
3770
|
scale: { type: "number", minimum: 0.25, maximum: 8, description: "Grain scale: 1 natural, 2 twice as coarse." },
|
|
2691
3771
|
seed: { type: "integer", minimum: 0, description: "Grain seed: another sheet of the same paper." },
|
|
@@ -2701,15 +3781,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2701
3781
|
},
|
|
2702
3782
|
additionalProperties: false
|
|
2703
3783
|
},
|
|
2704
|
-
annotations: { title: "Set the paper", ...
|
|
3784
|
+
annotations: { title: "Set the paper", ...SET2 },
|
|
2705
3785
|
handler: (a) => c.setPaper(a)
|
|
2706
3786
|
});
|
|
2707
3787
|
add({
|
|
2708
3788
|
name: "list_layers",
|
|
2709
3789
|
title: "List layers",
|
|
2710
3790
|
description: "Lists the layers top first: id, name, type (pixel or group), visibility, opacity, blend, clipping, locks, whether it is active, the painted bounds in document pixels (null when empty), parent and depth.",
|
|
2711
|
-
inputSchema:
|
|
2712
|
-
annotations: { title: "List layers", ...
|
|
3791
|
+
inputSchema: EMPTY2,
|
|
3792
|
+
annotations: { title: "List layers", ...READ2 },
|
|
2713
3793
|
handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
|
|
2714
3794
|
});
|
|
2715
3795
|
add({
|
|
@@ -2726,7 +3806,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2726
3806
|
},
|
|
2727
3807
|
additionalProperties: false
|
|
2728
3808
|
},
|
|
2729
|
-
annotations: { title: "Add a layer", ...
|
|
3809
|
+
annotations: { title: "Add a layer", ...WRITE2 },
|
|
2730
3810
|
handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
|
|
2731
3811
|
});
|
|
2732
3812
|
add({
|
|
@@ -2757,7 +3837,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2757
3837
|
required: ["layerId"],
|
|
2758
3838
|
additionalProperties: false
|
|
2759
3839
|
},
|
|
2760
|
-
annotations: { title: "Change a layer", ...
|
|
3840
|
+
annotations: { title: "Change a layer", ...SET2 },
|
|
2761
3841
|
handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
|
|
2762
3842
|
});
|
|
2763
3843
|
add({
|
|
@@ -2765,7 +3845,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2765
3845
|
title: "Choose the active layer",
|
|
2766
3846
|
description: "Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.",
|
|
2767
3847
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
2768
|
-
annotations: { title: "Choose the active layer", ...
|
|
3848
|
+
annotations: { title: "Choose the active layer", ...SET2 },
|
|
2769
3849
|
handler: ({ layerId }) => {
|
|
2770
3850
|
c.setActiveLayer(layerId);
|
|
2771
3851
|
return c.describeLayer(c.getLayer(layerId));
|
|
@@ -2776,7 +3856,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2776
3856
|
title: "Remove a layer",
|
|
2777
3857
|
description: "Removes a layer (a group with its children). One undo step. Returns the removed id and the new active layer.",
|
|
2778
3858
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
2779
|
-
annotations: { title: "Remove a layer", ...
|
|
3859
|
+
annotations: { title: "Remove a layer", ...DESTRUCTIVE2 },
|
|
2780
3860
|
handler: ({ layerId }) => c.removeLayer(layerId)
|
|
2781
3861
|
});
|
|
2782
3862
|
add({
|
|
@@ -2784,7 +3864,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2784
3864
|
title: "Duplicate a layer",
|
|
2785
3865
|
description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
|
|
2786
3866
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
2787
|
-
annotations: { title: "Duplicate a layer", ...
|
|
3867
|
+
annotations: { title: "Duplicate a layer", ...WRITE2 },
|
|
2788
3868
|
handler: ({ layerId }) => c.duplicateLayer(layerId)
|
|
2789
3869
|
});
|
|
2790
3870
|
add({
|
|
@@ -2801,7 +3881,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2801
3881
|
required: ["layerId", "index"],
|
|
2802
3882
|
additionalProperties: false
|
|
2803
3883
|
},
|
|
2804
|
-
annotations: { title: "Reorder a layer", ...
|
|
3884
|
+
annotations: { title: "Reorder a layer", ...WRITE2 },
|
|
2805
3885
|
handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
|
|
2806
3886
|
});
|
|
2807
3887
|
add({
|
|
@@ -2809,7 +3889,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2809
3889
|
title: "Merge a layer down",
|
|
2810
3890
|
description: "Merges a pixel layer into the pixel layer directly below it, which keeps its name, opacity and blend. One undo step. Returns the merged layer.",
|
|
2811
3891
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "The upper layer, from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
2812
|
-
annotations: { title: "Merge a layer down", ...
|
|
3892
|
+
annotations: { title: "Merge a layer down", ...DESTRUCTIVE2 },
|
|
2813
3893
|
handler: ({ layerId }) => c.mergeDown(layerId)
|
|
2814
3894
|
});
|
|
2815
3895
|
add({
|
|
@@ -2820,7 +3900,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2820
3900
|
type: "object",
|
|
2821
3901
|
properties: {
|
|
2822
3902
|
layerId: LAYER_ID,
|
|
2823
|
-
color: { ...
|
|
3903
|
+
color: { ...COLOR2, description: "Fill colour #rrggbb (default the paint colour)." },
|
|
2824
3904
|
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
2825
3905
|
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
|
|
2826
3906
|
pressure: { type: "number", minimum: 0, maximum: 1, description: "With grain: how far into the valleys, 0..1 (default 0.5)." },
|
|
@@ -2828,7 +3908,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2828
3908
|
},
|
|
2829
3909
|
additionalProperties: false
|
|
2830
3910
|
},
|
|
2831
|
-
annotations: { title: "Fill a layer", ...
|
|
3911
|
+
annotations: { title: "Fill a layer", ...WRITE2 },
|
|
2832
3912
|
handler: (a) => c.fillLayer(a)
|
|
2833
3913
|
});
|
|
2834
3914
|
add({
|
|
@@ -2836,15 +3916,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2836
3916
|
title: "Clear a layer",
|
|
2837
3917
|
description: "Removes every pixel of a pixel layer. One undo step. Returns the layer id and the bounds that were cleared (null if it was empty).",
|
|
2838
3918
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID }, additionalProperties: false },
|
|
2839
|
-
annotations: { title: "Clear a layer", ...
|
|
3919
|
+
annotations: { title: "Clear a layer", ...DESTRUCTIVE2 },
|
|
2840
3920
|
handler: ({ layerId }) => c.clearLayer(layerId)
|
|
2841
3921
|
});
|
|
2842
3922
|
add({
|
|
2843
3923
|
name: "list_brushes",
|
|
2844
3924
|
title: "List brushes",
|
|
2845
3925
|
description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers, blender, smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response and rotation mode, plus the active brush id.",
|
|
2846
|
-
inputSchema:
|
|
2847
|
-
annotations: { title: "List brushes", ...
|
|
3926
|
+
inputSchema: EMPTY2,
|
|
3927
|
+
annotations: { title: "List brushes", ...READ2 },
|
|
2848
3928
|
handler: () => {
|
|
2849
3929
|
const s = c.getState();
|
|
2850
3930
|
return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(c.getBrush(s.brush.id)?.builtin), brushes: s.brushes.map(brushSummary) };
|
|
@@ -2855,7 +3935,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2855
3935
|
title: "Get a brush",
|
|
2856
3936
|
description: "Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer). Without id, the active brush as edited.",
|
|
2857
3937
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes (default the active brush)." } }, additionalProperties: false },
|
|
2858
|
-
annotations: { title: "Get a brush", ...
|
|
3938
|
+
annotations: { title: "Get a brush", ...READ2 },
|
|
2859
3939
|
handler: ({ id }) => {
|
|
2860
3940
|
if (id === void 0) return brushToJSON(c.getState().brush);
|
|
2861
3941
|
const b = c.getBrush(id);
|
|
@@ -2868,7 +3948,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2868
3948
|
title: "Set the brush",
|
|
2869
3949
|
description: "Chooses the active brush (id) and / or changes its settings; strokes without a brush use it. Editor state, not an undo step. Changes stay on the active brush until save_brush stores them. Returns the active brush.",
|
|
2870
3950
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes to start from." }, ...BRUSH_FIELDS }, additionalProperties: false },
|
|
2871
|
-
annotations: { title: "Set the brush", ...
|
|
3951
|
+
annotations: { title: "Set the brush", ...SET2 },
|
|
2872
3952
|
handler: (a) => brushToJSON(c.setBrush(a))
|
|
2873
3953
|
});
|
|
2874
3954
|
add({
|
|
@@ -2884,7 +3964,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2884
3964
|
required: ["id"],
|
|
2885
3965
|
additionalProperties: false
|
|
2886
3966
|
},
|
|
2887
|
-
annotations: { title: "Save a brush", ...
|
|
3967
|
+
annotations: { title: "Save a brush", ...SET2 },
|
|
2888
3968
|
handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name }))
|
|
2889
3969
|
});
|
|
2890
3970
|
add({
|
|
@@ -2892,7 +3972,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2892
3972
|
title: "Remove a saved brush",
|
|
2893
3973
|
description: "Removes a user brush from the library (built-in brushes cannot be removed). Library state, not an undo step. Returns the removed id.",
|
|
2894
3974
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "User brush id from list_brushes." } }, required: ["id"], additionalProperties: false },
|
|
2895
|
-
annotations: { title: "Remove a saved brush", ...
|
|
3975
|
+
annotations: { title: "Remove a saved brush", ...DESTRUCTIVE2 },
|
|
2896
3976
|
handler: ({ id }) => {
|
|
2897
3977
|
c.removeBrush(id);
|
|
2898
3978
|
return { removed: id };
|
|
@@ -2907,7 +3987,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2907
3987
|
properties: { ids: { type: "array", items: { type: "string", description: "Brush id." }, description: "Brush ids from list_brushes (default every user brush)." } },
|
|
2908
3988
|
additionalProperties: false
|
|
2909
3989
|
},
|
|
2910
|
-
annotations: { title: "Export brushes", ...
|
|
3990
|
+
annotations: { title: "Export brushes", ...READ2 },
|
|
2911
3991
|
handler: ({ ids }) => c.exportBrushes(ids)
|
|
2912
3992
|
});
|
|
2913
3993
|
add({
|
|
@@ -2923,7 +4003,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2923
4003
|
required: ["library"],
|
|
2924
4004
|
additionalProperties: false
|
|
2925
4005
|
},
|
|
2926
|
-
annotations: { title: "Import brushes", ...
|
|
4006
|
+
annotations: { title: "Import brushes", ...WRITE2 },
|
|
2927
4007
|
handler: ({ library, replace }) => c.importBrushes(library, { replace })
|
|
2928
4008
|
});
|
|
2929
4009
|
add({
|
|
@@ -2936,11 +4016,11 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2936
4016
|
id: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
|
|
2937
4017
|
width: { type: "integer", minimum: 16, maximum: 512, description: "Width in pixels (default 160)." },
|
|
2938
4018
|
height: { type: "integer", minimum: 16, maximum: 512, description: "Height in pixels (default 48)." },
|
|
2939
|
-
color: { ...
|
|
4019
|
+
color: { ...COLOR2, description: "Stroke colour (default the paint colour)." }
|
|
2940
4020
|
},
|
|
2941
4021
|
additionalProperties: false
|
|
2942
4022
|
},
|
|
2943
|
-
annotations: { title: "Render a brush thumbnail", ...
|
|
4023
|
+
annotations: { title: "Render a brush thumbnail", ...READ2 },
|
|
2944
4024
|
handler: ({ id, width, height, color }) => {
|
|
2945
4025
|
const t = c.brushThumbnail(id ?? c.getState().brush.id, { width, height, color });
|
|
2946
4026
|
return {
|
|
@@ -2957,10 +4037,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2957
4037
|
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Editor state, not an undo step. Returns both colours.",
|
|
2958
4038
|
inputSchema: {
|
|
2959
4039
|
type: "object",
|
|
2960
|
-
properties: { color: { ...
|
|
4040
|
+
properties: { color: { ...COLOR2, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR2, description: "Second colour #rrggbb." } },
|
|
2961
4041
|
additionalProperties: false
|
|
2962
4042
|
},
|
|
2963
|
-
annotations: { title: "Set the paint colour", ...
|
|
4043
|
+
annotations: { title: "Set the paint colour", ...SET2 },
|
|
2964
4044
|
handler: ({ color, secondaryColor }) => {
|
|
2965
4045
|
if (color !== void 0) c.setColor(color);
|
|
2966
4046
|
if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
|
|
@@ -2982,23 +4062,23 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
2982
4062
|
required: ["x", "y"],
|
|
2983
4063
|
additionalProperties: false
|
|
2984
4064
|
},
|
|
2985
|
-
annotations: { title: "Pick a colour", ...
|
|
4065
|
+
annotations: { title: "Pick a colour", ...READ2 },
|
|
2986
4066
|
handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
|
|
2987
4067
|
});
|
|
2988
4068
|
add({
|
|
2989
4069
|
name: "get_mixer_brush",
|
|
2990
4070
|
title: "Get the Mixer Brush load",
|
|
2991
4071
|
description: "Reads what the Mixer Brush carries between strokes: reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null. Call before painting with mixer-brush when its loadEachStroke is false.",
|
|
2992
|
-
inputSchema:
|
|
2993
|
-
annotations: { title: "Get the Mixer Brush load", ...
|
|
4072
|
+
inputSchema: EMPTY2,
|
|
4073
|
+
annotations: { title: "Get the Mixer Brush load", ...READ2 },
|
|
2994
4074
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
2995
4075
|
});
|
|
2996
4076
|
add({
|
|
2997
4077
|
name: "load_mixer_brush",
|
|
2998
4078
|
title: "Load the Mixer Brush",
|
|
2999
4079
|
description: "Fills the Mixer Brush reservoir with a colour (amount 1), keeping what it picked up. Editor state, not an undo step. Returns the load.",
|
|
3000
|
-
inputSchema: { type: "object", properties: { color: { ...
|
|
3001
|
-
annotations: { title: "Load the Mixer Brush", ...
|
|
4080
|
+
inputSchema: { type: "object", properties: { color: { ...COLOR2, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
|
|
4081
|
+
annotations: { title: "Load the Mixer Brush", ...SET2 },
|
|
3002
4082
|
handler: ({ color }) => loadResult(c.loadMixerBrush(color))
|
|
3003
4083
|
});
|
|
3004
4084
|
add({
|
|
@@ -3006,13 +4086,13 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3006
4086
|
title: "Clean the Mixer Brush",
|
|
3007
4087
|
description: "Empties the Mixer Brush pickup well (and with emptyReservoir its paint too). Editor state, not an undo step. Returns the load.",
|
|
3008
4088
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
3009
|
-
annotations: { title: "Clean the Mixer Brush", ...
|
|
4089
|
+
annotations: { title: "Clean the Mixer Brush", ...SET2 },
|
|
3010
4090
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
3011
4091
|
});
|
|
3012
4092
|
add({
|
|
3013
4093
|
name: "paint_stroke",
|
|
3014
4094
|
title: "Paint a brush stroke",
|
|
3015
|
-
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush, blender, smudge) picks up and moves paint already on the layer. The
|
|
4095
|
+
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush, blender, smudge) picks up and moves paint already on the layer. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes) and, for the mixer, its load after the stroke.",
|
|
3016
4096
|
inputSchema: {
|
|
3017
4097
|
type: "object",
|
|
3018
4098
|
properties: {
|
|
@@ -3020,15 +4100,16 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3020
4100
|
layerId: LAYER_ID,
|
|
3021
4101
|
brushId: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
|
|
3022
4102
|
brush: { type: "object", description: "Brush changes for this stroke only.", properties: BRUSH_FIELDS, additionalProperties: false },
|
|
3023
|
-
color: { ...
|
|
3024
|
-
seed: { type: "integer", minimum: 0, description: "Jitter seed (default a new one); the same seed and points give the same marks." }
|
|
4103
|
+
color: { ...COLOR2, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
4104
|
+
seed: { type: "integer", minimum: 0, description: "Jitter seed (default a new one); the same seed and points give the same marks." },
|
|
4105
|
+
snapToPerspective: { type: "boolean", description: "Snap the points to the perspective guide, as a pen stroke would (default false)." }
|
|
3025
4106
|
},
|
|
3026
4107
|
required: ["points"],
|
|
3027
4108
|
additionalProperties: false
|
|
3028
4109
|
},
|
|
3029
|
-
annotations: { title: "Paint a brush stroke", ...
|
|
4110
|
+
annotations: { title: "Paint a brush stroke", ...WRITE2 },
|
|
3030
4111
|
handler: (a) => {
|
|
3031
|
-
const r = c.paintStroke({ points: a.points, layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, seed: a.seed });
|
|
4112
|
+
const r = c.paintStroke({ points: a.points, layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, seed: a.seed, snapToPerspective: a.snapToPerspective });
|
|
3032
4113
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
3033
4114
|
}
|
|
3034
4115
|
});
|
|
@@ -3044,7 +4125,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3044
4125
|
},
|
|
3045
4126
|
additionalProperties: false
|
|
3046
4127
|
},
|
|
3047
|
-
annotations: { title: "List recorded strokes", ...
|
|
4128
|
+
annotations: { title: "List recorded strokes", ...READ2 },
|
|
3048
4129
|
handler: ({ ids, last }) => {
|
|
3049
4130
|
const all = c.getStrokes();
|
|
3050
4131
|
const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(last ?? 100));
|
|
@@ -3093,7 +4174,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3093
4174
|
additionalProperties: false
|
|
3094
4175
|
},
|
|
3095
4176
|
pressureScale: { type: "number", minimum: 0, maximum: 4, description: "Multiply every pressure (default 1)." },
|
|
3096
|
-
color: { ...
|
|
4177
|
+
color: { ...COLOR2, description: "Recolour (default each stroke\u2019s colour)." },
|
|
3097
4178
|
brushId: { type: "string", description: "Replay with this brush from list_brushes (default each stroke\u2019s brush)." },
|
|
3098
4179
|
layerId: { type: "string", description: "Paint on this layer (default each stroke\u2019s layer)." },
|
|
3099
4180
|
repeat: { type: "integer", minimum: 1, maximum: 100, description: "How many copies (default 1)." }
|
|
@@ -3101,7 +4182,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3101
4182
|
required: ["strokeIds"],
|
|
3102
4183
|
additionalProperties: false
|
|
3103
4184
|
},
|
|
3104
|
-
annotations: { title: "Replay strokes, varied", ...
|
|
4185
|
+
annotations: { title: "Replay strokes, varied", ...WRITE2 },
|
|
3105
4186
|
handler: (a) => c.replayStrokes(a)
|
|
3106
4187
|
});
|
|
3107
4188
|
add({
|
|
@@ -3118,7 +4199,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3118
4199
|
},
|
|
3119
4200
|
additionalProperties: false
|
|
3120
4201
|
},
|
|
3121
|
-
annotations: { title: "Rotate, mirror or zoom the view", ...
|
|
4202
|
+
annotations: { title: "Rotate, mirror or zoom the view", ...SET2 },
|
|
3122
4203
|
handler: ({ reset, ...v }) => {
|
|
3123
4204
|
if (reset) c.resetView();
|
|
3124
4205
|
return c.setView(v);
|
|
@@ -3129,15 +4210,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3129
4210
|
title: "Set the drawing hand",
|
|
3130
4211
|
description: "Says which hand holds the pen; palm rejection ignores touches on that side of the pen. Editor state, not an undo step. Returns the hand.",
|
|
3131
4212
|
inputSchema: { type: "object", properties: { hand: { type: "string", enum: ["right", "left"], description: "right or left." } }, required: ["hand"], additionalProperties: false },
|
|
3132
|
-
annotations: { title: "Set the drawing hand", ...
|
|
4213
|
+
annotations: { title: "Set the drawing hand", ...SET2 },
|
|
3133
4214
|
handler: ({ hand }) => ({ hand: c.setHand(hand) })
|
|
3134
4215
|
});
|
|
3135
4216
|
add({
|
|
3136
4217
|
name: "undo_paint",
|
|
3137
4218
|
title: "Undo",
|
|
3138
4219
|
description: "Undoes the last step (a stroke, a fill, a layer or paper change). Returns the label undone (null when there was nothing) and what can be undone and redone next.",
|
|
3139
|
-
inputSchema:
|
|
3140
|
-
annotations: { title: "Undo", ...
|
|
4220
|
+
inputSchema: EMPTY2,
|
|
4221
|
+
annotations: { title: "Undo", ...WRITE2 },
|
|
3141
4222
|
handler: () => {
|
|
3142
4223
|
const undone = c.undo();
|
|
3143
4224
|
const s = c.getState();
|
|
@@ -3148,8 +4229,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3148
4229
|
name: "redo_paint",
|
|
3149
4230
|
title: "Redo",
|
|
3150
4231
|
description: "Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.",
|
|
3151
|
-
inputSchema:
|
|
3152
|
-
annotations: { title: "Redo", ...
|
|
4232
|
+
inputSchema: EMPTY2,
|
|
4233
|
+
annotations: { title: "Redo", ...WRITE2 },
|
|
3153
4234
|
handler: () => {
|
|
3154
4235
|
const redone = c.redo();
|
|
3155
4236
|
const s = c.getState();
|
|
@@ -3169,7 +4250,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3169
4250
|
},
|
|
3170
4251
|
additionalProperties: false
|
|
3171
4252
|
},
|
|
3172
|
-
annotations: { title: "Render a preview", ...
|
|
4253
|
+
annotations: { title: "Render a preview", ...READ2 },
|
|
3173
4254
|
handler: ({ maxSide, layerIds, paper }) => {
|
|
3174
4255
|
if (layerIds) {
|
|
3175
4256
|
for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
|
|
@@ -3192,7 +4273,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3192
4273
|
},
|
|
3193
4274
|
additionalProperties: false
|
|
3194
4275
|
},
|
|
3195
|
-
annotations: { title: "Export the painting", ...
|
|
4276
|
+
annotations: { title: "Export the painting", ...READ2 },
|
|
3196
4277
|
handler: async ({ format, quality, scale, paper }) => {
|
|
3197
4278
|
const bytes = await c.exportImage({ format, quality, scale, paper });
|
|
3198
4279
|
return { mimeType: format ?? "image/png", bytes: bytes.length, data: bytesToBase64(bytes) };
|
|
@@ -3202,8 +4283,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3202
4283
|
name: "export_psd",
|
|
3203
4284
|
title: "Export as PSD",
|
|
3204
4285
|
description: 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked "Paper" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment blending has no PSD equivalent).',
|
|
3205
|
-
inputSchema:
|
|
3206
|
-
annotations: { title: "Export as PSD", ...
|
|
4286
|
+
inputSchema: EMPTY2,
|
|
4287
|
+
annotations: { title: "Export as PSD", ...READ2 },
|
|
3207
4288
|
handler: () => {
|
|
3208
4289
|
const r = c.exportPsd();
|
|
3209
4290
|
return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
|
|
@@ -3214,7 +4295,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3214
4295
|
title: "Save the document",
|
|
3215
4296
|
description: "Returns the document as JSON (format pixerate-paint, version 1): canvas, paper, layers with PNG data URLs, and the recorded strokes unless strokes is false. Changes nothing.",
|
|
3216
4297
|
inputSchema: { type: "object", properties: { strokes: { type: "boolean", description: "Include the recorded strokes (default true)." } }, additionalProperties: false },
|
|
3217
|
-
annotations: { title: "Save the document", ...
|
|
4298
|
+
annotations: { title: "Save the document", ...READ2 },
|
|
3218
4299
|
handler: ({ strokes }) => c.toJSON({ strokes })
|
|
3219
4300
|
});
|
|
3220
4301
|
add({
|
|
@@ -3222,72 +4303,376 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
3222
4303
|
title: "Open a document",
|
|
3223
4304
|
description: "Opens a saved paint document (as save_document returns), replacing the painting and clearing the undo history. Returns the canvas, the layers and anything that could not be read.",
|
|
3224
4305
|
inputSchema: { type: "object", properties: { document: { type: "object", description: "The saved document JSON." } }, required: ["document"], additionalProperties: false },
|
|
3225
|
-
annotations: { title: "Open a document", ...
|
|
4306
|
+
annotations: { title: "Open a document", ...DESTRUCTIVE2 },
|
|
3226
4307
|
handler: ({ document }) => {
|
|
3227
4308
|
const problems = c.loadJSON(document);
|
|
3228
4309
|
const d = c.document;
|
|
3229
4310
|
return { width: d.canvas.width, height: d.canvas.height, layers: c.listLayers(), strokes: d.strokes.length, problems };
|
|
3230
4311
|
}
|
|
3231
4312
|
});
|
|
4313
|
+
tools.push(...createPaintGuideTools(c));
|
|
3232
4314
|
tools.push(...createPigmentTools());
|
|
3233
4315
|
return { id: options.id ?? "paint-editor", namespace: options.namespace, tools };
|
|
3234
4316
|
}
|
|
4317
|
+
|
|
4318
|
+
// src/paint-editor/ui.ts
|
|
4319
|
+
var PAINT_TOOLS = [
|
|
4320
|
+
{ id: "brush", label: "Brush", key: "b" },
|
|
4321
|
+
{ id: "eyedropper", label: "Eyedropper", key: "i" },
|
|
4322
|
+
{ id: "pan", label: "Pan", key: "h" },
|
|
4323
|
+
{ id: "rotate", label: "Rotate view", key: "r" }
|
|
4324
|
+
];
|
|
4325
|
+
var PaintGesture = class {
|
|
4326
|
+
constructor(controller, box) {
|
|
4327
|
+
this.controller = controller;
|
|
4328
|
+
this.box = box;
|
|
4329
|
+
}
|
|
4330
|
+
controller;
|
|
4331
|
+
box;
|
|
4332
|
+
tool = "brush";
|
|
4333
|
+
/** The last refusal (a hidden or locked layer), for the editor to show. */
|
|
4334
|
+
error = null;
|
|
4335
|
+
/** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */
|
|
4336
|
+
predicted = [];
|
|
4337
|
+
mode = null;
|
|
4338
|
+
penAt = null;
|
|
4339
|
+
penDown = false;
|
|
4340
|
+
doc(e) {
|
|
4341
|
+
const b = this.box();
|
|
4342
|
+
const s = this.controller.getState();
|
|
4343
|
+
return screenToDocument(s.view, s.document.canvas, b, e.clientX - b.left, e.clientY - b.top);
|
|
4344
|
+
}
|
|
4345
|
+
pen(e) {
|
|
4346
|
+
const p = this.doc(e);
|
|
4347
|
+
return pointerToPenPoint(e, p.x, p.y, this.controller.getState().view);
|
|
4348
|
+
}
|
|
4349
|
+
/** Pointer down; returns true when the gesture took the pointer (capture it). */
|
|
4350
|
+
down(e) {
|
|
4351
|
+
this.error = null;
|
|
4352
|
+
if (this.mode) return false;
|
|
4353
|
+
if (e.pointerType === "pen") {
|
|
4354
|
+
this.penDown = true;
|
|
4355
|
+
this.penAt = { x: e.clientX, y: e.clientY };
|
|
4356
|
+
} else if (e.pointerType === "touch" && (this.penDown || isPalmTouch({ x: e.clientX, y: e.clientY }, this.penAt, this.controller.getState().hand))) {
|
|
4357
|
+
return false;
|
|
4358
|
+
}
|
|
4359
|
+
const middle = e.button === 1;
|
|
4360
|
+
const tool = middle ? "pan" : this.tool;
|
|
4361
|
+
if (tool === "pan") {
|
|
4362
|
+
this.mode = { kind: "pan", pointerId: e.pointerId, x: e.clientX, y: e.clientY, pan: { ...this.controller.getState().view.pan } };
|
|
4363
|
+
return true;
|
|
4364
|
+
}
|
|
4365
|
+
if (tool === "rotate") {
|
|
4366
|
+
const b = this.box();
|
|
4367
|
+
this.mode = { kind: "rotate", pointerId: e.pointerId, angle: Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2), rotation: this.controller.getState().view.rotation };
|
|
4368
|
+
return true;
|
|
4369
|
+
}
|
|
4370
|
+
if (tool === "eyedropper" || e.altKey) {
|
|
4371
|
+
this.pick(e);
|
|
4372
|
+
return false;
|
|
4373
|
+
}
|
|
4374
|
+
try {
|
|
4375
|
+
const p = this.pen(e);
|
|
4376
|
+
const session = this.controller.beginStroke({ start: p });
|
|
4377
|
+
session.push([p]);
|
|
4378
|
+
this.mode = { kind: "stroke", pointerId: e.pointerId };
|
|
4379
|
+
return true;
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
4382
|
+
return false;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
pick(e) {
|
|
4386
|
+
const p = this.doc(e);
|
|
4387
|
+
const { width, height } = this.controller.document.canvas;
|
|
4388
|
+
if (p.x < 0 || p.y < 0 || p.x >= width || p.y >= height) return;
|
|
4389
|
+
this.controller.setColor(this.controller.pickColor(p.x, p.y).color);
|
|
4390
|
+
}
|
|
4391
|
+
move(e) {
|
|
4392
|
+
if (e.pointerType === "pen") this.penAt = { x: e.clientX, y: e.clientY };
|
|
4393
|
+
const m = this.mode;
|
|
4394
|
+
if (!m || m.pointerId !== e.pointerId) return;
|
|
4395
|
+
if (m.kind === "pan") {
|
|
4396
|
+
this.controller.setView({ pan: { x: m.pan.x + e.clientX - m.x, y: m.pan.y + e.clientY - m.y } });
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
if (m.kind === "rotate") {
|
|
4400
|
+
const b = this.box();
|
|
4401
|
+
const a = Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2);
|
|
4402
|
+
let r = m.rotation + (a - m.angle) * 180 / Math.PI;
|
|
4403
|
+
if (e.shiftKey) r = Math.round(r / 15) * 15;
|
|
4404
|
+
this.controller.setView({ rotation: r });
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
const session = this.controller.activeStroke;
|
|
4408
|
+
if (!session) return;
|
|
4409
|
+
const events = typeof e.getCoalescedEvents === "function" ? e.getCoalescedEvents() : [];
|
|
4410
|
+
session.push((events.length ? events : [e]).map((ev) => this.pen(ev)));
|
|
4411
|
+
const predicted = typeof e.getPredictedEvents === "function" ? e.getPredictedEvents() : [];
|
|
4412
|
+
this.predicted = predicted.map((ev) => this.pen(ev));
|
|
4413
|
+
}
|
|
4414
|
+
up(e) {
|
|
4415
|
+
if (e.pointerType === "pen") this.penDown = false;
|
|
4416
|
+
const m = this.mode;
|
|
4417
|
+
if (!m || m.pointerId !== e.pointerId) return;
|
|
4418
|
+
this.mode = null;
|
|
4419
|
+
this.predicted = [];
|
|
4420
|
+
if (m.kind === "stroke") this.controller.activeStroke?.end();
|
|
4421
|
+
}
|
|
4422
|
+
cancel() {
|
|
4423
|
+
this.penDown = false;
|
|
4424
|
+
this.predicted = [];
|
|
4425
|
+
if (this.mode?.kind === "stroke") this.controller.activeStroke?.cancel();
|
|
4426
|
+
this.mode = null;
|
|
4427
|
+
}
|
|
4428
|
+
get active() {
|
|
4429
|
+
return this.mode !== null;
|
|
4430
|
+
}
|
|
4431
|
+
/** Wheel: zoom about the pointer (Ctrl / pinch or plain wheel), Shift+wheel rotates the view. */
|
|
4432
|
+
wheel(e) {
|
|
4433
|
+
const s = this.controller.getState();
|
|
4434
|
+
if (e.shiftKey) {
|
|
4435
|
+
this.controller.setView({ rotation: s.view.rotation + (e.deltaY > 0 ? 5 : -5) });
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4438
|
+
const b = this.box();
|
|
4439
|
+
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, s.view.zoom * Math.exp(-e.deltaY * 15e-4)));
|
|
4440
|
+
const k = zoom / s.view.zoom;
|
|
4441
|
+
const px = e.clientX - b.left - b.width / 2;
|
|
4442
|
+
const py = e.clientY - b.top - b.height / 2;
|
|
4443
|
+
this.controller.setView({ zoom, pan: { x: px - (px - s.view.pan.x) * k, y: py - (py - s.view.pan.y) * k } });
|
|
4444
|
+
}
|
|
4445
|
+
};
|
|
4446
|
+
function handlePaintShortcut(controller, gesture, e) {
|
|
4447
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
4448
|
+
const key = e.key.toLowerCase();
|
|
4449
|
+
if (mod && key === "z") {
|
|
4450
|
+
if (controller.getState().stroking) return true;
|
|
4451
|
+
if (e.shiftKey) controller.redo();
|
|
4452
|
+
else controller.undo();
|
|
4453
|
+
return true;
|
|
4454
|
+
}
|
|
4455
|
+
if (mod && key === "y") {
|
|
4456
|
+
controller.redo();
|
|
4457
|
+
return true;
|
|
4458
|
+
}
|
|
4459
|
+
if (mod) return false;
|
|
4460
|
+
const tool = PAINT_TOOLS.find((t) => t.key === key);
|
|
4461
|
+
if (tool) {
|
|
4462
|
+
gesture.tool = tool.id;
|
|
4463
|
+
return true;
|
|
4464
|
+
}
|
|
4465
|
+
const brush = controller.getState().brush;
|
|
4466
|
+
if (key === "[" || key === "]") {
|
|
4467
|
+
const step = Math.max(1, Math.round(brush.tip.size * 0.15));
|
|
4468
|
+
controller.setBrush({ tip: { size: Math.max(1, brush.tip.size + (key === "]" ? step : -step)) } });
|
|
4469
|
+
return true;
|
|
4470
|
+
}
|
|
4471
|
+
if (key === "x") {
|
|
4472
|
+
controller.swapColors();
|
|
4473
|
+
return true;
|
|
4474
|
+
}
|
|
4475
|
+
if (key === "m") {
|
|
4476
|
+
controller.setView({ mirror: !controller.getState().view.mirror });
|
|
4477
|
+
return true;
|
|
4478
|
+
}
|
|
4479
|
+
if (key === "escape") {
|
|
4480
|
+
controller.resetView();
|
|
4481
|
+
return true;
|
|
4482
|
+
}
|
|
4483
|
+
return false;
|
|
4484
|
+
}
|
|
4485
|
+
var PaintCanvasPresenter = class {
|
|
4486
|
+
constructor(controller) {
|
|
4487
|
+
this.controller = controller;
|
|
4488
|
+
}
|
|
4489
|
+
controller;
|
|
4490
|
+
uploaded = /* @__PURE__ */ new Map();
|
|
4491
|
+
scratch = null;
|
|
4492
|
+
size = "";
|
|
4493
|
+
/** Draws what changed into `ctx` (a canvas the size of the document); returns the tiles drawn. */
|
|
4494
|
+
render(ctx) {
|
|
4495
|
+
const { width, height } = this.controller.document.canvas;
|
|
4496
|
+
const size = `${width}x${height}`;
|
|
4497
|
+
let dirty = this.controller.takeDirty();
|
|
4498
|
+
if (size !== this.size) {
|
|
4499
|
+
this.size = size;
|
|
4500
|
+
this.uploaded.clear();
|
|
4501
|
+
dirty = "all";
|
|
4502
|
+
}
|
|
4503
|
+
let drawn = 0;
|
|
4504
|
+
const rect = { x: 0, y: 0, width, height };
|
|
4505
|
+
this.controller.compositeTiles(rect, (tx, ty, data) => {
|
|
4506
|
+
const key = `${tx},${ty}`;
|
|
4507
|
+
if (this.uploaded.get(key) === data) return;
|
|
4508
|
+
this.uploaded.set(key, data);
|
|
4509
|
+
if (!this.scratch) this.scratch = ctx.createImageData(TILE, TILE);
|
|
4510
|
+
this.scratch.data.set(data);
|
|
4511
|
+
const ox = tx * TILE;
|
|
4512
|
+
const oy = ty * TILE;
|
|
4513
|
+
ctx.putImageData(this.scratch, ox, oy, 0, 0, Math.min(TILE, width - ox), Math.min(TILE, height - oy));
|
|
4514
|
+
drawn++;
|
|
4515
|
+
}, { dirty });
|
|
4516
|
+
return drawn;
|
|
4517
|
+
}
|
|
4518
|
+
};
|
|
4519
|
+
function guideOverlay(controller) {
|
|
4520
|
+
const s = controller.getState();
|
|
4521
|
+
const size = s.document.canvas;
|
|
4522
|
+
const out = [];
|
|
4523
|
+
for (const [from, to] of symmetryAxisSegments(s.guides.symmetry, size)) out.push({ from, to, kind: "symmetry" });
|
|
4524
|
+
if (s.guides.perspective.enabled) for (const [from, to] of perspectiveGuideLines(s.guides.perspective, size, 16)) out.push({ from, to, kind: "perspective" });
|
|
4525
|
+
return out;
|
|
4526
|
+
}
|
|
4527
|
+
var BRUSH_SLIDERS = [
|
|
4528
|
+
{ path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
|
|
4529
|
+
{ path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
|
|
4530
|
+
{ path: "tip.flow", label: "Flow", min: 0, max: 1, step: 0.01 },
|
|
4531
|
+
{ path: "tip.hardness", label: "Hardness", min: 0, max: 1, step: 0.01 },
|
|
4532
|
+
{ path: "tip.spacing", label: "Spacing", min: 0.01, max: 1, step: 0.01 },
|
|
4533
|
+
{ path: "tip.roundness", label: "Roundness", min: 0.05, max: 1, step: 0.01 },
|
|
4534
|
+
{ path: "tip.angle", label: "Angle", min: -180, max: 180, step: 1 },
|
|
4535
|
+
{ path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 },
|
|
4536
|
+
{ path: "tip.smoothing", label: "Smoothing", min: 0, max: 0.95, step: 0.01 },
|
|
4537
|
+
{ path: "pressureSmoothing", label: "Pressure smoothing", min: 0, max: 0.95, step: 0.01 },
|
|
4538
|
+
{ path: "dynamics.minSize", label: "Min size", min: 0, max: 1, step: 0.01 },
|
|
4539
|
+
{ path: "dynamics.tiltSize", label: "Tilt size", min: 0, max: 1, step: 0.01 },
|
|
4540
|
+
{ path: "dynamics.velocitySize", label: "Speed size", min: -1, max: 1, step: 0.01 }
|
|
4541
|
+
];
|
|
4542
|
+
var PAPER_SLIDERS = [
|
|
4543
|
+
{ path: "depth", label: "Grain depth", min: 0, max: 1, step: 0.01 },
|
|
4544
|
+
{ path: "scale", label: "Grain scale", min: 0.25, max: 8, step: 0.05 },
|
|
4545
|
+
{ path: "shading", label: "Shading", min: 0, max: 1, step: 0.01 },
|
|
4546
|
+
{ path: "dry.contrast", label: "Contrast", min: 0, max: 3, step: 0.05 },
|
|
4547
|
+
{ path: "absorption", label: "Absorption", min: 0, max: 1, step: 0.01 },
|
|
4548
|
+
{ path: "permeability", label: "Permeability", min: 0, max: 1, step: 0.01 },
|
|
4549
|
+
{ path: "roughness", label: "Roughness", min: 0, max: 1, step: 0.01 }
|
|
4550
|
+
];
|
|
4551
|
+
function readPath(obj, path) {
|
|
4552
|
+
let v = obj;
|
|
4553
|
+
for (const k of path.split(".")) v = v?.[k];
|
|
4554
|
+
return typeof v === "number" ? v : 0;
|
|
4555
|
+
}
|
|
4556
|
+
function patchPath(obj, path, value) {
|
|
4557
|
+
const [head, ...rest] = path.split(".");
|
|
4558
|
+
if (!rest.length) return { [head]: value };
|
|
4559
|
+
return { [head]: { ...obj[head], ...patchPath(obj[head] ?? {}, rest.join("."), value) } };
|
|
4560
|
+
}
|
|
4561
|
+
function wheelPoint(angle, radius) {
|
|
4562
|
+
const a = (angle - 90) * Math.PI / 180;
|
|
4563
|
+
return { x: Math.cos(a) * radius, y: Math.sin(a) * radius };
|
|
4564
|
+
}
|
|
4565
|
+
function wheelAngle(x, y) {
|
|
4566
|
+
return (Math.atan2(y, x) * 180 / Math.PI + 90 + 360) % 360;
|
|
4567
|
+
}
|
|
4568
|
+
function renderColorWheel(size, wheel, inner = 0.64) {
|
|
4569
|
+
const data = new Uint8ClampedArray(size * size * 4);
|
|
4570
|
+
const r = size / 2;
|
|
4571
|
+
for (let y = 0; y < size; y++) {
|
|
4572
|
+
for (let x = 0; x < size; x++) {
|
|
4573
|
+
const dx = x + 0.5 - r;
|
|
4574
|
+
const dy = y + 0.5 - r;
|
|
4575
|
+
const d = Math.hypot(dx, dy) / r;
|
|
4576
|
+
if (d > 1 || d < inner) continue;
|
|
4577
|
+
const [cr, cg, cb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), 1, 1);
|
|
4578
|
+
const i = (y * size + x) * 4;
|
|
4579
|
+
const edge = Math.min(1, (1 - d) * r, (d - inner) * r);
|
|
4580
|
+
data[i] = cr;
|
|
4581
|
+
data[i + 1] = cg;
|
|
4582
|
+
data[i + 2] = cb;
|
|
4583
|
+
data[i + 3] = Math.round(255 * Math.max(0, edge));
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
return { width: size, height: size, data };
|
|
4587
|
+
}
|
|
4588
|
+
function wheelPick(dx, dy, wheel, color) {
|
|
4589
|
+
const [r, g, b] = parseHexColor(color);
|
|
4590
|
+
const { s, v } = rgbToHsv(r, g, b);
|
|
4591
|
+
const [nr, ng, nb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), s || 1, v || 1);
|
|
4592
|
+
return rgbToHexColor(nr, ng, nb);
|
|
4593
|
+
}
|
|
3235
4594
|
export {
|
|
4595
|
+
BLEND_MODES,
|
|
3236
4596
|
BRUSH_ID,
|
|
3237
4597
|
BRUSH_ROTATIONS,
|
|
4598
|
+
BRUSH_SLIDERS,
|
|
3238
4599
|
BUILTIN_BRUSHES,
|
|
3239
4600
|
DEFAULT_BRUSH_ID,
|
|
3240
4601
|
DEFAULT_DPI,
|
|
3241
4602
|
DEFAULT_DYNAMICS,
|
|
3242
4603
|
DEFAULT_PAPER,
|
|
4604
|
+
DEFAULT_SWATCHES,
|
|
3243
4605
|
DEFAULT_VIEW,
|
|
3244
4606
|
DryStroke,
|
|
3245
4607
|
GRAIN_PATTERNS,
|
|
3246
4608
|
GRAIN_TILE,
|
|
4609
|
+
HARMONY_KINDS,
|
|
3247
4610
|
IDENTITY_AFFINE,
|
|
3248
4611
|
IDENTITY_COPY,
|
|
3249
4612
|
MAX_BRUSH_SIZE,
|
|
3250
4613
|
MAX_CANVAS_SIDE,
|
|
4614
|
+
MAX_COORDINATE,
|
|
4615
|
+
MAX_IMAGE_SIDE,
|
|
3251
4616
|
MAX_ZOOM,
|
|
3252
4617
|
MIN_CANVAS_SIDE,
|
|
3253
4618
|
MIN_ZOOM,
|
|
4619
|
+
MIXING_PAD_HEIGHT,
|
|
4620
|
+
MIXING_PAD_WIDTH,
|
|
4621
|
+
MixingPad,
|
|
3254
4622
|
PAINT_BRUSH_CATEGORIES,
|
|
3255
4623
|
PAINT_BRUSH_ENGINES,
|
|
3256
4624
|
PAINT_FORMAT,
|
|
3257
4625
|
PAINT_SCHEMA_VERSION,
|
|
4626
|
+
PAINT_TOOLS,
|
|
3258
4627
|
PAPER_FAMILIES,
|
|
3259
4628
|
PAPER_LIBRARY,
|
|
3260
4629
|
PAPER_NODE_ID,
|
|
4630
|
+
PAPER_SLIDERS,
|
|
4631
|
+
PaintCanvasPresenter,
|
|
3261
4632
|
PaintDocumentError,
|
|
3262
4633
|
PaintEditorController,
|
|
3263
4634
|
PaintEditorError,
|
|
4635
|
+
PaintGesture,
|
|
3264
4636
|
PaintStrokeSession,
|
|
3265
4637
|
PaperGrain,
|
|
3266
4638
|
PenDabWalker,
|
|
3267
4639
|
PenSmoother,
|
|
4640
|
+
PerspectiveSnapper,
|
|
4641
|
+
SNAP_DECIDE_DISTANCE,
|
|
4642
|
+
SYMMETRY_MODES,
|
|
3268
4643
|
TRACING_NODE_ID,
|
|
3269
4644
|
TileableNoise,
|
|
3270
4645
|
affineCopy,
|
|
3271
4646
|
applyAffine,
|
|
3272
4647
|
brushToJSON,
|
|
3273
4648
|
clonePaper,
|
|
4649
|
+
colorHarmony,
|
|
3274
4650
|
composeAffine,
|
|
3275
4651
|
createPaintCompositor,
|
|
3276
4652
|
createPaintDocument,
|
|
3277
4653
|
createPaintEditorToolProvider,
|
|
3278
4654
|
createPaintLayer,
|
|
4655
|
+
decodeImageBlob,
|
|
4656
|
+
defaultGuides,
|
|
3279
4657
|
defaultImageEncoder,
|
|
3280
4658
|
deserializePaintDocument,
|
|
3281
4659
|
deserializePaintDocumentAsync,
|
|
3282
4660
|
documentExtensions,
|
|
3283
4661
|
documentToScreen,
|
|
4662
|
+
downloadBytes,
|
|
3284
4663
|
downsampleRgba,
|
|
3285
4664
|
fitZoom,
|
|
3286
4665
|
grainCatch,
|
|
3287
4666
|
grainHeights,
|
|
4667
|
+
guideOverlay,
|
|
4668
|
+
handlePaintShortcut,
|
|
3288
4669
|
hash2,
|
|
3289
4670
|
hexToRgb8,
|
|
4671
|
+
hslToRgb,
|
|
4672
|
+
hsvToRgb,
|
|
4673
|
+
hueToWheel,
|
|
3290
4674
|
invertAffine,
|
|
4675
|
+
isBlendMode,
|
|
3291
4676
|
isPalmTouch,
|
|
3292
4677
|
mapPenPoint,
|
|
3293
4678
|
mix32,
|
|
@@ -3299,25 +4684,39 @@ export {
|
|
|
3299
4684
|
normalizeDynamics,
|
|
3300
4685
|
normalizePaintBrush,
|
|
3301
4686
|
normalizePaper,
|
|
4687
|
+
normalizePerspective,
|
|
3302
4688
|
normalizeRotation,
|
|
4689
|
+
normalizeSymmetry,
|
|
3303
4690
|
normalizeView,
|
|
3304
4691
|
paperGrain,
|
|
3305
4692
|
paperKey,
|
|
3306
4693
|
paperNode,
|
|
3307
4694
|
paperPreset,
|
|
3308
4695
|
parseBrushLibrary,
|
|
4696
|
+
parseHexColor,
|
|
4697
|
+
patchPath,
|
|
3309
4698
|
penOrientation,
|
|
4699
|
+
perspectiveDirections,
|
|
4700
|
+
perspectiveGuideLines,
|
|
3310
4701
|
pointerToPenPoint,
|
|
3311
4702
|
rand01,
|
|
3312
4703
|
randSigned,
|
|
4704
|
+
readPath,
|
|
3313
4705
|
renderBrushThumbnail,
|
|
4706
|
+
renderColorWheel,
|
|
3314
4707
|
renderPaper,
|
|
4708
|
+
rgbToHexColor,
|
|
4709
|
+
rgbToHsl,
|
|
4710
|
+
rgbToHsv,
|
|
3315
4711
|
samplePressure,
|
|
3316
4712
|
screenToDocument,
|
|
3317
4713
|
seedFromString,
|
|
3318
4714
|
serializePaintDocument,
|
|
3319
4715
|
serializePaintDocumentExternal,
|
|
3320
4716
|
strokeBounds,
|
|
4717
|
+
symmetryAxisAngles,
|
|
4718
|
+
symmetryAxisSegments,
|
|
4719
|
+
symmetryCopies,
|
|
3321
4720
|
thumbnailStroke,
|
|
3322
4721
|
tiltToOrientation,
|
|
3323
4722
|
toStrokePoint,
|
|
@@ -3325,6 +4724,10 @@ export {
|
|
|
3325
4724
|
varyStroke,
|
|
3326
4725
|
viewCssTransform,
|
|
3327
4726
|
viewMatrix,
|
|
3328
|
-
viewPenToDocument
|
|
4727
|
+
viewPenToDocument,
|
|
4728
|
+
wheelAngle,
|
|
4729
|
+
wheelPick,
|
|
4730
|
+
wheelPoint,
|
|
4731
|
+
wheelToHue
|
|
3329
4732
|
};
|
|
3330
4733
|
//# sourceMappingURL=index.js.map
|