// phys8-anim.js — easing-функции и tween-engine для микро-анимаций Физики 8. // Экспорт в window.P8Anim = { ease, tween, raf, lerp, clamp, smoothstep, oscillate } // Без зависимостей. (function () { 'use strict'; // === Math utils === const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); const lerp = (a, b, t) => a + (b - a) * t; const smoothstep = (a, b, x) => { const t = clamp((x - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); }; // === Easing functions (cubic-bezier-style + spring + bounce) === const ease = { linear: t => t, quadIn: t => t * t, quadOut: t => t * (2 - t), quadInOut: t => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t, cubicIn: t => t * t * t, cubicOut: t => (--t) * t * t + 1, cubicInOut: t => t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, expoOut: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t), backOut: t => { const s = 1.70158; return --t * t * ((s + 1) * t + s) + 1; }, elasticOut: t => { if (t === 0 || t === 1) return t; const p = 0.3; return Math.pow(2, -10 * t) * Math.sin((t - p/4) * (2 * Math.PI) / p) + 1; }, bounceOut: t => { if (t < 1/2.75) return 7.5625 * t * t; if (t < 2/2.75) return 7.5625 * (t -= 1.5/2.75) * t + .75; if (t < 2.5/2.75) return 7.5625 * (t -= 2.25/2.75) * t + .9375; return 7.5625 * (t -= 2.625/2.75) * t + .984375; }, // Spring — затухающие колебания spring: (t, mass = 1, stiff = 100, damp = 10) => { if (t >= 1) return 1; const w = Math.sqrt(stiff / mass); const z = damp / (2 * Math.sqrt(stiff * mass)); if (z < 1) { const wd = w * Math.sqrt(1 - z*z); return 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + z * w / wd * Math.sin(wd * t)); } return 1 - Math.exp(-w * t) * (1 + w * t); } }; // === Tween engine === // Использование: // P8Anim.tween({ from: 0, to: 100, duration: 500, easing: 'quadOut', // onUpdate: v => el.style.opacity = v, onComplete: () => {...} }); // Возвращает { cancel() } function tween(opts) { const { from = 0, to = 1, duration = 500, easing = 'cubicOut', onUpdate = () => {}, onComplete = () => {}, delay = 0 } = opts; const easeFn = typeof easing === 'function' ? easing : (ease[easing] || ease.cubicOut); let startTime = null; let rafId = null; let cancelled = false; function step(t) { if (cancelled) return; if (startTime === null) startTime = t + delay; if (t < startTime) { rafId = requestAnimationFrame(step); return; } const elapsed = t - startTime; const k = clamp(elapsed / duration, 0, 1); const eased = easeFn(k); const v = lerp(from, to, eased); onUpdate(v, eased, k); if (k < 1) { rafId = requestAnimationFrame(step); } else { onComplete(); } } rafId = requestAnimationFrame(step); return { cancel() { cancelled = true; if (rafId !== null) cancelAnimationFrame(rafId); } }; } // === RAF wrapper — для непрерывных симуляций === // Использование: // const loop = P8Anim.raf(dt => { /* dt в секундах */ }); // loop.start(); loop.stop(); function raf(callback) { let id = null, last = 0, running = false; function tick(t) { const dt = last ? (t - last) / 1000 : 0; last = t; if (!running) return; try { callback(dt, t); } catch (e) { console.warn('raf cb:', e.message); } id = requestAnimationFrame(tick); } return { start() { if (running) return; running = true; last = 0; id = requestAnimationFrame(tick); }, stop() { running = false; if (id) cancelAnimationFrame(id); id = null; }, get running() { return running; } }; } // === Oscillate (синус-волна, для пульсаций) === // amplitude * sin(2pi*frequency*t + phase) + offset function oscillate(t, frequency = 1, amplitude = 1, phase = 0, offset = 0) { return offset + amplitude * Math.sin(2 * Math.PI * frequency * t + phase); } // === Helper: scheduler — задержка с возможностью отмены === function after(ms, fn) { const id = setTimeout(fn, ms); return { cancel: () => clearTimeout(id) }; } // === Helper: animateCSS — добавить класс на duration, потом снять === function animateCSS(el, className, duration = 350) { if (!el) return; el.classList.add(className); return after(duration, () => el.classList.remove(className)); } // === Stagger — массив элементов с задержкой между анимациями === function stagger(elements, callback, perItemDelay = 50) { [...elements].forEach((el, i) => { after(i * perItemDelay, () => callback(el, i)); }); } // === Visibility observer — запуск анимации при появлении в viewport === function onVisible(el, fn, opts = {}) { if (!('IntersectionObserver' in window) || !el) { fn(); return { disconnect: () => {} }; } const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { fn(); obs.disconnect(); } }); }, { threshold: opts.threshold || 0.2, rootMargin: opts.rootMargin || '0px' }); obs.observe(el); return obs; } // === Export === window.P8Anim = { ease, tween, raf, oscillate, after, animateCSS, stagger, onVisible, clamp, lerp, smoothstep }; })();