fablecut 1.12.1 → 1.13.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/CLAUDE.md +228 -6
- package/README.md +22 -8
- package/app.js +1303 -25
- package/color.js +47 -14
- package/edit-ops.js +25 -4
- package/export-jobs.js +8 -5
- package/index.html +10 -2
- package/mask.js +467 -0
- package/mcp-server.js +207 -7
- package/package.json +1 -1
- package/server.js +22 -1
- package/style.css +11 -0
- package/tracker.js +253 -0
package/color.js
CHANGED
|
@@ -42,16 +42,20 @@
|
|
|
42
42
|
layers: [{ name?, on?, qualifier?, mask?, …grade keys }]
|
|
43
43
|
qualifier: { hue?: [centre°, width°, soft°], sat?: [lo, hi, soft],
|
|
44
44
|
luma?: [lo, hi, soft], invert? } — measured on the layer's input
|
|
45
|
-
mask: { shape: ellipse | rect | poly, x, y (centre, 0…1 of the picture),
|
|
45
|
+
mask: { shape: ellipse | rect | poly | bezier, x, y (centre, 0…1 of the picture),
|
|
46
46
|
w, h (size, 0…1), rotation°, feather (0…0.5 of the height),
|
|
47
47
|
invert?, points?: [[dx, dy], …] (poly, relative to x, y),
|
|
48
|
+
bezier: [[dx, dy, inX?, inY?, outX?, outY?], …] — anchors with
|
|
49
|
+
handles, the outline mask.js draws (a free-hand stroke,
|
|
50
|
+
{ shape: "freehand", stroke: [[u, v], …] }, is fitted into one),
|
|
48
51
|
keys?: [{ t, ease?, x?, y?, w?, h?, rotation?, feather? }] }
|
|
49
52
|
matte = qualifier × mask (each 1 when absent); out = mix(in, graded, matte). */
|
|
50
53
|
(function (root, factory) {
|
|
51
|
-
const
|
|
52
|
-
|
|
54
|
+
const node = typeof module === "object" && module.exports;
|
|
55
|
+
const api = factory(node ? require("./mask") : root.FableCutMask);
|
|
56
|
+
if (node) module.exports = api;
|
|
53
57
|
else root.FableCutColor = api;
|
|
54
|
-
})(typeof self !== "undefined" ? self : this, function () {
|
|
58
|
+
})(typeof self !== "undefined" ? self : this, function (MaskLib) {
|
|
55
59
|
"use strict";
|
|
56
60
|
|
|
57
61
|
const WHEELS = ["lift", "gamma", "gain", "offset"];
|
|
@@ -91,7 +95,9 @@
|
|
|
91
95
|
const HUE_N = 360; // hue-curve lookup-table size (also sat 0…1 for satLuma)
|
|
92
96
|
const MAX_LAYERS = 8;
|
|
93
97
|
const MAX_POLY = 16;
|
|
94
|
-
const
|
|
98
|
+
const MAX_BEZIER = 24; // bezier anchors on a grade mask
|
|
99
|
+
const MAX_OUTLINE = 96; // the flattened outline the shader walks
|
|
100
|
+
const MASK_SHAPES = ["ellipse", "rect", "poly", "bezier"];
|
|
95
101
|
const MASK_KEYED = ["x", "y", "w", "h", "rotation", "feather"];
|
|
96
102
|
const KEY_RANGE = { x: [-1, 2], y: [-1, 2], w: [0.001, 4], h: [0.001, 4], rotation: [-720, 720], feather: [0, 0.5] };
|
|
97
103
|
const WB_STOPS = 1 / 100; // temp / tint ±100 → ±1 stop per channel
|
|
@@ -487,6 +493,12 @@
|
|
|
487
493
|
if (m == null) return null;
|
|
488
494
|
const bad = (msg) => { if (strict) throw new Error(`${where}.mask${msg}`); return null; };
|
|
489
495
|
if (typeof m !== "object" || Array.isArray(m)) return bad(" must be {shape, x, y, w, h, rotation?, feather?, invert?}");
|
|
496
|
+
if (m.shape === "freehand") {
|
|
497
|
+
if (!Array.isArray(m.stroke) || m.stroke.length < 3 || m.stroke.some((p) => !Array.isArray(p) || !isNum(p[0]) || !isNum(p[1])))
|
|
498
|
+
return bad(".stroke must be at least 3 [u, v] points (fractions of the picture)");
|
|
499
|
+
m = { ...m, ...MaskLib.fitStroke(m.stroke, m.aspect, MAX_BEZIER), shape: "bezier" };
|
|
500
|
+
delete m.stroke; delete m.aspect;
|
|
501
|
+
}
|
|
490
502
|
const shape = m.shape == null ? "ellipse" : m.shape;
|
|
491
503
|
if (!MASK_SHAPES.includes(shape)) return bad(`.shape must be ${MASK_SHAPES.join(" | ")}`);
|
|
492
504
|
for (const k of Object.keys(m)) if (![...MASK_KEYED, "shape", "invert", "points", "keys"].includes(k)) bad(`.${k}: unknown key`);
|
|
@@ -509,6 +521,11 @@
|
|
|
509
521
|
if (!Array.isArray(pts) || pts.length < 3 || pts.length > MAX_POLY || pts.some((p) => !Array.isArray(p) || p.length !== 2 || !p.every(isNum)))
|
|
510
522
|
return bad(`.points must be 3…${MAX_POLY} [dx, dy] pairs around x, y (fractions of the picture)`);
|
|
511
523
|
out.points = pts.map(([a, b]) => [+clamp(a, -2, 2).toFixed(4), +clamp(b, -2, 2).toFixed(4)]);
|
|
524
|
+
} else if (shape === "bezier") {
|
|
525
|
+
const pts = m.points;
|
|
526
|
+
if (!Array.isArray(pts) || pts.length < 3 || pts.length > MAX_BEZIER || pts.some((p) => !Array.isArray(p) || (p.length !== 2 && p.length !== 6) || !p.every(isNum)))
|
|
527
|
+
return bad(`.points must be 3…${MAX_BEZIER} anchors [dx, dy] or [dx, dy, inX, inY, outX, outY] around x, y (fractions of the picture)`);
|
|
528
|
+
out.points = pts.map((p) => p.map((v) => +clamp(v, -2, 2).toFixed(4)));
|
|
512
529
|
}
|
|
513
530
|
if (m.keys != null) {
|
|
514
531
|
if (!Array.isArray(m.keys)) return bad(".keys must be a list of {t, x?, y?, w?, h?, rotation?, feather?}");
|
|
@@ -596,10 +613,26 @@
|
|
|
596
613
|
const qx = Math.abs(px) - rx, qy = Math.abs(py) - ry;
|
|
597
614
|
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0);
|
|
598
615
|
}
|
|
599
|
-
if (m.shape === "poly") return polyDist(m
|
|
616
|
+
if (m.shape === "poly" || m.shape === "bezier") return polyDist(outlineAt(m, aspect), px, py);
|
|
600
617
|
const k0 = Math.hypot(px / rx, py / ry), k1 = Math.hypot(px / (rx * rx), py / (ry * ry));
|
|
601
618
|
return k0 < 1e-6 ? -Math.min(rx, ry) : k0 * (k0 - 1) / k1;
|
|
602
619
|
}
|
|
620
|
+
/** A bezier mask's outline as a polygon around its centre (fractions of the
|
|
621
|
+
* picture, unrotated) — at most MAX_OUTLINE corners, the shader's budget. */
|
|
622
|
+
/** The poly / bezier outline in aspect-scaled units, cached per points array — the CPU path asks per pixel. */
|
|
623
|
+
const outlines = new WeakMap();
|
|
624
|
+
function outlineAt(m, aspect) {
|
|
625
|
+
let o = outlines.get(m.points);
|
|
626
|
+
if (!o || o.aspect !== aspect) {
|
|
627
|
+
o = { aspect, pts: (m.shape === "bezier" ? bezierOutline(m) : m.points).map(([a, b]) => [a * aspect, b]) };
|
|
628
|
+
outlines.set(m.points, o);
|
|
629
|
+
}
|
|
630
|
+
return o.pts;
|
|
631
|
+
}
|
|
632
|
+
function bezierOutline(m) {
|
|
633
|
+
const A = MaskLib.anchors({ shape: "bezier", x: 0, y: 0, scale: 1, rotation: 0, points: m.points }, 1, 1);
|
|
634
|
+
return MaskLib.flatten(A, Math.max(1, Math.min(16, Math.floor(MAX_OUTLINE / A.length))));
|
|
635
|
+
}
|
|
603
636
|
function polyDist(pts, px, py) {
|
|
604
637
|
let d = (px - pts[0][0]) ** 2 + (py - pts[0][1]) ** 2, s = 1;
|
|
605
638
|
for (let i = 0, j = pts.length - 1; i < pts.length; j = i, i++) {
|
|
@@ -757,10 +790,10 @@
|
|
|
757
790
|
u.uMRot = m ? m.rotation * Math.PI / 180 : 0;
|
|
758
791
|
u.uMFeather = m ? Math.max(0.002, m.feather) : 0.002;
|
|
759
792
|
u.uMInv = m && m.invert ? 1 : 0;
|
|
760
|
-
const pts = new Float32Array(
|
|
761
|
-
|
|
793
|
+
const pts = new Float32Array(MAX_OUTLINE * 2), P = m && m.points ? (m.shape === "bezier" ? bezierOutline(m) : m.points) : [];
|
|
794
|
+
P.forEach(([a, b], i) => { pts[i * 2] = a; pts[i * 2 + 1] = b; });
|
|
762
795
|
u.uMPts = pts;
|
|
763
|
-
u.uMN =
|
|
796
|
+
u.uMN = P.length;
|
|
764
797
|
passes.push(u);
|
|
765
798
|
}
|
|
766
799
|
return passes;
|
|
@@ -800,10 +833,10 @@ uniform int uLayer, uMatte, uPremul; // layer pass: mix by its matte · show t
|
|
|
800
833
|
uniform int uQOn, uQInv;
|
|
801
834
|
uniform ivec3 uQUse; // hue, sat, luma ranges in use
|
|
802
835
|
uniform vec3 uQHue, uQSat, uQLuma; // hue: centre, half width, soft (turns) · sat / luma: low, high, soft
|
|
803
|
-
uniform int uMShape, uMInv, uMN; // 0 none · 1 ellipse · 2 rect · 3 poly
|
|
836
|
+
uniform int uMShape, uMInv, uMN; // 0 none · 1 ellipse · 2 rect · 3 poly · 4 bezier (flattened)
|
|
804
837
|
uniform vec2 uMC, uMW;
|
|
805
838
|
uniform float uMRot, uMFeather, uAspect;
|
|
806
|
-
uniform vec2 uMPts[${
|
|
839
|
+
uniform vec2 uMPts[${MAX_OUTLINE}];
|
|
807
840
|
in vec2 vPos;
|
|
808
841
|
out vec4 outColor;
|
|
809
842
|
vec4 lutAt(sampler2D t, int n, float x) {
|
|
@@ -862,10 +895,10 @@ float maskDist(vec2 uv) {
|
|
|
862
895
|
vec2 q = abs(p) - r;
|
|
863
896
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
|
|
864
897
|
}
|
|
865
|
-
if (uMShape
|
|
898
|
+
if (uMShape >= 3) {
|
|
866
899
|
vec2 v0 = uMPts[0] * vec2(uAspect, 1.0);
|
|
867
900
|
float dd = dot(p - v0, p - v0), s = 1.0;
|
|
868
|
-
for (int i = 0; i < ${
|
|
901
|
+
for (int i = 0; i < ${MAX_OUTLINE}; i++) {
|
|
869
902
|
if (i >= uMN) break;
|
|
870
903
|
int j = i == 0 ? uMN - 1 : i - 1;
|
|
871
904
|
vec2 vi = uMPts[i] * vec2(uAspect, 1.0), vj = uMPts[j] * vec2(uAspect, 1.0);
|
|
@@ -1034,7 +1067,7 @@ void main() {
|
|
|
1034
1067
|
GRADE_PARAMS, GRADE_KEYS, WHEELS, GRADE_VERT, GRADE_FRAG, SKIN_ANGLE,
|
|
1035
1068
|
CURVE_CHANNELS, HUE_CURVES, HUE_KEYS, CURVE_N, HUE_N, BAND,
|
|
1036
1069
|
normalizeGrade, fullGrade, isNeutral, mergeGrade, summarizeGrade, withoutCurves,
|
|
1037
|
-
MAX_LAYERS, MAX_POLY, MASK_SHAPES, MASK_KEYED, mergeLayer, layerGrade, maskAt, gradeAt, qualAlpha, maskAlpha, maskDist, gradePasses,
|
|
1070
|
+
MAX_LAYERS, MAX_POLY, MAX_BEZIER, MASK_SHAPES, MASK_KEYED, mergeLayer, layerGrade, maskAt, gradeAt, qualAlpha, maskAlpha, maskDist, gradePasses,
|
|
1038
1071
|
monotone, curveFn, rgbToHsv, hsvToRgb,
|
|
1039
1072
|
wbGains, solveWhiteBalance, srgbToLinear, linearToSrgb, wheelToRgb, rgbToWheel, ycbcr,
|
|
1040
1073
|
prepareGrade, gradePixel, gradeImageData, gradeUniforms,
|
package/edit-ops.js
CHANGED
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
`forDoc(doc, opts)` builds the env from the document itself (its tracks,
|
|
20
20
|
lockedTracks and untargetedTracks) for headless callers. */
|
|
21
21
|
(function (root, factory) {
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const node = typeof module === "object" && module.exports;
|
|
23
|
+
const api = factory(node ? require("./mask") : root.FableCutMask, node ? require("./tracker") : root.FableCutTracker);
|
|
24
|
+
if (node) module.exports = api;
|
|
24
25
|
else root.FableCutEdit = api;
|
|
25
|
-
})(typeof self !== "undefined" ? self : this, function () {
|
|
26
|
+
})(typeof self !== "undefined" ? self : this, function (MaskLib, TrackLib) {
|
|
26
27
|
"use strict";
|
|
27
28
|
|
|
28
29
|
const MIN_DUR = 0.05;
|
|
@@ -92,6 +93,20 @@
|
|
|
92
93
|
}
|
|
93
94
|
return Object.keys(out).length ? out : undefined;
|
|
94
95
|
}
|
|
96
|
+
/** A clip's head moved by `offset` s: its mask keys and track samples follow, like shiftKF does for keyframes. */
|
|
97
|
+
function shiftMasks(c, offset) {
|
|
98
|
+
if (!offset || !c.props) return;
|
|
99
|
+
const ms = c.props.masks, ts = c.props.tracks;
|
|
100
|
+
if (MaskLib && Array.isArray(ms)) {
|
|
101
|
+
const next = MaskLib.shiftKeys(ms, offset);
|
|
102
|
+
if (next !== ms) c.props = { ...c.props, masks: next };
|
|
103
|
+
}
|
|
104
|
+
if (TrackLib && Array.isArray(ts)) {
|
|
105
|
+
const next = TrackLib.shiftTracks(ts, offset);
|
|
106
|
+
c.props = { ...c.props };
|
|
107
|
+
if (next.length) c.props.tracks = next; else delete c.props.tracks;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
95
110
|
/** Default stereo pan for an isolated linked stem (L −1, R +1, else center). */
|
|
96
111
|
function defaultPanForChannel(ch) {
|
|
97
112
|
if (ch === 0) return -1;
|
|
@@ -190,6 +205,7 @@
|
|
|
190
205
|
linkedId: undefined,
|
|
191
206
|
linkGroup: undefined,
|
|
192
207
|
};
|
|
208
|
+
shiftMasks(right, cut);
|
|
193
209
|
c.duration = cut;
|
|
194
210
|
c.keyframes = shiftKF(c.keyframes, 0, cut);
|
|
195
211
|
c.transitionOut = undefined;
|
|
@@ -274,6 +290,7 @@
|
|
|
274
290
|
else c.in = 0;
|
|
275
291
|
c.duration -= dIn;
|
|
276
292
|
c.keyframes = shiftKF(c.keyframes, dIn, c.duration);
|
|
293
|
+
shiftMasks(c, dIn);
|
|
277
294
|
c.transitionIn = undefined;
|
|
278
295
|
changed++;
|
|
279
296
|
}
|
|
@@ -559,6 +576,7 @@
|
|
|
559
576
|
c.start = +t1.toFixed(4);
|
|
560
577
|
c.transitionIn = undefined;
|
|
561
578
|
c.keyframes = shiftKF(c.keyframes, t1 - oldStart, c.duration);
|
|
579
|
+
shiftMasks(c, t1 - oldStart);
|
|
562
580
|
} else {
|
|
563
581
|
// Stubs on both sides → effectively inside the window.
|
|
564
582
|
drop(c);
|
|
@@ -595,6 +613,7 @@
|
|
|
595
613
|
c.start = +t1.toFixed(4);
|
|
596
614
|
c.transitionIn = undefined;
|
|
597
615
|
c.keyframes = shiftKF(c.keyframes, t1 - oldStart, c.duration);
|
|
616
|
+
shiftMasks(c, t1 - oldStart);
|
|
598
617
|
}
|
|
599
618
|
}
|
|
600
619
|
}
|
|
@@ -710,6 +729,7 @@
|
|
|
710
729
|
if (isMediaClip(x)) x.in = +(x.in + d * clipSpeed(x)).toFixed(4);
|
|
711
730
|
x.duration = +(x.duration - d).toFixed(4);
|
|
712
731
|
x.keyframes = shiftKF(x.keyframes, d, x.duration);
|
|
732
|
+
shiftMasks(x, d);
|
|
713
733
|
clampTransitions(x);
|
|
714
734
|
}
|
|
715
735
|
}
|
|
@@ -885,6 +905,7 @@
|
|
|
885
905
|
x.in = +Math.max(0, x.in - e2 * sp).toFixed(4);
|
|
886
906
|
x.duration = +(x.duration + e2).toFixed(4);
|
|
887
907
|
x.keyframes = shiftKF(x.keyframes, -e2, x.duration);
|
|
908
|
+
shiftMasks(x, -e2);
|
|
888
909
|
}
|
|
889
910
|
}
|
|
890
911
|
d = +Math.min(d, a.duration, b.duration).toFixed(3);
|
|
@@ -956,7 +977,7 @@
|
|
|
956
977
|
|
|
957
978
|
return {
|
|
958
979
|
MIN_DUR, MIN_TRANS_DUR, CROSSFADE_DUR, GAP_EPS, TRIM_LOCKED, DEFAULT_TRACKS,
|
|
959
|
-
clipEnd, clipSpeed, kfChannel, hasSpeedRamp, mediaTimeAt, shiftKF, defaultPanForChannel,
|
|
980
|
+
clipEnd, clipSpeed, kfChannel, hasSpeedRamp, mediaTimeAt, shiftKF, shiftMasks, defaultPanForChannel,
|
|
960
981
|
create, forDoc,
|
|
961
982
|
};
|
|
962
983
|
});
|
package/export-jobs.js
CHANGED
|
@@ -103,14 +103,15 @@ function createExportJobs(opts) {
|
|
|
103
103
|
|
|
104
104
|
return {
|
|
105
105
|
/** Queue an export. where: auto (open tab, else headless) · tab · headless.
|
|
106
|
-
* kind "scopes" measures one still at `time` instead (fablecut_scopes)
|
|
107
|
-
|
|
106
|
+
* kind "scopes" measures one still at `time` instead (fablecut_scopes);
|
|
107
|
+
* kind "track" follows a region of a clip and saves it (fablecut_track). */
|
|
108
|
+
request({ kind = "export", range, profile, time, revision, where = "auto", matte = null, mask = null, track = null }) {
|
|
108
109
|
if (!["auto", "tab", "headless"].includes(where)) throw Object.assign(new Error("where must be auto, tab or headless"), { code: 400 });
|
|
109
|
-
if (
|
|
110
|
+
if (!["export", "scopes", "track"].includes(kind)) throw Object.assign(new Error("kind must be export, scopes or track"), { code: 400 });
|
|
110
111
|
if (time != null && !(Number.isFinite(time) && time >= 0)) throw Object.assign(new Error("time must be seconds ≥ 0"), { code: 400 });
|
|
111
112
|
if (range != null && range !== "entire" && range !== "in-out") throw Object.assign(new Error("range must be \"entire\" or \"in-out\""), { code: 400 });
|
|
112
113
|
const busy = [...jobs.values()].find((j) => !isEnded(j));
|
|
113
|
-
if (busy) throw Object.assign(new Error(`${busy.kind === "scopes" ? "scope reading" : "export"} ${busy.id} is still ${busy.status} — wait for it, or cancel it`), { code: 409 });
|
|
114
|
+
if (busy) throw Object.assign(new Error(`${busy.kind === "scopes" ? "scope reading" : busy.kind === "track" ? "tracking job" : "export"} ${busy.id} is still ${busy.status} — wait for it, or cancel it`), { code: 409 });
|
|
114
115
|
const tabs = opts.editors();
|
|
115
116
|
if (where === "tab" && !tabs) throw Object.assign(new Error("no editor tab is open — open the editor, or use where:\"headless\""), { code: 409 });
|
|
116
117
|
const now = Date.now();
|
|
@@ -118,6 +119,8 @@ function createExportJobs(opts) {
|
|
|
118
119
|
id: "x_" + Math.random().toString(36).slice(2, 10), kind, status: "pending", progress: 0,
|
|
119
120
|
range: range || null, profile: profile || null, time: time ?? null, revision: Number.isFinite(revision) ? revision : null,
|
|
120
121
|
matte: kind === "scopes" && matte ? { clip: String(matte.clip), layer: matte.layer } : null,
|
|
122
|
+
mask: kind === "scopes" && mask ? { clip: String(mask.clip) } : null,
|
|
123
|
+
track: kind === "track" && track ? track : null,
|
|
121
124
|
via: where === "headless" || (where === "auto" && !tabs) ? "headless" : "tab",
|
|
122
125
|
created: now, updated: now,
|
|
123
126
|
};
|
|
@@ -129,7 +132,7 @@ function createExportJobs(opts) {
|
|
|
129
132
|
},
|
|
130
133
|
get(id) { const j = jobs.get(id); return j ? view(j) : null; },
|
|
131
134
|
/** What a tab needs to run the job (headless pages fetch it by id). */
|
|
132
|
-
ticket(id) { const j = jobs.get(id); return j ? { id: j.id, kind: j.kind, range: j.range, profile: j.profile, time: j.time, revision: j.revision, matte: j.matte || null, status: j.status } : null; },
|
|
135
|
+
ticket(id) { const j = jobs.get(id); return j ? { id: j.id, kind: j.kind, range: j.range, profile: j.profile, time: j.time, revision: j.revision, matte: j.matte || null, mask: j.mask || null, track: j.track || null, status: j.status } : null; },
|
|
133
136
|
/** First tab to claim a pending job runs it. */
|
|
134
137
|
claim(id) {
|
|
135
138
|
const j = jobs.get(id);
|
package/index.html
CHANGED
|
@@ -394,14 +394,20 @@
|
|
|
394
394
|
<tr><td><kbd>Z</kbd></td><td>Zoom to selected clip(s)</td></tr>
|
|
395
395
|
<tr><td><kbd>Alt</kbd>+<kbd>Z</kbd></td><td>Zoom to IN–OUT work area</td></tr>
|
|
396
396
|
<tr><td><kbd>⇧</kbd>+<kbd>Z</kbd></td><td>Zoom timeline to fit</td></tr>
|
|
397
|
-
<tr><td><kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>⇧</kbd>+<kbd>Z</kbd></td><td>Undo / redo</td></tr>
|
|
397
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>⇧</kbd>+<kbd>Z</kbd></td><td>Undo / redo (also <kbd>Ctrl</kbd>+<kbd>Y</kbd>)</td></tr>
|
|
398
398
|
<tr><td><kbd>Ctrl</kbd>+wheel</td><td>Zoom timeline at cursor</td></tr>
|
|
399
399
|
<tr><td>Audio volume line</td><td>Drag to change the level · <kbd>Ctrl/Cmd</kbd>+click adds a keyframe · <kbd>Alt</kbd>+click or double-click removes one · corner grips drag fades</td></tr>
|
|
400
400
|
<tr class="keys-head"><td colspan="2">Color workspace</td></tr>
|
|
401
401
|
<tr><td>Colour wheel</td><td>Drag to tint (<kbd>⇧</kbd> = fine) · double-click resets the colour · double-click a label or slider name to reset it</td></tr>
|
|
402
402
|
<tr><td>WB pick / Pick</td><td>Click the monitor to balance, to mark a colour on the curves, or to select it in a layer's qualifier · <kbd>Esc</kbd> cancels</td></tr>
|
|
403
403
|
<tr><td>Curves</td><td>Click adds a point on the curve · drag moves it · double-click removes it</td></tr>
|
|
404
|
-
<tr><td>Layer mask on the monitor</td><td>Drag inside to move · handles resize · the knob rotates (<kbd>⇧</kbd> snaps to 15°) · poly: <kbd>Ctrl/Cmd</kbd>+click adds a point, double-click removes one</td></tr>
|
|
404
|
+
<tr><td>Layer mask on the monitor</td><td>Drag inside to move · handles resize · the knob rotates (<kbd>⇧</kbd> snaps to 15°) · poly / bezier: <kbd>Ctrl/Cmd</kbd>+click adds a point, double-click removes one · <b>Draw</b>: drag a free-hand outline</td></tr>
|
|
405
|
+
<tr class="keys-head"><td colspan="2">Masks (Inspector → Masks)</td></tr>
|
|
406
|
+
<tr><td>Pen</td><td>Click to place points, drag while placing to pull curve handles · click the first point or <kbd>Enter</kbd> closes · <kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Backspace</kbd> removes the last point, <kbd>Ctrl</kbd>+<kbd>⇧</kbd>+<kbd>Z</kbd> puts it back · <kbd>Esc</kbd> cancels</td></tr>
|
|
407
|
+
<tr><td>Draw</td><td>Drag a free-hand outline — it becomes an editable bezier</td></tr>
|
|
408
|
+
<tr><td>Mask on the monitor</td><td>Drag inside to move · edge handles resize · the knob rotates (<kbd>⇧</kbd> snaps to 15°) · drag a point or its handles (<kbd>Alt</kbd> breaks the pair) · <kbd>Alt</kbd>+drag a point pulls new handles · <kbd>Ctrl/Cmd</kbd>+click the outline adds a point · double-click a point removes it · <kbd>Esc</kbd> deselects</td></tr>
|
|
409
|
+
<tr class="keys-head"><td colspan="2">Tracking (Inspector → Tracking, or a mask's Track buttons)</td></tr>
|
|
410
|
+
<tr><td>Track point</td><td>Click the spot to follow on the monitor — it tracks from the playhead to the clip's end · <kbd>Esc</kbd> cancels the pick, or stops a track that is running (what was tracked so far is kept)</td></tr>
|
|
405
411
|
</table>
|
|
406
412
|
</div>
|
|
407
413
|
<div class="dialog-actions"><button class="btn primary" id="btnCloseHelp">Close</button></div>
|
|
@@ -413,6 +419,8 @@
|
|
|
413
419
|
<script src="loudness.js"></script>
|
|
414
420
|
<script src="ducking.js"></script>
|
|
415
421
|
<script src="audio-fx.js"></script>
|
|
422
|
+
<script src="mask.js"></script>
|
|
423
|
+
<script src="tracker.js"></script>
|
|
416
424
|
<script src="color.js"></script>
|
|
417
425
|
<script src="edit-ops.js"></script>
|
|
418
426
|
<script src="app.js"></script>
|