#!/usr/bin/env python3 """ Easing Functions - Timing functions for smooth animations. Provides various easing functions for natural motion and timing. All functions take a value t (0.0 to 1.0) and return eased value (0.0 to 1.0). """ import math def linear(t: float) -> float: """Linear interpolation (no easing).""" return t def ease_in_quad(t: float) -> float: """Quadratic ease-in (slow start, accelerating).""" return t * t def ease_out_quad(t: float) -> float: """Quadratic ease-out (fast start, decelerating).""" return t * (2 - t) def ease_in_out_quad(t: float) -> float: """Quadratic ease-in-out (slow start and end).""" if t < 0.5: return 2 * t * t return -1 + (4 - 2 * t) * t def ease_in_cubic(t: float) -> float: """Cubic ease-in (slow start).""" return t * t * t def ease_out_cubic(t: float) -> float: """Cubic ease-out (fast start).""" return (t - 1) * (t - 1) * (t - 1) + 1 def ease_in_out_cubic(t: float) -> float: """Cubic ease-in-out.""" if t < 0.5: return 4 * t * t * t return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 def ease_in_bounce(t: float) -> float: """Bounce ease-in (bouncy start).""" return 1 - ease_out_bounce(1 - t) def ease_out_bounce(t: float) -> float: """Bounce ease-out (bouncy end).""" if t < 1 / 2.75: return 7.5625 * t * t elif t < 2 / 2.75: t -= 1.5 / 2.75 return 7.5625 * t * t + 0.75 elif t < 2.5 / 2.75: t -= 2.25 / 2.75 return 7.5625 * t * t + 0.9375 else: t -= 2.625 / 2.75 return 7.5625 * t * t + 0.984375 def ease_in_out_bounce(t: float) -> float: """Bounce ease-in-out.""" if t < 0.5: return ease_in_bounce(t * 2) * 0.5 return ease_out_bounce(t * 2 - 1) * 0.5 + 0.5 def ease_in_elastic(t: float) -> float: """Elastic ease-in (spring effect).""" if t == 0 or t == 1: return t return -math.pow(2, 10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi) def ease_out_elastic(t: float) -> float: """Elastic ease-out (spring effect).""" if t == 0 or t == 1: return t return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) + 1 def ease_in_out_elastic(t: float) -> float: """Elastic ease-in-out.""" if t == 0 or t == 1: return t t = t * 2 - 1 if t < 0: return -0.5 * math.pow(2, 10 * t) * math.sin((t - 0.1) * 5 * math.pi) return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) * 0.5 + 1 # Convenience mapping EASING_FUNCTIONS = { "linear": linear, "ease_in": ease_in_quad, "ease_out": ease_out_quad, "ease_in_out": ease_in_out_quad, "bounce_in": ease_in_bounce, "bounce_out": ease_out_bounce, "bounce": ease_in_out_bounce, "elastic_in": ease_in_elastic, "elastic_out": ease_out_elastic, "elastic": ease_in_out_elastic, } def get_easing(name: str = "linear"): """Get easing function by name.""" return EASING_FUNCTIONS.get(name, linear) def interpolate(start: float, end: float, t: float, easing: str = "linear") -> float: """ Interpolate between two values with easing. Args: start: Start value end: End value t: Progress from 0.0 to 1.0 easing: Name of easing function Returns: Interpolated value """ ease_func = get_easing(easing) eased_t = ease_func(t) return start + (end - start) * eased_t def ease_back_in(t: float) -> float: """Back ease-in (slight overshoot backward before forward motion).""" c1 = 1.70158 c3 = c1 + 1 return c3 * t * t * t - c1 * t * t def ease_back_out(t: float) -> float: """Back ease-out (overshoot forward then settle back).""" c1 = 1.70158 c3 = c1 + 1 return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2) def ease_back_in_out(t: float) -> float: """Back ease-in-out (overshoot at both ends).""" c1 = 1.70158 c2 = c1 * 1.525 if t < 0.5: return (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 return (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2 def apply_squash_stretch( base_scale: tuple[float, float], intensity: float, direction: str = "vertical" ) -> tuple[float, float]: """ Calculate squash and stretch scales for more dynamic animation. Args: base_scale: (width_scale, height_scale) base scales intensity: Squash/stretch intensity (0.0-1.0) direction: 'vertical', 'horizontal', or 'both' Returns: (width_scale, height_scale) with squash/stretch applied """ width_scale, height_scale = base_scale if direction == "vertical": # Compress vertically, expand horizontally (preserve volume) height_scale *= 1 - intensity * 0.5 width_scale *= 1 + intensity * 0.5 elif direction == "horizontal": # Compress horizontally, expand vertically width_scale *= 1 - intensity * 0.5 height_scale *= 1 + intensity * 0.5 elif direction == "both": # General squash (both dimensions) width_scale *= 1 - intensity * 0.3 height_scale *= 1 - intensity * 0.3 return (width_scale, height_scale) def calculate_arc_motion( start: tuple[float, float], end: tuple[float, float], height: float, t: float ) -> tuple[float, float]: """ Calculate position along a parabolic arc (natural motion path). Args: start: (x, y) starting position end: (x, y) ending position height: Arc height at midpoint (positive = upward) t: Progress (0.0-1.0) Returns: (x, y) position along arc """ x1, y1 = start x2, y2 = end # Linear interpolation for x x = x1 + (x2 - x1) * t # Parabolic interpolation for y # y = start + progress * (end - start) + arc_offset # Arc offset peaks at t=0.5 arc_offset = 4 * height * t * (1 - t) y = y1 + (y2 - y1) * t - arc_offset return (x, y) # Add new easing functions to the convenience mapping EASING_FUNCTIONS.update( { "back_in": ease_back_in, "back_out": ease_back_out, "back_in_out": ease_back_in_out, "anticipate": ease_back_in, # Alias "overshoot": ease_back_out, # Alias } )