SOURCE / PINNED RELEASE
Made of little things.
Safe Sand · Material studies
- Release
- 9101a9cd9fb4…
- Author-recorded commit
- 75d98e1b8edb…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-7682e55a791
Archive hash verified: 6c8ddabeb31100e1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Safe Sand · Material studies</title>
</head>
<body>
<main id="app" data-surface="light">
<div id="scene" class="scene">
<canvas id="sand" tabindex="0" aria-label="Granular playground. Drag to push grains. Use arrow keys or WASD to tilt, Space to shake, P to pause, R to pour.">This playground needs WebGL2. Use the material and tray controls to explore grains.</canvas>
</div>
<div class="specimen" aria-live="polite">
<span id="materialTrait" class="overline">FINE · GOLDEN</span>
<h1 id="materialName">Beach</h1>
<p id="materialHint">Drag a finger through the grains.</p>
</div>
<button id="materialsButton" class="glass materials-toggle" aria-haspopup="dialog"><span id="currentSwatch" class="mini-swatch"></span><span>Materials</span><span class="count">11</span></button>
<div id="stateBadge" class="state-badge" hidden>Paused</div>
<div class="dock-wrap">
<output id="status" role="status" aria-live="polite"></output>
<div class="dock glass" aria-label="Tray tools">
<button id="pourButton" class="primary" title="Refill the whole tray (R)"><span aria-hidden="true">↻</span> Refill</button>
<button id="pauseButton" title="Pause simulation (P)" aria-pressed="false"><span id="pauseIcon" aria-hidden="true">Ⅱ</span><span id="pauseText">Pause</span></button>
<span class="divider"></span>
<button id="toolButton" title="Choose a tool" aria-expanded="false" aria-controls="toolsPanel"><span aria-hidden="true">◎</span><span id="toolText">Push</span><span class="chevron" aria-hidden="true">⌄</span></button>
<button id="cameraButton" title="Rotate the view" aria-expanded="false" aria-controls="cameraPanel"><span aria-hidden="true">◇</span> View</button>
<button id="tiltButton" aria-expanded="false" aria-controls="tiltPanel"><span aria-hidden="true">⤢</span> Tilt</button>
<button id="shakeButton" title="Shake the tray (Space)"><span aria-hidden="true">≋</span> Shake</button>
<button id="soundButton" class="icon-button" title="Turn sound on" aria-label="Turn sound on" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9h4l5-4v14l-5-4H4zM17 9l4 6m0-6-4 6"/></svg></button>
</div>
<p class="keyboard-hint">DRAG TO SCULPT <span>·</span> ARROWS TO TILT <span>·</span> SPACE TO SHAKE</p>
</div>
<section id="tiltPanel" class="tilt-panel glass" aria-label="Tilt controls" hidden>
<div class="panel-heading"><span class="overline">TILT THE TRAY</span><button id="closeTilt" class="icon-button" aria-label="Close tilt controls">×</button></div>
<div class="tilt-body"><div id="tiltPad" role="group" tabindex="0" aria-label="Tilt pad. Drag in a direction, release to level. Arrow keys also work."><span id="tiltDot"></span><span class="pad-lines" aria-hidden="true"></span></div><p>Drag to tilt.<br>Release to level.<br><small>Or hold arrow keys.</small></p></div>
</section>
<section id="toolsPanel" class="floating-panel glass" aria-label="Drawing tools" hidden>
<div class="panel-heading"><span class="overline">MAKE YOUR MARK</span><button id="closeTools" class="icon-button" aria-label="Close drawing tools">×</button></div>
<div class="tool-grid">
<button data-tool="push" aria-pressed="true"><b>◎</b><span>Push</span><small>Move grains</small></button>
<button data-tool="pour" aria-pressed="false"><b>⁙</b><span>Pour</span><small>Add grains</small></button>
<button data-tool="rake" aria-pressed="false"><b>⋔</b><span>Rake</span><small>Five fine lines</small></button>
<button data-tool="erase" aria-pressed="false"><b>◌</b><span>Erase</span><small>Clear a path</small></button>
<button data-tool="magnet" aria-pressed="false"><b>⊕</b><span>Magnet</span><small>Attract iron</small></button>
<button data-tool="repel" aria-pressed="false"><b>⊖</b><span>Repel</span><small>Scatter iron</small></button>
</div>
<p class="tool-help">Magnet and Repel work with iron filings. Select them in Materials.</p>
<label class="setting-row" for="pourRate"><span>Pour rate</span><input id="pourRate" type="range" min="2000" max="24000" step="1000" value="12000" /><output id="pourRateValue">12k/s</output></label>
<label class="setting-row" for="pourHeight"><span>Drop height</span><input id="pourHeight" type="range" min="2" max="30" value="12" /><output id="pourHeightValue">12</output></label>
<label class="setting-row" for="speed"><span>Time</span><select id="speed"><option value="1">Normal</option><option value="0.5">Half speed</option><option value="0.25">Quarter speed</option></select></label>
<div class="panel-actions"><button id="undoButton" title="Undo the last stroke, refill or material change (U)">↶ Undo</button><button id="stepButton" disabled>Step one frame</button></div>
</section>
<section id="cameraPanel" class="floating-panel camera-panel glass" aria-label="View controls" hidden>
<div class="panel-heading"><span class="overline">A DIFFERENT ANGLE</span><button id="closeCamera" class="icon-button" aria-label="Close view controls">×</button></div>
<p class="tool-help">Look across the piles. The camera stays above the tray.</p>
<label class="setting-row" for="cameraPitch"><span>Lean</span><input id="cameraPitch" type="range" min="-35" max="35" value="0" /><output id="pitchValue">0°</output></label>
<label class="setting-row" for="cameraYaw"><span>Turn</span><input id="cameraYaw" type="range" min="-35" max="35" value="0" /><output id="yawValue">0°</output></label>
<div class="panel-actions"><button id="overheadButton">Overhead</button><button id="angledButton">Angled view</button><button id="orbitButton">Drag to orbit</button></div>
<p class="tool-help">Keyboard: focus the canvas, press 7 to orbit or 1 to push. Camera movement also works while paused.</p>
</section>
<dialog id="materialsDialog" aria-labelledby="materialsHeading">
<div class="dialog-heading"><div><span class="overline">MATERIAL STUDIES</span><h2 id="materialsHeading">Pick something to feel.</h2></div><button id="closeMaterials" class="icon-button" aria-label="Close materials">×</button></div>
<div class="dialog-scroll">
<p class="section-label">THE ORIGINAL SANDS</p><div id="originalMaterials" class="material-grid"></div>
<p class="section-label">SOMETHING DIFFERENT</p><div id="newMaterials" class="material-grid"></div>
<p id="materialDescription" class="description"></p>
<div class="setting-row"><span>Under the grains</span><div id="surfaces" class="segmented" aria-label="Tray surface"><button data-surface="light" aria-pressed="true">Porcelain</button><button data-surface="slate" aria-pressed="false">Slate</button><button data-surface="dark" aria-pressed="false">Ink</button></div></div>
<label class="setting-row" for="brush"><span>Brush width</span><input id="brush" type="range" min="2" max="10" step="0.5" value="4.5" /><output id="brushValue" for="brush">4.5</output></label>
<label class="setting-row" for="amount"><span>Next pour</span><select id="amount"><option value="0.35">A little</option><option value="0.8" selected>A handful</option><option value="1">A full tray</option></select></label>
<div class="save-row"><button id="saveButton">Save setup</button><button id="restoreButton">Restore setup</button><button id="clearButton">Empty tray</button></div>
<p id="storageStatus" class="footnote">Setup saves use this host's private storage.</p>
<details><summary>How to play & credits</summary><p>Drag to sculpt with Push, Pour, Rake or Erase. Iron filings support Magnet and Repel. View rotates the camera up to 35° in each direction; Tilt changes the physical slope of the tray.</p><p>Focus the canvas: arrows / WASD tilt; Space shakes; P pauses; R refills; U undoes; V toggles the angled view; hold F with Pour to pour at the cursor; keys 1–7 select Push, Pour, Rake, Erase, Magnet, Repel and Orbit. Slow motion and single-step are in the tool panel.</p><p>Changing material refills the tray. The Pour tool adds the current material; at capacity it recycles the oldest grain slots. Undo restores the state before the last stroke or refill. Setup saves remember controls, not individual grains.</p><p>Based on Safe Sand by av. Eleven tactile material approximations, simulated and lit on your GPU. Phone sensors are replaced by the tilt pad in this sandbox.</p></details>
</div>
<button id="returnButton" class="return-button">Back to the grains <span aria-hidden="true">↗</span></button>
</dialog>
<section id="errorPanel" class="error-panel" role="alert" hidden><h2 id="errorTitle">The tray couldn’t start</h2><p id="errorMessage"></p><button id="retryButton">Try again</button></section>
<div class="small-notice"><span aria-hidden="true">⤢</span><strong>A little more room.</strong><p>Enlarge this napplet to at least 280 × 240 to play. Your tray will stay here.</p></div>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
