f7d27ecb91
§4 Относительная атомная масса (весы атомов: во сколько раз тяжелее), §5 Молекулы и простые вещества (галерея молекул O2/O3/H2/N2 шариками), §6 Сложные вещества (классификатор простое/сложное + галерея H2O/CO2/CH4/NH3). Теория, тренажёры задач. Тест: 8/8 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
244 lines
16 KiB
JavaScript
244 lines
16 KiB
JavaScript
/* chem7_ch1_widgets.js — интерактивы главы 1 «Первоначальные химические понятия» (Химия 7).
|
||
* Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id] / window.FLAG_MOUNTS[id].
|
||
* Используют window.Chem8 (chem8_svg.js): molarMass, elementCounts, arOf, fmt, equationBalancer.
|
||
* Без эмоджи; KaTeX-рендер — через window.chem8RenderMath.
|
||
*/
|
||
(function (W) {
|
||
'use strict';
|
||
function C() { return W.Chem8 || {}; }
|
||
function $(id) { return document.getElementById(id); }
|
||
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
/* ── общий мини-классификатор: чипы → 2 корзины ──────────────────── */
|
||
function classifier(mount, opts) {
|
||
if (!mount || mount._built) return; mount._built = 1;
|
||
var items = opts.items.slice(); // {t, b} t=подпись, b=индекс корзины
|
||
var buckets = opts.buckets; // [name0, name1]
|
||
var placed = {}; // idx -> bucket
|
||
var pool = items.map(function (it, i) { return i; });
|
||
function colorOf(ok){ return ok ? '#059669' : '#dc2626'; }
|
||
function render() {
|
||
var chips = pool.map(function (i) {
|
||
return '<button class="c7-chip" data-i="' + i + '" style="padding:7px 12px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text);font-weight:600;font-size:.9rem;cursor:pointer;margin:3px">' + esc(items[i].t) + '</button>';
|
||
}).join('') || '<span style="color:var(--muted)">Все карточки распределены.</span>';
|
||
var cols = buckets.map(function (name, bi) {
|
||
var inb = Object.keys(placed).filter(function (k) { return placed[k] === bi; });
|
||
var cells = inb.map(function (k) {
|
||
var ok = items[k].b === bi;
|
||
return '<div style="padding:5px 10px;margin:3px 0;border-radius:8px;font-size:.86rem;font-weight:600;color:#fff;background:' + colorOf(ok) + '">' + esc(items[k].t) + (ok ? ' ✓' : ' ✗') + '</div>';
|
||
}).join('') || '<div style="color:var(--muted);font-size:.82rem">перетащите сюда…</div>';
|
||
return '<div class="c7-bucket" data-b="' + bi + '" style="flex:1;min-width:140px;border:1.5px dashed var(--border);border-radius:11px;padding:10px;background:var(--pri-soft)"><div style="font-weight:700;margin-bottom:6px">' + esc(name) + '</div>' + cells + '</div>';
|
||
}).join('');
|
||
mount.innerHTML = '<div style="margin-bottom:10px">' + chips + '</div><div style="display:flex;gap:10px;flex-wrap:wrap">' + cols + '</div>'
|
||
+ '<div style="margin-top:8px;font-size:.84rem;color:var(--muted)">Кликни карточку, затем — корзину. Зелёный — верно, красный — ошибка.</div>';
|
||
bind();
|
||
}
|
||
var sel = null;
|
||
function bind() {
|
||
mount.querySelectorAll('.c7-chip').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
mount.querySelectorAll('.c7-chip').forEach(function (x) { x.style.outline = ''; });
|
||
sel = +b.dataset.i; b.style.outline = '2px solid var(--pri)';
|
||
});
|
||
});
|
||
mount.querySelectorAll('.c7-bucket').forEach(function (col) {
|
||
col.addEventListener('click', function () {
|
||
if (sel == null) return;
|
||
placed[sel] = +col.dataset.b;
|
||
pool = pool.filter(function (x) { return x !== sel; });
|
||
sel = null; render();
|
||
});
|
||
});
|
||
}
|
||
render();
|
||
}
|
||
|
||
/* §1 — классификатор «тело / вещество» */
|
||
function mount_p1() {
|
||
var m = $('p1-cls'); if (!m) return;
|
||
classifier(m, {
|
||
buckets: ['Физическое тело', 'Вещество'],
|
||
items: [
|
||
{ t: 'стакан', b: 0 }, { t: 'вода', b: 1 }, { t: 'гвоздь', b: 0 }, { t: 'железо', b: 1 },
|
||
{ t: 'ложка', b: 0 }, { t: 'сахар', b: 1 }, { t: 'медь', b: 1 }, { t: 'линейка', b: 0 }
|
||
]
|
||
});
|
||
}
|
||
|
||
/* §2 / ПР1 — разделитель смесей: выбери метод для смеси */
|
||
var MIX = [
|
||
{ mix: 'Песок и вода', method: 'Фильтрование', why: 'Песок не растворяется — задерживается фильтром, вода проходит.' },
|
||
{ mix: 'Соль и вода', method: 'Выпаривание', why: 'Вода испаряется, соль остаётся на дне.' },
|
||
{ mix: 'Железные опилки и сера', method: 'Магнит', why: 'Железо притягивается магнитом, сера — нет.' },
|
||
{ mix: 'Вода и растительное масло', method: 'Отстаивание (делительная воронка)', why: 'Масло легче воды и не смешивается — слои разделяют.' },
|
||
{ mix: 'Спирт и вода', method: 'Перегонка (дистилляция)', why: 'У спирта и воды разные температуры кипения.' }
|
||
];
|
||
var METHODS = ['Фильтрование', 'Выпаривание', 'Магнит', 'Отстаивание (делительная воронка)', 'Перегонка (дистилляция)'];
|
||
function mount_sep(mountId) {
|
||
var m = $(mountId); if (!m || m._built) return; m._built = 1;
|
||
var idx = 0;
|
||
function render() {
|
||
var cur = MIX[idx];
|
||
m.innerHTML = '<div class="fld"><label>Смесь</label><select id="' + mountId + '-pick">'
|
||
+ MIX.map(function (x, i) { return '<option value="' + i + '"' + (i === idx ? ' selected' : '') + '>' + esc(x.mix) + '</option>'; }).join('') + '</select></div>'
|
||
+ '<div style="margin:8px 0;font-weight:600">Каким способом разделить смесь «' + esc(cur.mix) + '»?</div>'
|
||
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + METHODS.map(function (mt) {
|
||
return '<button class="c7-m btn" data-m="' + esc(mt) + '">' + esc(mt) + '</button>';
|
||
}).join('') + '</div>'
|
||
+ '<div class="out" id="' + mountId + '-out" style="margin-top:8px">Выбери способ разделения.</div>';
|
||
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; m._built = 0; render(); });
|
||
var out = $(mountId + '-out');
|
||
m.querySelectorAll('.c7-m').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var ok = b.dataset.m === cur.method;
|
||
out.className = 'out ' + (ok ? 'ok' : 'bad');
|
||
out.innerHTML = ok
|
||
? '<b>Верно!</b> ' + esc(cur.method) + '. ' + esc(cur.why)
|
||
: '<b>Не подходит.</b> Подумай, чем различаются вещества в смеси (растворимость, магнитные свойства, температура кипения, плотность).';
|
||
});
|
||
});
|
||
}
|
||
render();
|
||
}
|
||
function mount_p2() { mount_sep('p2-sep'); }
|
||
function mount_pr1() { mount_sep('pr1-sep'); }
|
||
|
||
/* §3 — каталог элементов + тренажёр «символ ↔ название» */
|
||
var EL = {
|
||
H: [1, 'Водород'], He: [2, 'Гелий'], Li: [3, 'Литий'], C: [6, 'Углерод'], N: [7, 'Азот'],
|
||
O: [8, 'Кислород'], F: [9, 'Фтор'], Na: [11, 'Натрий'], Mg: [12, 'Магний'], Al: [13, 'Алюминий'],
|
||
Si: [14, 'Кремний'], P: [15, 'Фосфор'], S: [16, 'Сера'], Cl: [17, 'Хлор'], K: [19, 'Калий'],
|
||
Ca: [20, 'Кальций'], Fe: [26, 'Железо'], Cu: [29, 'Медь'], Zn: [30, 'Цинк'], Ag: [47, 'Серебро']
|
||
};
|
||
function mount_p3() {
|
||
var grid = $('p3-el'), info = $('p3-elinfo');
|
||
if (grid && !grid._built) {
|
||
grid._built = 1;
|
||
Object.keys(EL).forEach(function (s) {
|
||
var ar = C().arOf ? C().arOf(s) : '';
|
||
var c = document.createElement('div'); c.className = 'el-cell';
|
||
c.innerHTML = '<span class="z">' + EL[s][0] + '</span><span class="s">' + s + '</span><span class="a">' + ar + '</span>';
|
||
c.addEventListener('click', function () {
|
||
grid.querySelectorAll('.el-cell').forEach(function (x) { x.classList.remove('on'); }); c.classList.add('on');
|
||
if (info) info.innerHTML = '<b>' + EL[s][1] + '</b> (' + s + ') · порядковый номер Z = ' + EL[s][0] + ' · A_r = ' + ar;
|
||
});
|
||
grid.appendChild(c);
|
||
});
|
||
}
|
||
/* drill: дан символ → выбери название */
|
||
var d = $('p3-drill'); if (d && !d._built) {
|
||
d._built = 1;
|
||
var keys = Object.keys(EL), order = keys.slice(), qi = 0, score = 0, total = 0;
|
||
function nextQ() {
|
||
var sym = order[qi % order.length];
|
||
var correct = EL[sym][1];
|
||
var opts = [correct];
|
||
while (opts.length < 4) { var r = EL[keys[(qi * 7 + opts.length * 13 + 3) % keys.length]][1]; if (opts.indexOf(r) < 0) opts.push(r); }
|
||
// детерминированная перестановка
|
||
opts = opts.sort(function (a, b) { return ((a.length + qi) % 3) - ((b.length + qi) % 3); });
|
||
d.innerHTML = '<div style="font-weight:700;margin-bottom:6px">Какому элементу соответствует символ <span style="font-family:var(--mono);color:var(--pri-d);font-size:1.1rem">' + sym + '</span>?</div>'
|
||
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + opts.map(function (o) { return '<button class="c7-d btn" data-o="' + esc(o) + '">' + esc(o) + '</button>'; }).join('') + '</div>'
|
||
+ '<div class="out" id="p3-drill-out" style="margin-top:8px">Счёт: ' + score + ' из ' + total + '</div>';
|
||
var out = $('p3-drill-out');
|
||
d.querySelectorAll('.c7-d').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
total++; var ok = b.dataset.o === correct; if (ok) score++;
|
||
out.className = 'out ' + (ok ? 'ok' : 'bad');
|
||
out.innerHTML = (ok ? '<b>Верно!</b> ' : '<b>Нет.</b> ') + sym + ' — это ' + correct + '. Счёт: ' + score + ' из ' + total;
|
||
qi++;
|
||
setTimeout(nextQ, 850);
|
||
});
|
||
});
|
||
}
|
||
nextQ();
|
||
}
|
||
}
|
||
|
||
/* ── Волна 2 ── */
|
||
|
||
/* §4 — «весы атомов»: во сколько раз один атом тяжелее другого */
|
||
var ABEL = ['H','C','N','O','Na','Mg','Al','S','Cl','Ca','Fe','Cu','Zn','Ag'];
|
||
function mount_p4() {
|
||
var m = $('p4-bal'); if (!m || m._built) return; m._built = 1;
|
||
function opts(sel){ return ABEL.map(function(e){ var ar=C().arOf?C().arOf(e):''; return '<option value="'+e+'"'+(e===sel?' selected':'')+'>'+e+' (A_r='+ar+')</option>'; }).join(''); }
|
||
m.innerHTML = '<div class="fld"><label>Атом A</label><select id="p4-a">'+opts('S')+'</select>'
|
||
+'<label>Атом B</label><select id="p4-b">'+opts('O')+'</select></div><div class="out" id="p4-out"></div>';
|
||
function upd(){
|
||
var a=$('p4-a').value, b=$('p4-b').value, ara=+C().arOf(a), arb=+C().arOf(b);
|
||
var out=$('p4-out');
|
||
if(!ara||!arb){ out.textContent='—'; return; }
|
||
var big=Math.max(ara,arb), sm=Math.min(ara,arb), k=big/sm;
|
||
var heavier=ara>=arb?a:b, lighter=ara>=arb?b:a;
|
||
out.className='out ok';
|
||
out.innerHTML='<span class="bd">A_r('+a+')='+ara+', A_r('+b+')='+arb+'<br>'
|
||
+(ara===arb?('Атомы '+a+' и '+b+' имеют одинаковую массу.')
|
||
:('Атом <b>'+heavier+'</b> тяжелее атома '+lighter+' в <b>'+(Math.round(k*100)/100).toString().replace('.',',')+'</b> раз.'))+'</span>';
|
||
}
|
||
$('p4-a').addEventListener('change',upd); $('p4-b').addEventListener('change',upd); upd();
|
||
}
|
||
|
||
/* рисуем молекулу как набор шариков-атомов */
|
||
var COL = { H:'#cbd5e1', O:'#ef4444', N:'#3b82f6', C:'#334155', S:'#eab308', Cl:'#22c55e', Fe:'#b45309', Na:'#a78bfa' };
|
||
var RAD = { H:11, O:16, N:15, C:16, S:18, Cl:17, Fe:18, Na:17 };
|
||
function molBalls(atoms) {
|
||
// atoms: [[el,n],...]; рисуем в ряд
|
||
var balls = [], cx = 26;
|
||
atoms.forEach(function(pair){ for(var i=0;i<pair[1];i++){ balls.push(pair[0]); } });
|
||
var W0 = Math.max(120, cx*2 + balls.reduce(function(s,e){return s+(RAD[e]||14)*2+8;},0));
|
||
var x = cx, svg = '';
|
||
balls.forEach(function(el){
|
||
var r = RAD[el]||14; x += r;
|
||
svg += '<circle cx="'+x+'" cy="34" r="'+r+'" fill="'+(COL[el]||'#94a3b8')+'" stroke="rgba(0,0,0,.25)"/>'
|
||
+ '<text x="'+x+'" y="39" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">'+el+'</text>';
|
||
x += r + 8;
|
||
});
|
||
return '<svg viewBox="0 0 '+W0+' 68" width="100%" style="max-width:'+W0+'px;height:auto">'+svg+'</svg>';
|
||
}
|
||
function molCard(name, formula, atoms, note) {
|
||
return '<div style="border:1.5px solid var(--border);border-radius:11px;padding:10px 12px;background:var(--card)">'
|
||
+'<div style="font-weight:700;margin-bottom:2px">'+esc(name)+' · '+(C().formula?C().formula(formula):formula)+'</div>'
|
||
+ molBalls(atoms)
|
||
+'<div style="font-size:.82rem;color:var(--muted);margin-top:4px">'+esc(note)+'</div></div>';
|
||
}
|
||
|
||
/* §5 — галерея простых веществ */
|
||
function mount_p5() {
|
||
var m = $('p5-gal'); if (!m || m._built) return; m._built = 1;
|
||
m.innerHTML = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">'
|
||
+ molCard('Водород','H2',[['H',2]],'2 атома H — двухатомная молекула')
|
||
+ molCard('Кислород','O2',[['O',2]],'2 атома O')
|
||
+ molCard('Озон','O3',[['O',3]],'3 атома O — тоже простое вещество')
|
||
+ molCard('Азот','N2',[['N',2]],'2 атома N')
|
||
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:8px">Во всех молекулах — атомы <b>одного</b> элемента → это <b>простые вещества</b>. Кислород $\\text{O}_2$ и озон $\\text{O}_3$ образованы одним элементом, но это разные простые вещества.</div>';
|
||
if (W.chem8RenderMath) try { W.chem8RenderMath(m); } catch(e){}
|
||
}
|
||
|
||
/* §6 — классификатор простое/сложное + галерея сложных веществ */
|
||
function mount_p6() {
|
||
var c = $('p6-cls');
|
||
if (c) classifier(c, {
|
||
buckets: ['Простое вещество', 'Сложное вещество'],
|
||
items: [
|
||
{ t:'O₂', b:0 }, { t:'H₂O', b:1 }, { t:'Fe', b:0 }, { t:'CO₂', b:1 },
|
||
{ t:'N₂', b:0 }, { t:'NH₃', b:1 }, { t:'S', b:0 }, { t:'CH₄', b:1 }
|
||
]
|
||
});
|
||
var g = $('p6-gal');
|
||
if (g && !g._built) { g._built = 1;
|
||
g.innerHTML = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px">'
|
||
+ molCard('Вода','H2O',[['O',1],['H',2]],'2 элемента: H и O')
|
||
+ molCard('Углекислый газ','CO2',[['C',1],['O',2]],'2 элемента: C и O')
|
||
+ molCard('Метан','CH4',[['C',1],['H',4]],'2 элемента: C и H')
|
||
+ molCard('Аммиак','NH3',[['N',1],['H',3]],'2 элемента: N и H')
|
||
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:8px">В каждой молекуле — атомы <b>разных</b> элементов → это <b>сложные вещества</b>.</div>';
|
||
if (W.chem8RenderMath) try { W.chem8RenderMath(g); } catch(e){}
|
||
}
|
||
}
|
||
|
||
W.CHEM8_WIDGETS = Object.assign(W.CHEM8_WIDGETS || {}, {
|
||
p1: mount_p1, p2: mount_p2, pr1: mount_pr1, p3: mount_p3,
|
||
p4: mount_p4, p5: mount_p5, p6: mount_p6
|
||
});
|
||
W.FLAG_MOUNTS = Object.assign(W.FLAG_MOUNTS || {}, {});
|
||
})(window);
|