Add portal web simulator, SPI bridges, and Pico firmware updates.
Bring the five-panel hex portal online with a browser 3D/schematic preview, Pi SPI backends, and renamed multi-panel Pico UDP firmware. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
185
web/js/components/portal-controls.js
Normal file
185
web/js/components/portal-controls.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const CONTROL_STYLES = `
|
||||
:host {
|
||||
display: block;
|
||||
border-right: 1px solid var(--border, #2a3044);
|
||||
background: var(--panel, #12151f);
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
color: var(--text, #e8ecf7);
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--muted, #8b93a8);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
a {
|
||||
color: var(--accent, #5b8cff);
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0.75rem 0 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted, #8b93a8);
|
||||
}
|
||||
select, input[type="range"], button {
|
||||
width: 100%;
|
||||
}
|
||||
select, button {
|
||||
background: #1a1f2e;
|
||||
color: inherit;
|
||||
border: 1px solid var(--border, #2a3044);
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
button {
|
||||
margin-top: 0.75rem;
|
||||
cursor: pointer;
|
||||
background: #243049;
|
||||
}
|
||||
button:hover { border-color: var(--accent, #5b8cff); }
|
||||
button.active {
|
||||
background: #2a3f6e;
|
||||
border-color: var(--accent, #5b8cff);
|
||||
}
|
||||
.view-toggle {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export class PortalControls extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {import('../api.js').PortalConfig | null} */
|
||||
this._config = null;
|
||||
this._animation = "rainbow";
|
||||
this._brightness = 0.35;
|
||||
this._fps = 25;
|
||||
this._paused = false;
|
||||
this._identify = false;
|
||||
this._view3d = true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) return;
|
||||
const root = this.attachShadow({ mode: "open" });
|
||||
root.innerHTML = `
|
||||
<style>${CONTROL_STYLES}</style>
|
||||
<h1>Portal Simulator</h1>
|
||||
<p>Hexagonal LED arch (see <a href="https://technical.kiwi/images/portal/IMG_20241029_220222.jpg" target="_blank" rel="noopener">photo</a>). Panel 2 top (Pi); 0, 1, 3, 4 walls; floor bare.</p>
|
||||
<label for="animation">Animation</label>
|
||||
<select id="animation"></select>
|
||||
<label for="brightness">Brightness</label>
|
||||
<input id="brightness" type="range" min="0.05" max="1" step="0.05" value="0.35" />
|
||||
<label for="fps">FPS</label>
|
||||
<input id="fps" type="range" min="5" max="40" step="1" value="25" />
|
||||
<div class="view-toggle">
|
||||
<button type="button" data-view="3d" class="active">3D view</button>
|
||||
<button type="button" data-view="flat">Flat map</button>
|
||||
</div>
|
||||
<button type="button" data-action="identify">Identify panels</button>
|
||||
<button type="button" data-action="pause">Pause</button>
|
||||
`;
|
||||
|
||||
this._animationEl = root.querySelector("#animation");
|
||||
this._brightnessEl = root.querySelector("#brightness");
|
||||
this._fpsEl = root.querySelector("#fps");
|
||||
|
||||
this._animationEl.addEventListener("change", () => {
|
||||
this._animation = this._animationEl.value;
|
||||
if (this._config?.defaultFps[this._animation]) {
|
||||
this._fps = Math.min(40, Math.round(this._config.defaultFps[this._animation]));
|
||||
this._fpsEl.value = String(this._fps);
|
||||
}
|
||||
this._emitChange();
|
||||
});
|
||||
this._brightnessEl.addEventListener("input", () => {
|
||||
this._brightness = Number(this._brightnessEl.value);
|
||||
this._emitChange();
|
||||
});
|
||||
this._fpsEl.addEventListener("input", () => {
|
||||
this._fps = Number(this._fpsEl.value);
|
||||
this._emitChange();
|
||||
});
|
||||
|
||||
root.querySelector('[data-view="3d"]').addEventListener("click", () => {
|
||||
this._view3d = true;
|
||||
this._syncViewButtons();
|
||||
this._emitChange();
|
||||
});
|
||||
root.querySelector('[data-view="flat"]').addEventListener("click", () => {
|
||||
this._view3d = false;
|
||||
this._syncViewButtons();
|
||||
this._emitChange();
|
||||
});
|
||||
root.querySelector('[data-action="identify"]').addEventListener("click", (e) => {
|
||||
this._identify = !this._identify;
|
||||
e.currentTarget.classList.toggle("active", this._identify);
|
||||
this._emitChange();
|
||||
});
|
||||
root.querySelector('[data-action="pause"]').addEventListener("click", (e) => {
|
||||
this._paused = !this._paused;
|
||||
e.currentTarget.textContent = this._paused ? "Resume" : "Pause";
|
||||
this._emitChange();
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('../api.js').PortalConfig} config */
|
||||
setConfig(config) {
|
||||
this._config = config;
|
||||
if (!this.shadowRoot) this.connectedCallback();
|
||||
this._animationEl.innerHTML = "";
|
||||
for (const name of config.animations) {
|
||||
if (name === "solid") continue;
|
||||
const opt = document.createElement("option");
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
this._animationEl.appendChild(opt);
|
||||
}
|
||||
this._animationEl.value = "rainbow";
|
||||
this._animation = "rainbow";
|
||||
this._brightness = config.defaultBrightness ?? 0.35;
|
||||
this._brightnessEl.value = String(this._brightness);
|
||||
this._fps = Math.min(40, Math.round(config.defaultFps.rainbow ?? 25));
|
||||
this._fpsEl.value = String(this._fps);
|
||||
this._emitChange();
|
||||
}
|
||||
|
||||
_syncViewButtons() {
|
||||
const root = this.shadowRoot;
|
||||
root.querySelector('[data-view="3d"]').classList.toggle("active", this._view3d);
|
||||
root.querySelector('[data-view="flat"]').classList.toggle("active", !this._view3d);
|
||||
}
|
||||
|
||||
_emitChange() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("portal-settings", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
animation: this._animation,
|
||||
brightness: this._brightness,
|
||||
fps: this._fps,
|
||||
paused: this._paused,
|
||||
identify: this._identify,
|
||||
view3d: this._view3d,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @returns {import('../api.js').PortalConfig | null} */
|
||||
get config() {
|
||||
return this._config;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("portal-controls", PortalControls);
|
||||
Reference in New Issue
Block a user