From 52ee8b1553db89d27c224522cc6a226c29c77e88 Mon Sep 17 00:00:00 2001 From: mar77i Date: Mon, 16 Dec 2024 02:26:01 +0100 Subject: [PATCH] add ease.py --- ease.py | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100755 ease.py diff --git a/ease.py b/ease.py new file mode 100755 index 0000000..a18527a --- /dev/null +++ b/ease.py @@ -0,0 +1,169 @@ +#!/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() -- 2.47.1