UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

154 lines (119 loc) 5.43 kB
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; }