greenlight-motion 0.3.4 → 0.4.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/README.md +15 -10
- package/dist/bundle-inputs.json +23 -1
- package/dist/greenlight-motion-script.mjs +549 -80
- package/dist/greenlight-motion.js +1078 -747
- package/dist/greenlight-motion.mjs +42670 -40592
- package/dist/script-inputs.json +3 -1
- package/package.json +1 -1
- package/schema/capabilities.json +1 -1
- package/skill/SKILL.md +10 -10
- package/skill/references/scene-script.md +2 -2
- package/skill/references/video-3d.md +107 -100
- package/skill/references/video-backgrounds.md +89 -41
- package/skill/references/video-camera.md +158 -83
- package/skill/references/video-design.md +39 -7
- package/skill/references/video-effects.md +936 -620
- package/skill/references/video-grid.md +24 -24
- package/skill/references/video-keyframes.md +145 -52
- package/skill/references/video-particles.md +48 -29
- package/skill/references/video-paths.md +69 -65
- package/skill/references/video-projects.md +159 -89
- package/skill/references/video-shapes.md +94 -31
- package/skill/references/video-text-kinetic.md +78 -27
- package/skill/references/video-text-presets.md +738 -17
- package/skill/references/video-text.md +171 -79
- package/skill/references/video-transitions.md +72 -17
|
@@ -6868,16 +6868,41 @@ var TXA = (function() {
|
|
|
6868
6868
|
};
|
|
6869
6869
|
const LAYER_FIELDS2 = {
|
|
6870
6870
|
pivotX: num8("Pivot X", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = left edge, 100 = right edge"),
|
|
6871
|
-
pivotY: num8("Pivot Y", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = top, 100 = bottom")
|
|
6871
|
+
pivotY: num8("Pivot Y", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = top, 100 = bottom"),
|
|
6872
|
+
// Who films the units' depth (Position Z, Rotate 3D). Each unit = its own camera in
|
|
6873
|
+
// front of it (letters turn like separate cards). Layer / Project = one camera for
|
|
6874
|
+
// the whole text: nearer letters grow and every letter converges to one vanishing
|
|
6875
|
+
// point. Project = the project's Camera layer itself (dolly, pan, tilt, zoom).
|
|
6876
|
+
camera: en(
|
|
6877
|
+
"Camera",
|
|
6878
|
+
"unit",
|
|
6879
|
+
{ unit: "Each unit", layer: "Layer", project: "Project" },
|
|
6880
|
+
"Each unit: every letter (word, line) has its own little camera. Layer: one camera in front of the text. Project camera: the project's Camera layer films the letters in depth"
|
|
6881
|
+
),
|
|
6882
|
+
perspective: num8(
|
|
6883
|
+
"Perspective",
|
|
6884
|
+
0,
|
|
6885
|
+
0,
|
|
6886
|
+
2e4,
|
|
6887
|
+
10,
|
|
6888
|
+
"px",
|
|
6889
|
+
false,
|
|
6890
|
+
"The layer camera's distance from the text. 0 = the composition camera's, so a letter moved in Z looks like a layer moved in Z"
|
|
6891
|
+
)
|
|
6872
6892
|
};
|
|
6873
|
-
const LAYER_ITEM_KEYS2 = { pivotX: "textAnimatorPivotX", pivotY: "textAnimatorPivotY" };
|
|
6893
|
+
const LAYER_ITEM_KEYS2 = { pivotX: "textAnimatorPivotX", pivotY: "textAnimatorPivotY", camera: "textAnimatorCamera", perspective: "textAnimatorPerspective" };
|
|
6894
|
+
const LAYER_SPARSE = { camera: "unit", perspective: 0 };
|
|
6874
6895
|
const EFFECTS2 = [
|
|
6875
6896
|
{
|
|
6876
6897
|
key: "position",
|
|
6877
6898
|
label: "Position",
|
|
6878
6899
|
group: "Transform",
|
|
6879
6900
|
desc: "Move the selected units",
|
|
6880
|
-
fields: {
|
|
6901
|
+
fields: {
|
|
6902
|
+
x: num8("X", 0, -2e3, 2e3, 1, "px"),
|
|
6903
|
+
y: num8("Y", -80, -2e3, 2e3, 1, "px"),
|
|
6904
|
+
z: num8("Z", 0, -2e4, 2e4, 1, "px", true, "Depth: + toward you, \u2212 away. Under a shared camera (More options \u25B8 Camera) letters converge to one vanishing point; with each unit's own camera a unit grows or shrinks in place")
|
|
6905
|
+
}
|
|
6881
6906
|
},
|
|
6882
6907
|
{
|
|
6883
6908
|
key: "scale",
|
|
@@ -7111,7 +7136,10 @@ var TXA = (function() {
|
|
|
7111
7136
|
for (const [k, key] of Object.entries(LAYER_ITEM_KEYS2)) raw[k] = item[key];
|
|
7112
7137
|
const lf = sanitizeGroup(LAYER_FIELDS2, raw);
|
|
7113
7138
|
const out = { textAnimators: list };
|
|
7114
|
-
for (const [k, key] of Object.entries(LAYER_ITEM_KEYS2))
|
|
7139
|
+
for (const [k, key] of Object.entries(LAYER_ITEM_KEYS2)) {
|
|
7140
|
+
if (k in LAYER_SPARSE && (lf[k] === LAYER_SPARSE[k] || lf.camera === "unit")) continue;
|
|
7141
|
+
out[key] = lf[k];
|
|
7142
|
+
}
|
|
7115
7143
|
return out;
|
|
7116
7144
|
}
|
|
7117
7145
|
function layerSettings2(item) {
|
|
@@ -7415,7 +7443,7 @@ var TXA = (function() {
|
|
|
7415
7443
|
return n1 * Math.pow(p - 2.625 / c, 2) + 0.984375;
|
|
7416
7444
|
}
|
|
7417
7445
|
const elasticOut = (p) => p === 1 ? 1 : Math.pow(2, -10 * p) * Math.sin((p - 0.075) * (2 * Math.PI / 0.3)) + 1;
|
|
7418
|
-
function
|
|
7446
|
+
function bezier2(p1x, p1y, p2x, p2y) {
|
|
7419
7447
|
const bx = (t) => 3 * p1x * t * (1 - t) * (1 - t) + 3 * p2x * t * t * (1 - t) + t * t * t;
|
|
7420
7448
|
const by = (t) => 3 * p1y * t * (1 - t) * (1 - t) + 3 * p2y * t * t * (1 - t) + t * t * t;
|
|
7421
7449
|
return (x) => {
|
|
@@ -7468,22 +7496,22 @@ var TXA = (function() {
|
|
|
7468
7496
|
Impulse: sampled(SAMPLED.Impulse),
|
|
7469
7497
|
Swing: sampled(SAMPLED.Swing),
|
|
7470
7498
|
Anticipate: sampled(SAMPLED.Anticipate),
|
|
7471
|
-
Natural:
|
|
7472
|
-
"Slow Down":
|
|
7473
|
-
"AE In (arrive)":
|
|
7474
|
-
"AE Smooth":
|
|
7475
|
-
"AE Out (leave)":
|
|
7476
|
-
Punch:
|
|
7477
|
-
"Apple Out":
|
|
7478
|
-
"Smooth Glide":
|
|
7479
|
-
"Glide Easing Stop":
|
|
7499
|
+
Natural: bezier2(0.8, 0, 0.2, 1),
|
|
7500
|
+
"Slow Down": bezier2(0, 0, 0, 1),
|
|
7501
|
+
"AE In (arrive)": bezier2(0.3141666673, 0, 0.0875000209, 1),
|
|
7502
|
+
"AE Smooth": bezier2(0.882083327, 0, 0.2445833683, 1),
|
|
7503
|
+
"AE Out (leave)": bezier2(0.8699999754, 0, 0.6433336333, 1),
|
|
7504
|
+
Punch: bezier2(0.7, 0, 0.1, 1),
|
|
7505
|
+
"Apple Out": bezier2(0.32, 0.72, 0, 1),
|
|
7506
|
+
"Smooth Glide": bezier2(0.2111, 0.7721, 0.8541, 0.112),
|
|
7507
|
+
"Glide Easing Stop": bezier2(0, 0.9125, 1, -0.0338)
|
|
7480
7508
|
};
|
|
7481
7509
|
})();
|
|
7482
7510
|
let lastBez = null;
|
|
7483
7511
|
const curveFn2 = (name, handles) => {
|
|
7484
7512
|
if (name === "Custom" && Array.isArray(handles) && handles.length === 4) {
|
|
7485
7513
|
const key = handles.join(",");
|
|
7486
|
-
if (!lastBez || lastBez.key !== key) lastBez = { key, fn:
|
|
7514
|
+
if (!lastBez || lastBez.key !== key) lastBez = { key, fn: bezier2(...handles) };
|
|
7487
7515
|
return lastBez.fn;
|
|
7488
7516
|
}
|
|
7489
7517
|
return CURVE_FNS[name] || CURVE_FNS["Ease Out"];
|
|
@@ -7935,7 +7963,7 @@ var TXA = (function() {
|
|
|
7935
7963
|
}
|
|
7936
7964
|
function xfOf(st, level) {
|
|
7937
7965
|
const xf = st.xf || (st.xf = {});
|
|
7938
|
-
return xf[level] || (xf[level] = { tx: 0, ty: 0, s: 1, sx: 1, sy: 1, rot: 0, rx: 0, ry: 0, skew: 0, axis: 0, persp: 0 });
|
|
7966
|
+
return xf[level] || (xf[level] = { tx: 0, ty: 0, tz: 0, s: 1, sx: 1, sy: 1, rot: 0, rx: 0, ry: 0, skew: 0, axis: 0, persp: 0 });
|
|
7939
7967
|
}
|
|
7940
7968
|
function applyEffect(st, type, v, k, i, time, g, fillRgb, level = "char") {
|
|
7941
7969
|
const kp = clamp6(k, 0, 1);
|
|
@@ -7944,6 +7972,7 @@ var TXA = (function() {
|
|
|
7944
7972
|
const e2 = xfOf(st, level);
|
|
7945
7973
|
e2.tx += v.x * k;
|
|
7946
7974
|
e2.ty += v.y * k;
|
|
7975
|
+
e2.tz += (v.z || 0) * k;
|
|
7947
7976
|
break;
|
|
7948
7977
|
}
|
|
7949
7978
|
case "scale": {
|
|
@@ -8075,13 +8104,20 @@ var TXA = (function() {
|
|
|
8075
8104
|
return [H[0] / w, H[3] / w, H[1] / w, H[4] / w, H[2] / w, H[5] / w];
|
|
8076
8105
|
}
|
|
8077
8106
|
const W_NEAR = 0.25;
|
|
8078
|
-
|
|
8107
|
+
const Z_PERSP = 2e3;
|
|
8108
|
+
function levelH(e2, box2, pivot, zPersp = 0) {
|
|
8079
8109
|
const cx = (box2.x0 + box2.x1) / 2;
|
|
8080
8110
|
const cy = (box2.y0 + box2.y1) / 2;
|
|
8081
8111
|
const px = cx + pivot.x * (box2.x1 - box2.x0) / 2;
|
|
8082
8112
|
const py = cy + pivot.y * (box2.y1 - box2.y0) / 2;
|
|
8083
|
-
|
|
8084
|
-
|
|
8113
|
+
let sx = e2.s * e2.sx;
|
|
8114
|
+
let sy = e2.s * e2.sy;
|
|
8115
|
+
if (e2.tz) {
|
|
8116
|
+
const Dz = e2.persp > 0 ? e2.persp : zPersp > 0 ? zPersp : Z_PERSP;
|
|
8117
|
+
const f2 = Dz / Math.max(Dz * W_NEAR, Dz - e2.tz);
|
|
8118
|
+
sx *= f2;
|
|
8119
|
+
sy *= f2;
|
|
8120
|
+
}
|
|
8085
8121
|
let k00 = 1, k01 = 0, k10 = 0, k11 = 1;
|
|
8086
8122
|
if (e2.skew) {
|
|
8087
8123
|
const t = Math.tan(clamp6(e2.skew, -85, 85) * Math.PI / 180);
|
|
@@ -8201,7 +8237,7 @@ var TXA = (function() {
|
|
|
8201
8237
|
];
|
|
8202
8238
|
const tx = px + e2.tx - (L[0] * px + L[1] * py);
|
|
8203
8239
|
const ty = py + e2.ty - (L[3] * px + L[4] * py);
|
|
8204
|
-
const tz = -(L[6] * px + L[7] * py);
|
|
8240
|
+
const tz = -(L[6] * px + L[7] * py) - (e2.tz || 0);
|
|
8205
8241
|
return [L[0], L[1], L[2], tx, L[3], L[4], L[5], ty, L[6], L[7], L[8], tz, 0, 0, 0, 1];
|
|
8206
8242
|
}
|
|
8207
8243
|
function unitTransform3d2(xf, boxOf, pivot) {
|
|
@@ -8214,13 +8250,36 @@ var TXA = (function() {
|
|
|
8214
8250
|
}
|
|
8215
8251
|
return M;
|
|
8216
8252
|
}
|
|
8217
|
-
|
|
8253
|
+
const CAM_NEAR = 0.05;
|
|
8254
|
+
function unitTransformCam2(xf, boxOf, pivot, cam) {
|
|
8255
|
+
const M = unitTransform3d2(xf, boxOf, pivot);
|
|
8256
|
+
if (!M) return null;
|
|
8257
|
+
const d = cam.d;
|
|
8258
|
+
const H = [
|
|
8259
|
+
M[0] + cam.x * M[8] / d,
|
|
8260
|
+
M[1] + cam.x * M[9] / d,
|
|
8261
|
+
M[3] + cam.x * M[11] / d,
|
|
8262
|
+
M[4] + cam.y * M[8] / d,
|
|
8263
|
+
M[5] + cam.y * M[9] / d,
|
|
8264
|
+
M[7] + cam.y * M[11] / d,
|
|
8265
|
+
M[8] / d,
|
|
8266
|
+
M[9] / d,
|
|
8267
|
+
1 + M[11] / d
|
|
8268
|
+
];
|
|
8269
|
+
const b2 = boxOf("char");
|
|
8270
|
+
let wMin = Infinity;
|
|
8271
|
+
for (const [x, y] of [[b2.x0, b2.y0], [b2.x1, b2.y0], [b2.x0, b2.y1], [b2.x1, b2.y1]]) wMin = Math.min(wMin, H[6] * x + H[7] * y + H[8]);
|
|
8272
|
+
const depth = M[8] * ((b2.x0 + b2.x1) / 2) + M[9] * ((b2.y0 + b2.y1) / 2) + M[11];
|
|
8273
|
+
const projective = Math.abs(H[6]) > 1e-12 || Math.abs(H[7]) > 1e-12;
|
|
8274
|
+
return { H, projective, depth, hidden: !(wMin > CAM_NEAR) };
|
|
8275
|
+
}
|
|
8276
|
+
function unitTransform2(xf, boxOf, pivot, zPersp = 0) {
|
|
8218
8277
|
let H = null;
|
|
8219
8278
|
let projective = false;
|
|
8220
8279
|
for (const lv of LEVELS2) {
|
|
8221
8280
|
const e2 = xf && xf[lv];
|
|
8222
8281
|
if (!e2) continue;
|
|
8223
|
-
const r = levelH(e2, boxOf(lv), pivot);
|
|
8282
|
+
const r = levelH(e2, boxOf(lv), pivot, zPersp);
|
|
8224
8283
|
H = H ? hMul(r.H, H) : r.H;
|
|
8225
8284
|
if (r.projective) projective = true;
|
|
8226
8285
|
}
|
|
@@ -8656,6 +8715,7 @@ var TXA = (function() {
|
|
|
8656
8715
|
maskedXf: maskedXf2,
|
|
8657
8716
|
unitTransform: unitTransform2,
|
|
8658
8717
|
unitTransform3d: unitTransform3d2,
|
|
8718
|
+
unitTransformCam: unitTransformCam2,
|
|
8659
8719
|
m4Mul: m4Mul2,
|
|
8660
8720
|
applyH: applyH2,
|
|
8661
8721
|
hToCanvas: hToCanvas2,
|
|
@@ -8730,6 +8790,7 @@ var {
|
|
|
8730
8790
|
maskedXf,
|
|
8731
8791
|
unitTransform,
|
|
8732
8792
|
unitTransform3d,
|
|
8793
|
+
unitTransformCam,
|
|
8733
8794
|
m4Mul,
|
|
8734
8795
|
applyH,
|
|
8735
8796
|
hToCanvas,
|
|
@@ -8765,6 +8826,7 @@ var factory = function(T) {
|
|
|
8765
8826
|
const CATEGORIES2 = [
|
|
8766
8827
|
{ id: "sweep", name: "Sweep Reveal" },
|
|
8767
8828
|
{ id: "kinetic", name: "Kinetic" },
|
|
8829
|
+
{ id: "depth", name: "Depth" },
|
|
8768
8830
|
{ id: "shape", name: "Type on Shape" },
|
|
8769
8831
|
{ id: "emphasis", name: "Emphasis" },
|
|
8770
8832
|
{ id: "loop", name: "Loop" },
|
|
@@ -9639,6 +9701,234 @@ var factory = function(T) {
|
|
|
9639
9701
|
}
|
|
9640
9702
|
]
|
|
9641
9703
|
},
|
|
9704
|
+
// The two after it film the text with ONE camera (layer: { camera: 'layer' }): every
|
|
9705
|
+
// letter at its own depth, all converging to one vanishing point, nearer covering
|
|
9706
|
+
// farther — what Position Z and a shared Rotate 3D are for.
|
|
9707
|
+
// Title Push 3D = MotionFast template5 "Push 1 — 3D Pop" (the title-push-3d template):
|
|
9708
|
+
// its stage's 1000 px CSS perspective on 4 vw type is a layer camera ~13 em away. Each
|
|
9709
|
+
// word pops in over 0.8 s on GSAP's power1.out, 0.2–0.4 s after the one before (0.2 s —
|
|
9710
|
+
// its shortest — and 0.75 s here, so seven words land by 2 s like every Kinetic preset; the modal's Speed
|
|
9711
|
+
// stretches both); here it flies in from deep behind the text and tips up flat, and Back
|
|
9712
|
+
// Out carries it a little past the line toward the camera before it settles. The
|
|
9713
|
+
// template slides its container left as each word lands so the words shown stay
|
|
9714
|
+
// centred; that needs the shown words' width, so here the whole text makes one Sine Out
|
|
9715
|
+
// glide left across the arrivals. At the end the words rush past the camera, last first.
|
|
9716
|
+
{
|
|
9717
|
+
id: "title-push-3d",
|
|
9718
|
+
name: "Title Push 3D",
|
|
9719
|
+
cat: "kinetic",
|
|
9720
|
+
sides: "inOut",
|
|
9721
|
+
anchor: "drive",
|
|
9722
|
+
sample: "Kinetic type with depth",
|
|
9723
|
+
tile: KIN_TILE,
|
|
9724
|
+
labLayer: { size: 110, weight: 800 },
|
|
9725
|
+
layer: { camera: "layer", perspective: em(13) },
|
|
9726
|
+
description: "Words fly in from depth one by one, overshoot toward the camera and settle as the line glides over to make room; at the end they rush past the camera",
|
|
9727
|
+
animators: [
|
|
9728
|
+
{
|
|
9729
|
+
name: "Fly in",
|
|
9730
|
+
range: kIn("words", "word", { stagger: 0.2, duration: 0.75, curve: "Back Out" }),
|
|
9731
|
+
effects: [["position", { x: 0, y: em(0.35), z: em(-26) }], ["rotate3d", { x: -50, y: 0, perspective: em(6) }], ["opacity", { opacity: 0 }]]
|
|
9732
|
+
},
|
|
9733
|
+
{
|
|
9734
|
+
name: "Push",
|
|
9735
|
+
range: kIn("lines", "all", { stagger: 0, duration: 1.6, curve: "Sine Out" }),
|
|
9736
|
+
effects: [["position", { x: em(2.4), y: 0 }]]
|
|
9737
|
+
},
|
|
9738
|
+
{
|
|
9739
|
+
name: "Rush out",
|
|
9740
|
+
range: kOut("words", "word", { order: "backward", stagger: 0.05, duration: 0.55, curve: "Ease In" }),
|
|
9741
|
+
effects: [["position", { x: 0, y: 0, z: em(9) }], ["opacity", { opacity: 0 }]]
|
|
9742
|
+
}
|
|
9743
|
+
]
|
|
9744
|
+
},
|
|
9745
|
+
// Perspective Type = the Kinetic Perspective Type template (kinetic-perspective-type,
|
|
9746
|
+
// look-dev html/kinetic-perspective-type.html): the whole text lies on a plane tilted
|
|
9747
|
+
// toward the camera (X 32°, Y 25°, Z −6°, 150 %; its focal 520 px on 250 px type is a
|
|
9748
|
+
// layer camera ~2 em away — 3 em here, the same keystone on lines ~4 em wide), the lines
|
|
9749
|
+
// drive in one after another (0.5 s apart, 0.5 s each, AE Smooth), rising from 1.45 line
|
|
9750
|
+
// heights below and growing from 55 %, and at 1.2 s the plane swings flat (0.7 s,
|
|
9751
|
+
// Natural: a Time entrance with a 1.2 s delay, so no keys). Not here: each arrival
|
|
9752
|
+
// shoving the earlier lines up the plane (a line cannot move on its neighbours' clocks),
|
|
9753
|
+
// the pushed word and its reflow, and the template's own motion blur. At the end the
|
|
9754
|
+
// lines sink back into depth.
|
|
9755
|
+
{
|
|
9756
|
+
id: "perspective-type",
|
|
9757
|
+
name: "Perspective Type",
|
|
9758
|
+
cat: "kinetic",
|
|
9759
|
+
sides: "inOut",
|
|
9760
|
+
anchor: "drive",
|
|
9761
|
+
sample: "Big\nbold\nideas",
|
|
9762
|
+
tile: KIN_TILE,
|
|
9763
|
+
// Centred like the template's block (its lines are left-aligned inside a block it
|
|
9764
|
+
// centres in frame): a left-aligned layer swings its tilted plane off the left edge.
|
|
9765
|
+
labLayer: { size: 160, weight: 800, lineHeight: 0.95 },
|
|
9766
|
+
layer: { camera: "layer", perspective: em(3) },
|
|
9767
|
+
description: "Lines drive in one after another on a plane tilted toward the camera, which then swings flat; at the end they sink back into depth",
|
|
9768
|
+
animators: [
|
|
9769
|
+
{
|
|
9770
|
+
name: "Lines in",
|
|
9771
|
+
range: kIn("lines", "line", { stagger: 0.5, duration: 0.5, curve: "AE Smooth" }),
|
|
9772
|
+
effects: [["position", { x: 0, y: em(1.4) }], ["scale", { scale: 55 }], ["opacity", { opacity: 0 }]]
|
|
9773
|
+
},
|
|
9774
|
+
{
|
|
9775
|
+
name: "Tilted plane",
|
|
9776
|
+
range: kIn("lines", "all", { delay: 1.2, stagger: 0, duration: 0.7, curve: "Natural" }),
|
|
9777
|
+
effects: [["rotate3d", { x: 32, y: 25, perspective: em(3) }], ["rotation", { angle: -6 }], ["scale", { scale: 150 }], ["position", { x: em(0.16), y: em(-0.04) }]]
|
|
9778
|
+
},
|
|
9779
|
+
{
|
|
9780
|
+
name: "Sink out",
|
|
9781
|
+
range: kOut("lines", "line", { order: "backward", stagger: 0.08, duration: 0.5, curve: "Ease In" }),
|
|
9782
|
+
effects: [["position", { x: 0, y: 0, z: em(-8) }], ["opacity", { opacity: 0 }]]
|
|
9783
|
+
}
|
|
9784
|
+
]
|
|
9785
|
+
},
|
|
9786
|
+
// ── Depth ──
|
|
9787
|
+
// Position Z under ONE camera (layer: { camera: 'layer', perspective }): a letter
|
|
9788
|
+
// moved in depth really goes there — nearer grows and moves out from the centre,
|
|
9789
|
+
// farther shrinks toward one vanishing point, nearer covers farther. Picked by the
|
|
9790
|
+
// owner from the Text in Depth lab (html/text-depth-lab, 2026-11-07).
|
|
9791
|
+
{
|
|
9792
|
+
id: "rack-focus",
|
|
9793
|
+
name: "Rack Focus",
|
|
9794
|
+
cat: "depth",
|
|
9795
|
+
sides: "inOut",
|
|
9796
|
+
anchor: "start",
|
|
9797
|
+
sample: "Pull focus to every word",
|
|
9798
|
+
tile: "Pull focus\nto every word",
|
|
9799
|
+
labLayer: { size: 130, weight: 600 },
|
|
9800
|
+
layer: { camera: "layer", perspective: em(14) },
|
|
9801
|
+
description: "Words come forward out of a soft background and snap into focus; at the end they pass the focus toward you",
|
|
9802
|
+
animators: [
|
|
9803
|
+
{
|
|
9804
|
+
name: "Focus in",
|
|
9805
|
+
range: kIn("words", "word", { stagger: 0.12, duration: 1.3, curve: "Expo Out" }),
|
|
9806
|
+
effects: [["position", { x: 0, y: 0, z: em(-9) }], ["blur", { amount: em(0.12) }], ["opacity", { opacity: 0 }]]
|
|
9807
|
+
},
|
|
9808
|
+
{
|
|
9809
|
+
name: "Focus out",
|
|
9810
|
+
range: kOut("words", "word", { stagger: 0.05, duration: 0.6, curve: "Ease In" }),
|
|
9811
|
+
effects: [["position", { x: 0, y: 0, z: em(5) }], ["blur", { amount: em(0.12) }], ["opacity", { opacity: 0 }]]
|
|
9812
|
+
}
|
|
9813
|
+
]
|
|
9814
|
+
},
|
|
9815
|
+
// Pivot Y 100: each letter turns on a hinge at its feet; under one camera the letters lie
|
|
9816
|
+
// on a real floor that recedes to the vanishing point.
|
|
9817
|
+
{
|
|
9818
|
+
id: "stand-up",
|
|
9819
|
+
name: "Stand Up",
|
|
9820
|
+
cat: "depth",
|
|
9821
|
+
sides: "inOut",
|
|
9822
|
+
anchor: "start",
|
|
9823
|
+
sample: "Stand up and be seen",
|
|
9824
|
+
tile: "Stand up\nand be seen",
|
|
9825
|
+
labLayer: { size: 150, weight: 800 },
|
|
9826
|
+
layer: { camera: "layer", perspective: em(6), pivotY: 100 },
|
|
9827
|
+
description: "Letters slide forward across the floor and stand up one after another, hinged at their feet; at the end they fall forward toward you",
|
|
9828
|
+
animators: [
|
|
9829
|
+
{
|
|
9830
|
+
name: "Stand",
|
|
9831
|
+
range: kIn("charsNoSpace", "char", { stagger: 0.04, duration: 0.9, curve: "Cubic Out" }),
|
|
9832
|
+
effects: [["position", { x: 0, y: 0, z: em(-3) }], ["rotate3d", { x: 90, y: 0, perspective: em(6) }], ["opacity", { opacity: 0 }]]
|
|
9833
|
+
},
|
|
9834
|
+
{
|
|
9835
|
+
name: "Fall",
|
|
9836
|
+
range: kOut("charsNoSpace", "char", { stagger: 0.025, duration: 0.6, curve: "Ease In" }),
|
|
9837
|
+
effects: [["position", { x: 0, y: 0, z: em(1.5) }], ["rotate3d", { x: -90, y: 0, perspective: em(6) }], ["opacity", { opacity: 0 }]]
|
|
9838
|
+
}
|
|
9839
|
+
]
|
|
9840
|
+
},
|
|
9841
|
+
// Words rest at their own depths, only behind the plate. One camera pulls a far word toward
|
|
9842
|
+
// the centre, so a nearer word just outside it would cover it: the depths form a bowl (the
|
|
9843
|
+
// middle words deepest, Shape Smooth) with a fixed scatter added (Wiggle at 0 / s), so an
|
|
9844
|
+
// outer neighbour is never much farther. Drift is the camera move, spread over the whole
|
|
9845
|
+
// layer: every word slides by the same amount past the fixed camera, which films exactly
|
|
9846
|
+
// like a slow pan and push-in — the nearer words travel farther (parallax). The bowl runs
|
|
9847
|
+
// in reading order, so it is written for ONE line: on two, the deep middle words are the
|
|
9848
|
+
// rows' outer ends and get pulled into their neighbours.
|
|
9849
|
+
{
|
|
9850
|
+
id: "strata",
|
|
9851
|
+
name: "Strata",
|
|
9852
|
+
cat: "depth",
|
|
9853
|
+
sides: "inOut",
|
|
9854
|
+
anchor: "start",
|
|
9855
|
+
sample: "Each word at its own depth",
|
|
9856
|
+
tile: "Layers of depth",
|
|
9857
|
+
labLayer: { size: 108, weight: 800 },
|
|
9858
|
+
layer: { camera: "layer", perspective: em(10) },
|
|
9859
|
+
description: "Words float in and rest at their own depths while a slow camera drift slides them past each other; at the end they fall back",
|
|
9860
|
+
animators: [
|
|
9861
|
+
{
|
|
9862
|
+
name: "Layers",
|
|
9863
|
+
range: { basedOn: "words", around: "word", shape: "smooth" },
|
|
9864
|
+
wiggle: { mode: "add", min: 0, max: 40, rate: 0, corr: 0, seed: 8 },
|
|
9865
|
+
effects: [["position", { x: 0, y: 0, z: em(-7) }]]
|
|
9866
|
+
},
|
|
9867
|
+
{
|
|
9868
|
+
name: "Drift",
|
|
9869
|
+
anchor: "clip",
|
|
9870
|
+
range: { basedOn: "words", around: "word" },
|
|
9871
|
+
effects: [["position", { x: em(0.7), y: 0, z: em(-1) }]],
|
|
9872
|
+
keys: { amount: [[0, 100, "Sine Smooth"], [100, -100]] }
|
|
9873
|
+
},
|
|
9874
|
+
{
|
|
9875
|
+
name: "Float in",
|
|
9876
|
+
range: kIn("words", "word", { stagger: 0.12, duration: 1.2, curve: "Cubic Out" }),
|
|
9877
|
+
effects: [["position", { x: 0, y: 0, z: em(-12) }], ["opacity", { opacity: 0 }]]
|
|
9878
|
+
},
|
|
9879
|
+
{
|
|
9880
|
+
name: "Fall back",
|
|
9881
|
+
range: kOut("words", "word", { order: "backward", stagger: 0.05, duration: 0.6, curve: "Ease In" }),
|
|
9882
|
+
effects: [["position", { x: 0, y: 0, z: em(-12) }], ["opacity", { opacity: 0 }]]
|
|
9883
|
+
}
|
|
9884
|
+
]
|
|
9885
|
+
},
|
|
9886
|
+
// Fill folds top to bottom: Sit back greys every word, the Spotlight's white wins at its turn.
|
|
9887
|
+
{
|
|
9888
|
+
id: "spotlight",
|
|
9889
|
+
name: "Spotlight",
|
|
9890
|
+
cat: "depth",
|
|
9891
|
+
sides: "once",
|
|
9892
|
+
anchor: "start",
|
|
9893
|
+
sample: "Every word takes the stage",
|
|
9894
|
+
tile: "Every word\ntakes the stage",
|
|
9895
|
+
labLayer: { size: 130, weight: 800 },
|
|
9896
|
+
layer: { camera: "layer", perspective: em(12) },
|
|
9897
|
+
description: "Words wait set back and dim; each in turn steps forward into the light, and the last one keeps it",
|
|
9898
|
+
animators: [
|
|
9899
|
+
{
|
|
9900
|
+
name: "Sit back",
|
|
9901
|
+
range: { basedOn: "words", around: "word" },
|
|
9902
|
+
effects: [["position", { x: 0, y: 0, z: em(-1.6) }], ["fill", { color: "#5d636b" }]]
|
|
9903
|
+
},
|
|
9904
|
+
{
|
|
9905
|
+
name: "Spotlight",
|
|
9906
|
+
range: { drive: "time", animate: "active", basedOn: "words", around: "word", delay: 0.3, stagger: 0.55, duration: 0.35, hold: 0, fadeOut: 0.3, keepLast: true, curve: "Cubic Out" },
|
|
9907
|
+
effects: [["position", { x: 0, y: 0, z: em(1.6) }], ["fill", { color: "#ffffff" }]]
|
|
9908
|
+
}
|
|
9909
|
+
]
|
|
9910
|
+
},
|
|
9911
|
+
// A Time cascade from the centre, looping: each letter swells toward the camera on its
|
|
9912
|
+
// turn and settles back, so a ring of depth spreads outward like a ripple on water.
|
|
9913
|
+
{
|
|
9914
|
+
id: "ripple",
|
|
9915
|
+
name: "Ripple",
|
|
9916
|
+
cat: "depth",
|
|
9917
|
+
sides: "loop",
|
|
9918
|
+
anchor: "none",
|
|
9919
|
+
sample: "Ripples from the centre",
|
|
9920
|
+
tile: "Ripples from\nthe centre",
|
|
9921
|
+
labLayer: { size: 130, weight: 700 },
|
|
9922
|
+
layer: { camera: "layer", perspective: em(10) },
|
|
9923
|
+
description: "Rings of depth spread from the centre of the text to its edges, over and over, like ripples on still water",
|
|
9924
|
+
animators: [
|
|
9925
|
+
{
|
|
9926
|
+
name: "Ripple",
|
|
9927
|
+
range: { drive: "time", animate: "active", basedOn: "charsNoSpace", around: "char", order: "center", delay: 0.2, stagger: 0.1, duration: 0.14, hold: 0, fadeOut: 0.3, loop: true, curve: "Sine Out" },
|
|
9928
|
+
effects: [["position", { x: 0, y: 0, z: em(1.1) }], ["fill", { color: "#8fe3ff", mix: 65 }]]
|
|
9929
|
+
}
|
|
9930
|
+
]
|
|
9931
|
+
},
|
|
9642
9932
|
// ── Type on Shape ──
|
|
9643
9933
|
// The typing animator carries a Highlight box set to Behind ▸ Revealed, so the
|
|
9644
9934
|
// shape grows with the letters on one set of keys. Per line = a bar per row,
|
|
@@ -10233,6 +10523,8 @@ var factory = function(T) {
|
|
|
10233
10523
|
return span;
|
|
10234
10524
|
}
|
|
10235
10525
|
const isTimed2 = (p) => p.anchor === "start" || p.anchor === "end";
|
|
10526
|
+
const keyedAt = (p, anchor) => p.animators.some((a) => a.keys && anchorOf2(p, a) === anchor);
|
|
10527
|
+
const keyedBothEnds2 = (p) => keyedAt(p, "start") && keyedAt(p, "end");
|
|
10236
10528
|
const resolveEm2 = (v, fontPx) => {
|
|
10237
10529
|
if (!v || typeof v !== "object" || Array.isArray(v)) return v;
|
|
10238
10530
|
let n2 = (Number(v.px) || 0) + (Number(v.em) || 0) * fontPx;
|
|
@@ -10252,8 +10544,9 @@ var factory = function(T) {
|
|
|
10252
10544
|
};
|
|
10253
10545
|
const span = presetSpan2(p);
|
|
10254
10546
|
const clip = Math.max(0.1, clipSec);
|
|
10547
|
+
const room = keyedBothEnds2(p) ? clip / 2 : clip;
|
|
10255
10548
|
let scale = span > 0 && duration > 0 ? duration / span : 1;
|
|
10256
|
-
if (span > 0 && span * scale >
|
|
10549
|
+
if (span > 0 && span * scale > room) scale = room / span;
|
|
10257
10550
|
const pos = (t, anchor) => {
|
|
10258
10551
|
let v;
|
|
10259
10552
|
if (anchor === "clip") v = t;
|
|
@@ -10299,7 +10592,8 @@ var factory = function(T) {
|
|
|
10299
10592
|
return {
|
|
10300
10593
|
animators: T.sanitizeAnimators(animators),
|
|
10301
10594
|
tracks,
|
|
10302
|
-
|
|
10595
|
+
// Layer fields may be sizes too (a shared camera's Perspective in em).
|
|
10596
|
+
layer: { ...T.defaultsOf(T.LAYER_FIELDS), ...Object.fromEntries(Object.entries(p.layer || {}).map(([k, v]) => [k, resolveEm2(v, fontPx)])) }
|
|
10303
10597
|
};
|
|
10304
10598
|
}
|
|
10305
10599
|
function fakeVoice2(text, { start = 0.35, rate = 1 } = {}) {
|
|
@@ -10314,22 +10608,106 @@ var factory = function(T) {
|
|
|
10314
10608
|
}
|
|
10315
10609
|
const usesVoice2 = (p) => !!(p && Array.isArray(p.animators) && p.animators.some((a) => a.range && a.range.drive === "voice"));
|
|
10316
10610
|
const hasExit2 = (p) => !!(p && Array.isArray(p.animators) && p.animators.some((a) => a.range && (a.range.animate === "out" || a.range.animate === "inOut")));
|
|
10611
|
+
const exitStagger = (basedOn) => basedOn === "words" ? 0.04 : basedOn === "lines" ? 0.08 : 0.015;
|
|
10317
10612
|
function withExit2(p) {
|
|
10318
|
-
if (!p || p.sides === "out" || p.sides === "loop" || hasExit2(p)) return p;
|
|
10613
|
+
if (!p || p.sides === "out" || p.sides === "inOut" || p.sides === "loop" || hasExit2(p)) return p;
|
|
10319
10614
|
const first = p.animators[0] && p.animators[0].range;
|
|
10320
10615
|
const basedOn = first && first.basedOn || "chars";
|
|
10321
|
-
const stagger = basedOn === "words" ? 0.04 : basedOn === "lines" ? 0.08 : 0.015;
|
|
10322
10616
|
return {
|
|
10323
10617
|
...p,
|
|
10324
10618
|
sides: "inOut",
|
|
10325
10619
|
animators: [...p.animators, {
|
|
10326
10620
|
name: "Out",
|
|
10327
|
-
range: { drive: "time", animate: "out", basedOn, stagger, duration: 0.3, curve: "Ease In" },
|
|
10621
|
+
range: { drive: "time", animate: "out", basedOn, stagger: exitStagger(basedOn), duration: 0.3, curve: "Ease In" },
|
|
10328
10622
|
effects: [["opacity", { opacity: 0 }]]
|
|
10329
10623
|
}]
|
|
10330
10624
|
};
|
|
10331
10625
|
}
|
|
10332
|
-
const
|
|
10626
|
+
const REVERSE_CURVE = {
|
|
10627
|
+
"Ease In": "Ease Out",
|
|
10628
|
+
"Ease Out": "Ease In",
|
|
10629
|
+
"Cubic In": "Cubic Out",
|
|
10630
|
+
"Cubic Out": "Cubic In",
|
|
10631
|
+
"Sine In": "Sine Out",
|
|
10632
|
+
"Sine Out": "Sine In",
|
|
10633
|
+
"Expo In": "Expo Out",
|
|
10634
|
+
"Expo Out": "Expo In",
|
|
10635
|
+
"Back In": "Back Out",
|
|
10636
|
+
"Back Out": "Back In",
|
|
10637
|
+
"AE In (arrive)": "AE Out (leave)",
|
|
10638
|
+
"AE Out (leave)": "AE In (arrive)",
|
|
10639
|
+
Overshoot: "Anticipate",
|
|
10640
|
+
"Smooth Overshoot": "Anticipate",
|
|
10641
|
+
Elastic: "Anticipate",
|
|
10642
|
+
Anticipate: "Overshoot",
|
|
10643
|
+
"Bounce Out": "Ease In",
|
|
10644
|
+
Power: "Cubic In",
|
|
10645
|
+
"Slow Down": "Expo In",
|
|
10646
|
+
"Apple Out": "Ease In"
|
|
10647
|
+
};
|
|
10648
|
+
const reverseCurve = (c) => REVERSE_CURVE[c] || c;
|
|
10649
|
+
const REVERSE_ORDER = { forward: "backward", backward: "forward", center: "edges", edges: "center" };
|
|
10650
|
+
const driven = (r) => r.drive === "time" || r.drive === "voice";
|
|
10651
|
+
function sideOf(p, a) {
|
|
10652
|
+
const r = a.range || {};
|
|
10653
|
+
if (r.loop || a.wiggle) return null;
|
|
10654
|
+
if (driven(r)) {
|
|
10655
|
+
const animate = r.animate || "in";
|
|
10656
|
+
return animate === "in" || animate === "out" ? animate : null;
|
|
10657
|
+
}
|
|
10658
|
+
if (!a.keys || !Object.keys(a.keys).length) return null;
|
|
10659
|
+
const anchor = anchorOf2(p, a);
|
|
10660
|
+
return anchor === "start" ? "in" : anchor === "end" ? "out" : null;
|
|
10661
|
+
}
|
|
10662
|
+
function sideVersions2(p) {
|
|
10663
|
+
if (!p || p.sides !== "in" && p.sides !== "out") return null;
|
|
10664
|
+
const sides = p.animators.map((a) => sideOf(p, a));
|
|
10665
|
+
return sides.includes(p.sides) && !sides.includes(p.sides === "in" ? "out" : "in") ? ["in", "out", "inOut"] : null;
|
|
10666
|
+
}
|
|
10667
|
+
function mirror(p, a) {
|
|
10668
|
+
const toOut = sideOf(p, a) === "in";
|
|
10669
|
+
const word = toOut ? "out" : "in";
|
|
10670
|
+
const m = new RegExp(`\\b${toOut ? "in" : "out"}$`, "i").exec(a.name);
|
|
10671
|
+
const name = m ? a.name.slice(0, m.index) + (m[0][0] === m[0][0].toUpperCase() ? word[0].toUpperCase() + word.slice(1) : word) : `${a.name} ${word}`;
|
|
10672
|
+
const r = a.range || {};
|
|
10673
|
+
if (driven(r)) {
|
|
10674
|
+
const range = { ...r, animate: word, order: REVERSE_ORDER[r.order || "forward"] || r.order, curve: reverseCurve(r.curve || "Ease Out") };
|
|
10675
|
+
if (r.curve === "Custom" && Array.isArray(r.bezier) && r.bezier.length === 4) {
|
|
10676
|
+
const [x1, y1, x2, y2] = r.bezier;
|
|
10677
|
+
range.bezier = [1 - x2, 1 - y2, 1 - x1, 1 - y1];
|
|
10678
|
+
}
|
|
10679
|
+
if (r.drive === "voice") {
|
|
10680
|
+
range.drive = "time";
|
|
10681
|
+
if (r.stagger == null) range.stagger = exitStagger(r.basedOn);
|
|
10682
|
+
}
|
|
10683
|
+
return { ...a, name, range };
|
|
10684
|
+
}
|
|
10685
|
+
const keys = {};
|
|
10686
|
+
for (const [chan, ks] of Object.entries(a.keys)) {
|
|
10687
|
+
const n2 = ks.length;
|
|
10688
|
+
keys[chan] = ks.map((_, i) => {
|
|
10689
|
+
const k = ks[n2 - 1 - i];
|
|
10690
|
+
const into = ks[n2 - 2 - i];
|
|
10691
|
+
const t = -k[0] || 0;
|
|
10692
|
+
return into ? [t, k[1], reverseCurve(into[2] || "Smooth")] : [t, k[1]];
|
|
10693
|
+
});
|
|
10694
|
+
}
|
|
10695
|
+
return { ...a, name, anchor: toOut ? "end" : "start", keys };
|
|
10696
|
+
}
|
|
10697
|
+
function withSides2(p, sides) {
|
|
10698
|
+
const versions = sideVersions2(p);
|
|
10699
|
+
if (!versions || !versions.includes(sides) || sides === p.sides) return p;
|
|
10700
|
+
const flip = { start: "end", end: "start" };
|
|
10701
|
+
const pinned = (a) => a.keys && !a.anchor ? { ...a, anchor: anchorOf2(p, a) } : a;
|
|
10702
|
+
const moving = (a) => sideOf(p, a) === p.sides;
|
|
10703
|
+
if (sides === "inOut") {
|
|
10704
|
+
const back = p.animators.filter(moving).map((a) => mirror(p, a));
|
|
10705
|
+
const own = p.animators.map(pinned);
|
|
10706
|
+
return { ...p, sides, anchor: p.anchor === "end" ? "start" : p.anchor, animators: p.sides === "in" ? [...own, ...back] : [...back, ...own] };
|
|
10707
|
+
}
|
|
10708
|
+
return { ...p, sides, anchor: flip[p.anchor] || p.anchor, animators: p.animators.map((a) => moving(a) ? mirror(p, a) : pinned(a)) };
|
|
10709
|
+
}
|
|
10710
|
+
const api = { CATEGORIES: CATEGORIES2, PRESETS: PRESETS3, anchorOf: anchorOf2, presetSpan: presetSpan2, isTimed: isTimed2, keyedBothEnds: keyedBothEnds2, resolveEm: resolveEm2, build: build2, fakeVoice: fakeVoice2, usesVoice: usesVoice2, hasExit: hasExit2, withExit: withExit2, sideVersions: sideVersions2, withSides: withSides2 };
|
|
10333
10711
|
return api;
|
|
10334
10712
|
};
|
|
10335
10713
|
var PRESETS_API = factory(textAnimators_default);
|
|
@@ -10339,12 +10717,15 @@ var {
|
|
|
10339
10717
|
anchorOf,
|
|
10340
10718
|
presetSpan,
|
|
10341
10719
|
isTimed,
|
|
10720
|
+
keyedBothEnds,
|
|
10342
10721
|
resolveEm,
|
|
10343
10722
|
build,
|
|
10344
10723
|
fakeVoice,
|
|
10345
10724
|
usesVoice,
|
|
10346
10725
|
hasExit,
|
|
10347
|
-
withExit
|
|
10726
|
+
withExit,
|
|
10727
|
+
sideVersions,
|
|
10728
|
+
withSides
|
|
10348
10729
|
} = PRESETS_API;
|
|
10349
10730
|
|
|
10350
10731
|
// src/components/videoEditor/textAnimatorClassicPresets.js
|
|
@@ -12584,13 +12965,21 @@ function presetPatch(item, preset, { clipSec, fontPx, duration = null, speed = 1
|
|
|
12584
12965
|
});
|
|
12585
12966
|
list = [...existing, ...renamed].slice(0, MAX_ANIMATORS);
|
|
12586
12967
|
}
|
|
12968
|
+
const shared = built.layer.camera && built.layer.camera !== "unit";
|
|
12969
|
+
const camera = {
|
|
12970
|
+
textAnimatorCamera: shared ? built.layer.camera : null,
|
|
12971
|
+
textAnimatorPerspective: shared && built.layer.perspective > 0 ? built.layer.perspective : null
|
|
12972
|
+
};
|
|
12973
|
+
const hasCamera = item?.textAnimatorCamera === "layer" || item?.textAnimatorCamera === "project";
|
|
12587
12974
|
return {
|
|
12588
12975
|
textAnimators: list,
|
|
12589
12976
|
propertyKeyframes: pk,
|
|
12590
12977
|
...adding ? null : {
|
|
12591
12978
|
textAnimatorPivotX: built.layer.pivotX,
|
|
12592
|
-
textAnimatorPivotY: built.layer.pivotY
|
|
12593
|
-
|
|
12979
|
+
textAnimatorPivotY: built.layer.pivotY,
|
|
12980
|
+
...camera
|
|
12981
|
+
},
|
|
12982
|
+
...adding && shared && !hasCamera ? camera : null
|
|
12594
12983
|
};
|
|
12595
12984
|
}
|
|
12596
12985
|
|
|
@@ -12712,6 +13101,8 @@ var LayerTransitionType = {
|
|
|
12712
13101
|
* reveals it; its exit maps the layer and dissolves the map to nothing.
|
|
12713
13102
|
* Pixel Blocks block the layer in (or out) over what is under it; a colour
|
|
12714
13103
|
* wipe fills only the layer's own pixels.
|
|
13104
|
+
* The Zoom Ruler's entrance opens the layer inside the growing ruler box over
|
|
13105
|
+
* what is under it; its exit keeps the layer outside the box until it clears.
|
|
12715
13106
|
*
|
|
12716
13107
|
* The options live in transitionIn/OutOptions, resolved through the SAME
|
|
12717
13108
|
* ADVANCED_TRANSITION_PRESETS descriptor the cross slot reads.
|
|
@@ -12723,6 +13114,7 @@ var LayerTransitionType = {
|
|
|
12723
13114
|
GLOW_STACK: "Gradient Glow Stack",
|
|
12724
13115
|
GRADIENT_MAP: "Gradient Map",
|
|
12725
13116
|
PIXEL_BLOCKS: "Pixel Blocks",
|
|
13117
|
+
ZOOM_RULER: "Zoom Ruler",
|
|
12726
13118
|
CUSTOM: "Custom"
|
|
12727
13119
|
};
|
|
12728
13120
|
var ADVANCED_LAYER_TRANSITIONS = Object.freeze([
|
|
@@ -12732,18 +13124,26 @@ var ADVANCED_LAYER_TRANSITIONS = Object.freeze([
|
|
|
12732
13124
|
LayerTransitionType.CAMERA_WHIP,
|
|
12733
13125
|
LayerTransitionType.GLOW_STACK,
|
|
12734
13126
|
LayerTransitionType.GRADIENT_MAP,
|
|
12735
|
-
LayerTransitionType.PIXEL_BLOCKS
|
|
13127
|
+
LayerTransitionType.PIXEL_BLOCKS,
|
|
13128
|
+
LayerTransitionType.ZOOM_RULER
|
|
12736
13129
|
]);
|
|
12737
13130
|
var TransitionEasing = {
|
|
13131
|
+
// The order is the easing picker's (EasingPicker.jsx GROUPS) so every plain dropdown
|
|
13132
|
+
// lists the curves the same way: Basic, After Effects, Expressive, springs, families.
|
|
12738
13133
|
LINEAR: "Linear",
|
|
12739
13134
|
EASE_IN: "Ease In",
|
|
12740
13135
|
EASE_OUT: "Ease Out",
|
|
12741
13136
|
EASE_IN_OUT: "Smooth",
|
|
12742
|
-
//
|
|
12743
|
-
//
|
|
12744
|
-
//
|
|
12745
|
-
|
|
12746
|
-
|
|
13137
|
+
// Expressive curves ported from the legacy MotionFast template easing catalogue
|
|
13138
|
+
// (greenlight-render-templates/templates/common/easings.ts). Natural and Slow Down
|
|
13139
|
+
// are cubic beziers; Overshoot/Impulse/Swing are sampled spring curves.
|
|
13140
|
+
NATURAL: "Natural",
|
|
13141
|
+
// From the "Easing Lab" board (2026-11-01), both plain cubic beziers. Punch winds up
|
|
13142
|
+
// slowly, hits hard and trails a long tail (scene changes on the beat, a logo hit, a
|
|
13143
|
+
// screen swipe); nothing else in the list is within 0.23 of it. Apple Out starts at
|
|
13144
|
+
// full speed and spends most of the move in a soft stop (UI panels, drawers, a
|
|
13145
|
+
// camera landing).
|
|
13146
|
+
PUNCH: "Punch",
|
|
12747
13147
|
// After Effects-style bezier curves — same curves as MotionFast's aeIn/aeSmooth/aeOut.
|
|
12748
13148
|
// AE names a curve by the side of the key its influence sits on, so the shapes read
|
|
12749
13149
|
// backwards to the CSS names: AE In starts fast and brakes into the key (an arrival),
|
|
@@ -12751,42 +13151,37 @@ var TransitionEasing = {
|
|
|
12751
13151
|
AE_IN: "AE In (arrive)",
|
|
12752
13152
|
AE_SMOOTH: "AE Smooth",
|
|
12753
13153
|
AE_OUT: "AE Out (leave)",
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
EXPO_IN: "Expo In",
|
|
12760
|
-
EXPO_OUT: "Expo Out",
|
|
12761
|
-
EXPO_IN_OUT: "Expo Smooth",
|
|
12762
|
-
// A hard, decisive stop (GSAP power4.out; drawn as cubic-bezier(0.23, 1, 0.32, 1)).
|
|
12763
|
-
POWER: "Power",
|
|
12764
|
-
// From the "Easing Lab" board (2026-11-01), both plain cubic beziers. Punch winds up
|
|
12765
|
-
// slowly, hits hard and trails a long tail (scene changes on the beat, a logo hit, a
|
|
12766
|
-
// screen swipe); nothing else in the list is within 0.23 of it. Apple Out starts at
|
|
12767
|
-
// full speed and spends most of the move in a soft stop (UI panels, drawers, a
|
|
12768
|
-
// camera landing).
|
|
12769
|
-
PUNCH: "Punch",
|
|
13154
|
+
// The owner's own curves from the "Easing study" board (2026-10-16): Smooth Glide arrives fast, glides slowly
|
|
13155
|
+
// through the middle and leaves fast (its "Glide Easing" layer); Glide Easing Stop jumps in, all but stops,
|
|
13156
|
+
// then goes (its "Glide Easing Stop" layer). Both are cubic beziers — see EASING_BEZIER.
|
|
13157
|
+
SMOOTH_GLIDE: "Smooth Glide",
|
|
13158
|
+
GLIDE_STOP: "Glide Easing Stop",
|
|
12770
13159
|
APPLE_OUT: "Apple Out",
|
|
13160
|
+
SLOW_DOWN: "Slow Down",
|
|
13161
|
+
OVERSHOOT: "Overshoot",
|
|
13162
|
+
IMPULSE: "Impulse",
|
|
13163
|
+
SWING: "Swing",
|
|
13164
|
+
// Winds back first, then snaps through to the end: an exit. Sampled.
|
|
13165
|
+
ANTICIPATE: "Anticipate",
|
|
12771
13166
|
BACK_IN: "Back In",
|
|
12772
13167
|
BACK_OUT: "Back Out",
|
|
12773
13168
|
BACK_IN_OUT: "Back Smooth",
|
|
12774
13169
|
BOUNCE_OUT: "Bounce Out",
|
|
12775
13170
|
ELASTIC_OUT: "Elastic",
|
|
12776
|
-
// Expressive curves ported from the legacy MotionFast template easing catalogue
|
|
12777
|
-
// (greenlight-render-templates/templates/common/easings.ts). Natural and Slow Down
|
|
12778
|
-
// are cubic beziers; Overshoot/Impulse/Swing are sampled spring curves.
|
|
12779
|
-
NATURAL: "Natural",
|
|
12780
|
-
SLOW_DOWN: "Slow Down",
|
|
12781
|
-
OVERSHOOT: "Overshoot",
|
|
12782
|
-
IMPULSE: "Impulse",
|
|
12783
|
-
SWING: "Swing",
|
|
12784
13171
|
// The smooth sibling of Overshoot/Elastic: the same springy character, but it
|
|
12785
13172
|
// leaves 0 at zero velocity instead of being thrown, and rings once instead
|
|
12786
13173
|
// of several times. Sampled — see SAMPLED_EASING_VALUES.
|
|
12787
13174
|
SMOOTH_OVERSHOOT: "Smooth Overshoot",
|
|
12788
|
-
|
|
12789
|
-
|
|
13175
|
+
SINE_IN: "Sine In",
|
|
13176
|
+
SINE_OUT: "Sine Out",
|
|
13177
|
+
SINE_IN_OUT: "Sine Smooth",
|
|
13178
|
+
CUBIC_IN: "Cubic In",
|
|
13179
|
+
CUBIC_OUT: "Cubic Out",
|
|
13180
|
+
EXPO_IN: "Expo In",
|
|
13181
|
+
EXPO_OUT: "Expo Out",
|
|
13182
|
+
EXPO_IN_OUT: "Expo Smooth",
|
|
13183
|
+
// A hard, decisive stop (GSAP power4.out; drawn as cubic-bezier(0.23, 1, 0.32, 1)).
|
|
13184
|
+
POWER: "Power",
|
|
12790
13185
|
// ── Keyframe-only easings ──
|
|
12791
13186
|
// CUSTOM reads the key's own `bezier: [x1, y1, x2, y2]` (the graph editor writes
|
|
12792
13187
|
// it by dragging handles); HOLD steps to the next key instead of interpolating.
|
|
@@ -13611,6 +14006,10 @@ function buildGradient(type = "linear", angle = 90, stops = []) {
|
|
|
13611
14006
|
// src/components/videoEditor/shapePath.js
|
|
13612
14007
|
init_define_import_meta_env();
|
|
13613
14008
|
|
|
14009
|
+
// src/components/videoEditor/borderKeyframes.js
|
|
14010
|
+
init_define_import_meta_env();
|
|
14011
|
+
var RADIUS_KF_KEYS = ["borderRadius", "borderRadiusTL", "borderRadiusTR", "borderRadiusBR", "borderRadiusBL"];
|
|
14012
|
+
|
|
13614
14013
|
// src/components/videoEditor/objectMotionPath.js
|
|
13615
14014
|
init_define_import_meta_env();
|
|
13616
14015
|
var OBJECT_MOTION_PATH_AIMS = ["tangent", "points", "free"];
|
|
@@ -13863,7 +14262,7 @@ var LINE_PATH_PRESETS = [
|
|
|
13863
14262
|
];
|
|
13864
14263
|
var ALL_PRESETS = [...SHAPE_PATH_PRESETS, ...LINE_PATH_PRESETS];
|
|
13865
14264
|
var LINE_PRESET_IDS = new Set(LINE_PATH_PRESETS.map((p) => p.id));
|
|
13866
|
-
var SHAPE_CORNER_KF_KEYS =
|
|
14265
|
+
var SHAPE_CORNER_KF_KEYS = RADIUS_KF_KEYS;
|
|
13867
14266
|
var SHAPE_SIZE_KF_KEYS = ["shapeWidthPx", "shapeHeightPx", "shapeWidthPct", "shapeHeightPct", "shapeRadiusPx", "shapeRadiusPct", ...SHAPE_CORNER_KF_KEYS];
|
|
13868
14267
|
var SIZE_KF_META = {
|
|
13869
14268
|
shapeWidthPx: { displayLabel: "Shape width", unit: "px", max: 2e4 },
|
|
@@ -15911,6 +16310,58 @@ function formatCounterValue(units, spec) {
|
|
|
15911
16310
|
}
|
|
15912
16311
|
var SHUTTER_SEC = 1 / 60;
|
|
15913
16312
|
|
|
16313
|
+
// src/components/videoEditor/textMorph.js
|
|
16314
|
+
init_define_import_meta_env();
|
|
16315
|
+
var MORPH_MATCH = Object.freeze(["order", "any"]);
|
|
16316
|
+
var MORPH_LEAVE = Object.freeze(["mask", "fade", "blur", "scale"]);
|
|
16317
|
+
var MORPH_ORDER = Object.freeze(["left", "center", "all"]);
|
|
16318
|
+
var MORPH_DEFAULTS = Object.freeze({
|
|
16319
|
+
duration: 0.8,
|
|
16320
|
+
easing: "EXPO_IN_OUT",
|
|
16321
|
+
match: "order",
|
|
16322
|
+
ignoreCase: true,
|
|
16323
|
+
letterStagger: 0.03,
|
|
16324
|
+
arc: 0,
|
|
16325
|
+
leave: "fade",
|
|
16326
|
+
order: "left"
|
|
16327
|
+
});
|
|
16328
|
+
function bezier(x1, y1, x2, y2) {
|
|
16329
|
+
const cx = 3 * x1;
|
|
16330
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
16331
|
+
const ax = 1 - cx - bx;
|
|
16332
|
+
const cy = 3 * y1;
|
|
16333
|
+
const by = 3 * (y2 - y1) - cy;
|
|
16334
|
+
const ay = 1 - cy - by;
|
|
16335
|
+
const X = (s) => ((ax * s + bx) * s + cx) * s;
|
|
16336
|
+
const Y = (s) => ((ay * s + by) * s + cy) * s;
|
|
16337
|
+
const dX = (s) => (3 * ax * s + 2 * bx) * s + cx;
|
|
16338
|
+
return (u) => {
|
|
16339
|
+
if (u <= 0) return 0;
|
|
16340
|
+
if (u >= 1) return 1;
|
|
16341
|
+
let s = u;
|
|
16342
|
+
for (let i = 0; i < 8; i += 1) {
|
|
16343
|
+
const e2 = X(s) - u;
|
|
16344
|
+
const d = dX(s);
|
|
16345
|
+
if (Math.abs(e2) < 1e-7 || Math.abs(d) < 1e-6) break;
|
|
16346
|
+
s -= e2 / d;
|
|
16347
|
+
}
|
|
16348
|
+
if (Math.abs(X(s) - u) > 1e-6) {
|
|
16349
|
+
let lo = 0;
|
|
16350
|
+
let hi = 1;
|
|
16351
|
+
s = u;
|
|
16352
|
+
for (let i = 0; i < 40; i += 1) {
|
|
16353
|
+
if (X(s) < u) lo = s;
|
|
16354
|
+
else hi = s;
|
|
16355
|
+
s = (lo + hi) / 2;
|
|
16356
|
+
}
|
|
16357
|
+
}
|
|
16358
|
+
return Y(s);
|
|
16359
|
+
};
|
|
16360
|
+
}
|
|
16361
|
+
var EASE_LEAVE = bezier(0.55, 0, 0.8, 0.3);
|
|
16362
|
+
var EASE_ARRIVE = bezier(0.22, 1, 0.36, 1);
|
|
16363
|
+
var EASE_SWAP = bezier(0.45, 0, 0.2, 1);
|
|
16364
|
+
|
|
15914
16365
|
// src/components/videoEditor/textPlaceholders.js
|
|
15915
16366
|
var TOKEN_RE = /\{\{\{\s*([A-Za-z]+)\s*(?::([^{}]*?))?\s*\}\}\}/g;
|
|
15916
16367
|
var PLACEHOLDER_CATALOG = Object.freeze([
|
|
@@ -15930,7 +16381,13 @@ var PLACEHOLDER_CATALOG = Object.freeze([
|
|
|
15930
16381
|
id: "CYCLE",
|
|
15931
16382
|
label: "Word cycle",
|
|
15932
16383
|
example: "{{{CYCLE:fast|simple|yours}}}",
|
|
15933
|
-
description: "Words separated by | take turns in the sentence: each one holds, then rolls or
|
|
16384
|
+
description: "Words or phrases separated by | take turns in the sentence: each one holds, then rolls, fades or morphs to the next (Morph: the letters they share travel to their new place). Pace: add hold=2 (seconds each word stays) and duration=0.6 (seconds the change takes), or set Hold and Duration in its card."
|
|
16385
|
+
},
|
|
16386
|
+
{
|
|
16387
|
+
id: "MORPH",
|
|
16388
|
+
label: "Text Morph",
|
|
16389
|
+
example: "{{{CYCLE:fast|simple|yours; style=morph; hold=1.2; duration=0.8}}}",
|
|
16390
|
+
description: "A word cycle whose letters travel: the letters two words share slide to their new place, the others rise out and in. Pace: hold = seconds each word stays, duration = seconds the morph takes (Hold and Travel in its card). Whole lines instead of words: write each on its own line and turn on Each line is a state."
|
|
15934
16391
|
},
|
|
15935
16392
|
{
|
|
15936
16393
|
id: "SCRAMBLE",
|
|
@@ -15964,6 +16421,7 @@ var PLACEHOLDER_CATALOG = Object.freeze([
|
|
|
15964
16421
|
}
|
|
15965
16422
|
]);
|
|
15966
16423
|
var clamp3 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
16424
|
+
var CYCLE_STYLES = ["roll", "fade", "blur", "morph"];
|
|
15967
16425
|
var CYCLE_DEFAULTS = Object.freeze({
|
|
15968
16426
|
textCycleStyle: "roll",
|
|
15969
16427
|
textCycleDelay: 0,
|
|
@@ -16008,14 +16466,22 @@ var PLACEHOLDER_OPTIONS = Object.freeze({
|
|
|
16008
16466
|
kf: { kf: true }
|
|
16009
16467
|
},
|
|
16010
16468
|
cycle: {
|
|
16011
|
-
style: { values:
|
|
16469
|
+
style: { values: CYCLE_STYLES },
|
|
16012
16470
|
start: { num: [0, 3600] },
|
|
16013
16471
|
hold: { num: [0, 60] },
|
|
16014
16472
|
duration: { num: [0.05, 10] },
|
|
16015
16473
|
easing: { easing: true },
|
|
16016
16474
|
width: { values: ["fit", "fixed"] },
|
|
16017
16475
|
direction: { values: ["up", "down"] },
|
|
16018
|
-
loop: { values: ["on", "off"] }
|
|
16476
|
+
loop: { values: ["on", "off"] },
|
|
16477
|
+
// Morph only (textMorph.js): how letters pair up, the letter stagger (s), the
|
|
16478
|
+
// travel's arc (0–1), how the unpaired letters leave / arrive, the rank order.
|
|
16479
|
+
match: { values: ["order", "any"] },
|
|
16480
|
+
case: { values: ["ignore", "keep"] },
|
|
16481
|
+
stagger: { num: [0, 2] },
|
|
16482
|
+
arc: { num: [0, 1] },
|
|
16483
|
+
leave: { values: ["mask", "fade", "blur", "scale"] },
|
|
16484
|
+
order: { values: ["left", "center", "centre", "all"] }
|
|
16019
16485
|
},
|
|
16020
16486
|
scramble: {
|
|
16021
16487
|
start: { num: [0, 3600] },
|
|
@@ -16107,13 +16573,15 @@ var PLACEHOLDER_LABELS = Object.freeze({
|
|
|
16107
16573
|
CYCLE: "Word cycle",
|
|
16108
16574
|
SCRAMBLE: "Scramble",
|
|
16109
16575
|
TIMECODE: "Timecode",
|
|
16110
|
-
FRAME: "Frame number"
|
|
16576
|
+
FRAME: "Frame number",
|
|
16577
|
+
LINES: "Line states"
|
|
16111
16578
|
});
|
|
16112
16579
|
function findVariable(variables, name) {
|
|
16113
16580
|
if (!Array.isArray(variables)) return null;
|
|
16114
16581
|
const key = name.toLowerCase();
|
|
16115
16582
|
return variables.find((v) => String(v?.name || "").toLowerCase() === key) || null;
|
|
16116
16583
|
}
|
|
16584
|
+
var LINE_STATE_STYLES = Object.freeze(["morph", "roll", "fade", "blur"]);
|
|
16117
16585
|
function formatTimecode(sec, fps, format = "hh:mm:ss:ff") {
|
|
16118
16586
|
const f2 = Math.max(1, Math.round(Number(fps) || 30));
|
|
16119
16587
|
const frames = Math.max(0, Math.floor(sec * f2 + 1e-6));
|
|
@@ -16655,11 +17123,11 @@ var PT_KF_KEYS = PT_NUMBER_FIELDS.filter((f2) => f2.kf).map((f2) => f2.key);
|
|
|
16655
17123
|
// src/components/videoEditor/gsapTransitions.js
|
|
16656
17124
|
var HOLD_EASE = () => 0;
|
|
16657
17125
|
var customEaseCache = /* @__PURE__ */ new Map();
|
|
16658
|
-
function customEase(
|
|
16659
|
-
const key =
|
|
17126
|
+
function customEase(bezier2) {
|
|
17127
|
+
const key = bezier2.join(",");
|
|
16660
17128
|
let fn = customEaseCache.get(key);
|
|
16661
17129
|
if (!fn) {
|
|
16662
|
-
fn = bezierEase(...
|
|
17130
|
+
fn = bezierEase(...bezier2);
|
|
16663
17131
|
if (customEaseCache.size > 256) customEaseCache.clear();
|
|
16664
17132
|
customEaseCache.set(key, fn);
|
|
16665
17133
|
}
|
|
@@ -16689,8 +17157,8 @@ function resolveWholeKeyEase(key) {
|
|
|
16689
17157
|
const easing = key?.easing || TransitionEasing.LINEAR;
|
|
16690
17158
|
if (easing === TransitionEasing.HOLD) return HOLD_EASE;
|
|
16691
17159
|
if (easing === TransitionEasing.CUSTOM) {
|
|
16692
|
-
const
|
|
16693
|
-
if (
|
|
17160
|
+
const bezier2 = sanitizeBezier(key?.bezier);
|
|
17161
|
+
if (bezier2) return customEase(bezier2);
|
|
16694
17162
|
return playedEase(TransitionEasing.EASE_IN_OUT);
|
|
16695
17163
|
}
|
|
16696
17164
|
return playedEase(easing);
|
|
@@ -18661,8 +19129,9 @@ var CAMERA_KF_PROPS = [
|
|
|
18661
19129
|
{ label: "camY", displayLabel: "Pan Y", group: "Motion", placeholder: "0", unit: "%", type: "number", min: -200, max: 200, step: 0.5 },
|
|
18662
19130
|
{ label: "camRoll", displayLabel: "Roll", group: "Motion", placeholder: "0", unit: "deg", type: "number", min: -180, max: 180, step: 0.5 },
|
|
18663
19131
|
{ label: "camDolly", displayLabel: "Dolly", group: "Motion", placeholder: "0", unit: "px", type: "number", min: -3e3, max: 3e3, step: 10 },
|
|
18664
|
-
|
|
18665
|
-
{ label: "
|
|
19132
|
+
// No min/max on tilt: an orbit may go past 90° and all the way round; the evaluators hold any angle.
|
|
19133
|
+
{ label: "camTiltX", displayLabel: "Tilt X", group: "Motion", placeholder: "0", unit: "deg", type: "number", step: 0.5 },
|
|
19134
|
+
{ label: "camTiltY", displayLabel: "Tilt Y", group: "Motion", placeholder: "0", unit: "deg", type: "number", step: 0.5 },
|
|
18666
19135
|
{ label: "camDofEnabled", displayLabel: "Depth of Field", group: "Depth of Field", placeholder: "0", unit: "", type: "number", min: 0, max: 1, step: 1 },
|
|
18667
19136
|
{ label: "camFocusDistance", displayLabel: "Focus Distance", group: "Depth of Field", placeholder: "0", unit: "px", type: "number", min: 0, max: 2e4, step: 10 },
|
|
18668
19137
|
{ label: "camAperture", displayLabel: "Aperture", group: "Depth of Field", placeholder: "24", unit: "px", type: "number", min: 0, max: 300, step: 1 },
|
|
@@ -18702,8 +19171,8 @@ function cameraMoveHash(propertyKeyframes) {
|
|
|
18702
19171
|
const seen = /* @__PURE__ */ new Set();
|
|
18703
19172
|
const keys = track.filter((k) => k && Number.isFinite(Number.parseFloat(k.position))).map((k) => {
|
|
18704
19173
|
const easing = easings.has(k.easing) ? k.easing : TransitionEasing.EASE_IN_OUT;
|
|
18705
|
-
const
|
|
18706
|
-
return [Math.round(Number.parseFloat(k.position) * 1e3) / 1e3, String(k.value ?? ""), easing,
|
|
19174
|
+
const bezier2 = easing === TransitionEasing.CUSTOM && Array.isArray(k.bezier) ? k.bezier.map((n2) => Math.round(Number(n2) * 1e4) / 1e4).join(",") : "";
|
|
19175
|
+
return [Math.round(Number.parseFloat(k.position) * 1e3) / 1e3, String(k.value ?? ""), easing, bezier2];
|
|
18707
19176
|
}).sort((a, b2) => a[0] - b2[0]).filter((k) => seen.has(k[0]) ? false : (seen.add(k[0]), true));
|
|
18708
19177
|
parts.push(`${prop}=${keys.map((k) => k.join("|")).join(";")}`);
|
|
18709
19178
|
}
|
|
@@ -21791,8 +22260,8 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
|
|
|
21791
22260
|
// src/motionEngine/sceneScriptGuide.js
|
|
21792
22261
|
init_define_import_meta_env();
|
|
21793
22262
|
|
|
21794
|
-
// ../skills/greenlight-dash/references/video
|
|
21795
|
-
var
|
|
22263
|
+
// ../skills/greenlight-dash/references/video/design.md
|
|
22264
|
+
var design_default = "# Motion design that looks made, not generated\n\nWhat makes a video read as designed rather than generated. The rules hold however the video is built \u2014 a project\nthrough the app's API, `project.json` with the engine's CLI, or a scene script \u2014 so read them before the first version\nand check every version against them.\n\n## Story and words\n\n- **One idea per scene**: 3\u20136 scenes of 2\u20133 s for a short video. A scene that says two things says neither.\n- **At most ~8 words on screen at once, held at least 1 s.** People read about 3 words a second; text that leaves\n before it is read is noise.\n- **9:16**: bigger text, stacked vertically, everything in the middle 80 %.\n\n## Hierarchy\n\n- One headline per scene; supporting text much smaller. Hierarchy comes from size and weight, not colour alone.\n- One accent colour and strong contrast.\n- **No text shadows unless the user asks for one.** A shadow under type reads as a stock default and softens the\n letter edges. When text has to read over footage or a busy picture, get the contrast from the design instead: a\n calmer or darker part of the frame, a caption bar (`textBgEnabled`), or a shape behind the text. So no\n `textShadowX` / `textShadowY` / `textShadowBlur` / `textShadowSpread` on a text layer, no `shadow` property in a\n Text Animator and no `longShadow` effect on text. A shadow already in the user's project stays.\n- Keep text inside the middle 80 % of the frame (in a project: |panX| and |panY| \u2264 40): players and feeds crop the\n edges.\n\n## Timing\n\n- Stagger related elements by 0.06\u20130.12 s.\n- **One focal move at a time.** If everything moves, nothing reads: supporting elements move less and later.\n- Shapes and pictures enter in 0.4\u20130.8 s; exits are faster than entrances.\n- A text animation takes at least 0.8\u20131 s over the whole line, first unit in to last unit settled, unless the video\n needs snap (a beat hit, a glitch flick): faster text flashes past before it is read. The arithmetic per kind of\n animation: [Text animation pace](#text-animation-pace).\n\n## Easing by intent\n\n- Entrances: `AE In (arrive)`, `Expo Out` or `Power` (fast start, soft landing); UI panels and drawers: `Apple Out`.\n Camera and big moves: `AE Smooth`.\n- Scene changes on the beat, a logo hit, a screen swipe: `Punch` (slow wind-up, hard hit, long tail).\n- A layer that passes through (in, glide, out): `Smooth Glide` with the `glide` expression \u2014 the Glide keyframe\n presets build it ([keyframes.md](keyframes.md)).\n- Playful arrivals: `Back Out` or `Overshoot`; a real spring: the `spring` expression (Snappy for buttons, icons and\n pop-ins, Gentle for mascots, big cards and the camera). Exits: `Ease In` or `AE Out (leave)`; the Kinetic recipes'\n `Anticipate` exits (wind back, then snap away) are the exception and stay as tuned.\n- `Linear` only for constant motion: tickers, rotation, progress along a path.\n\n## Text animation\n\n- Animate words or letters, never a whole line at once \u2014 the text presets do this.\n- One family per video. Calm presets for lines and captions: Classic Fade Up (`classic-fade-up`), Classic Rise (Mask)\n (`classic-rise-mask`), Glide Text (`glide-text`); Focus Type (`focus-type`) for a title. All are in\n [text-presets.md](text-presets.md).\n- Punchy presets that tilt or overshoot ([text-kinetic.md](text-kinetic.md): Swing Pop (`swing-pop`), Whip (`whip`),\n Letter Vortex (`letter-vortex`)\u2026) only on short titles, hooks and calls to action \u2014 up to about 5 words, or 2\u20134 short\n stacked lines \u2014 and only when the video wants energy. On a longer line or a caption under speech they read cheap.\n- Glide Continuous (`glide-continuous`) and Glide Soft Stop (`glide-soft-stop`) never let the text rest: use them\n only when the user asks for drifting text.\n- On a coloured card, pick a preset without a colour band (Classic Rise (Mask), Classic Fade Up): a band's colour can\n clash with the card.\n\n## Text animation pace\n\nBy default, text that animates in, animates out or gets a one-time pass (a sweep, a highlight, a typed line) takes\n**at least 0.8\u20131 s**, counted from the moment its first unit starts to the moment its last unit settles. Faster than\nthat, the words flash past before anyone can read them. In a Video Editor project the rule holds however the motion is\nbuilt:\n\n- **Advanced Animations on Time** (`textAnimators[].range.drive: \"time\"`): the span is\n `(units \u2212 1) \xD7 stagger + duration`. For example, 4 words with `stagger` 0.06 and `duration` 0.8 take 0.98 s. For\n moves (rise, slide, scale, rotate, blur), reach the span with `duration`, because a long `stagger` makes a long line\n crawl. A typewriter (`duration` 0) gets its pace from `stagger`.\n- **Keyframes** (`txa:` range tracks such as Start or Offset, or a text layer's own position, scale and opacity\n tracks): the first and last key of one move sit at least 0.8 s apart. Key positions are % of the layer, so a move\n from `p1` to `p2` lasts `(p2 \u2212 p1) / 100 \xD7 layer seconds`. On a layer with a speed curve, a `txa:` key's position is\n a % of the clip's source length (`trimEnd \u2212 trimStart`) instead, so time the move on that clock\n ([text.md](text.md)).\n- **Layer entrances and exits** on a text layer: `transitionInDuration` / `transitionOutDuration` of 0.8 or more\n (the most they take is 3 s).\n- **Recipes** from [text-kinetic.md](text-kinetic.md) and [text-presets.md](text-presets.md) keep their tuned timing.\n On very short text (one or two words), where the span works out under 0.8 s, raise `duration` until it reaches 0.8 s.\n\nGo faster only when the edit really needs it: the user asks for snappy or fast text, a word hits on a beat or a fast\ncut, a glitch or scramble flicks, the layer is too short to fit a 0.8 s move, or the animator uses `drive: \"voice\"`\n(the speech sets the pace there).\n\n## Transitions and camera\n\n- Vary two or three kinds of transition across a video: one kind everywhere looks like a template, a new one at\n every cut looks random.\n- Camera moves are slow and small unless they are the point. Fast motion looks best at 60 fps with motion blur.\n- A real 3D camera earns its place on one or two moments, not every scene: a zoom onto the one thing a scene is about\n (a number, the button a screen recording clicks; 1.3\u20132\xD7, then back out), or a slow orbit or push that shows depth.\n- Depth needs layers at different distances: a soft shape or picture far behind, the words at the front. Depth of\n field reads only then; keep the words sharp.\n- Keep an orbit in front of flat layers (within about \xB170\xB0): text turned further goes edge-on, then mirrored.\n\n## Sound\n\n- A soft whoosh on each transition and on the first headline is a good base. `data-1` or `selection-data-click` for\n UI-like pops, `cinematic-transition-13` for one big reveal.\n- Take sound effects from the user's Sound Library first\n ([media.md](../media.md#asset-libraries-sound--assets--music)). The ids above are GreenLight Motion library sounds, not\n Sound Library files: `GET /api/motion-sounds/v1/<id>.wav` serves one (e.g. `/api/motion-sounds/v1/data-1.wav`). Save\n it into the board folder and use the board url as an `audio` clip's `url`.\n\n## What to check in every version\n\nLook at real frames across the video (a contact sheet) before the user sees it \u2014 the JSON doesn't show these:\nblank or empty frames, overlapping or cut-off text, text too small or gone too fast, nothing moving or everything\nmoving at once, weak hierarchy, clashing colours. Fix them and look again; two rounds are usually enough.\n";
|
|
21796
22265
|
|
|
21797
22266
|
// src/motionEngine/sceneScriptGuide.js
|
|
21798
22267
|
var PREFERRED = {
|
|
@@ -21900,7 +22369,7 @@ matte, masks, particles, 3D) goes on from \`project.json\`: SKILL.md, "Beyond th
|
|
|
21900
22369
|
`;
|
|
21901
22370
|
}
|
|
21902
22371
|
function designGuide() {
|
|
21903
|
-
return `${
|
|
22372
|
+
return `${design_default.replace(/^(#+) /gm, "#$1 ").trim()}
|
|
21904
22373
|
|
|
21905
22374
|
`;
|
|
21906
22375
|
}
|