'use strict'; (function(global) { global.LabFX = global.LabFX || {}; /* ───────────────────────────────────────────── EASINGS — standard Penner equations ───────────────────────────────────────────── */ var easings = { linear: function(t) { return t; }, easeInQuad: function(t) { return t * t; }, easeOutQuad: function(t) { return t * (2 - t); }, easeInOutQuad: function(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }, easeInCubic: function(t) { return t * t * t; }, easeOutCubic: function(t) { var u = t - 1; return u * u * u + 1; }, easeInOutCubic: function(t) { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; }, easeInOutQuint: function(t) { return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t; }, easeOutBack: function(t) { var c1 = 1.70158; var c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }, easeOutElastic: function(t) { if (t === 0 || t === 1) return t; var c4 = (2 * Math.PI) / 3; return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; }, easeInExpo: function(t) { return t === 0 ? 0 : Math.pow(2, 10 * t - 10); }, easeOutExpo: function(t) { return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); } }; /* ───────────────────────────────────────────── TWEEN ───────────────────────────────────────────── */ /** * Tween a numeric value from `from` to `to` over `durMs` milliseconds. * * @param {number} from * @param {number} to * @param {number} durMs * @param {function|string} easing - easing fn or key in LabFX.motion.easings * @param {function} onUpdate - called with current value each frame * @param {function} [onDone] * @returns {{ cancel: function, running: boolean }} */ function tween(from, to, durMs, easing, onUpdate, onDone) { var easeFn = (typeof easing === 'function') ? easing : (easings[easing] || easings.linear); var start = null; var handle = { running: true, cancel: null }; var rafId; function step(now) { if (!handle.running) return; if (start === null) start = now; var elapsed = now - start; var progress = Math.min(elapsed / durMs, 1); var value = from + (to - from) * easeFn(progress); onUpdate(value); if (progress < 1) { rafId = requestAnimationFrame(step); } else { handle.running = false; if (typeof onDone === 'function') onDone(); } } handle.cancel = function() { handle.running = false; cancelAnimationFrame(rafId); }; rafId = requestAnimationFrame(step); return handle; } /* ───────────────────────────────────────────── SPRING (critically-damped simulation) ───────────────────────────────────────────── */ /** * Returns a spring factory. * Usage: LabFX.motion.spring(170, 26)(from, to, onUpdate, onDone) → handle * * @param {number} stiffness (default 170) * @param {number} damping (default 26) */ function springFactory(stiffness, damping) { stiffness = stiffness != null ? stiffness : 170; damping = damping != null ? damping : 26; return function(from, to, onUpdate, onDone) { var pos = from; var vel = 0; var prev = null; var handle = { running: true, cancel: null }; var rafId; function step(now) { if (!handle.running) return; if (prev === null) { prev = now; } var dt = Math.min((now - prev) / 1000, 0.05); /* cap at 50ms */ prev = now; var force = -stiffness * (pos - to) - damping * vel; vel += force * dt; pos += vel * dt; onUpdate(pos); var settled = Math.abs(pos - to) < 0.01 && Math.abs(vel) < 0.01; if (!settled) { rafId = requestAnimationFrame(step); } else { onUpdate(to); handle.running = false; if (typeof onDone === 'function') onDone(); } } handle.cancel = function() { handle.running = false; cancelAnimationFrame(rafId); }; rafId = requestAnimationFrame(step); return handle; }; } /* ───────────────────────────────────────────── EXPORT ───────────────────────────────────────────── */ global.LabFX.motion = { tween: tween, spring: springFactory, easings: easings }; })(window);