← All primitives
ContourField
MapA topographic map: flowing elevation contours from a procedural field with crisp resolution-independent isolines that glow. Reads instantly as terrain. Zero data, pointer-reactive, palette-driven.
$
npx shadcn@latest add https://prism.icglabs.co/r/contour-field.jsonDependencies:three@react-three/fiber
View raw manifest →components/prism/ContourField.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";
/**
* ContourField — a topographic "map": flowing elevation contour lines drawn from
* a procedural noise field with resolution-independent isolines (fwidth), tinted
* low→high and glowing on each contour. Reads instantly as terrain/elevation.
* Self-contained, pointer-reactive, palette-driven.
*/
export type ContourFieldProps = {
/** [low terrain, high terrain, contour-line glow] hex. */
palette?: [string, string, string];
speed?: 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 vec2 uMouse;
uniform float uAspect;
uniform vec3 uLow;
uniform vec3 uHigh;
uniform vec3 uLine;
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),f=fract(p); vec2 u=f*f*(3.0-2.0*f);
float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y); }
float fbm(vec2 p){ float v=0.0,a=0.5; for(int i=0;i<5;i++){ v+=a*noise(p); p*=2.0; a*=0.5; } return v; }
void main(){
vec2 uv = vUv; uv.x *= uAspect;
uv += (uMouse - 0.5) * 0.3;
float t = uTime * 0.04;
float h = fbm(uv * 3.0 + vec2(t, t * 0.5));
// crisp, resolution-independent isolines
float N = 13.0;
float d = fwidth(h * N);
float lines = abs(fract(h * N - 0.5) - 0.5) / max(d, 1e-4);
float c = 1.0 - min(lines, 1.0);
vec3 terrain = mix(uLow, uHigh, smoothstep(0.15, 0.85, h));
vec3 col = terrain * (0.45 + 0.4 * h) + c * uLine * 1.5;
float vig = smoothstep(1.05, 0.3, length(vUv - 0.5));
col *= mix(0.3, 1.0, vig);
col += (hash(vUv * 900.0 + t) - 0.5) * 0.02; // dither
gl_FragColor = vec4(col, 1.0);
}
`;
function toV3(hex: string) {
const c = new THREE.Color(hex);
return new THREE.Vector3(c.r, c.g, c.b);
}
function FieldMesh({ palette, speed }: { palette: [string, string, string]; speed: 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 },
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
uAspect: { value: 1 },
uLow: { value: toV3(palette[0]) },
uHigh: { value: toV3(palette[1]) },
uLine: { value: toV3(palette[2]) },
}),
[palette]
);
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;
const target = mouse.current;
target.set((state.pointer.x + 1) / 2, (state.pointer.y + 1) / 2);
m.uniforms.uMouse.value.lerp(target, 0.05);
});
return (
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial ref={mat} vertexShader={VERT} fragmentShader={FRAG} uniforms={uniforms} />
</mesh>
);
}
export function ContourField({
palette = ["#0c1030", "#5b7cff", "#1fd4e6"],
speed = 1,
className,
}: ContourFieldProps) {
const q = useDeviceTier();
return (
<div className={className} style={{ width: "100%", height: "100%" }}>
<Canvas dpr={q.dpr} gl={{ antialias: false, powerPreference: "high-performance" }}>
<FieldMesh palette={palette} speed={speed} />
<AutoDpr />
<PauseWhenOffscreen />
</Canvas>
</div>
);
}