refactor(labs): Фаза0 фундамент — убрать мёртвый SimUtil, добавить LabPalette + SimBase
- Удалён _util.js (SimUtil): 0 использований во всех симуляциях (проверено), грузился впустую. - LabPalette (_palette.js): единый источник цветов canvas + PX_PER_M вместо хардкода в каждом файле; задел под светлую тему. - SimBase (_simbase.js): опциональная база жизненного цикла (DPR-fit + RAF play/pause/reset/destroy). Существующие симуляции не трогаются; «дробовик» остаётся fallback. Адаптация — постепенно, по мере правок (нет фронт-тестов). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
/* LabPalette — единый источник цветов и констант для canvas-симуляций (Фаза 0).
|
||||
* В 2D-контексте нельзя использовать var(--…) из ls.css, поэтому палитра
|
||||
* дублировалась хардкодом в каждом файле. Теперь — одна точка правды.
|
||||
* Адаптация симуляций — постепенная (по мере правок), задел под светлую тему. */
|
||||
(function (global) {
|
||||
global.LabPalette = global.LabPalette || {
|
||||
bg: '#0D0D1A', // фон сцены
|
||||
panel: 'rgba(255,255,255,0.04)', // лёгкие плашки
|
||||
|
||||
violet: '#9B5DE5', // бренд (= --violet)
|
||||
cyan: '#06D6E0', // = --cyan
|
||||
green: '#7BF5A4',
|
||||
red: '#EF476F',
|
||||
amber: '#FFD166',
|
||||
pink: '#F15BB5', // = --pink
|
||||
|
||||
text: '#FFFFFF',
|
||||
text2: 'rgba(255,255,255,0.72)',
|
||||
muted: 'rgba(255,255,255,0.55)',
|
||||
faint: 'rgba(255,255,255,0.30)',
|
||||
grid: 'rgba(255,255,255,0.08)',
|
||||
axis: 'rgba(255,255,255,0.45)',
|
||||
|
||||
// Физическая шкала по умолчанию: 1 м = 100 px (вынесено из магического *100).
|
||||
PX_PER_M: 100,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,74 @@
|
||||
'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);
|
||||
@@ -1,191 +0,0 @@
|
||||
'use strict';
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
SimUtil — shared utility functions for lab simulations.
|
||||
Loaded once before all sim scripts.
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
const SimUtil = (() => {
|
||||
|
||||
/**
|
||||
* DPR-aware canvas resize. Sets canvas pixel size and applies
|
||||
* `setTransform` so subsequent drawing is in CSS-pixel coords.
|
||||
* Returns { W, H, dpr }.
|
||||
*/
|
||||
function fitCanvas(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.offsetWidth || canvas.parentElement?.offsetWidth || 600;
|
||||
const h = canvas.offsetHeight || canvas.parentElement?.offsetHeight || 400;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
return { W: w, H: h, dpr };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a "nice" grid step for the given pixel range and target ~n divisions.
|
||||
*/
|
||||
function niceStep(rangePx, scale, n) {
|
||||
if (!n) n = 8;
|
||||
const raw = rangePx / scale / n;
|
||||
const p = Math.pow(10, Math.floor(Math.log10(raw)));
|
||||
for (const m of [1, 2, 5, 10]) if (m * p >= raw) return m * p;
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number for grid/axis labels.
|
||||
*/
|
||||
function fmt(n, step) {
|
||||
if (n === 0) return '0';
|
||||
if (step >= 1 && Number.isInteger(n)) return String(n);
|
||||
if (step < 0.001) return n.toExponential(1);
|
||||
const dec = Math.max(0, -Math.floor(Math.log10(step)));
|
||||
return n.toFixed(dec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an arrow from (x1,y1) to (x2,y2).
|
||||
*/
|
||||
function arrow(ctx, x1, y1, x2, y2, color, lw) {
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len < 1) return;
|
||||
const ux = dx / len, uy = dy / len;
|
||||
const hs = Math.min(10, len * 0.3); // head size
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = color || '#fff';
|
||||
ctx.fillStyle = color || '#fff';
|
||||
ctx.lineWidth = lw || 2;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
// shaft
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2 - ux * hs * 0.5, y2 - uy * hs * 0.5);
|
||||
ctx.stroke();
|
||||
|
||||
// head
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(x2 - ux * hs - uy * hs * 0.4, y2 - uy * hs + ux * hs * 0.4);
|
||||
ctx.lineTo(x2 - ux * hs + uy * hs * 0.4, y2 - uy * hs - ux * hs * 0.4);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a coordinate grid with labels.
|
||||
* @param {object} opts — { ox, oy, scl, font, gridColor, labelColor }
|
||||
*/
|
||||
function drawGrid(ctx, W, H, opts) {
|
||||
const { ox = 0, oy = 0, scl = 50, font, gridColor, labelColor } = opts || {};
|
||||
|
||||
const step = niceStep(W, scl);
|
||||
|
||||
// math <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> pixel helpers
|
||||
const toPx = (mx, my) => [W / 2 + (mx - ox) * scl, H / 2 - (my - oy) * scl];
|
||||
const toMx = (px) => (px - W / 2) / scl + ox;
|
||||
const toMy = (py) => -(py - H / 2) / scl + oy;
|
||||
|
||||
const x0 = toMx(0), x1 = toMx(W);
|
||||
const y0 = toMy(H), y1 = toMy(0);
|
||||
const gx = Math.floor(x0 / step) * step;
|
||||
const gy = Math.floor(y0 / step) * step;
|
||||
|
||||
// grid lines
|
||||
ctx.strokeStyle = gridColor || 'rgba(255,255,255,0.065)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = gx; x <= x1 + step; x += step) {
|
||||
const [px] = toPx(x, 0);
|
||||
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();
|
||||
}
|
||||
for (let y = gy; y <= y1 + step; y += step) {
|
||||
const [, py] = toPx(0, y);
|
||||
ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke();
|
||||
}
|
||||
|
||||
// labels
|
||||
ctx.font = font || '11px Manrope, system-ui, sans-serif';
|
||||
ctx.fillStyle = labelColor || 'rgba(255,255,255,0.3)';
|
||||
const [axX, axY] = toPx(0, 0);
|
||||
const lblY = Math.max(4, Math.min(H - 18, axY + 5));
|
||||
const lblX = Math.max(28, Math.min(W - 6, axX - 5));
|
||||
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
for (let x = gx; x <= x1; x += step) {
|
||||
if (Math.abs(x) < step * 0.01) continue;
|
||||
const [px] = toPx(x, 0);
|
||||
if (px < 18 || px > W - 18) continue;
|
||||
ctx.fillText(fmt(x, step), px, lblY);
|
||||
}
|
||||
ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
|
||||
for (let y = gy; y <= y1; y += step) {
|
||||
if (Math.abs(y) < step * 0.01) continue;
|
||||
const [, py] = toPx(0, y);
|
||||
if (py < 12 || py > H - 12) continue;
|
||||
ctx.fillText(fmt(y, step), lblX, py);
|
||||
}
|
||||
|
||||
// axes
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(0, axY); ctx.lineTo(W - 10, axY); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(axX, H); ctx.lineTo(axX, 8); ctx.stroke();
|
||||
|
||||
// arrowheads
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
_arrowHead(ctx, W - 8, axY, 0);
|
||||
_arrowHead(ctx, axX, 6, -Math.PI / 2);
|
||||
|
||||
// axis labels
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.55)';
|
||||
ctx.font = 'bold 12px Manrope, sans-serif';
|
||||
ctx.textBaseline = 'middle'; ctx.textAlign = 'left';
|
||||
ctx.fillText('x', W - 10, axY - 13);
|
||||
ctx.textBaseline = 'top'; ctx.textAlign = 'left';
|
||||
ctx.fillText('y', axX + 7, 4);
|
||||
}
|
||||
|
||||
function _arrowHead(ctx, x, y, angle) {
|
||||
const s = 5;
|
||||
ctx.save(); ctx.translate(x, y); ctx.rotate(angle);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0); ctx.lineTo(-s * 1.6, -s * 0.6); ctx.lineTo(-s * 1.6, s * 0.6);
|
||||
ctx.closePath(); ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a tooltip panel at (x, y).
|
||||
* @param {string[]} rows — lines of text
|
||||
* @param {object} opts — { bg, fg, font, padding, radius }
|
||||
*/
|
||||
function tooltip(ctx, x, y, rows, opts) {
|
||||
const { bg = 'rgba(22,22,38,0.92)', fg = '#ddd', font = '12px Manrope, sans-serif',
|
||||
padding = 8, radius = 8 } = opts || {};
|
||||
ctx.save();
|
||||
ctx.font = font;
|
||||
let maxW = 0;
|
||||
for (const r of rows) maxW = Math.max(maxW, ctx.measureText(r).width);
|
||||
const w = maxW + padding * 2;
|
||||
const lineH = 17;
|
||||
const h = rows.length * lineH + padding * 2;
|
||||
const tx = x + 14, ty = y - h / 2;
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(tx, ty, w, h, radius);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = fg;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.textAlign = 'left';
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
ctx.fillText(rows[i], tx + padding, ty + padding + i * lineH);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
return { fitCanvas, niceStep, fmt, arrow, drawGrid, tooltip };
|
||||
})();
|
||||
+2
-1
@@ -425,6 +425,8 @@
|
||||
<script src="/js/labs/_registry.js"></script>
|
||||
<script src="/js/labs/_loader.js"></script>
|
||||
<script src="/js/labs/_sim_deps.js"></script>
|
||||
<script src="/js/labs/_palette.js"></script>
|
||||
<script src="/js/labs/_simbase.js"></script>
|
||||
<script src="/js/labs/_fx_core.js"></script>
|
||||
<script src="/js/labs/_fx_particles.js"></script>
|
||||
<script src="/js/labs/_fx_motion.js"></script>
|
||||
@@ -433,7 +435,6 @@
|
||||
<script src="/js/labs/_tasks.js"></script>
|
||||
<script src="/js/labs/_phys_visuals.js"></script>
|
||||
<script src="/js/labs/_chem_visuals.js"></script>
|
||||
<script src="/js/labs/_util.js"></script>
|
||||
<script src="/js/labs/graph.js"></script>
|
||||
<script src="/js/notifications.js"></script>
|
||||
<script src="/js/search.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user