Add pastel presets, piece selection and colors, mask brushes, subdivision and conservative decimation with bounded worker operations and mixed undo history. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
616 lines
20 KiB
HTML
616 lines
20 KiB
HTML
<!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="empty-clay" class="canvas-message" hidden>
|
||
<h1>A fresh canvas.</h1>
|
||
<p>Add a piece of clay to begin.</p>
|
||
<button id="add-empty" class="btn">Add clay</button>
|
||
</div>
|
||
<div id="clay-progress" class="canvas-message" role="status" hidden>
|
||
<h1 id="operation-label">Updating clay…</h1>
|
||
<p>Your other pieces stay unchanged.</p>
|
||
<button id="cancel-operation" class="btn">Cancel</button>
|
||
</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">
|
||
<button
|
||
id="pieces-toggle"
|
||
class="btn icon-button launcher"
|
||
aria-label="Clay pieces"
|
||
title="Clay pieces"
|
||
aria-expanded="false"
|
||
aria-controls="pieces-panel"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="m12 3 9 5-9 5-9-5zm-9 9 9 5 9-5M3 16l9 5 9-5" />
|
||
</svg>
|
||
</button>
|
||
<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>
|
||
<button
|
||
class="btn icon-button"
|
||
data-brush="mask"
|
||
aria-pressed="false"
|
||
aria-label="Mask"
|
||
title="Mask · 6"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="M4 5h16v8c0 5-8 8-8 8s-8-3-8-8zM12 5v16" />
|
||
<path d="M6 8h4M6 11h4M7 14h3" />
|
||
</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 id="invert-label">Subtract</span
|
||
><input class="input" role="switch" id="invert" type="checkbox"
|
||
/></label>
|
||
</div>
|
||
</section>
|
||
|
||
<section
|
||
id="pieces-panel"
|
||
class="panel pieces-panel"
|
||
aria-label="Clay pieces"
|
||
hidden
|
||
>
|
||
<div class="panel-heading">
|
||
<h2>Clay</h2>
|
||
<button class="btn" data-close aria-label="Close clay pieces">×</button>
|
||
</div>
|
||
<div id="piece-list" aria-label="Select a piece"></div>
|
||
<div class="piece-material">
|
||
<label for="piece-color">Color</label
|
||
><input id="piece-color" type="color" aria-label="Piece color" />
|
||
<button
|
||
id="delete-piece"
|
||
class="btn"
|
||
aria-label="Delete selected piece"
|
||
title="Delete selected piece"
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
<div class="preset-grid" aria-label="Add clay preset">
|
||
<button
|
||
class="btn"
|
||
data-preset="sphere"
|
||
aria-label="Add sphere"
|
||
title="Add sphere"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="9" />
|
||
<ellipse cx="12" cy="12" rx="4" ry="9" />
|
||
<path d="M3 12h18" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
data-preset="cube"
|
||
aria-label="Add cube"
|
||
title="Add cube"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="m12 2 9 5v10l-9 5-9-5V7zm0 10 9-5M12 12 3 7m9 5v10" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
data-preset="cylinder"
|
||
aria-label="Add cylinder"
|
||
title="Add cylinder"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<ellipse cx="12" cy="5" rx="8" ry="3" />
|
||
<path d="M4 5v14c0 4 16 4 16 0V5" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
data-preset="cone"
|
||
aria-label="Add cone"
|
||
title="Add cone"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="m4 18 8-16 8 16" />
|
||
<ellipse cx="12" cy="18" rx="8" ry="3" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
data-preset="torus"
|
||
aria-label="Add torus"
|
||
title="Add torus"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<ellipse cx="12" cy="12" rx="10" ry="7" />
|
||
<ellipse cx="12" cy="10" rx="4" ry="2" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<details class="disclosure">
|
||
<summary>Detail & mask</summary>
|
||
<div class="piece-actions">
|
||
<button id="subdivide" class="btn">Subdivide</button
|
||
><button id="decimate" class="btn">Decimate</button>
|
||
<button id="clear-mask" class="btn">Clear mask</button
|
||
><button id="invert-mask" class="btn">Invert mask</button>
|
||
</div>
|
||
<p class="hint">
|
||
Subdivide adds resolution. Decimate reduces detail and preserves
|
||
masked areas.
|
||
</p>
|
||
</details>
|
||
</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 & display</summary>
|
||
<div class="disclosure-content">
|
||
<label class="toggle-row" for="symmetry"
|
||
><span>Mirror left & 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>Build with clay</dt>
|
||
<dd>
|
||
Open Clay at the top of the toolbar. Add a preset, select its row,
|
||
then change its color or delete it. Each new piece gets a pastel
|
||
color.
|
||
</dd>
|
||
<dt>Protect and refine</dt>
|
||
<dd>
|
||
Mask paints protected areas; use Erase mask in brush settings to
|
||
remove it. Clay → Detail & mask contains Clear, Invert,
|
||
Subdivide and Decimate. Subdivide adds resolution without
|
||
smoothing. Decimate preserves masked areas and may reduce less
|
||
than half.
|
||
</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>
|