Add core-powered web sculpting studio with tablet controls

Add pressure-aware worker sculpting, bounded shared input, project files and history, and compact icon navigation with an XYZ orbit gizmo.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-09-28 08:53:50 +00:00
co-authored by heaust
parent eb8aa0e9dc
commit 347b676cf1
25 changed files with 5468 additions and 1 deletions
+472
View File
@@ -0,0 +1,472 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,viewport-fit=cover"
/>
<meta name="theme-color" content="#20242b" />
<meta name="description" content="A quiet space to sculpt with your pen." />
<title>Sculpt — Yawn</title>
<link
rel="stylesheet"
href="/assets/sculpt/vendor/basecoat/basecoat.min.css"
/>
<link rel="stylesheet" href="/assets/sculpt/styles.css" />
<script type="importmap">
{ "imports": { "@yawn/core": "/pkg/core.js" } }
</script>
<script type="module" src="/assets/sculpt/app.js"></script>
</head>
<body>
<main id="viewport" tabindex="0" aria-label="Sculpting canvas">
<canvas aria-label="Your sculpt"></canvas>
<div id="cursor" hidden aria-hidden="true"><i></i></div>
<div id="loading" class="canvas-message" role="status">
<h1>Preparing your clay.</h1>
<p>A moment to get ready.</p>
</div>
<div id="error" class="canvas-message" role="alert" hidden>
<h1>Your sculpt is safe.</h1>
<p id="error-detail"></p>
<button id="retry" class="btn">Try again</button>
</div>
</main>
<nav class="tool-rail" aria-label="Sculpt tools">
<div class="brushes" role="group" aria-label="Brush">
<button
class="btn icon-button"
data-variant="ghost"
data-brush="draw"
aria-pressed="true"
aria-label="Draw"
title="Draw · 1"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m14 4 6 6M5 15 16 4a2.8 2.8 0 0 1 4 4L9 19l-5 1z" />
</svg>
</button>
<button
class="btn icon-button"
data-variant="ghost"
data-brush="inflate"
aria-pressed="false"
aria-label="Inflate"
title="Inflate · 2"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="8" />
<path d="M12 7v10M7 12h10" />
</svg>
</button>
<button
class="btn icon-button"
data-variant="ghost"
data-brush="smooth"
aria-pressed="false"
aria-label="Smooth"
title="Smooth · 3"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M3 8c6-7 12 7 18 0M3 12c6-7 12 7 18 0M3 16c6-7 12 7 18 0"
/>
</svg>
</button>
<button
class="btn icon-button"
data-variant="ghost"
data-brush="flatten"
aria-pressed="false"
aria-label="Flatten"
title="Flatten · 4"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m3 16 5-7h13l-5 7zM5 20h14M12 2v4m-2-2 2 2 2-2" />
</svg>
</button>
<button
class="btn icon-button"
data-variant="ghost"
data-brush="grab"
aria-pressed="false"
aria-label="Grab"
title="Grab · 5"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M8 12V5a2 2 0 0 1 4 0v7-8a2 2 0 0 1 4 0v8-5a2 2 0 0 1 4 0v9c0 4-3 6-7 6-3 0-5-2-6-4l-3-5c-1-2 2-3 3-1l1 1"
/>
</svg>
</button>
</div>
<div class="rail-group">
<button
id="tools-toggle"
class="btn icon-button launcher"
data-variant="ghost"
aria-label="Brush settings"
title="Brush settings"
aria-expanded="false"
aria-controls="tools-panel"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h16M4 17h16M8 4v6M16 14v6" />
</svg>
</button>
<button
id="undo"
class="btn icon-button"
data-variant="ghost"
aria-label="Undo"
title="Undo"
disabled
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m8 4-5 5 5 5M3 9h10a7 7 0 0 1 0 14" />
</svg>
</button>
<button
id="redo"
class="btn icon-button"
data-variant="ghost"
aria-label="Redo"
title="Redo"
disabled
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m16 4 5 5-5 5M21 9H11a7 7 0 0 0 0 14" />
</svg>
</button>
</div>
<button
id="settings-toggle"
class="btn icon-button launcher"
data-variant="ghost"
aria-label="Settings and projects"
title="Settings and projects"
aria-expanded="false"
aria-controls="inspector"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 3h6l1 3 3 1 2 5-2 5-3 1-1 3H9l-1-3-3-1-2-5 2-5 3-1z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</nav>
<div id="view-navigation" aria-label="View navigation">
<div
id="orbit-gizmo"
data-nav="orbit"
tabindex="0"
role="group"
aria-label="Orbit view. Drag to rotate, or use arrow keys."
title="Drag to orbit · click an axis to align"
>
<svg viewBox="0 0 104 104" aria-hidden="true">
<line class="axis-line" x1="52" y1="52" />
<line class="axis-line" x1="52" y1="52" />
<line class="axis-line" x1="52" y1="52" />
</svg>
</div>
<div class="view-buttons">
<button
class="btn icon-button"
data-variant="ghost"
data-nav="pan"
aria-label="Pan view. Drag or use arrow keys."
title="Drag to pan"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 3v18M3 12h18m-12-6 3-3 3 3m-6 12 3 3 3-3M6 9l-3 3 3 3m12-6 3 3-3 3"
/>
</svg>
</button>
<button
class="btn icon-button"
data-variant="ghost"
data-nav="zoom"
aria-label="Zoom view. Drag up or down, or use arrow keys."
title="Drag to zoom"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="10" cy="10" r="6" />
<path d="m15 15 6 6M10 7v6M7 10h6" />
</svg>
</button>
<button
id="frame"
class="btn icon-button"
data-variant="ghost"
aria-label="Center view"
title="Center view · F"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 3H3v5m13-5h5v5M3 16v5h5m8 0h5v-5" />
<circle cx="12" cy="12" r="4" />
</svg>
</button>
</div>
</div>
<section
id="tools-panel"
class="panel tools-panel"
aria-label="Brush settings"
hidden
>
<div class="panel-heading">
<h2 id="brush-title">Draw</h2>
<button
class="btn"
data-variant="ghost"
data-size="icon-sm"
data-close
aria-label="Close brushes"
>
×
</button>
</div>
<div class="control-section">
<label for="radius"
>Size <output id="radius-value" for="radius"></output></label
><input
class="input"
id="radius"
type="range"
min="0.04"
max="0.6"
step="0.01"
value="0.22"
/>
<label for="strength"
>Strength <output id="strength-value" for="strength"></output></label
><input
class="input"
id="strength"
type="range"
min="0"
max="1"
step="0.01"
value="0.45"
/>
<label class="toggle-row" for="invert"
><span>Subtract</span
><input class="input" role="switch" id="invert" type="checkbox"
/></label>
</div>
</section>
<span id="modified" class="modified-label" hidden>Unsaved</span>
<aside
id="inspector"
class="panel settings-panel"
aria-label="Settings and projects"
hidden
>
<div class="panel-heading">
<h2>Settings</h2>
<button
class="btn"
data-variant="ghost"
data-size="icon-sm"
data-close
aria-label="Close settings"
>
×
</button>
</div>
<p id="document-name" class="project-name">Untitled sphere</p>
<div class="project-actions">
<button id="save" class="btn">Download project</button
><button class="btn" data-variant="outline" id="open">Open</button
><button class="btn" data-variant="outline" id="new">New</button
><button class="btn" data-variant="outline" id="export">Export</button>
</div>
<label class="toggle-row palm-row" for="palm-rejection"
><span>Palm rejection<small>Ignore touch on the canvas</small></span
><input
class="input"
role="switch"
id="palm-rejection"
type="checkbox"
checked
/></label>
<details id="pen-settings">
<summary>Pen response</summary>
<div class="disclosure-content">
<label for="gain"
>Sensitivity <output id="gain-value" for="gain"></output></label
><input
class="input"
id="gain"
type="range"
min="0.25"
max="2"
step="0.05"
value="1"
/>
<label for="gamma"
>Response <output id="gamma-value" for="gamma"></output></label
><input
class="input"
id="gamma"
type="range"
min="0.25"
max="3"
step="0.05"
value="1"
/>
<div class="pressure-meter" aria-label="Pen pressure">
<div id="pressure-fill"></div>
</div>
<span id="pen-state" class="hint">Try your pen on the canvas</span>
<svg
id="curve"
viewBox="0 0 200 54"
role="img"
aria-label="Pressure response"
>
<path class="curve-grid" d="M0 53H200M0 27H200M100 0V54" />
<path id="curve-line" d="M0 53L200 1" />
</svg>
<label class="toggle-row" for="pressureRadius"
><span>Pressure changes size</span
><input
class="input"
role="switch"
id="pressureRadius"
type="checkbox"
/></label>
<button id="reset-pressure" class="btn wide" data-variant="outline">
Reset pen settings
</button>
</div>
</details>
<details>
<summary>Sculpt &amp; display</summary>
<div class="disclosure-content">
<label class="toggle-row" for="symmetry"
><span>Mirror left &amp; right</span
><input
class="input"
role="switch"
id="symmetry"
type="checkbox"
checked
/></label>
<label for="quality">Display</label
><select class="select" id="quality">
<option value="economy">Battery saver</option>
<option value="balanced" selected>Balanced</option>
<option value="detail">Sharper</option>
</select>
<div class="swatches" role="group" aria-label="Clay color">
<button
class="btn"
data-variant="ghost"
data-color="0.5,0.3,0.2"
style="--swatch: #bb8d70"
aria-label="Terracotta clay"
aria-pressed="true"
></button>
<button
class="btn"
data-variant="ghost"
data-color="0.42,0.48,0.48"
style="--swatch: #aab9b7"
aria-label="Stone clay"
aria-pressed="false"
></button>
<button
class="btn"
data-variant="ghost"
data-color="0.21,0.32,0.43"
style="--swatch: #718fa9"
aria-label="Slate clay"
aria-pressed="false"
></button>
</div>
</div>
</details>
<div class="settings-footer">
<button id="fullscreen" class="btn" data-variant="ghost" hidden>
Full screen</button
><button id="help" class="btn" data-variant="ghost">Help</button>
</div>
</aside>
<div
id="status"
class="status-toast"
role="status"
aria-live="polite"
></div>
<input
id="file"
type="file"
accept=".yawn-sculpt,application/json"
hidden
/>
<dialog id="new-dialog" class="dialog" aria-labelledby="new-title">
<form method="dialog">
<header>
<h2 id="new-title">Fresh clay</h2>
<p id="new-warning">Choose how much detail you want.</p>
</header>
<section>
<label for="density">Detail</label
><select class="select" id="density">
<option value="4">Simple</option>
<option value="5" selected>Balanced</option>
<option value="6">Fine</option>
</select>
<p class="hint">Simple feels lighter. Fine allows smaller details.</p>
</section>
<footer>
<button class="btn" data-variant="outline" value="cancel">
Cancel</button
><button value="create" class="btn">Create sphere</button>
</footer>
</form>
</dialog>
<dialog id="help-dialog" class="dialog" aria-labelledby="help-title">
<form method="dialog">
<header>
<h2 id="help-title">Make something.</h2>
<p>Press lightly for a soft touch, harder for a deeper stroke.</p>
</header>
<section>
<dl>
<dt>Find your tools</dt>
<dd>
Choose a brush in the left toolbar. The sliders button opens brush
settings; the gear opens projects and settings.
</dd>
<dt>Move around</dt>
<dd>
Drag the colored axis gizmo to orbit. Click an axis for a
straight-on view. Drag the pan or magnifier icons to move or zoom.
Arrow keys work when these controls are focused. Escape cancels a
drag. The scene uses Y as its up axis.
</dd>
<dt>Rest your hand</dt>
<dd>
Palm rejection is on. Turn it off in Settings to draw with a
finger or move with two fingers.
</dd>
<dt>Keep your work</dt>
<dd>
Download your project before leaving. Open it here to keep
sculpting later.
</dd>
<dt>With a mouse</dt>
<dd>Right drag to rotate, middle drag to pan, scroll to zoom.</dd>
</dl>
</section>
<footer><button value="close" class="btn">Got it</button></footer>
</form>
</dialog>
</body>
</html>