mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(core): add deterministic keyframe ease runtime
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { evaluateSpringEase, parseSpringBounce } from "./springEase";
|
||||
|
||||
describe("single-parameter spring ease", () => {
|
||||
it("parses and clamps the bounce parameter", () => {
|
||||
expect(parseSpringBounce("spring(0.5)")).toBe(0.5);
|
||||
expect(parseSpringBounce(" spring(2) ")).toBe(1);
|
||||
expect(parseSpringBounce("spring(-1)")).toBe(0);
|
||||
expect(parseSpringBounce("spring(nope)")).toBeNull();
|
||||
});
|
||||
|
||||
it("starts at zero, overshoots, and settles exactly at one", () => {
|
||||
const samples = Array.from({ length: 101 }, (_, index) => evaluateSpringEase(index / 100, 0.5));
|
||||
expect(samples[0]).toBe(0);
|
||||
expect(samples.at(-1)).toBe(1);
|
||||
expect(Math.max(...samples)).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("is deterministic and makes higher bounce values more oscillatory", () => {
|
||||
const progress = Array.from({ length: 41 }, (_, index) => index / 40);
|
||||
expect(progress.map((value) => evaluateSpringEase(value, 0.75))).toEqual(
|
||||
progress.map((value) => evaluateSpringEase(value, 0.75)),
|
||||
);
|
||||
|
||||
const lowBounce = progress.map((value) => evaluateSpringEase(value, 0.25));
|
||||
const highBounce = progress.map((value) => evaluateSpringEase(value, 0.75));
|
||||
expect(Math.max(...highBounce)).toBeGreaterThan(Math.max(...lowBounce));
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,31 @@
|
||||
/** @deprecated Import from @hyperframes/parsers/spring-ease */
|
||||
// Preserve the legacy symbols (SpringPreset, SPRING_PRESETS, generateSpringEaseData)
|
||||
// on the still-published ./spring-ease subpath. Dropping them is a breaking change
|
||||
// for external importers; the canonical source stays @hyperframes/parsers/spring-ease.
|
||||
export * from "@hyperframes/parsers/spring-ease";
|
||||
|
||||
const SPRING_TOKEN = /^\s*spring\(\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*\)\s*$/;
|
||||
|
||||
function clampBounce(bounce: number): number {
|
||||
return Math.max(0, Math.min(1, bounce));
|
||||
}
|
||||
|
||||
/** Parse Studio's single-parameter spring token into a normalized bounce value. */
|
||||
export function parseSpringBounce(ease: string): number | null {
|
||||
const match = SPRING_TOKEN.exec(ease);
|
||||
if (!match) return null;
|
||||
const bounce = Number(match[1]);
|
||||
return Number.isFinite(bounce) ? clampBounce(bounce) : null;
|
||||
}
|
||||
|
||||
/** Evaluate Studio's deterministic, endpoint-normalized damped-cosine spring. */
|
||||
export function evaluateSpringEase(progress: number, bounce: number): number {
|
||||
if (!Number.isFinite(progress)) return progress;
|
||||
if (progress <= 0) return 0;
|
||||
if (progress >= 1) return 1;
|
||||
|
||||
const normalizedBounce = clampBounce(bounce);
|
||||
const decay = 12 - normalizedBounce * 6;
|
||||
const angularFrequency = Math.PI * 2 * (1 + normalizedBounce * 1.5);
|
||||
const endpoint = 1 - Math.exp(-decay) * Math.cos(angularFrequency);
|
||||
return (1 - Math.exp(-decay * progress) * Math.cos(angularFrequency * progress)) / endpoint;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user