feat(playground): add help/intro modal shown on first visit

- Glass-style modal explains NISPS, how learning works, and what to expect
- Documents all controls: touch, keyboard, and gamepad bindings
- Shows automatically on first visit (localStorage flag)
- Dismissible via close button, "Got it", overlay click, or Escape
- Reopenable via ? button at bottom right
This commit is contained in:
w1n5t0n 2026-03-22 22:41:49 +02:00
parent de75a3f0e4
commit 33580f0ae0
3 changed files with 300 additions and 0 deletions

View file

@ -157,6 +157,76 @@
</div> </div>
</div> </div>
<!-- Help button -->
<button class="help-btn" id="help-btn" title="Help">?</button>
<!-- Help modal -->
<div class="help-overlay hidden" id="help-overlay">
<div class="help-modal" id="help-modal">
<button class="help-close" id="help-close">&times;</button>
<h2>Welcome to NISPS</h2>
<p class="help-subtitle">Neural Interactive Shaping of Parameter Spaces</p>
<div class="help-section">
<h3>What is this?</h3>
<p>NISPS lets you <strong>teach a neural network</strong> how to map a 2D joystick position to a rich set of visual or audio parameters. Move the joystick, shape the outputs you want, and the network learns your preferences in real time.</p>
</div>
<div class="help-section">
<h3>How it works</h3>
<p>A small neural network takes your joystick X/Y position as input and produces dozens of output parameters. You teach it by either:</p>
<ul>
<li><strong>Examples</strong> &mdash; set the parameter sliders to what you want at a given joystick position, add the example, then hit Train. Do this a few times from different positions and the network interpolates between them.</li>
<li><strong>Feedback (RL)</strong> &mdash; move the joystick around. If you like what you see/hear, press <strong>+</strong> (keep this). If you don't, press <strong>&minus;</strong> (explore more). The network gradually learns what you prefer.</li>
</ul>
</div>
<div class="help-section">
<h3>What to expect</h3>
<ul>
<li><strong>Moving the joystick</strong> changes all outputs in real time through the network</li>
<li><strong>Randomize</strong> shuffles the network weights &mdash; instant new mapping</li>
<li><strong>Train</strong> fits the network to your saved examples</li>
<li><strong>+ / &minus; feedback</strong> nudges the network: + reinforces the current mapping, &minus; adds exploration noise</li>
<li>Switch between <strong>Visual</strong> (particle flow field) and <strong>Synth</strong> (C15 synthesizer) output modes</li>
<li>The <strong>heatmap bar</strong> at the top shows all output parameters at a glance</li>
</ul>
</div>
<div class="help-section">
<h3>Controls</h3>
<table class="help-keys">
<tr><th colspan="2">Touch / Mouse</th></tr>
<tr><td>Drag joystick</td><td>Move through parameter space</td></tr>
<tr><td>+ / &minus; buttons</td><td>Positive / negative feedback</td></tr>
<tr><td>Bottom bar</td><td>Train, Randomize, Clear, Follow mode</td></tr>
<tr><th colspan="2">Keyboard</th></tr>
<tr><td><kbd>1</kbd></td><td>Negative feedback (&minus;)</td></tr>
<tr><td><kbd>2</kbd></td><td>Positive feedback (+)</td></tr>
<tr><th colspan="2">Gamepad (Steam Deck, Xbox, etc.)</th></tr>
<tr><td>Left stick</td><td>Joystick control</td></tr>
<tr><td>LB (left bumper)</td><td>Negative feedback</td></tr>
<tr><td>RB (right bumper)</td><td>Positive feedback</td></tr>
<tr><td>A</td><td>Train</td></tr>
<tr><td>X</td><td>Randomize</td></tr>
<tr><td>B</td><td>Clear examples</td></tr>
</table>
</div>
<div class="help-section">
<h3>Tips</h3>
<ul>
<li>Start with <strong>Randomize</strong> a few times to hear/see different mappings, then use feedback to refine</li>
<li>Use <strong>Follow</strong> mode to let the joystick wander automatically while you give feedback</li>
<li>In Synth mode, press the <strong>play button</strong> (top left) to start audio, then explore</li>
<li>Tap a heatmap cell to see which parameter it controls</li>
</ul>
</div>
<button class="help-got-it" id="help-got-it">Got it</button>
</div>
</div>
<script type="module" src="js/a-app.js"></script> <script type="module" src="js/a-app.js"></script>
</body> </body>
</html> </html>

View file

@ -1093,6 +1093,216 @@ html, body {
border-radius: 2px; border-radius: 2px;
} }
/* ---- Help button ---- */
.help-btn {
position: fixed;
bottom: 96px;
right: 12px;
z-index: 45;
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--glass-border);
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
color: var(--text-dim);
font-size: 16px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
padding: 0;
line-height: 1;
}
.help-btn:hover, .help-btn:active {
border-color: var(--accent);
color: var(--accent);
}
/* ---- Help modal ---- */
.help-overlay {
position: fixed;
inset: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
animation: helpFadeIn 0.2s ease-out;
}
.help-overlay.hidden {
display: none;
}
@keyframes helpFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.help-modal {
position: relative;
max-width: 520px;
width: 100%;
max-height: 85vh;
overflow-y: auto;
background: rgba(18, 18, 18, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius);
padding: 28px 24px 20px;
color: var(--text);
}
.help-modal h2 {
font-size: 18px;
font-weight: 700;
margin: 0 0 2px;
color: #fff;
}
.help-subtitle {
font-size: 11px;
color: var(--text-dim);
margin: 0 0 16px;
letter-spacing: 0.5px;
}
.help-close {
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid var(--glass-border);
background: rgba(255, 255, 255, 0.06);
color: var(--text-dim);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
transition: all 0.15s;
}
.help-close:hover {
border-color: var(--accent);
color: var(--accent);
}
.help-section {
margin-bottom: 16px;
}
.help-section h3 {
font-size: 13px;
font-weight: 700;
color: var(--accent);
margin: 0 0 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.help-section p {
font-size: 12px;
line-height: 1.5;
margin: 0 0 6px;
color: var(--text);
}
.help-section ul {
margin: 0;
padding-left: 18px;
font-size: 12px;
line-height: 1.6;
color: var(--text);
}
.help-section li {
margin-bottom: 2px;
}
.help-keys {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.help-keys th {
text-align: left;
padding: 8px 0 4px;
color: var(--text-dim);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.help-keys td {
padding: 3px 0;
color: var(--text);
}
.help-keys td:first-child {
color: #fff;
font-weight: 500;
width: 45%;
}
.help-keys kbd {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.06);
font-family: inherit;
font-size: 11px;
}
.help-got-it {
display: block;
width: 100%;
padding: 10px;
margin-top: 8px;
border-radius: 6px;
border: 1px solid rgba(255, 106, 0, 0.3);
background: rgba(255, 106, 0, 0.1);
color: var(--accent);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.help-got-it:active {
background: rgba(255, 106, 0, 0.25);
}
/* Help modal scrollbar */
.help-modal::-webkit-scrollbar {
width: 4px;
}
.help-modal::-webkit-scrollbar-track {
background: transparent;
}
.help-modal::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
/* ---- Media queries ---- */ /* ---- Media queries ---- */
@media (min-width: 768px) { @media (min-width: 768px) {
.joystick-container { .joystick-container {

View file

@ -620,6 +620,7 @@ function init() {
wireKeyboard(); wireKeyboard();
wireQuickPlayControls(); wireQuickPlayControls();
wireGroupDrawer(); wireGroupDrawer();
wireHelp();
// Resize // Resize
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);
@ -1805,6 +1806,25 @@ function onResize() {
} }
} }
// ---- Help modal ----
function wireHelp() {
const overlay = document.getElementById('help-overlay');
const btnOpen = document.getElementById('help-btn');
const btnClose = document.getElementById('help-close');
const btnGotIt = document.getElementById('help-got-it');
function show() { overlay.classList.remove('hidden'); }
function hide() { overlay.classList.add('hidden'); localStorage.setItem('nisps-help-seen', '1'); }
btnOpen.addEventListener('click', show);
btnClose.addEventListener('click', hide);
btnGotIt.addEventListener('click', hide);
overlay.addEventListener('click', (e) => { if (e.target === overlay) hide(); });
// Show on first visit
if (!localStorage.getItem('nisps-help-seen')) show();
}
// ---- Animation ---- // ---- Animation ----
function animate() { function animate() {
if (gamepad) gamepad.poll(); if (gamepad) gamepad.poll();