Files
Learn_System/frontend/js/labs/_sim_demo.js
T

219 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* ════════════════════════════════════════════════════════════════════════
_sim_demo — рукописная демо-спека «Бросок тела» (projectile) для проверки
рантайма Фазы 0. Регистрируется как id 'customdemo' ТОЛЬКО за флагом — не
светится ученикам в каталоге (карточка не добавляется в SIMS).
Включение для проверки (любой из вариантов):
- URL: /lab?simdemo=1 (или ?sim=customdemo прямой deep-link)
- глоб: window.LAB_SHOW_SPEC_DEMO = true (до загрузки labs-скриптов)
- localStorage.setItem('lab-spec-demo','1')
Проверка: открыть /lab?simdemo=1 -> карточка появится; либо открыть
/lab?sim=customdemo напрямую. Слайдеры угла/скорости меняют траекторию,
play/pause/reset работают. Это ВРЕМЕННЫЙ раздел (удалить после Фазы 4).
════════════════════════════════════════════════════════════════════════ */
(function (global) {
function demoEnabled() {
try {
var qs = (global.location && global.location.search) || '';
if (/[?&]simdemo=1\b/.test(qs)) return true;
if (/[?&]sim=customdemo\b/.test(qs)) return true;
if (global.LAB_SHOW_SPEC_DEMO === true) return true;
if (global.localStorage && global.localStorage.getItem('lab-spec-demo') === '1') return true;
} catch (e) { /* noop */ }
return false;
}
// Спека v1+ (Фаза 1): бросок тела. g фиксирован 10 -> y = y0 + v*sin(θ)*t - 5*t^2.
// Старт (x0,y0) — перетаскиваемая ручка (drag). plot — статическая параболическая
// траектория y(x); readout — дальность и макс. высота. Вектор v0 — origin+dx/dy.
var PROJECTILE_DEMO = {
id: 'customdemo',
cat: 'phys',
meta: { title: 'Демо: бросок тела', desc: 'Спек-симуляция (Фаза 1): слайдеры, drag-старт, график, readout.' },
viewport: { xmin: 0, xmax: 60, ymin: 0, ymax: 30, grid: true, axes: true, bg: '#0D0D1A' },
time: { autoplay: false, loop: true, duration: 8, speed: 1 },
params: [
{ name: 'theta', label: 'Угол θ', min: 0, max: 90, step: 1, value: 45, unit: '°' },
{ name: 'v', label: 'Скорость v', min: 0, max: 30, step: 0.5, value: 20, unit: 'м/с' },
{ name: 'x0', label: 'Старт X', min: 0, max: 20, step: 0.5, value: 2, unit: 'м' },
{ name: 'y0', label: 'Старт Y', min: 0, max: 25, step: 0.5, value: 0, unit: 'м' }
],
objects: [
// снаряд: x = x0 + v*cos(θ)*t, y = y0 + v*sin(θ)*t - 5 t^2 (но не ниже 0)
{
id: 'ball', type: 'point',
x: 'x0 + v*cos(theta*pi/180)*t',
y: 'max(0, y0 + v*sin(theta*pi/180)*t - 5*t^2)',
r: 7, color: '#06D6E0', trail: true, trailColor: '#9B5DE5'
},
// график траектории y(x): парабола броска, var=x на [x0, x0+дальность].
// y(x) = y0 + tan(θ)(x-x0) - g(x-x0)^2/(2 v^2 cos^2θ), g=10.
{
type: 'plot', color: '#FFD166', width: 1.6,
var: 'x', range: ['x0', 'x0 + v*v*sin(2*theta*pi/180)/10 + 0.001'],
samples: 200,
expr: 'max(0, y0 + tan(theta*pi/180)*(x-x0) - 10*(x-x0)^2/(2*v*v*cos(theta*pi/180)^2))'
},
// перетаскиваемая ручка старта (drag по обеим осям -> x0/y0)
{
id: 'start', type: 'point',
x: 'x0', y: 'y0', r: 8, color: '#EF476F',
drag: { axis: 'xy', param: 'x0', paramY: 'y0', min: 0, max: 25 }
},
// вектор начальной скорости из старта (origin + dx/dy)
{
type: 'vector',
origin: ['x0', 'y0'],
dx: 'cos(theta*pi/180)*v*0.4',
dy: 'sin(theta*pi/180)*v*0.4',
color: '#FFD166', width: 3
},
// земля
{ type: 'segment', x1: 0, y1: 0, x2: 60, y2: 0, color: 'rgba(255,255,255,0.35)', width: 2 },
// подпись над снарядом
{
type: 'label', latex: true,
x: 'ball.x', y: 'ball.y + 2.5',
text: 'v_0', color: '#06D6E0', size: 15
},
// readout: дальность полёта R = v^2 sin(2θ)/g (для y0=0) и макс. высота H
{
type: 'readout', label: 'R', unit: 'м', precision: 1, color: '#FFD166',
expr: 'x0 + v*v*sin(2*theta*pi/180)/10'
},
{
type: 'readout', label: 'H', unit: 'м', precision: 1, color: '#06D6E0',
expr: 'y0 + (v*sin(theta*pi/180))^2/20'
}
]
};
/* ── Фаза 2: физ-демо за флагом ── */
// Маятник на пружине: груз (тело) подвешен пружиной к неподвижному якорю.
// Гравитация тянет вниз, пружина возвращает -> колебания. Груз можно тащить.
var PENDULUM_DEMO = {
id: 'customphys',
cat: 'phys',
meta: { title: 'Демо: пружинный маятник', desc: 'Спек-физика (Фаза 2): тело + пружина + гравитация, drag тела.' },
viewport: { xmin: -6, xmax: 6, ymin: -10, ymax: 2, grid: true, axes: true, bg: '#0D0D1A' },
time: { autoplay: true, loop: false, speed: 1 },
params: [
{ name: 'k', label: 'Жёсткость k', min: 5, max: 120, step: 1, value: 40, unit: 'Н/м' },
{ name: 'm', label: 'Масса m', min: 0.5, max: 5, step: 0.1, value: 1, unit: 'кг' },
{ name: 'L', label: 'Длина L', min: 2, max: 8, step: 0.1, value: 5, unit: 'м' }
],
physics: {
enabled: true,
gravity: { x: 0, y: -9.8 },
friction: 0.15,
restitution: 0.7,
walls: [{ side: 'bottom' }],
springs: [{ a: [0, 0], b: 'bob', k: 'k', length: 'L', damping: 0.4 }]
},
objects: [
// якорь подвеса
{ type: 'circle', x: 0, y: 0, r: 0.18, color: '#FFD166', fill: '#FFD166' },
// груз — физическое тело, стартует сбоку (выведено из равновесия), след включён
{
id: 'bob', type: 'circle', r: 0.6, color: '#06D6E0', fill: 'rgba(6,214,224,0.25)',
x: '3', y: '-4', trail: true, trailColor: '#9B5DE5',
body: { mass: 'm', vx: 0, vy: 0 }
},
{ type: 'label', latex: true, x: 'bob.x', y: 'bob.y - 1.1', text: 'm', color: '#06D6E0', size: 14 },
// живые показания скорости
{ type: 'readout', label: 'v_y', unit: 'м/с', precision: 2, color: '#FFD166', expr: 'bob.vy' },
{ type: 'readout', label: 'y', unit: 'м', precision: 2, color: '#06D6E0', expr: 'bob.y' }
]
};
// Note: масса груза задаётся выражением 'm' (param). При reset тело пересобирается
// с актуальной массой/нач.условиями; пружина length='L', k='k' пересчитываются тоже.
// Упругие шары: 3 тела в коробке из стен, разные начальные скорости, упругие
// столкновения друг с другом и со стенами. Гравитация мягкая.
var BALLS_DEMO = {
id: 'customballs',
cat: 'phys',
meta: { title: 'Демо: упругие шары', desc: 'Спек-физика (Фаза 2): столкновения круг-круг и круг-стена.' },
viewport: { xmin: 0, xmax: 12, ymin: 0, ymax: 9, grid: true, axes: false, bg: '#0D0D1A' },
time: { autoplay: true, loop: false, speed: 1 },
params: [
{ name: 'g', label: 'Гравитация', min: 0, max: 12, step: 0.5, value: 4, unit: 'м/с²' },
// NB: имя 'e' зарезервировано (число Эйлера в SimExpr) — используем 'el' для упругости.
{ name: 'el', label: 'Упругость', min: 0.5, max: 1, step: 0.02, value: 0.96 }
],
physics: {
enabled: true,
gravity: { x: 0, y: '-g' }, // gravity.y — выражение от param g (вычисляется на reset)
friction: 0,
restitution: 'el', // упругость от param el
walls: [{ side: 'bottom' }, { side: 'top' }, { side: 'left' }, { side: 'right' }]
},
objects: [
{ id: 'b1', type: 'circle', r: 0.7, color: '#06D6E0', fill: 'rgba(6,214,224,0.3)',
x: 2, y: 4.5, body: { mass: 1, vx: 6, vy: 2.4 }, trail: true, trailColor: '#06D6E0' },
{ id: 'b2', type: 'circle', r: 1.0, color: '#9B5DE5', fill: 'rgba(155,93,229,0.3)',
x: 8, y: 6, body: { mass: 2, vx: -4, vy: -3 }, trail: true, trailColor: '#9B5DE5' },
{ id: 'b3', type: 'circle', r: 0.5, color: '#FFD166', fill: 'rgba(255,209,102,0.3)',
x: 6, y: 2, body: { mass: 0.6, vx: 3, vy: 5 }, trail: true, trailColor: '#FFD166' },
{ type: 'readout', label: 'b2.vx', precision: 2, color: '#9B5DE5', expr: 'b2.vx' }
]
};
var DEMOS = [PROJECTILE_DEMO, PENDULUM_DEMO, BALLS_DEMO];
function tryRegister() {
if (!demoEnabled()) return;
if (typeof global.registerSpecSim !== 'function') {
if (global.console) console.warn('[sim-demo] registerSpecSim недоступен');
return;
}
for (var i = 0; i < DEMOS.length; i++) global.registerSpecSim(DEMOS[i]);
// Если каталог уже отрисован, добавить карточки демо вручную (минимально-
// инвазивно: только когда флаг включён; не трогаем SIMS/каталожный рендер).
addDemoCardsIfNeeded();
}
function addDemoCardsIfNeeded() {
var grid = document.getElementById('sim-grid');
if (!grid) return;
for (var i = 0; i < DEMOS.length; i++) {
var id = DEMOS[i].id;
if (document.getElementById('sim-card-' + id)) continue;
var m = global.LabRegistry && global.LabRegistry.get(id);
if (!m) continue;
var preview = global.LabRegistry.resolvePreview(m);
var card = document.createElement('div');
card.id = 'sim-card-' + id;
card.className = 'sim-card';
card.setAttribute('onclick', "openSim('" + id + "')");
card.innerHTML = preview +
'<div class="sim-body">' +
'<span class="sim-cat ' + (m.cat || 'phys') + '">демо</span>' +
'<div class="sim-title">' + esc(m.title) + '</div>' +
'<div class="sim-desc">' + esc(m.desc || '') + '</div>' +
'</div>';
grid.appendChild(card);
}
}
function esc(s) {
return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Зарегистрировать после готовности DOM/реестра. _register-all.js грузится
// последним (defer); этот файл — после него, поэтому LabRegistry уже есть.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryRegister);
} else {
tryRegister();
}
// экспонируем для ручной проверки из консоли
global.LAB_SPEC_DEMO = PROJECTILE_DEMO;
})(typeof window !== 'undefined' ? window : globalThis);