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
- GitHub repository: https://github.com/ouzhou/react-canvas
- Live demo: https://react-canvas-design.vercel.app/#/devto
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:
- High synchronization cost between two render outputs (scrolling, zooming, and camera state must stay aligned)
- 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:
- Render the full scene to an offscreen surface
- Apply a SkSL
RuntimeEffectto the offscreen image - 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:
- Center magnification: clear enlargement inside the lens
- Rim distortion: distortion concentrated near the outer ring
- Chromatic dispersion (RGB offset): subtle color fringing at the edge to enhance the glass feel
- 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) -
shouldContinueRepaintcontinues 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
- In visual effects, deciding where not to apply effects is often as important as deciding what to apply
- For lens effects, a stable center plus expressive edges looks more natural than full-area distortion
- 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)