sketchmark 2.0.0 → 2.1.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/ANIMATABLE_MATRIX.md +177 -0
- package/KERNEL_SPEC.md +412 -0
- package/PACKS.md +81 -0
- package/PRESETS.md +182 -0
- package/README.md +274 -188
- package/bin/editor-ui.cjs +2285 -0
- package/bin/preview-ui.cjs +74 -0
- package/bin/sketchmark.cjs +648 -2008
- package/dist/src/animatable.d.ts +21 -0
- package/dist/src/animatable.js +439 -0
- package/dist/src/builders/index.d.ts +1 -11
- package/dist/src/builders/index.js +1 -19
- package/dist/src/diagnostics.js +1 -64
- package/dist/src/edit.d.ts +27 -0
- package/dist/src/edit.js +162 -0
- package/dist/src/index.d.ts +4 -13
- package/dist/src/index.js +4 -13
- package/dist/src/keyframes.d.ts +48 -0
- package/dist/src/keyframes.js +182 -0
- package/dist/src/motion.d.ts +4 -0
- package/dist/src/motion.js +262 -0
- package/dist/src/normalize.js +120 -151
- package/dist/src/presets/characters.d.ts +15 -0
- package/dist/src/presets/characters.js +113 -0
- package/dist/src/presets/compose.d.ts +5 -0
- package/dist/src/presets/compose.js +80 -0
- package/dist/src/presets/effects.d.ts +40 -0
- package/dist/src/presets/effects.js +79 -0
- package/dist/src/presets/helpers.d.ts +33 -0
- package/dist/src/presets/helpers.js +165 -0
- package/dist/src/presets/index.d.ts +9 -0
- package/dist/src/presets/index.js +48 -0
- package/dist/src/presets/motions.d.ts +33 -0
- package/dist/src/presets/motions.js +75 -0
- package/dist/src/presets/scenes.d.ts +35 -0
- package/dist/src/presets/scenes.js +134 -0
- package/dist/src/presets/shapes.d.ts +71 -0
- package/dist/src/presets/shapes.js +96 -0
- package/dist/src/presets/transitions.d.ts +29 -0
- package/dist/src/presets/transitions.js +113 -0
- package/dist/src/presets/types.d.ts +34 -0
- package/dist/src/presets/types.js +2 -0
- package/dist/src/render/html.js +1 -4
- package/dist/src/render/svg.d.ts +2 -2
- package/dist/src/render/svg.js +86 -82
- package/dist/src/render/three-html.js +67 -113
- package/dist/src/scenes.js +1 -0
- package/dist/src/schema.js +218 -280
- package/dist/src/shapes/builtins.js +11 -47
- package/dist/src/shapes/common.js +12 -11
- package/dist/src/shapes/registry.d.ts +0 -1
- package/dist/src/shapes/registry.js +0 -4
- package/dist/src/shapes/types.d.ts +1 -3
- package/dist/src/types.d.ts +57 -288
- package/dist/src/utils.d.ts +2 -11
- package/dist/src/utils.js +13 -70
- package/dist/src/validate.js +321 -275
- package/dist/tests/run.js +576 -510
- package/examples/1730642890464.jpg +0 -0
- package/examples/app-screen.svg +1 -0
- package/examples/app-screen.visual.json +503 -0
- package/examples/dashboard-table.svg +1 -0
- package/examples/dashboard-table.visual.json +708 -0
- package/examples/dev-docs.svg +1 -0
- package/examples/dev-docs.visual.json +248 -0
- package/examples/explainer.mp4 +0 -0
- package/examples/explainer.visual.json +1713 -0
- package/examples/group-origin-effects-lab-check.svg +1 -0
- package/examples/group-origin-effects-lab.visual.json +1880 -0
- package/examples/image-clip-radius.visual.json +271 -0
- package/examples/make-app-screen.cjs +368 -0
- package/examples/make-dashboard-table.cjs +277 -0
- package/examples/make-dev-docs.cjs +233 -0
- package/examples/make-explainer.cjs +438 -0
- package/examples/make-group-origin-effects-lab.cjs +370 -0
- package/examples/make-image-clip-radius.cjs +169 -0
- package/examples/make-modal-dialog.cjs +355 -0
- package/examples/make-origin-effects-lab.cjs +311 -0
- package/examples/make-preset-character-motion.cjs +32 -0
- package/examples/make-presets-demo.cjs +30 -0
- package/examples/make-pricing.cjs +286 -0
- package/examples/make-product-demo.cjs +468 -0
- package/examples/make-product-hero.cjs +223 -0
- package/examples/make-release-notes.cjs +333 -0
- package/examples/make-settings-panel.cjs +435 -0
- package/examples/make-split-preview.cjs +248 -0
- package/examples/make-storyboard.cjs +215 -0
- package/examples/make-transcript.cjs +234 -0
- package/examples/make-typography-test.cjs +397 -0
- package/examples/make-ui-demo-explainer.cjs +1094 -0
- package/examples/make-ui-flow.cjs +762 -0
- package/examples/make-walkthrough.cjs +815 -0
- package/examples/modal-dialog.svg +1 -0
- package/examples/modal-dialog.visual.json +239 -0
- package/examples/origin-effects-lab-check.svg +1 -0
- package/examples/origin-effects-lab.visual.json +1412 -0
- package/examples/preset-character-motion.visual.json +949 -0
- package/examples/presets-demo.visual.json +787 -0
- package/examples/pricing.svg +1 -0
- package/examples/pricing.visual.json +652 -0
- package/examples/product-demo.mp4 +0 -0
- package/examples/product-demo.visual.json +866 -0
- package/examples/product-hero.svg +1 -0
- package/examples/product-hero.visual.json +242 -0
- package/examples/release-notes.svg +1 -0
- package/examples/release-notes.visual.json +467 -0
- package/examples/settings-panel.svg +1 -0
- package/examples/settings-panel.visual.json +501 -0
- package/examples/split-preview.svg +1 -0
- package/examples/split-preview.visual.json +124 -0
- package/examples/storyboard.svg +1 -0
- package/examples/storyboard.visual.json +312 -0
- package/examples/transcript.svg +1 -0
- package/examples/transcript.visual.json +407 -0
- package/examples/typography-indent-check.svg +1 -0
- package/examples/typography-lineheight-0.svg +1 -0
- package/examples/typography-lineheight-2.svg +1 -0
- package/examples/typography-test-check.svg +1 -0
- package/examples/typography-test.svg +1 -0
- package/examples/typography-test.visual.json +757 -0
- package/examples/ui-demo-explainer-billing.svg +1 -0
- package/examples/ui-demo-explainer-check.svg +1 -0
- package/examples/ui-demo-explainer-save.svg +1 -0
- package/examples/ui-demo-explainer-toggle.svg +1 -0
- package/examples/ui-demo-explainer.mp4 +0 -0
- package/examples/ui-demo-explainer.visual.json +2597 -0
- package/examples/ui-flow.mp4 +0 -0
- package/examples/ui-flow.visual.json +1211 -0
- package/examples/walkthrough.mp4 +0 -0
- package/examples/walkthrough.visual.json +1372 -0
- package/package.json +52 -52
- package/schema/visual.schema.json +1086 -930
package/dist/src/validate.js
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateVisualDocument = validateVisualDocument;
|
|
4
|
-
const
|
|
4
|
+
const animatable_1 = require("./animatable");
|
|
5
5
|
const utils_1 = require("./utils");
|
|
6
|
-
const
|
|
6
|
+
const ELEMENT_TYPES = new Set(["path", "text", "image", "point", "group"]);
|
|
7
|
+
const TOP_LEVEL_FIELDS = new Set(["version", "canvas", "elements"]);
|
|
8
|
+
const CANVAS_FIELDS = new Set(["width", "height", "background", "duration", "fps"]);
|
|
9
|
+
const COMMON_ELEMENT_FIELDS = new Set([
|
|
10
|
+
"id",
|
|
11
|
+
"type",
|
|
12
|
+
"opacity",
|
|
13
|
+
"fill",
|
|
14
|
+
"stroke",
|
|
15
|
+
"strokeWidth",
|
|
16
|
+
"strokeCap",
|
|
17
|
+
"strokeJoin",
|
|
18
|
+
"miterLimit",
|
|
19
|
+
"dashArray",
|
|
20
|
+
"dashOffset",
|
|
21
|
+
"drawStart",
|
|
22
|
+
"drawEnd",
|
|
23
|
+
"effects",
|
|
24
|
+
"blendMode",
|
|
25
|
+
"rotation",
|
|
26
|
+
"scale",
|
|
27
|
+
"scaleX",
|
|
28
|
+
"scaleY",
|
|
29
|
+
"origin",
|
|
30
|
+
"clip",
|
|
31
|
+
"mask",
|
|
32
|
+
"timeline"
|
|
33
|
+
]);
|
|
34
|
+
const TYPE_FIELDS = {
|
|
35
|
+
path: new Set(["d", "x", "y"]),
|
|
36
|
+
text: new Set(["x", "y", "text", "lines", "align", "valign", "fontSize", "fontFamily", "weight", "fontStyle", "lineHeight", "letterSpacing", "maxWidth", "wrap"]),
|
|
37
|
+
image: new Set(["src", "x", "y", "width", "height", "fit", "source"]),
|
|
38
|
+
point: new Set(["x", "y"]),
|
|
39
|
+
group: new Set(["x", "y", "width", "height", "children"])
|
|
40
|
+
};
|
|
41
|
+
const TIMELINE_TRACK_FIELDS = new Set(["keyframes", "curve", "ease"]);
|
|
42
|
+
const TIMELINE_KEYFRAME_FIELDS = new Set(["time", "value", "in", "out", "interpolation"]);
|
|
7
43
|
function validateVisualDocument(document) {
|
|
8
44
|
const issues = [];
|
|
9
45
|
const warnings = [];
|
|
@@ -11,183 +47,174 @@ function validateVisualDocument(document) {
|
|
|
11
47
|
issues.push(issue("/", "invalid_document", "Document must be an object."));
|
|
12
48
|
return { ok: false, issues, warnings };
|
|
13
49
|
}
|
|
50
|
+
for (const key of Object.keys(document)) {
|
|
51
|
+
if (!TOP_LEVEL_FIELDS.has(key))
|
|
52
|
+
issues.push(issue(`/${key}`, "non_kernel_field", `Field '${key}' is not part of the render kernel.`));
|
|
53
|
+
}
|
|
14
54
|
if (document.version !== 1)
|
|
15
55
|
issues.push(issue("/version", "invalid_version", "Document version must be 1."));
|
|
16
|
-
|
|
17
|
-
issues.push(issue("/canvas", "missing_canvas", "Document must define canvas."));
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
if (!(0, utils_1.isFiniteNumber)(document.canvas.width))
|
|
21
|
-
issues.push(issue("/canvas/width", "missing_canvas_width", "Canvas width must be a number."));
|
|
22
|
-
if (!(0, utils_1.isFiniteNumber)(document.canvas.height))
|
|
23
|
-
issues.push(issue("/canvas/height", "missing_canvas_height", "Canvas height must be a number."));
|
|
24
|
-
if (document.canvas.background !== undefined && typeof document.canvas.background !== "string") {
|
|
25
|
-
issues.push(issue("/canvas/background", "invalid_canvas_background", "Canvas background must be a color string.", "For gradient, pattern, or image backgrounds, keep canvas.background as a fallback color string and add a full-canvas rect/image as the first element."));
|
|
26
|
-
}
|
|
27
|
-
if (document.canvas.space === "3d" && document.canvas.renderer !== "three") {
|
|
28
|
-
issues.push(issue("/canvas/renderer", "3d_requires_three", "3D documents must set canvas.renderer to 'three'."));
|
|
29
|
-
}
|
|
30
|
-
if (document.canvas.renderer === "three" && document.canvas.space !== "3d") {
|
|
31
|
-
issues.push(issue("/canvas/space", "three_requires_3d", "Three renderer requires canvas.space to be '3d'."));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
56
|
+
validateCanvas(document.canvas, issues);
|
|
34
57
|
const elements = document.elements ?? [];
|
|
35
58
|
if (document.elements !== undefined && !Array.isArray(elements))
|
|
36
59
|
issues.push(issue("/elements", "invalid_elements", "Document elements must be an array."));
|
|
37
60
|
const allElements = Array.isArray(elements) ? (0, utils_1.flattenElements)(elements) : [];
|
|
38
|
-
const ids = new
|
|
61
|
+
const ids = new Set();
|
|
39
62
|
for (const [index, element] of allElements.entries()) {
|
|
40
63
|
const path = `/elements/${index}`;
|
|
41
64
|
if (typeof element.id === "string") {
|
|
42
|
-
if (!/^[A-Za-z_][A-Za-z0-9_
|
|
43
|
-
issues.push(issue(`${path}/id`, "invalid_id", `Invalid id '${element.id}'. Use letters, numbers, '_' or '
|
|
65
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(element.id)) {
|
|
66
|
+
issues.push(issue(`${path}/id`, "invalid_id", `Invalid id '${element.id}'. Use letters, numbers, '_', '-' or '.'.`));
|
|
44
67
|
}
|
|
45
68
|
else if (ids.has(element.id)) {
|
|
46
69
|
issues.push(issue(`${path}/id`, "duplicate_id", `Duplicate element id '${element.id}'.`));
|
|
47
70
|
}
|
|
48
71
|
else {
|
|
49
|
-
ids.
|
|
72
|
+
ids.add(element.id);
|
|
50
73
|
}
|
|
51
74
|
}
|
|
52
75
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
for (const [sceneId, scene] of Object.entries(document.scenes ?? {})) {
|
|
57
|
-
if (!Array.isArray(scene.elements))
|
|
58
|
-
issues.push(issue(`/scenes/${sceneId}/elements`, "invalid_scene_elements", `Scene '${sceneId}' elements must be an array.`));
|
|
59
|
-
const sceneDoc = { ...document, canvas: { ...document.canvas, ...(scene.canvas ?? {}) }, elements: scene.elements, scenes: undefined, sequences: undefined };
|
|
60
|
-
const sceneResult = validateVisualDocument(sceneDoc);
|
|
61
|
-
issues.push(...sceneResult.issues.map((item) => ({ ...item, path: `/scenes/${sceneId}${item.path}` })));
|
|
62
|
-
warnings.push(...sceneResult.warnings.map((item) => ({ ...item, path: `/scenes/${sceneId}${item.path}` })));
|
|
63
|
-
}
|
|
64
|
-
for (const [sequenceId, sequence] of Object.entries(document.sequences ?? {})) {
|
|
65
|
-
if (!Array.isArray(sequence.clips))
|
|
66
|
-
issues.push(issue(`/sequences/${sequenceId}/clips`, "invalid_sequence_clips", `Sequence '${sequenceId}' clips must be an array.`));
|
|
67
|
-
for (const [index, clip] of (sequence.clips ?? []).entries()) {
|
|
68
|
-
if (!document.scenes?.[clip.scene])
|
|
69
|
-
issues.push(issue(`/sequences/${sequenceId}/clips/${index}/scene`, "unknown_sequence_scene", `Unknown scene '${clip.scene}' in sequence '${sequenceId}'.`));
|
|
70
|
-
if (!(0, utils_1.isFiniteNumber)(clip.duration) || clip.duration <= 0)
|
|
71
|
-
issues.push(issue(`/sequences/${sequenceId}/clips/${index}/duration`, "invalid_clip_duration", "Clip duration must be a positive number."));
|
|
72
|
-
validateTransition(clip.transition, `/sequences/${sequenceId}/clips/${index}/transition`, issues);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
76
|
+
if (Array.isArray(elements))
|
|
77
|
+
validateElementList(elements, "/elements", issues, warnings);
|
|
75
78
|
return { ok: issues.length === 0, issues, warnings };
|
|
76
79
|
}
|
|
77
|
-
function
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
if (value === "cut" || value === "fade")
|
|
81
|
-
return;
|
|
82
|
-
if (!value || typeof value !== "object") {
|
|
83
|
-
issues.push(issue(path, "invalid_transition", "Transition must be 'cut', 'fade', or an object with type/duration."));
|
|
80
|
+
function validateCanvas(canvas, issues) {
|
|
81
|
+
if (!canvas || typeof canvas !== "object") {
|
|
82
|
+
issues.push(issue("/canvas", "missing_canvas", "Document must define canvas."));
|
|
84
83
|
return;
|
|
85
84
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
for (const key of Object.keys(canvas)) {
|
|
86
|
+
if (!CANVAS_FIELDS.has(key))
|
|
87
|
+
issues.push(issue(`/canvas/${key}`, "non_kernel_canvas_field", `Canvas field '${key}' is not part of the render kernel.`));
|
|
88
|
+
}
|
|
89
|
+
if (!(0, utils_1.isFiniteNumber)(canvas.width))
|
|
90
|
+
issues.push(issue("/canvas/width", "missing_canvas_width", "Canvas width must be a number."));
|
|
91
|
+
if (!(0, utils_1.isFiniteNumber)(canvas.height))
|
|
92
|
+
issues.push(issue("/canvas/height", "missing_canvas_height", "Canvas height must be a number."));
|
|
93
|
+
if (canvas.background !== undefined && typeof canvas.background !== "string") {
|
|
94
|
+
issues.push(issue("/canvas/background", "invalid_canvas_background", "Canvas background must be a color string."));
|
|
95
|
+
}
|
|
96
|
+
if (canvas.duration !== undefined && (!(0, utils_1.isFiniteNumber)(canvas.duration) || canvas.duration < 0)) {
|
|
97
|
+
issues.push(issue("/canvas/duration", "invalid_canvas_duration", "Canvas duration must be a non-negative number."));
|
|
89
98
|
}
|
|
90
|
-
if (
|
|
91
|
-
issues.push(issue(
|
|
99
|
+
if (canvas.fps !== undefined && (!(0, utils_1.isFiniteNumber)(canvas.fps) || canvas.fps <= 0)) {
|
|
100
|
+
issues.push(issue("/canvas/fps", "invalid_canvas_fps", "Canvas fps must be a positive number."));
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
103
|
+
function validateElementList(elements, path, issues, warnings) {
|
|
104
|
+
for (const [index, element] of elements.entries())
|
|
105
|
+
validateElement(element, `${path}/${index}`, issues, warnings);
|
|
106
|
+
}
|
|
107
|
+
function validateElement(element, path, issues, warnings) {
|
|
108
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
96
109
|
issues.push(issue(path, "invalid_element", "Element must be an object."));
|
|
97
110
|
return;
|
|
98
111
|
}
|
|
99
|
-
const type = String(element.type
|
|
100
|
-
if (
|
|
101
|
-
issues.push(issue(`${path}/type`, "
|
|
112
|
+
const type = String(element.type ?? "");
|
|
113
|
+
if (!ELEMENT_TYPES.has(type)) {
|
|
114
|
+
issues.push(issue(`${path}/type`, "unsupported_type", `Unsupported kernel element type '${type}'.`));
|
|
102
115
|
return;
|
|
103
116
|
}
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
validateElementFields(element, type, path, issues);
|
|
118
|
+
if (type === "path") {
|
|
119
|
+
if (typeof element.d !== "string" || !element.d?.trim()) {
|
|
120
|
+
issues.push(issue(`${path}/d`, "missing_path_d", "Path elements require a non-empty d string."));
|
|
121
|
+
}
|
|
122
|
+
if (element.x !== undefined)
|
|
123
|
+
requireNumber(element.x, `${path}/x`, issues);
|
|
124
|
+
if (element.y !== undefined)
|
|
125
|
+
requireNumber(element.y, `${path}/y`, issues);
|
|
126
|
+
}
|
|
127
|
+
if (type === "text") {
|
|
128
|
+
requireNumber(element.x, `${path}/x`, issues);
|
|
129
|
+
requireNumber(element.y, `${path}/y`, issues);
|
|
130
|
+
if (element.text !== undefined && typeof element.text !== "string") {
|
|
131
|
+
issues.push(issue(`${path}/text`, "invalid_text", "Text must be a string."));
|
|
132
|
+
}
|
|
133
|
+
const lines = element.lines;
|
|
134
|
+
if (lines !== undefined && (!Array.isArray(lines) || lines.some((line) => typeof line !== "string"))) {
|
|
135
|
+
issues.push(issue(`${path}/lines`, "invalid_lines", "Text lines must be an array of strings."));
|
|
136
|
+
}
|
|
137
|
+
if (typeof element.text === "string" && element.text.length > 80 && !element.wrap && !(0, utils_1.isFiniteNumber)(element.maxWidth)) {
|
|
138
|
+
warnings.push(warning(`${path}/text`, "long_text_no_wrap", "Long text should use maxWidth/wrap or explicit line breaks."));
|
|
139
|
+
}
|
|
108
140
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
141
|
+
if (type === "image")
|
|
142
|
+
validateImage(element, path, issues);
|
|
143
|
+
if (type === "point") {
|
|
144
|
+
requireNumber(element.x, `${path}/x`, issues);
|
|
145
|
+
requireNumber(element.y, `${path}/y`, issues);
|
|
146
|
+
}
|
|
147
|
+
if (type === "group") {
|
|
148
|
+
requireNumber(element.x, `${path}/x`, issues);
|
|
149
|
+
requireNumber(element.y, `${path}/y`, issues);
|
|
150
|
+
if (element.width !== undefined)
|
|
151
|
+
requireNumber(element.width, `${path}/width`, issues);
|
|
152
|
+
if (element.height !== undefined)
|
|
153
|
+
requireNumber(element.height, `${path}/height`, issues);
|
|
154
|
+
const children = element.children;
|
|
155
|
+
if (!Array.isArray(children))
|
|
156
|
+
issues.push(issue(`${path}/children`, "invalid_group_children", "Group children must be an array."));
|
|
157
|
+
else
|
|
158
|
+
validateElementList(children, `${path}/children`, issues, warnings);
|
|
111
159
|
}
|
|
112
|
-
const context = createShapeValidationContext(document, path, ids, issues, warnings);
|
|
113
|
-
definition.validateGeometry(element, context);
|
|
114
160
|
validateStyle(element, path, issues);
|
|
115
|
-
|
|
116
|
-
definition.validateReferences?.(element, context);
|
|
117
|
-
definition.validateWarnings?.(element, context);
|
|
161
|
+
validateTimeline(element, element.timeline, `${path}/timeline`, issues, warnings);
|
|
118
162
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
issues,
|
|
125
|
-
warnings,
|
|
126
|
-
addIssue(issuePath, code, message, suggestion) {
|
|
127
|
-
issues.push(issue(issuePath, code, message, suggestion));
|
|
128
|
-
},
|
|
129
|
-
addWarning(warningPath, code, message, suggestion) {
|
|
130
|
-
warnings.push(warning(warningPath, code, message, suggestion));
|
|
131
|
-
},
|
|
132
|
-
requireNumber(value, numberPath) {
|
|
133
|
-
requireNumber(value, numberPath, issues);
|
|
134
|
-
},
|
|
135
|
-
requirePoint2(value, pointPath, code, message) {
|
|
136
|
-
if (!(0, utils_1.isPoint2)(value))
|
|
137
|
-
issues.push(issue(pointPath, code, message));
|
|
138
|
-
},
|
|
139
|
-
requirePoint2Array(value, minLength, pointPath, code, message) {
|
|
140
|
-
if (!(0, utils_1.isPoint2Array)(value, minLength))
|
|
141
|
-
issues.push(issue(pointPath, code, message));
|
|
142
|
-
},
|
|
143
|
-
requirePoint3(value, pointPath, code, message) {
|
|
144
|
-
if (!isPoint3(value))
|
|
145
|
-
issues.push(issue(pointPath, code, message));
|
|
146
|
-
},
|
|
147
|
-
validateEndpoint(value, endpointPath) {
|
|
148
|
-
validateEndpoint(value, endpointPath, ids, issues);
|
|
149
|
-
},
|
|
150
|
-
validateImageOptions(element) {
|
|
151
|
-
validateImageOptions(element, path, issues);
|
|
152
|
-
},
|
|
153
|
-
isFollowable(type) {
|
|
154
|
-
return (0, shapes_1.isFollowableAuthoringShape)(type);
|
|
163
|
+
function validateElementFields(element, type, path, issues) {
|
|
164
|
+
const typeFields = TYPE_FIELDS[type] ?? new Set();
|
|
165
|
+
for (const key of Object.keys(element)) {
|
|
166
|
+
if (!COMMON_ELEMENT_FIELDS.has(key) && !typeFields.has(key)) {
|
|
167
|
+
issues.push(issue(`${path}/${key}`, "non_kernel_element_field", `Field '${key}' is not valid on kernel ${type} elements.`));
|
|
155
168
|
}
|
|
156
|
-
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function validateImage(element, path, issues) {
|
|
172
|
+
if (typeof element.src !== "string" || !element.src)
|
|
173
|
+
issues.push(issue(`${path}/src`, "missing_image_src", "Image src must be a string."));
|
|
174
|
+
requireNumber(element.x, `${path}/x`, issues);
|
|
175
|
+
requireNumber(element.y, `${path}/y`, issues);
|
|
176
|
+
requireNumber(element.width, `${path}/width`, issues);
|
|
177
|
+
requireNumber(element.height, `${path}/height`, issues);
|
|
178
|
+
if ((0, utils_1.isFiniteNumber)(element.width) && element.width <= 0)
|
|
179
|
+
issues.push(issue(`${path}/width`, "invalid_image_width", "Image width must be positive."));
|
|
180
|
+
if ((0, utils_1.isFiniteNumber)(element.height) && element.height <= 0)
|
|
181
|
+
issues.push(issue(`${path}/height`, "invalid_image_height", "Image height must be positive."));
|
|
182
|
+
validateImageFit(element.fit, `${path}/fit`, issues);
|
|
183
|
+
if (!element.source)
|
|
184
|
+
return;
|
|
185
|
+
requireNumber(element.source.x, `${path}/source/x`, issues);
|
|
186
|
+
requireNumber(element.source.y, `${path}/source/y`, issues);
|
|
187
|
+
requireNumber(element.source.width, `${path}/source/width`, issues);
|
|
188
|
+
requireNumber(element.source.height, `${path}/source/height`, issues);
|
|
189
|
+
requireNumber(element.source.imageWidth, `${path}/source/imageWidth`, issues);
|
|
190
|
+
requireNumber(element.source.imageHeight, `${path}/source/imageHeight`, issues);
|
|
157
191
|
}
|
|
158
192
|
function validateStyle(element, path, issues) {
|
|
159
193
|
validatePaint(element.fill, `${path}/fill`, issues);
|
|
160
194
|
validatePaint(element.stroke, `${path}/stroke`, issues);
|
|
161
|
-
if (element.
|
|
195
|
+
if (element.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(element.opacity) || element.opacity < 0 || element.opacity > 1))
|
|
196
|
+
issues.push(issue(`${path}/opacity`, "invalid_opacity", "Opacity must be between 0 and 1."));
|
|
197
|
+
if (element.strokeWidth !== undefined && !(0, utils_1.isFiniteNumber)(element.strokeWidth))
|
|
198
|
+
issues.push(issue(`${path}/strokeWidth`, "invalid_stroke_width", "strokeWidth must be a number."));
|
|
199
|
+
if (element.strokeCap !== undefined && !["butt", "round", "square"].includes(String(element.strokeCap)))
|
|
162
200
|
issues.push(issue(`${path}/strokeCap`, "invalid_stroke_cap", "strokeCap must be 'butt', 'round', or 'square'."));
|
|
163
|
-
|
|
164
|
-
if (element.strokeJoin !== undefined && !["miter", "round", "bevel"].includes(String(element.strokeJoin))) {
|
|
201
|
+
if (element.strokeJoin !== undefined && !["miter", "round", "bevel"].includes(String(element.strokeJoin)))
|
|
165
202
|
issues.push(issue(`${path}/strokeJoin`, "invalid_stroke_join", "strokeJoin must be 'miter', 'round', or 'bevel'."));
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (element.
|
|
172
|
-
issues.push(issue(`${path}/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if ((0, utils_1.isFiniteNumber)(element.drawStart) && (0, utils_1.isFiniteNumber)(element.drawEnd) && element.drawStart > element.drawEnd)
|
|
203
|
+
for (const key of ["miterLimit", "dashOffset", "rotation", "scale", "scaleX", "scaleY", "fontSize", "lineHeight", "letterSpacing", "maxWidth"]) {
|
|
204
|
+
const value = element[key];
|
|
205
|
+
if (value !== undefined && !(0, utils_1.isFiniteNumber)(value))
|
|
206
|
+
issues.push(issue(`${path}/${key}`, "invalid_number", `${key} must be a finite number.`));
|
|
207
|
+
}
|
|
208
|
+
if (element.dashArray !== undefined && (!Array.isArray(element.dashArray) || element.dashArray.some((value) => !(0, utils_1.isFiniteNumber)(value))))
|
|
209
|
+
issues.push(issue(`${path}/dashArray`, "invalid_dash_array", "dashArray must be an array of numbers."));
|
|
210
|
+
if (element.drawStart !== undefined && (!(0, utils_1.isFiniteNumber)(element.drawStart) || element.drawStart < 0 || element.drawStart > 1))
|
|
211
|
+
issues.push(issue(`${path}/drawStart`, "invalid_draw_start", "drawStart must be between 0 and 1."));
|
|
212
|
+
if (element.drawEnd !== undefined && (!(0, utils_1.isFiniteNumber)(element.drawEnd) || element.drawEnd < 0 || element.drawEnd > 1))
|
|
213
|
+
issues.push(issue(`${path}/drawEnd`, "invalid_draw_end", "drawEnd must be between 0 and 1."));
|
|
214
|
+
if ((0, utils_1.isFiniteNumber)(element.drawStart) && (0, utils_1.isFiniteNumber)(element.drawEnd) && element.drawStart > element.drawEnd)
|
|
178
215
|
issues.push(issue(`${path}/drawStart`, "invalid_draw_range", "drawStart must be less than or equal to drawEnd."));
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
issues.push(issue(`${path}/rotation`, "invalid_rotation", "rotation must be a finite number in degrees."));
|
|
182
|
-
if (element.scale !== undefined && !(0, utils_1.isFiniteNumber)(element.scale))
|
|
183
|
-
issues.push(issue(`${path}/scale`, "invalid_scale", "scale must be a finite number."));
|
|
184
|
-
if (element.scaleX !== undefined && !(0, utils_1.isFiniteNumber)(element.scaleX))
|
|
185
|
-
issues.push(issue(`${path}/scaleX`, "invalid_scale_x", "scaleX must be a finite number."));
|
|
186
|
-
if (element.scaleY !== undefined && !(0, utils_1.isFiniteNumber)(element.scaleY))
|
|
187
|
-
issues.push(issue(`${path}/scaleY`, "invalid_scale_y", "scaleY must be a finite number."));
|
|
188
|
-
if (element.origin !== undefined && !(typeof element.origin === "string" && utils_1.ANCHORS.has(element.origin)) && !(0, utils_1.isPoint2)(element.origin)) {
|
|
189
|
-
issues.push(issue(`${path}/origin`, "invalid_origin", "origin must be an anchor name or [x,y]."));
|
|
190
|
-
}
|
|
216
|
+
if (element.origin !== undefined && !(0, utils_1.isPoint2)(element.origin))
|
|
217
|
+
issues.push(issue(`${path}/origin`, "invalid_origin", "origin must be [x,y]."));
|
|
191
218
|
validateEffects(element.effects, `${path}/effects`, issues);
|
|
192
219
|
validateClip(element.clip, `${path}/clip`, issues);
|
|
193
220
|
validateMask(element.mask, `${path}/mask`, issues);
|
|
@@ -195,8 +222,8 @@ function validateStyle(element, path, issues) {
|
|
|
195
222
|
function validatePaint(value, path, issues) {
|
|
196
223
|
if (value === undefined || typeof value === "string")
|
|
197
224
|
return;
|
|
198
|
-
if (!value || typeof value !== "object") {
|
|
199
|
-
issues.push(issue(path, "invalid_paint", "Paint must be a color string or
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
226
|
+
issues.push(issue(path, "invalid_paint", "Paint must be a color string or structured paint object."));
|
|
200
227
|
return;
|
|
201
228
|
}
|
|
202
229
|
if (value.type === "linearGradient") {
|
|
@@ -211,7 +238,7 @@ function validatePaint(value, path, issues) {
|
|
|
211
238
|
if (!(0, utils_1.isPoint2)(value.center))
|
|
212
239
|
issues.push(issue(`${path}/center`, "invalid_gradient_center", "Radial gradient center must be [x,y]."));
|
|
213
240
|
if (!(0, utils_1.isFiniteNumber)(value.radius) || value.radius < 0)
|
|
214
|
-
issues.push(issue(`${path}/radius`, "invalid_gradient_radius", "Radial gradient radius must be
|
|
241
|
+
issues.push(issue(`${path}/radius`, "invalid_gradient_radius", "Radial gradient radius must be non-negative."));
|
|
215
242
|
if (value.focus !== undefined && !(0, utils_1.isPoint2)(value.focus))
|
|
216
243
|
issues.push(issue(`${path}/focus`, "invalid_gradient_focus", "Radial gradient focus must be [x,y]."));
|
|
217
244
|
validateGradientStops(value.stops, `${path}/stops`, issues);
|
|
@@ -221,20 +248,13 @@ function validatePaint(value, path, issues) {
|
|
|
221
248
|
if (typeof value.src !== "string" || !value.src)
|
|
222
249
|
issues.push(issue(`${path}/src`, "invalid_pattern_src", "Pattern src must be a string."));
|
|
223
250
|
if (!(0, utils_1.isFiniteNumber)(value.width) || value.width <= 0)
|
|
224
|
-
issues.push(issue(`${path}/width`, "invalid_pattern_width", "Pattern width must be
|
|
251
|
+
issues.push(issue(`${path}/width`, "invalid_pattern_width", "Pattern width must be positive."));
|
|
225
252
|
if (!(0, utils_1.isFiniteNumber)(value.height) || value.height <= 0)
|
|
226
|
-
issues.push(issue(`${path}/height`, "invalid_pattern_height", "Pattern height must be
|
|
227
|
-
if (value.x !== undefined && !(0, utils_1.isFiniteNumber)(value.x))
|
|
228
|
-
issues.push(issue(`${path}/x`, "invalid_pattern_x", "Pattern x must be a finite number."));
|
|
229
|
-
if (value.y !== undefined && !(0, utils_1.isFiniteNumber)(value.y))
|
|
230
|
-
issues.push(issue(`${path}/y`, "invalid_pattern_y", "Pattern y must be a finite number."));
|
|
253
|
+
issues.push(issue(`${path}/height`, "invalid_pattern_height", "Pattern height must be positive."));
|
|
231
254
|
validateImageFit(value.fit, `${path}/fit`, issues);
|
|
232
|
-
if (value.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(value.opacity) || value.opacity < 0 || value.opacity > 1)) {
|
|
233
|
-
issues.push(issue(`${path}/opacity`, "invalid_pattern_opacity", "Pattern opacity must be between 0 and 1."));
|
|
234
|
-
}
|
|
235
255
|
return;
|
|
236
256
|
}
|
|
237
|
-
issues.push(issue(`${path}/type`, "invalid_paint_type", "Paint object type must be
|
|
257
|
+
issues.push(issue(`${path}/type`, "invalid_paint_type", "Paint object type must be linearGradient, radialGradient, or pattern."));
|
|
238
258
|
}
|
|
239
259
|
function validateGradientStops(value, path, issues) {
|
|
240
260
|
if (!Array.isArray(value) || value.length < 2) {
|
|
@@ -266,166 +286,192 @@ function validateEffects(value, path, issues) {
|
|
|
266
286
|
const shadow = value.shadow;
|
|
267
287
|
if (!shadow || typeof shadow !== "object" || Array.isArray(shadow)) {
|
|
268
288
|
issues.push(issue(`${path}/shadow`, "invalid_shadow", "shadow must be an object."));
|
|
289
|
+
return;
|
|
269
290
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
issues.push(issue(`${path}/shadow/opacity`, "invalid_shadow_opacity", "shadow opacity must be between 0 and 1."));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
291
|
+
requireNumber(shadow.dx, `${path}/shadow/dx`, issues);
|
|
292
|
+
requireNumber(shadow.dy, `${path}/shadow/dy`, issues);
|
|
293
|
+
requireNumber(shadow.blur, `${path}/shadow/blur`, issues);
|
|
294
|
+
if (typeof shadow.color !== "string")
|
|
295
|
+
issues.push(issue(`${path}/shadow/color`, "invalid_shadow_color", "shadow color must be a string."));
|
|
296
|
+
if (shadow.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(shadow.opacity) || shadow.opacity < 0 || shadow.opacity > 1))
|
|
297
|
+
issues.push(issue(`${path}/shadow/opacity`, "invalid_shadow_opacity", "shadow opacity must be between 0 and 1."));
|
|
280
298
|
}
|
|
281
299
|
}
|
|
282
300
|
function validateClip(value, path, issues) {
|
|
283
|
-
|
|
301
|
+
if (value === undefined)
|
|
302
|
+
return;
|
|
303
|
+
if (!value || typeof value !== "object" || value.type !== "path" || typeof value.d !== "string" || !value.d.trim())
|
|
304
|
+
issues.push(issue(path, "invalid_clip", "clip must be { type:'path', d:string }."));
|
|
284
305
|
}
|
|
285
306
|
function validateMask(value, path, issues) {
|
|
286
|
-
|
|
287
|
-
|
|
307
|
+
if (value === undefined)
|
|
308
|
+
return;
|
|
309
|
+
if (!value || typeof value !== "object" || value.type !== "path" || typeof value.d !== "string" || !value.d.trim())
|
|
310
|
+
issues.push(issue(path, "invalid_mask", "mask must be { type:'path', d:string, opacity?:number }."));
|
|
311
|
+
if (value && value.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(value.opacity) || value.opacity < 0 || value.opacity > 1))
|
|
288
312
|
issues.push(issue(`${path}/opacity`, "invalid_mask_opacity", "mask opacity must be between 0 and 1."));
|
|
289
|
-
}
|
|
290
313
|
}
|
|
291
|
-
function
|
|
314
|
+
function validateTimeline(element, value, path, issues, warnings) {
|
|
292
315
|
if (value === undefined)
|
|
293
316
|
return;
|
|
294
|
-
if (!value || typeof value !== "object") {
|
|
295
|
-
issues.push(issue(path,
|
|
317
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
318
|
+
issues.push(issue(path, "invalid_timeline", "timeline must be an object."));
|
|
296
319
|
return;
|
|
297
320
|
}
|
|
298
|
-
if (value.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
321
|
+
if (value.start !== undefined && (!(0, utils_1.isFiniteNumber)(value.start) || value.start < 0))
|
|
322
|
+
issues.push(issue(`${path}/start`, "invalid_timeline_start", "timeline.start must be a non-negative number."));
|
|
323
|
+
if (value.end !== undefined && (!(0, utils_1.isFiniteNumber)(value.end) || value.end < 0))
|
|
324
|
+
issues.push(issue(`${path}/end`, "invalid_timeline_end", "timeline.end must be a non-negative number."));
|
|
325
|
+
if ((0, utils_1.isFiniteNumber)(value.start) && (0, utils_1.isFiniteNumber)(value.end) && value.end < value.start)
|
|
326
|
+
issues.push(issue(`${path}/end`, "invalid_timeline_range", "timeline.end must be greater than or equal to timeline.start."));
|
|
327
|
+
if (value.tracks === undefined)
|
|
303
328
|
return;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
requireNumber(value.cx, `${path}/cx`, issues);
|
|
307
|
-
requireNumber(value.cy, `${path}/cy`, issues);
|
|
308
|
-
requireNumber(value.radius, `${path}/radius`, issues);
|
|
329
|
+
if (!value.tracks || typeof value.tracks !== "object" || Array.isArray(value.tracks)) {
|
|
330
|
+
issues.push(issue(`${path}/tracks`, "invalid_timeline_tracks", "timeline.tracks must be an object."));
|
|
309
331
|
return;
|
|
310
332
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
333
|
+
for (const [property, track] of Object.entries(value.tracks))
|
|
334
|
+
validateTrack(element, property, track, `${path}/tracks/${property}`, issues, warnings);
|
|
335
|
+
for (const message of (0, animatable_1.conflictWarningsForTracks)(Object.keys(value.tracks))) {
|
|
336
|
+
warnings.push(warning(`${path}/tracks`, "conflicting_timeline_tracks", message));
|
|
315
337
|
}
|
|
316
|
-
issues.push(issue(`${path}/type`, `invalid_${label}_type`, `${label} type must be 'rect', 'circle', or 'path'.`));
|
|
317
338
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
339
|
+
function validateTrack(element, property, track, path, issues, warnings) {
|
|
340
|
+
if (!track || typeof track !== "object" || Array.isArray(track)) {
|
|
341
|
+
issues.push(issue(path, "invalid_timeline_track", "Timeline track must be an object."));
|
|
321
342
|
return;
|
|
322
|
-
requireNumber(element.source.x, `${path}/source/x`, issues);
|
|
323
|
-
requireNumber(element.source.y, `${path}/source/y`, issues);
|
|
324
|
-
requireNumber(element.source.width, `${path}/source/width`, issues);
|
|
325
|
-
requireNumber(element.source.height, `${path}/source/height`, issues);
|
|
326
|
-
requireNumber(element.source.imageWidth, `${path}/source/imageWidth`, issues);
|
|
327
|
-
requireNumber(element.source.imageHeight, `${path}/source/imageHeight`, issues);
|
|
328
|
-
if ((0, utils_1.isFiniteNumber)(element.source.width) && element.source.width <= 0)
|
|
329
|
-
issues.push(issue(`${path}/source/width`, "invalid_source_width", "Image source width must be positive."));
|
|
330
|
-
if ((0, utils_1.isFiniteNumber)(element.source.height) && element.source.height <= 0)
|
|
331
|
-
issues.push(issue(`${path}/source/height`, "invalid_source_height", "Image source height must be positive."));
|
|
332
|
-
if ((0, utils_1.isFiniteNumber)(element.source.imageWidth) && element.source.imageWidth <= 0)
|
|
333
|
-
issues.push(issue(`${path}/source/imageWidth`, "invalid_source_image_width", "Image source imageWidth must be positive."));
|
|
334
|
-
if ((0, utils_1.isFiniteNumber)(element.source.imageHeight) && element.source.imageHeight <= 0)
|
|
335
|
-
issues.push(issue(`${path}/source/imageHeight`, "invalid_source_image_height", "Image source imageHeight must be positive."));
|
|
336
|
-
}
|
|
337
|
-
function validateImageFit(value, path, issues) {
|
|
338
|
-
if (value !== undefined && value !== "fill" && value !== "contain" && value !== "cover") {
|
|
339
|
-
issues.push(issue(path, "invalid_image_fit", "fit must be 'fill', 'contain', or 'cover'."));
|
|
340
343
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
for (const key of Object.keys(track)) {
|
|
345
|
+
if (!TIMELINE_TRACK_FIELDS.has(key))
|
|
346
|
+
issues.push(issue(`${path}/${key}`, "non_kernel_timeline_track_field", `Timeline track field '${key}' is not part of the render kernel.`));
|
|
347
|
+
}
|
|
348
|
+
validateTimelineCurve(track.curve, `${path}/curve`, issues);
|
|
349
|
+
if (track.ease !== undefined && typeof track.ease !== "string")
|
|
350
|
+
issues.push(issue(`${path}/ease`, "invalid_timeline_ease", "Timeline ease must be a string."));
|
|
351
|
+
const propertySpec = (0, animatable_1.animatablePropertySpec)(element, property);
|
|
352
|
+
if (!propertySpec) {
|
|
353
|
+
const code = (0, animatable_1.knownAnimatableProperty)(property) ? "unsupported_timeline_track_for_element" : "unknown_timeline_track";
|
|
354
|
+
issues.push(issue(path, code, `Timeline track '${property}' is not a supported animatable property for ${element.type} elements.`));
|
|
355
|
+
}
|
|
356
|
+
if (!Array.isArray(track.keyframes) || !track.keyframes.length) {
|
|
357
|
+
issues.push(issue(`${path}/keyframes`, "invalid_timeline_keyframes", "Track keyframes must be a non-empty array."));
|
|
344
358
|
return;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
const hasStaticValue = property in element || (POSITION_ANIMATION_PROPERTIES.has(property) && "position" in element);
|
|
352
|
-
if (!hasStaticValue && property !== "opacity") {
|
|
353
|
-
issues.push(issue(`${path}/animate/${property}`, "missing_static_animation_property", `Animated property '${property}' must also have a static value.`));
|
|
354
|
-
}
|
|
355
|
-
if (!animation || typeof animation !== "object") {
|
|
356
|
-
issues.push(issue(`${path}/animate/${property}`, "invalid_animation", "Animation must be an object with from/to or keyframes."));
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
if (Array.isArray(animation)) {
|
|
360
|
-
issues.push(issue(`${path}/animate/${property}`, "invalid_animation", "Animation must be an object. Use { keyframes: [...] }, not a raw keyframe array."));
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
const values = [animation.from, animation.to].filter((value) => value !== undefined);
|
|
364
|
-
let hasTimeline = animation.from !== undefined || animation.to !== undefined;
|
|
365
|
-
if (animation.duration !== undefined && !(0, utils_1.isFiniteNumber)(animation.duration)) {
|
|
366
|
-
issues.push(issue(`${path}/animate/${property}/duration`, "invalid_animation_duration", "Animation duration must be a finite number."));
|
|
367
|
-
}
|
|
368
|
-
if (animation.delay !== undefined && !(0, utils_1.isFiniteNumber)(animation.delay)) {
|
|
369
|
-
issues.push(issue(`${path}/animate/${property}/delay`, "invalid_animation_delay", "Animation delay must be a finite number."));
|
|
370
|
-
}
|
|
371
|
-
if (animation.keyframes !== undefined) {
|
|
372
|
-
if (!Array.isArray(animation.keyframes)) {
|
|
373
|
-
issues.push(issue(`${path}/animate/${property}/keyframes`, "invalid_animation_keyframes", "Animation keyframes must be an array of [time,value] pairs."));
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
hasTimeline = hasTimeline || animation.keyframes.length > 0;
|
|
377
|
-
for (const [index, frame] of animation.keyframes.entries()) {
|
|
378
|
-
if (!Array.isArray(frame) || frame.length !== 2) {
|
|
379
|
-
issues.push(issue(`${path}/animate/${property}/keyframes/${index}`, "invalid_animation_keyframe", "Animation keyframe must be [time,value]."));
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if (!(0, utils_1.isFiniteNumber)(frame[0])) {
|
|
383
|
-
issues.push(issue(`${path}/animate/${property}/keyframes/${index}/0`, "invalid_animation_keyframe_time", "Animation keyframe time must be a finite number of seconds."));
|
|
384
|
-
}
|
|
385
|
-
values.push(frame[1]);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (!hasTimeline) {
|
|
390
|
-
issues.push(issue(`${path}/animate/${property}`, "invalid_animation", "Animation must define from/to or keyframes."));
|
|
359
|
+
}
|
|
360
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
361
|
+
for (const [index, frame] of track.keyframes.entries()) {
|
|
362
|
+
const time = validateKeyframe(frame, `${path}/keyframes/${index}`, issues, propertySpec);
|
|
363
|
+
if (time === undefined)
|
|
391
364
|
continue;
|
|
365
|
+
if (time < previous)
|
|
366
|
+
issues.push(issue(`${path}/keyframes/${index}/time`, "unsorted_timeline_keyframes", "Keyframe times must be sorted."));
|
|
367
|
+
previous = time;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function validateKeyframe(frame, path, issues, propertySpec) {
|
|
371
|
+
if (Array.isArray(frame)) {
|
|
372
|
+
if (frame.length !== 2) {
|
|
373
|
+
issues.push(issue(path, "invalid_timeline_keyframe", "Keyframe tuple must be [time,value]."));
|
|
374
|
+
return undefined;
|
|
392
375
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (!(0, utils_1.isFiniteNumber)(value)) {
|
|
397
|
-
issues.push(issue(`${path}/animate/${property}`, "invalid_animation_value", `Animation values for '${property}' must be finite numbers${property === "fill" || property === "stroke" ? " or color strings" : ""}.`));
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
376
|
+
if (!(0, utils_1.isFiniteNumber)(frame[0])) {
|
|
377
|
+
issues.push(issue(`${path}/0`, "invalid_timeline_keyframe_time", "Keyframe time must be finite seconds."));
|
|
378
|
+
return undefined;
|
|
400
379
|
}
|
|
380
|
+
validateTimelineValue(frame[1], `${path}/1`, issues, propertySpec);
|
|
381
|
+
return frame[0];
|
|
382
|
+
}
|
|
383
|
+
if (!frame || typeof frame !== "object") {
|
|
384
|
+
issues.push(issue(path, "invalid_timeline_keyframe", "Keyframe must be [time,value] or { time, value }."));
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
for (const key of Object.keys(frame)) {
|
|
388
|
+
if (!TIMELINE_KEYFRAME_FIELDS.has(key))
|
|
389
|
+
issues.push(issue(`${path}/${key}`, "non_kernel_timeline_keyframe_field", `Timeline keyframe field '${key}' is not part of the render kernel.`));
|
|
390
|
+
}
|
|
391
|
+
if (!(0, utils_1.isFiniteNumber)(frame.time)) {
|
|
392
|
+
issues.push(issue(`${path}/time`, "invalid_timeline_keyframe_time", "Keyframe time must be finite seconds."));
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
validateTimelineValue(frame.value, `${path}/value`, issues, propertySpec);
|
|
396
|
+
validateTimelineCurve(frame.in, `${path}/in`, issues);
|
|
397
|
+
validateTimelineCurve(frame.out, `${path}/out`, issues);
|
|
398
|
+
validateTimelineCurve(frame.interpolation, `${path}/interpolation`, issues);
|
|
399
|
+
return frame.time;
|
|
400
|
+
}
|
|
401
|
+
function validateTimelineValue(value, path, issues, propertySpec) {
|
|
402
|
+
if (!(0, animatable_1.isTimelineValue)(value)) {
|
|
403
|
+
issues.push(issue(path, "invalid_timeline_value", "Track value must be a JSON-safe timeline value."));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (propertySpec && !(0, animatable_1.validateMotionValueForProperty)(propertySpec, value)) {
|
|
407
|
+
issues.push(issue(path, "invalid_timeline_value_for_property", `Track value is not valid for '${propertySpec.property}'.`));
|
|
401
408
|
}
|
|
402
409
|
}
|
|
403
|
-
function
|
|
404
|
-
if (
|
|
410
|
+
function validateTimelineCurve(curve, path, issues) {
|
|
411
|
+
if (curve === undefined)
|
|
412
|
+
return;
|
|
413
|
+
if (!curve || typeof curve !== "object" || Array.isArray(curve)) {
|
|
414
|
+
issues.push(issue(path, "invalid_timeline_curve", "Timeline curve must be an object."));
|
|
405
415
|
return;
|
|
406
|
-
|
|
407
|
-
|
|
416
|
+
}
|
|
417
|
+
if (curve.type === "hold") {
|
|
418
|
+
validateAllowedKeys(curve, path, new Set(["type"]), issues);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (curve.type === "cubicBezier") {
|
|
422
|
+
validateAllowedKeys(curve, path, new Set(["type", "x1", "y1", "x2", "y2"]), issues);
|
|
423
|
+
if (!(0, utils_1.isFiniteNumber)(curve.x1) || curve.x1 < 0 || curve.x1 > 1)
|
|
424
|
+
issues.push(issue(`${path}/x1`, "invalid_curve_x1", "cubicBezier.x1 must be between 0 and 1."));
|
|
425
|
+
if (!(0, utils_1.isFiniteNumber)(curve.x2) || curve.x2 < 0 || curve.x2 > 1)
|
|
426
|
+
issues.push(issue(`${path}/x2`, "invalid_curve_x2", "cubicBezier.x2 must be between 0 and 1."));
|
|
427
|
+
if (!(0, utils_1.isFiniteNumber)(curve.y1))
|
|
428
|
+
issues.push(issue(`${path}/y1`, "invalid_curve_y1", "cubicBezier.y1 must be a finite number."));
|
|
429
|
+
if (!(0, utils_1.isFiniteNumber)(curve.y2))
|
|
430
|
+
issues.push(issue(`${path}/y2`, "invalid_curve_y2", "cubicBezier.y2 must be a finite number."));
|
|
408
431
|
return;
|
|
409
432
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
433
|
+
if (curve.type === "graph") {
|
|
434
|
+
validateAllowedKeys(curve, path, new Set(["type", "points"]), issues);
|
|
435
|
+
if (!Array.isArray(curve.points) || curve.points.length < 2) {
|
|
436
|
+
issues.push(issue(`${path}/points`, "invalid_curve_points", "Graph curve points must contain at least two [x,y] points."));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let previousX = Number.NEGATIVE_INFINITY;
|
|
440
|
+
for (const [index, point] of curve.points.entries()) {
|
|
441
|
+
if (!(0, utils_1.isPoint2)(point)) {
|
|
442
|
+
issues.push(issue(`${path}/points/${index}`, "invalid_curve_point", "Graph curve points must be [x,y]."));
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (point[0] < 0 || point[0] > 1)
|
|
446
|
+
issues.push(issue(`${path}/points/${index}/0`, "invalid_curve_point_x", "Graph curve x values must be between 0 and 1."));
|
|
447
|
+
if (point[0] <= previousX)
|
|
448
|
+
issues.push(issue(`${path}/points/${index}/0`, "unsorted_curve_points", "Graph curve x values must be strictly increasing."));
|
|
449
|
+
previousX = point[0];
|
|
450
|
+
}
|
|
451
|
+
const first = curve.points[0];
|
|
452
|
+
const last = curve.points[curve.points.length - 1];
|
|
453
|
+
if ((0, utils_1.isPoint2)(first) && first[0] !== 0)
|
|
454
|
+
issues.push(issue(`${path}/points/0/0`, "invalid_curve_start", "Graph curve must start at x=0."));
|
|
455
|
+
if ((0, utils_1.isPoint2)(last) && last[0] !== 1)
|
|
456
|
+
issues.push(issue(`${path}/points/${curve.points.length - 1}/0`, "invalid_curve_end", "Graph curve must end at x=1."));
|
|
414
457
|
return;
|
|
415
458
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
459
|
+
issues.push(issue(`${path}/type`, "invalid_timeline_curve_type", "Timeline curve type must be graph, cubicBezier, or hold."));
|
|
460
|
+
}
|
|
461
|
+
function validateAllowedKeys(value, path, allowed, issues) {
|
|
462
|
+
for (const key of Object.keys(value)) {
|
|
463
|
+
if (!allowed.has(key))
|
|
464
|
+
issues.push(issue(`${path}/${key}`, "non_kernel_timeline_curve_field", `Timeline curve field '${key}' is not part of this curve type.`));
|
|
420
465
|
}
|
|
421
466
|
}
|
|
467
|
+
function validateImageFit(value, path, issues) {
|
|
468
|
+
if (value !== undefined && value !== "fill" && value !== "contain" && value !== "cover")
|
|
469
|
+
issues.push(issue(path, "invalid_image_fit", "fit must be 'fill', 'contain', or 'cover'."));
|
|
470
|
+
}
|
|
422
471
|
function requireNumber(value, path, issues) {
|
|
423
472
|
if (!(0, utils_1.isFiniteNumber)(value))
|
|
424
473
|
issues.push(issue(path, "missing_number", "A finite number is required."));
|
|
425
474
|
}
|
|
426
|
-
function isPoint3(value) {
|
|
427
|
-
return Array.isArray(value) && value.length === 3 && (0, utils_1.isFiniteNumber)(value[0]) && (0, utils_1.isFiniteNumber)(value[1]) && (0, utils_1.isFiniteNumber)(value[2]);
|
|
428
|
-
}
|
|
429
475
|
function issue(path, code, message, suggestion) {
|
|
430
476
|
return { path, code, message, ...(suggestion ? { suggestion } : {}) };
|
|
431
477
|
}
|