@hyperframes/lint 0.8.145 → 0.8.147
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/dist/browser.js +45 -46
- package/dist/index.js +60 -62
- package/package.json +2 -2
- package/dist/browser.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/browser.js
CHANGED
|
@@ -1077,7 +1077,7 @@ function describeRootDimensionsDrift(source, dataWidth, dataHeight) {
|
|
|
1077
1077
|
const declared = `Root composition declares data-width="${dataWidth}" data-height="${dataHeight}"`;
|
|
1078
1078
|
if (!bodyCssMismatch) {
|
|
1079
1079
|
return {
|
|
1080
|
-
message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture
|
|
1080
|
+
message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture (the renderer sizes the viewport from the root's own data-width/data-height), so this is stale metadata, not a clipping risk.`,
|
|
1081
1081
|
fixHint: "update the meta viewport to match, or scaffold with `hyperframes init --resolution portrait`"
|
|
1082
1082
|
};
|
|
1083
1083
|
}
|
|
@@ -1349,7 +1349,7 @@ var coreRules = [
|
|
|
1349
1349
|
findings.push({
|
|
1350
1350
|
code: "unclosed_tag_swallowed_element",
|
|
1351
1351
|
severity: "error",
|
|
1352
|
-
message: `<${tag.name}> is missing its closing \`>\` before the next
|
|
1352
|
+
message: `<${tag.name}> is missing its closing \`>\` before the next \`<\`, so the following element is swallowed as bogus attribute text and never becomes a real node.`,
|
|
1353
1353
|
fixHint: "Close the previous tag's `>` before opening the next element.",
|
|
1354
1354
|
snippet: truncateSnippet(tag.raw)
|
|
1355
1355
|
});
|
|
@@ -2064,7 +2064,7 @@ var mediaRules = [
|
|
|
2064
2064
|
severity: "error",
|
|
2065
2065
|
message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.`,
|
|
2066
2066
|
elementId,
|
|
2067
|
-
fixHint: 'Add data-has-audio="true" when the file has sound (recommended
|
|
2067
|
+
fixHint: 'Add data-has-audio="true" when the file has sound (recommended: it keeps picture and sound on one clip), or add `muted` for silent footage.',
|
|
2068
2068
|
snippet: truncateSnippet(tag.raw)
|
|
2069
2069
|
});
|
|
2070
2070
|
}
|
|
@@ -2149,7 +2149,7 @@ var mediaRules = [
|
|
|
2149
2149
|
severity: "error",
|
|
2150
2150
|
message: `Self-closing <${tagName}/> is invalid HTML. The browser will leave the tag open, swallowing all subsequent elements as invisible fallback content. This makes compositions INVISIBLE.`,
|
|
2151
2151
|
elementId,
|
|
2152
|
-
fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}
|
|
2152
|
+
fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}>. Media elements MUST have explicit closing tags.`,
|
|
2153
2153
|
snippet: truncateSnippet(scMatch[0])
|
|
2154
2154
|
});
|
|
2155
2155
|
}
|
|
@@ -2192,7 +2192,7 @@ var mediaRules = [
|
|
|
2192
2192
|
findings.push({
|
|
2193
2193
|
code: "base64_media_prohibited",
|
|
2194
2194
|
severity: "error",
|
|
2195
|
-
message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? "
|
|
2195
|
+
message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? ", likely fabricated data" : ""}. Base64 media is prohibited: it bloats file size and breaks rendering.`,
|
|
2196
2196
|
fixHint: "Use a relative path (assets/music.mp3) or HTTPS URL for the audio/video src. Never embed media as base64.",
|
|
2197
2197
|
snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "...")
|
|
2198
2198
|
});
|
|
@@ -2221,7 +2221,7 @@ var mediaRules = [
|
|
|
2221
2221
|
findings.push({
|
|
2222
2222
|
code: "media_missing_id",
|
|
2223
2223
|
severity: "error",
|
|
2224
|
-
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements
|
|
2224
|
+
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements, so this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
|
|
2225
2225
|
fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
|
|
2226
2226
|
snippet: truncateSnippet(tag.raw)
|
|
2227
2227
|
});
|
|
@@ -2278,7 +2278,7 @@ var mediaRules = [
|
|
|
2278
2278
|
findings.push({
|
|
2279
2279
|
code: "media_crossorigin_breaks_preview",
|
|
2280
2280
|
severity: "error",
|
|
2281
|
-
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work
|
|
2281
|
+
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work, which hides the bug.`,
|
|
2282
2282
|
elementId,
|
|
2283
2283
|
fixHint: "Remove the crossorigin attribute unless you read the media back via canvas/WebGL/WebAudio AND the host is known to send CORS headers. Plain displayed media never needs it.",
|
|
2284
2284
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2312,7 +2312,7 @@ var mediaRules = [
|
|
|
2312
2312
|
findings.push({
|
|
2313
2313
|
code: "video_audio_double_source",
|
|
2314
2314
|
severity: "error",
|
|
2315
|
-
message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio
|
|
2315
|
+
message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio, so the duplicate <audio> will cause double playback and echo.`,
|
|
2316
2316
|
elementId: audioInfo.id,
|
|
2317
2317
|
fixHint: "Remove the <audio> element and let the video carry its own sound (recommended), or mute the video (add `muted`) and keep the separate <audio> when picture and sound must be cut independently.",
|
|
2318
2318
|
snippet: truncateSnippet(audioInfo.raw)
|
|
@@ -2348,7 +2348,7 @@ function findVolumeTweenOverridesGainFindings(ctx) {
|
|
|
2348
2348
|
findings.push({
|
|
2349
2349
|
code: "audio_volume_tween_overrides_gain",
|
|
2350
2350
|
severity: "warning",
|
|
2351
|
-
message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute
|
|
2351
|
+
message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute: they REPLACE this gain rather than scale it, so wherever the tween names a value the clip plays at that value, not at ${db}.`,
|
|
2352
2352
|
elementId: id,
|
|
2353
2353
|
fixHint: "Write the tween's targets in the same absolute gain (e.g. `volume: 1.95`, not `volume: 1`), or reset data-volume to 1 and let the tween carry the level on its own.",
|
|
2354
2354
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2367,7 +2367,7 @@ function findVolumeDoubleAutomationFindings(ctx) {
|
|
|
2367
2367
|
findings.push({
|
|
2368
2368
|
code: "audio_volume_double_automation",
|
|
2369
2369
|
severity: "warning",
|
|
2370
|
-
message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins
|
|
2370
|
+
message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins, and the tween is ignored in preview and in the render.`,
|
|
2371
2371
|
elementId: id,
|
|
2372
2372
|
fixHint: "Keep one of them: delete the volume lane to go back to tweening, or drop the tween and shape the level in the automation lane.",
|
|
2373
2373
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2415,7 +2415,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
|
|
|
2415
2415
|
severity: "warning",
|
|
2416
2416
|
message: `${elementId ? `#${elementId}'s` : "This"} carve names ${clipIds.length} voice clips directly (${clipIds.join(", ")}) instead of a group.`,
|
|
2417
2417
|
elementId,
|
|
2418
|
-
fixHint: "Group the voice clips and carve against the group
|
|
2418
|
+
fixHint: "Group the voice clips and carve against the group. A hand-rolled clip list silently rots when a clip is added.",
|
|
2419
2419
|
snippet: truncateSnippet(tag.raw)
|
|
2420
2420
|
});
|
|
2421
2421
|
}
|
|
@@ -2464,7 +2464,7 @@ function findAudioGroupTimingAttrFindings(ctx) {
|
|
|
2464
2464
|
findings.push({
|
|
2465
2465
|
code: "audio_group_timing_attrs",
|
|
2466
2466
|
severity: "warning",
|
|
2467
|
-
message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for
|
|
2467
|
+
message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for: its members carry the timing and its automation clock is composition time.`,
|
|
2468
2468
|
elementId,
|
|
2469
2469
|
fixHint: `Remove ${present.map((attr) => `\`${attr}\``).join(", ")} from the group element.`,
|
|
2470
2470
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2481,7 +2481,7 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2481
2481
|
findings.push({
|
|
2482
2482
|
code: "audio_group_carve_attr",
|
|
2483
2483
|
severity: "warning",
|
|
2484
|
-
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved
|
|
2484
|
+
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved. A bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
|
|
2485
2485
|
elementId,
|
|
2486
2486
|
fixHint: "Remove `data-fx-carve` and the `fromCarve` nodes it wrote into this bus's `data-fx-chain`, and carve the bed clip instead.",
|
|
2487
2487
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -3159,7 +3159,7 @@ var compositionRules = [
|
|
|
3159
3159
|
findings.push({
|
|
3160
3160
|
code: "root_composition_missing_html_wrapper",
|
|
3161
3161
|
severity: "error",
|
|
3162
|
-
message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment
|
|
3162
|
+
message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment: browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
|
|
3163
3163
|
fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
|
|
3164
3164
|
snippet: rootTag ? truncateSnippet(rootTag.raw) : void 0
|
|
3165
3165
|
});
|
|
@@ -3314,7 +3314,7 @@ var compositionRules = [
|
|
|
3314
3314
|
findings.push({
|
|
3315
3315
|
code: "unknown_variable_binding",
|
|
3316
3316
|
severity: "warning",
|
|
3317
|
-
message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables
|
|
3317
|
+
message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables, so the binding will silently keep the authored fallback.`,
|
|
3318
3318
|
fixHint: `Declare the variable on the composition root (<html>, or the [data-composition-id] root element for a template/fragment comp): data-composition-variables='[{"id":"${id}","type":"${attr === "data-var-src" ? "image" : "string"}","label":"${id}","default":"..."}]', or fix the binding id.`,
|
|
3319
3319
|
elementId: readAttr(tag.raw, "id") || void 0,
|
|
3320
3320
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -3355,8 +3355,8 @@ var compositionRules = [
|
|
|
3355
3355
|
{
|
|
3356
3356
|
code: "html_dir_attribute_breaks_render",
|
|
3357
3357
|
severity: "error",
|
|
3358
|
-
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render
|
|
3359
|
-
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead
|
|
3358
|
+
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render. This is a confirmed, silent failure.`,
|
|
3359
|
+
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead. Text still shapes correctly via the browser's own bidi algorithm.`,
|
|
3360
3360
|
snippet: truncateSnippet(htmlTag.raw)
|
|
3361
3361
|
}
|
|
3362
3362
|
];
|
|
@@ -3394,7 +3394,7 @@ var compositionRules = [
|
|
|
3394
3394
|
findings.push({
|
|
3395
3395
|
code: "subcomposition_blanks_before_host",
|
|
3396
3396
|
severity: "warning",
|
|
3397
|
-
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s
|
|
3397
|
+
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s, so its slot will be blank for ~${gap}s.`,
|
|
3398
3398
|
elementId,
|
|
3399
3399
|
fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
|
|
3400
3400
|
snippet: truncateSnippet(t.tag.raw)
|
|
@@ -3433,7 +3433,7 @@ var compositionRules = [
|
|
|
3433
3433
|
severity: "error",
|
|
3434
3434
|
message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the scoped element itself. Since #1886 the producer preserves the authored root as an inner wrapper, so this no longer renders the scene unstyled, but #root is the shape the scoper special-cases and the registry blocks model. Use it so preview, render, and Studio agree.`,
|
|
3435
3435
|
selector: example,
|
|
3436
|
-
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`)
|
|
3436
|
+
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`). The runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
|
|
3437
3437
|
snippet: truncateSnippet(rootTag.raw)
|
|
3438
3438
|
}
|
|
3439
3439
|
];
|
|
@@ -3516,7 +3516,7 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3516
3516
|
{
|
|
3517
3517
|
code: "root_composition_missing_duration_source",
|
|
3518
3518
|
severity: "error",
|
|
3519
|
-
message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection)
|
|
3519
|
+
message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection), so render will fail with "Composition has zero duration".`,
|
|
3520
3520
|
fixHint: 'Add data-duration="<seconds>" to the root element.',
|
|
3521
3521
|
snippet: truncateSnippet(rootTag.raw)
|
|
3522
3522
|
}
|
|
@@ -3579,8 +3579,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3579
3579
|
{
|
|
3580
3580
|
code: "composition_heavy_overlay_count_high",
|
|
3581
3581
|
severity: "warning",
|
|
3582
|
-
message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements
|
|
3583
|
-
fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute
|
|
3582
|
+
message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements (including opacity:0 / visibility:hidden ones) captures solid-black for the first ~half of the render, recovering near the end. Reproduces identically via drawElement, forced screenshot capture, and snapshot, so the capture layer itself is the offender (not encoder/mux). Independent of duration. Presence alone matters; only display:none elements are excluded here.`,
|
|
3583
|
+
fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute: either remove truly unused ones from the source or scope them into their own per-transition sub-composition. If an overlay is genuinely inert for the whole clip, use display:none so it never enters the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`
|
|
3584
3584
|
}
|
|
3585
3585
|
];
|
|
3586
3586
|
},
|
|
@@ -3623,8 +3623,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3623
3623
|
{
|
|
3624
3624
|
code: "root_zoom_rescales_a_fixed_canvas",
|
|
3625
3625
|
severity: "error",
|
|
3626
|
-
message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels
|
|
3627
|
-
fixHint: `Author the composition at its real size
|
|
3626
|
+
message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels. It is inside the declared composition and absent from the output, with nothing else reporting it.` : `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
|
|
3627
|
+
fixHint: `Author the composition at its real size: set data-width/data-height (and the root's width/height) to the dimensions you want, and drop the canvas-level \`zoom\`. To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. \`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
|
|
3628
3628
|
snippet: hit.snippet
|
|
3629
3629
|
}
|
|
3630
3630
|
];
|
|
@@ -3668,8 +3668,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3668
3668
|
{
|
|
3669
3669
|
code: "composition_exceeds_inspection_viewport_cap",
|
|
3670
3670
|
severity: "warning",
|
|
3671
|
-
message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct
|
|
3672
|
-
fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare
|
|
3671
|
+
message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct, but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, while snapshot returns 4096x400 with the element at left:4500 absent.`,
|
|
3672
|
+
fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare. A clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass \`--output-resolution\` to render instead, which supersamples without changing layout.`,
|
|
3673
3673
|
snippet: truncateSnippet(rootTag.raw)
|
|
3674
3674
|
}
|
|
3675
3675
|
];
|
|
@@ -4548,7 +4548,7 @@ ${right.raw}`)
|
|
|
4548
4548
|
findings.push({
|
|
4549
4549
|
code: "gsap_animates_clip_element",
|
|
4550
4550
|
severity: "error",
|
|
4551
|
-
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity
|
|
4551
|
+
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity. Do not animate these properties on clip elements.`,
|
|
4552
4552
|
selector: sel,
|
|
4553
4553
|
elementId: clipInfo.id || void 0,
|
|
4554
4554
|
fixHint: "Remove the visibility/display/autoAlpha tween, or move the content into a child <div> and target that instead.",
|
|
@@ -4742,7 +4742,7 @@ ${right.raw}`)
|
|
|
4742
4742
|
findings.push({
|
|
4743
4743
|
code: "audio_reactive_single_tween_per_group",
|
|
4744
4744
|
severity: "warning",
|
|
4745
|
-
message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start
|
|
4745
|
+
message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start, which is not perceptible as audio reactivity.",
|
|
4746
4746
|
fixHint: "Sample audio data at 100-200ms intervals throughout each group's lifetime (for loop from group.start to group.end) and create a tween at each sample point for visible pulsing."
|
|
4747
4747
|
});
|
|
4748
4748
|
}
|
|
@@ -4827,7 +4827,7 @@ ${right.raw}`)
|
|
|
4827
4827
|
...locate(script, gsapCallOffset(script, "timeline")),
|
|
4828
4828
|
code: "gsap_timeline_not_registered",
|
|
4829
4829
|
severity: "error",
|
|
4830
|
-
message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry
|
|
4830
|
+
message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry. Without registration, animations will not play during preview or render.",
|
|
4831
4831
|
fixHint: 'Add `window.__timelines = window.__timelines || {};` and `window.__timelines["root"] = tl;` after creating the timeline (use the composition\'s data-composition-id as the key).'
|
|
4832
4832
|
});
|
|
4833
4833
|
}
|
|
@@ -4895,9 +4895,9 @@ ${right.raw}`)
|
|
|
4895
4895
|
findings.push({
|
|
4896
4896
|
code: "gsap_from_opacity_noop",
|
|
4897
4897
|
severity: "error",
|
|
4898
|
-
message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value
|
|
4898
|
+
message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value. Since CSS is already 0, the element animates from 0\u21920 and never becomes visible.`,
|
|
4899
4899
|
selector: sel,
|
|
4900
|
-
fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state
|
|
4900
|
+
fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state. It will animate FROM 0 TO the CSS value (1 by default).`,
|
|
4901
4901
|
snippet: truncateSnippet(win.raw)
|
|
4902
4902
|
});
|
|
4903
4903
|
}
|
|
@@ -4976,12 +4976,12 @@ ${right.raw}`)
|
|
|
4976
4976
|
if (layoutProps.length > 0 && allTargetsHtmlInCanvas(call.selector)) layoutProps = [];
|
|
4977
4977
|
if (layoutProps.length === 0 && reflowProps.length === 0 && !usesRoundProps) continue;
|
|
4978
4978
|
const flagged = [...layoutProps, ...reflowProps, ...usesRoundProps ? ["roundProps"] : []];
|
|
4979
|
-
const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine
|
|
4979
|
+
const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine. Animate transforms (x/y/scale/opacity) instead.`;
|
|
4980
4980
|
const fixes = [];
|
|
4981
4981
|
if (layoutProps.length > 0) {
|
|
4982
4982
|
const tokens = [...new Set(layoutProps.flatMap((p) => LAYOUT_FIX[p] ?? []))];
|
|
4983
4983
|
fixes.push(
|
|
4984
|
-
`replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")})
|
|
4984
|
+
`replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}), e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`
|
|
4985
4985
|
);
|
|
4986
4986
|
}
|
|
4987
4987
|
if (reflowProps.length > 0) {
|
|
@@ -4993,7 +4993,7 @@ ${right.raw}`)
|
|
|
4993
4993
|
}
|
|
4994
4994
|
if (spacing.length > 0) {
|
|
4995
4995
|
parts.push(
|
|
4996
|
-
`for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread)
|
|
4996
|
+
`for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread), since uniform scale is NOT equivalent, or hold the final value statically`
|
|
4997
4997
|
);
|
|
4998
4998
|
}
|
|
4999
4999
|
fixes.push(
|
|
@@ -5057,7 +5057,7 @@ ${right.raw}`)
|
|
|
5057
5057
|
findings.push({
|
|
5058
5058
|
code: "gsap_relative_value_second_writer",
|
|
5059
5059
|
severity: "error",
|
|
5060
|
-
message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state
|
|
5060
|
+
message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state, so the same frame renders at two different positions (snap at chunk boundaries).`,
|
|
5061
5061
|
selector: win.targetSelector,
|
|
5062
5062
|
fixHint: `Use absolute values for ${sharedProps.join(", ")}, or a fromTo() with explicit endpoints, so every seek path resolves the same state. Single-writer relative values are safe; the conflict is the second writer.`,
|
|
5063
5063
|
snippet: truncateSnippet(`${win.raw}
|
|
@@ -5126,13 +5126,13 @@ ${other.raw}`)
|
|
|
5126
5126
|
const readsInvariant = TRANSFORM_INVARIANT_READ.test(fn.body);
|
|
5127
5127
|
const badMember = firstParamMemberAccessHazard(fn);
|
|
5128
5128
|
if (!readsSensitive && !readsInvariant && !badMember) continue;
|
|
5129
|
-
const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter
|
|
5129
|
+
const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter. GSAP function values receive (index, target, targets), so the first parameter is a NUMBER and this throws at tween init` : "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
|
|
5130
5130
|
findings.push({
|
|
5131
5131
|
code: "gsap_function_value_hazard",
|
|
5132
5132
|
severity: readsSensitive || badMember ? "error" : "warning",
|
|
5133
5133
|
message: `Function-valued tween var for ${prop} on "${anim.targetSelector}" ${reason}. Each render worker initializes tweens independently.`,
|
|
5134
5134
|
selector: anim.targetSelector,
|
|
5135
|
-
fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) =>
|
|
5135
|
+
fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) => ..., or index arithmetic like (i) => i * 20." : "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
|
|
5136
5136
|
snippet: truncateSnippet(raw)
|
|
5137
5137
|
});
|
|
5138
5138
|
}
|
|
@@ -5168,9 +5168,9 @@ ${other.raw}`)
|
|
|
5168
5168
|
findings.push({
|
|
5169
5169
|
code: "gsap_callback_dom_measurement",
|
|
5170
5170
|
severity: "warning",
|
|
5171
|
-
message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek
|
|
5171
|
+
message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek, and a cold render worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
|
|
5172
5172
|
selector: truncateSnippet(site, 120),
|
|
5173
|
-
fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline
|
|
5173
|
+
fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline, or derive geometry from fixed composition coordinates instead of measuring.",
|
|
5174
5174
|
snippet: truncateSnippet(snippet)
|
|
5175
5175
|
});
|
|
5176
5176
|
};
|
|
@@ -5243,8 +5243,8 @@ ${other.raw}`)
|
|
|
5243
5243
|
findings.push({
|
|
5244
5244
|
code: "gsap_timeline_return_used_as_tween",
|
|
5245
5245
|
severity: "error",
|
|
5246
|
-
message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it
|
|
5247
|
-
fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0)
|
|
5246
|
+
message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it (\`.kill()\` above all) hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
|
|
5247
|
+
fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);`. Then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
|
|
5248
5248
|
snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60))
|
|
5249
5249
|
});
|
|
5250
5250
|
}
|
|
@@ -5339,7 +5339,7 @@ ${other.raw}`)
|
|
|
5339
5339
|
findings.push({
|
|
5340
5340
|
code: "svg_drawon_css_dasharray_conflict",
|
|
5341
5341
|
severity: "error",
|
|
5342
|
-
message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px")
|
|
5342
|
+
message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px"). The draw-on hide only hides one gap's worth and the line stays visible the whole scene.`,
|
|
5343
5343
|
selector: quotedSelector ?? void 0,
|
|
5344
5344
|
fixHint: `Remove the CSS stroke-dasharray from "${conflictToken}" (decorative dashes belong on a separate element), or set the full two-component value in GSAP: strokeDasharray: "\${len} \${len}".`,
|
|
5345
5345
|
snippet: truncateSnippet(match[0] + firstVars.slice(1))
|
|
@@ -5461,7 +5461,7 @@ ${other.raw}`)
|
|
|
5461
5461
|
findings.push({
|
|
5462
5462
|
code: "svg_measure_before_path_d",
|
|
5463
5463
|
severity: sameVarAssignmentExists ? "warning" : "error",
|
|
5464
|
-
message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body
|
|
5464
|
+
message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body. If the measure runs before that function (e.g. the function is a timeline callback), the length is 0 and the dash animation is dead.` : `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d assignment exists anywhere, so getTotalLength() returns 0 in Chrome, silently killing dash animations.`,
|
|
5465
5465
|
selector: tokenLabel,
|
|
5466
5466
|
fixHint: "Assign the path's `d` synchronously at build time (top level, before measuring), or author a static d attribute in the HTML.",
|
|
5467
5467
|
snippet: truncateSnippet(match[0] + ")")
|
|
@@ -6009,7 +6009,7 @@ var fontRules = [
|
|
|
6009
6009
|
{
|
|
6010
6010
|
code: "system_font_will_alias",
|
|
6011
6011
|
severity: "warning",
|
|
6012
|
-
message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled
|
|
6012
|
+
message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled, so these fonts will fall back to OS defaults. Embed explicit @font-face declarations instead.`
|
|
6013
6013
|
}
|
|
6014
6014
|
];
|
|
6015
6015
|
},
|
|
@@ -6029,7 +6029,7 @@ var fontRules = [
|
|
|
6029
6029
|
code: "font_family_without_font_face",
|
|
6030
6030
|
severity: "error",
|
|
6031
6031
|
message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
|
|
6032
|
-
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead
|
|
6032
|
+
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead. The declaration alone satisfies this check without needing a font file."
|
|
6033
6033
|
});
|
|
6034
6034
|
return findings;
|
|
6035
6035
|
}
|
|
@@ -6457,4 +6457,3 @@ export {
|
|
|
6457
6457
|
lintMediaUrls,
|
|
6458
6458
|
shouldBlockRender
|
|
6459
6459
|
};
|
|
6460
|
-
//# sourceMappingURL=browser.js.map
|