feat: WebSocket real-time + rAF render gate + guest board + screen picker
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>
This commit is contained in:
@@ -1,4 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
/* Strip SVG markup for canvas fillText — replaces icon SVGs with Unicode */
|
||||
function _csClean(s) {
|
||||
if (!s || !s.includes('<svg')) return s;
|
||||
return s.replace(/<svg[\s\S]*?<\/svg>/g, m => {
|
||||
if (m.includes('x1="5" y1="12" x2="19"')) return '\u2192'; // → right arrow
|
||||
if (m.includes('x1="12" y1="5" x2="12" y2="19"')) return '\u2193'; // ↓ down (precip)
|
||||
if (m.includes('x1="12" y1="19" x2="12" y2="5"')) return '\u2191'; // ↑ up (gas)
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════
|
||||
ChemSandboxSim v2 — «Химическая песочница»
|
||||
• Колба Эрленмейера с реалистичным стеклом
|
||||
@@ -1089,7 +1101,7 @@ class ChemSandboxSim {
|
||||
// ── Молекулярное уравнение ──
|
||||
ctx.font = 'bold 17px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = rx.fx.none ? 'rgba(255,100,100,0.75)' : 'rgba(255,255,255,0.95)';
|
||||
ctx.fillText(rx.eq, W / 2, y);
|
||||
ctx.fillText(_csClean(rx.eq), W / 2, y);
|
||||
y += 22;
|
||||
|
||||
// ── Тип реакции + пояснение ──
|
||||
@@ -1109,7 +1121,7 @@ class ChemSandboxSim {
|
||||
if (rx.why) {
|
||||
ctx.font = '13px sans-serif';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.55)';
|
||||
ctx.fillText(rx.why, W / 2, y);
|
||||
ctx.fillText(_csClean(rx.why), W / 2, y);
|
||||
y += 17;
|
||||
}
|
||||
|
||||
@@ -1117,7 +1129,7 @@ class ChemSandboxSim {
|
||||
if (rx.ionFull) {
|
||||
ctx.font = '13px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = 'rgba(155,200,255,0.60)';
|
||||
ctx.fillText('Полн.: ' + rx.ionFull, W / 2, y);
|
||||
ctx.fillText('Полн.: ' + _csClean(rx.ionFull), W / 2, y);
|
||||
y += 16;
|
||||
}
|
||||
|
||||
@@ -1125,7 +1137,7 @@ class ChemSandboxSim {
|
||||
if (rx.ionNet) {
|
||||
ctx.font = 'bold 13px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = 'rgba(123,245,164,0.75)';
|
||||
ctx.fillText('Сокр.: ' + rx.ionNet, W / 2, y);
|
||||
ctx.fillText('Сокр.: ' + _csClean(rx.ionNet), W / 2, y);
|
||||
y += 16;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user