Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ Or skip the install and import directly from a CDN:

## How It Works

1. **Non-glass children of the root** are rasterised onto a hidden canvas using `html-to-image` (which clones the subtree, inlines computed styles, and renders via SVG `foreignObject`). Static children are captured once and cached; children with `data-dynamic` (or any `<video>`) are re-captured every frame.
2. **`<img>`, `<canvas>`, and `<video>`** are drawn directly via `ctx.drawImage` (faster than `html-to-image`, and the only way to capture live video frames).
3. **Glass elements** receive an injected child `<canvas>` that displays the WebGL output. For each glass element, the renderer crops the scene at the panel's location, runs an optional Gaussian blur, then runs a fragment shader that applies refraction, chromatic aberration, Fresnel reflection, multi-light specular highlights, an inner-stroke rim, and a drop shadow.
4. **Layered compositing** writes each rendered glass canvas back to the compositing canvas before the next glass element runs, so a glass element above another sees the lower one in its refraction.
1. **Non-glass children of the root** are turned into a local scene for each glass panel. Simple media (`<img>`, `<canvas>`, `<video>`) are drawn directly via `drawImage`; other DOM wrappers are rasterised with `html-to-image`.
2. **Static content is cached aggressively.** Wrappers that are not marked `data-dynamic` are captured once, reused, and only refreshed when layout, config, or observed content changes.
3. **Glass elements** receive an injected child `<canvas>` that displays the WebGL result. For each panel, the renderer crops the relevant scene region, runs an optional blur pass, then shades refraction, chromatic aberration, Fresnel reflection, highlights, tint, and shadow.
4. **Overlapping glass is composited in order.** As each panel renders, its result is written back into the local scene so later glass panels can refract earlier ones correctly.
5. **The runtime adapts to the device.** Idle pages stop scheduling animation frames entirely, coarse-pointer / constrained devices use a lighter blur profile, and desktop renders reuse per-frame layout and DOM query caches to reduce CPU overhead.

## API

Expand Down Expand Up @@ -213,11 +214,13 @@ If you put an overlay above a background image and the glass shows the bg but no

### Performance

- **Capturing DOM into a canvas is expensive.** Every non-glass wrapper is rasterised via `html-to-image` (style inlining + SVG-foreignObject decode). Keep wrappers small and shallow.
- **`data-dynamic` re-captures every frame.** Use it sparingly — only for content that actually changes.
- **Each LiquidGlass instance opens its own WebGL context.** Browsers cap concurrent contexts (typically 16 system-wide); don't spawn dozens.
- **Window resize re-captures everything.** Don't drive layout in a tight resize loop.
- **The render loop short-circuits when nothing is dirty** — a static page with no `<video>` and no `data-dynamic` content does almost no work per frame.
- **`html-to-image` is still the most expensive path.** Keep non-glass wrappers shallow when possible; images, videos, and canvases are much cheaper because they bypass DOM rasterisation.
- **`data-dynamic` is the main escape hatch from caching.** Use it only for content that truly changes every frame. For one-off visual updates, prefer `instance.markChanged()` so the page can stay idle otherwise.
- **Idle pages do not spin a permanent render loop.** When nothing is dirty, the instance stops requesting animation frames and wakes back up only on observed changes, drag, hover/press state, video, or explicit invalidation.
- **Mobile devices use an adaptive quality profile.** On coarse-pointer / constrained devices the renderer caps effective DPR and runs blur on a smaller buffer with fewer passes to reduce battery, thermal, and GPU pressure.
- **Desktop keeps quality but avoids repeated work.** Per-frame caches reuse bounds, padded rects, paint-overflow checks, dynamic/media descendant lookups, media fit styles, and canvas contexts across overlapping glass panels.
- **Resize is still a global invalidation event.** The library rebuilds canvas sizes and recaptures content on resize, so avoid driving layout in a tight resize loop.
- **Each LiquidGlass instance opens its own WebGL context.** Browsers cap concurrent contexts (typically around 16 system-wide), so avoid creating lots of independent roots.

### Text & fonts

Expand All @@ -232,7 +235,7 @@ If you put an overlay above a background image and the glass shows the bg but no

## Browser Support

Requires WebGL 1.0 + Canvas 2D + SVG `foreignObject`. Effectively all evergreen browsers (Chrome, Firefox, Safari, Edge). WebGL context loss is recovered automatically.
Requires WebGL 1.0 + Canvas 2D + SVG `foreignObject`. In practice that means current evergreen browsers (Chrome, Safari, Firefox, Edge). WebGL context loss is recovered automatically, and the runtime will fall back to lighter quality settings on constrained devices rather than exposing extra API flags.

## License

Expand Down
16 changes: 9 additions & 7 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<title>LiquidGlass — WebGL Glass Effects for the Web</title>
<meta name="description" content="Apply realistic glass refraction, blur, chromatic aberration, and lighting effects to any HTML element using WebGL shaders.">

<link rel="icon" type="image/png" href="/favicon.png">
<link rel="icon" type="image/png" href="./favicon.png">

<!-- Open Graph -->
<meta property="og:title" content="LiquidGlass">
Expand Down Expand Up @@ -849,7 +849,7 @@ <h2>Dome Bevel (Magnifier)</h2>
<div class="section">
<h2>Limitations &amp; Gotchas</h2>
<p>
The library leans on real-time DOM rasterisation and a multi-pass WebGL pipeline. That comes with a handful of constraints worth knowing before you wire it into a production page.
The runtime is much lighter than the earliest versions, but it still combines DOM capture, compositing, and WebGL shading. These are the practical constraints and performance tradeoffs to keep in mind when you wire it into a real page.
</p>

<h3>Structural</h3>
Expand All @@ -862,10 +862,13 @@ <h3>Structural</h3>

<h3>Performance</h3>
<ul>
<li><strong>Capturing DOM into a canvas is expensive.</strong> Every non-glass wrapper is rasterised via <code>html-to-image</code> (style inlining + SVG-foreignObject decode). Keep wrappers small and shallow.</li>
<li><strong><code>data-dynamic</code> elements are treated as always dirty by definition.</strong> Use it only for content that genuinely changes every frame — animations, counters, charts. For one-shot updates, prefer <code>instance.markChanged()</code> instead — it costs nothing on idle frames.</li>
<li><strong>The library now parks completely on idle frames.</strong> A static scene no longer burns a permanent <code>requestAnimationFrame</code> loop; rendering wakes up only when content, layout, hover/press state, drag state, video, or explicit invalidation changes.</li>
<li><strong><code>html-to-image</code> is still the heaviest code path.</strong> Media elements are cheap because they draw directly, but non-glass wrappers still need DOM rasterisation. Keep those wrappers shallow when possible.</li>
<li><strong><code>data-dynamic</code> is what defeats most caching.</strong> Use it only for genuinely live content such as counters, charts, or animated text. For one-shot changes, prefer <code>instance.markChanged()</code> so the page can remain idle the rest of the time.</li>
<li><strong>Mobile devices now use an adaptive quality profile.</strong> On coarse-pointer / constrained devices the renderer caps effective DPR and runs blur on a smaller buffer with fewer passes to reduce battery, thermals, and GPU cost.</li>
<li><strong>Desktop keeps full quality but does less CPU work.</strong> Bounds, descendant queries, media fit styles, paint-overflow checks, and canvas contexts are cached per frame so overlapping glass panels do not keep recomputing the same data.</li>
<li><strong>Resize is still a full invalidation event.</strong> The library rebuilds glass canvases and recaptures content on resize, so avoid driving layout in a tight resize loop.</li>
<li><strong>Each LiquidGlass instance opens its own WebGL context.</strong> Browsers cap concurrent contexts (typically 16 system-wide); don't spawn dozens.</li>
<li><strong>Window resize re-captures everything.</strong> Don't drive layout in a tight resize loop.</li>
</ul>

<h3>Text &amp; fonts</h3>
Expand Down Expand Up @@ -959,8 +962,7 @@ <h3>API</h3>
</script>

<script type="module">
//import { LiquidGlass } from '../dist/index.js';
import { LiquidGlass } from 'https://cdn.jsdelivr.net/npm/@ybouane/liquidglass/dist/index.js';
import { LiquidGlass } from '../dist/index.js';

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// HERO — single root, nested glass elements
Expand Down
17 changes: 11 additions & 6 deletions src/GlassRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,13 @@ export class GlassRenderer {
width: number,
height: number,
blurAmount: number,
blurDownsample = 1,
blurIterations = BLUR_ITERATIONS,
): void {
if (this.contextLost) return;
const gl = this.gl;
if (!this._setActiveSize(width, height)) return;
const effectiveBlurScale = blurAmount > 0 ? Math.max(0.25, Math.min(1, blurDownsample)) : 1;
if (!this._setActiveSize(width, height, effectiveBlurScale)) return;
const W = this.width;
const H = this.height;
const fboSet = this.activeFBOs!;
Expand Down Expand Up @@ -210,7 +213,7 @@ export class GlassRenderer {
const spread = blurAmount * 2.5;
gl.useProgram(this.blurP);
gl.uniform1i(this.blurU.u_tex, 0);
for (let i = 0; i < BLUR_ITERATIONS; i++) {
for (let i = 0; i < blurIterations; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fboSet.blurB.fbo);
gl.viewport(0, 0, bw, bh);
gl.bindTexture(gl.TEXTURE_2D, fboSet.blurA.tex);
Expand Down Expand Up @@ -315,7 +318,7 @@ export class GlassRenderer {
// FBO management
// ────────────────────────────────────────────

private _setActiveSize(w: number, h: number): boolean {
private _setActiveSize(w: number, h: number, blurScale = 1): boolean {
if (w <= 0 || h <= 0) return false;

this.width = w;
Expand All @@ -326,13 +329,15 @@ export class GlassRenderer {
this.canvas.height = Math.max(this.canvas.height, h);
}

const key = `${w}x${h}`;
const blurW = Math.max(1, Math.round(w * blurScale));
const blurH = Math.max(1, Math.round(h * blurScale));
const key = `${w}x${h}@${blurW}x${blurH}`;
let fboSet = this.fboCache.get(key);
if (!fboSet) {
fboSet = {
bg: this._makeFBO(w, h),
blurA: this._makeFBO(w, h),
blurB: this._makeFBO(w, h),
blurA: this._makeFBO(blurW, blurH),
blurB: this._makeFBO(blurW, blurH),
};
this.fboCache.set(key, fboSet);
}
Expand Down
Loading