A zero-dependency browser toolkit for designing glass / resin capsule buttons on the web. Highlights are baked from a sampled height field, surface normals, Fresnel rim, specular thresholds, and layered Canvas textures—applied to real <button> elements, not a WebGL scene.
Author: Yu Fei · personal project by @inserter
- Optical baking — Multi-light rig, IOR, transmission, absorption, dispersion, up to four internal round-trips (bottom Fresnel + march to top + exit or TIR) feeding the transmission pocket layer, plus contact shadows
- Button spec rig — Capsule geometry plus customizable outer / inner edge strokes and inset ring
- Background rig — Solid, radial, grid, dots, brushed, or image URL environments (image mode lightens cavity fill and boosts backdrop blur so the photo shows through the capsule face)
- Orthographic views — X–Y top view plus X–Z / Y–Z sections with incident, refracted, and shadow paths (demo page)
- Backdrop displacement — Optional checkbox under Material Rig: SVG
feDisplacementMapchained inbackdrop-filter(IOR-scaled strength; WebKit-first) - Size scale — Seven breakpoints from
xxstoxxldriven by the same global parameters (demo page) - Presentation overlay (demo) — After orthographic views: four xs columns (Rest / Hover / Pressed / Inactive), each with its own sliders (opacity, brightness, saturation, glow, button tint color + strength). Scoped CSS variables — does not rebake optics.
| File | Role |
|---|---|
index.html |
Demo page shell: control panels, preview stages, cross-section host |
app.css |
Demo chrome: page layout, background modes, control panels, cross-section panel |
app.js |
Demo app: rig state (sceneMaterial, sceneLighting, …), form wiring, scheduleRenderAll |
OpticalCapsule.css |
Reusable capsule UI: stages, underlay, .optical-capsule, optical SVG layers, size tokens |
OpticalCapsule.js |
Core engine: createOpticalCapsuleCore(getRigs), bake pipeline, renderStage, makeUnderlay / SVG layers |
package.json |
"type": "module" so Node can syntax-check / import the ES modules |
Integrating into another page: include OpticalCapsule.css, add the SVG filter block (see index.html), host .capsule-stage markup, and call createOpticalCapsuleCore(() => ({ material, lighting, buttonSpec, shell, background, label })) with your own rig objects and renderStage on resize or when rigs change.
Use a local static server (recommended). ES modules and imports require http(s)—not file://.
cd "/path/to/optical-capsule"
python3 -m http.server 8080
# or: npx serve .Open http://localhost:8080/index.html
Add ?debug=1 to print bake timings in the browser console while tuning rigs.
Optional: node -e "import('./OpticalCapsule.js')" from the project root checks module syntax.
- Tune Background Rig, Global Light Rig, Material Rig (presets: Glass, Crystal, Diamond, Ruby, Sapphire, …), and Button Spec Rig in the demo UI. Optionally enable Backdrop displacement under Material Rig.
- Use Presentation states (four xs columns below orthographic diagrams) to tune purely CSS overlays per stance — optics stay unchanged.
- The demo script bakes optical layers for every
.capsule-stageand updates the orthographic diagrams. - For production, copy rig defaults from
app.jsinto your app and import onlyOpticalCapsule.js+OpticalCapsule.css, or build presets on top ofgetRigs().
- No build step beyond static hosting — vanilla DOM, Canvas 2D, and SVG (ES modules in the browser).
- Renders are debounced (~80ms) on control input; window
resizetriggers a full rebake. - Internal transmission pocket:
internalMultiBounceEnergy()traces up to four bottom→top cycles per light (Schlick Fresnel at flat base and at upper surface; Snell exit with TIR fall back to reflection and continue). - Backdrop displacement:
#oc-backdrop-displacein the page SVG feedsbackdrop-filter: url(#oc-backdrop-displace)on.optical-capsulewhenmaterial.backdropDisplaceTryis true;feDisplacementMap/@scaleis driven from IOR ((ior - 1) * 18 + 4, clamped). Best tested in Safari / WebKit; Chromium may ignore or partially applyurl()onbackdrop-filter.
Interactive demo: https://inserter.github.io/OpticalCapsule/
Hosted from this repo’s index.html at the root (main). Append ?debug=1 to log bake timings in the console—same behavior as running the demo locally over HTTP(S).
The github.io/… path must match the repository name (here: OpticalCapsule, not optical-capsule). If Pages is off or never deployed, you’ll see GitHub’s “There isn’t a GitHub Pages site here” — enable it under Settings → Pages:
- Deploy from a branch:
main+/(root) · Pages docs, or - GitHub Actions: this repo’s
.github/workflows/deploy-github-pages.yml· custom workflows
Copyright © 2026 Yu Fei. Released under the GNU Affero General Public License v3.0 (AGPL-3.0).
If you modify this project and run it as a network service (for example, a hosted demo or SaaS), you must offer corresponding source to users interacting with it over the network, as required by AGPL-3.0 Section 13.