/ proof-of-concept / cymsimweb
Sign in
proof-of-concept / cymsimweb
UI: exaggeration defaults to x1, play position returns to 0 after playback, resizable panes
Jeremy Magland <jmagland@flatironinstitute.org> committed commit cffea67fb18a parent 1e29264 Browse files
3 changed files+41−9
index.htmlmodified+4−1View file
@@ -40,16 +40,18 @@
4040 <div id="tab-summary" class="tab hidden"><div id="summary"></div></div>
4141 <ul id="issues"></ul>
4242 </aside>
43+ <div class="splitter vertical" data-target="sidebar" data-var="--sidebar-w"></div>
4344 <main>
4445 <section id="plate-section">
4546 <div class="toolbar">
4647 <span>Plate</span>
47- <label>exaggeration <input id="exag" type="range" min="0" max="3" step="0.1" value="1.3" /><span id="exag-text">x20</span></label>
48+ <label>exaggeration <input id="exag" type="range" min="0" max="3" step="0.1" value="0" /><span id="exag-text">x1</span></label>
4849 <button id="reset-view">Reset view</button>
4950 <span id="plate-info" class="muted"></span>
5051 </div>
5152 <canvas id="plate"></canvas>
5253 </section>
54+ <div class="splitter horizontal" data-target="plate-section" data-var="--plate-h"></div>
5355 <section id="spec-section">
5456 <div class="toolbar">
5557 <span>Spectrogram</span>
@@ -58,6 +60,7 @@
5860 </div>
5961 <canvas id="spectrogram"></canvas>
6062 </section>
63+ <div class="splitter horizontal" data-target="readouts" data-var="--readouts-h" data-invert="1"></div>
6164 <section id="playback">
6265 <button id="play" disabled>Play</button>
6366 <button id="stop-audio" disabled>Stop</button>
src/main.tsmodified+24−2View file
@@ -86,7 +86,26 @@ const exag = $<HTMLInputElement>("exag");
8686 const applyExag = () => { plate.exaggeration = Math.pow(10, Number(exag.value)); $("exag-text").textContent = `x${plate.exaggeration.toFixed(0)}`; plate.render(); };
8787 exag.oninput = applyExag; applyExag();
8888 $("reset-view").onclick = () => plate.resetView();
89-window.addEventListener("resize", () => { plate.render(); specView.draw(); });
89+const redraw = () => { plate.render(); specView.draw(); };
90+new ResizeObserver(redraw).observe($("plate"));
91+new ResizeObserver(redraw).observe($("spectrogram"));
92+
93+// splitters: drag to set the CSS variable that sizes the neighbouring pane
94+for (const sp of document.querySelectorAll<HTMLElement>(".splitter")) {
95+ const target = $(sp.dataset.target!), cssVar = sp.dataset.var!, vertical = sp.classList.contains("vertical"), invert = sp.dataset.invert === "1";
96+ sp.addEventListener("pointerdown", (e) => {
97+ e.preventDefault();
98+ sp.setPointerCapture(e.pointerId); sp.classList.add("active");
99+ const start = vertical ? e.clientX : e.clientY, size0 = vertical ? target.getBoundingClientRect().width : target.getBoundingClientRect().height;
100+ const move = (ev: PointerEvent) => {
101+ const delta = (vertical ? ev.clientX : ev.clientY) - start;
102+ const size = Math.max(60, size0 + (invert ? -delta : delta));
103+ document.documentElement.style.setProperty(cssVar, `${size}px`);
104+ };
105+ const up = () => { sp.classList.remove("active"); sp.removeEventListener("pointermove", move); sp.removeEventListener("pointerup", up); redraw(); };
106+ sp.addEventListener("pointermove", move); sp.addEventListener("pointerup", up);
107+ });
108+}
90109
91110 // ---------------------------------------------------------------- run state
92111 let setup: Setup | null = null;
@@ -200,7 +219,10 @@ playBtn.onclick = () => {
200219 player.play(scaleForPlayback(signal(), normalize.checked), setup.fs, Number(position.value) * setup.config.numerics.duration);
201220 stopAudioBtn.disabled = false;
202221 };
203-player.onEnded = () => { stopAudioBtn.disabled = true; specView.cursorTime = null; specView.draw(); };
222+player.onEnded = () => {
223+ // playback reached the end of the available signal: return the position to the start
224+ stopAudioBtn.disabled = true; position.value = "0"; timeEl.textContent = "0.000 s"; specView.cursorTime = null; specView.draw();
225+};
204226 stopAudioBtn.onclick = () => { player.stop(); stopAudioBtn.disabled = true; specView.cursorTime = null; specView.draw(); };
205227 position.oninput = () => { if (setup) { const t = Number(position.value) * setup.config.numerics.duration; timeEl.textContent = `${t.toFixed(3)} s`; specView.cursorTime = t; specView.draw(); } };
206228 wavBtn.onclick = () => { if (setup) download(encodeWav(scaleForPlayback(signal(), normalize.checked), setup.fs), `${setup.config.output.prefix}_${signalSel.value}.wav`); };
src/style.cssmodified+13−6View file
@@ -1,7 +1,11 @@
1-:root { --bg: #14161a; --panel: #1d2026; --border: #2c3038; --text: #e6e6e6; --muted: #9aa0a8; --accent: #e0a23a; --danger: #d9534f; }
1+:root { --sidebar-w: 420px; --plate-h: 40vh; --readouts-h: 190px; --bg: #14161a; --panel: #1d2026; --border: #2c3038; --text: #e6e6e6; --muted: #9aa0a8; --accent: #e0a23a; --danger: #d9534f; }
22 * { box-sizing: border-box; }
33 html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 13px/1.4 system-ui, sans-serif; }
4-#app { display: grid; grid-template-columns: 420px 1fr; height: 100vh; }
4+#app { display: grid; grid-template-columns: var(--sidebar-w) 6px 1fr; height: 100vh; }
5+.splitter { background: var(--border); flex: none; }
6+.splitter.vertical { width: 6px; cursor: col-resize; }
7+.splitter.horizontal { height: 6px; cursor: row-resize; }
8+.splitter:hover, .splitter.active { background: var(--accent); }
59 #sidebar { background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }
610 #sidebar header { padding: 10px 12px 4px; }
711 #sidebar h1 { margin: 0; font-size: 18px; display: inline; }
@@ -40,17 +44,20 @@ progress { flex: 1; height: 10px; }
4044 #form .hit .head { display: flex; justify-content: space-between; align-items: center; }
4145 #form input[type="number"] { width: 100%; }
4246 #summary { padding: 6px 12px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; white-space: pre-wrap; }
43-main { display: grid; grid-template-rows: 1fr 1fr auto auto; min-height: 0; }
44-main section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid var(--border); }
47+main { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
48+main section { min-height: 0; display: flex; flex-direction: column; }
49+#plate-section { height: var(--plate-h); flex: none; }
50+#spec-section { flex: 1; min-height: 60px; }
51+#playback { flex: none; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
4552 .toolbar { display: flex; gap: 14px; align-items: center; padding: 4px 10px; font-weight: 600; }
4653 .toolbar label { font-weight: normal; color: var(--muted); display: flex; gap: 6px; align-items: center; }
4754 canvas { flex: 1; width: 100%; height: 100%; display: block; min-height: 0; }
4855 #playback { flex-direction: row; align-items: center; gap: 10px; padding: 6px 10px; }
4956 #playback #position { flex: 1; }
5057 #playback label { color: var(--muted); display: flex; gap: 6px; align-items: center; }
51-#readouts { padding: 6px 10px; max-height: 190px; overflow: auto; font-size: 12px; }
58+#readouts { padding: 6px 10px; height: var(--readouts-h); flex: none; overflow: auto; font-size: 12px; }
5259 #readouts table { border-collapse: collapse; margin-right: 24px; display: inline-table; vertical-align: top; }
5360 #readouts th, #readouts td { padding: 1px 8px; text-align: right; border-bottom: 1px solid var(--border); }
5461 #readouts th { color: var(--muted); font-weight: normal; }
5562 #readouts .log { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--muted); white-space: pre-wrap; margin-top: 4px; }
56-@media (max-width: 900px) { #app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; } #sidebar { max-height: 45vh; } }
63+@media (max-width: 900px) { #app { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr; } #sidebar { max-height: 45vh; } .splitter.vertical { display: none; } }
↑↓ move↵openescclose