feat(cli): keyframes command (surface GSAP/CSS/Anime keyframes + 3D onion-skin --shot) (#1603)

Renames the motion-surfacing tool from `hyperframes keyframes` to `hyperframes motion`,
renames the implementation from keyframes*.ts to motion*.ts (keeping the keyframe data
model name where still accurate), and renames the shipped skill from
hyperframes-keyframes to hyperframes-motion. Expands the skill from a command
reference into a full motion-design workflow: reading motion, 3D angle verification,
layered GSAP motion, one-shot reference reproduction, diagnostic checks, and
eval-derived craft guidance.
This commit is contained in:
Miguel Ángel
2026-07-01 19:51:55 -07:00
committed by GitHub
parent 145c71e837
commit a908af11a8
34 changed files with 3973 additions and 159 deletions
+31
View File
@@ -21,6 +21,11 @@ export const SCRIPT_BLOCK_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
const COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
export const TIMELINE_REGISTRY_INIT_PATTERN =
/window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
// Object-literal registration that assigns at least one `key: value` entry inline,
// e.g. `window.__timelines = { main: tl }` or `window.__timelines = { "comp-1": tl }`.
// Distinct from the empty-init form (`= {}`) — requires a key followed by `:`.
export const TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN =
/window\.__timelines\s*=\s*\{\s*(?:["'][^"']+["']|[A-Za-z_$][\w$]*)\s*:/i;
export const TIMELINE_REGISTRY_ASSIGN_PATTERN =
/window\.__timelines(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)\s*=/i;
export const WINDOW_TIMELINE_ASSIGN_PATTERN =
@@ -30,6 +35,14 @@ export const INVALID_SCRIPT_CLOSE_PATTERN = /<script[^>]*>[\s\S]*?<\s*\/\s*scrip
const TIMELINE_REGISTRY_KEY_PATTERN =
/window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=/g;
// The `window.__timelines = { ... }` object-literal body (group 1), captured so its
// `key: value` entries can be scanned for registered keys.
const TIMELINE_REGISTRY_OBJECT_BODY_PATTERN = /window\.__timelines\s*=\s*\{([\s\S]*?)\}/i;
// A single object-literal entry whose value is an identifier (real timeline registration),
// e.g. `main: tl` or `"comp-1": tl`. Captures the key in group 1 (quoted) or 2 (bare).
const TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN =
/(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:\s*[A-Za-z_$][\w$]*/g;
export function extractOpenTags(source: string): OpenTag[] {
const tags: OpenTag[] = [];
let match: RegExpExecArray | null;
@@ -177,6 +190,17 @@ export function extractTimelineRegistryKeys(source: string): string[] {
const key = match[1] ?? match[2];
if (key) keys.add(key);
}
const objectBody = TIMELINE_REGISTRY_OBJECT_BODY_PATTERN.exec(source)?.[1];
if (objectBody) {
const entryPattern = new RegExp(
TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN.source,
TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN.flags,
);
while ((match = entryPattern.exec(objectBody)) !== null) {
const key = match[1] ?? match[2];
if (key) keys.add(key);
}
}
return [...keys];
}
@@ -300,6 +324,13 @@ export function isMediaTag(tagName: string): boolean {
return tagName === "video" || tagName === "audio" || tagName === "img";
}
// Whether any <style> block in the composition defines caption group/word
// classes (`.caption-group`, `.caption_word`, etc.) — the signal several
// caption-specific rules use to skip non-caption compositions entirely.
export function hasCaptionStyles(styles: ExtractedBlock[]): boolean {
return styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
}
export function truncateSnippet(value: string, maxLength = 220): string | undefined {
const normalized = value.replace(/\s+/g, " ").trim();
if (!normalized) return undefined;