'use strict'; /* SimBase — опциональная база жизненного цикла симуляции (Фаза 0). * Унифицирует то, что сейчас каждый sim-класс изобретает заново: DPR-fit canvas * и RAF-петлю play/pause + reset/destroy. Подключение — постепенное (новые и * рефакторимые симуляции делают `extends SimBase`); существующие не трогаются, * legacy-«дробовик» _pauseAllSims/closeSim остаётся как fallback, пока все не * мигрируют. Подкласс реализует step(dt) и/или draw(). * * class MySim extends SimBase { * constructor(canvas){ super(canvas); ... } * step(dt){ ... } // физика (опц.) * draw(){ ... } // отрисовка * } */ (function (global) { function SimBase(canvas) { this.canvas = canvas; this.ctx = canvas ? canvas.getContext('2d') : null; this.dpr = 1; this.w = 0; this.h = 0; this._raf = 0; this._running = false; this._last = 0; } SimBase.prototype.fit = function () { var c = this.canvas; if (!c) return; var dpr = Math.min(global.devicePixelRatio || 1, 2); var r = (c.parentElement || c).getBoundingClientRect(); var w = Math.max(1, Math.round(r.width)); var h = Math.max(1, Math.round(r.height)); this.dpr = dpr; this.w = w; this.h = h; c.width = w * dpr; c.height = h * dpr; c.style.width = w + 'px'; c.style.height = h + 'px'; if (this.ctx) this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); if (typeof this.onResize === 'function') this.onResize(w, h); }; SimBase.prototype.play = function () { if (this._running) return; this._running = true; this._last = (global.performance || Date).now(); var self = this; function loop(t) { if (!self._running) return; var dt = Math.min((t - self._last) / 1000, 0.05); // кламп против скачков self._last = t; if (typeof self.step === 'function') self.step(dt); if (typeof self.draw === 'function') self.draw(); self._raf = global.requestAnimationFrame(loop); } self._raf = global.requestAnimationFrame(loop); }; SimBase.prototype.pause = function () { this._running = false; if (this._raf) { global.cancelAnimationFrame(this._raf); this._raf = 0; } }; SimBase.prototype.isRunning = function () { return this._running; }; SimBase.prototype.reset = function () { if (typeof this.draw === 'function') this.draw(); }; // Полная остановка + снятие ресурсов. Подкласс переопределяет для удаления // слушателей/ResizeObserver, затем вызывает super.destroy(). SimBase.prototype.destroy = function () { this.pause(); }; global.SimBase = SimBase; })(window);