diff --git a/src/engine.py b/src/engine.py index aa1bfa5..0816805 100644 --- a/src/engine.py +++ b/src/engine.py @@ -1,5 +1,6 @@ from decimal import Decimal import logging +import math from models import ( EngineFixture, @@ -67,27 +68,18 @@ class Engine: continue still_running = True - # Move keyframes forward + # Move track forward t.step(step_size) if t.current is None: - raise BaseException(f"Track has no current keyframes.") + raise BaseException(f"Track has no current keyframe.") - # Interpolate over keyframes of track - for k in t.current: - - next_value: KeyframeValue = None - if t.next: - try: - next_value = t.next.get(k.parameter).value - except: - pass # next_value already None by default - - interpolated_value = self.interpolate_value(k.value, next_value, k.interpolation) + # Interpolate and update value + interpolated_value = self.get_current_value(t) - # Update values of relevant fixtures - for f in self.fixtures.values(): - if f.fixture.id in t.track.fixture_ids: - f.set(k.parameter, interpolated_value) + # Update values of relevant fixtures + for f in self.fixtures.values(): + if f.fixture.id in t.track.fixture_ids: + f.set(t.track.parameter, interpolated_value) # Update running state self.is_running = still_running @@ -100,11 +92,97 @@ class Engine: api = self.apis[f.fixture.api] # TODO: Pass update to API + def get_current_value(self, track: EngineTrack) -> KeyframeValue: + """Interpolate between current keyframes of track and return the interpolated value.""" + current = track.current + if current is None: + raise BaseException( + f"Track has no current keyframe. Cannot interpolate values." + ) + + next = track.next + if next is None: + return current.frame.value + + t = current.delta.copy_abs() / ( + current.delta.copy_abs() + next.delta.copy_abs() + ) + + return self.interpolate_value( + current.frame.value, next.frame.value, t, current.frame.interpolation + ) + def interpolate_value( - self, current: KeyframeValue, next: KeyframeValue, interpolation: Interpolation - ) -> KeyframeValue: - """Interpolate between two keyframe values and return the interpolated value.""" - cur_frame: Keyframe = current.get(parameter) - if - # TODO - return current.get(parameter).value + self, + start: KeyframeValue, + end: KeyframeValue, + t: Decimal, + interpolation: Interpolation, + ): + """CHATGPT. Interpolate between two numeric values with given interpolation type. + t is normalized between 0.0 (start) and 1.0 (end).""" + + if not isinstance(start, (int, float)) or not isinstance(end, (int, float)): + logging.warning( + f"Interpolation [{interpolation}] only supported for int/float. " + f"Got {type(start)} → {start}, {type(end)} → {end}. Falling back to STEP." + ) + return end # default STEP behavior + + # Clamp t for safety + t = max(Decimal.from_float(0), min(Decimal.from_float(1), t)) + + match interpolation: + case Interpolation.STEP: + return end + case Interpolation.LINEAR: + f = t + case Interpolation.EASE_IN: + f = t * t + case Interpolation.EASE_OUT: + f = 1 - (1 - t) * (1 - t) + case Interpolation.EASE_IN_OUT: + f = 2 * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 2) / 2 + case Interpolation.EXPONENTIAL: + f = pow(2, 10 * (t - 1)) if t > 0 else 0 + case Interpolation.LOGARITHMIC: + f = math.log10(9 * t + 1) # maps 0→0, 1→1 smoothly + case Interpolation.SINE: + f = 0.5 - 0.5 * math.cos(Decimal.from_float(math.pi) * t) + case Interpolation.SMOOTHSTEP: + f = t * t * (3 - 2 * t) + case Interpolation.BOUNCE: + f = self.bounce_out(float(t)) + case Interpolation.ELASTIC: + f = self.elastic_out(t) + case _: + logging.warning( + f"Unhandled interpolation type [{interpolation}]. Using STEP." + ) + return end + + raise BaseException("Unreachable code reached.") + + def bounce_out(self, t: float) -> float: + """CHATGPT. Bounce easing function (out).""" + n1, d1 = 7.5625, 2.75 + if t < 1 / d1: + return n1 * t * t + elif t < 2 / d1: + t -= 1.5 / d1 + return n1 * t * t + 0.75 + elif t < 2.5 / d1: + t -= 2.25 / d1 + return n1 * t * t + 0.9375 + else: + t -= 2.625 / d1 + return n1 * t * t + 0.984375 + + def elastic_out(self, t: Decimal) -> float: + """CHATGPT. Elastic easing function (out).""" + c4 = (2 * math.pi) / 3 + if t == 0: + return 0 + if t == 1: + return 1 + return pow(2, -10 * t) * math.sin((float(t) * 10 - 0.75) * c4) + 1 diff --git a/src/models.py b/src/models.py index 98b955c..cc7693e 100644 --- a/src/models.py +++ b/src/models.py @@ -151,6 +151,12 @@ class Interpolation(Enum): EASE_IN_OUT = "ease-in-out" EASE_IN = "ease-in" EASE_OUT = "ease-out" + EXPONENTIAL = "exponential" + LOGARITHMIC = "logarithmic" + SINE = "sine" + SMOOTHSTEP = "smoothstep" + BOUNCE = "bounce" + ELASTIC = "elastic" @dataclass_json