Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js, общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/ workshop.css), компоненты читают данные пода по SSE /workshop/sse. Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/. build.js собирает страницу (active: workshop).
71 lines
No EOL
2.9 KiB
JavaScript
71 lines
No EOL
2.9 KiB
JavaScript
/* <workshop-target> — where the fly is heading right now.
|
|
*
|
|
* The flight reports a `target` event on every step (throttled server-side to a
|
|
* sane rate), naming the word it currently pulls toward. That word changes
|
|
* fast — several per second — and this element shows it as a running line, with
|
|
* a short trail of the words it passed on the way. This is the fly *flying*,
|
|
* not the fly eating: the meal stream (workshop-words) stays separate and still
|
|
* arrives ~once every ten seconds.
|
|
*
|
|
* A `target` with `none: true` means every word was on cooldown — there is no
|
|
* destination at that moment, and the element says so instead of freezing on a
|
|
* stale word.
|
|
*
|
|
* Honesty: the fly does not "want" or "aim" — "target" is our layer over the
|
|
* brain's state. Which word wins is measured (`sim`); the hungriness shown is
|
|
* the author's frame, labelled on the page.
|
|
*/
|
|
class WorkshopTarget extends HTMLElement {
|
|
connectedCallback() {
|
|
this.trailLen = parseInt(this.getAttribute("trail") || "8", 10);
|
|
this.trail = [];
|
|
this.current = null;
|
|
this.render();
|
|
this._off = WorkshopStream.on("target", (ev) => this.push(ev));
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this._off) this._off();
|
|
}
|
|
|
|
push(ev) {
|
|
this.current = ev;
|
|
if (!ev.none && ev.term && ev.term !== (this.trail[0] && this.trail[0].term)) {
|
|
this.trail.unshift(ev);
|
|
if (this.trail.length > this.trailLen) this.trail.pop();
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
const cur = this.current;
|
|
let head;
|
|
if (!cur) {
|
|
head = '<span class="wt-idle">полёт ещё не пошёл — муха ждёт настроения от мозга</span>';
|
|
} else if (cur.none) {
|
|
head = '<span class="wt-none" title="все слова на кулдауне — цели нет">нет цели · все слова отрастают</span>';
|
|
} else {
|
|
const sim = typeof cur.sim === "number" ? cur.sim.toFixed(3) : "—";
|
|
const hunger = typeof cur.hunger === "number" ? cur.hunger.toFixed(2) : "—";
|
|
const entropy = typeof cur.entropy === "number" ? cur.entropy.toFixed(2) : "—";
|
|
const tip = `lang=${cur.lang} sim=${sim} step=${cur.step} ` +
|
|
`hunger=${hunger} entropy=${entropy}`;
|
|
head = `<span class="wt-cur" title="${tip}">` +
|
|
`${escapeHtml(cur.term)}<i>${sim}</i></span>`;
|
|
}
|
|
const trail = this.trail.slice(1).map((t) =>
|
|
`<span class="wt-past" title="sim=${t.sim}">${escapeHtml(t.term)}</span>`
|
|
).join("");
|
|
this.innerHTML = `<div class="wt-line"><span class="wt-label">цель сейчас:</span>` +
|
|
`${head}</div>` +
|
|
(trail ? `<div class="wt-trail">${trail}</div>` : "");
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
})[c]);
|
|
}
|
|
|
|
customElements.define("workshop-target", WorkshopTarget); |