Generate AI vector characters (Recraft) and animate them with GSAP (MorphSVG, DrawSVG, MotionPath)
You are generating and animating SVG characters. You use Recraft V4 via Vercel AI Gateway to generate vector SVG artwork, then GSAP (MorphSVGPlugin, DrawSVGPlugin, MotionPathPlugin) to animate it. The output is a self-contained React component with embedded SVG path data — no AI calls at runtime.
The consumer project needs:
gsap and @gsap/react installed (npm install gsap @gsap/react)AI_GATEWAY_API_KEY environment variable set (Vercel AI Gateway)/svg-character "description of character/scene" [options]
Options (parsed from natural language):
--frames N — Number of SVG frames to generate (default: 2 for breathing, 3+ for gestures)--preset NAME — Animation preset (breathing-loop, wave, reveal, progress-fill, float)--style SUBSTYLE — Recraft substyle override (bold_stroke, line_art, engraving, kawaii, etc.)--output PATH — Where to place the component (default: src/components/svg/)--ref PATH — Reference image for style inspiration (read and describe in prompt)Look for svg-style.json in the project root. If it exists, use its settings. If not, use these defaults:
{
"recraft": {
"model": "recraft-v4",
"style": "vector_illustration",
"substyle": "bold_stroke"
},
"prompt": {
"prefix": "cute kawaii vector character, round shapes, big expressive eyes, simple features, thick outlines, ",
"suffix": ", flat colors, transparent background, centered composition, clean SVG paths"
},
"svg": {
"viewBox": "0 0 200 200"
}
}
If --ref PATH is provided, read the reference image and prepend a description of its style to the prompt prefix.
For each frame, call the Recraft V4 API through Vercel AI Gateway. Use the generate-frames.ts script or call directly:
import { generateImage } from 'ai';
import { gateway } from '@ai-sdk/gateway';
const result = await generateImage({
model: gateway.imageModel('recraft/recraft-v4'),
prompt: `${config.prompt.prefix}${userPrompt}, ${poseInstruction}${config.prompt.suffix}`,
providerOptions: {
recraft: {
style: 'vector_illustration',
// NOTE: substyles are NOT supported for V4 via the gateway — use prompt wording instead
},
},
});
// Decode base64 SVG (mediaType incorrectly reports image/png — content is SVG)
const svg = Buffer.from(result.image.base64, 'base64').toString('utf-8');
Multi-frame pose instructions: Generate each frame with an explicit pose description. Keep the character description identical — only change the pose:
Save raw SVGs to a temporary location (e.g., /tmp/svg-frames/ or the project's asset directory).
Run the optimization pipeline on each frame. This is critical for GSAP morph compatibility:
npx tsx scripts/optimize-svg.ts --input /path/to/frame-1.svg --output /path/to/optimized/
The optimizer:
<rect>, <circle>, <ellipse>, <polygon>) to <path> elementstransform attributes into path dataIf you don't have the script available, perform these steps manually using SVGO configuration or by hand-editing the SVG.
Check that all frames can morph smoothly:
npx tsx scripts/validate-morph.ts --frames /path/to/optimized/*.svg
Validation checks:
<path> elementsIf frames have different path counts, add invisible placeholder paths (d="M0,0" with opacity="0") to the frame with fewer paths.
Create a React component that:
useGSAP hook for animation lifecycleuseRef for SVG element referencesprefers-reduced-motion (show static first frame)fallback prop for loading/error statesgsap.set()Component structure — follow this pattern:
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { MorphSVGPlugin } from "gsap/MorphSVGPlugin";
gsap.registerPlugin(MorphSVGPlugin);
// Embedded path data from optimized SVGs
const FRAMES = {
idle: { body: "M...", head: "M...", armLeft: "M...", armRight: "M..." },
mid: { body: "M...", head: "M...", armLeft: "M...", armRight: "M..." },
peak: { body: "M...", head: "M...", armLeft: "M...", armRight: "M..." },
};
interface Props {
className?: string;
fallback?: React.ReactNode;
ariaLabel?: string;
/** Theme accent color (updates fill/stroke dynamically) */
accentColor?: string;
}
export default function WavingCharacter({ className, fallback, ariaLabel, accentColor }: Props) {
const svgRef = useRef<SVGSVGElement>(null);
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
useGSAP(() => {
if (reducedMotion || !svgRef.current) return;
const tl = gsap.timeline({ repeat: -1, yoyo: true, defaults: { duration: 0.6, ease: "power2.inOut" } });
// Morph each part from idle → mid → peak and back
Object.keys(FRAMES.idle).forEach((part) => {
tl.to(`#${part}`, { morphSVG: FRAMES.mid[part] }, 0)
.to(`#${part}`, { morphSVG: FRAMES.peak[part] }, 0.6);
});
return () => tl.kill();
}, { scope: svgRef });
// Apply accent color
useGSAP(() => {
if (!accentColor || !svgRef.current) return;
gsap.set(svgRef.current.querySelectorAll("[data-accent]"), { fill: accentColor });
}, { scope: svgRef, dependencies: [accentColor] });
return (
<div className={className} role="img" aria-label={ariaLabel}>
<svg ref={svgRef} viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
{/* Render idle frame paths with semantic IDs */}
<path id="body" d={FRAMES.idle.body} fill="..." />
<path id="head" d={FRAMES.idle.head} fill="..." />
<path id="armLeft" d={FRAMES.idle.armLeft} fill="..." />
<path id="armRight" d={FRAMES.idle.armRight} fill="..." />
</svg>
{reducedMotion && fallback}
</div>
);
}
Read references/presets.md for the full preset library. Quick reference:
| Preset | Plugin | Frames | Pattern |
|---|---|---|---|
breathing-loop | MorphSVG | 2 | Gentle scale/morph between 2 poses, repeat: -1, yoyo: true |
wave | MorphSVG | 3 | Sequential morph through poses: idle → mid → peak → mid → idle |
reveal | DrawSVG | 1 | drawSVG: "0%" → "100%" on viewport entry, plays once |
progress-fill | DrawSVG | 1 | drawSVG: "0% 0%" → "0% N%" driven by progress prop |
float | Core | 1 | y: "+=8" with yoyo: true, gentle sine ease |
walk-cycle | MorphSVG + MotionPath | 4+ | Morph legs/arms while following a path |
prefers-reduced-motion and show static framegsap.set(), not inline stylesdangerouslySetInnerHTML — render SVG paths as JSX elements with d attributeRead these before generating:
references/recraft-api.md — Recraft V4 API, styles, sub-styles, prompt engineeringreferences/gsap-patterns.md — MorphSVG, DrawSVG, MotionPath exact APIsreferences/frame-strategy.md — Multi-frame consistency techniquesreferences/presets.md — Animation preset implementations