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:
16def linear(x: float) -> float:
17    return x
def easeInQuad(x: float) -> float:
20def easeInQuad(x: float) -> float:
21    return x * x
def easeOutQuad(x: float) -> float:
24def easeOutQuad(x: float) -> float:
25    return 1 - (1 - x) * (1 - x)
def easeInOutQuad(x: float) -> float:
28def easeInOutQuad(x: float) -> float:
29    return 2 * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 2) / 2
def easeInCubic(x: float) -> float:
32def easeInCubic(x: float) -> float:
33    return x * x * x
def easeOutCubic(x: float) -> float:
36def easeOutCubic(x: float) -> float:
37    return 1 - pow(1 - x, 3)
def easeInOutCubic(x: float) -> float:
40def easeInOutCubic(x: float) -> float:
41    return 4 * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 3) / 2
def easeInQuart(x: float) -> float:
44def easeInQuart(x: float) -> float:
45    return x * x * x * x
def easeOutQuart(x: float) -> float:
48def easeOutQuart(x: float) -> float:
49    return 1 - pow(1 - x, 4)
def easeInOutQuart(x: float) -> float:
52def easeInOutQuart(x: float) -> float:
53    return 8 * x * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 4) / 2
def easeInQuint(x: float) -> float:
56def easeInQuint(x: float) -> float:
57    return x * x * x * x * x
def easeOutQuint(x: float) -> float:
60def easeOutQuint(x: float) -> float:
61    return 1 - pow(1 - x, 5)
def easeInOutQuint(x: float) -> float:
64def easeInOutQuint(x: float) -> float:
65    return 16 * x * x * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 5) / 2
def easeInSine(x: float) -> float:
68def easeInSine(x: float) -> float:
69    return 1 - cos((x * pi) / 2)
def easeOutSine(x: float) -> float:
72def easeOutSine(x: float) -> float:
73    return sin((x * pi) / 2)
def easeInOutSine(x: float) -> float:
76def easeInOutSine(x: float) -> float:
77    return -(cos(pi * x) - 1) / 2
def easeInExpo(x: float) -> float:
80def easeInExpo(x: float) -> float:
81    return 0 if x == 0 else pow(2, 10 * x - 10)
def easeOutExpo(x: float) -> float:
84def easeOutExpo(x: float) -> float:
85    return 1 if x == 1 else 1 - pow(2, -10 * x)
def easeInOutExpo(x: float) -> float:
88def easeInOutExpo(x: float) -> float:
89    return (
90        0
91        if x == 0
92        else (
93            1
94            if x == 1
95            else pow(2, 20 * x - 10) / 2 if x < 0.5 else (2 - pow(2, -20 * x + 10))
96        )
97    )
def easeInCirc(x: float) -> float:
100def easeInCirc(x: float) -> float:
101    return 1 - sqrt(1 - pow(x, 2))
def easeOutCirc(x: float) -> float:
104def easeOutCirc(x: float) -> float:
105    return sqrt(1 - pow(x - 1, 2))
def easeInOutCirc(x: float) -> float:
108def easeInOutCirc(x: float) -> float:
109    return (
110        (1 - sqrt(1 - pow(2 * x, 2))) / 2
111        if x < 0.5
112        else (sqrt(1 - pow(-2 * x + 2, 2)) + 1) / 2
113    )
def easeInBack(x: float) -> float:
116def easeInBack(x: float) -> float:
117    return _c3 * pow(x, 3) - _c1 * pow(x, 2)
def easeOutBack(x: float) -> float:
120def easeOutBack(x: float) -> float:
121    return 1 + _c3 * pow(x - 1, 3) + _c1 * pow(x - 1, 2)
def easeInOutBack(x: float) -> float:
124def easeInOutBack(x: float) -> float:
125    return (
126        (pow(2 * x, 2) * ((_c2 + 1) * 2 * x - _c2)) / 2
127        if x < 0.5
128        else (pow(2 * x - 2, 2) * ((_c2 + 1) * (x * 2 - 2) + _c2) + 2) / 2
129    )
def easeInElastic(x: float) -> float:
132def easeInElastic(x: float) -> float:
133    return (
134        0
135        if x == 0
136        else 1 if x == 1 else -pow(2, 10 * x - 10) * sin((x * 10 - 10.75) * _c4)
137    )
def easeOutElastic(x: float) -> float:
140def easeOutElastic(x: float) -> float:
141    return (
142        0
143        if x == 0
144        else 1 if x == 1 else pow(2, -10 * x) * sin((x * 10 - 0.75) * _c4) + 1
145    )
def easeInOutElastic(x: float) -> float:
148def easeInOutElastic(x: float) -> float:
149    return (
150        0
151        if x == 0
152        else (
153            1
154            if x == 1
155            else (
156                -(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * _c5)) / 2
157                if x < 0.5
158                else (pow(2, -20 * x + 10) * sin((20 * x - 11.125) * _c5)) / 2 + 1
159            )
160        )
161    )
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 => 5
  • start=(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]