@pixerate/editor 0.93.0 → 0.94.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-73L6TROJ.cjs +3685 -0
- package/dist/chunk-73L6TROJ.cjs.map +1 -0
- package/dist/chunk-747CGOT2.js +3685 -0
- package/dist/chunk-747CGOT2.js.map +1 -0
- package/dist/image-editor/index.cjs +752 -292
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +83 -17
- package/dist/image-editor/index.d.ts +83 -17
- package/dist/image-editor/index.js +534 -74
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint/index.d.cts +1 -1
- package/dist/paint/index.d.ts +1 -1
- package/dist/raster/index.cjs +78 -2
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +99 -348
- package/dist/raster/index.d.ts +99 -348
- package/dist/raster/index.js +78 -2
- package/dist/{surface-fJhkmNQn.d.cts → surface-CuBg2c6k.d.cts} +1 -1
- package/dist/{surface-fJhkmNQn.d.ts → surface-CuBg2c6k.d.ts} +1 -1
- package/dist/zlib-BJ6U7LuB.d.cts +795 -0
- package/dist/zlib-BjQkpMFi.d.ts +795 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +57 -6
- package/dist/chunk-H543GD6T.cjs +0 -1696
- package/dist/chunk-H543GD6T.cjs.map +0 -1
- package/dist/chunk-P5TPUNBZ.js +0 -1696
- package/dist/chunk-P5TPUNBZ.js.map +0 -1
|
@@ -5,17 +5,25 @@ import {
|
|
|
5
5
|
createCaptureTool
|
|
6
6
|
} from "../chunk-REOJJIY3.js";
|
|
7
7
|
import {
|
|
8
|
+
DEFAULT_GLOBAL_LIGHT,
|
|
8
9
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
10
|
+
LAYER_EFFECT_KINDS,
|
|
11
|
+
LayerEffectError,
|
|
9
12
|
RasterCompositor,
|
|
10
13
|
ZlibError,
|
|
11
14
|
adler32,
|
|
12
15
|
base64ToBytes,
|
|
13
16
|
blendTile,
|
|
17
|
+
builtinPattern,
|
|
18
|
+
builtinPatternIds,
|
|
14
19
|
bytesToBase64,
|
|
15
20
|
changedTiles,
|
|
21
|
+
curveLut,
|
|
16
22
|
decodePng,
|
|
23
|
+
defaultLayerEffect,
|
|
17
24
|
deflate,
|
|
18
25
|
deflateRaw,
|
|
26
|
+
effectsMargin,
|
|
19
27
|
encodePng,
|
|
20
28
|
findLayer,
|
|
21
29
|
flattenLayers,
|
|
@@ -23,12 +31,18 @@ import {
|
|
|
23
31
|
inflate,
|
|
24
32
|
inflateRaw,
|
|
25
33
|
insertLayer,
|
|
34
|
+
isLayerEffectKind,
|
|
26
35
|
isLayerLocked,
|
|
27
36
|
isPng,
|
|
37
|
+
layerEffectLabel,
|
|
28
38
|
mapLayer,
|
|
39
|
+
mergeLayerEffect,
|
|
40
|
+
newEffectId,
|
|
41
|
+
normalizeLayerEffects,
|
|
29
42
|
parseDataUrl,
|
|
30
|
-
pngDataUrl
|
|
31
|
-
|
|
43
|
+
pngDataUrl,
|
|
44
|
+
validateLayerEffects
|
|
45
|
+
} from "../chunk-747CGOT2.js";
|
|
32
46
|
import {
|
|
33
47
|
CoverageMap,
|
|
34
48
|
DEFAULT_BRUSH,
|
|
@@ -1496,59 +1510,6 @@ function normalizeAdjustmentLayers(raw) {
|
|
|
1496
1510
|
}
|
|
1497
1511
|
return out;
|
|
1498
1512
|
}
|
|
1499
|
-
function curveLut(points, size = ADJUSTMENT_LUT_SIZE) {
|
|
1500
|
-
const pts = points.filter((p) => Number.isFinite(p.input) && Number.isFinite(p.output)).map((p) => [p.input, p.output]).sort((a, b) => a[0] - b[0]);
|
|
1501
|
-
const dedup = [];
|
|
1502
|
-
for (const p of pts) {
|
|
1503
|
-
if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;
|
|
1504
|
-
dedup.push(p);
|
|
1505
|
-
}
|
|
1506
|
-
const out = new Float32Array(size);
|
|
1507
|
-
const n = dedup.length;
|
|
1508
|
-
if (n < 2) {
|
|
1509
|
-
for (let k = 0; k < size; k++) out[k] = k / (size - 1);
|
|
1510
|
-
return out;
|
|
1511
|
-
}
|
|
1512
|
-
const x = dedup.map((p) => p[0]);
|
|
1513
|
-
const y = dedup.map((p) => p[1]);
|
|
1514
|
-
const m2 = new Float64Array(n);
|
|
1515
|
-
if (n > 2) {
|
|
1516
|
-
const c = new Float64Array(n);
|
|
1517
|
-
const d = new Float64Array(n);
|
|
1518
|
-
for (let i = 1; i < n - 1; i++) {
|
|
1519
|
-
const h0 = x[i] - x[i - 1];
|
|
1520
|
-
const h1 = x[i + 1] - x[i];
|
|
1521
|
-
const a = h0 / 6;
|
|
1522
|
-
const b = (h0 + h1) / 3;
|
|
1523
|
-
const cc = h1 / 6;
|
|
1524
|
-
const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;
|
|
1525
|
-
const denom = b - a * c[i - 1];
|
|
1526
|
-
c[i] = cc / denom;
|
|
1527
|
-
d[i] = (r - a * d[i - 1]) / denom;
|
|
1528
|
-
}
|
|
1529
|
-
for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];
|
|
1530
|
-
}
|
|
1531
|
-
let seg = 0;
|
|
1532
|
-
for (let k = 0; k < size; k++) {
|
|
1533
|
-
const xv = k / (size - 1);
|
|
1534
|
-
if (xv <= x[0]) {
|
|
1535
|
-
out[k] = clamp01(y[0]);
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
if (xv >= x[n - 1]) {
|
|
1539
|
-
out[k] = clamp01(y[n - 1]);
|
|
1540
|
-
continue;
|
|
1541
|
-
}
|
|
1542
|
-
while (seg < n - 2 && xv > x[seg + 1]) seg++;
|
|
1543
|
-
const x0 = x[seg], x1 = x[seg + 1];
|
|
1544
|
-
const h = x1 - x0;
|
|
1545
|
-
const a = (x1 - xv) / h;
|
|
1546
|
-
const b = (xv - x0) / h;
|
|
1547
|
-
const v = a * y[seg] + b * y[seg + 1] + ((a * a * a - a) * m2[seg] + (b * b * b - b) * m2[seg + 1]) * (h * h) / 6;
|
|
1548
|
-
out[k] = clamp01(v);
|
|
1549
|
-
}
|
|
1550
|
-
return out;
|
|
1551
|
-
}
|
|
1552
1513
|
function curveLutFromLevels(points) {
|
|
1553
1514
|
return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));
|
|
1554
1515
|
}
|
|
@@ -2700,7 +2661,7 @@ function developTarget(layers, activeLayerId) {
|
|
|
2700
2661
|
}
|
|
2701
2662
|
return pixels.find((l) => l.source === "document") ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;
|
|
2702
2663
|
}
|
|
2703
|
-
var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks;
|
|
2664
|
+
var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks && !l.effects;
|
|
2704
2665
|
function legacyView(stack, width, height, activeLayerId = null) {
|
|
2705
2666
|
const { layers } = stack;
|
|
2706
2667
|
const target = developTarget(layers, activeLayerId);
|
|
@@ -2740,7 +2701,7 @@ function isLegacyShape(stack) {
|
|
|
2740
2701
|
const bg = layers[0];
|
|
2741
2702
|
if (!bg || bg.type !== "pixel" || bg.source !== "document") return false;
|
|
2742
2703
|
if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;
|
|
2743
|
-
if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks) return false;
|
|
2704
|
+
if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks || bg.effects) return false;
|
|
2744
2705
|
if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;
|
|
2745
2706
|
const filters = bg.filters ?? [];
|
|
2746
2707
|
if (filters.length < 1 || filters.length > 2) return false;
|
|
@@ -2756,7 +2717,7 @@ function isLegacyShape(stack) {
|
|
|
2756
2717
|
let adjustments = 0;
|
|
2757
2718
|
for (; i < layers.length && layers[i].type === "adjustment"; i++) {
|
|
2758
2719
|
const l = layers[i];
|
|
2759
|
-
if (l.clipped || l.locks || l.mask?.source) return false;
|
|
2720
|
+
if (l.clipped || l.locks || l.mask?.source || l.effects) return false;
|
|
2760
2721
|
adjustments++;
|
|
2761
2722
|
}
|
|
2762
2723
|
for (; i < layers.length; i++) {
|
|
@@ -3262,7 +3223,9 @@ var ImageEditorRenderer = class {
|
|
|
3262
3223
|
return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
|
|
3263
3224
|
},
|
|
3264
3225
|
rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),
|
|
3265
|
-
version: `${host?.version() ?? ""}:${this.assetVersion}
|
|
3226
|
+
version: `${host?.version() ?? ""}:${this.assetVersion}`,
|
|
3227
|
+
globalLight: host?.globalLight?.(),
|
|
3228
|
+
pattern: host?.pattern ? (id) => host.pattern(id) : void 0
|
|
3266
3229
|
};
|
|
3267
3230
|
}
|
|
3268
3231
|
/** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */
|
|
@@ -4535,8 +4498,29 @@ function baseFields(layer, ctx) {
|
|
|
4535
4498
|
if (layer.clipped) out.clipped = true;
|
|
4536
4499
|
if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
|
|
4537
4500
|
if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
|
|
4501
|
+
if (layer.effects) out.effects = serializeEffects(layer.effects);
|
|
4502
|
+
return out;
|
|
4503
|
+
}
|
|
4504
|
+
function serializeEffects(effects) {
|
|
4505
|
+
const out = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };
|
|
4506
|
+
if (effects.scale !== void 0 && effects.scale !== 1) out.scale = effects.scale;
|
|
4507
|
+
if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };
|
|
4538
4508
|
return out;
|
|
4539
4509
|
}
|
|
4510
|
+
function deserializeEffects(raw) {
|
|
4511
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
4512
|
+
const r = raw;
|
|
4513
|
+
let psd;
|
|
4514
|
+
const p = r.psd;
|
|
4515
|
+
if (p && typeof p.key === "string" && typeof p.data === "string") {
|
|
4516
|
+
try {
|
|
4517
|
+
psd = { key: p.key, data: base64ToBytes(p.data) };
|
|
4518
|
+
} catch {
|
|
4519
|
+
psd = void 0;
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
return normalizeLayerEffects({ ...r, psd });
|
|
4523
|
+
}
|
|
4540
4524
|
function serializeMask(mask, hint, ctx) {
|
|
4541
4525
|
const out = { defaultValue: mask.defaultValue };
|
|
4542
4526
|
if (!mask.source) {
|
|
@@ -4739,6 +4723,8 @@ function deserializeLayer(raw, ctx) {
|
|
|
4739
4723
|
for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
|
|
4740
4724
|
if (Object.keys(locks).length) base.locks = locks;
|
|
4741
4725
|
}
|
|
4726
|
+
const effects = deserializeEffects(r.effects);
|
|
4727
|
+
if (effects && r.type !== "adjustment") base.effects = effects;
|
|
4742
4728
|
switch (r.type) {
|
|
4743
4729
|
case "pixel": {
|
|
4744
4730
|
const layer = {
|
|
@@ -5021,6 +5007,77 @@ function removeLayerFilter(layers, id, filterId) {
|
|
|
5021
5007
|
else delete next.filters;
|
|
5022
5008
|
return mapLayer(layers, id, () => next);
|
|
5023
5009
|
}
|
|
5010
|
+
function requireEffectLayer(layers, id, action) {
|
|
5011
|
+
const layer = requireLayer(layers, id).layer;
|
|
5012
|
+
if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
|
|
5013
|
+
requireUnlocked(layer, "all", action);
|
|
5014
|
+
return layer;
|
|
5015
|
+
}
|
|
5016
|
+
function layerEffectsOf(layer) {
|
|
5017
|
+
return layer.effects ?? { enabled: true, items: [] };
|
|
5018
|
+
}
|
|
5019
|
+
function guard(fn) {
|
|
5020
|
+
try {
|
|
5021
|
+
return fn();
|
|
5022
|
+
} catch (e) {
|
|
5023
|
+
if (e instanceof LayerEffectError) fail(e.message);
|
|
5024
|
+
throw e;
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
function setLayerEffects(layers, id, effects) {
|
|
5028
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5029
|
+
const next = { ...layer };
|
|
5030
|
+
if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
|
|
5031
|
+
guard(() => validateLayerEffects(effects));
|
|
5032
|
+
next.effects = effects;
|
|
5033
|
+
} else delete next.effects;
|
|
5034
|
+
return mapLayer(layers, id, () => next);
|
|
5035
|
+
}
|
|
5036
|
+
function addLayerEffect(layers, id, kind, params = {}, index) {
|
|
5037
|
+
if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
|
|
5038
|
+
const layer = requireEffectLayer(layers, id, "add effects");
|
|
5039
|
+
const current = layerEffectsOf(layer);
|
|
5040
|
+
const ids = new Set(current.items.map((e) => e.id));
|
|
5041
|
+
let effectId = newEffectId();
|
|
5042
|
+
while (ids.has(effectId)) effectId = newEffectId();
|
|
5043
|
+
const { id: _ignored, kind: _kind, ...rest } = params;
|
|
5044
|
+
const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
|
|
5045
|
+
const items = [...current.items];
|
|
5046
|
+
const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
|
|
5047
|
+
items.splice(at, 0, effect);
|
|
5048
|
+
return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
|
|
5049
|
+
}
|
|
5050
|
+
function requireEffect(layer, effectId) {
|
|
5051
|
+
const items = layer.effects?.items ?? [];
|
|
5052
|
+
const i = items.findIndex((e) => e.id === effectId);
|
|
5053
|
+
if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
|
|
5054
|
+
return i;
|
|
5055
|
+
}
|
|
5056
|
+
function updateLayerEffect(layers, id, effectId, changes) {
|
|
5057
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5058
|
+
const i = requireEffect(layer, effectId);
|
|
5059
|
+
const current = layer.effects;
|
|
5060
|
+
const items = [...current.items];
|
|
5061
|
+
items[i] = guard(() => mergeLayerEffect(items[i], changes));
|
|
5062
|
+
return setLayerEffects(layers, id, { ...current, items });
|
|
5063
|
+
}
|
|
5064
|
+
function removeLayerEffect(layers, id, effectId) {
|
|
5065
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5066
|
+
requireEffect(layer, effectId);
|
|
5067
|
+
const current = layer.effects;
|
|
5068
|
+
return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
|
|
5069
|
+
}
|
|
5070
|
+
function setLayerEffectsEnabled(layers, id, enabled, scale) {
|
|
5071
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
5072
|
+
if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
|
|
5073
|
+
const current = layerEffectsOf(layer);
|
|
5074
|
+
const next = { ...current, enabled };
|
|
5075
|
+
if (scale !== void 0) {
|
|
5076
|
+
if (scale === 1) delete next.scale;
|
|
5077
|
+
else next.scale = scale;
|
|
5078
|
+
}
|
|
5079
|
+
return setLayerEffects(layers, id, next);
|
|
5080
|
+
}
|
|
5024
5081
|
function parseBlend(value, allowPassThrough = false) {
|
|
5025
5082
|
if (value === void 0) return void 0;
|
|
5026
5083
|
if (value === "pass-through" && allowPassThrough) return value;
|
|
@@ -7305,7 +7362,8 @@ var ImageEditorController = class {
|
|
|
7305
7362
|
this.renderer.layerSources = {
|
|
7306
7363
|
surfaceOf: (layer) => this.surfaceOf(layer),
|
|
7307
7364
|
sourceIsOpaque: () => this.sourceIsOpaque(),
|
|
7308
|
-
version: () => this.sourceVersion()
|
|
7365
|
+
version: () => this.sourceVersion(),
|
|
7366
|
+
globalLight: () => this.state.globalLight
|
|
7309
7367
|
};
|
|
7310
7368
|
this.state = {
|
|
7311
7369
|
sourceUrl: typeof options.image === "string" ? options.image : null,
|
|
@@ -7459,6 +7517,7 @@ var ImageEditorController = class {
|
|
|
7459
7517
|
}
|
|
7460
7518
|
};
|
|
7461
7519
|
if (s.legacyOutputBlur !== void 0) entry.legacyOutputBlur = s.legacyOutputBlur;
|
|
7520
|
+
if (s.globalLight) entry.globalLight = { ...s.globalLight };
|
|
7462
7521
|
return entry;
|
|
7463
7522
|
}
|
|
7464
7523
|
restoreHistoryEntry(entry) {
|
|
@@ -7471,6 +7530,8 @@ var ImageEditorController = class {
|
|
|
7471
7530
|
s.outputOpacity = entry.outputOpacity;
|
|
7472
7531
|
if (entry.legacyOutputBlur !== void 0) s.legacyOutputBlur = entry.legacyOutputBlur;
|
|
7473
7532
|
else delete s.legacyOutputBlur;
|
|
7533
|
+
if (entry.globalLight) s.globalLight = { ...entry.globalLight };
|
|
7534
|
+
else delete s.globalLight;
|
|
7474
7535
|
s.selection = entry.selection;
|
|
7475
7536
|
if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
|
|
7476
7537
|
this.imageElement = entry.source.element;
|
|
@@ -7692,7 +7753,7 @@ var ImageEditorController = class {
|
|
|
7692
7753
|
}
|
|
7693
7754
|
compositorSources() {
|
|
7694
7755
|
const { width, height } = this.naturalSize;
|
|
7695
|
-
return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion() };
|
|
7756
|
+
return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion(), globalLight: this.state.globalLight };
|
|
7696
7757
|
}
|
|
7697
7758
|
sourceIds = /* @__PURE__ */ new WeakMap();
|
|
7698
7759
|
nextSourceId = 1;
|
|
@@ -8104,6 +8165,55 @@ var ImageEditorController = class {
|
|
|
8104
8165
|
this.commitLayers(next);
|
|
8105
8166
|
return findLayer(next, id).layer.filters ?? [];
|
|
8106
8167
|
}
|
|
8168
|
+
// --- Layer effects ---
|
|
8169
|
+
/** A layer's effects (an empty, enabled list when it has none). */
|
|
8170
|
+
getLayerEffects(id) {
|
|
8171
|
+
return layerEffectsOf(requireLayer(this.state.layers, id).layer);
|
|
8172
|
+
}
|
|
8173
|
+
/**
|
|
8174
|
+
* Adds an effect of `kind` with Photoshop's defaults and `params` applied (one undo step), at
|
|
8175
|
+
* `index` in the layer's effect list (default the end). Returns the new effect and the effects.
|
|
8176
|
+
*/
|
|
8177
|
+
addLayerEffect(id, kind, params = {}, index) {
|
|
8178
|
+
const res = addLayerEffect(this.state.layers, id, kind, params, index);
|
|
8179
|
+
this.commitLayers(res.layers);
|
|
8180
|
+
return { effect: res.effect, effects: this.getLayerEffects(id) };
|
|
8181
|
+
}
|
|
8182
|
+
/** Changes one effect (one undo step, or none with `commitToHistory = false` during a drag). Returns the effects. */
|
|
8183
|
+
updateLayerEffect(id, effectId, changes, commitToHistory = true) {
|
|
8184
|
+
this.commitLayers(updateLayerEffect(this.state.layers, id, effectId, changes), void 0, commitToHistory);
|
|
8185
|
+
return this.getLayerEffects(id);
|
|
8186
|
+
}
|
|
8187
|
+
/** Removes one effect (one undo step). Returns the remaining effects. */
|
|
8188
|
+
removeLayerEffect(id, effectId) {
|
|
8189
|
+
this.commitLayers(removeLayerEffect(this.state.layers, id, effectId));
|
|
8190
|
+
return this.getLayerEffects(id);
|
|
8191
|
+
}
|
|
8192
|
+
/** Turns a layer's effects on or off as a whole and, when given, sets Scale Effects (one undo step). */
|
|
8193
|
+
setLayerEffectsEnabled(id, enabled, scale, commitToHistory = true) {
|
|
8194
|
+
this.commitLayers(setLayerEffectsEnabled(this.state.layers, id, enabled, scale), void 0, commitToHistory);
|
|
8195
|
+
return this.getLayerEffects(id);
|
|
8196
|
+
}
|
|
8197
|
+
/** Replaces a layer's effects (one undo step; `null` removes them). Returns the effects. */
|
|
8198
|
+
setLayerEffects(id, effects, commitToHistory = true) {
|
|
8199
|
+
this.commitLayers(setLayerEffects(this.state.layers, id, effects), void 0, commitToHistory);
|
|
8200
|
+
return this.getLayerEffects(id);
|
|
8201
|
+
}
|
|
8202
|
+
/** The light effects with "use global light" follow. */
|
|
8203
|
+
getGlobalLight() {
|
|
8204
|
+
return { ...this.state.globalLight ?? DEFAULT_GLOBAL_LIGHT };
|
|
8205
|
+
}
|
|
8206
|
+
/** Sets the global light (one undo step). `angle` -360..360°, `altitude` 0..90°. Returns it. */
|
|
8207
|
+
setGlobalLight(light, commitToHistory = true) {
|
|
8208
|
+
const next = { ...this.getGlobalLight(), ...light };
|
|
8209
|
+
if (!(Number.isFinite(next.angle) && next.angle >= -360 && next.angle <= 360)) throw new LayerOperationError(`angle must be from -360 to 360 degrees, got ${next.angle}.`);
|
|
8210
|
+
if (!(Number.isFinite(next.altitude) && next.altitude >= 0 && next.altitude <= 90)) throw new LayerOperationError(`altitude must be from 0 to 90 degrees, got ${next.altitude}.`);
|
|
8211
|
+
if (commitToHistory) this.recordHistory();
|
|
8212
|
+
if (next.angle === DEFAULT_GLOBAL_LIGHT.angle && next.altitude === DEFAULT_GLOBAL_LIGHT.altitude) delete this.state.globalLight;
|
|
8213
|
+
else this.state.globalLight = next;
|
|
8214
|
+
this.notify();
|
|
8215
|
+
return this.getGlobalLight();
|
|
8216
|
+
}
|
|
8107
8217
|
/** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
|
|
8108
8218
|
async compositeToSurface(layers, rect) {
|
|
8109
8219
|
const { width, height } = this.naturalSize;
|
|
@@ -8133,7 +8243,8 @@ var ImageEditorController = class {
|
|
|
8133
8243
|
}
|
|
8134
8244
|
/**
|
|
8135
8245
|
* Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,
|
|
8136
|
-
* opacity and blend; masks, fill and
|
|
8246
|
+
* opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.
|
|
8247
|
+
* Returns the merged id.
|
|
8137
8248
|
*/
|
|
8138
8249
|
async mergeDown(id) {
|
|
8139
8250
|
const loc = requireLayer(this.state.layers, id);
|
|
@@ -8145,7 +8256,9 @@ var ImageEditorController = class {
|
|
|
8145
8256
|
if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
|
|
8146
8257
|
requireUnlocked(upper, "all", "merge it");
|
|
8147
8258
|
requireUnlocked(lower, "pixels", "merge into it");
|
|
8148
|
-
|
|
8259
|
+
let rect = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
8260
|
+
const reach = Math.max(effectsMargin(lower.effects), upper.type === "adjustment" ? 0 : effectsMargin(upper.effects));
|
|
8261
|
+
if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };
|
|
8149
8262
|
const base = { ...lower, blend: "normal", opacity: 1 };
|
|
8150
8263
|
delete base.clipped;
|
|
8151
8264
|
const surface = await this.compositeToSurface([base, upper], rect);
|
|
@@ -8154,6 +8267,7 @@ var ImageEditorController = class {
|
|
|
8154
8267
|
delete merged.fillOpacity;
|
|
8155
8268
|
delete merged.filters;
|
|
8156
8269
|
delete merged.source;
|
|
8270
|
+
delete merged.effects;
|
|
8157
8271
|
let next = mapLayer(this.state.layers, id, () => null);
|
|
8158
8272
|
next = mapLayer(next, lower.id, () => merged);
|
|
8159
8273
|
this.commitLayers(next, () => {
|
|
@@ -9534,6 +9648,9 @@ var ImageEditorController = class {
|
|
|
9534
9648
|
if (typeof data.legacyOutputBlur === "number" && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;
|
|
9535
9649
|
else delete s.legacyOutputBlur;
|
|
9536
9650
|
s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;
|
|
9651
|
+
const light = data.globalLight;
|
|
9652
|
+
if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };
|
|
9653
|
+
else delete s.globalLight;
|
|
9537
9654
|
if (!this.imageElement && typeof data.width === "number" && typeof data.height === "number" && data.width > 0 && data.height > 0) {
|
|
9538
9655
|
s.imageDimensions = { width: data.width, height: data.height };
|
|
9539
9656
|
}
|
|
@@ -9547,6 +9664,7 @@ var ImageEditorController = class {
|
|
|
9547
9664
|
s.crop = data.crop ? { ...data.crop } : null;
|
|
9548
9665
|
s.transform = { ...data.transform };
|
|
9549
9666
|
s.activeLayerId = null;
|
|
9667
|
+
delete s.globalLight;
|
|
9550
9668
|
}
|
|
9551
9669
|
s.activeTarget = "pixels";
|
|
9552
9670
|
this.viewKey = [];
|
|
@@ -9556,7 +9674,7 @@ var ImageEditorController = class {
|
|
|
9556
9674
|
serializeState() {
|
|
9557
9675
|
const { width, height } = this.naturalSize;
|
|
9558
9676
|
const view = legacyView(this.stack(), width, height, null);
|
|
9559
|
-
if (view.exact) {
|
|
9677
|
+
if (view.exact && !this.state.globalLight) {
|
|
9560
9678
|
return {
|
|
9561
9679
|
// States without adjustment layers keep schema 1, so they round-trip unchanged.
|
|
9562
9680
|
version: view.adjustments.layers?.length ? 2 : 1,
|
|
@@ -9583,6 +9701,7 @@ var ImageEditorController = class {
|
|
|
9583
9701
|
opacity: this.state.outputOpacity
|
|
9584
9702
|
};
|
|
9585
9703
|
if (this.state.legacyOutputBlur !== void 0) out.legacyOutputBlur = this.state.legacyOutputBlur;
|
|
9704
|
+
if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };
|
|
9586
9705
|
return out;
|
|
9587
9706
|
}
|
|
9588
9707
|
};
|
|
@@ -10820,6 +10939,7 @@ function describeLayer(layer, activeId, boundsOf) {
|
|
|
10820
10939
|
clipped: Boolean(layer.clipped),
|
|
10821
10940
|
locks: layer.locks ? { ...layer.locks } : {},
|
|
10822
10941
|
mask: maskSummary(layer.mask),
|
|
10942
|
+
effects: layer.effects ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) } : null,
|
|
10823
10943
|
active: layer.id === activeId
|
|
10824
10944
|
};
|
|
10825
10945
|
switch (layer.type) {
|
|
@@ -10879,7 +10999,7 @@ function createImageLayerTools(controller) {
|
|
|
10879
10999
|
const listLayers = {
|
|
10880
11000
|
name: "list_layers",
|
|
10881
11001
|
title: "List image layers",
|
|
10882
|
-
description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
|
|
11002
|
+
description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
|
|
10883
11003
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
10884
11004
|
annotations: { title: "List image layers", ...READ2 },
|
|
10885
11005
|
handler: () => {
|
|
@@ -11315,9 +11435,338 @@ function createImageLayerTools(controller) {
|
|
|
11315
11435
|
];
|
|
11316
11436
|
}
|
|
11317
11437
|
|
|
11318
|
-
// src/image-editor/
|
|
11438
|
+
// src/image-editor/effect-tools.ts
|
|
11439
|
+
var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11319
11440
|
var WRITE3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
11441
|
+
var WRITE_IDEMPOTENT3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11320
11442
|
var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
11443
|
+
var LAYER_ID3 = { type: "string", description: "Layer id from list_layers (a pixel, annotation or group layer)." };
|
|
11444
|
+
var EFFECT_ID = { type: "string", description: "Effect id from list_layer_effects or add_layer_effect." };
|
|
11445
|
+
var BLEND_IDS = BLEND_MODES.map((m) => m.id);
|
|
11446
|
+
var DEFS = {
|
|
11447
|
+
color: { type: "string", pattern: "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$", description: "Colour as #rrggbb." },
|
|
11448
|
+
contour: {
|
|
11449
|
+
type: "object",
|
|
11450
|
+
description: "A transfer curve over the effect coverage: points { input, output } from 0 to 1 (at least 2), sorted by input. Linear is [{0,0},{1,1}].",
|
|
11451
|
+
properties: {
|
|
11452
|
+
name: { type: "string", description: 'Display name (e.g. "Cone", "Gaussian").' },
|
|
11453
|
+
points: {
|
|
11454
|
+
type: "array",
|
|
11455
|
+
minItems: 2,
|
|
11456
|
+
description: "Curve points.",
|
|
11457
|
+
items: {
|
|
11458
|
+
type: "object",
|
|
11459
|
+
properties: {
|
|
11460
|
+
input: { type: "number", minimum: 0, maximum: 1, description: "Coverage in (0..1)." },
|
|
11461
|
+
output: { type: "number", minimum: 0, maximum: 1, description: "Coverage out (0..1)." },
|
|
11462
|
+
corner: { type: "boolean", description: "Sharp corner instead of a smooth point (default false)." }
|
|
11463
|
+
},
|
|
11464
|
+
required: ["input", "output"],
|
|
11465
|
+
additionalProperties: false
|
|
11466
|
+
}
|
|
11467
|
+
}
|
|
11468
|
+
},
|
|
11469
|
+
required: ["name", "points"],
|
|
11470
|
+
additionalProperties: false
|
|
11471
|
+
},
|
|
11472
|
+
gradient: {
|
|
11473
|
+
type: "object",
|
|
11474
|
+
description: "A gradient: colour stops, optional opacity stops and its layout.",
|
|
11475
|
+
properties: {
|
|
11476
|
+
stops: {
|
|
11477
|
+
type: "array",
|
|
11478
|
+
minItems: 1,
|
|
11479
|
+
description: "Colour stops { position 0..1, color #rrggbb }.",
|
|
11480
|
+
items: {
|
|
11481
|
+
type: "object",
|
|
11482
|
+
properties: { position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." }, color: { $ref: "#/$defs/color" } },
|
|
11483
|
+
required: ["position", "color"],
|
|
11484
|
+
additionalProperties: false
|
|
11485
|
+
}
|
|
11486
|
+
},
|
|
11487
|
+
opacityStops: {
|
|
11488
|
+
type: "array",
|
|
11489
|
+
description: "Opacity stops { position 0..1, opacity 0..1 } (default opaque).",
|
|
11490
|
+
items: {
|
|
11491
|
+
type: "object",
|
|
11492
|
+
properties: {
|
|
11493
|
+
position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." },
|
|
11494
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1." }
|
|
11495
|
+
},
|
|
11496
|
+
required: ["position", "opacity"],
|
|
11497
|
+
additionalProperties: false
|
|
11498
|
+
}
|
|
11499
|
+
},
|
|
11500
|
+
style: { type: "string", enum: ["linear", "radial", "angle", "reflected", "diamond"], description: "Geometry." },
|
|
11501
|
+
angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, 90 = bottom to top." },
|
|
11502
|
+
scale: { type: "number", minimum: 0.1, maximum: 1.5, description: "1 = 100 %." },
|
|
11503
|
+
reverse: { type: "boolean", description: "Swap the ends." },
|
|
11504
|
+
align: { type: "boolean", description: "Lay out over the layer bounds (true) or the canvas." },
|
|
11505
|
+
offset: {
|
|
11506
|
+
type: "object",
|
|
11507
|
+
description: "Centre offset as fractions of the bounds.",
|
|
11508
|
+
properties: { x: { type: "number", minimum: -1, maximum: 1, description: "Fraction of the width." }, y: { type: "number", minimum: -1, maximum: 1, description: "Fraction of the height." } },
|
|
11509
|
+
required: ["x", "y"],
|
|
11510
|
+
additionalProperties: false
|
|
11511
|
+
}
|
|
11512
|
+
},
|
|
11513
|
+
required: ["stops", "style", "angle", "scale", "reverse", "align", "offset"],
|
|
11514
|
+
additionalProperties: false
|
|
11515
|
+
},
|
|
11516
|
+
phase: {
|
|
11517
|
+
type: "object",
|
|
11518
|
+
description: "Pattern origin offset in px.",
|
|
11519
|
+
properties: { x: { type: "number", description: "px." }, y: { type: "number", description: "px." } },
|
|
11520
|
+
required: ["x", "y"],
|
|
11521
|
+
additionalProperties: false
|
|
11522
|
+
},
|
|
11523
|
+
fill: {
|
|
11524
|
+
description: "What a stroke or glow paints with. Glows take color or gradient only.",
|
|
11525
|
+
anyOf: [
|
|
11526
|
+
{
|
|
11527
|
+
type: "object",
|
|
11528
|
+
properties: { type: { const: "color", description: "Solid colour." }, color: { $ref: "#/$defs/color" } },
|
|
11529
|
+
required: ["type", "color"],
|
|
11530
|
+
additionalProperties: false
|
|
11531
|
+
},
|
|
11532
|
+
{
|
|
11533
|
+
type: "object",
|
|
11534
|
+
properties: { type: { const: "gradient", description: "Gradient." }, gradient: { $ref: "#/$defs/gradient" } },
|
|
11535
|
+
required: ["type", "gradient"],
|
|
11536
|
+
additionalProperties: false
|
|
11537
|
+
},
|
|
11538
|
+
{
|
|
11539
|
+
type: "object",
|
|
11540
|
+
properties: {
|
|
11541
|
+
type: { const: "pattern", description: "Pattern (strokes only)." },
|
|
11542
|
+
patternId: { type: "string", description: "Pattern id from list_layer_effects.patterns." },
|
|
11543
|
+
name: { type: "string", description: "Display name." },
|
|
11544
|
+
scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 %." },
|
|
11545
|
+
angle: { type: "number", minimum: -360, maximum: 360, description: "Rotation in degrees (default 0)." },
|
|
11546
|
+
link: { type: "boolean", description: "Tile from the layer (default true) or the canvas origin." },
|
|
11547
|
+
phase: { $ref: "#/$defs/phase" }
|
|
11548
|
+
},
|
|
11549
|
+
required: ["type", "patternId", "scale"],
|
|
11550
|
+
additionalProperties: false
|
|
11551
|
+
}
|
|
11552
|
+
]
|
|
11553
|
+
}
|
|
11554
|
+
};
|
|
11555
|
+
var nullable = (schema, description) => ({ anyOf: [schema, { type: "null" }], description });
|
|
11556
|
+
function settingsSchema(forUpdate) {
|
|
11557
|
+
const opt = (schema, description) => forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description };
|
|
11558
|
+
return {
|
|
11559
|
+
type: "object",
|
|
11560
|
+
description: forUpdate ? "Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect." : "Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.",
|
|
11561
|
+
properties: {
|
|
11562
|
+
enabled: { type: "boolean", description: "All kinds: false keeps the effect but hides it (default true)." },
|
|
11563
|
+
blend: { type: "string", enum: BLEND_IDS, description: "All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal)." },
|
|
11564
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1)." },
|
|
11565
|
+
color: { $ref: "#/$defs/color", description: "drop-shadow, inner-shadow, satin, color-overlay: #rrggbb." },
|
|
11566
|
+
angle: { type: "number", minimum: -360, maximum: 360, description: "Shadows, satin, bevel-emboss: light direction in degrees, counter-clockwise from 3 o'clock (default 120; satin 19)." },
|
|
11567
|
+
useGlobalLight: { type: "boolean", description: "Shadows, bevel-emboss: follow the document global light instead of angle (default true)." },
|
|
11568
|
+
distance: { type: "number", minimum: 0, maximum: 3e4, description: "Shadows, satin: offset in px (default 5; satin 11)." },
|
|
11569
|
+
spread: { type: "number", minimum: 0, maximum: 1, description: "drop-shadow, outer-glow: 0..1 of the size made solid (default 0)." },
|
|
11570
|
+
choke: { type: "number", minimum: 0, maximum: 1, description: "inner-shadow, inner-glow: 0..1 of the size made solid (default 0)." },
|
|
11571
|
+
size: { type: "number", minimum: 0, maximum: 250, description: "Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3)." },
|
|
11572
|
+
contour: opt({ $ref: "#/$defs/contour" }, "Shadows, glows, satin: coverage contour (default linear)."),
|
|
11573
|
+
antiAlias: { type: "boolean", description: "Shadows, glows, satin: smooth the contour (default false; satin true)." },
|
|
11574
|
+
noise: { type: "number", minimum: 0, maximum: 1, description: "Shadows, glows: speckle 0..1 (default 0)." },
|
|
11575
|
+
knocksOut: { type: "boolean", description: "drop-shadow: the layer hides the shadow beneath its see-through fill (default true)." },
|
|
11576
|
+
fill: { $ref: "#/$defs/fill", description: "outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern)." },
|
|
11577
|
+
technique: { type: "string", enum: ["softer", "precise", "smooth", "chisel-hard", "chisel-soft"], description: "Glows: softer | precise (default softer); bevel-emboss: smooth | chisel-hard | chisel-soft (default smooth)." },
|
|
11578
|
+
range: { type: "number", minimum: 0.01, maximum: 1, description: "Glows: the part of the glow the contour spans (default 0.5)." },
|
|
11579
|
+
jitter: { type: "number", minimum: 0, maximum: 1, description: "Glows: gradient jitter 0..1 (stored, not rendered; default 0)." },
|
|
11580
|
+
source: { type: "string", enum: ["edge", "center"], description: "inner-glow: glow from the edge (default) or the centre." },
|
|
11581
|
+
style: { type: "string", enum: ["inner-bevel", "outer-bevel", "emboss", "pillow-emboss", "stroke-emboss"], description: "bevel-emboss: style (default inner-bevel)." },
|
|
11582
|
+
depth: { type: "number", minimum: 0, maximum: 10, description: "bevel-emboss: depth, 1 = 100 % (default 1)." },
|
|
11583
|
+
direction: { type: "string", enum: ["up", "down"], description: "bevel-emboss: raised (up, default) or sunk (down)." },
|
|
11584
|
+
soften: { type: "number", minimum: 0, maximum: 16, description: "bevel-emboss: blur of the height in px (default 0)." },
|
|
11585
|
+
altitude: { type: "number", minimum: 0, maximum: 90, description: "bevel-emboss: light altitude in degrees (default 30)." },
|
|
11586
|
+
glossContour: opt({ $ref: "#/$defs/contour" }, "bevel-emboss: gloss contour over the shading (default linear)."),
|
|
11587
|
+
highlightColor: { $ref: "#/$defs/color", description: "bevel-emboss: highlight colour (default #ffffff)." },
|
|
11588
|
+
highlightBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: highlight mode (default screen)." },
|
|
11589
|
+
highlightOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: highlight opacity (default 0.75)." },
|
|
11590
|
+
shadowColor: { $ref: "#/$defs/color", description: "bevel-emboss: shadow colour (default #000000)." },
|
|
11591
|
+
shadowBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: shadow mode (default multiply)." },
|
|
11592
|
+
shadowOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: shadow opacity (default 0.75)." },
|
|
11593
|
+
contourElement: opt(
|
|
11594
|
+
{
|
|
11595
|
+
type: "object",
|
|
11596
|
+
properties: {
|
|
11597
|
+
contour: { $ref: "#/$defs/contour" },
|
|
11598
|
+
range: { type: "number", minimum: 0, maximum: 1, description: "Stored range (default 0.5)." },
|
|
11599
|
+
antiAlias: { type: "boolean", description: "Smooth the contour." }
|
|
11600
|
+
},
|
|
11601
|
+
required: ["contour", "range", "antiAlias"],
|
|
11602
|
+
additionalProperties: false
|
|
11603
|
+
},
|
|
11604
|
+
"bevel-emboss: the Contour element, shaping the bevel's height profile (default off)."
|
|
11605
|
+
),
|
|
11606
|
+
texture: opt(
|
|
11607
|
+
{
|
|
11608
|
+
type: "object",
|
|
11609
|
+
properties: {
|
|
11610
|
+
patternId: { type: "string", description: "Pattern id from list_layer_effects.patterns." },
|
|
11611
|
+
name: { type: "string", description: "Display name." },
|
|
11612
|
+
scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 %." },
|
|
11613
|
+
depth: { type: "number", minimum: -10, maximum: 10, description: "1 = +100 %; negative carves the pattern in." },
|
|
11614
|
+
invert: { type: "boolean", description: "Invert the pattern." },
|
|
11615
|
+
link: { type: "boolean", description: "Tile from the layer (default true)." },
|
|
11616
|
+
phase: { $ref: "#/$defs/phase" }
|
|
11617
|
+
},
|
|
11618
|
+
required: ["patternId", "scale", "depth", "invert"],
|
|
11619
|
+
additionalProperties: false
|
|
11620
|
+
},
|
|
11621
|
+
"bevel-emboss: the Texture element, a pattern's luminance as extra height (default off)."
|
|
11622
|
+
),
|
|
11623
|
+
invert: { type: "boolean", description: "satin: invert (default true)." },
|
|
11624
|
+
gradient: { $ref: "#/$defs/gradient", description: "gradient-overlay: the gradient (default black to white, 90\xB0)." },
|
|
11625
|
+
dither: { type: "boolean", description: "gradient-overlay: dither (stored; default false)." },
|
|
11626
|
+
patternId: { type: "string", description: "pattern-overlay: pattern id from list_layer_effects.patterns (default checker)." },
|
|
11627
|
+
name: { type: "string", description: "pattern-overlay: pattern display name." },
|
|
11628
|
+
scale: { type: "number", minimum: 0.01, maximum: 10, description: "pattern-overlay: 1 = 100 % (default 1)." },
|
|
11629
|
+
link: { type: "boolean", description: "pattern-overlay: tile from the layer (default true) or the canvas origin." },
|
|
11630
|
+
phase: { $ref: "#/$defs/phase", description: "pattern-overlay: origin offset in px (default 0, 0)." },
|
|
11631
|
+
position: { type: "string", enum: ["outside", "inside", "center"], description: "stroke: where the stroke sits on the edge (default outside)." }
|
|
11632
|
+
},
|
|
11633
|
+
additionalProperties: false
|
|
11634
|
+
};
|
|
11635
|
+
}
|
|
11636
|
+
function patternList() {
|
|
11637
|
+
return builtinPatternIds().map((id) => {
|
|
11638
|
+
const p = builtinPattern(id);
|
|
11639
|
+
return { id, name: p.name, width: p.width, height: p.height, builtin: true };
|
|
11640
|
+
});
|
|
11641
|
+
}
|
|
11642
|
+
function describeLayerEffects(effects) {
|
|
11643
|
+
return {
|
|
11644
|
+
enabled: effects.enabled,
|
|
11645
|
+
scale: effects.scale ?? 1,
|
|
11646
|
+
margin: effectsMargin(effects),
|
|
11647
|
+
fromPsd: Boolean(effects.psd),
|
|
11648
|
+
effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) }))
|
|
11649
|
+
};
|
|
11650
|
+
}
|
|
11651
|
+
function createImageEffectTools(controller) {
|
|
11652
|
+
const layerArg = (id) => {
|
|
11653
|
+
if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a layer id from list_layers).");
|
|
11654
|
+
requireLayer(controller.getLayers(), id);
|
|
11655
|
+
return id;
|
|
11656
|
+
};
|
|
11657
|
+
const result = (layerId, extra = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });
|
|
11658
|
+
const list = {
|
|
11659
|
+
name: "list_layer_effects",
|
|
11660
|
+
title: "List layer effects",
|
|
11661
|
+
description: "Returns a layer's effects: the master switch (enabled), Scale Effects (scale, 1 = 100 %), margin (px the effects reach beyond the shape), whether they came from a PSD, and every effect with its id, kind, label and all settings. Also returns the document global light and the pattern ids pattern effects can use.",
|
|
11662
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, required: ["layerId"], additionalProperties: false },
|
|
11663
|
+
annotations: { title: "List layer effects", ...READ3 },
|
|
11664
|
+
handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(), kinds: [...LAYER_EFFECT_KINDS] })
|
|
11665
|
+
};
|
|
11666
|
+
const add = {
|
|
11667
|
+
name: "add_layer_effect",
|
|
11668
|
+
title: "Add a layer effect",
|
|
11669
|
+
description: "Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
|
|
11670
|
+
inputSchema: {
|
|
11671
|
+
type: "object",
|
|
11672
|
+
$defs: DEFS,
|
|
11673
|
+
properties: {
|
|
11674
|
+
layerId: LAYER_ID3,
|
|
11675
|
+
kind: { type: "string", enum: [...LAYER_EFFECT_KINDS], description: "Effect kind." },
|
|
11676
|
+
settings: settingsSchema(false),
|
|
11677
|
+
index: { type: "integer", minimum: 0, description: "Position in the layer's effect list (default the end)." }
|
|
11678
|
+
},
|
|
11679
|
+
required: ["layerId", "kind"],
|
|
11680
|
+
additionalProperties: false
|
|
11681
|
+
},
|
|
11682
|
+
annotations: { title: "Add a layer effect", ...WRITE3 },
|
|
11683
|
+
handler: ({ layerId, kind, settings, index }) => {
|
|
11684
|
+
const id = layerArg(layerId);
|
|
11685
|
+
if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(", ")}.`);
|
|
11686
|
+
const { effect } = controller.addLayerEffect(id, kind, settings ?? {}, index);
|
|
11687
|
+
return result(id, { effectId: effect.id });
|
|
11688
|
+
}
|
|
11689
|
+
};
|
|
11690
|
+
const update = {
|
|
11691
|
+
name: "update_layer_effect",
|
|
11692
|
+
title: "Change a layer effect",
|
|
11693
|
+
description: "Changes settings of one effect (only the given ones; fill, gradient, contour and texture objects are replaced whole, null removes an optional contour or texture). The kind cannot change. Validates everything before changing anything. One undo step. Returns the layer's effects.",
|
|
11694
|
+
inputSchema: {
|
|
11695
|
+
type: "object",
|
|
11696
|
+
$defs: DEFS,
|
|
11697
|
+
properties: { layerId: LAYER_ID3, effectId: EFFECT_ID, settings: settingsSchema(true) },
|
|
11698
|
+
required: ["layerId", "effectId", "settings"],
|
|
11699
|
+
additionalProperties: false
|
|
11700
|
+
},
|
|
11701
|
+
annotations: { title: "Change a layer effect", ...WRITE_IDEMPOTENT3 },
|
|
11702
|
+
handler: ({ layerId, effectId, settings }) => {
|
|
11703
|
+
const id = layerArg(layerId);
|
|
11704
|
+
const changes = settings ?? {};
|
|
11705
|
+
if (!Object.keys(changes).length) throw new LayerOperationError("Give at least one setting to change.");
|
|
11706
|
+
controller.updateLayerEffect(id, effectId, changes);
|
|
11707
|
+
return result(id);
|
|
11708
|
+
}
|
|
11709
|
+
};
|
|
11710
|
+
const remove = {
|
|
11711
|
+
name: "remove_layer_effect",
|
|
11712
|
+
title: "Remove a layer effect",
|
|
11713
|
+
description: "Removes one effect from a layer (to hide it but keep its settings, set enabled false with update_layer_effect instead). One undo step. Returns the layer's remaining effects.",
|
|
11714
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID3, effectId: EFFECT_ID }, required: ["layerId", "effectId"], additionalProperties: false },
|
|
11715
|
+
annotations: { title: "Remove a layer effect", ...DESTRUCTIVE2 },
|
|
11716
|
+
handler: ({ layerId, effectId }) => {
|
|
11717
|
+
const id = layerArg(layerId);
|
|
11718
|
+
controller.removeLayerEffect(id, effectId);
|
|
11719
|
+
return result(id);
|
|
11720
|
+
}
|
|
11721
|
+
};
|
|
11722
|
+
const setEnabled = {
|
|
11723
|
+
name: "set_layer_effects_enabled",
|
|
11724
|
+
title: "Switch layer effects on or off",
|
|
11725
|
+
description: "Turns all of a layer's effects on or off at once (the master switch; each effect keeps its own enabled flag) and optionally sets Scale Effects, which multiplies every size, distance and pattern scale. One undo step. Returns the layer's effects.",
|
|
11726
|
+
inputSchema: {
|
|
11727
|
+
type: "object",
|
|
11728
|
+
properties: {
|
|
11729
|
+
layerId: LAYER_ID3,
|
|
11730
|
+
enabled: { type: "boolean", description: "true renders the enabled effects; false hides them all." },
|
|
11731
|
+
scale: { type: "number", minimum: 0.01, maximum: 10, description: "Scale Effects, 1 = 100 % (unchanged when left out)." }
|
|
11732
|
+
},
|
|
11733
|
+
required: ["layerId", "enabled"],
|
|
11734
|
+
additionalProperties: false
|
|
11735
|
+
},
|
|
11736
|
+
annotations: { title: "Switch layer effects on or off", ...WRITE_IDEMPOTENT3 },
|
|
11737
|
+
handler: ({ layerId, enabled, scale }) => {
|
|
11738
|
+
const id = layerArg(layerId);
|
|
11739
|
+
controller.setLayerEffectsEnabled(id, enabled, scale);
|
|
11740
|
+
return result(id);
|
|
11741
|
+
}
|
|
11742
|
+
};
|
|
11743
|
+
const light = {
|
|
11744
|
+
name: "set_global_light",
|
|
11745
|
+
title: "Set the global light",
|
|
11746
|
+
description: "Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120\xB0, altitude 30\xB0). One undo step. Returns the light.",
|
|
11747
|
+
inputSchema: {
|
|
11748
|
+
type: "object",
|
|
11749
|
+
properties: {
|
|
11750
|
+
angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out)." },
|
|
11751
|
+
altitude: { type: "number", minimum: 0, maximum: 90, description: "Degrees above the canvas, for bevels (unchanged when left out)." }
|
|
11752
|
+
},
|
|
11753
|
+
additionalProperties: false
|
|
11754
|
+
},
|
|
11755
|
+
annotations: { title: "Set the global light", ...WRITE_IDEMPOTENT3 },
|
|
11756
|
+
handler: ({ angle, altitude }) => {
|
|
11757
|
+
if (angle === void 0 && altitude === void 0) throw new LayerOperationError("Give angle, altitude or both.");
|
|
11758
|
+
const next = {};
|
|
11759
|
+
if (angle !== void 0) next.angle = angle;
|
|
11760
|
+
if (altitude !== void 0) next.altitude = altitude;
|
|
11761
|
+
return { globalLight: controller.setGlobalLight(next) };
|
|
11762
|
+
}
|
|
11763
|
+
};
|
|
11764
|
+
return [list, add, update, remove, setEnabled, light];
|
|
11765
|
+
}
|
|
11766
|
+
|
|
11767
|
+
// src/image-editor/paint-tools.ts
|
|
11768
|
+
var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
11769
|
+
var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
11321
11770
|
var TARGET = {
|
|
11322
11771
|
layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
|
|
11323
11772
|
channel: { type: "string", enum: ["pixels", "mask"], description: "pixels (default for a pixel layer) or the layer's mask: black hides, white reveals." }
|
|
@@ -11375,7 +11824,7 @@ function createImagePaintTools(controller) {
|
|
|
11375
11824
|
required: ["points"],
|
|
11376
11825
|
additionalProperties: false
|
|
11377
11826
|
},
|
|
11378
|
-
annotations: { title: "Paint a brush stroke", ...
|
|
11827
|
+
annotations: { title: "Paint a brush stroke", ...WRITE4 },
|
|
11379
11828
|
handler: (args) => controller.paintStroke({
|
|
11380
11829
|
tool: args.tool,
|
|
11381
11830
|
points: args.points,
|
|
@@ -11399,7 +11848,7 @@ function createImagePaintTools(controller) {
|
|
|
11399
11848
|
},
|
|
11400
11849
|
additionalProperties: false
|
|
11401
11850
|
},
|
|
11402
|
-
annotations: { title: "Fill pixels", ...
|
|
11851
|
+
annotations: { title: "Fill pixels", ...WRITE4 },
|
|
11403
11852
|
handler: (args) => controller.fillPixels({
|
|
11404
11853
|
color: args.color,
|
|
11405
11854
|
opacity: args.opacity,
|
|
@@ -11421,15 +11870,15 @@ function createImagePaintTools(controller) {
|
|
|
11421
11870
|
},
|
|
11422
11871
|
additionalProperties: false
|
|
11423
11872
|
},
|
|
11424
|
-
annotations: { title: "Clear pixels", ...
|
|
11873
|
+
annotations: { title: "Clear pixels", ...DESTRUCTIVE3 },
|
|
11425
11874
|
handler: (args) => controller.clearPixels({ opacity: args.opacity, useSelection: args.useSelection, ...readTarget(args) })
|
|
11426
11875
|
};
|
|
11427
11876
|
return [paintStroke, fill, clear];
|
|
11428
11877
|
}
|
|
11429
11878
|
|
|
11430
11879
|
// src/image-editor/psd-tools.ts
|
|
11431
|
-
var
|
|
11432
|
-
var
|
|
11880
|
+
var READ4 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11881
|
+
var DESTRUCTIVE4 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
11433
11882
|
var DATA = {
|
|
11434
11883
|
type: "string",
|
|
11435
11884
|
minLength: 1,
|
|
@@ -11449,7 +11898,7 @@ function createImagePsdTools(controller) {
|
|
|
11449
11898
|
title: "Inspect a PSD",
|
|
11450
11899
|
description: "Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.",
|
|
11451
11900
|
inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
|
|
11452
|
-
annotations: { title: "Inspect a PSD", ...
|
|
11901
|
+
annotations: { title: "Inspect a PSD", ...READ4 },
|
|
11453
11902
|
handler: ({ data }) => {
|
|
11454
11903
|
const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
|
|
11455
11904
|
return {
|
|
@@ -11467,7 +11916,7 @@ function createImagePsdTools(controller) {
|
|
|
11467
11916
|
title: "Open a PSD as the document",
|
|
11468
11917
|
description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility and locks map directly; type, smart-object, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
|
|
11469
11918
|
inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
|
|
11470
|
-
annotations: { title: "Open a PSD as the document", ...
|
|
11919
|
+
annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
|
|
11471
11920
|
handler: ({ data }) => {
|
|
11472
11921
|
const res = controller.loadPsd(psdBytesFromBase64(data));
|
|
11473
11922
|
const s = controller.getState();
|
|
@@ -11486,7 +11935,7 @@ function createImagePsdTools(controller) {
|
|
|
11486
11935
|
title: "Export the document as a PSD",
|
|
11487
11936
|
description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers and masks, in the source frame (crop and rotation are not applied). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
|
|
11488
11937
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11489
|
-
annotations: { title: "Export the document as a PSD", ...
|
|
11938
|
+
annotations: { title: "Export the document as a PSD", ...READ4 },
|
|
11490
11939
|
handler: async () => {
|
|
11491
11940
|
const res = await controller.toPsd();
|
|
11492
11941
|
return {
|
|
@@ -11740,6 +12189,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
11740
12189
|
...createImageSelectionTools(controller),
|
|
11741
12190
|
...createImageEditTools(controller),
|
|
11742
12191
|
...createImageLayerTools(controller),
|
|
12192
|
+
...createImageEffectTools(controller),
|
|
11743
12193
|
...createImagePaintTools(controller),
|
|
11744
12194
|
...createPigmentTools(),
|
|
11745
12195
|
...createImagePsdTools(controller)
|
|
@@ -12427,6 +12877,7 @@ export {
|
|
|
12427
12877
|
XYZ_D50_TO_SRGB,
|
|
12428
12878
|
ZlibError,
|
|
12429
12879
|
activeAdjustmentLayers,
|
|
12880
|
+
addLayerEffect,
|
|
12430
12881
|
addLayerFilter,
|
|
12431
12882
|
addToHistogram,
|
|
12432
12883
|
adjustmentKindLabel,
|
|
@@ -12490,6 +12941,7 @@ export {
|
|
|
12490
12941
|
createImageCaptureSurface,
|
|
12491
12942
|
createImageEditTools,
|
|
12492
12943
|
createImageEditorToolProvider,
|
|
12944
|
+
createImageEffectTools,
|
|
12493
12945
|
createImageLayerTools,
|
|
12494
12946
|
createImagePaintTools,
|
|
12495
12947
|
createImagePsdTools,
|
|
@@ -12516,7 +12968,9 @@ export {
|
|
|
12516
12968
|
depthMaskAlpha,
|
|
12517
12969
|
depthRasterMask,
|
|
12518
12970
|
describeLayer,
|
|
12971
|
+
describeLayerEffects,
|
|
12519
12972
|
describeSelection,
|
|
12973
|
+
deserializeEffects,
|
|
12520
12974
|
deserializeLayer,
|
|
12521
12975
|
deserializeLayers,
|
|
12522
12976
|
deserializeSelection,
|
|
@@ -12601,6 +13055,7 @@ export {
|
|
|
12601
13055
|
isSpatialFilter,
|
|
12602
13056
|
kelvinToTemperature,
|
|
12603
13057
|
layerBadge,
|
|
13058
|
+
layerEffectsOf,
|
|
12604
13059
|
layerLabel,
|
|
12605
13060
|
layerMaskCoverage,
|
|
12606
13061
|
layerMaskFromSelection,
|
|
@@ -12686,6 +13141,7 @@ export {
|
|
|
12686
13141
|
removeAlpha,
|
|
12687
13142
|
removeCurvePoint,
|
|
12688
13143
|
removeLayer,
|
|
13144
|
+
removeLayerEffect,
|
|
12689
13145
|
removeLayerFilter,
|
|
12690
13146
|
renderLayeredBase,
|
|
12691
13147
|
requireLayer,
|
|
@@ -12708,11 +13164,14 @@ export {
|
|
|
12708
13164
|
selectionToCoverage,
|
|
12709
13165
|
selectionToPixels,
|
|
12710
13166
|
selectionValueAt,
|
|
13167
|
+
serializeEffects,
|
|
12711
13168
|
serializeLayer,
|
|
12712
13169
|
serializeLayers,
|
|
12713
13170
|
serializeLayersExternal,
|
|
12714
13171
|
serializeSelection,
|
|
12715
13172
|
setControlPath,
|
|
13173
|
+
setLayerEffects,
|
|
13174
|
+
setLayerEffectsEnabled,
|
|
12716
13175
|
setLayerMask,
|
|
12717
13176
|
setLayerProperties,
|
|
12718
13177
|
setLum,
|
|
@@ -12746,6 +13205,7 @@ export {
|
|
|
12746
13205
|
uniformTile,
|
|
12747
13206
|
unionRect,
|
|
12748
13207
|
unprojectCanvasPoint,
|
|
13208
|
+
updateLayerEffect,
|
|
12749
13209
|
updateLayerFilter,
|
|
12750
13210
|
updateLayerMaskProps,
|
|
12751
13211
|
uvToXy,
|