--- /dev/null
+#!/usr/bin/env python3
+
+from math import asin, atan2, ceil, cos, floor, pi, sin
+
+import pygame
+
+tau = 2 * pi
+FPS = 60
+
+
+def make_line_shape(p1, p2, n, shape_r):
+ angle = atan2(p1[1] - p2[1], p1[0] - p2[0])
+ if angle < 0:
+ angle += tau
+ # 0 <= angle < tau
+ initial_corner = ceil((angle + tau * 3 / 4) * n / tau) % n
+ opposing_corner = (floor((angle + tau / 4) * n / tau) - initial_corner) % n
+ shape = [
+ (cos(a) * shape_r, sin(a) * shape_r)
+ for a in ((i + initial_corner) * tau / n for i in range(n))
+ ]
+ left = (cos(angle + tau * 3 / 4) * shape_r, sin(angle + tau * 3 / 4) * shape_r)
+ right = (cos(angle + tau / 4) * shape_r, sin(angle + tau / 4) * shape_r)
+ yield (p2[0] + left[0], p2[1] + left[1])
+ yield (p1[0] + left[0], p1[1] + left[1])
+ p = p1
+ for i in range(n):
+ s = shape[i]
+ yield (p[0] + s[0], p[1] + s[1])
+ if i == opposing_corner and p == p1 and p1 != p2:
+ yield (p[0] + right[0], p[1] + right[1])
+ p = p2
+ yield (p[0] + right[0], p[1] + right[1])
+
+
+class EaseGraph:
+ CYCLE = {"length": 4, "pause": 1}
+ STEPS = 256
+ WIDTH = 2
+ LINES_COLOR = "blue"
+ GRAPH_COLOR = "lightblue"
+
+ def __init__(self, rect, line, func):
+ self.rect = rect
+ self.line = line
+ self.current_frame = 0
+ self.current_setting = 0
+ self.func = func
+
+ def draw(self, surf):
+ pygame.draw.rect(surf, self.LINES_COLOR, self.rect, 1)
+ pygame.draw.line(surf, self.LINES_COLOR, *self.line)
+ pos = (
+ self.line[0][0],
+ self.line[0][1] + (
+ self.line[1][1] - self.line[0][1]
+ ) * (1 - self.func(self.current_setting)),
+ )
+ pygame.draw.circle(surf, self.GRAPH_COLOR, pos, 32)
+ prev = self.rect.bottomleft
+ for i in range(1, self.STEPS + 1):
+ i_fract = i / self.STEPS
+ pos = (
+ self.rect.left + (i_fract * self.rect.width),
+ self.rect.bottom - self.func(i_fract) * self.rect.height,
+ )
+ pygame.draw.polygon(
+ surf, self.GRAPH_COLOR, list(make_line_shape(prev, pos, 8, self.WIDTH))
+ )
+ prev = pos
+
+ def update(self):
+ self.current_frame += 1
+ full_cycle = sum(self.CYCLE.values())
+ elapsed = self.current_frame / FPS % (full_cycle * 2)
+ if elapsed > full_cycle:
+ elapsed = 2 * full_cycle - elapsed
+ start = self.CYCLE["pause"] / 2
+ end = start + self.CYCLE["length"]
+ if elapsed < start:
+ current_setting = 0
+ elif elapsed > end:
+ current_setting = 1
+ else:
+ current_setting = (elapsed - start) / self.CYCLE["length"]
+ if current_setting == self.current_setting:
+ return False
+ self.current_setting = current_setting
+ return True
+
+
+def ease_in_out_elastic(mag):
+ p = 1 - mag
+ s = p / tau * asin(1)
+ def inner(x):
+ if x == 0:
+ return 0
+ elif x == 1:
+ return 1
+ elif x < 0 or x > 1:
+ raise ValueError(f"x must be between 0 and 1: got {x}")
+ st = x * 2
+ st1 = st - 1
+ sgn = (st >= 1) * 2 - 1
+ return 2 ** (-sgn * 10 * st1 - 1) * sin((st1 - s) * tau / p) * sgn + (sgn > 0)
+ return inner
+
+
+def main():
+ pygame.init()
+ surf = pygame.display.set_mode((800, 600))
+ clock = pygame.time.Clock()
+ running = True
+ dirty = False
+ #lerp = lambda start, end, x: start + (end - start) * x
+ ease_in = lambda x: x * x
+ flip = lambda x: 1 - x
+ mirror = lambda f, x: (f(2 * x) if x < 0.5 else flip(f(2 * x - 2)) + 1) / 2
+ #ease_out = lambda x: flip(ease_in(flip(x)))
+ #ease_in_out = lambda x: lerp(ease_in(x), ease_out(x), x)
+ #ease_in_out = lambda x: ease_in(x) if x < .5 else ease_out(x)
+ ease_in_out = lambda x: mirror(ease_in, x)
+ #one_minus_cos = lambda x: .5 - cos(tau * x / 2) / 2
+ eg = EaseGraph(
+ pygame.Rect((64, 64), (472, 472)),
+ ((600, 64), (600, 536)),
+ ease_in_out_elastic(5 ** .5 / 2 - 0.5)
+ )
+ frame = 0
+ while True:
+ for ev in pygame.event.get():
+ if ev.type == pygame.QUIT:
+ running = False
+ break
+ elif ev.type == pygame.WINDOWEXPOSED:
+ dirty = True
+ elif ev.type == pygame.KEYDOWN:
+ if ev.key == pygame.K_ESCAPE:
+ running = False
+ break
+ if not running:
+ break
+ dirty |= eg.update()
+ dirty = True
+ if dirty:
+ surf.fill("black")
+ angle = frame / (tau * 30)
+ a = (300 + cos(angle) * 200, 300 + sin(angle) * 200)
+ b = (300 + cos(angle + pi) * 200, 300 + sin(angle + pi) * 200)
+ width = 100
+ n = 5
+ pygame.draw.polygon(
+ surf, "purple", list(make_line_shape(a, b, n, width / 2)), 1
+ )
+ shape = [
+ (cos(a) * width / 2, sin(a) * width / 2)
+ for a in (i * tau / n for i in range(n))
+ ]
+ pygame.draw.polygon(surf, "yellow", [(s[0] + a[0], s[1] + a[1]) for s in shape], 1)
+ pygame.draw.polygon(surf, "yellow", [(s[0] + b[0], s[1] + b[1]) for s in shape], 1)
+ frame += 1
+ eg.draw(surf)
+ pygame.display.update()
+ dirty = False
+ clock.tick(FPS)
+
+
+if __name__ == "__main__":
+ main()