@routevn/creator-model 1.13.2 → 1.14.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 +34 -8
- package/package.json +1 -1
- package/src/model.js +124 -149
package/README.md
CHANGED
|
@@ -106,6 +106,22 @@ Design rules:
|
|
|
106
106
|
- random ids across RouteVN should use `nanoid` with the RouteVN base58 variant;
|
|
107
107
|
deterministic derived tokens such as partition hashes are a separate case
|
|
108
108
|
|
|
109
|
+
## Animation Camera Tracks
|
|
110
|
+
|
|
111
|
+
Animation resources may explicitly group synchronized position and scale tracks
|
|
112
|
+
with `cameraTracks: ["update"]`, or `["prev", "next"]` for transitions.
|
|
113
|
+
Each listed side must contain `x`, `y`, `scaleX`, and `scaleY` keyframe
|
|
114
|
+
tracks with matching timing/easing and start-value presence, absolute values,
|
|
115
|
+
and positive scales. Track initial values are optional; they must be numeric
|
|
116
|
+
on all four tracks or omitted from all four to use the target's existing pose.
|
|
117
|
+
Translation tracks cannot coexist with Camera on the same side.
|
|
118
|
+
|
|
119
|
+
This authoring marker does not alter the runtime animation format. Existing
|
|
120
|
+
animations without the marker remain separate tracks; no grouping is inferred.
|
|
121
|
+
An update may use `cameraTracks: []` to remove grouping. Updates to animation
|
|
122
|
+
data are validated with the resource's retained grouping unless it is replaced
|
|
123
|
+
in the same command.
|
|
124
|
+
|
|
109
125
|
## Font Weight Metadata
|
|
110
126
|
|
|
111
127
|
Font resources may store extracted weight capabilities in three flat fields:
|
|
@@ -354,8 +370,17 @@ Audio effects are foldered resources stored in `audioEffects.items` and
|
|
|
354
370
|
},
|
|
355
371
|
audioEffect: {
|
|
356
372
|
type: "transition",
|
|
357
|
-
prev: {
|
|
358
|
-
|
|
373
|
+
prev: {
|
|
374
|
+
volume: {
|
|
375
|
+
keyframes: [{ value: 0, duration: 600, easing: "easeInOutSine" }],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
next: {
|
|
379
|
+
volume: {
|
|
380
|
+
initialValue: 0,
|
|
381
|
+
keyframes: [{ value: 100, duration: 900, easing: "easeInOutSine" }],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
359
384
|
},
|
|
360
385
|
}
|
|
361
386
|
```
|
|
@@ -365,12 +390,13 @@ transition effect, or a single `target` sound slot for an update effect. Each
|
|
|
365
390
|
slot is an object containing a `soundId`.
|
|
366
391
|
|
|
367
392
|
`audioEffect.type` is either `transition` or `update`. Transition effects have
|
|
368
|
-
at least one `prev
|
|
369
|
-
`volume`, `pan`, and `playbackRate
|
|
370
|
-
must finish with an absolute numeric
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
393
|
+
at least one property under `prev` or `next`; update effects place properties
|
|
394
|
+
under `tween`. Both support only `volume`, `pan`, and `playbackRate`. Each
|
|
395
|
+
property has non-empty keyframes and must finish with an absolute numeric
|
|
396
|
+
keyframe. A final `tween` or `next` value becomes the persistent BGM property
|
|
397
|
+
value after the effect finishes. Absolute volume values are bounded to
|
|
398
|
+
`0..100`, pan to `-1..1`, and playback rate to `>= 0`; relative keyframes
|
|
399
|
+
represent unbounded numeric deltas and cannot be final.
|
|
374
400
|
|
|
375
401
|
## Current Scope
|
|
376
402
|
|
package/package.json
CHANGED
package/src/model.js
CHANGED
|
@@ -344,7 +344,7 @@ const ANIMATION_EASING_KEYS = [
|
|
|
344
344
|
"easeOutElastic",
|
|
345
345
|
"easeInOutElastic",
|
|
346
346
|
];
|
|
347
|
-
const
|
|
347
|
+
const AUDIO_EFFECT_PROPERTY_KEYS = ["volume", "pan", "playbackRate"];
|
|
348
348
|
const VARIABLE_SCOPE_KEYS = ["context", "device", "account"];
|
|
349
349
|
const VARIABLE_TYPE_KEYS = ["string", "number", "boolean", "object"];
|
|
350
350
|
const LAYOUT_TYPE_KEYS = [
|
|
@@ -408,7 +408,7 @@ const SAVE_LOAD_DATE_FORMATS = new Set([
|
|
|
408
408
|
"DD MMM YYYY",
|
|
409
409
|
"YYYY年MM月DD日",
|
|
410
410
|
]);
|
|
411
|
-
export const SCHEMA_VERSION =
|
|
411
|
+
export const SCHEMA_VERSION = 14;
|
|
412
412
|
const LAYOUT_CONTAINER_ELEMENT_TYPES = [
|
|
413
413
|
"folder",
|
|
414
414
|
"container",
|
|
@@ -2345,6 +2345,85 @@ const validateAnimationDefinition = ({ animation, path, errorFactory }) => {
|
|
|
2345
2345
|
}
|
|
2346
2346
|
};
|
|
2347
2347
|
|
|
2348
|
+
// Camera is an authored grouping of four synchronized runtime tween tracks.
|
|
2349
|
+
// No grouping is inferred for older animations that omit cameraTracks.
|
|
2350
|
+
const validateCameraTracks = ({ value, animation, path, errorFactory }) => {
|
|
2351
|
+
if (value === undefined) return;
|
|
2352
|
+
if (
|
|
2353
|
+
!Array.isArray(value) ||
|
|
2354
|
+
value.some((side) => !["update", "prev", "next"].includes(side)) ||
|
|
2355
|
+
new Set(value).size !== value.length
|
|
2356
|
+
) {
|
|
2357
|
+
return invalidFromErrorFactory(
|
|
2358
|
+
errorFactory,
|
|
2359
|
+
`${path} must be an array of unique update, prev, or next sides`,
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
// Partial update payloads are checked against the merged resource below.
|
|
2363
|
+
if (animation === undefined) return;
|
|
2364
|
+
for (const side of value) {
|
|
2365
|
+
const tween = side === "update" ? animation.tween : animation[side]?.tween;
|
|
2366
|
+
const validSide =
|
|
2367
|
+
animation.type === "update" ? side === "update" : side !== "update";
|
|
2368
|
+
const properties = ["x", "y", "scaleX", "scaleY"];
|
|
2369
|
+
const tracks = properties.map((property) => tween?.[property]);
|
|
2370
|
+
if (
|
|
2371
|
+
!validSide ||
|
|
2372
|
+
tracks.some(
|
|
2373
|
+
(track) =>
|
|
2374
|
+
!Array.isArray(track?.keyframes) ||
|
|
2375
|
+
(track.initialValue !== undefined &&
|
|
2376
|
+
!Number.isFinite(track.initialValue)) ||
|
|
2377
|
+
(track.initialValue !== undefined) !==
|
|
2378
|
+
(tracks[0]?.initialValue !== undefined),
|
|
2379
|
+
) ||
|
|
2380
|
+
tween.translateX !== undefined ||
|
|
2381
|
+
tween.translateY !== undefined
|
|
2382
|
+
) {
|
|
2383
|
+
return invalidFromErrorFactory(
|
|
2384
|
+
errorFactory,
|
|
2385
|
+
`${path}.${side} requires x, y, scaleX, and scaleY keyframe tracks with matching initial-value presence and no translation tracks`,
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
const reference = tracks[0].keyframes;
|
|
2389
|
+
for (const [index, track] of tracks.entries()) {
|
|
2390
|
+
if (
|
|
2391
|
+
track.keyframes.length !== reference.length ||
|
|
2392
|
+
track.keyframes.some((frame, frameIndex) => {
|
|
2393
|
+
const other = reference[frameIndex];
|
|
2394
|
+
return (
|
|
2395
|
+
frame.relative === true ||
|
|
2396
|
+
frame.duration !== other.duration ||
|
|
2397
|
+
(frame.delay ?? 0) !== (other.delay ?? 0) ||
|
|
2398
|
+
(frame.easing ?? "linear") !== (other.easing ?? "linear") ||
|
|
2399
|
+
(frame.startValue !== undefined) !==
|
|
2400
|
+
(other.startValue !== undefined)
|
|
2401
|
+
);
|
|
2402
|
+
})
|
|
2403
|
+
) {
|
|
2404
|
+
return invalidFromErrorFactory(
|
|
2405
|
+
errorFactory,
|
|
2406
|
+
`${path}.${side} requires synchronized absolute keyframes`,
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (
|
|
2410
|
+
index >= 2 &&
|
|
2411
|
+
((track.initialValue !== undefined && track.initialValue <= 0) ||
|
|
2412
|
+
track.keyframes.some(
|
|
2413
|
+
(frame) =>
|
|
2414
|
+
frame.value <= 0 ||
|
|
2415
|
+
(frame.startValue !== undefined && frame.startValue <= 0),
|
|
2416
|
+
))
|
|
2417
|
+
) {
|
|
2418
|
+
return invalidFromErrorFactory(
|
|
2419
|
+
errorFactory,
|
|
2420
|
+
`${path}.${side} requires positive camera scales`,
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
|
|
2348
2427
|
const validateAnimationItems = ({ items, path, errorFactory }) => {
|
|
2349
2428
|
for (const [itemId, item] of Object.entries(items)) {
|
|
2350
2429
|
const itemPath = `${path}.${itemId}`;
|
|
@@ -2370,6 +2449,7 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
|
|
|
2370
2449
|
"tagIds",
|
|
2371
2450
|
"thumbnailFileId",
|
|
2372
2451
|
"preview",
|
|
2452
|
+
"cameraTracks",
|
|
2373
2453
|
"animation",
|
|
2374
2454
|
],
|
|
2375
2455
|
path: itemPath,
|
|
@@ -2452,6 +2532,13 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
|
|
|
2452
2532
|
return result;
|
|
2453
2533
|
}
|
|
2454
2534
|
}
|
|
2535
|
+
const cameraResult = validateCameraTracks({
|
|
2536
|
+
value: item.cameraTracks,
|
|
2537
|
+
animation: item.animation,
|
|
2538
|
+
path: `${itemPath}.cameraTracks`,
|
|
2539
|
+
errorFactory,
|
|
2540
|
+
});
|
|
2541
|
+
if (cameraResult?.valid === false) return cameraResult;
|
|
2455
2542
|
}
|
|
2456
2543
|
}
|
|
2457
2544
|
};
|
|
@@ -2656,23 +2743,23 @@ const validateAudioEffectKeyframes = ({
|
|
|
2656
2743
|
}
|
|
2657
2744
|
};
|
|
2658
2745
|
|
|
2659
|
-
const
|
|
2660
|
-
if (!isPlainObject(
|
|
2746
|
+
const validateAudioEffectPropertyTracks = ({ tracks, path, errorFactory }) => {
|
|
2747
|
+
if (!isPlainObject(tracks)) {
|
|
2661
2748
|
return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
|
|
2662
2749
|
}
|
|
2663
2750
|
|
|
2664
|
-
if (Object.keys(
|
|
2751
|
+
if (Object.keys(tracks).length === 0) {
|
|
2665
2752
|
return invalidFromErrorFactory(
|
|
2666
2753
|
errorFactory,
|
|
2667
|
-
`${path} must contain at least one
|
|
2754
|
+
`${path} must contain at least one audio property`,
|
|
2668
2755
|
);
|
|
2669
2756
|
}
|
|
2670
2757
|
|
|
2671
|
-
for (const [property, config] of Object.entries(
|
|
2672
|
-
if (!
|
|
2758
|
+
for (const [property, config] of Object.entries(tracks)) {
|
|
2759
|
+
if (!AUDIO_EFFECT_PROPERTY_KEYS.includes(property)) {
|
|
2673
2760
|
return invalidFromErrorFactory(
|
|
2674
2761
|
errorFactory,
|
|
2675
|
-
`${path}.${property} is not a supported audio effect
|
|
2762
|
+
`${path}.${property} is not a supported audio effect property`,
|
|
2676
2763
|
);
|
|
2677
2764
|
}
|
|
2678
2765
|
|
|
@@ -2721,141 +2808,6 @@ const validateAudioEffectTween = ({ tween, path, errorFactory }) => {
|
|
|
2721
2808
|
}
|
|
2722
2809
|
};
|
|
2723
2810
|
|
|
2724
|
-
const validateAudioEffectFade = ({ fade, path, side, errorFactory }) => {
|
|
2725
|
-
if (!isPlainObject(fade)) {
|
|
2726
|
-
return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
if (Object.hasOwn(fade, "keyframes")) {
|
|
2730
|
-
{
|
|
2731
|
-
const result = validateAllowedKeys({
|
|
2732
|
-
value: fade,
|
|
2733
|
-
allowedKeys: ["initialValue", "keyframes"],
|
|
2734
|
-
path,
|
|
2735
|
-
errorFactory,
|
|
2736
|
-
});
|
|
2737
|
-
if (result?.valid === false) {
|
|
2738
|
-
return result;
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
{
|
|
2743
|
-
const result = validateAudioEffectInitialValue({
|
|
2744
|
-
value: fade.initialValue,
|
|
2745
|
-
property: "volume",
|
|
2746
|
-
path: `${path}.initialValue`,
|
|
2747
|
-
errorFactory,
|
|
2748
|
-
});
|
|
2749
|
-
if (result?.valid === false) {
|
|
2750
|
-
return result;
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
{
|
|
2755
|
-
const result = validateAudioEffectKeyframes({
|
|
2756
|
-
keyframes: fade.keyframes,
|
|
2757
|
-
property: "volume",
|
|
2758
|
-
path: `${path}.keyframes`,
|
|
2759
|
-
errorFactory,
|
|
2760
|
-
});
|
|
2761
|
-
if (result?.valid === false) {
|
|
2762
|
-
return result;
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
for (const [index, keyframe] of fade.keyframes.entries()) {
|
|
2767
|
-
if (keyframe.relative === true) {
|
|
2768
|
-
return invalidFromErrorFactory(
|
|
2769
|
-
errorFactory,
|
|
2770
|
-
`${path}.keyframes[${index}].relative is not supported for transition fades`,
|
|
2771
|
-
);
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
return;
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
{
|
|
2779
|
-
const result = validateAllowedKeys({
|
|
2780
|
-
value: fade,
|
|
2781
|
-
allowedKeys: ["initialValue", "delay", "duration", "easing"],
|
|
2782
|
-
path,
|
|
2783
|
-
errorFactory,
|
|
2784
|
-
});
|
|
2785
|
-
if (result?.valid === false) {
|
|
2786
|
-
return result;
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
{
|
|
2791
|
-
const result = validateAudioEffectInitialValue({
|
|
2792
|
-
value: fade.initialValue,
|
|
2793
|
-
property: "volume",
|
|
2794
|
-
path: `${path}.initialValue`,
|
|
2795
|
-
errorFactory,
|
|
2796
|
-
});
|
|
2797
|
-
if (result?.valid === false) {
|
|
2798
|
-
return result;
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
if (!Object.hasOwn(fade, "duration")) {
|
|
2803
|
-
return invalidFromErrorFactory(
|
|
2804
|
-
errorFactory,
|
|
2805
|
-
`${path}.duration is required`,
|
|
2806
|
-
);
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
if (!isFiniteNumber(fade.duration) || fade.duration < 0) {
|
|
2810
|
-
return invalidFromErrorFactory(
|
|
2811
|
-
errorFactory,
|
|
2812
|
-
`${path}.duration must be a finite number >= 0`,
|
|
2813
|
-
);
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
if (
|
|
2817
|
-
fade.delay !== undefined &&
|
|
2818
|
-
(!isFiniteNumber(fade.delay) || fade.delay < 0)
|
|
2819
|
-
) {
|
|
2820
|
-
return invalidFromErrorFactory(
|
|
2821
|
-
errorFactory,
|
|
2822
|
-
`${path}.delay must be a finite number >= 0 when provided`,
|
|
2823
|
-
);
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
return validateAudioEffectEasing({
|
|
2827
|
-
value: fade.easing,
|
|
2828
|
-
path: `${path}.easing`,
|
|
2829
|
-
errorFactory,
|
|
2830
|
-
});
|
|
2831
|
-
};
|
|
2832
|
-
|
|
2833
|
-
const validateAudioEffectTransitionSide = ({
|
|
2834
|
-
side,
|
|
2835
|
-
sideName,
|
|
2836
|
-
path,
|
|
2837
|
-
errorFactory,
|
|
2838
|
-
}) => {
|
|
2839
|
-
{
|
|
2840
|
-
const result = validateExactKeys({
|
|
2841
|
-
value: side,
|
|
2842
|
-
expectedKeys: ["fade"],
|
|
2843
|
-
path,
|
|
2844
|
-
errorFactory,
|
|
2845
|
-
});
|
|
2846
|
-
if (result?.valid === false) {
|
|
2847
|
-
return result;
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
return validateAudioEffectFade({
|
|
2852
|
-
fade: side.fade,
|
|
2853
|
-
path: `${path}.fade`,
|
|
2854
|
-
side: sideName,
|
|
2855
|
-
errorFactory,
|
|
2856
|
-
});
|
|
2857
|
-
};
|
|
2858
|
-
|
|
2859
2811
|
const validateAudioEffectDefinition = ({ audioEffect, path, errorFactory }) => {
|
|
2860
2812
|
{
|
|
2861
2813
|
const result = validateAllowedKeys({
|
|
@@ -2891,8 +2843,8 @@ const validateAudioEffectDefinition = ({ audioEffect, path, errorFactory }) => {
|
|
|
2891
2843
|
);
|
|
2892
2844
|
}
|
|
2893
2845
|
|
|
2894
|
-
return
|
|
2895
|
-
|
|
2846
|
+
return validateAudioEffectPropertyTracks({
|
|
2847
|
+
tracks: audioEffect.tween,
|
|
2896
2848
|
path: `${path}.tween`,
|
|
2897
2849
|
errorFactory,
|
|
2898
2850
|
});
|
|
@@ -2917,9 +2869,8 @@ const validateAudioEffectDefinition = ({ audioEffect, path, errorFactory }) => {
|
|
|
2917
2869
|
continue;
|
|
2918
2870
|
}
|
|
2919
2871
|
|
|
2920
|
-
const result =
|
|
2921
|
-
|
|
2922
|
-
sideName: side,
|
|
2872
|
+
const result = validateAudioEffectPropertyTracks({
|
|
2873
|
+
tracks: audioEffect[side],
|
|
2923
2874
|
path: `${path}.${side}`,
|
|
2924
2875
|
errorFactory,
|
|
2925
2876
|
});
|
|
@@ -12396,6 +12347,7 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
|
|
|
12396
12347
|
"tagIds",
|
|
12397
12348
|
"thumbnailFileId",
|
|
12398
12349
|
"preview",
|
|
12350
|
+
"cameraTracks",
|
|
12399
12351
|
"animation",
|
|
12400
12352
|
],
|
|
12401
12353
|
path: "payload.data",
|
|
@@ -12463,6 +12415,12 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
|
|
|
12463
12415
|
return result;
|
|
12464
12416
|
}
|
|
12465
12417
|
}
|
|
12418
|
+
return validateCameraTracks({
|
|
12419
|
+
value: data.cameraTracks,
|
|
12420
|
+
animation: data.animation,
|
|
12421
|
+
path: "payload.data.cameraTracks",
|
|
12422
|
+
errorFactory,
|
|
12423
|
+
});
|
|
12466
12424
|
}
|
|
12467
12425
|
};
|
|
12468
12426
|
|
|
@@ -12476,6 +12434,7 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
|
|
|
12476
12434
|
"tagIds",
|
|
12477
12435
|
"thumbnailFileId",
|
|
12478
12436
|
"preview",
|
|
12437
|
+
"cameraTracks",
|
|
12479
12438
|
"animation",
|
|
12480
12439
|
],
|
|
12481
12440
|
path: "payload.data",
|
|
@@ -12551,6 +12510,12 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
|
|
|
12551
12510
|
}
|
|
12552
12511
|
}
|
|
12553
12512
|
}
|
|
12513
|
+
return validateCameraTracks({
|
|
12514
|
+
value: data.cameraTracks,
|
|
12515
|
+
animation: data.animation,
|
|
12516
|
+
path: "payload.data.cameraTracks",
|
|
12517
|
+
errorFactory,
|
|
12518
|
+
});
|
|
12554
12519
|
};
|
|
12555
12520
|
|
|
12556
12521
|
const validateAudioEffectCreateData = ({ data, errorFactory }) => {
|
|
@@ -19202,6 +19167,9 @@ const COMMAND_DEFINITIONS = [
|
|
|
19202
19167
|
if (payload.data.preview !== undefined) {
|
|
19203
19168
|
nextAnimation.preview = structuredClone(payload.data.preview);
|
|
19204
19169
|
}
|
|
19170
|
+
if (payload.data.cameraTracks !== undefined) {
|
|
19171
|
+
nextAnimation.cameraTracks = [...payload.data.cameraTracks];
|
|
19172
|
+
}
|
|
19205
19173
|
nextAnimation.animation = structuredClone(payload.data.animation);
|
|
19206
19174
|
}
|
|
19207
19175
|
|
|
@@ -19271,6 +19239,13 @@ const COMMAND_DEFINITIONS = [
|
|
|
19271
19239
|
}
|
|
19272
19240
|
|
|
19273
19241
|
if (currentAnimation.type === "animation") {
|
|
19242
|
+
const cameraResult = validateCameraTracks({
|
|
19243
|
+
value: payload.data.cameraTracks ?? currentAnimation.cameraTracks,
|
|
19244
|
+
animation: payload.data.animation ?? currentAnimation.animation,
|
|
19245
|
+
path: "payload.data.cameraTracks",
|
|
19246
|
+
errorFactory: createPreconditionValidationError,
|
|
19247
|
+
});
|
|
19248
|
+
if (cameraResult?.valid === false) return cameraResult;
|
|
19274
19249
|
if (payload.data.animation !== undefined) {
|
|
19275
19250
|
const result = validateAnimationMaskImageReferences({
|
|
19276
19251
|
state,
|