Source code for simvx.core.math.curves
"""Curve — 1D property-animation curve with linear interpolation and optional bake."""
# ============================================================================
# Curve
# ============================================================================
[docs]
class Curve:
"""1D curve for property animation — stores (t, value) pairs with linear interpolation."""
def __init__(self, points: list[tuple[float, float]] | None = None):
self._points: list[tuple[float, float]] = sorted(points or [], key=lambda p: p[0])
self._baked: list[tuple[float, float]] | None = None
[docs]
def add_point(self, t: float, value: float):
"""Insert a point, maintaining sort order by t."""
self._points.append((float(t), float(value)))
self._points.sort(key=lambda p: p[0])
self._baked = None
[docs]
def remove_point(self, index: int):
"""Remove the point at the given index."""
del self._points[index]
self._baked = None
[docs]
def sample(self, t: float) -> float:
"""Sample the curve at t using linear interpolation."""
if not self._points:
return 0.0
if t <= self._points[0][0]:
return self._points[0][1]
if t >= self._points[-1][0]:
return self._points[-1][1]
# Binary search for the segment
lo, hi = 0, len(self._points) - 1
while lo < hi - 1:
mid = (lo + hi) // 2
if self._points[mid][0] <= t:
lo = mid
else:
hi = mid
t0, v0 = self._points[lo]
t1, v1 = self._points[hi]
dt = t1 - t0
if dt < 1e-10:
return v0
frac = (t - t0) / dt
return v0 + (v1 - v0) * frac
[docs]
def sample_baked(self, t: float) -> float:
"""Sample using a baked (cached) lookup for performance. Falls back to sample()."""
if self._baked is None:
self._bake()
return self._sample_from(self._baked, t)
def _bake(self, resolution: int = 100):
"""Bake the curve into a uniform lookup table."""
if not self._points:
self._baked = []
return
t_min = self._points[0][0]
t_max = self._points[-1][0]
self._baked = []
for i in range(resolution + 1):
t = t_min + (t_max - t_min) * i / resolution
self._baked.append((t, self.sample(t)))
def _sample_from(self, pts: list[tuple[float, float]], t: float) -> float:
if not pts:
return 0.0
if t <= pts[0][0]:
return pts[0][1]
if t >= pts[-1][0]:
return pts[-1][1]
lo, hi = 0, len(pts) - 1
while lo < hi - 1:
mid = (lo + hi) // 2
if pts[mid][0] <= t:
lo = mid
else:
hi = mid
t0, v0 = pts[lo]
t1, v1 = pts[hi]
dt = t1 - t0
if dt < 1e-10:
return v0
return v0 + (v1 - v0) * (t - t0) / dt
@property
def point_count(self) -> int:
return len(self._points)
[docs]
def __repr__(self):
return f"Curve({len(self._points)} points)"