Caustics
Customize
Colors
Strength0.60
Waves0.35
Depth1.40
Scale1.00
Dispersion0.50
Chop0.55
Direction23°
Softness0.05
Bloom1.10
Sparkle1.25
Sparkle size1.00
Ray angle70°
Vignette0.25
Shade0.80
Speed1.0
Grain0.015
Seed0
Follow0.75
Touch0.50
Ripple strength0.35
Ripple size1.00
Ripple speed1.00
Usage
import { Caustics } from "@/components/uibrik/caustics/caustics";
<Caustics />Installation
npx shadcn@latest add https://uibrik.com/r/caustics.jsonAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/caustics/caustics.tsx"use client"; import { ShaderSurface, type ShaderBackgroundProps } from "../_shader-background/ShaderSurface"; import { causticsLight } from "./light"; // Sunlight on a pool floor. The light itself is worked out in light.ts by bending real rays through // a moving water surface; this pass lays it over the floor. The floor takes the colour list from // deep to shallow (all but the last colour), the light is tinted by the last colour, and the light // multiplies the floor colour the way light does, so bright cores lift toward the sun's tint // instead of being painted on top. const SHADER = /* glsl */ ` uniform sampler2D u_tex; // caustic light, a quarter of its real value per channel uniform float u_strength; // how strong the light is against the plain floor uniform float u_scale; uniform float u_shade; // how much the floor colour varies uniform float u_softness; // blur on the light, like a larger sun uniform float u_vignette; // darkening toward the corners uniform float u_bloom; // glow spilling around the brightest light uniform float u_sparkle; // star glints where the light peaks uniform float u_sparkleSize; // length of the glint rays uniform float u_rayAngle; // direction of the long glint rays, degrees vec3 lightAt(vec2 uv) { return textureLod(u_tex, uv, 0.0).rgb * 4.0; } vec3 shade(vec2 frag) { vec2 uv = frag / u_res; vec3 light = texture(u_tex, uv).rgb * 4.0; if (u_softness > 0.0) { vec2 o = u_softness * 6.0 * u_dpr / u_res; light = light * 0.4 + ( texture(u_tex, uv + vec2(o.x, 0.0)).rgb + texture(u_tex, uv - vec2(o.x, 0.0)).rgb + texture(u_tex, uv + vec2(0.0, o.y)).rgb + texture(u_tex, uv - vec2(0.0, o.y)).rgb ) * 0.6; } // the floor: slow, broad variation between the deeper and shallower colours float t = u_time * 0.03; vec2 p = frag / u_res.y / u_scale; float n = snoise(vec3(p * 0.8, t)) * 0.6 + snoise(vec3(p * 2.1 + 4.0, t * 1.4)) * 0.25; float top = float(u_count - 2) / float(u_count - 1); // the last colour is the light vec3 floorLab = paletteRamp(clamp(0.5 + 0.5 * n * u_shade, 0.0, 1.0) * top); vec3 floorCol = oklabToLinear(floorLab); vec3 sun = oklabToLinear(u_colors[u_count - 1]); // plain light is 1; focused light is more, the gaps between less vec3 lit = mix(vec3(1.0), light, u_strength); vec3 c = floorCol * (0.62 + 0.38 * lit) + sun * max(lit - 1.0, 0.0) * 0.16 * u_strength; // bloom: the light above plain daylight spills softly around itself, read from two blurred // (mipmap) levels of the light texture: smooth, and no extra samples if (u_bloom > 0.0) { vec3 wide = (textureLod(u_tex, uv, 1.5).rgb * 0.55 + textureLod(u_tex, uv, 3.0).rgb * 0.45) * 4.0; vec3 excess = max(wide - 1.0, 0.0) * 0.8; c += mix(sun, vec3(1.0), 0.5) * excess * u_bloom * 0.45 * u_strength; } // sun glints: a soft star wherever the light peaks. One candidate spot per cell, lit only when // the caustic light there is at its brightest; it twinkles, and its rays taper off softly. if (u_sparkle > 0.0) { float cs = 70.0 * max(u_sparkleSize, 1.0) * u_dpr; vec2 home = floor(frag / cs); float ra = radians(u_rayAngle); vec2 d1 = vec2(cos(ra), sin(ra)); vec2 d2 = vec2(-d1.y, d1.x); float star = 0.0; for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec2 cell = home + vec2(float(i), float(j)); vec2 spot = (cell + 0.2 + 0.6 * vec2(hash12(cell + 1.3), hash12(cell + 5.1))) * cs; float peak = smoothstep(1.1, 1.8, dot(lightAt(spot / u_res), vec3(0.3333))); if (peak <= 0.0) continue; float tw = 0.55 + 0.45 * sin(u_time * (2.0 + 2.0 * hash12(cell + 2.2)) + hash12(cell) * 40.0); vec2 d = frag - spot; float L = 34.0 * u_sparkleSize * u_dpr * (0.6 + 0.8 * hash12(cell + 9.0)); float w = 1.1 * u_dpr; float along1 = dot(d, d1); float along2 = dot(d, d2); float core = exp(-dot(d, d) / pow(3.5 * u_dpr * u_sparkleSize, 2.0)); float halo = exp(-dot(d, d) / pow(L * 0.55, 2.0)) * 0.45; float ray1 = exp(-pow(along2 / w, 2.0)) * exp(-pow(along1 / L, 2.0)); float ray2 = exp(-pow(along1 / w, 2.0)) * exp(-pow(along2 / (L * 0.55), 2.0)); star += (core + halo + 0.7 * ray1 + 0.4 * ray2) * peak * tw; } } c += mix(sun, vec3(1.0), 0.6) * star * u_sparkle * 0.55; } // depth haze: the far corners sink a little c *= 1.0 - 0.5 * u_vignette * dot(uv - 0.5, uv - 0.5); return c; } `; const COLORS = ["#0b3a53", "#0f6f8f", "#3fb6c9", "#e6fbff"]; export type CausticsProps = ShaderBackgroundProps & { /** Size of the waves; larger is calmer. */ scale?: number; /** Wave height. */ waves?: number; /** Floor depth: deeper water focuses the light into thinner, brighter lines. */ depth?: number; /** How strong the light is against the plain floor. */ strength?: number; /** Colour fringing at the edges of the light. */ dispersion?: number; /** How much the floor colour varies. */ shade?: number; /** Irregular chop on top of the wave trains; 0 leaves only smooth swells. */ chop?: number; /** Direction the main waves travel, in degrees. */ direction?: number; /** Blur on the light, like a larger sun. */ softness?: number; /** Darkening toward the corners. */ vignette?: number; /** Glow spilling around the brightest light. */ bloom?: number; /** Star-shaped sun glints where the light peaks. */ sparkle?: number; /** Length of the glint rays. */ sparkleSize?: number; /** Direction of the long glint rays, in degrees. */ rayAngle?: number; /** A click drops a ripple on the water. */ ripples?: boolean; /** Height of a click ripple. */ rippleStrength?: number; /** Wavelength of a click ripple; larger is broader. */ rippleSize?: number; /** How fast a ripple spreads. */ rippleSpeed?: number; /** How deep the pointer dips into the water. */ touch?: number; /** How lazily the fingertip dip follows the pointer, 0 (locked) to 1 (drifts after it). */ follow?: number; }; export function Caustics({ colors = COLORS, speed = 1, animate = true, interactive = true, grain = 0.015, seed = 0, scale = 1, waves = 0.35, depth = 1.4, strength = 0.6, dispersion = 0.5, shade = 0.8, chop = 0.55, direction = 23, softness = 0.05, vignette = 0.25, bloom = 1.1, sparkle = 1.25, sparkleSize = 1, rayAngle = 70, ripples = true, rippleStrength = 0.35, rippleSize = 1, rippleSpeed = 1, touch = 0.5, follow = 0.75, ...rest }: CausticsProps) { return ( <ShaderSurface shader={SHADER} prepass={causticsLight} uniforms={{ u_scale: scale, u_waves: waves, u_depth: depth, u_strength: strength, u_dispersion: dispersion, u_shade: shade, u_chop: chop, u_direction: direction, u_softness: softness, u_vignette: vignette, u_bloom: bloom, u_sparkle: sparkle, u_sparkleSize: sparkleSize, u_rayAngle: rayAngle, u_rippleStrength: rippleStrength, u_rippleSize: rippleSize, u_rippleSpeed: rippleSpeed, u_touch: touch, }} colors={colors} speed={speed} animate={animate} interactive={interactive} grain={grain} seed={seed} ripples={ripples} follow={follow} fallback={() => `radial-gradient(at 30% 20%, ${colors[colors.length - 1]}40, transparent 55%), linear-gradient(160deg, ${colors.slice(0, -1).join(", ")})` } {...rest} /> ); }
Adds the files to
components/uibrik/caustics. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/caustics.Props
Caustics- colors
- Type string[]
- ["#0b3a53","#0f6f8f","#3fb6c9","#e6fbff"]
- Pool floor from deep to shallow, then the light's tint last; 2 to 6 colours.
- strength
- Type number
- 0.6
- How strong the light is against the plain floor.
- waves
- Type number
- 0.35
- Wave height.
- depth
- Type number
- 1.4
- Floor depth: deeper water focuses the light into thinner, brighter lines.
- scale
- Type number
- 1
- Size of the waves; larger is calmer.
- dispersion
- Type number
- 0.5
- Colour fringing at the edges of the light.
- chop
- Type number
- 0.55
- Irregular chop on top of the wave trains; 0 leaves only smooth swells.
- direction
- Type number
- 23
- Direction the main waves travel.
- softness
- Type number
- 0.05
- Blur on the light, like a larger sun.
- bloom
- Type number
- 1.1
- Glow spilling around the brightest light.
- sparkle
- Type number
- 1.25
- Star-shaped sun glints where the light peaks.
- sparkleSize
- Type number
- 1
- Length of the glint rays.
- rayAngle
- Type number
- 70
- Direction of the long glint rays.
- vignette
- Type number
- 0.25
- Darkening toward the corners.
- shade
- Type number
- 0.8
- How much the floor colour varies.
- speed
- Type number
- 1
- Animation speed; 1 is the designed pace.
- grain
- Type number
- 0.015
- Film grain strength.
- seed
- Type number
- 0
- Shifts the composition, so two backgrounds with the same settings still differ.
- animate
- Type boolean
- true
- Move on its own. Off draws one still frame.
- interactive
- Type boolean
- true
- React to the pointer.
- follow
- Type number
- 0.75
- How lazily the fingertip dip follows the pointer, 0 (locked) to 1 (drifts after it).
- touch
- Type number
- 0.5
- How deep the pointer dips into the water.
- ripples
- Type boolean
- true
- A click drops a ripple on the water.
- rippleStrength
- Type number
- 0.35
- Height of a click ripple.
- rippleSize
- Type number
- 1
- Wavelength of a click ripple; larger is broader.
- rippleSpeed
- Type number
- 1
- How fast a ripple spreads.
- children
- Type ReactNode
- –
- Content shown on top of the background.
- className
- Type string
- –
- Classes for the box; it fills whatever size you give it.