@routevn/creator-model 1.13.3 → 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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/package.json +1 -1
  3. package/src/model.js +112 -1
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routevn/creator-model",
3
- "version": "1.13.3",
3
+ "version": "1.14.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/model.js CHANGED
@@ -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 = 13;
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
  };
@@ -12260,6 +12347,7 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
12260
12347
  "tagIds",
12261
12348
  "thumbnailFileId",
12262
12349
  "preview",
12350
+ "cameraTracks",
12263
12351
  "animation",
12264
12352
  ],
12265
12353
  path: "payload.data",
@@ -12327,6 +12415,12 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
12327
12415
  return result;
12328
12416
  }
12329
12417
  }
12418
+ return validateCameraTracks({
12419
+ value: data.cameraTracks,
12420
+ animation: data.animation,
12421
+ path: "payload.data.cameraTracks",
12422
+ errorFactory,
12423
+ });
12330
12424
  }
12331
12425
  };
12332
12426
 
@@ -12340,6 +12434,7 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
12340
12434
  "tagIds",
12341
12435
  "thumbnailFileId",
12342
12436
  "preview",
12437
+ "cameraTracks",
12343
12438
  "animation",
12344
12439
  ],
12345
12440
  path: "payload.data",
@@ -12415,6 +12510,12 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
12415
12510
  }
12416
12511
  }
12417
12512
  }
12513
+ return validateCameraTracks({
12514
+ value: data.cameraTracks,
12515
+ animation: data.animation,
12516
+ path: "payload.data.cameraTracks",
12517
+ errorFactory,
12518
+ });
12418
12519
  };
12419
12520
 
12420
12521
  const validateAudioEffectCreateData = ({ data, errorFactory }) => {
@@ -19066,6 +19167,9 @@ const COMMAND_DEFINITIONS = [
19066
19167
  if (payload.data.preview !== undefined) {
19067
19168
  nextAnimation.preview = structuredClone(payload.data.preview);
19068
19169
  }
19170
+ if (payload.data.cameraTracks !== undefined) {
19171
+ nextAnimation.cameraTracks = [...payload.data.cameraTracks];
19172
+ }
19069
19173
  nextAnimation.animation = structuredClone(payload.data.animation);
19070
19174
  }
19071
19175
 
@@ -19135,6 +19239,13 @@ const COMMAND_DEFINITIONS = [
19135
19239
  }
19136
19240
 
19137
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;
19138
19249
  if (payload.data.animation !== undefined) {
19139
19250
  const result = validateAnimationMaskImageReferences({
19140
19251
  state,