fd29acbbdd
Classroom performance: - WebSocket server (ws-server.js) for low-latency cursor & stroke preview Replaces HTTP POST per event → eliminates per-message auth overhead Session member cache (30s TTL) avoids SQLite query per WS message Fallback to HTTP POST when WS not connected - Cursor throttle reduced 100ms → 33ms (~30fps) - Stroke preview throttle reduced 50ms → 20ms - whiteboard.js: render() is now rAF-gated (_doRender/_rafPending) Multiple render() calls within one frame collapse into one repaint document.hidden check — zero CPU when tab is in background visibilitychange listener restores canvas on tab focus Guest board: - guestClassroom.js route: public token-based read-only access - guest-board.html: name entry + read-only whiteboard + SSE - SSE: addGuestClient/removeGuestClient/emitToGuests Screen share picker: - Discord-style modal with tab switching (screen/window/tab) - Live video preview before confirming share - useExistingScreenStream() in ClassroomRTC Fullscreen exit overlay: - #cr-fs-exit-overlay button inside cr-board-wrap - Visible only via CSS :fullscreen selector (touchpad users) File sharing from library: - Teacher picks file from library, sends as styled card in chat - crDownloadLibraryFile() fetches with Bearer auth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
542 lines
19 KiB
JavaScript
542 lines
19 KiB
JavaScript
'use strict';
|
||
/**
|
||
* ElectrolysisSim v2 — Электролиз водных растворов
|
||
* Закон Фарадея: m = M·I·t / (n·F), F = 96485 Кл/моль
|
||
* Чистый рерайт: стабильная физика, ионная анимация, пузырьки, осадок.
|
||
*/
|
||
class ElectrolysisSim {
|
||
static F = 96485;
|
||
static BG = '#0b0b1a';
|
||
static FONT = 'Manrope, system-ui, sans-serif';
|
||
|
||
static ELECTROLYTES = {
|
||
NaCl: {
|
||
name: 'NaCl', displayName: 'NaCl (водный р-р)',
|
||
cation: 'Na\u207A', anion: 'Cl\u207B',
|
||
M: 2, n: 2, R: 8,
|
||
solColor: [160, 200, 230],
|
||
cathodeProduct: 'H\u2082', anodeProduct: 'Cl\u2082',
|
||
depositColor: null,
|
||
cathodeBubColor: 'rgba(160,210,255,0.55)',
|
||
anodeBubColor: 'rgba(180,255,140,0.50)',
|
||
cathodeEq: '2H\u2082O + 2e\u207B \u2192 H\u2082 + 2OH\u207B',
|
||
anodeEq: '2Cl\u207B \u2212 2e\u207B \u2192 Cl\u2082',
|
||
},
|
||
CuSO4: {
|
||
name: 'CuSO\u2084', displayName: 'CuSO\u2084 (водный р-р)',
|
||
cation: 'Cu\u00B2\u207A', anion: 'SO\u2084\u00B2\u207B',
|
||
M: 63.546, n: 2, R: 12,
|
||
solColor: [55, 120, 210],
|
||
cathodeProduct: 'Cu\u2193', anodeProduct: 'O\u2082',
|
||
depositColor: '#b87333',
|
||
cathodeBubColor: null,
|
||
anodeBubColor: 'rgba(200,210,255,0.50)',
|
||
cathodeEq: 'Cu\u00B2\u207A + 2e\u207B \u2192 Cu\u2193',
|
||
anodeEq: '2H\u2082O \u2212 4e\u207B \u2192 O\u2082 + 4H\u207A',
|
||
},
|
||
H2SO4: {
|
||
name: 'H\u2082SO\u2084', displayName: 'H\u2082SO\u2084 (водный р-р)',
|
||
cation: 'H\u207A', anion: 'SO\u2084\u00B2\u207B',
|
||
M: 2, n: 2, R: 6,
|
||
solColor: [200, 200, 215],
|
||
cathodeProduct: 'H\u2082', anodeProduct: 'O\u2082',
|
||
depositColor: null,
|
||
cathodeBubColor: 'rgba(160,210,255,0.55)',
|
||
anodeBubColor: 'rgba(200,210,255,0.50)',
|
||
cathodeEq: '2H\u207A + 2e\u207B \u2192 H\u2082',
|
||
anodeEq: '2H\u2082O \u2212 4e\u207B \u2192 O\u2082 + 4H\u207A',
|
||
},
|
||
};
|
||
|
||
constructor(canvas) {
|
||
this.canvas = canvas;
|
||
this.ctx = canvas.getContext('2d');
|
||
this.W = 0; this.H = 0;
|
||
|
||
this.voltage = 6;
|
||
this.electrolyte = 'NaCl';
|
||
this.speed = 1;
|
||
|
||
this._time = 0;
|
||
this._massDeposit = 0;
|
||
this._gasVolume = 0;
|
||
this._depositH = 0;
|
||
this._ions = [];
|
||
this._bubbles = [];
|
||
this._electronPhase = 0;
|
||
|
||
this.playing = false;
|
||
this._raf = null;
|
||
this._lastTs = null;
|
||
this.onUpdate = null;
|
||
|
||
new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);
|
||
}
|
||
|
||
// ── public API ────────────────────────────────────────────────
|
||
|
||
fit() {
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const w = this.canvas.offsetWidth || 640;
|
||
const h = this.canvas.offsetHeight || 420;
|
||
this.canvas.width = w * dpr;
|
||
this.canvas.height = h * dpr;
|
||
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
this.W = w; this.H = h;
|
||
this._initIons();
|
||
}
|
||
|
||
getParams() { return { voltage: this.voltage, electrolyte: this.electrolyte }; }
|
||
setParams({ voltage, electrolyte } = {}) {
|
||
if (voltage !== undefined) this.voltage = Math.max(1, Math.min(12, +voltage));
|
||
if (electrolyte !== undefined) {
|
||
// accept both 'nacl' (from lab.html) and 'NaCl' (canonical)
|
||
const keyMap = { nacl: 'NaCl', cuso4: 'CuSO4', h2so4: 'H2SO4' };
|
||
const key = keyMap[String(electrolyte).toLowerCase()] || electrolyte;
|
||
if (ElectrolysisSim.ELECTROLYTES[key] && this.electrolyte !== key) {
|
||
this.electrolyte = key;
|
||
this.reset(); return;
|
||
}
|
||
}
|
||
this.draw(); this._emit();
|
||
}
|
||
|
||
preset(name) {
|
||
const map = { nacl: ['NaCl', 6], cuso4: ['CuSO4', 4], h2so4: ['H2SO4', 3] };
|
||
const [el, v] = map[name] || map.nacl;
|
||
this.voltage = v; this.electrolyte = el;
|
||
this.reset();
|
||
}
|
||
|
||
reset() {
|
||
this.pause();
|
||
this._time = 0; this._massDeposit = 0;
|
||
this._gasVolume = 0; this._depositH = 0;
|
||
this._bubbles = []; this._electronPhase = 0;
|
||
this._initIons();
|
||
this.draw(); this._emit();
|
||
}
|
||
|
||
play() { if (this.playing) return; this.playing = true; this._lastTs = null; this._tick(); }
|
||
pause() { this.playing = false; if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; } }
|
||
start() { this.play(); }
|
||
stop() { this.pause(); }
|
||
|
||
info() {
|
||
return {
|
||
voltage: this.voltage,
|
||
current: +this._current().toFixed(3),
|
||
electrolyte: this.electrolyte,
|
||
massDeposited: +this._massDeposit.toFixed(4),
|
||
gasVolume: +this._gasVolume.toFixed(2),
|
||
time: +this._time.toFixed(1),
|
||
};
|
||
}
|
||
|
||
// ── internals ─────────────────────────────────────────────────
|
||
|
||
_emit() { if (this.onUpdate) this.onUpdate(this.info()); }
|
||
_el() { return ElectrolysisSim.ELECTROLYTES[this.electrolyte]; }
|
||
_current() { return this.voltage / this._el().R; }
|
||
|
||
_cell() {
|
||
const { W, H } = this;
|
||
const cw = Math.min(W * 0.50, 300);
|
||
const ch = Math.min(H * 0.48, 210);
|
||
return { cx: (W - cw) / 2, cy: H * 0.28, cw, ch };
|
||
}
|
||
|
||
_electrodes() {
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
const ew = 13, eh = ch * 0.70, gap = cw * 0.12;
|
||
const ey = cy + ch - eh - 8;
|
||
return {
|
||
cathode: { x: cx + gap, y: ey, w: ew, h: eh },
|
||
anode: { x: cx + cw - gap - ew, y: ey, w: ew, h: eh },
|
||
};
|
||
}
|
||
|
||
_initIons() {
|
||
this._ions = [];
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
if (!cw || !ch) return;
|
||
const el = this._el();
|
||
for (let i = 0; i < 30; i++) {
|
||
const isCat = i < 15;
|
||
this._ions.push({
|
||
x: cx + 18 + Math.random() * (cw - 36),
|
||
y: cy + 12 + Math.random() * (ch - 24),
|
||
vx: (Math.random() - 0.5) * 0.7,
|
||
vy: (Math.random() - 0.5) * 0.5,
|
||
charge: isCat ? 1 : -1,
|
||
label: isCat ? el.cation : el.anion,
|
||
color: isCat ? '#EF476F' : '#06D6E0',
|
||
});
|
||
}
|
||
}
|
||
|
||
_spawnIon(charge) {
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
const el = this._el();
|
||
this._ions.push({
|
||
x: charge > 0 ? cx + 8 : cx + cw - 8,
|
||
y: cy + 12 + Math.random() * (ch - 24),
|
||
vx: charge > 0 ? 0.55 : -0.55,
|
||
vy: (Math.random() - 0.5) * 0.4,
|
||
charge,
|
||
label: charge > 0 ? el.cation : el.anion,
|
||
color: charge > 0 ? '#EF476F' : '#06D6E0',
|
||
});
|
||
}
|
||
|
||
_spawnBubble(x, y, color) {
|
||
this._bubbles.push({
|
||
x, y,
|
||
r: 1.5 + Math.random() * 2.5,
|
||
vx: (Math.random() - 0.5) * 0.3,
|
||
vy: -(0.5 + Math.random() * 0.9),
|
||
life: 1,
|
||
decay: 0.005 + Math.random() * 0.007,
|
||
color,
|
||
});
|
||
}
|
||
|
||
// ── simulation tick ────────────────────────────────────────────
|
||
|
||
_tick() {
|
||
if (!this.playing) return;
|
||
this._raf = requestAnimationFrame(ts => {
|
||
if (!this._lastTs) this._lastTs = ts;
|
||
const dt = Math.min((ts - this._lastTs) / 1000, 0.05) * this.speed;
|
||
this._lastTs = ts;
|
||
this._step(dt); this.draw(); this._emit(); this._tick();
|
||
});
|
||
}
|
||
|
||
_step(dt) {
|
||
const el = this._el(), I = this._current();
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
const elec = this._electrodes();
|
||
|
||
this._time += dt;
|
||
this._electronPhase = (this._electronPhase + dt * I * 1.2) % 1;
|
||
|
||
// Faraday's law
|
||
const molesPS = I / (el.n * ElectrolysisSim.F);
|
||
if (el.depositColor) {
|
||
this._massDeposit += el.M * molesPS * dt;
|
||
this._depositH = Math.min(elec.cathode.h * 0.72, this._depositH + dt * 0.14 * I);
|
||
}
|
||
this._gasVolume += molesPS * 22400 * dt;
|
||
|
||
// Ion drift + thermal jitter
|
||
const drift = I * 0.45;
|
||
for (const ion of this._ions) {
|
||
ion.vx += (ion.charge > 0 ? -drift : drift) * dt + (Math.random() - 0.5) * 0.18;
|
||
ion.vy += (Math.random() - 0.5) * 0.14;
|
||
ion.vx = Math.max(-3.5, Math.min(3.5, ion.vx * 0.96));
|
||
ion.vy = Math.max(-3.5, Math.min(3.5, ion.vy * 0.96));
|
||
ion.x += ion.vx; ion.y += ion.vy;
|
||
ion.x = Math.max(cx + 4, Math.min(cx + cw - 4, ion.x));
|
||
ion.y = Math.max(cy + 4, Math.min(cy + ch - 4, ion.y));
|
||
}
|
||
|
||
// Ions reaching electrodes → discharge + bubbles
|
||
const rm = new Set();
|
||
for (let i = 0; i < this._ions.length; i++) {
|
||
const ion = this._ions[i];
|
||
if (ion.charge > 0 && ion.x <= elec.cathode.x + elec.cathode.w + 5) {
|
||
rm.add(i);
|
||
if (el.cathodeBubColor) {
|
||
for (let b = 0; b < 2; b++)
|
||
this._spawnBubble(
|
||
elec.cathode.x + elec.cathode.w + 2 + Math.random() * 4,
|
||
elec.cathode.y + Math.random() * elec.cathode.h,
|
||
el.cathodeBubColor);
|
||
}
|
||
}
|
||
if (ion.charge < 0 && ion.x >= elec.anode.x - 5) {
|
||
rm.add(i);
|
||
if (el.anodeBubColor) {
|
||
for (let b = 0; b < 2; b++)
|
||
this._spawnBubble(
|
||
elec.anode.x - 2 - Math.random() * 4,
|
||
elec.anode.y + Math.random() * elec.anode.h,
|
||
el.anodeBubColor);
|
||
}
|
||
}
|
||
}
|
||
this._ions = this._ions.filter((_, i) => !rm.has(i));
|
||
|
||
// Replenish ions to keep count ~15 each
|
||
let cat = 0, an = 0;
|
||
for (const ion of this._ions) ion.charge > 0 ? cat++ : an++;
|
||
while (cat < 15) { this._spawnIon(1); cat++; }
|
||
while (an < 15) { this._spawnIon(-1); an++; }
|
||
|
||
// Bubble physics
|
||
this._bubbles = this._bubbles.filter(b => {
|
||
b.x += b.vx + Math.sin(b.life * 22) * 0.12;
|
||
b.y += b.vy;
|
||
b.life -= b.decay;
|
||
return b.life > 0 && b.y > cy + 2;
|
||
});
|
||
}
|
||
|
||
// ── draw ──────────────────────────────────────────────────────
|
||
|
||
draw() {
|
||
const { ctx, W, H } = this;
|
||
if (!W || !H) return;
|
||
|
||
// Background
|
||
ctx.fillStyle = ElectrolysisSim.BG; ctx.fillRect(0, 0, W, H);
|
||
|
||
// Dot grid
|
||
ctx.fillStyle = 'rgba(255,255,255,0.018)';
|
||
for (let x = 22; x < W; x += 22)
|
||
for (let y = 22; y < H; y += 22) {
|
||
ctx.beginPath(); ctx.arc(x, y, 0.7, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
this._drawWiresAndBattery();
|
||
this._drawCellBody();
|
||
this._drawSolution();
|
||
this._drawDeposit();
|
||
this._drawElectrodes();
|
||
this._drawBubbles();
|
||
this._drawIons();
|
||
this._drawLabels();
|
||
this._drawInfoPanel();
|
||
}
|
||
|
||
_drawCellBody() {
|
||
const { ctx } = this;
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
ctx.save();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 2;
|
||
ctx.beginPath(); ctx.roundRect(cx, cy, cw, ch, 6); ctx.stroke();
|
||
// glass shimmer
|
||
const gg = ctx.createLinearGradient(cx, cy, cx + 14, cy);
|
||
gg.addColorStop(0, 'rgba(255,255,255,0.05)');
|
||
gg.addColorStop(1, 'rgba(255,255,255,0)');
|
||
ctx.fillStyle = gg; ctx.fillRect(cx + 1, cy + 1, 14, ch - 2);
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawSolution() {
|
||
const { ctx } = this;
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
const [r, g, b] = this._el().solColor;
|
||
ctx.save();
|
||
ctx.beginPath(); ctx.roundRect(cx + 2, cy + 2, cw - 4, ch - 4, 4); ctx.clip();
|
||
const sg = ctx.createLinearGradient(cx, cy, cx, cy + ch);
|
||
sg.addColorStop(0, `rgba(${r},${g},${b},0.06)`);
|
||
sg.addColorStop(1, `rgba(${r},${g},${b},0.22)`);
|
||
ctx.fillStyle = sg; ctx.fillRect(cx + 2, cy + 2, cw - 4, ch - 4);
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawElectrodes() {
|
||
const { ctx } = this;
|
||
const e = this._electrodes();
|
||
const FN = ElectrolysisSim.FONT;
|
||
|
||
ctx.fillStyle = '#42425a';
|
||
ctx.beginPath(); ctx.roundRect(e.cathode.x, e.cathode.y, e.cathode.w, e.cathode.h, 3); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(6,214,224,0.3)'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.roundRect(e.cathode.x, e.cathode.y, e.cathode.w, e.cathode.h, 3); ctx.stroke();
|
||
|
||
ctx.fillStyle = '#525268';
|
||
ctx.beginPath(); ctx.roundRect(e.anode.x, e.anode.y, e.anode.w, e.anode.h, 3); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(239,71,111,0.3)'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.roundRect(e.anode.x, e.anode.y, e.anode.w, e.anode.h, 3); ctx.stroke();
|
||
|
||
ctx.font = `bold 16px ${FN}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||
ctx.fillStyle = '#06D6E0';
|
||
ctx.fillText('\u2212', e.cathode.x + e.cathode.w / 2, e.cathode.y - 3);
|
||
ctx.fillStyle = '#EF476F';
|
||
ctx.fillText('+', e.anode.x + e.anode.w / 2, e.anode.y - 3);
|
||
}
|
||
|
||
_drawDeposit() {
|
||
const el = this._el();
|
||
if (!el.depositColor || this._depositH < 1) return;
|
||
const { ctx } = this;
|
||
const c = this._electrodes().cathode;
|
||
const dh = Math.min(this._depositH, c.h * 0.72);
|
||
ctx.save();
|
||
const dg = ctx.createLinearGradient(c.x + c.w, c.y + c.h - dh, c.x + c.w + 10, c.y + c.h);
|
||
dg.addColorStop(0, 'rgba(184,115,51,0.35)');
|
||
dg.addColorStop(1, 'rgba(184,115,51,0.85)');
|
||
ctx.fillStyle = dg;
|
||
ctx.beginPath(); ctx.roundRect(c.x + c.w, c.y + c.h - dh, 10, dh, [2, 2, 0, 0]); ctx.fill();
|
||
ctx.shadowColor = '#b87333'; ctx.shadowBlur = 6;
|
||
ctx.fillStyle = 'rgba(210,150,80,0.5)';
|
||
ctx.beginPath(); ctx.roundRect(c.x + c.w, c.y + c.h - dh, 10, 3, [2, 2, 0, 0]); ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawIons() {
|
||
const { ctx } = this;
|
||
const FN = ElectrolysisSim.FONT;
|
||
ctx.save();
|
||
for (const ion of this._ions) {
|
||
const g = ctx.createRadialGradient(ion.x, ion.y, 0, ion.x, ion.y, 11);
|
||
g.addColorStop(0, ion.color + '2a'); g.addColorStop(1, ion.color + '00');
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath(); ctx.arc(ion.x, ion.y, 11, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = ion.color;
|
||
ctx.beginPath(); ctx.arc(ion.x, ion.y, 4, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.68)';
|
||
ctx.font = `8px ${FN}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||
ctx.fillText(ion.label, ion.x, ion.y - 5);
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawBubbles() {
|
||
const { ctx } = this;
|
||
ctx.save();
|
||
for (const b of this._bubbles) {
|
||
ctx.globalAlpha = b.life * 0.65;
|
||
ctx.strokeStyle = b.color; ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.fillStyle = `rgba(255,255,255,${b.life * 0.18})`;
|
||
ctx.beginPath(); ctx.arc(b.x - b.r * 0.3, b.y - b.r * 0.3, b.r * 0.3, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawWiresAndBattery() {
|
||
const { ctx } = this;
|
||
const { cx, cy, cw } = this._cell();
|
||
const e = this._electrodes();
|
||
const FN = ElectrolysisSim.FONT;
|
||
|
||
const cXt = e.cathode.x + e.cathode.w / 2; // cathode top center
|
||
const aXt = e.anode.x + e.anode.w / 2; // anode top center
|
||
const bx = cx + cw / 2; // battery center x
|
||
const by = cy - Math.max(42, this.H * 0.09); // battery y
|
||
|
||
ctx.save();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1.5;
|
||
|
||
// Cathode wire: up then right to battery −
|
||
ctx.beginPath();
|
||
ctx.moveTo(cXt, e.cathode.y); ctx.lineTo(cXt, by); ctx.lineTo(bx - 22, by);
|
||
ctx.stroke();
|
||
|
||
// Anode wire: up then left to battery +
|
||
ctx.beginPath();
|
||
ctx.moveTo(aXt, e.anode.y); ctx.lineTo(aXt, by); ctx.lineTo(bx + 22, by);
|
||
ctx.stroke();
|
||
|
||
// Electron flow dots (cathode side: from battery − toward cathode)
|
||
const dist = (bx - 22) - cXt;
|
||
for (let i = 0; i < 4; i++) {
|
||
const t = ((this._electronPhase + i / 4) % 1);
|
||
const ex = (bx - 22) - t * dist;
|
||
const ey = by;
|
||
if (ex >= cXt - 1 && ex <= bx - 22 + 1) {
|
||
ctx.fillStyle = '#4CC9F0'; ctx.shadowColor = '#4CC9F0'; ctx.shadowBlur = 5;
|
||
ctx.beginPath(); ctx.arc(ex, ey, 2.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.shadowBlur = 0;
|
||
}
|
||
}
|
||
|
||
// Battery symbol — two plates
|
||
ctx.strokeStyle = '#EF476F'; ctx.lineWidth = 3;
|
||
ctx.beginPath(); ctx.moveTo(bx + 22, by - 14); ctx.lineTo(bx + 22, by + 14); ctx.stroke();
|
||
ctx.strokeStyle = '#06D6E0'; ctx.lineWidth = 1.8;
|
||
ctx.beginPath(); ctx.moveTo(bx - 22, by - 8); ctx.lineTo(bx - 22, by + 8); ctx.stroke();
|
||
// Connecting wire between battery plates
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(bx - 22, by); ctx.lineTo(bx + 22, by); ctx.stroke();
|
||
|
||
// Voltage label
|
||
ctx.fillStyle = '#FFD166';
|
||
ctx.font = `bold 12px ${FN}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||
ctx.fillText(this.voltage.toFixed(1) + ' V', bx, by - 18);
|
||
|
||
// +/− labels on battery
|
||
ctx.fillStyle = '#EF476F'; ctx.font = `bold 10px ${FN}`; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText('+', bx + 26, by);
|
||
ctx.fillStyle = '#06D6E0'; ctx.textAlign = 'right';
|
||
ctx.fillText('\u2212', bx - 26, by);
|
||
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawLabels() {
|
||
const { ctx } = this;
|
||
const el = this._el(), e = this._electrodes();
|
||
const { cx, cy, cw, ch } = this._cell();
|
||
const FN = ElectrolysisSim.FONT;
|
||
|
||
ctx.save();
|
||
ctx.font = `10px ${FN}`; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||
|
||
ctx.fillStyle = '#06D6E0';
|
||
ctx.fillText('\u041A\u0430\u0442\u043E\u0434 (\u2212)', e.cathode.x + e.cathode.w / 2, cy + ch + 6);
|
||
ctx.fillStyle = '#EF476F';
|
||
ctx.fillText('\u0410\u043D\u043E\u0434 (+)', e.anode.x + e.anode.w / 2, cy + ch + 6);
|
||
|
||
ctx.fillStyle = 'rgba(255,255,255,0.42)';
|
||
ctx.fillText(el.cathodeProduct, e.cathode.x + e.cathode.w / 2, cy + ch + 20);
|
||
ctx.fillText(el.anodeProduct, e.anode.x + e.anode.w / 2, cy + ch + 20);
|
||
|
||
ctx.fillStyle = '#9B5DE5'; ctx.font = `bold 11px ${FN}`;
|
||
ctx.fillText(el.displayName, cx + cw / 2, cy + ch + 36);
|
||
|
||
ctx.font = `8px ${FN}`;
|
||
ctx.fillStyle = 'rgba(6,214,224,0.48)';
|
||
ctx.fillText(el.cathodeEq, e.cathode.x + e.cathode.w / 2, cy + ch + 52);
|
||
ctx.fillStyle = 'rgba(239,71,111,0.48)';
|
||
ctx.fillText(el.anodeEq, e.anode.x + e.anode.w / 2, cy + ch + 52);
|
||
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawInfoPanel() {
|
||
const { ctx } = this;
|
||
const inf = this.info(), el = this._el();
|
||
const FN = ElectrolysisSim.FONT;
|
||
const px = 12, py = 10, pw = 170, lh = 17;
|
||
|
||
const rows = [
|
||
['U', inf.voltage.toFixed(1) + ' \u0412'],
|
||
['I', this._current().toFixed(3) + ' \u0410'],
|
||
['\u0422\u0432\u0440\u0435\u043C\u044F', this._fmtTime(inf.time)],
|
||
];
|
||
if (el.depositColor) rows.push(['m(Cu)', inf.massDeposited.toFixed(4) + ' \u0433']);
|
||
rows.push(['V(\u0433\u0430\u0437)', inf.gasVolume.toFixed(2) + ' \u043C\u043B']);
|
||
|
||
const ph = 12 + rows.length * lh + 8;
|
||
ctx.save();
|
||
ctx.fillStyle = 'rgba(5,5,20,0.86)';
|
||
ctx.beginPath(); ctx.roundRect(px, py, pw, ph, 7); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.roundRect(px, py, pw, ph, 7); ctx.stroke();
|
||
|
||
ctx.font = `10px ${FN}`; ctx.textBaseline = 'middle';
|
||
rows.forEach(([k, v], i) => {
|
||
const ry = py + 10 + i * lh + lh / 2;
|
||
ctx.fillStyle = 'rgba(255,255,255,0.38)'; ctx.textAlign = 'left'; ctx.fillText(k, px + 10, ry);
|
||
ctx.fillStyle = 'rgba(255,255,255,0.88)'; ctx.textAlign = 'right'; ctx.fillText(v, px + pw - 10, ry);
|
||
});
|
||
|
||
ctx.fillStyle = 'rgba(255,255,255,0.16)';
|
||
ctx.font = `italic 8px ${FN}`; ctx.textAlign = 'left';
|
||
ctx.fillText('m = M\u00B7I\u00B7t / (n\u00B7F)', px + 10, py + ph + 10);
|
||
ctx.restore();
|
||
}
|
||
|
||
_fmtTime(s) {
|
||
if (s < 60) return s.toFixed(1) + ' \u0441';
|
||
return Math.floor(s / 60) + ' \u043C\u0438\u043D ' + (s % 60).toFixed(0) + ' \u0441';
|
||
}
|
||
}
|
||
|
||
if (typeof module !== 'undefined') module.exports = ElectrolysisSim;
|