geedback

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit e0260d9171386436f3c2aab2d041ba89e95cfdb3
parent 90c92c218cb8147e9bd76b78590507904e0626ee
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Fri,  8 May 2026 20:02:46 +0900

"feat(ui): Add About page and implement client-side navigation"
success to copy to clip

Diffstat:
Asrc/about.ts | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.ts | 209++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/style.css | 50++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 236 insertions(+), 81 deletions(-)

diff --git a/src/about.ts b/src/about.ts @@ -0,0 +1,58 @@ +export const renderAbout = (app: HTMLDivElement) => { + app.innerHTML = ` + <div class="about-container"> + <nav> + <a href="#" id="back-home">← Back to App</a> + </nav> + + <h1>About Geedback</h1> + + <section class="about-section"> + <h2>What is Geedback?</h2> + <p> + Geedback is a multi-oscillator synthesizer that uses smartphone feedback and gravity sensors + to create unique soundscapes. It explores the interaction between movement and sound synthesis. + </p> + </section> + + <section class="about-section"> + <h2>Author</h2> + <p>Created by <strong>tetsu AKA Minerva_Juppiter</strong></p> + </section> + + <section class="about-section"> + <h2>Repository</h2> + <p> + You can find the source code on GitHub: + <a href="https://github.com/minerva-jupiter/geedback" target="_blank" rel="noopener noreferrer"> + minerva-jupiter/geedback + </a> + </p> + </section> + + <section class="about-section"> + <h2>Demo Video</h2> + <div class="video-container"> + <iframe + width="560" + height="315" + src="https://www.youtube.com/embed/8O0NMlRxeOw?si=wnrLWbrW070iwMPI" + title="Geedback Demo" + frameborder="0" + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" + allowfullscreen> + </iframe> + </div> + </section> + + <footer style="margin-top: 40px; font-size: 0.8em; color: #666;"> + &copy; 2026 Minerva_Juppiter + </footer> + </div> + `; + + document.getElementById("back-home")?.addEventListener("click", (e) => { + e.preventDefault(); + window.location.hash = ""; + }); +}; diff --git a/src/main.ts b/src/main.ts @@ -3,68 +3,22 @@ import init, { GeedbackProcessor, Waveform, } from "../geedback-dsp/pkg/geedback_dsp.js"; +import { renderAbout } from "./about"; const app = document.querySelector<HTMLDivElement>("#app")!; -app.innerHTML = ` - <div> - <h1>Multi-Osc WASM Synth</h1> - <button id="start-btn">Start Audio & Sensors</button> - - <canvas id="kaoss-pad"></canvas> - - <div style="margin-top: 20px; padding: 15px; border: 2px solid #ff4444; border-radius: 8px; background: #1a1a1a;"> - <div style="display: flex; justify-content: space-between; margin-bottom: 10px;"> - <strong>DSP Output:</strong> <span id="wasm-output" style="font-family: monospace;">-</span> - </div> - <div style="font-size: 0.8em; color: #888; margin-bottom: 10px;"> - Cutoff (X): <span id="touch-x">-</span> | Resonance (Y): <span id="touch-y">-</span> - </div> - - <div class="osc-controls"> - ${[0, 1, 2] - .map( - (i) => ` - <div style="margin-bottom: 8px;"> - <label>Osc ${i + 1} Wave:</label> - <select id="osc-${i}-wave" class="wave-select"> - <option value="Sine">Sine</option> - <option value="Triangle">Triangle</option> - <option value="TriangleSawtooth">Tri-Saw</option> - <option value="Sawtooth">Sawtooth</option> - <option value="ReverseSawtooth">Rev-Saw</option> - <option value="Square">Square</option> - <option value="WidePulse">Wide Pulse</option> - <option value="NarrowPulse">Narrow Pulse</option> - </select> - </div> - `, - ) - .join("")} - </div> - </div> - - <table border="1" style="margin-top: 20px; width: 100%; border-collapse: collapse; font-size: 0.8em; opacity: 0.7;"> - <tbody id="sensor-table"> - <tr id="row-accel"><td>Accel</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr> - <tr id="row-linear"><td>Linear</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr> - <tr id="row-orient"><td>Orient</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr> - </tbody> - </table> - </div> -`; - -const startBtn = document.querySelector<HTMLButtonElement>("#start-btn")!; -const wasmOutput = document.querySelector<HTMLSpanElement>("#wasm-output")!; -const canvas = document.querySelector<HTMLCanvasElement>("#kaoss-pad")!; -const touchXEl = document.querySelector<HTMLSpanElement>("#touch-x")!; -const touchYEl = document.querySelector<HTMLSpanElement>("#touch-y")!; - let processor: GeedbackProcessor | null = null; let audioCtx: AudioContext | null = null; let isActive = false; let wasmInitialized = false; +// UI Elements (updated in renderHome) +let startBtn: HTMLButtonElement | null = null; +let wasmOutput: HTMLSpanElement | null = null; +let canvas: HTMLCanvasElement | null = null; +let touchXEl: HTMLSpanElement | null = null; +let touchYEl: HTMLSpanElement | null = null; + const updateRow = ( id: string, x: number | null, @@ -73,12 +27,18 @@ const updateRow = ( ) => { const row = document.getElementById(id); if (!row) return; - if (x !== null) - (row.querySelector(".x") as HTMLElement).textContent = x.toFixed(2); - if (y !== undefined && y !== null) - (row.querySelector(".y") as HTMLElement).textContent = y.toFixed(2); - if (z !== undefined && z !== null) - (row.querySelector(".z") as HTMLElement).textContent = z.toFixed(2); + if (x !== null) { + const xEl = row.querySelector(".x"); + if (xEl) xEl.textContent = x.toFixed(2); + } + if (y !== undefined && y !== null) { + const yEl = row.querySelector(".y"); + if (yEl) yEl.textContent = y.toFixed(2); + } + if (z !== undefined && z !== null) { + const zEl = row.querySelector(".z"); + if (zEl) zEl.textContent = z.toFixed(2); + } }; const setupWaveformControls = () => { @@ -86,19 +46,20 @@ const setupWaveformControls = () => { const select = document.getElementById( `osc-${i}-wave`, ) as HTMLSelectElement; + if (!select) return; select.addEventListener("change", (e) => { if (processor) { const val = (e.target as HTMLSelectElement).value; processor.set_waveform(i, (Waveform as any)[val]); } }); - if (i === 0) select.value = "Sine"; - if (i === 1) select.value = "Sine"; - if (i === 2) select.value = "Sine"; + // Restore state if processor already exists + // (Simplified: just default to Sine or use processor state if we had getters) }); }; const drawPad = (x: number, y: number) => { + if (!canvas) return; const ctx = canvas.getContext("2d")!; ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -136,10 +97,11 @@ const drawPad = (x: number, y: number) => { }; const setupKaossPad = () => { + if (!canvas) return; let isTouching = false; const updateTouch = (e: MouseEvent | TouchEvent) => { - if (!isActive) return; + if (!isActive || !canvas) return; const rect = canvas.getBoundingClientRect(); let clientX, clientY; @@ -147,8 +109,8 @@ const setupKaossPad = () => { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { - clientX = e.clientX; - clientY = e.clientY; + clientX = (e as MouseEvent).clientX; + clientY = (e as MouseEvent).clientY; } const x = (clientX - rect.left) / rect.width; @@ -160,8 +122,8 @@ const setupKaossPad = () => { processor.set_touch(clampedX, clampedY); } - touchXEl.textContent = clampedX.toFixed(2); - touchYEl.textContent = clampedY.toFixed(2); + if (touchXEl) touchXEl.textContent = clampedX.toFixed(2); + if (touchYEl) touchYEl.textContent = clampedY.toFixed(2); drawPad(clampedX, clampedY); }; @@ -214,8 +176,6 @@ const startSynth = async () => { await init(); wasmInitialized = true; processor = new GeedbackProcessor(); - setupWaveformControls(); - setupKaossPad(); } catch (e) { alert("WASM failed"); return; @@ -239,12 +199,18 @@ const startSynth = async () => { // Sensor Permissions (iOS) if (typeof (DeviceMotionEvent as any).requestPermission === "function") { - await (DeviceMotionEvent as any).requestPermission(); + try { + await (DeviceMotionEvent as any).requestPermission(); + } catch (e) { + console.error("Permission denied", e); + } } isActive = true; - startBtn.textContent = "Stop Audio & Sensors"; - startBtn.classList.add("active"); + if (startBtn) { + startBtn.textContent = "Stop Audio & Sensors"; + startBtn.classList.add("active"); + } }; const stopSynth = async () => { @@ -253,17 +219,95 @@ const stopSynth = async () => { await audioCtx.close(); audioCtx = null; } - startBtn.textContent = "Start Audio & Sensors"; - startBtn.classList.remove("active"); + if (startBtn) { + startBtn.textContent = "Start Audio & Sensors"; + startBtn.classList.remove("active"); + } }; -startBtn.addEventListener("click", () => { - if (!isActive) { - startSynth(); +const renderHome = () => { + app.innerHTML = ` + <div> + <nav style="text-align: right;"> + <a href="#about">About</a> + </nav> + <h1>Geedback</h1> + <p>Multi-oscillator synthesizer feedback of gravity for Smartphones</p> + <button id="start-btn">${isActive ? "Stop Audio & Sensors" : "Start Audio & Sensors"}</button> + + <canvas id="kaoss-pad"></canvas> + + <div style="margin-top: 20px; padding: 15px; border: 2px solid #ff4444; border-radius: 8px; background: #1a1a1a;"> + <div style="display: flex; justify-content: space-between; margin-bottom: 10px;"> + <strong>DSP Output:</strong> <span id="wasm-output" style="font-family: monospace;">-</span> + </div> + <div style="font-size: 0.8em; color: #888; margin-bottom: 10px;"> + Cutoff (X): <span id="touch-x">-</span> | Resonance (Y): <span id="touch-y">-</span> + </div> + + <div class="osc-controls"> + ${[0, 1, 2] + .map( + (i) => ` + <div style="margin-bottom: 8px;"> + <label>Osc ${i + 1} Wave:</label> + <select id="osc-${i}-wave" class="wave-select"> + <option value="Sine">Sine</option> + <option value="Triangle">Triangle</option> + <option value="TriangleSawtooth">Tri-Saw</option> + <option value="Sawtooth">Sawtooth</option> + <option value="ReverseSawtooth">Rev-Saw</option> + <option value="Square">Square</option> + <option value="WidePulse">Wide Pulse</option> + <option value="NarrowPulse">Narrow Pulse</option> + </select> + </div> + `, + ) + .join("")} + </div> + </div> + + <table border="1" style="margin-top: 20px; width: 100%; border-collapse: collapse; font-size: 0.8em; opacity: 0.7;"> + <tbody id="sensor-table"> + <tr id="row-accel"><td>Accel</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr> + <tr id="row-linear"><td>Linear</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr> + <tr id="row-orient"><td>Orient</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr> + </tbody> + </table> + </div> + `; + + startBtn = document.querySelector<HTMLButtonElement>("#start-btn")!; + wasmOutput = document.querySelector<HTMLSpanElement>("#wasm-output")!; + canvas = document.querySelector<HTMLCanvasElement>("#kaoss-pad")!; + touchXEl = document.querySelector<HTMLSpanElement>("#touch-x")!; + touchYEl = document.querySelector<HTMLSpanElement>("#touch-y")!; + + if (isActive) startBtn.classList.add("active"); + + startBtn.addEventListener("click", () => { + if (!isActive) { + startSynth(); + } else { + stopSynth(); + } + }); + + setupWaveformControls(); + setupKaossPad(); +}; + +const router = () => { + const hash = window.location.hash; + if (hash === "#about") { + renderAbout(app); } else { - stopSynth(); + renderHome(); } -}); +}; + +window.addEventListener("hashchange", router); // Setup global event listeners once window.addEventListener("devicemotion", (e) => { @@ -284,8 +328,11 @@ window.addEventListener("deviceorientation", (e) => { }); const uiLoop = () => { - if (processor && isActive) + if (processor && isActive && wasmOutput) wasmOutput.textContent = processor.get_latest_output().toFixed(6); requestAnimationFrame(uiLoop); }; + +// Start +router(); uiLoop(); diff --git a/src/style.css b/src/style.css @@ -78,6 +78,56 @@ button.active:hover { touch-action: none; /* Only disable scroll on the pad itself */ cursor: crosshair; } + +/* About Page Styles */ +.about-container { + text-align: left; + padding-bottom: 2rem; +} + +.about-section { + margin-bottom: 2rem; +} + +.about-section h2 { + font-size: 1.2em; + border-bottom: 1px solid #444; + padding-bottom: 0.5rem; + margin-top: 1.5rem; +} + +.video-container { + position: relative; + padding-bottom: 56.25%; /* 16:9 */ + height: 0; + overflow: hidden; + max-width: 100%; + border-radius: 8px; + background: #000; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +nav { + margin-bottom: 1rem; +} + +nav a { + color: #646cff; + text-decoration: none; + font-weight: 500; +} + +nav a:hover { + text-decoration: underline; +} + @media (prefers-color-scheme: light) { :root { color: #213547;