eureka-web/assets/workshop/workshop-pass.js
eureka cc3d6c5ca0 workshop: страница в общей оболочке сайта + компоненты
Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js,
общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/
workshop.css), компоненты читают данные пода по SSE /workshop/sse.
Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/.
build.js собирает страницу (active: workshop).
2026-10-04 01:08:06 +03:00

86 lines
3.3 KiB
JavaScript
Raw Permalink 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.

/* <workshop-pass> — the words the fly flashes past, in flight order.
*
* Task 014 gave the fly inertia: it steers with a limited turn rate, so it
* overshoots and *passes* many words instead of parking on them. The server
* emits one `pass` event per fly-by (entering the pass radius, not one per
* step inside it), at ~3–10 per second. This element turns that stream into
* the visible flight: each passed word blinks in bright and settles dim, so
* the eye reads a fast body moving through meaning — not a table of meals.
*
* Every passed word is shown, not only the unique ones (the author's
* decision): the point is the motion, and the same word may be passed again
* when the fly loops back. The stream is batched per animation frame and the
* node count is capped, so 10 events/s costs one cheap append per frame, not
* a DOM rebuild on every event.
*
* Honesty: the fly does not "aim" or "want". `pass` is our layer over the
* brain's state; the field geometry decides what is near. Which word is
* passed is measured (`sim`/`dist`); the motion is the author's frame.
*/
class WorkshopPass extends HTMLElement {
connectedCallback() {
this.count = parseInt(this.getAttribute("count") || "28", 10);
this.nodes = [];
this.queue = [];
this._raf = null;
this.renderEmpty();
this._off = WorkshopStream.on("pass", (ev) => this.push(ev));
}
disconnectedCallback() {
if (this._off) this._off();
if (this._raf != null) cancelAnimationFrame(this._raf);
}
push(ev) {
this.queue.push(ev);
if (this._raf == null) {
this._raf = requestAnimationFrame(() => this.flush());
}
}
flush() {
this._raf = null;
const batch = this.queue;
this.queue = [];
if (!batch.length) return;
const placeholder = this.querySelector(".wp-empty");
if (placeholder) placeholder.remove();
for (const ev of batch) {
const el = this._make(ev);
this.appendChild(el);
this.nodes.push(el);
}
// Cap the visible trail; the oldest words are already dim by now, so
// dropping them is invisible.
while (this.nodes.length > this.count) {
this.nodes.shift().remove();
}
}
_make(ev) {
const el = document.createElement("span");
el.className = "wp-word";
// Closeness at the fly-by brightens the resting glow a little: a word
// passed very near lingers more than one clipped at the radius edge.
if (typeof ev.sim === "number") {
const closeness = Math.max(0, Math.min(1, (ev.sim + 1) / 2));
el.style.setProperty("--wp-dim", (0.16 + 0.5 * closeness).toFixed(3));
}
el.textContent = ev.term;
const sim = typeof ev.sim === "number" ? ev.sim.toFixed(3) : "—";
const dist = typeof ev.dist === "number" ? ev.dist.toFixed(3) : "—";
const hunger = typeof ev.hunger === "number" ? ev.hunger.toFixed(2) : "—";
const entropy = typeof ev.entropy === "number" ? ev.entropy.toFixed(2) : "—";
el.title = `lang=${ev.lang} sim=${sim} dist=${dist} ` +
`hunger=${hunger} entropy=${entropy}`;
return el;
}
renderEmpty() {
this.innerHTML =
'<span class="wp-empty">полёт ещё не пошёл — слова мелькнут, когда муха полетит</span>';
}
}
customElements.define("workshop-pass", WorkshopPass);