Effects
SplashCursor
A WebGL fluid simulation that trails the pointer in coloured dye behind the page content.
Guidelines#
Use it for ambient decoration only. Max mounts it app-wide with quality="low" when Settings › Appearance › Splash cursor is on, and behind the public booking page.
What you provide:
quality:"high"(default): dye 768, simulation 128, 20 pressure iterations, pixel ratio up to 1.5."low": dye 512, simulation 128, 16 iterations, pixel ratio 1.
- Optional overrides, with their defaults:
CAPTURE_RESOLUTION512,DENSITY_DISSIPATION2.2,VELOCITY_DISSIPATION2,PRESSURE0.1,CURL3,SPLAT_RADIUS0.32,SPLAT_FORCE6000,SHADINGtrue,COLOR_UPDATE_SPEED10,BACK_COLOR{ r: 0.5, g: 0, b: 0 },TRANSPARENTtrue.SIM_RESOLUTION,DYE_RESOLUTIONandPRESSURE_ITERATIONSoverride the quality preset.
Anatomy: a pointer-events-none fixed inset-0 z-0 canvas. Sections with their own background cover it, and nothing underneath loses clicks.
Behaviour:
- It never starts under
prefers-reduced-motion(checked live), on coarse pointers, or on devices reporting 4 CPU cores or fewer, or 4 GB of memory or less. - The render loop stops 2s after the last input, once the dye has faded. It also pauses while the tab is hidden, and releases every GPU resource on unmount.
- If WebGL setup fails, it silently switches itself off.
In this preview the canvas is contained by a transform on the 320px box, and a short scripted sweep runs on load. The headless renderer reports 4 cores, so there the effect switches itself off and the box stays blank.
Don't mount more than one.
Facts#
| Group | Effects |
|---|---|
| Product | Max |
| Global | window.DigitalCrew.SplashCursor |
| Source | max-agent: src/components/ui/splash-cursor.tsx |
| Import in the app | import { SplashCursor } from "@/components/ui/splash-cursor"; |
API#
TypeScript declarations emitted from src/components/ui/splash-cursor.tsx.
export type SplashQuality = "high" | "low";
type SplashCursorProps = {
quality?: SplashQuality;
SIM_RESOLUTION?: number;
DYE_RESOLUTION?: number;
CAPTURE_RESOLUTION?: number;
DENSITY_DISSIPATION?: number;
VELOCITY_DISSIPATION?: number;
PRESSURE?: number;
PRESSURE_ITERATIONS?: number;
CURL?: number;
SPLAT_RADIUS?: number;
SPLAT_FORCE?: number;
SHADING?: boolean;
COLOR_UPDATE_SPEED?: number;
BACK_COLOR?: {
r: number;
g: number;
b: number;
};
TRANSPARENT?: boolean;
};
declare function SplashCursor(props: SplashCursorProps): JSX.Element;
export { SplashCursor };Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
function App() {
var ref = React.useRef(null);
React.useEffect(function () {
// One short scripted sweep so the panel is not blank before the viewer moves the pointer.
var box = ref.current, step = 0, timer;
function tick() {
if (!box || step > 40) return;
var r = box.getBoundingClientRect(), t = step / 40;
window.dispatchEvent(new MouseEvent("mousemove", {
clientX: r.left + r.width * (0.1 + 0.8 * t),
clientY: r.top + r.height * (0.5 + 0.28 * Math.sin(t * Math.PI * 2))
}));
step += 1;
timer = setTimeout(tick, 16);
}
timer = setTimeout(tick, 400);
return function () { clearTimeout(timer); };
}, []);
// The transform makes this box the containing block for SplashCursor's fixed, inset-0 canvas.
return h("div", { ref: ref, className: "relative h-[320px] overflow-hidden rounded-xl border bg-background [transform:translateZ(0)]" },
h(DC.SplashCursor, { quality: "low" }),
h("div", { className: "pointer-events-none relative z-10 flex h-full flex-col items-center justify-center gap-1 text-center" },
h("p", { className: "text-sm font-medium" }, "Move your pointer across this panel"),
h("p", { className: "max-w-xs text-xs text-muted-foreground" }, "Skipped under reduced motion, on touch screens, and on devices with 4 cores or 4 GB of memory or less.")));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>