// 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
  };
})();
