DEV Community

ouzz
ouzz

Posted on

I Built a Liquid Glass Lens Effect in React Canvas

I Built a Liquid Glass Lens Effect in React Canvas

I recently implemented a "liquid glass lens" post-processing effect in react-canvas, with three goals:

  • A clear magnification feel at the lens center
  • Strong glass-like distortion plus subtle chromatic dispersion near the rim
  • No second rendering stack, by fully reusing the existing CanvasKit pipeline

This article documents the implementation approach, key ideas, and the pitfalls I ran into.

Project and Live Demo

demo

Why Post-Process Instead of an Overlay Layer

At first, the most intuitive approach seemed to be adding another canvas or WebGL layer dedicated to the lens effect.\
But that introduces two major issues:

  1. High synchronization cost between two render outputs (scrolling, zooming, and camera state must stay aligned)
  2. Interaction picking can conflict with visual composition

So I chose a different route: full-screen post-processing within the same CanvasKit pipeline.

The flow is straightforward:

  1. Render the full scene to an offscreen surface
  2. Apply a SkSL RuntimeEffect to the offscreen image
  3. Draw the processed result to the main canvas in one pass

Benefits of this design:

  • Scene logic is fully reused, with no need for a dual render tree
  • Effect control is centralized in shader code and uniforms
  • High compatibility with the existing rendering architecture

Breaking Down the Lens Effect

The current liquid glass lens consists of four parts:

  1. Center magnification: clear enlargement inside the lens
  2. Rim distortion: distortion concentrated near the outer ring
  3. Chromatic dispersion (RGB offset): subtle color fringing at the edge to enhance the glass feel
  4. Rim highlight + shadow: gives the lens a tangible physical presence

One key optimization I focused on:\
the center should magnify but not distort; distortion should be concentrated in the outer ~20% ring.

The implementation introduces a normalized radius t, then uses:

  • rim = smoothstep(0.8, 1.0, t) as the edge weight
  • Near-zero distortion weight at the center (t < 0.8)
  • Gradually increasing distortion and dispersion as t -> 1

Visually, this feels much closer to edge refraction of a real lens, instead of making the entire area wobble.

Animation and Performance: From Always Repainting to On-Demand Repainting

The lens is a pure uniform-driven post effect. Forcing a repaint every frame is simple, but unnecessarily expensive.\
I switched to an on-demand strategy:

  • On pointer movement, actively wake rendering via requestCanvasRepaint(canvas)
  • shouldContinueRepaint continues only while the lens is still chasing its target point
  • Continuous repainting stops automatically once the lens settles

This change significantly improved perceived performance: smooth while moving, no idle spinning when stopped.

Coordinate and Boundary Handling

On the web, coordinate mapping and edge sampling are the easiest places to make mistakes:

  • Convert pointer coordinates using getBoundingClientRect() plus backing-store scale
  • Clamp shader UV sampling with clamp(0..1) to avoid black edges or out-of-bounds artifacts

These details look minor, but they directly affect the final polish.

Key Takeaways

  1. In visual effects, deciding where not to apply effects is often as important as deciding what to apply
  2. For lens effects, a stable center plus expressive edges looks more natural than full-area distortion
  3. Once the architecture is right (a unified post-process pipeline), iterative tuning becomes much cheaper

If you're building glass, depth-of-field, or color-grading effects in Canvas/Skia scenes, I strongly recommend starting with the "offscreen scene + RuntimeEffect" approach. It scales much better for future extensions.

Top comments (0)