← All primitives
ShaderField
ShaderFull-screen domain-warped GLSL noise refracted through a color spectrum. Pointer-reactive, resolution-independent, zero geometry.
$
npx shadcn@latest add https://prism.icglabs.co/r/shader-field.jsonDependencies:three@react-three/fiber
View raw manifest →components/prism/ShaderField.tsx
"use client";
import { useMemo, useRef } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { useDeviceTier } from "@/lib/useDeviceTier";
import { AutoDpr, PauseWhenOffscreen } from "@/components/showcase/CanvasPerf";
/**
* ShaderField — a full-screen fragment shader: domain-warped fractal noise
* refracted through a configurable color spectrum. Flows on its own, drifts
* toward the pointer. Zero geometry beyond a clip-space quad, so it's cheap and
* resolution-independent. Custom GLSL — the kind of "signature surface" that
* defines award-winning sites.
*/
export type ShaderFieldProps = {
/** Exactly 5 hex stops sampled across the noise field. */
palette?: [string, string, string, string, string];
speed?: number;
/** Higher = more turbulent warping. */
warp?: number;
className?: string;
};
const VERT = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`;
const FRAG = /* glsl */ `
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform float uWarp;
uniform vec2 uMouse;
uniform float uAspect;
uniform vec3 uPalette[5];
// hash / value-noise / fbm
float hash(vec2 p) {
p = fract(p * vec2(123.34, 345.45));
p += dot(p, p + 34.345);
return fract(p.x * p.y);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
vec3 prism(float t) {
vec3 c = mix(uPalette[0], uPalette[1], smoothstep(0.0, 0.25, t));
c = mix(c, uPalette[2], smoothstep(0.25, 0.5, t));
c = mix(c, uPalette[3], smoothstep(0.5, 0.75, t));
c = mix(c, uPalette[4], smoothstep(0.75, 1.0, t));
return c;
}
void main() {
vec2 uv = vUv;
uv.x *= uAspect;
float t = uTime * 0.08;
// pointer pulls the field
vec2 m = (uMouse - 0.5) * 0.6;
uv += m;
// single-pass domain warp (Inigo Quilez style) — cheap but rich
vec2 q = vec2(fbm(uv + t), fbm(uv + vec2(5.2, 1.3) - t));
float f = fbm(uv + uWarp * q + 0.12 * t);
float shade = clamp(f * f * 2.4 + 0.15, 0.0, 1.0);
vec3 col = prism(clamp(f + 0.25 * q.x, 0.0, 1.0));
col *= 0.35 + 0.9 * shade;
// bright refraction filaments where the warp folds
float fil = smoothstep(0.78, 0.92, f) * 0.6;
col += fil * mix(uPalette[2], uPalette[3], q.y);
// vignette toward obsidian
vec2 vc = vUv - 0.5;
float vig = smoothstep(0.95, 0.25, length(vc));
col *= mix(0.25, 1.0, vig);
// subtle dithered grain to kill banding
float g = hash(vUv * 850.0 + t) * 0.025 - 0.0125;
col += g;
gl_FragColor = vec4(col, 1.0);
}
`;
function paletteToFloats(palette: string[]): Float32Array {
const arr = new Float32Array(15);
for (let i = 0; i < 5; i++) {
const c = new THREE.Color(palette[Math.min(i, palette.length - 1)]);
arr[i * 3] = c.r;
arr[i * 3 + 1] = c.g;
arr[i * 3 + 2] = c.b;
}
return arr;
}
function FieldMesh({
palette,
speed,
warp,
}: {
palette: string[];
speed: number;
warp: number;
}) {
const mat = useRef<THREE.ShaderMaterial>(null);
const mouse = useRef(new THREE.Vector2(0.5, 0.5));
const size = useThree((s) => s.size);
const reduced =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const uniforms = useMemo(
() => ({
uTime: { value: 0 },
uWarp: { value: warp },
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
uAspect: { value: 1 },
uPalette: { value: paletteToFloats(palette) },
}),
[palette, warp]
);
useFrame((state, delta) => {
const m = mat.current;
if (!m) return;
if (!reduced) m.uniforms.uTime.value += delta * speed;
m.uniforms.uAspect.value = size.width / size.height;
// ease toward pointer (-1..1 → 0..1)
const target = mouse.current;
target.set((state.pointer.x + 1) / 2, (state.pointer.y + 1) / 2);
m.uniforms.uMouse.value.lerp(target, 0.04);
});
return (
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial ref={mat} vertexShader={VERT} fragmentShader={FRAG} uniforms={uniforms} />
</mesh>
);
}
export function ShaderField({
palette = ["#0a0a14", "#5b7cff", "#1fd4e6", "#8b5cff", "#ff3d81"],
speed = 1,
warp = 4.0,
className,
}: ShaderFieldProps) {
const q = useDeviceTier();
return (
<div className={className} style={{ width: "100%", height: "100%" }}>
{/* dpr is a range; AdaptiveDpr scales it by measured FPS and the loop pauses
off-screen — a full-bleed fragment shader stays cheap even on weak GPUs. */}
<Canvas dpr={q.dpr} gl={{ antialias: false, powerPreference: "high-performance" }}>
<FieldMesh palette={palette} speed={speed} warp={warp} />
<AutoDpr />
<PauseWhenOffscreen />
</Canvas>
</div>
);
}