feat(techdemo): add opaque island bridge
Add a canvas leaf-widget island to the techdemo that receives server snapshots through Effect::event/CustomEvent while keeping slhx core and runtime semantics unchanged. req: interop/001 req: examples/001 req: examples/005
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>slhx Techdemo</title>
|
||||
<script src="/slhx.js" defer></script>
|
||||
<script src="/island.js" defer></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<link rel="stylesheet" href="/control_center.css">
|
||||
</head>
|
||||
|
||||
@@ -5,3 +5,8 @@
|
||||
.work-card { border:1px solid rgba(255,255,255,.18); border-radius:20px; margin:12px 0; padding:14px; background:linear-gradient(145deg, rgba(255,255,255,.15), rgba(255,255,255,.055)); box-shadow:0 14px 38px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.1); overflow:hidden; overflow-wrap:anywhere; cursor:grab; }
|
||||
.work-card:active { cursor:grabbing; }
|
||||
.work-card.is-selected { border-color:rgba(68,231,255,.9); box-shadow:0 0 0 1px rgba(68,231,255,.4), 0 22px 55px rgba(68,231,255,.14); }
|
||||
.island-card { position:relative; overflow:hidden; }
|
||||
.island-card::before { content:""; position:absolute; inset:-30% -20%; background:radial-gradient(circle at 35% 30%, rgba(68,231,255,.22), transparent 35%), radial-gradient(circle at 70% 70%, rgba(184,255,90,.14), transparent 34%); pointer-events:none; }
|
||||
.island-card h2, .island-card canvas, .island-card p { position:relative; z-index:1; }
|
||||
.island-card canvas { width:100%; height:auto; display:block; margin:10px 0; border:1px solid rgba(255,255,255,.14); border-radius:20px; background:#06121f; box-shadow:inset 0 1px 0 rgba(255,255,255,.08), 0 18px 50px rgba(0,0,0,.2); }
|
||||
.island-card p { margin:0; color:var(--muted); font-size:13px; }
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
<h2>Effect inspector</h2>
|
||||
<div data-slhx-slot="inspector">{+= self.inspector =+}</div>
|
||||
</article>
|
||||
<article class="glass-card island-card" data-slhx-island="orbit" +data-island-snapshot="self.island_snapshot">
|
||||
<h2>Opaque island bridge</h2>
|
||||
<canvas width="360" height="180" aria-label="Animated island orbit"></canvas>
|
||||
<p data-island-readout="">Waiting for Rust snapshot…</p>
|
||||
</article>
|
||||
<article class="glass-card">
|
||||
<h2>Activity stream</h2>
|
||||
<div data-slhx-slot="activity">{+= self.activity =+}</div>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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);
|
||||
|
||||
rootOf(island).addEventListener("slhx:island-orbit", (event) => {
|
||||
state.snapshot = parseSnapshot(event.detail);
|
||||
island.setAttribute("data-island-snapshot", event.detail);
|
||||
});
|
||||
|
||||
function tick() {
|
||||
state.frame += 1;
|
||||
render(canvas, readout, state);
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
}
|
||||
|
||||
function scan() {
|
||||
forEachElement(document, (el) => {
|
||||
if (el.hasAttribute("data-slhx-island")) boot(el);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", scan);
|
||||
else scan();
|
||||
})();
|
||||
Reference in New Issue
Block a user