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

141 lines
7.1 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'
}
]
};
function tryRegister() {
if (!demoEnabled()) return;
if (typeof global.registerSpecSim !== 'function') {
if (global.console) console.warn('[sim-demo] registerSpecSim недоступен');
return;
}
global.registerSpecSim(PROJECTILE_DEMO);
// Если каталог уже отрисован, добавить карточку демо вручную (минимально-
// инвазивно: только когда флаг включён; не трогаем SIMS/каталожный рендер).
addDemoCardIfNeeded();
}
function addDemoCardIfNeeded() {
var grid = document.getElementById('sim-grid');
if (!grid) return;
if (document.getElementById('sim-card-customdemo')) return;
var m = global.LabRegistry && global.LabRegistry.get('customdemo');
if (!m) return;
var preview = global.LabRegistry.resolvePreview(m);
var card = document.createElement('div');
card.id = 'sim-card-customdemo';
card.className = 'sim-card';
card.setAttribute('onclick', "openSim('customdemo')");
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);