d732c921c2
Keep opaque island widgets live when slhx swaps or later DOM insertion adds a new island, and tear down the listener when an island leaves the document. req: interop/001 req: examples/001
123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
// Opaque leaf-widget island. slhx talks to it only with native CustomEvent payloads.
|
|
// req: interop/001 req: examples/001
|
|
(() => {
|
|
const roots = new WeakMap();
|
|
|
|
function forEachElement(scope, visit) {
|
|
for (let node = scope && scope.firstElementChild; node; node = node.nextElementSibling) {
|
|
visit(node);
|
|
forEachElement(node, visit);
|
|
}
|
|
}
|
|
|
|
function firstElement(scope, predicate) {
|
|
let found = null;
|
|
forEachElement(scope, (el) => {
|
|
if (!found && predicate(el)) found = el;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function rootOf(node) {
|
|
for (let el = node; el; el = el.parentElement) {
|
|
if (el.hasAttribute && el.hasAttribute("data-slhx-root")) return el;
|
|
}
|
|
return document.documentElement;
|
|
}
|
|
|
|
function parseSnapshot(raw) {
|
|
const parts = String(raw || "0|0|0|waiting for Rust").split("|");
|
|
return {
|
|
power: Number(parts[0] || 0) || 0,
|
|
cards: Number(parts[1] || 0) || 0,
|
|
impact: Number(parts[2] || 0) || 0,
|
|
label: parts.slice(3).join("|") || "waiting for Rust",
|
|
};
|
|
}
|
|
|
|
function render(canvas, readout, state) {
|
|
const ctx = canvas && canvas.getContext && canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
const t = state.frame / 48;
|
|
ctx.clearRect(0, 0, w, h);
|
|
ctx.fillStyle = "#06121f";
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const cx = w / 2;
|
|
const cy = h / 2;
|
|
const radius = 38 + Math.min(52, state.snapshot.impact * 2);
|
|
ctx.strokeStyle = "rgba(68,231,255,.45)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(cx, cy, radius * 1.55, radius * 0.72, -0.18, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = "rgba(184,255,90,.95)";
|
|
for (let i = 0; i < Math.max(1, state.snapshot.cards); i += 1) {
|
|
const angle = t + (Math.PI * 2 * i / Math.max(1, state.snapshot.cards));
|
|
const x = cx + Math.cos(angle) * radius * 1.55;
|
|
const y = cy + Math.sin(angle) * radius * 0.72;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 4 + (state.snapshot.power % 4), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.fillStyle = "#fff";
|
|
ctx.font = "700 16px system-ui, sans-serif";
|
|
ctx.fillText("slhx island", 18, 30);
|
|
ctx.font = "12px system-ui, sans-serif";
|
|
ctx.fillStyle = "rgba(255,255,255,.74)";
|
|
ctx.fillText(`cards ${state.snapshot.cards} · impact ${state.snapshot.impact} · boost ${state.snapshot.power}`, 18, 50);
|
|
if (readout) readout.textContent = state.snapshot.label;
|
|
}
|
|
|
|
function boot(island) {
|
|
if (roots.has(island)) return;
|
|
const canvas = firstElement(island, (el) => el.tagName === "CANVAS");
|
|
const readout = firstElement(island, (el) => el.hasAttribute("data-island-readout"));
|
|
const state = { frame: 0, snapshot: parseSnapshot(island.getAttribute("data-island-snapshot")) };
|
|
roots.set(island, state);
|
|
|
|
const root = rootOf(island);
|
|
const update = (event) => {
|
|
state.snapshot = parseSnapshot(event.detail);
|
|
island.setAttribute("data-island-snapshot", event.detail);
|
|
};
|
|
root.addEventListener("slhx:island-orbit", update);
|
|
|
|
function tick() {
|
|
if (!document.contains(island)) {
|
|
root.removeEventListener("slhx:island-orbit", update);
|
|
return;
|
|
}
|
|
state.frame += 1;
|
|
render(canvas, readout, state);
|
|
requestAnimationFrame(tick);
|
|
}
|
|
tick();
|
|
}
|
|
|
|
function scan() {
|
|
forEachElement(document, (el) => {
|
|
if (el.hasAttribute("data-slhx-island")) boot(el);
|
|
});
|
|
}
|
|
|
|
function bootAddedIslands(records) {
|
|
for (const record of records) {
|
|
for (const node of record.addedNodes) {
|
|
if (node.nodeType === 1) {
|
|
scan();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", scan);
|
|
else scan();
|
|
if (typeof MutationObserver !== "undefined") new MutationObserver(bootAddedIslands).observe(document.documentElement, { childList: true, subtree: true });
|
|
})();
|