easing.py 6.1 KB

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