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
+23 -3
View File
@@ -14,6 +14,25 @@ import { ensureDOMParser } from "../utils/dom.js";
import { resolveProject } from "../utils/project.js";
import { withMeta } from "../utils/updateCheck.js";
/** Derive orientation label from actual pixel dimensions. */
export function orientation(width: number, height: number): "landscape" | "portrait" | "square" {
if (width > height) return "landscape";
if (height > width) return "portrait";
return "square";
}
/**
* Duration of the composition: prefer the root element's data-duration,
* fall back to the computed timeline end.
*/
export function durationFromHtml(html: string, fallback: number): number {
const match =
html.match(/data-composition-id[^>]*data-duration=["']([\d.]+)["']/) ||
html.match(/data-duration=["']([\d.]+)["'][^>]*data-composition-id/);
const value = match?.[1] ? parseFloat(match[1]) : NaN;
return Number.isFinite(value) ? value : fallback;
}
function totalSize(dir: string): number {
let total = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -56,6 +75,7 @@ export default defineCommand({
const width = widthMatch?.[1] ? parseInt(widthMatch[1], 10) : fallback.width;
const height = heightMatch?.[1] ? parseInt(heightMatch[1], 10) : fallback.height;
const resolution = `${width}x${height}`;
const duration = durationFromHtml(html, maxEnd);
const size = totalSize(project.dir);
const typeCounts: Record<string, number> = {};
@@ -71,10 +91,10 @@ export default defineCommand({
JSON.stringify(
withMeta({
name: project.name,
resolution: parsed.resolution,
resolution: orientation(width, height),
width,
height,
duration: maxEnd,
duration,
elements: parsed.elements.length,
tracks: tracks.size,
types: typeCounts,
@@ -89,7 +109,7 @@ export default defineCommand({
console.log(`${c.success("◇")} ${c.accent(project.name)}`);
console.log(label("Resolution", resolution));
console.log(label("Duration", `${maxEnd.toFixed(1)}s`));
console.log(label("Duration", `${duration.toFixed(1)}s`));
console.log(label("Elements", `${parsed.elements.length}${typeStr ? ` (${typeStr})` : ""}`));
console.log(label("Tracks", `${tracks.size}`));
console.log(label("Size", formatBytes(size)));