main
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)