mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
Fixes critical P0 issue where animation-tokens.json wasn't consumed by the generate command, breaking the value chain. The animation extraction system now properly flows through: animation-extract → tokens → generate → prototypes. Changes: - Added animation-extract command with hybrid CSS extraction + interactive fallback strategy - Updated generate.md to load and inject animation tokens into prototypes - Added CSS animation support (custom properties, keyframes, interactions, accessibility) - Integrated animation extraction into explore-auto and imitate-auto workflows 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
244 lines
7.1 KiB
JavaScript
244 lines
7.1 KiB
JavaScript
/**
|
|
* Animation & Transition Extraction Script
|
|
*
|
|
* Extracts CSS animations, transitions, and transform patterns from a live web page.
|
|
* This script runs in the browser context via Chrome DevTools Protocol.
|
|
*
|
|
* @returns {Object} Structured animation data
|
|
*/
|
|
(() => {
|
|
const extractionTimestamp = new Date().toISOString();
|
|
const currentUrl = window.location.href;
|
|
|
|
/**
|
|
* Parse transition shorthand or individual properties
|
|
*/
|
|
function parseTransition(element, computedStyle) {
|
|
const transition = computedStyle.transition || computedStyle.webkitTransition;
|
|
|
|
if (!transition || transition === 'none' || transition === 'all 0s ease 0s') {
|
|
return null;
|
|
}
|
|
|
|
// Parse shorthand: "property duration easing delay"
|
|
const transitions = [];
|
|
const parts = transition.split(/,\s*/);
|
|
|
|
parts.forEach(part => {
|
|
const match = part.match(/^(\S+)\s+([\d.]+m?s)\s+(\S+)(?:\s+([\d.]+m?s))?/);
|
|
if (match) {
|
|
transitions.push({
|
|
property: match[1],
|
|
duration: match[2],
|
|
easing: match[3],
|
|
delay: match[4] || '0s'
|
|
});
|
|
}
|
|
});
|
|
|
|
return transitions.length > 0 ? transitions : null;
|
|
}
|
|
|
|
/**
|
|
* Extract animation name and properties
|
|
*/
|
|
function parseAnimation(element, computedStyle) {
|
|
const animationName = computedStyle.animationName || computedStyle.webkitAnimationName;
|
|
|
|
if (!animationName || animationName === 'none') {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
name: animationName,
|
|
duration: computedStyle.animationDuration || computedStyle.webkitAnimationDuration,
|
|
easing: computedStyle.animationTimingFunction || computedStyle.webkitAnimationTimingFunction,
|
|
delay: computedStyle.animationDelay || computedStyle.webkitAnimationDelay || '0s',
|
|
iterationCount: computedStyle.animationIterationCount || computedStyle.webkitAnimationIterationCount || '1',
|
|
direction: computedStyle.animationDirection || computedStyle.webkitAnimationDirection || 'normal',
|
|
fillMode: computedStyle.animationFillMode || computedStyle.webkitAnimationFillMode || 'none'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract transform value
|
|
*/
|
|
function parseTransform(computedStyle) {
|
|
const transform = computedStyle.transform || computedStyle.webkitTransform;
|
|
|
|
if (!transform || transform === 'none') {
|
|
return null;
|
|
}
|
|
|
|
return transform;
|
|
}
|
|
|
|
/**
|
|
* Get element selector (simplified for readability)
|
|
*/
|
|
function getSelector(element) {
|
|
if (element.id) {
|
|
return `#${element.id}`;
|
|
}
|
|
|
|
if (element.className && typeof element.className === 'string') {
|
|
const classes = element.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
if (classes) {
|
|
return `.${classes}`;
|
|
}
|
|
}
|
|
|
|
return element.tagName.toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Extract all stylesheets and find @keyframes rules
|
|
*/
|
|
function extractKeyframes() {
|
|
const keyframes = {};
|
|
|
|
try {
|
|
// Iterate through all stylesheets
|
|
Array.from(document.styleSheets).forEach(sheet => {
|
|
try {
|
|
// Skip external stylesheets due to CORS
|
|
if (sheet.href && !sheet.href.startsWith(window.location.origin)) {
|
|
return;
|
|
}
|
|
|
|
Array.from(sheet.cssRules || sheet.rules || []).forEach(rule => {
|
|
// Check for @keyframes rules
|
|
if (rule.type === CSSRule.KEYFRAMES_RULE || rule.type === CSSRule.WEBKIT_KEYFRAMES_RULE) {
|
|
const name = rule.name;
|
|
const frames = {};
|
|
|
|
Array.from(rule.cssRules || []).forEach(keyframe => {
|
|
const key = keyframe.keyText; // e.g., "0%", "50%", "100%"
|
|
frames[key] = keyframe.style.cssText;
|
|
});
|
|
|
|
keyframes[name] = frames;
|
|
}
|
|
});
|
|
} catch (e) {
|
|
// Skip stylesheets that can't be accessed (CORS)
|
|
console.warn('Cannot access stylesheet:', sheet.href, e.message);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error('Error extracting keyframes:', e);
|
|
}
|
|
|
|
return keyframes;
|
|
}
|
|
|
|
/**
|
|
* Scan visible elements for animations and transitions
|
|
*/
|
|
function scanElements() {
|
|
const elements = document.querySelectorAll('*');
|
|
const transitionData = [];
|
|
const animationData = [];
|
|
const transformData = [];
|
|
|
|
const uniqueTransitions = new Set();
|
|
const uniqueAnimations = new Set();
|
|
const uniqueEasings = new Set();
|
|
const uniqueDurations = new Set();
|
|
|
|
elements.forEach(element => {
|
|
// Skip invisible elements
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.width === 0 && rect.height === 0) {
|
|
return;
|
|
}
|
|
|
|
const computedStyle = window.getComputedStyle(element);
|
|
|
|
// Extract transitions
|
|
const transitions = parseTransition(element, computedStyle);
|
|
if (transitions) {
|
|
const selector = getSelector(element);
|
|
transitions.forEach(t => {
|
|
const key = `${t.property}-${t.duration}-${t.easing}`;
|
|
if (!uniqueTransitions.has(key)) {
|
|
uniqueTransitions.add(key);
|
|
transitionData.push({
|
|
selector,
|
|
...t
|
|
});
|
|
uniqueEasings.add(t.easing);
|
|
uniqueDurations.add(t.duration);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Extract animations
|
|
const animation = parseAnimation(element, computedStyle);
|
|
if (animation) {
|
|
const selector = getSelector(element);
|
|
const key = `${animation.name}-${animation.duration}`;
|
|
if (!uniqueAnimations.has(key)) {
|
|
uniqueAnimations.add(key);
|
|
animationData.push({
|
|
selector,
|
|
...animation
|
|
});
|
|
uniqueEasings.add(animation.easing);
|
|
uniqueDurations.add(animation.duration);
|
|
}
|
|
}
|
|
|
|
// Extract transforms (on hover/active, we only get current state)
|
|
const transform = parseTransform(computedStyle);
|
|
if (transform) {
|
|
const selector = getSelector(element);
|
|
transformData.push({
|
|
selector,
|
|
transform
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
transitions: transitionData,
|
|
animations: animationData,
|
|
transforms: transformData,
|
|
uniqueEasings: Array.from(uniqueEasings),
|
|
uniqueDurations: Array.from(uniqueDurations)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Main extraction function
|
|
*/
|
|
function extractAnimations() {
|
|
const elementData = scanElements();
|
|
const keyframes = extractKeyframes();
|
|
|
|
return {
|
|
metadata: {
|
|
timestamp: extractionTimestamp,
|
|
url: currentUrl,
|
|
method: 'chrome-devtools',
|
|
version: '1.0.0'
|
|
},
|
|
transitions: elementData.transitions,
|
|
animations: elementData.animations,
|
|
transforms: elementData.transforms,
|
|
keyframes: keyframes,
|
|
summary: {
|
|
total_transitions: elementData.transitions.length,
|
|
total_animations: elementData.animations.length,
|
|
total_transforms: elementData.transforms.length,
|
|
total_keyframes: Object.keys(keyframes).length,
|
|
unique_easings: elementData.uniqueEasings,
|
|
unique_durations: elementData.uniqueDurations
|
|
}
|
|
};
|
|
}
|
|
|
|
// Execute extraction
|
|
return extractAnimations();
|
|
})();
|