lib.easing
1from typing import Literal, Callable 2from math import pi, cos, sin, sqrt, ceil 3from loguru import logger 4 5_c1 = 1.70158 6_c2 = _c1 * 1.525 7_c3 = _c1 + 1 8_c4 = (2 * pi) / 3 9_c5 = (2 * pi) / 4.5 10 11 12# Easing functions 13 14 15def linear(x: float) -> float: 16 return x 17 18 19def easeInQuad(x: float) -> float: 20 return x * x 21 22 23def easeOutQuad(x: float) -> float: 24 return 1 - (1 - x) * (1 - x) 25 26 27def easeInOutQuad(x: float) -> float: 28 return 2 * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 2) / 2 29 30 31def easeInCubic(x: float) -> float: 32 return x * x * x 33 34 35def easeOutCubic(x: float) -> float: 36 return 1 - pow(1 - x, 3) 37 38 39def easeInOutCubic(x: float) -> float: 40 return 4 * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 3) / 2 41 42 43def easeInQuart(x: float) -> float: 44 return x * x * x * x 45 46 47def easeOutQuart(x: float) -> float: 48 return 1 - pow(1 - x, 4) 49 50 51def easeInOutQuart(x: float) -> float: 52 return 8 * x * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 4) / 2 53 54 55def easeInQuint(x: float) -> float: 56 return x * x * x * x * x 57 58 59def easeOutQuint(x: float) -> float: 60 return 1 - pow(1 - x, 5) 61 62 63def easeInOutQuint(x: float) -> float: 64 return 16 * x * x * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 5) / 2 65 66 67def easeInSine(x: float) -> float: 68 return 1 - cos((x * pi) / 2) 69 70 71def easeOutSine(x: float) -> float: 72 return sin((x * pi) / 2) 73 74 75def easeInOutSine(x: float) -> float: 76 return -(cos(pi * x) - 1) / 2 77 78 79def easeInExpo(x: float) -> float: 80 return 0 if x == 0 else pow(2, 10 * x - 10) 81 82 83def easeOutExpo(x: float) -> float: 84 return 1 if x == 1 else 1 - pow(2, -10 * x) 85 86 87def easeInOutExpo(x: float) -> float: 88 return ( 89 0 90 if x == 0 91 else ( 92 1 93 if x == 1 94 else pow(2, 20 * x - 10) / 2 if x < 0.5 else (2 - pow(2, -20 * x + 10)) 95 ) 96 ) 97 98 99def easeInCirc(x: float) -> float: 100 return 1 - sqrt(1 - pow(x, 2)) 101 102 103def easeOutCirc(x: float) -> float: 104 return sqrt(1 - pow(x - 1, 2)) 105 106 107def easeInOutCirc(x: float) -> float: 108 return ( 109 (1 - sqrt(1 - pow(2 * x, 2))) / 2 110 if x < 0.5 111 else (sqrt(1 - pow(-2 * x + 2, 2)) + 1) / 2 112 ) 113 114 115def easeInBack(x: float) -> float: 116 return _c3 * pow(x, 3) - _c1 * pow(x, 2) 117 118 119def easeOutBack(x: float) -> float: 120 return 1 + _c3 * pow(x - 1, 3) + _c1 * pow(x - 1, 2) 121 122 123def easeInOutBack(x: float) -> float: 124 return ( 125 (pow(2 * x, 2) * ((_c2 + 1) * 2 * x - _c2)) / 2 126 if x < 0.5 127 else (pow(2 * x - 2, 2) * ((_c2 + 1) * (x * 2 - 2) + _c2) + 2) / 2 128 ) 129 130 131def easeInElastic(x: float) -> float: 132 return ( 133 0 134 if x == 0 135 else 1 if x == 1 else -pow(2, 10 * x - 10) * sin((x * 10 - 10.75) * _c4) 136 ) 137 138 139def easeOutElastic(x: float) -> float: 140 return ( 141 0 142 if x == 0 143 else 1 if x == 1 else pow(2, -10 * x) * sin((x * 10 - 0.75) * _c4) + 1 144 ) 145 146 147def easeInOutElastic(x: float) -> float: 148 return ( 149 0 150 if x == 0 151 else ( 152 1 153 if x == 1 154 else ( 155 -(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * _c5)) / 2 156 if x < 0.5 157 else (pow(2, -20 * x + 10) * sin((20 * x - 11.125) * _c5)) / 2 + 1 158 ) 159 ) 160 ) 161 162 163# Math helpers 164 165 166def lerp( 167 start: float | tuple[float, float], stop: float | tuple[float, float], amount: float 168) -> float | tuple[float, float]: 169 """ 170 Linearly interpolates between start and stop by amount. 171 172 Args: 173 start: Start value or tuple. 174 stop: Stop value or tuple. 175 amount: Interpolation factor between 0 and 1. 176 177 Returns: 178 Interpolated value or tuple. 179 180 Example: 181 - `start=0 stop=10 amount=0.5` => 5 182 - `start=(0,0) stop=(10,20) amount=0.5` => (5, 10) 183 """ 184 185 def calc(start, stop): 186 try: 187 return start * (1 - amount) + stop * amount 188 except: 189 logger.warning("Lerp failed {} {} {}", start, stop, amount) 190 return 0 191 192 if all(isinstance(e, tuple) for e in [start, stop]): 193 return tuple(map(calc, start, stop)) 194 else: 195 return calc(start, stop) 196 197 198def invLerp(start, stop, amount): 199 """ 200 Calculates the normalized value of amount between start and stop. 201 202 Args: 203 start: Start value. 204 stop: Stop value. 205 amount: Value to normalize. 206 207 Returns: 208 Normalized value between 0 and 1. 209 """ 210 return clamp((amount - start) / (stop - start)) 211 212 213def clamp(amount, aMin=0, aMax=1): 214 """Returns amount clamped between aMin and aMax.""" 215 return min(aMax, max(aMin, amount)) 216 217 218def lerpRange(start1, stop1, start2, stop2, amount): 219 """ 220 Maps amount from range [start1, stop1] to [start2, stop2] using linear interpolation. 221 222 Args: 223 start1: Start of input range. 224 stop1: End of input range. 225 start2: Start of output range. 226 stop2: End of output range. 227 amount: Value in input range. 228 229 Returns: 230 Value mapped to output range. 231 """ 232 return lerp(start2, stop2, invLerp(start1, stop1, amount)) 233 234 235def retrieve( 236 func: Literal["sine", "quad", "cubic", "quart", "quint"], 237 direction: Literal["in", "out"], 238) -> Callable: 239 """ 240 Get easing function on the fly. 241 242 Args: 243 func: Easing function type. 244 direction: 'in' or 'out'. 245 246 Returns: 247 Corresponding easing function. 248 """ 249 functions = dict( 250 sine=(easeInSine, easeOutSine), 251 quad=(easeInQuad, easeOutQuad), 252 cubic=(easeInCubic, easeOutCubic), 253 quart=(easeInQuart, easeOutQuart), 254 quint=(easeInQuint, easeOutQuint), 255 ) 256 257 funcIn, funcOut = functions.get(func) 258 return funcIn if direction == "in" else funcOut 259 260 261def interpolate( 262 start=0, 263 stop=10, 264 steps=10, 265 offset=0, 266 speed=1, 267 func: Callable = easeInOutQuart, 268 factor=1, 269 mirror=False, 270 flip=False, 271): 272 """ 273 Create n steps with easing. 274 275 Args: 276 start: Start value. 277 stop: Stop value. 278 steps: Number of steps. 279 offset: Time offset. 280 speed: Number of iterations. 281 func: Easing function to use. 282 factor: Amount of easing applied (0 = `linear`). 283 mirror: If True, mirror the easing. 284 flip: If True, flip start and stop. 285 286 Returns: 287 List of interpolated values. 288 289 Example: 290 - Create n steps with easing => `[0, 0.11, 0.3, 0.7, 1]` 291 - Can be flipped/mirrored: `[0, 0.6, 1, 0.6, 0]` 292 """ 293 294 def _doStep(step: int | tuple): 295 prog = (step / end) * speed 296 297 if prog % 1: 298 prog = prog % 1 299 else: 300 prog = min(prog, 1) 301 302 if mirror: 303 isSecondHalf = prog > 0.5 304 if isSecondHalf: 305 prog = (end - step) / minorHalf 306 else: 307 prog = step / minorHalf 308 309 progOffset = prog + offset 310 311 if progOffset > 1: 312 progOffset -= 1 313 314 progEase = func(progOffset) 315 # 0 = linear, 1 = 100% eased 316 progFactored = lerp(progOffset, progEase, abs(factor)) 317 318 value = lerp(start, stop, progFactored) 319 return value 320 321 if flip: 322 start, stop = stop, start 323 324 end = steps - 1 325 # 5 steps => 2 326 minorHalf = ceil(steps / 2) - 1 or 1 327 328 return [_doStep(step) for step in range(steps)]
def
linear(x: float) -> float:
def
easeInQuad(x: float) -> float:
def
easeOutQuad(x: float) -> float:
def
easeInOutQuad(x: float) -> float:
def
easeInCubic(x: float) -> float:
def
easeOutCubic(x: float) -> float:
def
easeInOutCubic(x: float) -> float:
def
easeInQuart(x: float) -> float:
def
easeOutQuart(x: float) -> float:
def
easeInOutQuart(x: float) -> float:
def
easeInQuint(x: float) -> float:
def
easeOutQuint(x: float) -> float:
def
easeInOutQuint(x: float) -> float:
def
easeInSine(x: float) -> float:
def
easeOutSine(x: float) -> float:
def
easeInOutSine(x: float) -> float:
def
easeInExpo(x: float) -> float:
def
easeOutExpo(x: float) -> float:
def
easeInOutExpo(x: float) -> float:
def
easeInCirc(x: float) -> float:
def
easeOutCirc(x: float) -> float:
def
easeInOutCirc(x: float) -> float:
def
easeInBack(x: float) -> float:
def
easeOutBack(x: float) -> float:
def
easeInOutBack(x: float) -> float:
def
easeInElastic(x: float) -> float:
def
easeOutElastic(x: float) -> float:
def
easeInOutElastic(x: float) -> float:
def
lerp( start: float | tuple[float, float], stop: float | tuple[float, float], amount: float) -> float | tuple[float, float]:
167def lerp( 168 start: float | tuple[float, float], stop: float | tuple[float, float], amount: float 169) -> float | tuple[float, float]: 170 """ 171 Linearly interpolates between start and stop by amount. 172 173 Args: 174 start: Start value or tuple. 175 stop: Stop value or tuple. 176 amount: Interpolation factor between 0 and 1. 177 178 Returns: 179 Interpolated value or tuple. 180 181 Example: 182 - `start=0 stop=10 amount=0.5` => 5 183 - `start=(0,0) stop=(10,20) amount=0.5` => (5, 10) 184 """ 185 186 def calc(start, stop): 187 try: 188 return start * (1 - amount) + stop * amount 189 except: 190 logger.warning("Lerp failed {} {} {}", start, stop, amount) 191 return 0 192 193 if all(isinstance(e, tuple) for e in [start, stop]): 194 return tuple(map(calc, start, stop)) 195 else: 196 return calc(start, stop)
Linearly interpolates between start and stop by amount.
Arguments:
- start: Start value or tuple.
- stop: Stop value or tuple.
- amount: Interpolation factor between 0 and 1.
Returns:
Interpolated value or tuple.
Example:
start=0 stop=10 amount=0.5=> 5start=(0,0) stop=(10,20) amount=0.5=> (5, 10)
def
invLerp(start, stop, amount):
199def invLerp(start, stop, amount): 200 """ 201 Calculates the normalized value of amount between start and stop. 202 203 Args: 204 start: Start value. 205 stop: Stop value. 206 amount: Value to normalize. 207 208 Returns: 209 Normalized value between 0 and 1. 210 """ 211 return clamp((amount - start) / (stop - start))
Calculates the normalized value of amount between start and stop.
Arguments:
- start: Start value.
- stop: Stop value.
- amount: Value to normalize.
Returns:
Normalized value between 0 and 1.
def
clamp(amount, aMin=0, aMax=1):
214def clamp(amount, aMin=0, aMax=1): 215 """Returns amount clamped between aMin and aMax.""" 216 return min(aMax, max(aMin, amount))
Returns amount clamped between aMin and aMax.
def
lerpRange(start1, stop1, start2, stop2, amount):
219def lerpRange(start1, stop1, start2, stop2, amount): 220 """ 221 Maps amount from range [start1, stop1] to [start2, stop2] using linear interpolation. 222 223 Args: 224 start1: Start of input range. 225 stop1: End of input range. 226 start2: Start of output range. 227 stop2: End of output range. 228 amount: Value in input range. 229 230 Returns: 231 Value mapped to output range. 232 """ 233 return lerp(start2, stop2, invLerp(start1, stop1, amount))
Maps amount from range [start1, stop1] to [start2, stop2] using linear interpolation.
Arguments:
- start1: Start of input range.
- stop1: End of input range.
- start2: Start of output range.
- stop2: End of output range.
- amount: Value in input range.
Returns:
Value mapped to output range.
def
retrieve( func: Literal['sine', 'quad', 'cubic', 'quart', 'quint'], direction: Literal['in', 'out']) -> Callable:
236def retrieve( 237 func: Literal["sine", "quad", "cubic", "quart", "quint"], 238 direction: Literal["in", "out"], 239) -> Callable: 240 """ 241 Get easing function on the fly. 242 243 Args: 244 func: Easing function type. 245 direction: 'in' or 'out'. 246 247 Returns: 248 Corresponding easing function. 249 """ 250 functions = dict( 251 sine=(easeInSine, easeOutSine), 252 quad=(easeInQuad, easeOutQuad), 253 cubic=(easeInCubic, easeOutCubic), 254 quart=(easeInQuart, easeOutQuart), 255 quint=(easeInQuint, easeOutQuint), 256 ) 257 258 funcIn, funcOut = functions.get(func) 259 return funcIn if direction == "in" else funcOut
Get easing function on the fly.
Arguments:
- func: Easing function type.
- direction: 'in' or 'out'.
Returns:
Corresponding easing function.
def
interpolate( start=0, stop=10, steps=10, offset=0, speed=1, func: Callable = <function easeInOutQuart>, factor=1, mirror=False, flip=False):
262def interpolate( 263 start=0, 264 stop=10, 265 steps=10, 266 offset=0, 267 speed=1, 268 func: Callable = easeInOutQuart, 269 factor=1, 270 mirror=False, 271 flip=False, 272): 273 """ 274 Create n steps with easing. 275 276 Args: 277 start: Start value. 278 stop: Stop value. 279 steps: Number of steps. 280 offset: Time offset. 281 speed: Number of iterations. 282 func: Easing function to use. 283 factor: Amount of easing applied (0 = `linear`). 284 mirror: If True, mirror the easing. 285 flip: If True, flip start and stop. 286 287 Returns: 288 List of interpolated values. 289 290 Example: 291 - Create n steps with easing => `[0, 0.11, 0.3, 0.7, 1]` 292 - Can be flipped/mirrored: `[0, 0.6, 1, 0.6, 0]` 293 """ 294 295 def _doStep(step: int | tuple): 296 prog = (step / end) * speed 297 298 if prog % 1: 299 prog = prog % 1 300 else: 301 prog = min(prog, 1) 302 303 if mirror: 304 isSecondHalf = prog > 0.5 305 if isSecondHalf: 306 prog = (end - step) / minorHalf 307 else: 308 prog = step / minorHalf 309 310 progOffset = prog + offset 311 312 if progOffset > 1: 313 progOffset -= 1 314 315 progEase = func(progOffset) 316 # 0 = linear, 1 = 100% eased 317 progFactored = lerp(progOffset, progEase, abs(factor)) 318 319 value = lerp(start, stop, progFactored) 320 return value 321 322 if flip: 323 start, stop = stop, start 324 325 end = steps - 1 326 # 5 steps => 2 327 minorHalf = ceil(steps / 2) - 1 or 1 328 329 return [_doStep(step) for step in range(steps)]
Create n steps with easing.
Arguments:
- start: Start value.
- stop: Stop value.
- steps: Number of steps.
- offset: Time offset.
- speed: Number of iterations.
- func: Easing function to use.
- factor: Amount of easing applied (0 =
linear). - mirror: If True, mirror the easing.
- flip: If True, flip start and stop.
Returns:
List of interpolated values.
Example:
- Create n steps with easing =>
[0, 0.11, 0.3, 0.7, 1]- Can be flipped/mirrored:
[0, 0.6, 1, 0.6, 0]