#!/usr/bin/env python3 """ Motion Graphics Generator Programmatically generate title cards, lower thirds, data visualizations, and other synthetic video elements with modern 2025 aesthetics. Styles: - neo_brutalist: Raw, glitchy, utilitarian - deep_glow: Intense light blooms, layered neons - liquid_motion: Fluid, morphing typography - retro_revival: 80s/90s grain and neon - glass_morphism: Frosted glass, depth layers """ import argparse import json import math import os import subprocess import tempfile from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Optional class Style(Enum): NEO_BRUTALIST = "neo_brutalist" DEEP_GLOW = "deep_glow" LIQUID_MOTION = "liquid_motion" RETRO_REVIVAL = "retro_revival" GLASS_MORPHISM = "glass_morphism" @dataclass class ColorPalette: background: str primary: str secondary: str accent: str text: str glow: Optional[str] = None PALETTES = { Style.NEO_BRUTALIST: ColorPalette( background="#0a0a0a", primary="#ff3366", secondary="#00ff88", accent="#ffff00", text="#ffffff", ), Style.DEEP_GLOW: ColorPalette( background="#0d0d1a", primary="#7c3aed", secondary="#06b6d4", accent="#f472b6", text="#ffffff", glow="#7c3aed", ), Style.LIQUID_MOTION: ColorPalette( background="#1a1a2e", primary="#4361ee", secondary="#3a0ca3", accent="#f72585", text="#ffffff", glow="#4361ee", ), Style.RETRO_REVIVAL: ColorPalette( background="#1a0a2e", primary="#ff006e", secondary="#8338ec", accent="#ffbe0b", text="#ffffff", glow="#ff006e", ), Style.GLASS_MORPHISM: ColorPalette( background="#0f172a", primary="#38bdf8", secondary="#818cf8", accent="#f472b6", text="#f8fafc", glow="#38bdf8", ), } def generate_svg_title_card( title: str, subtitle: Optional[str] = None, style: Style = Style.DEEP_GLOW, width: int = 1920, height: int = 1080, ) -> str: """Generate an SVG title card.""" palette = PALETTES[style] # Base SVG svg_parts = [ f'', '', ] # Add glow filter for styles that use it if palette.glow: svg_parts.append(f''' ''') # Gradient for text svg_parts.append(f''' ''') # Background svg_parts.append(f'') # Style-specific decorations if style == Style.NEO_BRUTALIST: # Grid lines for i in range(0, width, 100): opacity = 0.1 if i % 200 == 0 else 0.05 svg_parts.append(f'') for i in range(0, height, 100): opacity = 0.1 if i % 200 == 0 else 0.05 svg_parts.append(f'') # Accent block svg_parts.append(f'') elif style == Style.DEEP_GLOW: # Ambient glow circles svg_parts.append(f'') svg_parts.append(f'') elif style == Style.GLASS_MORPHISM: # Frosted glass panel svg_parts.append(f''' ''') # Title text title_y = height // 2 - (50 if subtitle else 0) font_size = min(120, width // (len(title) * 0.7)) filter_attr = 'filter="url(#glow)"' if palette.glow else '' svg_parts.append(f''' {title} ''') # Subtitle if subtitle: svg_parts.append(f''' {subtitle} ''') svg_parts.append('') return '\n'.join(svg_parts) def generate_svg_lower_third( name: str, title: str, style: Style = Style.DEEP_GLOW, width: int = 1920, height: int = 1080, position: str = "left", # left, right, center ) -> str: """Generate an SVG lower third overlay.""" palette = PALETTES[style] # Calculate position if position == "left": x_offset = 80 text_anchor = "start" elif position == "right": x_offset = width - 80 text_anchor = "end" else: x_offset = width // 2 text_anchor = "middle" y_base = height - 150 svg = f''' {name} {title} ''' return svg def generate_svg_data_chart( data: list[dict], # [{"label": "A", "value": 75}, ...] chart_type: str = "bar", # bar, horizontal_bar, radial title: Optional[str] = None, style: Style = Style.DEEP_GLOW, width: int = 1920, height: int = 1080, ) -> str: """Generate an SVG data visualization.""" palette = PALETTES[style] max_value = max(d["value"] for d in data) svg_parts = [ f'', '', f''' ''', ] if palette.glow: svg_parts.append(f''' ''') svg_parts.append('') svg_parts.append(f'') # Title if title: svg_parts.append(f''' {title} ''') chart_area_top = 150 if title else 80 chart_area_height = height - chart_area_top - 120 chart_area_width = width - 200 if chart_type == "bar": bar_width = min(80, chart_area_width // len(data) - 20) bar_spacing = chart_area_width // len(data) for i, d in enumerate(data): bar_height = (d["value"] / max_value) * chart_area_height x = 100 + i * bar_spacing + (bar_spacing - bar_width) // 2 y = chart_area_top + chart_area_height - bar_height filter_attr = 'filter="url(#barGlow)"' if palette.glow else '' svg_parts.append(f''' {d["label"]} {d["value"]} ''') elif chart_type == "radial": cx, cy = width // 2, height // 2 + 30 radius = min(chart_area_width, chart_area_height) // 3 for i, d in enumerate(data): angle = (i / len(data)) * 2 * math.pi - math.pi / 2 bar_length = (d["value"] / max_value) * radius x1 = cx + math.cos(angle) * 50 y1 = cy + math.sin(angle) * 50 x2 = cx + math.cos(angle) * (50 + bar_length) y2 = cy + math.sin(angle) * (50 + bar_length) svg_parts.append(f''' ''') # Label label_x = cx + math.cos(angle) * (radius + 80) label_y = cy + math.sin(angle) * (radius + 80) svg_parts.append(f''' {d["label"]} ''') svg_parts.append('') return '\n'.join(svg_parts) def svg_to_png(svg_content: str, output_path: Path, width: int = 1920, height: int = 1080) -> Path: """Convert SVG to PNG using system tools.""" with tempfile.NamedTemporaryFile(mode='w', suffix='.svg', delete=False) as f: f.write(svg_content) svg_path = f.name try: # Try rsvg-convert first (best quality) result = subprocess.run( ['rsvg-convert', '-w', str(width), '-h', str(height), '-o', str(output_path), svg_path], capture_output=True ) if result.returncode == 0: return output_path except FileNotFoundError: pass try: # Fall back to ImageMagick subprocess.run( ['convert', '-background', 'none', '-density', '150', svg_path, str(output_path)], check=True, capture_output=True ) return output_path except (FileNotFoundError, subprocess.CalledProcessError): pass # Last resort: save as SVG svg_output = output_path.with_suffix('.svg') with open(svg_output, 'w') as f: f.write(svg_content) print(f"Warning: Could not convert to PNG, saved as SVG: {svg_output}") return svg_output def generate_animated_title( title: str, subtitle: Optional[str] = None, style: Style = Style.DEEP_GLOW, duration: float = 3.0, fps: int = 30, output_path: Path = Path("title_animated.mp4"), ) -> Path: """Generate an animated title card video using FFmpeg.""" palette = PALETTES[style] width, height = 1920, 1080 with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) # Generate frames total_frames = int(duration * fps) for frame in range(total_frames): progress = frame / total_frames # Animation: fade in (0-30%), hold (30-70%), fade out (70-100%) if progress < 0.3: opacity = progress / 0.3 scale = 0.9 + 0.1 * (progress / 0.3) elif progress > 0.7: opacity = 1 - (progress - 0.7) / 0.3 scale = 1.0 else: opacity = 1.0 scale = 1.0 # Generate SVG with animation state svg = generate_svg_title_card(title, subtitle, style, width, height) # Apply opacity via group wrapper svg = svg.replace( '', f'' ).replace('', '') frame_path = tmpdir / f"frame_{frame:04d}.png" svg_to_png(svg, frame_path, width, height) # Combine frames with FFmpeg subprocess.run([ 'ffmpeg', '-y', '-framerate', str(fps), '-i', str(tmpdir / 'frame_%04d.png'), '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'medium', '-crf', '18', str(output_path) ], check=True, capture_output=True) return output_path def main(): parser = argparse.ArgumentParser(description="Motion Graphics Generator") parser.add_argument("--type", choices=["title", "lower_third", "chart"], default="title") parser.add_argument("--style", choices=[s.value for s in Style], default="deep_glow") parser.add_argument("--title", type=str, default="Your Title Here") parser.add_argument("--subtitle", type=str, default=None) parser.add_argument("--output", type=Path, default=Path("output.png")) parser.add_argument("--animated", action="store_true", help="Generate animated video") parser.add_argument("--duration", type=float, default=3.0, help="Animation duration (seconds)") parser.add_argument("--data", type=str, help="JSON data for charts") parser.add_argument("--chart-type", choices=["bar", "horizontal_bar", "radial"], default="bar") parser.add_argument("--width", type=int, default=1920) parser.add_argument("--height", type=int, default=1080) parser.add_argument("--list-styles", action="store_true", help="List available styles") args = parser.parse_args() if args.list_styles: print("Available Styles:") for style in Style: palette = PALETTES[style] print(f"\n {style.value}:") print(f" Background: {palette.background}") print(f" Primary: {palette.primary}") print(f" Secondary: {palette.secondary}") print(f" Accent: {palette.accent}") return style = Style(args.style) if args.type == "title": if args.animated: output = generate_animated_title( args.title, args.subtitle, style, args.duration, output_path=args.output.with_suffix('.mp4'), ) print(f"Generated animated title: {output}") else: svg = generate_svg_title_card( args.title, args.subtitle, style, args.width, args.height, ) output = svg_to_png(svg, args.output, args.width, args.height) print(f"Generated title card: {output}") elif args.type == "lower_third": svg = generate_svg_lower_third( args.title, args.subtitle or "Title", style, args.width, args.height, ) output = svg_to_png(svg, args.output, args.width, args.height) print(f"Generated lower third: {output}") elif args.type == "chart": if args.data: data = json.loads(args.data) else: # Sample data data = [ {"label": "Jan", "value": 65}, {"label": "Feb", "value": 78}, {"label": "Mar", "value": 90}, {"label": "Apr", "value": 81}, {"label": "May", "value": 95}, ] svg = generate_svg_data_chart( data, args.chart_type, args.title, style, args.width, args.height, ) output = svg_to_png(svg, args.output, args.width, args.height) print(f"Generated chart: {output}") if __name__ == "__main__": main()