Skip to content

Releases: VII-Cae/hyalite--liquid-glass

hyalite 0.5.0 — the glass never switches off

Choose a tag to compare

@VII-Cae VII-Cae released this 11 Sep 09:59

Two flickers, both invisible in a screenshot and obvious in a recording. Each was found by driving the page in Chrome 152 over CDP, capturing every compositor frame, and reading the rim back pixel by pixel.

The resize flicker (rule 6)

A chat bubble that grows a line during a streaming reply used to go lens → frost → lens at every pause in the stream. Two things stacked: the filter region was pinned to the pixel size the map was built for, so the moment an element grew, Chromium painted it unfiltered; and settle papered over that by dropping to a plain blur, waiting 120 ms of quiet, then ramping the refraction back from zero. Every network gap longer than 120 ms was one full cycle.

  • The <filter> region is now the element's own box (objectBoundingBox 0 0 1 1) and feImage fills it. Between rebuilds the old map is merely stretched over the new size — the glass never drops out.
  • Live mode (settle: 0, now the default) rebuilds the first change of a burst synchronously inside the ResizeObserver callback — after layout, before paint — so the frame that shows the new size already shows the new map. While the size keeps moving it rebuilds at most once per 90 ms, and once more after the last change.
  • A freshly built filter renders correctly on its very first frame (verified frame by frame), so the swap is a single clean frame with nothing to hide.
  • settle > 0 still exists and only coalesces: one rebuild, settle ms after the last change, glass on meanwhile. For a box someone is dragging.

The materialize flash (rule 7)

The first one or two frames of a materialize ramp painted a white hairline along the whole rim, then recovered — a bright line at night, a white band on a pale backdrop, a full white ring on a small circle. Recorded over 24 open/close cycles it showed on 16; removing the CSS rim, the box-shadow, the border, rim and shade changed nothing; only materialize: 0 made it go away.

The backdrop a reference filter receives ends at the element's box, so feGaussianBlur fades its outermost rows to partial alpha. The dispersion pass then sums three premultiplied channel images, which is only exact for opaque pixels: on those rows the alpha clamps while the colour triples. At steady state the rim samples inward and never sees its own boundary; at the start of a ramp the displacement is still ≈ 0 and it does. One feColorMatrix after the blur pins alpha to 1 — it works on unpremultiplied colour, so the blurred colour is extended, not darkened. Not applied in self mode, whose source is meant to be translucent. Steady-state output is byte-identical to 0.4.0; 24 of 24 ramps clean afterwards.

Also: the ramp clock now starts at its first frame's own timestamp. A rAF timestamp can trail performance.now(), and a negative t flipped the displacement outward for a frame.

Breaking

  • settle changed meaning and default (120 → 0): it never falls back to blur any more; it coalesces resizes. Elements that change size in steps want the default; attach a shape morph after it lands, as before.
  • The filter region is objectBoundingBox. If you relied on the userSpaceOnUse pixel region, note that the map now stretches with the element between rebuilds.

32 self-checking cases (npm test), four demo pages verified headless. The playground's live card is 0.5.0; drag it or hit materialize.

hyalite 0.4.0 — the edge is shaded, not just bent

Choose a tag to compare

@VII-Cae VII-Cae released this 10 Sep 09:25

Refraction alone reads as a bendy film. feDisplacementMap moves pixels without changing their brightness, so a card came out clear but weightless — and what makes Apple’s glass look like glass is not only that the edge bends, it is that the edge darkens where it bends and returns a line of light.

The map’s blue channel now carries a signed edge profile: above 128 is light the glass sends back (added), below it multiplies the backdrop down (feBlend multiply). The shade is derived, not invented — the edge magnifies the backdrop, so ds/dd = 1 + m′ < 1 spreads its energy thin. That ratio is linear-light while the filter chain interpolates in sRGB, so it is re-encoded (^1/2.2) before it multiplies; skipping that turns a gain of 0.15 into a near-black outline instead of a shade.

New

  • slope — the decay cap is an option. Above 1 the field folds: the same backdrop shows up twice, which is where the liquid swirls come from. Folding has no two-pass split (no inverse exists), so smooth is ignored there and the shipped default is deliberately one pass.
  • shape — circle / squircle / lip. The lip perturbation reaches zero in both value and slope at each end, or the bevel does not meet the slab and the join creases.
  • shade and rim — how much the edge darkens and how much light it returns. Each one drives two effects in a fixed, tuned ratio: shade the caustic plus the Fresnel transmission loss, rim a wide sheen plus a tight specular line. Two knobs that always move together are worse than one.
  • edgeW — how far in the shading and the sheen reach, in absolute pixels.
  • --hyalite-edge — the pixel-sharp rim as inset shadows, written to its own custom property. On the element rather than in the filter, where feImage downsampling and kNearest would eat a half-pixel line. It works in Safari and Firefox too, which get no refraction at all.
  • The bevel is clamped to half the short side, not to the corner radius. A lens has to be able to reach the centre of a circle.
  • info() carries the sampled edge profile; demo/edge-lab.html plots it.

Rule 3: geometry scales, optics does not

The bevel and the displacement are proportions of the element. The dispersion, the bright line and the dark band under it are fixed pixel counts. Mixing the two is what turned a thick lens into a radial rainbow and a wide rim into grey mud.

Breaking

  • rim now means the light the edge returns (was an additive white pass).
  • dispersion is pixels of channel separation (was a fraction of the displacement).
  • The bevel is no longer capped by the corner radius, so wide bevels bend further than before.
  • The CSS needs box-shadow: var(--hyalite-edge, none) to get the sharp rim.
  • Defaults are a tuned set, not a conservative one: a narrow bevel over a very thick slab with a folding slope. For something calmer, drop slope below 1 — you lose the swirls and get the two-pass anti-staircase back.

27 self-checking cases, four demo pages verified headless.

hyalite 0.3.1

Choose a tag to compare

@VII-Cae VII-Cae released this 10 Sep 09:24

Two bugs that only surface at the edges of the parameter space.

Circles were broken. The direction field widens each radius by the bevel (rule 3) and capped that at the short side — but makeSDF’s rounded-rect formula only holds while the radius is at most half of it. Past that W/2 − R goes negative, both q terms are positive everywhere, min(max(qx, qy), 0) is pinned at 0, and the field degenerates into a shifted circle whose gradient flips sign across the axes. A circle (border-radius: 50%) sits exactly at the limit, so any bevel at all pushed it over: it rendered with a cross-shaped seam and radial colour fans. Now capped at half the short side.

A thick lens turned into a rainbow. dispersion is a fraction of the displacement, so the channel separation grew with the lens — 60px of displacement at 0.16 pulls the channels 10px apart. Real glass disperses by a material constant, unrelated to how strong the lens is. The separation is clamped to 3px; ordinary setups (under 3px) come out bit-identical, only the runaway is caught. Making it an absolute pixel option came in 0.4.0.

Two cases cover both, reading the values back: the axis seam drops from ≈ 114 to 1, and a 50px lens separates by 3px instead of 25.

hyalite 0.3.0

Choose a tag to compare

@VII-Cae VII-Cae released this 10 Sep 09:24

No stairs.

Chromium samples the bent picture nearest-neighbour — Skia’s displacement effect is pinned to kNearest (skbug 40045448). A decay slope of 0.85 is a 6.7× stretch at the rim, so every source pixel there becomes a 6.7px block and any hard edge behind the glass turns into a staircase. No amount of accuracy in the map can fix a sampler.

So the field is split into two displacement passes of equal stretch (≈ 2.6× each) that compose exactly to the one-pass field — the inner table is the inverse of the outer one, solved by bisection, not a halving. Between them sits a smooth-px blur masked to the bevel ring, which melts the inner pass’s staircase before the outer pass stretches it again. 6.7px stairs at full contrast come out ≈ 2.6px at a fraction of it, and the centre is untouched.

  • New option smooth (default 1; 0 goes back to one pass — cheaper, and the stairs come back).
  • info() gains mapInner and split.
  • A map build always produces both PNGs, so smooth can be toggled without rebuilding one.
  • Pre-blurring the whole source was tried instead and rejected: it fogs the centre.

hyalite 0.2.0

Choose a tag to compare

@VII-Cae VII-Cae released this 10 Sep 09:24

Caching, correctness and a way to check both.

  • Two-level cache. A map depends on geometry, bevel, thickness and light; a filter adds blur, dispersion and rim. So setOpts({ blur }) rebuilds a handful of DOM nodes and reuses every map already in memory.
  • Size buckets. Map sizes are rounded up by ≤ 2 % per side, so a column of chat bubbles a few pixels apart shares one map instead of one canvas per message. Radii are deliberately not rescaled to match — that would put the element’s own width back into the key and defeat the bucket.
  • The CSS corner-overlap rule. Radii shrink by one shared factor, and only when two radii sharing an edge do not fit on it — not clamped corner by corner. A 320×40 card with border-radius: 24px 24px 0 0 really gets 24px corners; the old clamp drew the refraction at 20 while the browser drew the glass at 24.
  • Quarter-symmetry. With four equal corners one quadrant is computed and the other three mirrored.
  • light — the rim light’s direction in degrees.
  • force(true|false|null) — an escape hatch over the engine sniff, for the day WebKit ships SVG backdrop filters.
  • demo/cases.html — a self-checking page that reads values back out of the maps and the filters rather than trusting that nothing threw.

hyalite 0.1.0

Choose a tag to compare

@VII-Cae VII-Cae released this 07 Sep 03:44

First release. Real refraction liquid glass for the web: per-element SDF lens maps, Snell profile, no-fold constraint, larger-radius direction field, per-corner radii, settle/materialize behaviour, watch/attach API, Chromium-only with a clean CSS fallback elsewhere. Live playground: https://vii-cae.github.io/hyalite--liquid-glass/demo/index.html