/* — 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 = 'полёт ещё не пошёл — слова мелькнут, когда муха полетит'; } } customElements.define("workshop-pass", WorkshopPass);