Files
psxbase/tools/web/index.html
T
ryanfitzpatrickio 8a96ede9f2 Initial public release of PSX Adventure Engine
Browser reference stack for PSX-era third-person adventure: fixed cameras,
inventory puzzles, Box3D physics, host-authoritative P2P co-op, and a modular
character harness. Ships the Ashgrove Precinct Level 1 investigation demo with
a full cast and nine linked rooms.
2026-07-31 06:32:43 -05:00

214 lines
9.0 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PSX Character Creator</title>
<link rel="stylesheet" href="/src/style.css" />
</head>
<body>
<div id="app">
<aside class="panel panel--left" id="controls">
<header class="panel__header">
<h1>Character Creator</h1>
<p class="panel__sub">body · parts · palette → GLB</p>
</header>
<section class="section">
<h2>Preset</h2>
<label class="field">
<span>Load cast recipe</span>
<select id="preset"></select>
</label>
<div class="row">
<label class="field grow">
<span>Name</span>
<input id="name" type="text" autocomplete="off" />
</label>
<label class="field grow">
<span>Id</span>
<input id="id" type="text" autocomplete="off" spellcheck="false" />
</label>
</div>
</section>
<section class="section">
<h2>Body</h2>
<div class="seg" id="body" role="group" aria-label="Body type">
<button type="button" data-body="male" class="seg__btn is-active">Male</button>
<button type="button" data-body="female" class="seg__btn">Female</button>
</div>
<label class="field">
<span>Height <em id="height-label">1.72 m</em></span>
<input id="height" type="range" min="1.45" max="2.05" step="0.01" value="1.72" />
</label>
<div class="body-style" id="body-style" aria-label="Physique">
<label class="field field--slider">
<span>Mass <em id="mass-label">0.00</em></span>
<div class="slider-ends"><i>Lean</i><i>Heavy</i></div>
<input id="mass" type="range" min="-1" max="1" step="0.01" value="0" />
</label>
<label class="field field--slider">
<span>Muscle <em id="muscle-label">0.00</em></span>
<div class="slider-ends"><i>Soft</i><i>Muscular</i></div>
<input id="muscle" type="range" min="-1" max="1" step="0.01" value="0" />
</label>
<label class="field field--slider">
<span>Fat <em id="fat-label">0.00</em></span>
<div class="slider-ends"><i>Base</i><i>Heavy</i></div>
<input id="fat" type="range" min="0" max="1" step="0.01" value="0" />
</label>
<button type="button" class="btn btn--ghost btn--small" id="body-style-reset" title="Reset physique to average">
Reset physique
</button>
</div>
<label class="field">
<span>Skin tone</span>
<div class="swatches" id="skin-swatches"></div>
</label>
</section>
<section class="section">
<h2>Face</h2>
<p class="hint">
Male and female bases already have their own skulls; these reshape
whichever one is selected.
</p>
<div class="body-style" id="head-style" aria-label="Face">
<label class="field field--slider">
<span>Face length <em id="face-length-label">0.00</em></span>
<div class="slider-ends"><i>Round</i><i>Long</i></div>
<input id="face-length" type="range" min="-1" max="1" step="0.01" value="0" />
</label>
<label class="field field--slider">
<span>Jaw <em id="face-jaw-label">0.00</em></span>
<div class="slider-ends"><i>Tapered</i><i>Square</i></div>
<input id="face-jaw" type="range" min="-1" max="1" step="0.01" value="0" />
</label>
<label class="field field--slider">
<span>Brow <em id="face-brow-label">0.00</em></span>
<div class="slider-ends"><i>Soft</i><i>Heavy</i></div>
<input id="face-brow" type="range" min="-1" max="1" step="0.01" value="0" />
</label>
<button type="button" class="btn btn--ghost btn--small" id="head-style-reset" title="Reset face to the neutral head for this body">
Reset face
</button>
</div>
</section>
<section class="section">
<h2>Parts</h2>
<div id="parts"></div>
</section>
<section class="section">
<h2>Palette</h2>
<div class="palette" id="palette"></div>
</section>
<section class="section">
<h2>Texture</h2>
<p class="hint">
Unwrap UVs, bake a guide sheet, then paint albedo with Grok Imagine
(dev server). Results are cached by prompt — the prompt doesn't
include the mesh, so after reshaping a body or face you need
Regenerate to see it repainted.
</p>
<label class="field">
<span>Wardrobe / style notes</span>
<textarea
id="style-notes"
rows="3"
spellcheck="false"
placeholder="charcoal three-piece business suit, white shirt, burgundy tie, polished black oxfords"
></textarea>
</label>
<p class="hint">
Steers colour, material and detailing only — the cut is fixed by the
equipped parts, so pick a suit-shaped upper first. Editing this
invalidates the cached albedo by itself.
</p>
<div class="actions">
<button type="button" class="btn" id="uv-guide" title="Cylindrical unwrap + UV guide PNG">
Bake UV guide
</button>
<button type="button" class="btn btn--accent" id="texture-grok" title="UV guide → Grok Imagine → textured GLB (uses the cached albedo if there is one)">
Texture with Grok
</button>
<button type="button" class="btn" id="texture-regen" title="Repaint with Grok, ignoring and overwriting the cached albedo — costs a fresh generation">
Regenerate texture
</button>
<button type="button" class="btn" id="albedo-import" title="Apply an albedo PNG/JPEG you painted yourself — no Grok, no cost">
Import albedo…
</button>
<input id="albedo-file" type="file" accept="image/png,image/jpeg" hidden />
</div>
<p class="hint">
Import applies any image painted over the UV guide, so you can use a
different generator or paint it by hand.
</p>
<div class="tex-previews" id="tex-previews" hidden>
<figure class="tex-card">
<figcaption>UV guide</figcaption>
<a id="uv-guide-link" href="#" target="_blank" rel="noreferrer">
<img id="uv-guide-img" alt="UV guide" />
</a>
</figure>
<figure class="tex-card" id="albedo-card" hidden>
<figcaption>Albedo</figcaption>
<a id="albedo-link" href="#" target="_blank" rel="noreferrer">
<img id="albedo-img" alt="Albedo texture" />
</a>
</figure>
</div>
<p class="status" id="tex-status" aria-live="polite"></p>
</section>
<section class="section section--actions">
<h2>Export</h2>
<div class="actions">
<button type="button" class="btn btn--primary" id="download-glb">Download GLB</button>
<button type="button" class="btn" id="download-recipe">Download recipe</button>
<button type="button" class="btn" id="copy-recipe">Copy recipe JSON</button>
<button type="button" class="btn btn--accent" id="save-assets" title="Writes to assets/characters/ (dev server only)">
Save to assets/
</button>
</div>
<p class="status" id="status" aria-live="polite"></p>
</section>
</aside>
<main class="stage">
<canvas id="viewport"></canvas>
<div class="stage__hud">
<div class="stage__stats" id="stats">Assembling…</div>
<div class="stage__controls">
<label class="inline">
<span>Clip</span>
<select id="clip">
<option value="Idle">Idle</option>
<option value="Walk">Walk</option>
<option value="Run">Run</option>
<option value="QuickTurn">QuickTurn</option>
<option value="Slumped">Slumped</option>
</select>
</label>
<label class="inline checkbox">
<input type="checkbox" id="auto-rotate" checked />
<span>Spin</span>
</label>
</div>
</div>
</main>
<aside class="panel panel--right">
<header class="panel__header">
<h2>Recipe</h2>
</header>
<pre id="recipe-json" class="code"></pre>
</aside>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>