Prism Burst
Installation
npx shadcn@latest add https://uibrik.com/r/prism-burst.jsonAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/prism-burst/prism-burst.tsx"use client"; import { ShaderSurface, type ShaderBackgroundProps } from "../_shader-background/ShaderSurface"; import { sourcePass } from "../_shader-background/source-pass"; // A burst of light through a prism. Built like a zoom-blur in a compositing tool: // source - broken chunks of light in a ring around a dark, blobby centre, with a fine streaky // texture (drawn into a texture by the pre-pass) // zoom blur - every pixel gathers light from points nearer the centre, so the chunks smear into // rays streaming outward // prism - red, green and blue are blurred over slightly different lengths, so every ray edge // splits into a rainbow; the rays are tinted around the circle from the colour list const SOURCE = /* glsl */ ` uniform float u_hole; // radius of the dark centre uniform float u_chunks; // how broken the ring of light is uniform float u_ringWidth; uniform float u_lean; vec2 centre() { return 0.5 * u_res + (u_pointer - 0.5 * u_res) * 0.15 * u_lean * u_pointerAmt; } float source(vec2 frag) { float t = u_time * 0.12; vec2 p = (frag - centre()) / u_res.y; float r = length(p); vec2 dir = p / max(r, 1e-4); // the dark centre is a lumpy blob, slowly changing shape float hole = u_hole * (1.0 + 0.3 * snoise(vec3(dir * 1.6, t)) + 0.12 * snoise(vec3(dir * 4.0, t * 1.3))); float d = r - hole; // light starts at the blob's edge and fades outward // (a steep falloff: linear light fades slowly to the eye, so a gentle tail would read as a ray) float ring = smoothstep(-0.035, 0.03, d) * exp(-pow(max(d, 0.0) / (0.045 * u_ringWidth), 2.0)); // broken into chunks around the circle float chunks = smoothstep(-0.3, 0.45, snoise(vec3(dir * 7.5 * u_chunks, t * 1.4))) * (0.6 + 0.4 * smoothstep(-0.3, 0.6, snoise(vec3(dir * 2.0, t * 0.7 + 5.0)))); // fine radial streaks inside each chunk, drifting outward float fine = 0.55 + 0.45 * snoise(vec3(dir * 38.0, log(r + 0.05) * 3.0 - t * 4.0)); // a faint glow inside the blob, so the centre is dark grey rather than flat black float inner = 0.006 * smoothstep(0.0, hole, r) * (1.0 - smoothstep(hole * 0.9, hole * 1.1, r)); return ring * chunks * fine * 1.6 + inner; } `; const PREPASS = sourcePass(SOURCE, 0.5); const SHADER = /* glsl */ ` uniform sampler2D u_tex; uniform float u_length; // how far the rays reach uniform float u_dispersion; // rainbow split along the ray edges uniform float u_intensity; uniform float u_iridescence; // how strongly the rays take the colour list uniform float u_lean; vec3 sampleRay(vec2 uv, vec2 c, float len) { // 28 taps from this pixel back toward the centre, weighted toward the near end vec3 acc = vec3(0.0); float wsum = 0.0; float jitter = hash12(uv * u_res); for (int i = 0; i < 28; i++) { float k = (float(i) + jitter) / 28.0; // light thins out as it travels: far taps (deep toward the centre) count for less, so each // ray is brightest at the ring and fades outward float w = pow(1.0 - k, 2.5); vec2 q = c + (uv - c) * vec2(1.0 - k * len); // red, green and blue each come from a slightly different distance and angle: the prism split, // which fringes both the ends and the sides of every ray float s = u_dispersion * 0.045 * k; float a = u_dispersion * 0.012; vec2 dq = (q - c); vec2 side = vec2(-dq.y, dq.x); float r = textureLod(u_tex, c + dq * (1.0 + s) + side * a, 0.5).r; float g = textureLod(u_tex, q, 0.5).g; float b = textureLod(u_tex, c + dq * (1.0 - s) - side * a, 0.5).b; acc += vec3(r, g, b) * w; wsum += w; } return acc / wsum * 4.0; } vec3 shade(vec2 frag) { vec2 uv = frag / u_res; vec2 c = (0.5 * u_res + (u_pointer - 0.5 * u_res) * 0.15 * u_lean * u_pointerAmt) / u_res; vec3 light = sampleRay(uv, c, 0.55 * u_length); // a touch of the sharp source on top, so ray roots keep some crispness light += textureLod(u_tex, uv, 1.5).rgb * 4.0 * 0.35; // tint around the circle from the colour list, leaving the brightest parts white vec2 p = (frag - c * u_res) / u_res.y; float ang = atan(p.y, p.x) / 6.2831853 + 0.5; vec3 tint = oklabToLinear(paletteLoop(ang * 2.0 + u_time * 0.02)); float lum = dot(light, vec3(0.333)); vec3 col = light * mix(vec3(1.0), tint * 1.3, u_iridescence * (1.0 - smoothstep(0.6, 1.6, lum))); // a dark frame edge, as in a lens col *= 1.0 - 0.55 * smoothstep(0.35, 0.95, length(p)); col = 1.0 - exp(-col * u_intensity * 2.6); return oklabToLinear(u_bg) + col; } `; const COLORS = ["#ffd9b0", "#c9d6ff", "#ffc6e4", "#d8fff0", "#fff1c7"]; export type PrismBurstProps = ShaderBackgroundProps & { /** Radius of the dark centre. */ hole?: number; /** How far the rays reach. */ length?: number; /** Rainbow split along the ray edges. */ dispersion?: number; /** How broken the ring of light is; higher makes more, smaller chunks. */ chunks?: number; /** Width of the ring the rays start from. */ ringWidth?: number; /** Brightness. */ intensity?: number; /** How strongly the rays take the colour list; 0 is plain white light. */ iridescence?: number; /** Colour behind the light. */ background?: string; /** How far the centre drifts toward the pointer. */ lean?: number; /** How lazily it follows the pointer, 0 (locked) to 1 (drifts after it). */ follow?: number; }; export function PrismBurst({ colors = COLORS, speed = 1, animate = true, interactive = true, grain = 0.07, seed = 0, hole = 0.25, length = 1, dispersion = 1.95, chunks = 0.6, ringWidth = 1.8, intensity = 0.95, iridescence = 0.8, background = "#050506", lean = 0.6, follow = 0.8, ...rest }: PrismBurstProps) { return ( <ShaderSurface shader={SHADER} prepass={PREPASS} uniforms={{ u_hole: hole, u_length: length, u_dispersion: dispersion, u_chunks: chunks, u_ringWidth: ringWidth, u_intensity: intensity, u_iridescence: iridescence, u_lean: lean, }} colors={colors} speed={speed} animate={animate} interactive={interactive} grain={grain} seed={seed} background={background} follow={follow} fallback={(_, base) => `radial-gradient(circle at 50% 50%, ${base} ${Math.round(hole * 60)}%, #ffffffaa ${Math.round(hole * 75)}%, ${colors[0]}55 ${Math.round(hole * 120)}%, ${base} 75%)` } {...rest} /> ); }
Adds the files to
components/uibrik/prism-burst. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/prism-burst.Props
PrismBurst- colors
- Type string[]
- ["#ffd9b0","#c9d6ff","#ffc6e4","#d8fff0","#fff1c7"]
- Tints the rays take around the circle; the brightest parts stay white.
- background
- Type string
- "#050506"
- Colour behind the light.
- hole
- Type number
- 0.25
- Radius of the dark centre.
- length
- Type number
- 1
- How far the rays reach.
- dispersion
- Type number
- 1.95
- Rainbow split along the ray edges.
- chunks
- Type number
- 0.6
- How broken the ring of light is; higher makes more, smaller chunks.
- ringWidth
- Type number
- 1.8
- Width of the ring the rays start from.
- intensity
- Type number
- 0.95
- Brightness.
- iridescence
- Type number
- 0.8
- How strongly the rays take the colour list; 0 is plain white light.
- speed
- Type number
- 1
- Animation speed; 1 is the designed pace.
- grain
- Type number
- 0.07
- 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.
- lean
- Type number
- 0.6
- How far the centre drifts toward the pointer.
- follow
- Type number
- 0.8
- How lazily it follows the pointer, 0 (locked) to 1 (drifts after it).
- children
- Type ReactNode
- –
- Content shown on top of the background.
- className
- Type string
- –
- Classes for the box; it fills whatever size you give it.