@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
154 lines (119 loc) • 5.43 kB
JavaScript
import { assert } from "../../../core/assert.js";
import AABB2 from "../../../core/geom/2d/aabb/AABB2.js";
import { spline3_hermite_derivative } from "../../../core/math/spline/spline3_hermite_derivative.js";
import { animation_curve_compute_aabb } from "./animation_curve_compute_aabb.js";
import { evaluate_two_key_curve } from "./evaluate_two_key_curve.js";
const bounds = new AABB2();
// Scaling factor to project slope error into value error.
// 0.125 (1/8th) is a standard approximation for cubic bezier error bounds.
const TANGENT_ERROR_WEIGHT = 0.125;
/**
* Computes importance by checking Position AND Slope integrity.
* Uses exact derivative calculation instead of neighbor sampling
*
* @param {Keyframe} key_middle
* @param {Keyframe} key_previous
* @param {Keyframe} key_next
* @return {number} value delta if the middle frame is removed, the higher this value - the more important the middle keyframe is
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
function compute_keyframe_value_effect(
key_middle,
key_previous,
key_next
) {
const duration = key_next.time - key_previous.time;
// Safety check for zero-duration (duplicate keys handled by outer loop, but safe to guard)
if (duration < 1e-9){
return 0;
}
// 1. Calculate Normalized Time t [0, 1]
const t_relative = key_middle.time - key_previous.time;
const t_normalized = t_relative / duration;
// 2. Calculate Position Error (The "Hit Test")
// Does the simplified curve actually hit the middle key's value?
const v_actual = evaluate_two_key_curve(key_middle.time, key_previous, key_next);
const v_error = Math.abs(v_actual - key_middle.value);
// 3. Calculate Derivative Error (The "Shape Test")
// Does the simplified curve flow in the same direction as the original key?
// NOTE: Hermite basis requires tangents scaled by duration (m0, m1)
// We assume keyframes have .outTangent and .inTangent or similar.
// If you use standard Hermite data, it looks like this:
const m0 = key_previous.outTangent * duration;
const m1 = key_next.inTangent * duration;
// This returns the derivative in "Value per Normalized Time"
const slope_normalized = spline3_hermite_derivative(
t_normalized,
key_previous.value,
key_next.value,
m0,
m1
);
// Convert to "Value per Second" to match the keyframe's stored tangent
const slope_actual = slope_normalized / duration;
// We compare against the explicit tangent stored on the middle key (or calculated implicit tangent)
// Assuming the key has a unified tangent or we use the inTangent
const slope_expected = key_middle.inTangent; // or .outTangent, or average if smooth
const slope_delta = Math.abs(slope_actual - slope_expected);
// 4. Project Slope Delta into Value Units
// If the slope is off by 1 unit/sec, how much drift does that cause over the duration?
const slope_error_projected = slope_delta * duration * TANGENT_ERROR_WEIGHT;
return Math.max(v_error, slope_error_projected);
}
/**
* Will remove keyframes that do not affect the shape of the curve significantly.
* The error tolerance determines the significance degree.
* Intended to reduce the complexity of a curve to improve performance and lower memory usage
* @param {AnimationCurve} curve The curve to optimize. The curve is modified in place.
* @param {number} [error_tolerance] how much of a loss to accept, this is relative to normalized value bounds of the curve
* @returns {number} number of removed keys, 0 if curve was unchanged
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export function animation_curve_optimize(
curve,
error_tolerance = 1e-3
) {
assert.lessThan(error_tolerance, 1, 'error_tolerance must be less than 1');
let key_count = curve.length;
if (key_count <= 1) {
// nothing to remove
return 0;
}
animation_curve_compute_aabb(bounds, curve);
const EPSILON = 1e-9;
// Clamp against epsilon to handle the flat-line case or very low tolerances resulting in precision issues
const absolute_error_tolerance = Math.max(
bounds.height * error_tolerance,
EPSILON,
);
const keyframes = curve.keys;
const initial_key_count = key_count;
for (let i = 1; i < key_count; i++) {
const key_previous = keyframes[i - 1];
const key_current = keyframes[i];
let should_remove = false;
if (key_current.equals(key_previous)) {
// adds no semantic value
should_remove = true;
} else if (i < key_count - 1) {
const key_next = keyframes[i + 1];
const max_error = compute_keyframe_value_effect(key_current, key_previous, key_next);
if (max_error <= absolute_error_tolerance) {
// does not significantly affect the curve shape
should_remove = true;
}
}
if (should_remove) {
curve.remove(key_current);
// update iterator
i--;
key_count--;
}
}
// number of removed keys
return initial_key_count - key_count;
}