Release NaraMotion v0.26.0: Structure of Arrays (SoA), Path Arena Allocator, Uniform Grid Indexing, Dual-Buffer Rendering & Centroid Spring Pop-in
b516e00 verified Download blend2d/motion/easing.py from ArifiIslam/NaraMotion: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/ArifiIslam/NaraMotion/resolve/main/blend2d/motion/easing.py
- Command line
-
hf download hf://ArifiIslam/NaraMotion/blend2d/motion/easing.py
-
curl -L -o easing.py https://huggingface.co/ArifiIslam/NaraMotion/resolve/main/blend2d/motion/easing.py
14.6 kB
| """ | |
| High-Performance Easing Curves, Physics Springs, and Interpolators for Blend2D Motion. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from typing import Callable, Union, Tuple, Optional, Dict, Any | |
| from blend2d.color import parse_color, Rgba32 | |
| from blend2d.matrix import Matrix2D | |
| # ============================================================================ | |
| # Core Interpolators | |
| # ============================================================================ | |
| def lerp(a: float, b: float, t: float) -> float: | |
| """Standard linear interpolation between a and b at progress t.""" | |
| return a + (b - a) * t | |
| def clamp(val: float, min_val: float = 0.0, max_val: float = 1.0) -> float: | |
| """Clamp value between min_val and max_val.""" | |
| return max(min_val, min(max_val, val)) | |
| def lerp_color(c1: Union[str, Rgba32, int], c2: Union[str, Rgba32, int], t: float) -> Rgba32: | |
| """Smooth linear color interpolation between two colors.""" | |
| t = clamp(t, 0.0, 1.0) | |
| col1 = parse_color(c1) | |
| col2 = parse_color(c2) | |
| r = int(round(lerp(col1.r, col2.r, t))) | |
| g = int(round(lerp(col1.g, col2.g, t))) | |
| b = int(round(lerp(col1.b, col2.b, t))) | |
| a = int(round(lerp(col1.a, col2.a, t))) | |
| return Rgba32(r, g, b, a) | |
| def lerp_matrix(m1: Matrix2D, m2: Matrix2D, t: float) -> Matrix2D: | |
| """Interpolate all affine transformation matrix components.""" | |
| return Matrix2D( | |
| m00=lerp(m1.m00, m2.m00, t), | |
| m01=lerp(m1.m01, m2.m01, t), | |
| m10=lerp(m1.m10, m2.m10, t), | |
| m11=lerp(m1.m11, m2.m11, t), | |
| m20=lerp(m1.m20, m2.m20, t), | |
| m21=lerp(m1.m21, m2.m21, t) | |
| ) | |
| # ============================================================================ | |
| # Penner Easing Equations | |
| # ============================================================================ | |
| class Easing: | |
| """Collection of standard Penner animation curves and timing functions.""" | |
| def linear(t: float) -> float: | |
| return t | |
| # Quad | |
| def quad_in(t: float) -> float: | |
| return t * t | |
| def quad_out(t: float) -> float: | |
| return t * (2.0 - t) | |
| def quad_in_out(t: float) -> float: | |
| return 2.0 * t * t if t < 0.5 else -1.0 + (4.0 - 2.0 * t) * t | |
| # Cubic | |
| def cubic_in(t: float) -> float: | |
| return t * t * t | |
| def cubic_out(t: float) -> float: | |
| t -= 1.0 | |
| return t * t * t + 1.0 | |
| def cubic_in_out(t: float) -> float: | |
| return 4.0 * t * t * t if t < 0.5 else (t - 1.0) * (2.0 * t - 2.0) * (2.0 * t - 2.0) + 1.0 | |
| # Quart | |
| def quart_in(t: float) -> float: | |
| return t * t * t * t | |
| def quart_out(t: float) -> float: | |
| t -= 1.0 | |
| return 1.0 - t * t * t * t | |
| def quart_in_out(t: float) -> float: | |
| t2 = t * 2.0 | |
| if t2 < 1.0: | |
| return 0.5 * t2 * t2 * t2 * t2 | |
| t2 -= 2.0 | |
| return -0.5 * (t2 * t2 * t2 * t2 - 2.0) | |
| # Quint | |
| def quint_in(t: float) -> float: | |
| return t * t * t * t * t | |
| def quint_out(t: float) -> float: | |
| t -= 1.0 | |
| return t * t * t * t * t + 1.0 | |
| def quint_in_out(t: float) -> float: | |
| t2 = t * 2.0 | |
| if t2 < 1.0: | |
| return 0.5 * t2 * t2 * t2 * t2 * t2 | |
| t2 -= 2.0 | |
| return 0.5 * (t2 * t2 * t2 * t2 * t2 + 2.0) | |
| # Sine | |
| def sine_in(t: float) -> float: | |
| return 1.0 - math.cos(t * math.pi * 0.5) | |
| def sine_out(t: float) -> float: | |
| return math.sin(t * math.pi * 0.5) | |
| def sine_in_out(t: float) -> float: | |
| return 0.5 * (1.0 - math.cos(math.pi * t)) | |
| # Expo | |
| def expo_in(t: float) -> float: | |
| return 0.0 if t <= 0.0 else math.pow(2.0, 10.0 * (t - 1.0)) | |
| def expo_out(t: float) -> float: | |
| return 1.0 if t >= 1.0 else 1.0 - math.pow(2.0, -10.0 * t) | |
| def expo_in_out(t: float) -> float: | |
| if t <= 0.0: return 0.0 | |
| if t >= 1.0: return 1.0 | |
| t2 = t * 2.0 | |
| if t2 < 1.0: | |
| return 0.5 * math.pow(2.0, 10.0 * (t2 - 1.0)) | |
| return 0.5 * (2.0 - math.pow(2.0, -10.0 * (t2 - 1.0))) | |
| # Circ | |
| def circ_in(t: float) -> float: | |
| return 1.0 - math.sqrt(max(0.0, 1.0 - t * t)) | |
| def circ_out(t: float) -> float: | |
| t -= 1.0 | |
| return math.sqrt(max(0.0, 1.0 - t * t)) | |
| def circ_in_out(t: float) -> float: | |
| t2 = t * 2.0 | |
| if t2 < 1.0: | |
| return -0.5 * (math.sqrt(max(0.0, 1.0 - t2 * t2)) - 1.0) | |
| t2 -= 2.0 | |
| return 0.5 * (math.sqrt(max(0.0, 1.0 - t2 * t2)) + 1.0) | |
| # Back | |
| def back_in(t: float, s: float = 1.70158) -> float: | |
| return t * t * ((s + 1.0) * t - s) | |
| def back_out(t: float, s: float = 1.70158) -> float: | |
| t -= 1.0 | |
| return t * t * ((s + 1.0) * t + s) + 1.0 | |
| def back_in_out(t: float, s: float = 1.70158) -> float: | |
| s2 = s * 1.525 | |
| t2 = t * 2.0 | |
| if t2 < 1.0: | |
| return 0.5 * (t2 * t2 * ((s2 + 1.0) * t2 - s2)) | |
| t2 -= 2.0 | |
| return 0.5 * (t2 * t2 * ((s2 + 1.0) * t2 + s2) + 2.0) | |
| # Elastic | |
| def elastic_in(t: float) -> float: | |
| if t <= 0.0: return 0.0 | |
| if t >= 1.0: return 1.0 | |
| p = 0.3 | |
| s = p / 4.0 | |
| t -= 1.0 | |
| return -(math.pow(2.0, 10.0 * t) * math.sin((t - s) * (2.0 * math.pi) / p)) | |
| def elastic_out(t: float) -> float: | |
| if t <= 0.0: return 0.0 | |
| if t >= 1.0: return 1.0 | |
| p = 0.3 | |
| s = p / 4.0 | |
| return math.pow(2.0, -10.0 * t) * math.sin((t - s) * (2.0 * math.pi) / p) + 1.0 | |
| def elastic_in_out(t: float) -> float: | |
| if t <= 0.0: return 0.0 | |
| if t >= 1.0: return 1.0 | |
| t2 = t * 2.0 | |
| p = 0.3 * 1.5 | |
| s = p / 4.0 | |
| if t2 < 1.0: | |
| t2 -= 1.0 | |
| return -0.5 * (math.pow(2.0, 10.0 * t2) * math.sin((t2 - s) * (2.0 * math.pi) / p)) | |
| t2 -= 1.0 | |
| return math.pow(2.0, -10.0 * t2) * math.sin((t2 - s) * (2.0 * math.pi) / p) * 0.5 + 1.0 | |
| # Bounce | |
| def bounce_out(t: float) -> float: | |
| if t < (1.0 / 2.75): | |
| return 7.5625 * t * t | |
| elif t < (2.0 / 2.75): | |
| t -= 1.5 / 2.75 | |
| return 7.5625 * t * t + 0.75 | |
| elif t < (2.5 / 2.75): | |
| t -= 2.25 / 2.75 | |
| return 7.5625 * t * t + 0.9375 | |
| else: | |
| t -= 2.625 / 2.75 | |
| return 7.5625 * t * t + 0.984375 | |
| def bounce_in(t: float) -> float: | |
| return 1.0 - Easing.bounce_out(1.0 - t) | |
| def bounce_in_out(t: float) -> float: | |
| if t < 0.5: | |
| return Easing.bounce_in(t * 2.0) * 0.5 | |
| return Easing.bounce_out(t * 2.0 - 1.0) * 0.5 + 0.5 | |
| def get(cls, name: Union[str, Callable[[float], float]]) -> Callable[[float], float]: | |
| """Lookup easing function by name (e.g. 'easeInOutCubic', 'bounce_out', 'sineIn').""" | |
| if callable(name): | |
| return name | |
| key = name.lower().replace("-", "_").replace(" ", "_") | |
| # Standard camelCase conversions | |
| mapping = { | |
| "easeinquad": cls.quad_in, | |
| "easeoutquad": cls.quad_out, | |
| "easeinoutquad": cls.quad_in_out, | |
| "easeincubic": cls.cubic_in, | |
| "easeoutcubic": cls.cubic_out, | |
| "easeinoutcubic": cls.cubic_in_out, | |
| "easeinquart": cls.quart_in, | |
| "easeoutquart": cls.quart_out, | |
| "easeinoutquart": cls.quart_in_out, | |
| "easeinquint": cls.quint_in, | |
| "easeoutquint": cls.quint_out, | |
| "easeinoutquint": cls.quint_in_out, | |
| "easeinsine": cls.sine_in, | |
| "easeoutsine": cls.sine_out, | |
| "easeinoutsine": cls.sine_in_out, | |
| "easeinexpo": cls.expo_in, | |
| "easeoutexpo": cls.expo_out, | |
| "easeinoutexpo": cls.expo_in_out, | |
| "easeincirc": cls.circ_in, | |
| "easeoutcirc": cls.circ_out, | |
| "easeinoutcirc": cls.circ_in_out, | |
| "easeinback": cls.back_in, | |
| "easeoutback": cls.back_out, | |
| "easeinoutback": cls.back_in_out, | |
| "easeinelastic": cls.elastic_in, | |
| "easeoutelastic": cls.elastic_out, | |
| "easeinoutelastic": cls.elastic_in_out, | |
| "easeinbounce": cls.bounce_in, | |
| "easeoutbounce": cls.bounce_out, | |
| "easeinoutbounce": cls.bounce_in_out, | |
| } | |
| clean_key = key.replace("_", "") | |
| if clean_key in mapping: | |
| return mapping[clean_key] | |
| # Check attributes | |
| if hasattr(cls, key): | |
| return getattr(cls, key) | |
| # Check reversed formats like in_out_cubic, in_cubic, out_cubic | |
| for fam in ("quad", "cubic", "quart", "quint", "sine", "expo", "circ", "back", "elastic", "bounce"): | |
| if fam in clean_key: | |
| if "inout" in clean_key or "in_out" in key: | |
| return getattr(cls, f"{fam}_in_out", cls.linear) | |
| elif "out" in clean_key: | |
| return getattr(cls, f"{fam}_out", cls.linear) | |
| elif "in" in clean_key: | |
| return getattr(cls, f"{fam}_in", cls.linear) | |
| return cls.linear | |
| # Direct aliases for common AI hallucinations: | |
| in_quad = quad_in | |
| out_quad = quad_out | |
| in_out_quad = quad_in_out | |
| easeInOutQuad = quad_in_out | |
| easeInQuad = quad_in | |
| easeOutQuad = quad_out | |
| in_cubic = cubic_in | |
| out_cubic = cubic_out | |
| in_out_cubic = cubic_in_out | |
| easeInOutCubic = cubic_in_out | |
| easeInCubic = cubic_in | |
| easeOutCubic = cubic_out | |
| in_quart = quart_in | |
| out_quart = quart_out | |
| in_out_quart = quart_in_out | |
| easeInOutQuart = quart_in_out | |
| in_quint = quint_in | |
| out_quint = quint_out | |
| in_out_quint = quint_in_out | |
| easeInOutQuint = quint_in_out | |
| in_sine = sine_in | |
| out_sine = sine_out | |
| in_out_sine = sine_in_out | |
| easeInOutSine = sine_in_out | |
| in_expo = expo_in | |
| out_expo = expo_out | |
| in_out_expo = expo_in_out | |
| easeInOutExpo = expo_in_out | |
| in_circ = circ_in | |
| out_circ = circ_out | |
| in_out_circ = circ_in_out | |
| easeInOutCirc = circ_in_out | |
| in_back = back_in | |
| out_back = back_out | |
| in_out_back = back_in_out | |
| easeInOutBack = back_in_out | |
| in_elastic = elastic_in | |
| out_elastic = elastic_out | |
| in_out_elastic = elastic_in_out | |
| easeInOutElastic = elastic_in_out | |
| in_bounce = bounce_in | |
| out_bounce = bounce_out | |
| in_out_bounce = bounce_in_out | |
| easeInOutBounce = bounce_in_out | |
| # ============================================================================ | |
| # Cubic Bezier Easing Evaluator | |
| # ============================================================================ | |
| class CubicBezier: | |
| """ | |
| CSS / After Effects style Cubic Bezier timing function `cubic-bezier(x1, y1, x2, y2)`. | |
| """ | |
| def __init__(self, x1: float, y1: float, x2: float, y2: float): | |
| self.x1 = float(x1) | |
| self.y1 = float(y1) | |
| self.x2 = float(x2) | |
| self.y2 = float(y2) | |
| def evaluate(self, t: float) -> float: | |
| if t <= 0.0: return 0.0 | |
| if t >= 1.0: return 1.0 | |
| # Solve for parameter u where B_x(u) = t using Newton-Raphson | |
| u = t | |
| for _ in range(8): | |
| # B_x(u) = 3(1-u)^2*u*x1 + 3(1-u)*u^2*x2 + u^3 | |
| u2 = u * u | |
| u3 = u2 * u | |
| om_u = 1.0 - u | |
| om_u2 = om_u * om_u | |
| current_x = 3.0 * om_u2 * u * self.x1 + 3.0 * om_u * u2 * self.x2 + u3 | |
| dx = 3.0 * om_u2 * self.x1 + 6.0 * om_u * u * (self.x2 - self.x1) + 3.0 * u2 * (1.0 - self.x2) | |
| diff = current_x - t | |
| if abs(diff) < 1e-6: | |
| break | |
| if abs(dx) < 1e-6: | |
| break | |
| u -= diff / dx | |
| u = max(0.0, min(1.0, u)) | |
| # Evaluate B_y(u) | |
| om_u = 1.0 - u | |
| return 3.0 * om_u * om_u * u * self.y1 + 3.0 * om_u * u * u * self.y2 + u * u * u | |
| def __call__(self, t: float) -> float: | |
| return self.evaluate(t) | |
| # ============================================================================ | |
| # Physics-Based Spring Dynamics | |
| # ============================================================================ | |
| class Spring: | |
| """ | |
| Analytical Damped Harmonic Oscillator for natural physics animations. | |
| """ | |
| def __init__( | |
| self, | |
| stiffness: float = 100.0, | |
| damping: float = 10.0, | |
| mass: float = 1.0, | |
| initial_velocity: float = 0.0 | |
| ): | |
| self.stiffness = float(stiffness) | |
| self.damping = float(damping) | |
| self.mass = float(mass) | |
| self.v0 = float(initial_velocity) | |
| # Natural frequency and damping ratio | |
| self.omega0 = math.sqrt(self.stiffness / self.mass) | |
| self.zeta = self.damping / (2.0 * math.sqrt(self.stiffness * self.mass)) | |
| if self.zeta < 1.0: | |
| # Underdamped | |
| self.omega_d = self.omega0 * math.sqrt(1.0 - self.zeta * self.zeta) | |
| else: | |
| self.omega_d = 0.0 | |
| def evaluate(self, t: float) -> float: | |
| """Evaluate normalized spring value at time t seconds (approaching 1.0).""" | |
| if t <= 0.0: | |
| return 0.0 | |
| if self.zeta < 1.0: | |
| # Underdamped (oscillates and settles) | |
| alpha = self.zeta * self.omega0 | |
| c1 = 1.0 | |
| c2 = (self.v0 + alpha * c1) / self.omega_d if self.omega_d > 0.0 else 0.0 | |
| decay = math.exp(-alpha * t) | |
| osc = c1 * math.cos(self.omega_d * t) + c2 * math.sin(self.omega_d * t) | |
| return 1.0 - decay * osc | |
| elif abs(self.zeta - 1.0) < 1e-5: | |
| # Critically damped | |
| alpha = self.omega0 | |
| c1 = 1.0 | |
| c2 = self.v0 + alpha * c1 | |
| return 1.0 - (c1 + c2 * t) * math.exp(-alpha * t) | |
| else: | |
| # Overdamped | |
| alpha = self.zeta * self.omega0 | |
| beta = self.omega0 * math.sqrt(self.zeta * self.zeta - 1.0) | |
| r1 = -alpha + beta | |
| r2 = -alpha - beta | |
| c2 = (self.v0 - r1) / (r2 - r1) | |
| c1 = 1.0 - c2 | |
| return 1.0 - (c1 * math.exp(r1 * t) + c2 * math.exp(r2 * t)) | |
| def __call__(self, t: float) -> float: | |
| return self.evaluate(t) | |