video-to-pixel-art
Turn videos into pixel art in real-time using javascript, webgl, and html canvas
A liquid glass effect library for the web. Apply realistic glass refraction, blur, chromatic aberration, and lighting e…
A liquid glass effect library for the web. Live Demo Apply realistic glass refraction, blur, chromatic aberration, and lighting effects to any HTML element using WebGL shaders.
https://liquid-glass.ybouane.com/
npm install @ybouane/liquidglass
Or skip the install and import directly from a CDN:
<script type="module">
import { LiquidGlass } from 'https://cdn.jsdelivr.net/npm/@ybouane/liquidglass/dist/index.js';
</script>
<div id="root">
<!-- Background image (sibling of the glass, captured by the shader) -->
<img class="bg" src="background.jpg" alt="">
<!-- Static content -->
<h1 class="title">Hello World</h1>
<!-- Animated content needs data-dynamic so it re-captures every frame -->
<div class="counter" data-dynamic>0</div>
<!-- Glass element -->
<div class="my-glass">Glass Panel</div>
</div>
<script type="module">
import { LiquidGlass } from '@ybouane/liquidglass';
const glassEl = document.querySelector('.my-glass');
glassEl.dataset.config = JSON.stringify({
floating: true,
blurAmount: 0.25,
});
const instance = await LiquidGlass.init({
root: document.querySelector('#root'),
glassElements: [glassEl],
});
// Later, to tear down:
// instance.destroy();
</script>
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.<img>, <canvas>, and <video> are drawn directly via ctx.drawImage (faster than html-to-image, and the only way to capture live video frames).<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.LiquidGlass.init(options)Async — creates and starts a LiquidGlass instance. Resolves once the page's webfonts have been pre-fetched and every glass element's content has been pre-captured.
| Option | Type | Default | Description |
|---|---|---|---|
root |
HTMLElement |
(required) | The container element. Glass elements must be direct children of this element. |
glassElements |
NodeList | HTMLElement[] |
[] |
Elements to apply the glass effect to. |
defaults |
Partial<GlassConfig> |
{} |
Override the default per-element configuration values for this instance. |
Returns a Promise<LiquidGlass> resolving to the instance, which exposes:
.fps: number — current measured frames-per-second (updated once per second)..destroy(): void — stop the render loop, remove injected canvases, restore mutated inline styles, and free WebGL resources..markChanged(element?: HTMLElement): void — manually flag content the library can't observe on its own (see below).The library also exports:
invalidateFontEmbedCache(): void — call after dynamically loading new font stylesheets so the next init() rebuilds the embedded font cache.const instance = await LiquidGlass.init({
root: document.querySelector('#root'),
glassElements: document.querySelectorAll('.glass'),
defaults: {
cornerRadius: 24,
refraction: 0.8,
},
});
Configure individual glass elements by setting data-config to a JSON string:
element.dataset.config = JSON.stringify({
blurAmount: 0.25,
floating: true,
cornerRadius: 40,
});
The library re-reads data-config whenever it changes (via a MutationObserver), so you can update it dynamically.
| Option | Type | Default | Description |
|---|---|---|---|
blurAmount |
number |
0.00 |
Background blur strength (0 = sharp, 1 = maximum blur) |
refraction |
number |
0.69 |
How much the glass bends the image behind it |
chromAberration |
number |
0.05 |
Chromatic aberration / colour fringing at edges |
edgeHighlight |
number |
0.05 |
Edge glow / rim lighting intensity |
specular |
number |
0.00 |
Specular highlight intensity (multi-light Blinn-Phong) |
fresnel |
number |
1.00 |
Fresnel reflection at grazing angles |
distortion |
number |
0.00 |
Micro-distortion noise strength |
cornerRadius |
number |
65 |
Corner radius in CSS pixels |
zRadius |
number |
40 |
Bevel depth — controls the curvature of the pill's cross-section |
opacity |
number |
1.00 |
Overall glass panel opacity |
saturation |
number |
0.00 |
Saturation adjustment (-1 = grayscale, 0 = normal, 1 = vivid) |
tintStrength |
number |
0.00 |
Cool blue glass tint strength |
brightness |
number |
0.00 |
Brightness adjustment (-0.5 to 0.5) |
shadowOpacity |
number |
0.30 |
Drop shadow opacity |
shadowSpread |
number |
10 |
Drop shadow spread in CSS pixels |
shadowOffsetY |
number |
1 |
Shadow vertical offset in CSS pixels |
floating |
boolean |
false |
Enable drag-to-move via Pointer Events |
button |
boolean |
false |
Button mode — hovering brightens the panel; pressing flattens the bevel and deepens the shadow |
bevelMode |
0 | 1 |
0 |
0 = biconvex pill (default). 1 = dome / plano-convex; pair with cornerRadius === zRadius for a half-sphere magnifier. |
data-dynamicAdd data-dynamic to any direct child of the root whose contents change every frame (counters, animated text, charts). Without it, that wrapper is captured once and cached forever.
<div id="root"> <div class="static-bg">...</div> <!-- captured once, cached --> <div class="counter" data-dynamic>...</div> <!-- re-captured every frame --> <div class="glass">...</div> </div>
data-dynamic elements are treated as always dirty by definition — the library re-rasterises them every frame and re-runs the shader for every glass that overlaps them. Use it sparingly: it's the only thing on the page that defeats the per-element dirty-tracking optimisation.
<video> elements are auto-detected as dynamic — you don't need to add data-dynamic to them.
For one-shot updates that don't happen every frame, prefer instance.markChanged() (see below) — it costs nothing on idle frames.
data-configJSON string of per-element configuration options (see the table above). Must decode to an object; invalid JSON or non-object values are ignored with a console warning.
instance.markChanged()The library auto-detects most things that affect the glass: DOM mutations inside glass subtrees, data-config changes, layout shifts, drag, hover/press, window resize, async capture cache landings, and data-dynamic / <video> elements (which are treated as always dirty by definition and re-rendered every frame).
What it cannot detect:
<canvas> whose pixels you just updated via getContext('2d') / WebGL.<img> whose src you just swapped via JS.background-image or other paint property you just updated.For these cases, call:
const instance = await LiquidGlass.init({ root, glassElements });
// You just repainted a wrapper — only glasses overlapping it will re-render.
instance.markChanged(myCanvasElement);
// Or invalidate everything on this instance:
instance.markChanged();
markChanged(element) walks every glass on the instance, finds the ones whose sample rect intersects the element's bounding rect, and marks just those for a re-render on the next frame. Glasses that don't overlap the element keep their cached output and skip the WebGL pipeline entirely.
markChanged() with no argument flags every glass — useful as a "I don't know what changed but please redraw" escape hatch.
For elements with data-dynamic, calling markChanged is harmless but unnecessary — the library already treats them as dirty every frame.
The library re-implements the CSS stacking-context spec to decide painting order on the compositing canvas. It recognises the following stacking-context triggers on direct children of the root:
position (with z-index)z-indexopacity < 1transform, filter, perspective, clip-path, mix-blend-mode, isolation, backdrop-filter, mask-image, contain: layout|paint|strict|contentwill-change listing any of the above propertiesIf you put an overlay above a background image and the glass shows the bg but not the overlay, you've hit a missing trigger — file an issue with the property name.
LiquidGlass.init() call.<canvas> is injected as the glass element's first child for shader output. Avoid :first-child selectors on glass elements.overflow: hidden.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.<video> and no data-dynamic content does almost no work per frame.init() and served with CORS-friendly headers. Google Fonts, jsdelivr, and unpkg work out of the box. Webfonts loaded after init will fall back to system fonts inside captured rasters. Call invalidateFontEmbedCache() then re-init if you load fonts dynamically.<img> elements need crossorigin="anonymous". Tainted canvases break texture upload and disable the glass effect for the entire root.LiquidGlass.init() is async. It resolves only after the font CSS prefetch, glass content pre-capture, and static-content pre-warm have all completed (typically 100–500 ms on a fresh page).data-dynamic only catches direct children of the root. Live content nested inside a wrapper that lacks data-dynamic will not trigger re-captures.destroy() does not restore an element's original position: static if the library overwrote it with relative. Re-init on the same elements is fine; exotic external mutation in between is not.Requires WebGL 1.0 + Canvas 2D + SVG foreignObject. Effectively all evergreen browsers (Chrome, Firefox, Safari, Edge). WebGL context loss is recovered automatically.
MIT
more like this
Turn videos into pixel art in real-time using javascript, webgl, and html canvas
A unity shader for smooth pixel filtering, based on CptPotato's smooth filtering
search projects, people, and tags