fix(labs): таблица Менделеева + UX качественных реакций + анимации стехиометрии
- Менделеев: clamp() для font-size символа элемента (2.4rem..4.4rem) + padding-top 28px → символ не обрезается на узких панелях - Качественные реакции: в Свободно/Тренировке Проб1-4 содержат известные ионы (видна подпись), в Тренировке Образец — отдельный неизвестный; в Экзамене можно переключаться между пробирками и ответить отдельно для каждой (verdict сохраняется) - Стехиометрия: непрерывный анимационный цикл — волна на поверхности жидкости, пузырьки в газах/растворах, пульсирующая красная рамка + ЛИМИТ-лейбл у лимитирующего реагента, искры вдоль стрелки реакции, glow на стрелке во время реакции
This commit is contained in:
@@ -949,6 +949,25 @@ class QualAnalysisSim {
|
||||
const idx = this._hitTestTube(x, y);
|
||||
if (idx >= 0) {
|
||||
this._activeTube = idx;
|
||||
/* in exam: switching tube allows re-submit; verdict resets */
|
||||
if (this._mode === 'exam') {
|
||||
const alreadyAnswered = this._examAnswered && this._examAnswered[idx];
|
||||
this._answered = !!alreadyAnswered;
|
||||
const verdict = document.getElementById('qa-verdict');
|
||||
if (verdict) {
|
||||
if (alreadyAnswered) {
|
||||
verdict.style.display = 'block';
|
||||
verdict.textContent = alreadyAnswered.text;
|
||||
verdict.style.background = alreadyAnswered.bg;
|
||||
verdict.style.color = alreadyAnswered.fg;
|
||||
verdict.style.border = alreadyAnswered.border;
|
||||
} else {
|
||||
verdict.style.display = 'none';
|
||||
verdict.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
this._updateTaskText();
|
||||
this._drawScene();
|
||||
}
|
||||
});
|
||||
@@ -979,6 +998,7 @@ class QualAnalysisSim {
|
||||
_startMode(mode) {
|
||||
this._mode = mode;
|
||||
this._answered = false;
|
||||
this._examAnswered = {}; // per-tube answered tracking for exam
|
||||
this._log = [];
|
||||
this._dragReagent = null;
|
||||
this._pendingReagent = null;
|
||||
@@ -991,27 +1011,44 @@ class QualAnalysisSim {
|
||||
|
||||
const ions = QualAnalysisSim.IONS;
|
||||
|
||||
/* pick random ions */
|
||||
/* pick known ions for helper tubes (visible labels) */
|
||||
this._helperIons = [];
|
||||
const shuffled = ions.slice().sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < this._tubeCount; i++) {
|
||||
this._helperIons.push(shuffled[i % shuffled.length]);
|
||||
}
|
||||
|
||||
/* pick random ions per mode */
|
||||
if (mode === 'train') {
|
||||
this._targetIon = ions[Math.floor(Math.random() * ions.length)];
|
||||
/* pick target distinct from helpers if possible */
|
||||
const helperIds = new Set(this._helperIons.map(h => h.id));
|
||||
const pool = ions.filter(i => !helperIds.has(i.id));
|
||||
const src = pool.length > 0 ? pool : ions;
|
||||
this._targetIon = src[Math.floor(Math.random() * src.length)];
|
||||
this._examIons = [];
|
||||
} else if (mode === 'exam') {
|
||||
this._examIons = [];
|
||||
const shuffled = ions.slice().sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < this._tubeCount; i++) {
|
||||
this._examIons.push(shuffled[i % shuffled.length]);
|
||||
}
|
||||
/* exam: helper tubes hold unknown ions, no sample */
|
||||
this._examIons = this._helperIons.slice();
|
||||
this._helperIons = []; // hide helper labels in exam (they're unknown)
|
||||
this._targetIon = null;
|
||||
} else {
|
||||
/* free mode: helpers have known ions, no sample */
|
||||
this._targetIon = null;
|
||||
this._examIons = [];
|
||||
}
|
||||
|
||||
/* reset tubes */
|
||||
this._resetTubes();
|
||||
/* paint helper tubes with their ion colors (free + train modes) */
|
||||
if (mode !== 'exam') {
|
||||
for (let i = 0; i < this._tubeCount; i++) {
|
||||
if (this._helperIons[i]) {
|
||||
this._tubes[i].solColor = this._helperIons[i].solColor || 'rgba(100,180,255,0.15)';
|
||||
}
|
||||
}
|
||||
}
|
||||
/* set solution color for sample tube in train mode */
|
||||
if (mode === 'train' && this._targetIon) {
|
||||
/* sample = index tubeCount */
|
||||
this._tubes[this._tubeCount].solColor = this._targetIon.solColor || 'rgba(100,180,255,0.15)';
|
||||
}
|
||||
/* set colors for exam tubes */
|
||||
@@ -1051,11 +1088,12 @@ class QualAnalysisSim {
|
||||
const el = document.getElementById('qa-task');
|
||||
if (!el) return;
|
||||
if (this._mode === 'free') {
|
||||
el.textContent = 'Свободный режим — добавляй реагенты в пробирки и наблюдай реакции';
|
||||
el.textContent = 'Свободно: в каждой пробирке известный ион (см. подпись) — пробуй реагенты, изучай реакции';
|
||||
} else if (this._mode === 'train') {
|
||||
el.textContent = 'Тренировка: определи неизвестный ион в Образце';
|
||||
el.textContent = 'Тренировка: определи ион в Образце (золотая рамка). В Проб1–4 — известные ионы для сравнения';
|
||||
} else {
|
||||
el.textContent = 'Экзамен: определи неизвестный ион в каждой пробирке (выбери пробирку, затем дай ответ)';
|
||||
const ansFor = 'Проб' + (this._activeTube + 1);
|
||||
el.textContent = 'Экзамен: в каждой пробирке свой неизвестный ион. Кликни на пробирку → определи реакциями → ответь. Сейчас отвечаешь для: ' + ansFor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,12 +1118,7 @@ class QualAnalysisSim {
|
||||
_applyReagent(tubeIdx, reagentId) {
|
||||
/* which ion is in this tube? */
|
||||
const isSample = tubeIdx === this._tubeCount;
|
||||
let ion = null;
|
||||
if (this._mode === 'train' && isSample) {
|
||||
ion = this._targetIon;
|
||||
} else if (this._mode === 'exam' && !isSample) {
|
||||
ion = this._examIons[tubeIdx] || null;
|
||||
}
|
||||
let ion = this._getIonForTube(tubeIdx);
|
||||
/* if no assigned ion → free tube with blank reactions */
|
||||
/* still animate the drop but log "нет ионов" */
|
||||
if (!ion) {
|
||||
@@ -1326,19 +1359,30 @@ class QualAnalysisSim {
|
||||
const totalEl = document.getElementById('qa-score-total');
|
||||
if (totalEl) totalEl.textContent = '/' + this._scoreTotal;
|
||||
|
||||
let vText, vBg, vFg, vBorder;
|
||||
if (correct) {
|
||||
verdict.textContent = 'Верно! Это ' + (correctIon ? correctIon.label : chosen);
|
||||
verdict.style.cssText = verdict.style.cssText.replace(/display:[^;]+/, 'display:block');
|
||||
verdict.style.background = 'rgba(94,240,142,0.15)';
|
||||
verdict.style.color = '#5EF08E';
|
||||
verdict.style.border = '1px solid rgba(94,240,142,0.35)';
|
||||
vText = 'Верно! Это ' + (correctIon ? correctIon.label : chosen);
|
||||
vBg = 'rgba(94,240,142,0.15)';
|
||||
vFg = '#5EF08E';
|
||||
vBorder = '1px solid rgba(94,240,142,0.35)';
|
||||
if (window.LabFX) LabFX.sound.play('chime');
|
||||
} else {
|
||||
const label = correctIon ? correctIon.label : '?';
|
||||
verdict.textContent = 'Неверно — это ' + label;
|
||||
verdict.style.background = 'rgba(239,71,111,0.12)';
|
||||
verdict.style.color = '#EF476F';
|
||||
verdict.style.border = '1px solid rgba(239,71,111,0.35)';
|
||||
vText = 'Неверно — это ' + label;
|
||||
vBg = 'rgba(239,71,111,0.12)';
|
||||
vFg = '#EF476F';
|
||||
vBorder = '1px solid rgba(239,71,111,0.35)';
|
||||
}
|
||||
verdict.textContent = vText;
|
||||
verdict.style.background = vBg;
|
||||
verdict.style.color = vFg;
|
||||
verdict.style.border = vBorder;
|
||||
verdict.style.display = 'block';
|
||||
|
||||
/* exam: remember verdict per tube; allow switching to next */
|
||||
if (this._mode === 'exam') {
|
||||
this._examAnswered = this._examAnswered || {};
|
||||
this._examAnswered[this._activeTube] = { text: vText, bg: vBg, fg: vFg, border: vBorder, correct };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1663,12 +1707,20 @@ class QualAnalysisSim {
|
||||
ctx.fillText('(?)', tx + tubeW / 2, labelY + 14);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = isActive ? '#4CC9F0' : 'rgba(255,255,255,0.65)';
|
||||
ctx.fillStyle = isActive ? '#4CC9F0' : 'rgba(255,255,255,0.78)';
|
||||
ctx.fillText('Проб' + (i + 1), tx + tubeW / 2, labelY);
|
||||
if (this._mode === 'exam') {
|
||||
ctx.font = '600 10px Manrope,sans-serif';
|
||||
ctx.fillStyle = isActive ? 'rgba(76,201,240,0.65)' : 'rgba(255,255,255,0.35)';
|
||||
ctx.fillStyle = isActive ? 'rgba(76,201,240,0.85)' : 'rgba(255,255,255,0.45)';
|
||||
ctx.fillText('(?)', tx + tubeW / 2, labelY + 14);
|
||||
} else {
|
||||
/* show known ion label in free / train modes */
|
||||
const knownIon = (this._helperIons || [])[i];
|
||||
if (knownIon) {
|
||||
ctx.font = '700 11px Manrope,sans-serif';
|
||||
ctx.fillStyle = isActive ? '#4CC9F0' : 'rgba(255,255,255,0.6)';
|
||||
ctx.fillText(knownIon.label, tx + tubeW / 2, labelY + 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
@@ -1678,6 +1730,7 @@ class QualAnalysisSim {
|
||||
const isSample = tubeIdx === this._tubeCount;
|
||||
if (this._mode === 'train' && isSample) return this._targetIon;
|
||||
if (this._mode === 'exam' && !isSample) return this._examIons[tubeIdx] || null;
|
||||
if (this._mode !== 'exam' && !isSample) return (this._helperIons || [])[tubeIdx] || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user