# Daphnis Glass

**Developed by Codex in collaboration with areoform. MIT licensed.**

A small, dependency-free JavaScript/CSS component for liquid glass and deliberately roughened glass. Its controls can have an **etched footprint**: a roughened rim and inset light/shadow cues make the icon feel cut into the surrounding pane.

[Contribution credits](AUTHORS.md) · [License](LICENSE) · [Validation](VALIDATION.md)

## Try it

From the source ZIP, with Node installed:

```sh
npm run demo
```

Open the local address printed by the server. The demo uses ordinary HTML controls and placeholder glyphs. Choose Liquid glass or Burred glass, toggle Etched controls, and vary the roughened rim. “Burred” describes the local roughening treatment used in this material study.

The server binds only to loopback and serves an explicit list of demo/component files. It is a development convenience, not a hosting server.

For a complete static-page example, open [As We May Think](examples/as-we-may-think/index.html), available at `/examples/as-we-may-think/` on the local server. It is a plain reading page with a sticky glass toolbar, section links, text-size controls and surface selection. The essay is separate from the MIT-licensed code; see the [content and attribution note](examples/as-we-may-think/README.md). The example is included in the source tree and source ZIP, rather than the npm runtime package.

`npm run build:site` exports a complete static website into `site/`, including every shared asset and a root entry page. For Cloudflare Pages use that build command and `site` as the output directory. For Workers Static Assets use the same generated directory. Deploy the whole output, rather than just the example folder. The reading bar has its own solid/CSS fallback when the shared glass files fail to load. Its named Finish selector is directly on the bar, with finer adjustments behind the gear. Finishes and starting values are declared in the HTML; see the [example guide](examples/as-we-may-think/README.md).

## Use it

Copy `glass.js`, `maps.js`, and `glass.css` together, or install the prepared local tarball:

```sh
npm install ./areoform-felt-glass-0.1.0.tgz
```

```html
<link rel="stylesheet" href="./glass.css">
<div class="fg-glass" id="toolbar">
  <button class="fg-etch" type="button" aria-label="Highlight">✎</button>
</div>
<script type="module">
  import {createGlass} from './glass.js';
  const glass = createGlass('#toolbar', {material: 'refracted'});
  // glass.setMaterial('frosted');
  // glass.destroy();
</script>
```

With a package-aware bundler, import `createGlass` from `@areoform/felt-glass` and the stylesheet from `@areoform/felt-glass/glass.css`. The package is prepared locally; registry publication and access to that scope are separate steps.

The component decorates existing HTML elements. It does not replace, wrap or re-create their contents. Your buttons retain their event handlers, labels, native focus and form behavior. Apply `fg-etch` to individual controls; the roughened band follows the control footprint, independently of the surface's rendering engine.

## API

`createGlass(targets, options)` accepts a selector, one element, or an iterable of distinct HTML elements in the current document's light DOM. It returns a controller:

| Method | Behavior |
|---|---|
| `setMaterial(name)` | Select `auto`, `refracted`, `frosted`, `translucent`, or `solid`. |
| `setRecipes(patch)` | Update validated numeric parameters for either rich material. |
| `setOptics(patch)` | Enable/update an index-driven optical material; `null` returns to artistic recipes. |
| `refresh()` | Re-evaluate capabilities and geometry, useful after revealing a hidden element. |
| `state` | Read the requested material and each element's actual material, renderer and engine. |
| `destroy()` | Disconnect observers/listeners, cancel owned map work, remove owned SVG filters, and restore the element attributes/styles/classes taken over by the controller. Repeated calls are safe. |

Elements can belong to only one controller at a time. Controllers use separate IDs, recipes, caches and choices. Importing the module is safe during server-side rendering; call `createGlass` only in a browser, after the elements exist. Shadow DOM, cross-document targets and arbitrary SVG shapes are not part of this initial release.

```js
const glass = createGlass(document.querySelectorAll('.pane'), {
  material: 'frosted',
  recipes: {frosted: {roughness: .7, blur: 2, displacement: 4, grainSize: 176}}
});
glass.setMaterial('refracted');
```

Recipe fields are `bend` and `roughness` (0–1), `rim` (0.5–64 px), `blur` (0–32 px), `displacement` (0–64 px), `saturation` (0–2), `fallbackBlur` (0–40 px), `grainSize` (32–512 CSS px), and an unsigned 32-bit `seed`. Invalid values fail before changing the current configuration. Curvature follows the element's CSS border radius; the optical shape is a rounded rectangle.

### Index-driven materials

```js
const glass = createGlass('.pane', {
  material: 'refracted',
  optics: {refractiveIndex: 1.52}
});
glass.setOptics({refractiveIndex: 1.333});
```

This optional path generates Snell-law rays and Fresnel direct transmission for a rounded cap with a flat underside. Defaults are 6 CSS pixels of thickness, zero document gap and surrounding index 1. The standalone interface solver also supports arbitrary incident directions, normals and positive indices. A scalar index does not imply a color, dispersion or frost texture. The demo's **Use Snell optics** checkbox exposes a custom index. Read [the model, caching contract and limits](OPTICS.md) before using physical parameters.

## Appearance

```css
.pane {
  --fg-radius: 32px;
  --fg-border: #24a68e; /* same teal as the demo's confirmation button */
  --fg-color: #223a33;
}
.icon-button {
  --fg-etch-radius: 14px;
  --fg-etch-width: 3px;
}
```

The grain is static and embedded. It varies in size, strength and dispersal; there is no animated noise shader or frame loop. The etched ring uses a masked grain band where supported, with an inset border/shadow fallback. All decoration leaves pointer events and foreground content alone. Focus styling remains the host application's responsibility; the demo includes it. Judge icon clearance from the rendered shape, its weight and its visible gap to the control boundary. A nominal layout box or padding value does not establish sufficient visual space.

## Fallbacks within fallbacks

- **Material:** refracted → frosted → translucent → solid.
- **Backdrop renderer:** SVG optics → CSS frost/blur → simpler CSS surfaces.
- **Artistic map generation:** WebGPU → bundled WASM → JavaScript, using the same parameters for both rich materials. Index-driven optics currently uses cooperative JavaScript and retains the same CSS renderer fallbacks.
- **Etching:** masked roughened rim → inset CSS edge. Forced colors remove the decorative layers.

A failed map engine does not require a different supported material. A renderer failure does not disable the contents. Parsing an SVG filter URL does not establish that a browser paints it; this release enables that enhancement only in the tested Chromium family. Other engines retain CSS surfaces.

`auto` honors reduced transparency and increased contrast. An explicit material can override those surface defaults; forced colors always win. The component introduces no motion, persistence, telemetry, cookies, authentication or network requests. It does not alter document source or process user data.

Maps are limited to 300,000 pixels and 4,096 pixels per dimension. WASM memory is fixed at 1.5 MiB. Per controller, work is limited to two active and 32 queued jobs, with four shared cached images plus each live surface's retained map. Unchanged panes reuse those maps across scrolling, refreshes and visibility changes. Setup and decoding have deadlines. GPU resources are released after generation. A richer backend is not a performance guarantee; device performance has not been benchmarked.

The host must serve the modules and CSS. Optional maps/grain need `img-src data:` under a strict CSP. The WASM backend can use the narrowly scoped `script-src 'wasm-unsafe-eval'` permission; if it is blocked, JavaScript generation remains available. JavaScript `unsafe-eval` is not required. Do not weaken an untrusted document sandbox just to enable this decorative effect.

Technical references: [backdrop filtering](https://drafts.csswg.org/filter-effects-2/), [WebGPU](https://www.w3.org/TR/webgpu/), [WASM-specific CSP permission](https://www.w3.org/TR/CSP3/). WebGPU/WASM generate maps; SVG/CSS compose the browser's actual backdrop.

## Development and release

`npm test` checks backend equivalence, bounds, SSR-safe imports and credits. Browser checks use an isolated Chrome debugging port:

```sh
FELT_TEST_CDP_PORT=9222 npm run test:browser
```

Start the demo first. The tests create/close their own tab and deliberately simulate failures. Use a disposable Chrome profile, never your regular browser profile. See `VALIDATION.md` for the actual tested platform and limits.

With the same isolated debugging port, `npm run test:reading` checks the reading bar, presets, settings, missing CSS/modules and simpler rendering paths. Set `FELT_TEST_ORIGIN` to a local static-site server to test the exported website instead of the development server. When validating a source release, export the website outside the checkout (`node scripts/build-site.mjs /tmp/daphnis-site`); the release map deliberately rejects generated site output in the source tree.

`scripts/glass-map.c` reproduces the embedded no-import WASM module; its build command is in the source. `scripts/glass-grain.py` generates the original static texture using Python's standard library.

```sh
python3 scripts/release.py --check
python3 scripts/release.py --output /tmp/felt-glass-release
```

The exporter accepts only the explicit release file map, rejects unknown files and links, scans for private material, runs unit checks, checks the npm package membership, and creates checksummed source ZIP and npm tarball archives. It performs no publishing or remote writes.

Future material work is recorded in the [optics roadmap](ROADMAP.md). Index-driven direct refraction is implemented as an opt-in path; the browser adapter remains a bounded displacement-map approximation. Existing recipes are unchanged by default.

### Water-drop optics study

The [first physical reference](experiments/sessile-drop/MODEL.md) models a sessile water drop on a hydrophobic transparent support. Its [standalone study](experiments/sessile-drop/standalone.html) exposes contact angle, volume, optical depth and wavelength, with traced rays and a rainbow diagnostic. It opens directly from disk. This experiment does not replace the production renderer or claim experimental calibration.
