1#!/usr/bin/env python3
  2"""
  3Easing Functions - Timing functions for smooth animations.
  4
  5Provides various easing functions for natural motion and timing.
  6All functions take a value t (0.0 to 1.0) and return eased value (0.0 to 1.0).
  7"""
  8
  9import math
 10
 11
 12def linear(t: float) -> float:
 13    """Linear interpolation (no easing)."""
 14    return t
 15
 16
 17def ease_in_quad(t: float) -> float:
 18    """Quadratic ease-in (slow start, accelerating)."""
 19    return t * t
 20
 21
 22def ease_out_quad(t: float) -> float:
 23    """Quadratic ease-out (fast start, decelerating)."""
 24    return t * (2 - t)
 25
 26
 27def ease_in_out_quad(t: float) -> float:
 28    """Quadratic ease-in-out (slow start and end)."""
 29    if t < 0.5:
 30        return 2 * t * t
 31    return -1 + (4 - 2 * t) * t
 32
 33
 34def ease_in_cubic(t: float) -> float:
 35    """Cubic ease-in (slow start)."""
 36    return t * t * t
 37
 38
 39def ease_out_cubic(t: float) -> float:
 40    """Cubic ease-out (fast start)."""
 41    return (t - 1) * (t - 1) * (t - 1) + 1
 42
 43
 44def ease_in_out_cubic(t: float) -> float:
 45    """Cubic ease-in-out."""
 46    if t < 0.5:
 47        return 4 * t * t * t
 48    return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
 49
 50
 51def ease_in_bounce(t: float) -> float:
 52    """Bounce ease-in (bouncy start)."""
 53    return 1 - ease_out_bounce(1 - t)
 54
 55
 56def ease_out_bounce(t: float) -> float:
 57    """Bounce ease-out (bouncy end)."""
 58    if t < 1 / 2.75:
 59        return 7.5625 * t * t
 60    elif t < 2 / 2.75:
 61        t -= 1.5 / 2.75
 62        return 7.5625 * t * t + 0.75
 63    elif t < 2.5 / 2.75:
 64        t -= 2.25 / 2.75
 65        return 7.5625 * t * t + 0.9375
 66    else:
 67        t -= 2.625 / 2.75
 68        return 7.5625 * t * t + 0.984375
 69
 70
 71def ease_in_out_bounce(t: float) -> float:
 72    """Bounce ease-in-out."""
 73    if t < 0.5:
 74        return ease_in_bounce(t * 2) * 0.5
 75    return ease_out_bounce(t * 2 - 1) * 0.5 + 0.5
 76
 77
 78def ease_in_elastic(t: float) -> float:
 79    """Elastic ease-in (spring effect)."""
 80    if t == 0 or t == 1:
 81        return t
 82    return -math.pow(2, 10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi)
 83
 84
 85def ease_out_elastic(t: float) -> float:
 86    """Elastic ease-out (spring effect)."""
 87    if t == 0 or t == 1:
 88        return t
 89    return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) + 1
 90
 91
 92def ease_in_out_elastic(t: float) -> float:
 93    """Elastic ease-in-out."""
 94    if t == 0 or t == 1:
 95        return t
 96    t = t * 2 - 1
 97    if t < 0:
 98        return -0.5 * math.pow(2, 10 * t) * math.sin((t - 0.1) * 5 * math.pi)
 99    return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) * 0.5 + 1
100
101
102# Convenience mapping
103EASING_FUNCTIONS = {
104    "linear": linear,
105    "ease_in": ease_in_quad,
106    "ease_out": ease_out_quad,
107    "ease_in_out": ease_in_out_quad,
108    "bounce_in": ease_in_bounce,
109    "bounce_out": ease_out_bounce,
110    "bounce": ease_in_out_bounce,
111    "elastic_in": ease_in_elastic,
112    "elastic_out": ease_out_elastic,
113    "elastic": ease_in_out_elastic,
114}
115
116
117def get_easing(name: str = "linear"):
118    """Get easing function by name."""
119    return EASING_FUNCTIONS.get(name, linear)
120
121
122def interpolate(start: float, end: float, t: float, easing: str = "linear") -> float:
123    """
124    Interpolate between two values with easing.
125
126    Args:
127        start: Start value
128        end: End value
129        t: Progress from 0.0 to 1.0
130        easing: Name of easing function
131
132    Returns:
133        Interpolated value
134    """
135    ease_func = get_easing(easing)
136    eased_t = ease_func(t)
137    return start + (end - start) * eased_t
138
139
140def ease_back_in(t: float) -> float:
141    """Back ease-in (slight overshoot backward before forward motion)."""
142    c1 = 1.70158
143    c3 = c1 + 1
144    return c3 * t * t * t - c1 * t * t
145
146
147def ease_back_out(t: float) -> float:
148    """Back ease-out (overshoot forward then settle back)."""
149    c1 = 1.70158
150    c3 = c1 + 1
151    return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)
152
153
154def ease_back_in_out(t: float) -> float:
155    """Back ease-in-out (overshoot at both ends)."""
156    c1 = 1.70158
157    c2 = c1 * 1.525
158    if t < 0.5:
159        return (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
160    return (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2
161
162
163def apply_squash_stretch(
164    base_scale: tuple[float, float], intensity: float, direction: str = "vertical"
165) -> tuple[float, float]:
166    """
167    Calculate squash and stretch scales for more dynamic animation.
168
169    Args:
170        base_scale: (width_scale, height_scale) base scales
171        intensity: Squash/stretch intensity (0.0-1.0)
172        direction: 'vertical', 'horizontal', or 'both'
173
174    Returns:
175        (width_scale, height_scale) with squash/stretch applied
176    """
177    width_scale, height_scale = base_scale
178
179    if direction == "vertical":
180        # Compress vertically, expand horizontally (preserve volume)
181        height_scale *= 1 - intensity * 0.5
182        width_scale *= 1 + intensity * 0.5
183    elif direction == "horizontal":
184        # Compress horizontally, expand vertically
185        width_scale *= 1 - intensity * 0.5
186        height_scale *= 1 + intensity * 0.5
187    elif direction == "both":
188        # General squash (both dimensions)
189        width_scale *= 1 - intensity * 0.3
190        height_scale *= 1 - intensity * 0.3
191
192    return (width_scale, height_scale)
193
194
195def calculate_arc_motion(
196    start: tuple[float, float], end: tuple[float, float], height: float, t: float
197) -> tuple[float, float]:
198    """
199    Calculate position along a parabolic arc (natural motion path).
200
201    Args:
202        start: (x, y) starting position
203        end: (x, y) ending position
204        height: Arc height at midpoint (positive = upward)
205        t: Progress (0.0-1.0)
206
207    Returns:
208        (x, y) position along arc
209    """
210    x1, y1 = start
211    x2, y2 = end
212
213    # Linear interpolation for x
214    x = x1 + (x2 - x1) * t
215
216    # Parabolic interpolation for y
217    # y = start + progress * (end - start) + arc_offset
218    # Arc offset peaks at t=0.5
219    arc_offset = 4 * height * t * (1 - t)
220    y = y1 + (y2 - y1) * t - arc_offset
221
222    return (x, y)
223
224
225# Add new easing functions to the convenience mapping
226EASING_FUNCTIONS.update(
227    {
228        "back_in": ease_back_in,
229        "back_out": ease_back_out,
230        "back_in_out": ease_back_in_out,
231        "anticipate": ease_back_in,  # Alias
232        "overshoot": ease_back_out,  # Alias
233    }
234)