# chuk-motion/src/chuk_motion/tokens/motion.py
"""
Motion token system for Remotion video design system.
Comprehensive motion system including:
- Duration tokens (instant to ultra_slow with frame conversions)
- Easing curves (15+ cubic bezier presets)
- Spring configurations (gentle to explosive)
- Enter/exit transitions (fade, slide, scale, etc.)
- Tempo/rhythm tokens (sprint to cinematic)
- Platform-specific timing (hook durations, scene change intervals)
Critical for consistent, platform-optimized motion design across components.
"""
from typing import Any
from pydantic import BaseModel, Field
# ============================================================================
# PYDANTIC MODELS
# ============================================================================
class DurationConfig(BaseModel):
"""Duration configuration with multiple time representations."""
ms: int = Field(..., description="Duration in milliseconds")
frames_30fps: int = Field(..., description="Duration in frames at 30 FPS")
frames_60fps: int = Field(..., description="Duration in frames at 60 FPS")
seconds: float = Field(..., description="Duration in seconds")
css: str = Field(..., description="CSS-compatible duration string")
description: str = Field(..., description="Human-readable description")
class EasingConfig(BaseModel):
"""Easing curve configuration."""
curve: list[float] = Field(..., description="Cubic bezier curve [x1, y1, x2, y2]")
css: str = Field(..., description="CSS-compatible easing function")
description: str = Field(..., description="Human-readable description")
usage: str = Field(..., description="When to use this easing")
class RemotionSpringConfig(BaseModel):
"""Remotion spring physics configuration."""
damping: float = Field(..., description="Damping coefficient")
mass: float = Field(..., description="Mass of the spring")
stiffness: float = Field(..., description="Stiffness of the spring")
overshootClamping: bool = Field(..., description="Whether to clamp overshoot")
class SpringConfig(BaseModel):
"""Spring animation configuration."""
config: RemotionSpringConfig = Field(..., description="Remotion spring config")
description: str = Field(..., description="Human-readable description")
feel: str = Field(..., description="How the animation feels")
usage: str = Field(..., description="When to use this spring")
class TransitionProperties(BaseModel):
"""Properties for a transition animation."""
from_value: Any = Field(..., alias="from", description="Starting value")
to: Any = Field(..., description="Ending value")
class Config:
populate_by_name = True
class EnterTransition(BaseModel):
"""Enter transition configuration."""
properties: dict[str, TransitionProperties] = Field(..., description="Animated properties")
description: str = Field(..., description="Human-readable description")
usage: str = Field(..., description="When to use this transition")
default_duration: str = Field(..., description="Recommended duration token")
default_easing: str = Field(..., description="Recommended easing token")
class ExitTransition(BaseModel):
"""Exit transition configuration."""
properties: dict[str, TransitionProperties] = Field(..., description="Animated properties")
description: str = Field(..., description="Human-readable description")
usage: str = Field(..., description="When to use this transition")
default_duration: str = Field(..., description="Recommended duration token")
default_easing: str = Field(..., description="Recommended easing token")
class TempoConfig(BaseModel):
"""Tempo/rhythm configuration for narrative pacing."""
beat_duration: float = Field(..., description="Duration of one 'beat' in seconds")
frames_30fps: int = Field(..., description="Frames per beat at 30 FPS")
frames_60fps: int = Field(..., description="Frames per beat at 60 FPS")
caption_duration: float = Field(..., description="Recommended caption display duration")
scene_change_interval: float = Field(..., description="Recommended scene change interval")
cuts_per_minute: int = Field(..., description="Average cuts per minute")
description: str = Field(..., description="Human-readable description")
feel: str = Field(..., description="How the pacing feels")
usage: str = Field(..., description="When to use this tempo")
class CTATiming(BaseModel):
"""Call-to-action timing configuration."""
first_cta: float = Field(..., description="Time for first CTA in seconds")
final_cta: float = Field(..., description="Time for final CTA (negative = from end)")
mid_roll_cta: float | None = Field(default=None, description="Optional mid-roll CTA timing")
class PlatformTimingConfig(BaseModel):
"""Platform-specific timing configuration."""
hook_duration: float = Field(..., description="Duration of opening hook in seconds")
scene_change_interval: float = Field(..., description="Average scene change interval")
caption_display_duration: float = Field(..., description="Recommended caption duration")
cta_timing: CTATiming = Field(..., description="Call-to-action timing")
attention_span: str = Field(..., description="Platform attention span characteristic")
recommended_tempo: str = Field(..., description="Recommended tempo token")
description: str = Field(..., description="Human-readable description")
class MotionTokens(BaseModel):
"""Complete motion token system."""
duration: dict[str, DurationConfig]
easing: dict[str, EasingConfig]
spring_configs: dict[str, SpringConfig]
enter: dict[str, EnterTransition]
exit: dict[str, ExitTransition]
tempo: dict[str, TempoConfig]
platform_timing: dict[str, PlatformTimingConfig]
# ============================================================================
# MOTION TOKENS DATA
# ============================================================================
MOTION_TOKENS = MotionTokens(
# ============================================================================
# DURATION TOKENS
# ============================================================================
duration={
"instant": DurationConfig(
ms=0,
frames_30fps=0,
frames_60fps=0,
seconds=0.0,
css="0s",
description="Instant (no animation)",
),
"ultra_fast": DurationConfig(
ms=100,
frames_30fps=3,
frames_60fps=6,
seconds=0.1,
css="0.1s",
description="Ultra fast motion",
),
"fast": DurationConfig(
ms=200,
frames_30fps=6,
frames_60fps=12,
seconds=0.2,
css="0.2s",
description="Fast motion",
),
"normal": DurationConfig(
ms=350,
frames_30fps=11,
frames_60fps=21,
seconds=0.35,
css="0.35s",
description="Normal motion (default)",
),
"medium": DurationConfig(
ms=500,
frames_30fps=15,
frames_60fps=30,
seconds=0.5,
css="0.5s",
description="Medium motion",
),
"slow": DurationConfig(
ms=700,
frames_30fps=21,
frames_60fps=42,
seconds=0.7,
css="0.7s",
description="Slow motion",
),
"slower": DurationConfig(
ms=1000,
frames_30fps=30,
frames_60fps=60,
seconds=1.0,
css="1.0s",
description="Slower motion",
),
"ultra_slow": DurationConfig(
ms=1500,
frames_30fps=45,
frames_60fps=90,
seconds=1.5,
css="1.5s",
description="Ultra slow motion",
),
},
# ============================================================================
# EASING CURVES
# ============================================================================
easing={
"linear": EasingConfig(
curve=[0.0, 0.0, 1.0, 1.0],
css="linear",
description="No easing, constant speed",
usage="Use for continuous loops, loading spinners",
),
"ease_in_out": EasingConfig(
curve=[0.42, 0.0, 0.58, 1.0],
css="ease-in-out",
description="Starts slow, accelerates, decelerates",
usage="Default for most transitions",
),
"ease_out": EasingConfig(
curve=[0.0, 0.0, 0.58, 1.0],
css="ease-out",
description="Starts fast, decelerates",
usage="Enter transitions, appearing elements",
),
"ease_in": EasingConfig(
curve=[0.42, 0.0, 1.0, 1.0],
css="ease-in",
description="Starts slow, accelerates",
usage="Exit transitions, disappearing elements",
),
"ease_out_back": EasingConfig(
curve=[0.34, 1.56, 0.64, 1.0],
css="cubic-bezier(0.34, 1.56, 0.64, 1)",
description="Overshoots slightly then settles",
usage="Playful entrances, bounce effects",
),
"ease_out_expo": EasingConfig(
curve=[0.16, 1.0, 0.3, 1.0],
css="cubic-bezier(0.16, 1, 0.3, 1)",
description="Exponential deceleration",
usage="Snappy UI responses, quick slides",
),
"ease_out_quint": EasingConfig(
curve=[0.22, 1.0, 0.36, 1.0],
css="cubic-bezier(0.22, 1, 0.36, 1)",
description="Smooth, elegant deceleration",
usage="Refined transitions, elegant slides",
),
"ease_out_cubic": EasingConfig(
curve=[0.33, 1.0, 0.68, 1.0],
css="cubic-bezier(0.33, 1, 0.68, 1)",
description="Moderate deceleration",
usage="General purpose, balanced feel",
),
"ease_in_cubic": EasingConfig(
curve=[0.32, 0.0, 0.67, 0.0],
css="cubic-bezier(0.32, 0, 0.67, 0)",
description="Moderate acceleration",
usage="Exit animations, dismissals",
),
"ease_in_out_quart": EasingConfig(
curve=[0.76, 0.0, 0.24, 1.0],
css="cubic-bezier(0.76, 0, 0.24, 1)",
description="Strong acceleration and deceleration",
usage="Dramatic transitions, scene changes",
),
"ease_in_out_back": EasingConfig(
curve=[0.68, -0.55, 0.27, 1.55],
css="cubic-bezier(0.68, -0.55, 0.27, 1.55)",
description="Anticipates before moving, overshoots",
usage="Playful, energetic transitions",
),
"ease_out_circ": EasingConfig(
curve=[0.08, 0.82, 0.17, 1.0],
css="cubic-bezier(0.08, 0.82, 0.17, 1)",
description="Circular deceleration",
usage="Smooth, rounded motion",
),
"bounce": EasingConfig(
curve=[0.68, -0.55, 0.27, 1.55],
css="cubic-bezier(0.68, -0.55, 0.27, 1.55)",
description="Bounces at the end",
usage="Fun, playful emphasis",
),
"elastic": EasingConfig(
curve=[0.68, -0.55, 0.27, 1.55],
css="cubic-bezier(0.68, -0.55, 0.27, 1.55)",
description="Elastic spring effect",
usage="Attention-grabbing, cartoonish",
),
"anticipate": EasingConfig(
curve=[0.68, -0.55, 0.27, 1.55],
css="cubic-bezier(0.68, -0.55, 0.27, 1.55)",
description="Pulls back before moving forward",
usage="Dramatic reveals, scene transitions",
),
},
# ============================================================================
# SPRING CONFIGURATIONS
# ============================================================================
spring_configs={
"gentle": SpringConfig(
config=RemotionSpringConfig(
damping=100, mass=1.0, stiffness=100, overshootClamping=False
),
description="Smooth, gradual spring with minimal overshoot",
feel="Calm, refined, elegant",
usage="Subtle UI transitions, professional content",
),
"smooth": SpringConfig(
config=RemotionSpringConfig(
damping=50, mass=1.0, stiffness=120, overshootClamping=False
),
description="Balanced spring animation (default)",
feel="Natural, pleasant, general-purpose",
usage="Most transitions, default choice",
),
"bouncy": SpringConfig(
config=RemotionSpringConfig(
damping=30, mass=1.0, stiffness=150, overshootClamping=False
),
description="Playful spring with noticeable bounce",
feel="Fun, energetic, playful",
usage="Playful UI, creator content, emphasizing actions",
),
"snappy": SpringConfig(
config=RemotionSpringConfig(
damping=80, mass=0.5, stiffness=200, overshootClamping=False
),
description="Quick, responsive spring",
feel="Fast, responsive, modern",
usage="UI responses, button presses, snappy interactions",
),
"wobbly": SpringConfig(
config=RemotionSpringConfig(
damping=20, mass=1.2, stiffness=100, overshootClamping=False
),
description="Exaggerated wobble spring",
feel="Cartoonish, attention-grabbing, silly",
usage="Comedic emphasis, playful reveals",
),
"stiff": SpringConfig(
config=RemotionSpringConfig(
damping=150, mass=1.0, stiffness=300, overshootClamping=True
),
description="Very stiff, minimal bounce",
feel="Precise, controlled, mechanical",
usage="Precise UI movements, technical content",
),
"explosive": SpringConfig(
config=RemotionSpringConfig(
damping=10, mass=0.8, stiffness=250, overshootClamping=False
),
description="Dramatic, high-energy spring",
feel="Explosive, dramatic, intense",
usage="Hero moments, dramatic reveals, climactic scenes",
),
},
# ============================================================================
# ENTER TRANSITIONS
# ============================================================================
enter={
"fade_in": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
},
description="Simple fade in",
usage="Subtle entrances, text, overlays",
default_duration="normal",
default_easing="ease_out",
),
"fade_up": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"translateY": TransitionProperties(**{"from": 30, "to": 0}),
},
description="Fade in while sliding up",
usage="Content blocks, cards, sections",
default_duration="medium",
default_easing="ease_out_expo",
),
"fade_down": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"translateY": TransitionProperties(**{"from": -30, "to": 0}),
},
description="Fade in while sliding down",
usage="Dropdown content, notifications",
default_duration="medium",
default_easing="ease_out_expo",
),
"scale_in": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"scale": TransitionProperties(**{"from": 0.8, "to": 1}),
},
description="Fade in while scaling up",
usage="Modals, popups, emphasis",
default_duration="medium",
default_easing="ease_out_back",
),
"zoom_in": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"scale": TransitionProperties(**{"from": 0, "to": 1}),
},
description="Zoom from 0% to 100%",
usage="Dramatic entrances, hero elements",
default_duration="medium",
default_easing="ease_out_expo",
),
"slide_in_left": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"translateX": TransitionProperties(**{"from": -100, "to": 0}),
},
description="Slide in from left",
usage="Side panels, nav menus",
default_duration="medium",
default_easing="ease_out_expo",
),
"slide_in_right": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"translateX": TransitionProperties(**{"from": 100, "to": 0}),
},
description="Slide in from right",
usage="Side panels, nav menus",
default_duration="medium",
default_easing="ease_out_expo",
),
"bounce_in": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"scale": TransitionProperties(**{"from": 0.3, "to": 1}),
},
description="Bounce in with overshoot",
usage="Playful emphasis, notifications",
default_duration="medium",
default_easing="ease_out_back",
),
"rotate_in": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"rotate": TransitionProperties(**{"from": -180, "to": 0}),
},
description="Rotate in from -180deg",
usage="Playful transitions, icons",
default_duration="medium",
default_easing="ease_out_back",
),
"blur_in": EnterTransition(
properties={
"opacity": TransitionProperties(**{"from": 0, "to": 1}),
"blur": TransitionProperties(**{"from": 20, "to": 0}),
},
description="Fade in from blurred",
usage="Backgrounds, hero images",
default_duration="slow",
default_easing="ease_out",
),
},
# ============================================================================
# EXIT TRANSITIONS
# ============================================================================
exit={
"fade_out": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
},
description="Simple fade out",
usage="Subtle exits, dismissals",
default_duration="normal",
default_easing="ease_in",
),
"fade_out_down": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"translateY": TransitionProperties(**{"from": 0, "to": 30}),
},
description="Fade out while sliding down",
usage="Content blocks, cards",
default_duration="medium",
default_easing="ease_in_cubic",
),
"fade_out_up": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"translateY": TransitionProperties(**{"from": 0, "to": -30}),
},
description="Fade out while sliding up",
usage="Notifications, toasts",
default_duration="medium",
default_easing="ease_in_cubic",
),
"scale_out": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"scale": TransitionProperties(**{"from": 1, "to": 0.8}),
},
description="Fade out while scaling down",
usage="Modals, popups",
default_duration="medium",
default_easing="ease_in_cubic",
),
"zoom_out": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"scale": TransitionProperties(**{"from": 1, "to": 0}),
},
description="Zoom from 100% to 0%",
usage="Dramatic exits",
default_duration="medium",
default_easing="ease_in_cubic",
),
"slide_out_left": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"translateX": TransitionProperties(**{"from": 0, "to": -100}),
},
description="Slide out to left",
usage="Side panels, dismissals",
default_duration="medium",
default_easing="ease_in_cubic",
),
"slide_out_right": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"translateX": TransitionProperties(**{"from": 0, "to": 100}),
},
description="Slide out to right",
usage="Side panels, dismissals",
default_duration="medium",
default_easing="ease_in_cubic",
),
"blur_out": ExitTransition(
properties={
"opacity": TransitionProperties(**{"from": 1, "to": 0}),
"blur": TransitionProperties(**{"from": 0, "to": 20}),
},
description="Fade out to blurred",
usage="Backgrounds, scene transitions",
default_duration="slow",
default_easing="ease_in",
),
},
# ============================================================================
# TEMPO / RHYTHM TOKENS
# ============================================================================
tempo={
"sprint": TempoConfig(
beat_duration=1.0,
frames_30fps=30,
frames_60fps=60,
caption_duration=0.8,
scene_change_interval=2.0,
cuts_per_minute=45,
description="Ultra-fast pacing for maximum retention",
feel="Frenetic, intense, rapid-fire",
usage="TikTok, Shorts, attention-grabbing content",
),
"fast": TempoConfig(
beat_duration=1.5,
frames_30fps=45,
frames_60fps=90,
caption_duration=1.2,
scene_change_interval=3.0,
cuts_per_minute=30,
description="Fast pacing for energetic content",
feel="Energetic, dynamic, engaging",
usage="Short-form content, energetic tutorials",
),
"medium": TempoConfig(
beat_duration=2.0,
frames_30fps=60,
frames_60fps=120,
caption_duration=1.8,
scene_change_interval=4.5,
cuts_per_minute=20,
description="Balanced pacing (default)",
feel="Comfortable, natural, conversational",
usage="Most content types, general tutorials",
),
"slow": TempoConfig(
beat_duration=3.0,
frames_30fps=90,
frames_60fps=180,
caption_duration=2.5,
scene_change_interval=6.0,
cuts_per_minute=12,
description="Deliberate pacing for clarity",
feel="Thoughtful, clear, educational",
usage="Technical content, presentations, explanations",
),
"cinematic": TempoConfig(
beat_duration=4.0,
frames_30fps=120,
frames_60fps=240,
caption_duration=3.5,
scene_change_interval=8.0,
cuts_per_minute=8,
description="Slow, cinematic pacing",
feel="Atmospheric, dramatic, contemplative",
usage="Storytelling, documentaries, cinematic content",
),
},
# ============================================================================
# PLATFORM-SPECIFIC TIMING
# ============================================================================
platform_timing={
"tiktok": PlatformTimingConfig(
hook_duration=1.5,
scene_change_interval=2.0,
caption_display_duration=0.8,
cta_timing=CTATiming(
first_cta=3.0,
final_cta=-2.0,
),
attention_span="ultra_short",
recommended_tempo="sprint",
description="TikTok algorithm-optimized timing",
),
"youtube_shorts": PlatformTimingConfig(
hook_duration=2.0,
scene_change_interval=3.0,
caption_display_duration=1.0,
cta_timing=CTATiming(
first_cta=5.0,
final_cta=-3.0,
),
attention_span="short",
recommended_tempo="fast",
description="YouTube Shorts optimized timing",
),
"instagram_reel": PlatformTimingConfig(
hook_duration=2.0,
scene_change_interval=3.5,
caption_display_duration=1.2,
cta_timing=CTATiming(
first_cta=5.0,
final_cta=-3.0,
),
attention_span="short",
recommended_tempo="fast",
description="Instagram Reels optimized timing",
),
"youtube_long_form": PlatformTimingConfig(
hook_duration=3.0,
scene_change_interval=5.0,
caption_display_duration=2.0,
cta_timing=CTATiming(
first_cta=15.0,
mid_roll_cta=180.0,
final_cta=-10.0,
),
attention_span="medium",
recommended_tempo="medium",
description="YouTube long-form optimized timing",
),
"linkedin": PlatformTimingConfig(
hook_duration=2.5,
scene_change_interval=4.0,
caption_display_duration=2.0,
cta_timing=CTATiming(
first_cta=10.0,
final_cta=-5.0,
),
attention_span="medium",
recommended_tempo="medium",
description="LinkedIn feed optimized timing",
),
"presentation": PlatformTimingConfig(
hook_duration=4.0,
scene_change_interval=6.0,
caption_display_duration=3.0,
cta_timing=CTATiming(
first_cta=20.0,
final_cta=-10.0,
),
attention_span="long",
recommended_tempo="slow",
description="Presentation/educational optimized timing",
),
},
)
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
def get_duration(name: str) -> DurationConfig:
"""Get a duration config by name, fallback to 'normal' if not found."""
return MOTION_TOKENS.duration.get(name, MOTION_TOKENS.duration["normal"])
def get_easing(name: str) -> EasingConfig:
"""Get an easing config by name, fallback to 'ease_out' if not found."""
return MOTION_TOKENS.easing.get(name, MOTION_TOKENS.easing["ease_out"])
def get_spring(name: str) -> SpringConfig:
"""Get a spring config by name, fallback to 'smooth' if not found."""
return MOTION_TOKENS.spring_configs.get(name, MOTION_TOKENS.spring_configs["smooth"])
def get_enter_transition(name: str) -> EnterTransition:
"""Get an enter transition by name, fallback to 'fade_in' if not found."""
return MOTION_TOKENS.enter.get(name, MOTION_TOKENS.enter["fade_in"])
def get_exit_transition(name: str) -> ExitTransition:
"""Get an exit transition by name, fallback to 'fade_out' if not found."""
return MOTION_TOKENS.exit.get(name, MOTION_TOKENS.exit["fade_out"])
def get_tempo(name: str) -> TempoConfig:
"""Get a tempo config by name, fallback to 'medium' if not found."""
return MOTION_TOKENS.tempo.get(name, MOTION_TOKENS.tempo["medium"])
def get_platform_timing(platform: str) -> PlatformTimingConfig:
"""Get platform timing config by name, fallback to 'youtube_long_form' if not found."""
return MOTION_TOKENS.platform_timing.get(
platform, MOTION_TOKENS.platform_timing["youtube_long_form"]
)
def list_durations() -> list[str]:
"""List all available duration token names."""
return list(MOTION_TOKENS.duration.keys())
def list_easings() -> list[str]:
"""List all available easing token names."""
return list(MOTION_TOKENS.easing.keys())
def list_springs() -> list[str]:
"""List all available spring config names."""
return list(MOTION_TOKENS.spring_configs.keys())
def list_enter_transitions() -> list[str]:
"""List all available enter transition names."""
return list(MOTION_TOKENS.enter.keys())
def list_exit_transitions() -> list[str]:
"""List all available exit transition names."""
return list(MOTION_TOKENS.exit.keys())
def list_tempos() -> list[str]:
"""List all available tempo names."""
return list(MOTION_TOKENS.tempo.keys())
def list_platforms() -> list[str]:
"""List all available platform timing configs."""
return list(MOTION_TOKENS.platform_timing.keys())