@pixerate/editor 0.219.2 → 0.220.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/dist/{chunk-S55JFWDA.cjs → chunk-R35YOUP2.cjs} +407 -161
- package/dist/{chunk-F7Z2MKY2.js → chunk-S6KKLFZO.js} +261 -15
- package/dist/image-editor/controller.d.cts +25 -1
- package/dist/image-editor/controller.d.ts +25 -1
- package/dist/image-editor/index.cjs +1239 -1107
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +147 -15
- package/dist/image-editor/layers/hybrid-mask.d.cts +31 -0
- package/dist/image-editor/layers/hybrid-mask.d.ts +31 -0
- package/dist/image-editor/layers/model.d.cts +17 -0
- package/dist/image-editor/layers/model.d.ts +17 -0
- package/dist/image-editor/layers/serialize.d.cts +2 -0
- package/dist/image-editor/layers/serialize.d.ts +2 -0
- package/dist/image-editor/layers/vector-mask.d.cts +10 -5
- package/dist/image-editor/layers/vector-mask.d.ts +10 -5
- package/dist/image-editor/path-edit.d.cts +4 -0
- package/dist/image-editor/path-edit.d.ts +4 -0
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ export * from './smart-tools.cjs';
|
|
|
38
38
|
export * from './layers/shape.cjs';
|
|
39
39
|
export * from './layers/vector-mask.cjs';
|
|
40
40
|
export * from './layers/edge-color.cjs';
|
|
41
|
+
export * from './layers/hybrid-mask.cjs';
|
|
41
42
|
export * from './shape-tools.cjs';
|
|
42
43
|
export * from './layers/text.cjs';
|
|
43
44
|
export * from './layers/text-shapers.cjs';
|
|
@@ -38,6 +38,7 @@ export * from './smart-tools.js';
|
|
|
38
38
|
export * from './layers/shape.js';
|
|
39
39
|
export * from './layers/vector-mask.js';
|
|
40
40
|
export * from './layers/edge-color.js';
|
|
41
|
+
export * from './layers/hybrid-mask.js';
|
|
41
42
|
export * from './shape-tools.js';
|
|
42
43
|
export * from './layers/text.js';
|
|
43
44
|
export * from './layers/text-shapers.js';
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
TINT_SCALE,
|
|
92
92
|
TONE_LUT_SIZE,
|
|
93
93
|
TRANSFORM_ANCHORS,
|
|
94
|
+
VECTOR_MASK_EDGE_DEFAULTS,
|
|
94
95
|
VIBRANCE_GREY,
|
|
95
96
|
XYZ_D50_TO_SRGB,
|
|
96
97
|
activeAdjustmentLayers,
|
|
@@ -201,6 +202,7 @@ import {
|
|
|
201
202
|
hueRangeTables,
|
|
202
203
|
hueRangeWeight,
|
|
203
204
|
hueSaturation,
|
|
205
|
+
hybridVectorMaskCoverage,
|
|
204
206
|
identityCurve,
|
|
205
207
|
identityLevels,
|
|
206
208
|
identityTransform,
|
|
@@ -224,6 +226,7 @@ import {
|
|
|
224
226
|
isValidWarp,
|
|
225
227
|
kelvinToTemperature,
|
|
226
228
|
layerEffectsOf,
|
|
229
|
+
layerGuide,
|
|
227
230
|
layerMaskCoverage,
|
|
228
231
|
layerMaskFromSelection,
|
|
229
232
|
layerMaskKey,
|
|
@@ -383,8 +386,10 @@ import {
|
|
|
383
386
|
updateTypeLayer,
|
|
384
387
|
userErrorMessage,
|
|
385
388
|
uvToXy,
|
|
389
|
+
validVectorMaskEdges,
|
|
386
390
|
vectorMaskBounds,
|
|
387
391
|
vectorMaskCoverage,
|
|
392
|
+
vectorMaskSegments,
|
|
388
393
|
vectorMaskThumbnail,
|
|
389
394
|
versionedDescriptorBytes,
|
|
390
395
|
vibranceLinear,
|
|
@@ -400,7 +405,7 @@ import {
|
|
|
400
405
|
writePsd,
|
|
401
406
|
xyToTempTint,
|
|
402
407
|
xyToUv
|
|
403
|
-
} from "../chunk-
|
|
408
|
+
} from "../chunk-S6KKLFZO.js";
|
|
404
409
|
import {
|
|
405
410
|
parseCssColor
|
|
406
411
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1822,10 +1827,22 @@ var ImageEditorRenderer = class {
|
|
|
1822
1827
|
else ctx.lineTo(b.x, b.y);
|
|
1823
1828
|
};
|
|
1824
1829
|
ctx.beginPath();
|
|
1825
|
-
e.curves.forEach(trace2);
|
|
1830
|
+
e.curves.forEach((c, i) => (e.curveModes?.[i] ?? "hard") === "hard" && trace2(c));
|
|
1826
1831
|
ctx.lineWidth = 1.5 * u;
|
|
1827
1832
|
ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
|
|
1828
1833
|
ctx.stroke();
|
|
1834
|
+
for (const [mode, color, width, dash] of [["feather", "#f59e0b", 2, [5, 3]], ["matte", "#d946ef", 3, []]]) {
|
|
1835
|
+
if (!e.curveModes?.includes(mode)) continue;
|
|
1836
|
+
ctx.beginPath();
|
|
1837
|
+
e.curves.forEach((c, i) => e.curveModes[i] === mode && trace2(c));
|
|
1838
|
+
ctx.lineWidth = width * u;
|
|
1839
|
+
ctx.strokeStyle = color;
|
|
1840
|
+
ctx.setLineDash(dash.map((d) => d * u));
|
|
1841
|
+
ctx.stroke();
|
|
1842
|
+
ctx.setLineDash([]);
|
|
1843
|
+
}
|
|
1844
|
+
ctx.lineWidth = 1.5 * u;
|
|
1845
|
+
ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
|
|
1829
1846
|
if (e.preview) {
|
|
1830
1847
|
ctx.beginPath();
|
|
1831
1848
|
trace2(e.preview);
|
|
@@ -6931,6 +6948,8 @@ var ImageEditorController = class {
|
|
|
6931
6948
|
return String(id);
|
|
6932
6949
|
}
|
|
6933
6950
|
/** The pixels a pixel layer, smart object or shape composites (the decoded source for the Background). */
|
|
6951
|
+
/** A layer's real pixels for hybrid vector masks' matte guide (the Background's from the source). */
|
|
6952
|
+
guideSurfaceOf = (layer) => layer.type === "pixel" || layer.type === "smart" || layer.type === "shape" || layer.type === "type" ? this.surfaceOf(layer) : void 0;
|
|
6934
6953
|
surfaceOf(layer) {
|
|
6935
6954
|
if (layer.type !== "pixel" || layer.source !== "document") return layer.surface;
|
|
6936
6955
|
return this.sourceSurface() ?? layer.surface;
|
|
@@ -7149,7 +7168,7 @@ var ImageEditorController = class {
|
|
|
7149
7168
|
/** Whether the layer's mask (painted and vector, with feather) has a soft band. */
|
|
7150
7169
|
maskHasSoftBand(layer) {
|
|
7151
7170
|
const { width, height } = this.naturalSize;
|
|
7152
|
-
const mask = effectiveLayerMask(layer, width, height);
|
|
7171
|
+
const mask = effectiveLayerMask(layer, width, height, this.guideSurfaceOf);
|
|
7153
7172
|
if (!mask || mask.enabled === false || mask.source) return false;
|
|
7154
7173
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
7155
7174
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
@@ -7523,7 +7542,11 @@ var ImageEditorController = class {
|
|
|
7523
7542
|
this.commitLayers(layers, void 0, commitToHistory);
|
|
7524
7543
|
} else {
|
|
7525
7544
|
const { vm } = this.requireVectorMask(target.layerId);
|
|
7526
|
-
|
|
7545
|
+
const edges = validVectorMaskEdges(vm.edges, next);
|
|
7546
|
+
const nextVm = { ...vm, components: next };
|
|
7547
|
+
if (edges.length) nextVm.edges = edges;
|
|
7548
|
+
else delete nextVm.edges;
|
|
7549
|
+
this.commitLayers(mapLayer(this.state.layers, target.layerId, (l) => ({ ...l, vectorMask: nextVm })), void 0, commitToHistory);
|
|
7527
7550
|
}
|
|
7528
7551
|
}
|
|
7529
7552
|
/**
|
|
@@ -7618,6 +7641,9 @@ var ImageEditorController = class {
|
|
|
7618
7641
|
setVectorMaskPath(id, path) {
|
|
7619
7642
|
const { vm } = this.requireVectorMask(id);
|
|
7620
7643
|
const next = { ...vm, components: this.vectorMaskComponents(path) };
|
|
7644
|
+
const kept = validVectorMaskEdges(vm.edges, next.components);
|
|
7645
|
+
if (kept.length) next.edges = kept;
|
|
7646
|
+
else delete next.edges;
|
|
7621
7647
|
if (path.fillRule === "evenodd") next.fillRule = "evenodd";
|
|
7622
7648
|
else if (path.fillRule === "nonzero") delete next.fillRule;
|
|
7623
7649
|
this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
|
|
@@ -7667,7 +7693,7 @@ var ImageEditorController = class {
|
|
|
7667
7693
|
const { width, height } = this.canvasSize;
|
|
7668
7694
|
const raster = layer.mask;
|
|
7669
7695
|
const plain3 = { ...layer, vectorMask: { ...layer.vectorMask, enabled: true }, mask: raster ? { surface: raster.surface, defaultValue: raster.defaultValue } : void 0 };
|
|
7670
|
-
const baked = effectiveLayerMask(plain3, width, height);
|
|
7696
|
+
const baked = effectiveLayerMask(plain3, width, height, this.guideSurfaceOf);
|
|
7671
7697
|
const mask = raster ? { ...raster, surface: baked.surface, defaultValue: baked.defaultValue } : baked;
|
|
7672
7698
|
this.commitLayers(
|
|
7673
7699
|
mapLayer(this.state.layers, id, (l) => {
|
|
@@ -7703,8 +7729,55 @@ var ImageEditorController = class {
|
|
|
7703
7729
|
density: vm.density ?? 1,
|
|
7704
7730
|
feather: vm.feather ?? 0,
|
|
7705
7731
|
bounds: b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null,
|
|
7706
|
-
components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) }))
|
|
7732
|
+
components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) })),
|
|
7733
|
+
edges: validVectorMaskEdges(vm.edges, vm.components).map((e) => ({ ...e })),
|
|
7734
|
+
segmentCount: vectorMaskSegments(vm.components).length
|
|
7735
|
+
};
|
|
7736
|
+
}
|
|
7737
|
+
/**
|
|
7738
|
+
* Hybrid mask: a vector mask segment's edge mode. `hard` (the default) is the path's own sharp
|
|
7739
|
+
* edge; `feather` blurs across it by `radius` px (default 4); `matte` solves a corridor from
|
|
7740
|
+
* `inner` px inside to `outer` px outside (defaults 4 and 8) from the layer's colours, for hair
|
|
7741
|
+
* and fur. Modes blend where they meet. One undo step. Returns the vector mask's info.
|
|
7742
|
+
*/
|
|
7743
|
+
setVectorMaskEdge(id, at, mode, options = {}, commitToHistory = true) {
|
|
7744
|
+
const { layer, vm } = this.requireVectorMask(id);
|
|
7745
|
+
const name = layer.name || id;
|
|
7746
|
+
const sp = vm.components[at.component]?.subpaths[at.subpath];
|
|
7747
|
+
const count = sp ? subpathSegmentCount(sp) : 0;
|
|
7748
|
+
if (!sp || !Number.isInteger(at.segment) || at.segment < 0 || at.segment >= count) {
|
|
7749
|
+
const where = vm.components.map((c, ci) => c.subpaths.map((p, si) => `component ${ci} subpath ${si}: segments 0..${subpathSegmentCount(p) - 1}`).join("; ")).join("; ");
|
|
7750
|
+
throw new LayerOperationError(`The vector mask of "${name}" has no segment ${at.component}/${at.subpath}/${at.segment}; it has ${where || "no segments"} (see get_vector_mask).`);
|
|
7751
|
+
}
|
|
7752
|
+
if (mode !== "hard" && mode !== "feather" && mode !== "matte") throw new LayerOperationError(`mode must be hard, feather or matte, got "${String(mode)}".`);
|
|
7753
|
+
const num3 = (v, what, lo, hi) => {
|
|
7754
|
+
if (v === void 0) return void 0;
|
|
7755
|
+
if (!(typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi)) throw new LayerOperationError(`${what} must be ${lo} to ${hi} px, got ${String(v)}.`);
|
|
7756
|
+
return v;
|
|
7707
7757
|
};
|
|
7758
|
+
const radius = num3(options.radius, "radius", 0.5, 250);
|
|
7759
|
+
const inner = num3(options.inner, "inner", 0.5, 200);
|
|
7760
|
+
const outer = num3(options.outer, "outer", 0.5, 200);
|
|
7761
|
+
if (mode !== "feather" && radius !== void 0) throw new LayerOperationError("radius is for feather edges; matte edges take inner and outer.");
|
|
7762
|
+
if (mode !== "matte" && (inner !== void 0 || outer !== void 0)) throw new LayerOperationError("inner and outer are for matte edges; feather edges take radius.");
|
|
7763
|
+
const same = (e) => e.component === at.component && e.subpath === at.subpath && e.segment === at.segment;
|
|
7764
|
+
const edges = validVectorMaskEdges(vm.edges, vm.components).filter((e) => !same(e));
|
|
7765
|
+
if (mode === "feather") edges.push({ ...at, mode, radius: radius ?? VECTOR_MASK_EDGE_DEFAULTS.radius });
|
|
7766
|
+
else if (mode === "matte") edges.push({ ...at, mode, inner: inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner, outer: outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer });
|
|
7767
|
+
const next = { ...vm };
|
|
7768
|
+
if (edges.length) next.edges = edges;
|
|
7769
|
+
else delete next.edges;
|
|
7770
|
+
this.commitLayers(mapLayer(this.state.layers, layer.id, (l) => ({ ...l, vectorMask: next })), void 0, commitToHistory);
|
|
7771
|
+
return this.getVectorMask(layer.id);
|
|
7772
|
+
}
|
|
7773
|
+
/** A layer's vector mask over the canvas as a selection mask, hybrid edges, feather and density included. */
|
|
7774
|
+
vectorMaskSelection(layer) {
|
|
7775
|
+
const { width, height } = this.naturalSize;
|
|
7776
|
+
const vm = layer.vectorMask;
|
|
7777
|
+
const v = vectorMaskCoverage(vm, width, height, vm.edges?.length ? layerGuide(layer, width, height, this.guideSurfaceOf) : null);
|
|
7778
|
+
const bytes = new Uint8Array(v.length);
|
|
7779
|
+
for (let i = 0; i < v.length; i++) bytes[i] = Math.round(v[i] * 255);
|
|
7780
|
+
return createSelectionMask(width, height, bytes);
|
|
7708
7781
|
}
|
|
7709
7782
|
/** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
|
|
7710
7783
|
setLayerMaskProps(id, props, commitToHistory = true) {
|
|
@@ -10546,9 +10619,8 @@ var ImageEditorController = class {
|
|
|
10546
10619
|
mask = this.layerMaskCoverage(layer.id);
|
|
10547
10620
|
if (!mask) throw new LayerOperationError(`Layer "${layer.name || layer.id}" has no painted mask; add one with add_layer_mask, or inspect its vector-mask.`);
|
|
10548
10621
|
} else {
|
|
10549
|
-
|
|
10550
|
-
mask =
|
|
10551
|
-
if (vm.inverted) mask = invertSelection(mask);
|
|
10622
|
+
this.requireVectorMask(layer.id);
|
|
10623
|
+
mask = this.vectorMaskSelection(layer);
|
|
10552
10624
|
}
|
|
10553
10625
|
if (!mask.bounds) throw new LayerOperationError(`The ${target} of "${layer.name || layer.id}" hides everything: there is no edge to inspect.`);
|
|
10554
10626
|
} else {
|
|
@@ -10622,9 +10694,8 @@ var ImageEditorController = class {
|
|
|
10622
10694
|
case "vector-mask": {
|
|
10623
10695
|
const layer = this.getLayer(source.layerId);
|
|
10624
10696
|
if (!layer) throw new LayerOperationError(`There is no layer "${source.layerId}" (see list_layers).`);
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
return vm.inverted ? invertSelection(mask) : mask;
|
|
10697
|
+
this.requireVectorMask(layer.id);
|
|
10698
|
+
return this.vectorMaskSelection(layer);
|
|
10628
10699
|
}
|
|
10629
10700
|
default:
|
|
10630
10701
|
throw new LayerOperationError(`Unknown selection source ${JSON.stringify(source)}. Use selection, history, channel, layer-mask or vector-mask.`);
|
|
@@ -11905,6 +11976,12 @@ var ImageEditorController = class {
|
|
|
11905
11976
|
if (layer.edge?.ambient) add2("The live edge cast (set_edge_ambient) has no PSD equivalent and was left out", layer.name);
|
|
11906
11977
|
const p = await prepareLayer(layer);
|
|
11907
11978
|
if (p?.edge) delete p.edge;
|
|
11979
|
+
if (p && isActiveVectorMask(layer.vectorMask) && layer.vectorMask.edges?.length) {
|
|
11980
|
+
add2("Hybrid vector masks (soft edge segments) were baked into pixel masks", layer.name);
|
|
11981
|
+
const baked = effectiveLayerMask(layer, width, height, this.guideSurfaceOf);
|
|
11982
|
+
if (baked) p.mask = baked;
|
|
11983
|
+
delete p.vectorMask;
|
|
11984
|
+
}
|
|
11908
11985
|
if (p?.blend === "pigment") add2("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
|
|
11909
11986
|
return p;
|
|
11910
11987
|
};
|
|
@@ -11980,7 +12057,7 @@ var ImageEditorController = class {
|
|
|
11980
12057
|
}
|
|
11981
12058
|
if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
|
|
11982
12059
|
delete out.filters;
|
|
11983
|
-
const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height) : void 0;
|
|
12060
|
+
const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height, this.guideSurfaceOf) : void 0;
|
|
11984
12061
|
if (unmixMask && !unmixMask.source && unmixMask.enabled !== false) {
|
|
11985
12062
|
add2("Edge decontamination and despill were baked into the pixels", layer.name);
|
|
11986
12063
|
const { x: ox, y: oy } = layer.offset;
|
|
@@ -18645,6 +18722,37 @@ function createImageShapeTools(controller) {
|
|
|
18645
18722
|
return { vectorMask: controller.getVectorMask(id) };
|
|
18646
18723
|
}
|
|
18647
18724
|
};
|
|
18725
|
+
const setVectorMaskEdge = {
|
|
18726
|
+
name: "set_vector_mask_edge",
|
|
18727
|
+
title: "Set a vector mask segment's edge",
|
|
18728
|
+
description: "Hybrid mask: gives one segment of a layer's vector mask its own edge, so one mask holds sharp edges and hair or fur. hard (default) is the path's sharp edge; feather blurs across it by radius px; matte solves a corridor from inner px inside the path to outer px outside it from the layer's colours (the colour-guided filter), so strands keep their detail. Modes blend where they meet. Address the segment with component, subpath and segment (the segment from anchor segment to the next; see get_vector_mask segmentCount and components). Check the result with inspect_mask_edge target vector-mask. One undo step. Returns the vector mask with its edges.",
|
|
18729
|
+
inputSchema: {
|
|
18730
|
+
type: "object",
|
|
18731
|
+
properties: {
|
|
18732
|
+
layerId: LAYER,
|
|
18733
|
+
component: { type: "integer", minimum: 0, description: "Component index of the path (0 for a single shape)." },
|
|
18734
|
+
subpath: { type: "integer", minimum: 0, description: "Subpath index in the component (0 for a single outline)." },
|
|
18735
|
+
segment: { type: "integer", minimum: 0, description: "Segment index in the subpath: from anchor segment to the next (the last closes back to anchor 0)." },
|
|
18736
|
+
mode: { type: "string", enum: ["hard", "feather", "matte"], description: "hard (sharp, the default), feather (a soft falloff) or matte (a solved corridor for hair and fur)." },
|
|
18737
|
+
radius: { type: "number", minimum: 0.5, maximum: 250, description: "feather only: Gaussian falloff in px across the curve (default 4)." },
|
|
18738
|
+
inner: { type: "number", minimum: 0.5, maximum: 200, description: "matte only: how far inside the path the unsure corridor reaches, in px (default 4)." },
|
|
18739
|
+
outer: { type: "number", minimum: 0.5, maximum: 200, description: "matte only: how far outside the path the unsure corridor reaches, in px (default 8; cover the longest strands)." }
|
|
18740
|
+
},
|
|
18741
|
+
required: ["layerId", "component", "subpath", "segment", "mode"],
|
|
18742
|
+
additionalProperties: false
|
|
18743
|
+
},
|
|
18744
|
+
annotations: { title: "Set a vector mask segment's edge", ...WRITE_IDEMPOTENT5 },
|
|
18745
|
+
handler: ({ layerId, component, subpath, segment, mode, radius, inner, outer }) => {
|
|
18746
|
+
const id = layerArg(layerId);
|
|
18747
|
+
const vectorMask = controller.setVectorMaskEdge(
|
|
18748
|
+
id,
|
|
18749
|
+
{ component, subpath, segment },
|
|
18750
|
+
mode,
|
|
18751
|
+
{ radius, inner, outer }
|
|
18752
|
+
);
|
|
18753
|
+
return { vectorMask };
|
|
18754
|
+
}
|
|
18755
|
+
};
|
|
18648
18756
|
const rasterizeVectorMask = {
|
|
18649
18757
|
name: "rasterize_vector_mask",
|
|
18650
18758
|
title: "Rasterize a vector mask",
|
|
@@ -18787,6 +18895,7 @@ function createImageShapeTools(controller) {
|
|
|
18787
18895
|
addVectorMask,
|
|
18788
18896
|
setVectorMaskPath,
|
|
18789
18897
|
setVectorMaskProps,
|
|
18898
|
+
setVectorMaskEdge,
|
|
18790
18899
|
rasterizeVectorMask,
|
|
18791
18900
|
removeVectorMask,
|
|
18792
18901
|
selectionToPath,
|
|
@@ -21649,6 +21758,7 @@ var PathEditGesture = class {
|
|
|
21649
21758
|
return;
|
|
21650
21759
|
}
|
|
21651
21760
|
const seg = this.segmentAt(cs, p);
|
|
21761
|
+
if (seg && mods.alt && mods.shift && this.target?.kind === "vector-mask") return this.cycleEdge(this.target.layerId, seg.ref);
|
|
21652
21762
|
if (seg) {
|
|
21653
21763
|
if (!mods.shift) this.selected.clear();
|
|
21654
21764
|
this.drag = { kind: "segment", ...seg, base: cloneComponents(cs), moved: false };
|
|
@@ -21657,6 +21767,17 @@ var PathEditGesture = class {
|
|
|
21657
21767
|
this.drag = { kind: "marquee", start: pt(p), end: pt(p), keep: mods.shift ? new Set(this.selected) : /* @__PURE__ */ new Set() };
|
|
21658
21768
|
if (!mods.shift) this.selected.clear();
|
|
21659
21769
|
}
|
|
21770
|
+
/** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
|
|
21771
|
+
cycleEdge(layerId, r) {
|
|
21772
|
+
const at = { component: r.component, subpath: r.subpath, segment: r.anchor };
|
|
21773
|
+
const now = this.controller.getVectorMask(layerId)?.edges.find((e) => e.component === at.component && e.subpath === at.subpath && e.segment === at.segment)?.mode ?? "hard";
|
|
21774
|
+
const next = now === "hard" ? "feather" : now === "feather" ? "matte" : "hard";
|
|
21775
|
+
try {
|
|
21776
|
+
this.controller.setVectorMaskEdge(layerId, at, next);
|
|
21777
|
+
} catch (e) {
|
|
21778
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
21779
|
+
}
|
|
21780
|
+
}
|
|
21660
21781
|
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
21661
21782
|
convert(cs, r) {
|
|
21662
21783
|
const next = cloneComponents(cs);
|
|
@@ -21852,12 +21973,18 @@ var PathEditGesture = class {
|
|
|
21852
21973
|
const cs = this.components();
|
|
21853
21974
|
if (!cs) return null;
|
|
21854
21975
|
const curves = [];
|
|
21976
|
+
const curveModes = [];
|
|
21977
|
+
const edges = this.target?.kind === "vector-mask" ? this.controller.getVectorMask(this.target.layerId)?.edges ?? [] : [];
|
|
21978
|
+
const modeOf = (ci, si, k) => edges.find((e) => e.component === ci && e.subpath === si && e.segment === k)?.mode ?? "hard";
|
|
21855
21979
|
const anchors = [];
|
|
21856
21980
|
const handles = [];
|
|
21857
21981
|
const selectedComponents = new Set(this.selection.map((r) => r.component));
|
|
21858
21982
|
cs.forEach(
|
|
21859
21983
|
(c, ci) => c.subpaths.forEach((sp, si) => {
|
|
21860
|
-
for (let i = 0; i < subpathSegmentCount(sp); i++)
|
|
21984
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) {
|
|
21985
|
+
curves.push(subpathSegment(sp, i).map(([x, y]) => ({ x, y })));
|
|
21986
|
+
curveModes.push(modeOf(ci, si, i));
|
|
21987
|
+
}
|
|
21861
21988
|
sp.anchors.forEach((a, ai) => {
|
|
21862
21989
|
const selected = this.selected.has(key({ component: ci, subpath: si, anchor: ai }));
|
|
21863
21990
|
if (tool === "path-select" && !selectedComponents.has(ci)) return;
|
|
@@ -21875,7 +22002,7 @@ var PathEditGesture = class {
|
|
|
21875
22002
|
if (last) preview = last.out ? [pt(last), pt(last.out), this.pointer, this.pointer] : [pt(last), this.pointer];
|
|
21876
22003
|
}
|
|
21877
22004
|
const marquee = this.drag?.kind === "marquee" ? rect(this.drag.start, this.drag.end) : null;
|
|
21878
|
-
return { curves, anchors, handles, marquee, preview };
|
|
22005
|
+
return { curves, ...edges.length ? { curveModes } : {}, anchors, handles, marquee, preview };
|
|
21879
22006
|
}
|
|
21880
22007
|
};
|
|
21881
22008
|
function rect(a, b) {
|
|
@@ -22453,6 +22580,7 @@ export {
|
|
|
22453
22580
|
TypeToolGesture,
|
|
22454
22581
|
UNITS,
|
|
22455
22582
|
U_MAX,
|
|
22583
|
+
VECTOR_MASK_EDGE_DEFAULTS,
|
|
22456
22584
|
VIBRANCE_GREY,
|
|
22457
22585
|
W9,
|
|
22458
22586
|
WARP_GRID_OPTIONS,
|
|
@@ -22801,6 +22929,7 @@ export {
|
|
|
22801
22929
|
hueRangeTables,
|
|
22802
22930
|
hueRangeWeight,
|
|
22803
22931
|
hueSaturation,
|
|
22932
|
+
hybridVectorMaskCoverage,
|
|
22804
22933
|
identityCurve,
|
|
22805
22934
|
identityLevels,
|
|
22806
22935
|
identityTransform,
|
|
@@ -22871,6 +23000,7 @@ export {
|
|
|
22871
23000
|
layerEffectsBadge,
|
|
22872
23001
|
layerEffectsOf,
|
|
22873
23002
|
layerEffectsPanel,
|
|
23003
|
+
layerGuide,
|
|
22874
23004
|
layerHasThumbnail,
|
|
22875
23005
|
layerLabel,
|
|
22876
23006
|
layerMaskCoverage,
|
|
@@ -23276,11 +23406,13 @@ export {
|
|
|
23276
23406
|
upscaleRegion,
|
|
23277
23407
|
userErrorMessage,
|
|
23278
23408
|
uvToXy,
|
|
23409
|
+
validVectorMaskEdges,
|
|
23279
23410
|
validateEffectGradient,
|
|
23280
23411
|
validateLayerEffect,
|
|
23281
23412
|
validateLayerEffects,
|
|
23282
23413
|
vectorMaskBounds,
|
|
23283
23414
|
vectorMaskCoverage,
|
|
23415
|
+
vectorMaskSegments,
|
|
23284
23416
|
vectorMaskThumbnail,
|
|
23285
23417
|
vectorMaskThumbnailClick,
|
|
23286
23418
|
versionedDescriptorBytes,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid vector masks: a vector mask whose segments each have an edge mode. Hard segments are
|
|
3
|
+
* the path's own anti-aliased edge; feather segments blur across the curve; matte segments solve
|
|
4
|
+
* a corridor between two rails (inside by `inner`, outside by `outer`) from the layer's pixels
|
|
5
|
+
* with the colour-guided filter, so hair and fur keep their detail while the rest stays sharp.
|
|
6
|
+
* Where modes meet, they blend over a few pixels so there is no seam. Rendered on demand from
|
|
7
|
+
* the path and the pixels, and cached with the layer like any vector mask.
|
|
8
|
+
*/
|
|
9
|
+
import type { ShapeComponent, VectorMask, VectorMaskEdge } from './model.cjs';
|
|
10
|
+
export type VectorMaskEdgeMode = 'hard' | 'feather' | 'matte';
|
|
11
|
+
/** Defaults for a segment switched to a soft mode. */
|
|
12
|
+
export declare const VECTOR_MASK_EDGE_DEFAULTS: {
|
|
13
|
+
readonly radius: 4;
|
|
14
|
+
readonly inner: 4;
|
|
15
|
+
readonly outer: 8;
|
|
16
|
+
};
|
|
17
|
+
/** The segments of a vector mask's path as `{ component, subpath, segment }` triples. */
|
|
18
|
+
export declare function vectorMaskSegments(components: readonly ShapeComponent[]): Array<{
|
|
19
|
+
component: number;
|
|
20
|
+
subpath: number;
|
|
21
|
+
segment: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** Edges whose segment still exists in `components` (after the path was edited). */
|
|
24
|
+
export declare function validVectorMaskEdges(edges: readonly VectorMaskEdge[] | undefined, components: readonly ShapeComponent[]): VectorMaskEdge[];
|
|
25
|
+
/**
|
|
26
|
+
* The hybrid coverage (0..1, `width × height`) from the path's hard coverage `hard` (as
|
|
27
|
+
* `componentsCoverage` gives it, before the mask's own feather and density) and the soft
|
|
28
|
+
* `edges`. `guide` is the layer's RGBA over the canvas, for matte corridors; without one a matte
|
|
29
|
+
* segment is feathered instead. Returns `hard` itself when no edge is soft.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hybridVectorMaskCoverage(vm: Pick<VectorMask, 'components' | 'edges'>, hard: Float32Array, width: number, height: number, guide: ArrayLike<number> | null): Float32Array;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid vector masks: a vector mask whose segments each have an edge mode. Hard segments are
|
|
3
|
+
* the path's own anti-aliased edge; feather segments blur across the curve; matte segments solve
|
|
4
|
+
* a corridor between two rails (inside by `inner`, outside by `outer`) from the layer's pixels
|
|
5
|
+
* with the colour-guided filter, so hair and fur keep their detail while the rest stays sharp.
|
|
6
|
+
* Where modes meet, they blend over a few pixels so there is no seam. Rendered on demand from
|
|
7
|
+
* the path and the pixels, and cached with the layer like any vector mask.
|
|
8
|
+
*/
|
|
9
|
+
import type { ShapeComponent, VectorMask, VectorMaskEdge } from './model.js';
|
|
10
|
+
export type VectorMaskEdgeMode = 'hard' | 'feather' | 'matte';
|
|
11
|
+
/** Defaults for a segment switched to a soft mode. */
|
|
12
|
+
export declare const VECTOR_MASK_EDGE_DEFAULTS: {
|
|
13
|
+
readonly radius: 4;
|
|
14
|
+
readonly inner: 4;
|
|
15
|
+
readonly outer: 8;
|
|
16
|
+
};
|
|
17
|
+
/** The segments of a vector mask's path as `{ component, subpath, segment }` triples. */
|
|
18
|
+
export declare function vectorMaskSegments(components: readonly ShapeComponent[]): Array<{
|
|
19
|
+
component: number;
|
|
20
|
+
subpath: number;
|
|
21
|
+
segment: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** Edges whose segment still exists in `components` (after the path was edited). */
|
|
24
|
+
export declare function validVectorMaskEdges(edges: readonly VectorMaskEdge[] | undefined, components: readonly ShapeComponent[]): VectorMaskEdge[];
|
|
25
|
+
/**
|
|
26
|
+
* The hybrid coverage (0..1, `width × height`) from the path's hard coverage `hard` (as
|
|
27
|
+
* `componentsCoverage` gives it, before the mask's own feather and density) and the soft
|
|
28
|
+
* `edges`. `guide` is the layer's RGBA over the canvas, for matte corridors; without one a matte
|
|
29
|
+
* segment is feathered instead. Returns `hard` itself when no edge is soft.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hybridVectorMaskCoverage(vm: Pick<VectorMask, 'components' | 'edges'>, hard: Float32Array, width: number, height: number, guide: ArrayLike<number> | null): Float32Array;
|
|
@@ -51,6 +51,23 @@ export interface VectorMask {
|
|
|
51
51
|
density?: number;
|
|
52
52
|
/** Gaussian feather σ in px (default 0). */
|
|
53
53
|
feather?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Hybrid mask: soft edge modes per path segment (see `hybridVectorMaskCoverage`); segments not
|
|
56
|
+
* listed are hard. Indices follow the path's components, subpaths and segments.
|
|
57
|
+
*/
|
|
58
|
+
edges?: VectorMaskEdge[];
|
|
59
|
+
}
|
|
60
|
+
/** One segment's edge mode in a hybrid vector mask. */
|
|
61
|
+
export interface VectorMaskEdge {
|
|
62
|
+
component: number;
|
|
63
|
+
subpath: number;
|
|
64
|
+
/** Segment index in the subpath (from anchor `segment` to the next). */
|
|
65
|
+
segment: number;
|
|
66
|
+
/** `feather`: a Gaussian falloff of `radius` px across the curve; `matte`: a corridor from `inner` px inside to `outer` px outside, solved from the layer's colours. */
|
|
67
|
+
mode: 'hard' | 'feather' | 'matte';
|
|
68
|
+
radius?: number;
|
|
69
|
+
inner?: number;
|
|
70
|
+
outer?: number;
|
|
54
71
|
}
|
|
55
72
|
/** The base sliders of the old document-level adjustments, as a filter on one layer. */
|
|
56
73
|
export interface DevelopSettings {
|
|
@@ -51,6 +51,23 @@ export interface VectorMask {
|
|
|
51
51
|
density?: number;
|
|
52
52
|
/** Gaussian feather σ in px (default 0). */
|
|
53
53
|
feather?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Hybrid mask: soft edge modes per path segment (see `hybridVectorMaskCoverage`); segments not
|
|
56
|
+
* listed are hard. Indices follow the path's components, subpaths and segments.
|
|
57
|
+
*/
|
|
58
|
+
edges?: VectorMaskEdge[];
|
|
59
|
+
}
|
|
60
|
+
/** One segment's edge mode in a hybrid vector mask. */
|
|
61
|
+
export interface VectorMaskEdge {
|
|
62
|
+
component: number;
|
|
63
|
+
subpath: number;
|
|
64
|
+
/** Segment index in the subpath (from anchor `segment` to the next). */
|
|
65
|
+
segment: number;
|
|
66
|
+
/** `feather`: a Gaussian falloff of `radius` px across the curve; `matte`: a corridor from `inner` px inside to `outer` px outside, solved from the layer's colours. */
|
|
67
|
+
mode: 'hard' | 'feather' | 'matte';
|
|
68
|
+
radius?: number;
|
|
69
|
+
inner?: number;
|
|
70
|
+
outer?: number;
|
|
54
71
|
}
|
|
55
72
|
/** The base sliders of the old document-level adjustments, as a filter on one layer. */
|
|
56
73
|
export interface DevelopSettings {
|
|
@@ -57,6 +57,8 @@ export interface SerializedVectorMask {
|
|
|
57
57
|
linked?: false;
|
|
58
58
|
density?: number;
|
|
59
59
|
feather?: number;
|
|
60
|
+
/** Hybrid mask: soft edge modes per segment. */
|
|
61
|
+
edges?: unknown[];
|
|
60
62
|
}
|
|
61
63
|
/** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */
|
|
62
64
|
export interface SerializedLayerEffects {
|
|
@@ -57,6 +57,8 @@ export interface SerializedVectorMask {
|
|
|
57
57
|
linked?: false;
|
|
58
58
|
density?: number;
|
|
59
59
|
feather?: number;
|
|
60
|
+
/** Hybrid mask: soft edge modes per segment. */
|
|
61
|
+
edges?: unknown[];
|
|
60
62
|
}
|
|
61
63
|
/** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */
|
|
62
64
|
export interface SerializedLayerEffects {
|
|
@@ -6,26 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CropBox } from '../types.cjs';
|
|
8
8
|
import type { AffineMatrix } from '../../geom/types.cjs';
|
|
9
|
+
import { readRegion } from '../../raster/surface.cjs';
|
|
9
10
|
import type { ImageLayer, RasterMask, VectorMask } from './model.cjs';
|
|
10
11
|
/** Whether a vector mask takes part in compositing. */
|
|
11
12
|
export declare function isActiveVectorMask(vm: VectorMask | undefined): vm is VectorMask;
|
|
12
13
|
/**
|
|
13
|
-
* The vector mask's coverage (0..1) over the `width × height` canvas, with its
|
|
14
|
-
* density applied.
|
|
14
|
+
* The vector mask's coverage (0..1) over the `width × height` canvas, with its hybrid edges,
|
|
15
|
+
* feather and density applied.
|
|
15
16
|
*/
|
|
16
|
-
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number): Float32Array;
|
|
17
|
+
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number, guide?: ArrayLike<number> | null): Float32Array;
|
|
18
|
+
/** Where a layer's pixels really are (the Background's come from the source image). */
|
|
19
|
+
export type LayerSurfaceOf = (layer: ImageLayer) => Parameters<typeof readRegion>[0] | undefined;
|
|
20
|
+
/** A layer's own RGBA over the canvas (the guide for hybrid matte edges), or null without pixels. */
|
|
21
|
+
export declare function layerGuide(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): Uint8ClampedArray | null;
|
|
17
22
|
/**
|
|
18
23
|
* The raster mask a layer composites with: its raster mask times its vector mask (layer space,
|
|
19
24
|
* feather and density baked in), or the raster mask alone when there is no active vector mask.
|
|
20
25
|
* A depth (parametric) mask cannot be combined, so it wins.
|
|
21
26
|
*/
|
|
22
|
-
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number): RasterMask | undefined;
|
|
27
|
+
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): RasterMask | undefined;
|
|
23
28
|
/**
|
|
24
29
|
* The stack as the compositor should see it: each layer with an active vector mask replaced by a
|
|
25
30
|
* copy whose `mask` is `effectiveLayerMask` (cached per layer object and canvas size, so the
|
|
26
31
|
* engine's tile caches keep working). Returns `layers` itself when there are none.
|
|
27
32
|
*/
|
|
28
|
-
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number): readonly ImageLayer[];
|
|
33
|
+
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number, surfaceOf?: LayerSurfaceOf): readonly ImageLayer[];
|
|
29
34
|
/** A vector mask moved by (dx, dy) document pixels. */
|
|
30
35
|
export declare function offsetVectorMask(vm: VectorMask, dx: number, dy: number): VectorMask;
|
|
31
36
|
/** A vector mask with its paths mapped by `f` (document space); `affine` keeps live shapes live. */
|
|
@@ -6,26 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CropBox } from '../types.js';
|
|
8
8
|
import type { AffineMatrix } from '../../geom/types.js';
|
|
9
|
+
import { readRegion } from '../../raster/surface.js';
|
|
9
10
|
import type { ImageLayer, RasterMask, VectorMask } from './model.js';
|
|
10
11
|
/** Whether a vector mask takes part in compositing. */
|
|
11
12
|
export declare function isActiveVectorMask(vm: VectorMask | undefined): vm is VectorMask;
|
|
12
13
|
/**
|
|
13
|
-
* The vector mask's coverage (0..1) over the `width × height` canvas, with its
|
|
14
|
-
* density applied.
|
|
14
|
+
* The vector mask's coverage (0..1) over the `width × height` canvas, with its hybrid edges,
|
|
15
|
+
* feather and density applied.
|
|
15
16
|
*/
|
|
16
|
-
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number): Float32Array;
|
|
17
|
+
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number, guide?: ArrayLike<number> | null): Float32Array;
|
|
18
|
+
/** Where a layer's pixels really are (the Background's come from the source image). */
|
|
19
|
+
export type LayerSurfaceOf = (layer: ImageLayer) => Parameters<typeof readRegion>[0] | undefined;
|
|
20
|
+
/** A layer's own RGBA over the canvas (the guide for hybrid matte edges), or null without pixels. */
|
|
21
|
+
export declare function layerGuide(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): Uint8ClampedArray | null;
|
|
17
22
|
/**
|
|
18
23
|
* The raster mask a layer composites with: its raster mask times its vector mask (layer space,
|
|
19
24
|
* feather and density baked in), or the raster mask alone when there is no active vector mask.
|
|
20
25
|
* A depth (parametric) mask cannot be combined, so it wins.
|
|
21
26
|
*/
|
|
22
|
-
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number): RasterMask | undefined;
|
|
27
|
+
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): RasterMask | undefined;
|
|
23
28
|
/**
|
|
24
29
|
* The stack as the compositor should see it: each layer with an active vector mask replaced by a
|
|
25
30
|
* copy whose `mask` is `effectiveLayerMask` (cached per layer object and canvas size, so the
|
|
26
31
|
* engine's tile caches keep working). Returns `layers` itself when there are none.
|
|
27
32
|
*/
|
|
28
|
-
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number): readonly ImageLayer[];
|
|
33
|
+
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number, surfaceOf?: LayerSurfaceOf): readonly ImageLayer[];
|
|
29
34
|
/** A vector mask moved by (dx, dy) document pixels. */
|
|
30
35
|
export declare function offsetVectorMask(vm: VectorMask, dx: number, dy: number): VectorMask;
|
|
31
36
|
/** A vector mask with its paths mapped by `f` (document space); `affine` keeps live shapes live. */
|
|
@@ -22,6 +22,8 @@ export interface PathAnchorRef {
|
|
|
22
22
|
export interface PathEditOverlay {
|
|
23
23
|
/** The target's segments: 2 points for a line, 4 for a cubic. */
|
|
24
24
|
curves: ImagePoint[][];
|
|
25
|
+
/** A hybrid vector mask's edge mode per curve (same order); missing: all hard. */
|
|
26
|
+
curveModes?: Array<'hard' | 'feather' | 'matte'>;
|
|
25
27
|
/** Anchors shown; selected ones are drawn filled. */
|
|
26
28
|
anchors: Array<{
|
|
27
29
|
point: ImagePoint;
|
|
@@ -77,6 +79,8 @@ export declare class PathEditGesture {
|
|
|
77
79
|
private selectComponent;
|
|
78
80
|
private commit;
|
|
79
81
|
down(p: ImagePoint, tool: PathEditTool, mods?: PathEditModifiers): void;
|
|
82
|
+
/** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
|
|
83
|
+
private cycleEdge;
|
|
80
84
|
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
81
85
|
private convert;
|
|
82
86
|
private penDown;
|
|
@@ -22,6 +22,8 @@ export interface PathAnchorRef {
|
|
|
22
22
|
export interface PathEditOverlay {
|
|
23
23
|
/** The target's segments: 2 points for a line, 4 for a cubic. */
|
|
24
24
|
curves: ImagePoint[][];
|
|
25
|
+
/** A hybrid vector mask's edge mode per curve (same order); missing: all hard. */
|
|
26
|
+
curveModes?: Array<'hard' | 'feather' | 'matte'>;
|
|
25
27
|
/** Anchors shown; selected ones are drawn filled. */
|
|
26
28
|
anchors: Array<{
|
|
27
29
|
point: ImagePoint;
|
|
@@ -77,6 +79,8 @@ export declare class PathEditGesture {
|
|
|
77
79
|
private selectComponent;
|
|
78
80
|
private commit;
|
|
79
81
|
down(p: ImagePoint, tool: PathEditTool, mods?: PathEditModifiers): void;
|
|
82
|
+
/** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
|
|
83
|
+
private cycleEdge;
|
|
80
84
|
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
81
85
|
private convert;
|
|
82
86
|
private penDown;
|