Shader Lens Blur
Shader Lens Blur is a React and Tailwind CSS effect component from Cult UI, licensed MIT. Copy it and paste it into your project.
Cult UI
MIT
effects
What it is
Shader Lens Blur is a React and Tailwind CSS effect component from Cult UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i jotai motion next-themes three
Licence
This component comes from Cult UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Shader Lens Blur
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { atom, useAtom } from "jotai"
import { motion } from "motion/react"
import { useTheme } from "next-themes"
import * as THREE from "three"
const fragmentShader = `
varying vec2 v_texcoord;
uniform vec2 u_mouse;
uniform vec2 u_resolution;
uniform float u_pixelRatio;
uniform float u_time;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_color3;
uniform vec3 u_color4;
uniform float u_hoverStrength;
uniform bool u_invertMouse;
uniform bool u_isDarkMode;
#define PI 3.1415926535897932384626433832795
#define TWO_PI 6.2831853071795864769252867665590
vec2 coord(in vec2 p) {
p = p / u_resolution.xy;
if (u_resolution.x > u_resolution.y) {
p.x *= u_resolution.x / u_resolution.y;
p.x += (u_resolution.y - u_resolution.x) / u_resolution.y / 2.0;
} else {
p.y *= u_resolution.y / u_resolution.x;
p.y += (u_resolution.x - u_resolution.y) / u_resolution.x / 2.0;
}
p -= 0.5;
p *= vec2(-1.0, 1.0);
return p;
}
#define st0 coord(gl_FragCoord.xy)
#define mx coord(u_mouse * u_pixelRatio)
float sdRoundRect(vec2 p, vec2 b, float r) {
vec2 d = abs(p - 0.5) * 4.2 - b + vec2(r);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
}
float sdCircle(in vec2 st, in vec2 center) {
return length(st - center) * 2.0;
}
float sdPoly(in vec2 p, in float w, in int sides) {
float a = atan(p.x, p.y) + PI;
float r = TWO_PI / float(sides);
float d = cos(floor(0.5 + a / r) * r - a) * length(max(abs(p) * 1.0, 0.0));
return d * 2.0 - w;
}
float aastep(float threshold, float value) {
float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757;
return smoothstep(threshold - afwidth, threshold + afwidth, value);
}
float fill(float x, float size, float edge) {
return 1.0 - smoothstep(size - edge, size + edge, x);
}
float stroke(float x, float size, float w, float edge) {
float d = smoothstep(size - edge, size + edge, x + w * 0.5) - smoothstep(size - edge, size + edge, x - w * 0.5);
return clamp(d, 0.0, 1.0);
}
void main() {
vec2 st = st0 + 0.5;
vec2 posMouse = mx + 0.5;
float size = 1.2 + sin(u_time) * 0.1;
float roundness = 0.4 + sin(u_time * 0.5) * 0.1;
float borderSize = 0.05 + sin(u_time * 0.7) * 0.02;
float circleSize = 0.3 + sin(u_time * 0.8) * 0.05;
float circleEdge = 0.5 + sin(u_time * 0.6) * 0.1;
float sdfCircle = fill(
sdCircle(st, posMouse),
circleSize,
circleEdge
);
float sdf;
if (VAR == 0) {
sdf = sdRoundRect(st, vec2(size), roundness);
sdf = stroke(sdf, 0.0, borderSize, sdfCircle) * 4.0;
} else if (VAR == 1) {
sdf = sdCircle(st, vec2(0.5));
sdf = fill(sdf, 0.6, sdfCircle) * 1.2;
} else if (VAR == 2) {
sdf = sdCircle(st, vec2(0.5));
sdf = stroke(sdf, 0.58, 0.02, sdfCircle) * 4.0;
} else if (VAR == 3) {
sdf = sdPoly(st - vec2(0.5, 0.45), 0.3, 3);
sdf = fill(sdf, 0.05, sdfCircle) * 1.4;
}
vec3 gradient = mix(
mix(u_color1, u_color2, 0.5 + 0.5 * cos(u_time + st.x + 0.0)),
mix(u_color3, u_color4, 0.5 + 0.5 * cos(u_time + st.y + 2.0)),
0.5 + 0.5 * cos(u_time + st.x + st.y + 4.0)
);
vec3 shapeColor = sdf * gradient;
float mouseEffect = u_invertMouse ? 1.0 - sdfCircle : sdfCircle;
shapeColor = mix(shapeColor, vec3(1.0) - shapeColor, u_hoverStrength * mouseEffect);
if (u_isDarkMode) {
shapeColor = mix(shapeColor, vec3(1.0), 0.2);
} else {
shapeColor = mix(shapeColor, vec3(0.0), 0.1);
}
gl_FragColor = vec4(shapeColor, sdf);
}
`
interface ShaderConfig {
variation: number
color1: string
color2: string
color3: string
color4: string
enableHover: boolean
invertMouse: boolean
width: string
height: string
}
const initialState: ShaderConfig = {
variation: 3,
color1: "#D5F981",
color2: "#A1BBE7",
color3: "#F2BAE2",
color4: "#68E8FA",
enableHover: true,
invertMouse: true,
width: "100%",
height: "400px",
}
export const configAtom = atom<ShaderConfig>(initialState)
export function ShaderLensBlur() {
const [config] = useAtom(configAtom)
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const rendererRef = useRef<THREE.WebGLRenderer | null>(null)
const sceneRef = useRef<THREE.Scene | null>(null)
const cameraRef = useRef<THREE.OrthographicCamera | null>(null)
const materialRef = useRef<THREE.ShaderMaterial | null>(null)
const rafRef = useRef<number | null>(null)
const pixelRatioRef = useRef(1)
const [isInteracting, setIsInteracting] = useState(false)
const { theme } = useTheme()
const updateSize = useCallback(() => {
if (
!containerRef.current ||
!canvasRef.current ||
!rendererRef.current ||
!cameraRef.current ||
!materialRef.current
)
return
const { clientWidth: w, clientHeight: h } = containerRef.current
const aspect = w / h
cameraRef.current.left = -aspect
cameraRef.current.right = aspect
cameraRef.current.top = 1
cameraRef.current.bottom = -1
cameraRef.current.updateProjectionMatrix()
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
pixelRatioRef.current = pixelRatio
rendererRef.current.setPixelRatio(pixelRatio)
rendererRef.current.setSize(w, h)
const drawingBufferSize = rendererRef.current.getDrawingBufferSize(
new THREE.Vector2()
)
materialRef.current.uniforms.u_resolution.value.copy(drawingBufferSize)
materialRef.current.uniforms.u_pixelRatio.value = pixelRatio
}, [])
const updateMousePosition = useCallback(
(x: number, y: number) => {
if (!containerRef.current || !materialRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const mouseX = x - rect.left
const mouseY = y - rect.top
if (isInteracting || config.enableHover) {
materialRef.current.uniforms.u_mouse.value.set(
mouseX,
rect.height - mouseY
)
}
},
[isInteracting, config.enableHover]
)
const animate = useCallback(
(time: number) => {
if (
!rendererRef.current ||
!sceneRef.current ||
!cameraRef.current ||
!materialRef.current
)
return
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
if (pixelRatio !== pixelRatioRef.current) {
updateSize()
}
materialRef.current.uniforms.u_time.value = time * 0.001
materialRef.current.uniforms.u_hoverStrength.value =
isInteracting || config.enableHover ? 0.3 : 0
rendererRef.current.render(sceneRef.current, cameraRef.current)
rafRef.current = requestAnimationFrame(animate)
},
[config.enableHover, isInteracting, updateSize]
)
useEffect(() => {
if (!containerRef.current || !canvasRef.current) return
const scene = new THREE.Scene()
sceneRef.current = scene
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000)
camera.position.z = 1
cameraRef.current = camera
const renderer = new THREE.WebGLRenderer({
canvas: canvasRef.current,
antialias: true,
alpha: true,
})
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setClearColor(0x000000, 0)
rendererRef.current = renderer
const geometry = new THREE.PlaneGeometry(2, 2)
const material = new THREE.ShaderMaterial({
vertexShader: `
varying vec2 v_texcoord;
void main() {
gl_Position = vec4(position, 1.0);
v_texcoord = uv;
}
`,
fragmentShader,
uniforms: {
u_mouse: { value: new THREE.Vector2() },
u_resolution: { value: new THREE.Vector2() },
u_pixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
u_time: { value: 0 },
u_color1: { value: new THREE.Color(config.color1) },
u_color2: { value: new THREE.Color(config.color2) },
u_color3: { value: new THREE.Color(config.color3) },
u_color4: { value: new THREE.Color(config.color4) },
u_hoverStrength: { value: 0 },
u_invertMouse: { value: config.invertMouse },
u_isDarkMode: { value: theme === "dark" },
},
defines: {
VAR: config.variation,
},
transparent: true,
blending: THREE.NormalBlending,
})
materialRef.current = material
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
updateSize()
rafRef.current = requestAnimationFrame(animate)
const resizeObserver = new ResizeObserver(updateSize)
resizeObserver.observe(containerRef.current)
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current)
if (rendererRef.current) rendererRef.current.dispose()
if (containerRef.current) resizeObserver.unobserve(containerRef.current)
}
}, [config, updateSize, animate, theme])
useEffect(() => {
const container = containerRef.current
if (!container) return
const handlePointerMove = (event: PointerEvent) =>
updateMousePosition(event.clientX, event.clientY)
const handleTouchMove = (event: TouchEvent) => {
if (event.touches.length > 0) {
const touch = event.touches[0]
updateMousePosition(touch.clientX, touch.clientY)
}
}
const handlePointerDown = () => setIsInteracting(true)
const handlePointerUp = () => setIsInteracting(false)
const handleTouchStart = () => setIsInteracting(true)
const handleTouchEnd = () => setIsInteracting(false)
container.addEventListener("pointermove", handlePointerMove)
container.addEventListener("touchmove", handleTouchMove)
container.addEventListener("pointerdown", handlePointerDown)
container.addEventListener("pointerup", handlePointerUp)
container.addEventListener("touchstart", handleTouchStart)
container.addEventListener("touchend", handleTouchEnd)
return () => {
container.removeEventListener("pointermove", handlePointerMove)
container.removeEventListener("touchmove", handleTouchMove)
container.removeEventListener("pointerdown", handlePointerDown)
container.removeEventListener("pointerup", handlePointerUp)
container.removeEventListener("touchstart", handleTouchStart)
container.removeEventListener("touchend", handleTouchEnd)
}
}, [updateMousePosition])
useEffect(() => {
if (materialRef.current) {
materialRef.current.uniforms.u_color1.value.set(config.color1)
materialRef.current.uniforms.u_color2.value.set(config.color2)
materialRef.current.uniforms.u_color3.value.set(config.color3)
materialRef.current.uniforms.u_color4.value.set(config.color4)
materialRef.current.uniforms.u_invertMouse.value = config.invertMouse
materialRef.current.uniforms.u_isDarkMode.value = theme === "dark"
materialRef.current.defines.VAR = config.variation
materialRef.current.needsUpdate = true
}
}, [config, theme])
return (
<motion.div
ref={containerRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="relative"
style={{
width: config.width,
height: config.height,
backgroundColor: "black",
borderRadius: "8px",
}}
>
<canvas ref={canvasRef} className="w-full h-full touch-none" />
<div
className={`absolute bottom-4 left-4 text-sm ${
theme === "dark" ? "text-neutral-300" : "text-neutral-200"
}`}
>
{config.enableHover
? `${
config.invertMouse ? "Inverted mouse" : "Normal mouse"
} interaction`
: "Interact with the animation"}{" "}
using mouse or touch
</div>
</motion.div>
)
}
export default ShaderLensBlur
// "use client"
// // npm install jotai three
// import React, { useCallback, useEffect, useRef, useState } from "react"
// import { motion } from "motion/react"
// import { atom, useAtom } from "jotai"
// import * as THREE from "three"
// const fragmentShader = `
// varying vec2 v_texcoord;
// uniform vec2 u_mouse;
// uniform vec2 u_resolution;
// uniform float u_pixelRatio;
// uniform float u_time;
// uniform vec3 u_color1;
// uniform vec3 u_color2;
// uniform vec3 u_color3;
// uniform vec3 u_color4;
// uniform float u_hoverStrength;
// uniform bool u_invertMouse;
// #define PI 3.1415926535897932384626433832795
// #define TWO_PI 6.2831853071795864769252867665590
// vec2 coord(in vec2 p) {
// p = p / u_resolution.xy;
// if (u_resolution.x > u_resolution.y) {
// p.x *= u_resolution.x / u_resolution.y;
// p.x += (u_resolution.y - u_resolution.x) / u_resolution.y / 2.0;
// } else {
// p.y *= u_resolution.y / u_resolution.x;
// p.y += (u_resolution.x - u_resolution.y) / u_resolution.x / 2.0;
// }
// p -= 0.5;
// p *= vec2(-1.0, 1.0);
// return p;
// }
// #define st0 coord(gl_FragCoord.xy)
// #define mx coord(u_mouse)
// float sdRoundRect(vec2 p, vec2 b, float r) {
// vec2 d = abs(p - 0.5) * 4.2 - b + vec2(r);
// return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
// }
// float sdCircle(in vec2 st, in vec2 center) {
// return length(st - center) * 2.0;
// }
// float sdPoly(in vec2 p, in float w, in int sides) {
// float a = atan(p.x, p.y) + PI;
// float r = TWO_PI / float(sides);
// float d = cos(floor(0.5 + a / r) * r - a) * length(max(abs(p) * 1.0, 0.0));
// return d * 2.0 - w;
// }
// float aastep(float threshold, float value) {
// float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757;
// return smoothstep(threshold - afwidth, threshold + afwidth, value);
// }
// float fill(float x, float size, float edge) {
// return 1.0 - smoothstep(size - edge, size + edge, x);
// }
// float stroke(float x, float size, float w, float edge) {
// float d = smoothstep(size - edge, size + edge, x + w * 0.5) - smoothstep(size - edge, size + edge, x - w * 0.5);
// return clamp(d, 0.0, 1.0);
// }
// void main() {
// vec2 st = st0 + 0.5;
// vec2 posMouse = mx + 0.5;
// float size = 1.2 + sin(u_time) * 0.1;
// float roundness = 0.4 + sin(u_time * 0.5) * 0.1;
// float borderSize = 0.05 + sin(u_time * 0.7) * 0.02;
// float circleSize = 0.3 + sin(u_time * 0.8) * 0.05;
// float circleEdge = 0.5 + sin(u_time * 0.6) * 0.1;
// float sdfCircle = fill(
// sdCircle(st, posMouse),
// circleSize,
// circleEdge
// );
// float sdf;
// if (VAR == 0) {
// sdf = sdRoundRect(st, vec2(size), roundness);
// sdf = stroke(sdf, 0.0, borderSize, sdfCircle) * 4.0;
// } else if (VAR == 1) {
// sdf = sdCircle(st, vec2(0.5));
// sdf = fill(sdf, 0.6, sdfCircle) * 1.2;
// } else if (VAR == 2) {
// sdf = sdCircle(st, vec2(0.5));
// sdf = stroke(sdf, 0.58, 0.02, sdfCircle) * 4.0;
// } else if (VAR == 3) {
// sdf = sdPoly(st - vec2(0.5, 0.45), 0.3, 3);
// sdf = fill(sdf, 0.05, sdfCircle) * 1.4;
// }
// vec3 gradient = mix(
// mix(u_color1, u_color2, 0.5 + 0.5 * cos(u_time + st.x + 0.0)),
// mix(u_color3, u_color4, 0.5 + 0.5 * cos(u_time + st.y + 2.0)),
// 0.5 + 0.5 * cos(u_time + st.x + st.y + 4.0)
// );
// vec3 shapeColor = sdf * gradient;
// float mouseEffect = u_invertMouse ? 1.0 - sdfCircle : sdfCircle;
// shapeColor = mix(shapeColor, vec3(1.0) - shapeColor, u_hoverStrength * mouseEffect);
// gl_FragColor = vec4(shapeColor, sdf);
// }
// `
// interface ShaderConfig {
// variation: number
// color1: string
// color2: string
// color3: string
// color4: string
// enableHover: boolean
// invertMouse: boolean
// width: string
// height: string
// }
// const initialState: ShaderConfig = {
// variation: 0,
// color1: "#ff0000",
// color2: "#00ff00",
// color3: "#0000ff",
// color4: "#ffff00",
// enableHover: true,
// invertMouse: true,
// width: "100%",
// height: "400px",
// }
// export const configAtom = atom<ShaderConfig>(initialState)
// export function ShaderLensBlur() {
// const [config] = useAtom(configAtom)
// const containerRef = useRef<HTMLDivElement>(null)
// const canvasRef = useRef<HTMLCanvasElement>(null)
// const rendererRef = useRef<THREE.WebGLRenderer | null>(null)
// const sceneRef = useRef<THREE.Scene | null>(null)
// const cameraRef = useRef<THREE.OrthographicCamera | null>(null)
// const materialRef = useRef<THREE.ShaderMaterial | null>(null)
// const rafRef = useRef<number | null>(null)
// const [isInteracting, setIsInteracting] = useState(false)
// const updateSize = useCallback(() => {
// if (
// !containerRef.current ||
// !canvasRef.current ||
// !rendererRef.current ||
// !cameraRef.current ||
// !materialRef.current
// )
// return
// const { clientWidth: w, clientHeight: h } = containerRef.current
// const aspect = w / h
// cameraRef.current.left = -aspect
// cameraRef.current.right = aspect
// cameraRef.current.top = 1
// cameraRef.current.bottom = -1
// cameraRef.current.updateProjectionMatrix()
// rendererRef.current.setSize(w, h)
// materialRef.current.uniforms.u_resolution.value.set(w, h)
// }, [])
// const updateMousePosition = useCallback(
// (x: number, y: number) => {
// if (!containerRef.current || !materialRef.current) return
// const rect = containerRef.current.getBoundingClientRect()
// const mouseX = x - rect.left
// const mouseY = y - rect.top
// if (isInteracting || config.enableHover) {
// materialRef.current.uniforms.u_mouse.value.set(
// mouseX,
// rect.height - mouseY
// )
// }
// },
// [isInteracting, config.enableHover]
// )
// const animate = useCallback(
// (time: number) => {
// if (
// !rendererRef.current ||
// !sceneRef.current ||
// !cameraRef.current ||
// !materialRef.current
// )
// return
// materialRef.current.uniforms.u_time.value = time * 0.001
// materialRef.current.uniforms.u_hoverStrength.value =
// isInteracting || config.enableHover ? 0.3 : 0
// rendererRef.current.render(sceneRef.current, cameraRef.current)
// rafRef.current = requestAnimationFrame(animate)
// },
// [config.enableHover, isInteracting]
// )
// useEffect(() => {
// if (!containerRef.current || !canvasRef.current) return
// const scene = new THREE.Scene()
// sceneRef.current = scene
// const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000)
// camera.position.z = 1
// cameraRef.current = camera
// const renderer = new THREE.WebGLRenderer({
// canvas: canvasRef.current,
// antialias: true,
// alpha: true,
// })
// renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// renderer.setClearColor(0x000000, 0)
// rendererRef.current = renderer
// const geometry = new THREE.PlaneGeometry(2, 2)
// const material = new THREE.ShaderMaterial({
// vertexShader: `
// varying vec2 v_texcoord;
// void main() {
// gl_Position = vec4(position, 1.0);
// v_texcoord = uv;
// }
// `,
// fragmentShader,
// uniforms: {
// u_mouse: { value: new THREE.Vector2() },
// u_resolution: { value: new THREE.Vector2() },
// u_pixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
// u_time: { value: 0 },
// u_color1: { value: new THREE.Color(config.color1) },
// u_color2: { value: new THREE.Color(config.color2) },
// u_color3: { value: new THREE.Color(config.color3) },
// u_color4: { value: new THREE.Color(config.color4) },
// u_hoverStrength: { value: 0 },
// u_invertMouse: { value: config.invertMouse },
// },
// defines: {
// VAR: config.variation,
// },
// transparent: true,
// blending: THREE.NormalBlending,
// })
// materialRef.current = material
// const mesh = new THREE.Mesh(geometry, material)
// scene.add(mesh)
// updateSize()
// rafRef.current = requestAnimationFrame(animate)
// const resizeObserver = new ResizeObserver(updateSize)
// resizeObserver.observe(containerRef.current)
// return () => {
// if (rafRef.current) cancelAnimationFrame(rafRef.current)
// if (rendererRef.current) rendererRef.current.dispose()
// if (containerRef.current) resizeObserver.unobserve(containerRef.current)
// }
// }, [config, updateSize, animate])
// useEffect(() => {
// const container = containerRef.current
// if (!container) return
// const handlePointerMove = (event: PointerEvent) =>
// updateMousePosition(event.clientX, event.clientY)
// const handleTouchMove = (event: TouchEvent) => {
// if (event.touches.length > 0) {
// const touch = event.touches[0]
// updateMousePosition(touch.clientX, touch.clientY)
// }
// }
// container.addEventListener("pointermove", handlePointerMove)
// container.addEventListener("touchmove", handleTouchMove)
// container.addEventListener("pointerdown", () => setIsInteracting(true))
// container.addEventListener("pointerup", () => setIsInteracting(false))
// container.addEventListener("touchstart", () => setIsInteracting(true))
// container.addEventListener("touchend", () => setIsInteracting(false))
// return () => {
// container.removeEventListener("pointermove", handlePointerMove)
// container.removeEventListener("touchmove", handleTouchMove)
// container.removeEventListener("pointerdown", () => setIsInteracting(true))
// container.removeEventListener("pointerup", () => setIsInteracting(false))
// container.removeEventListener("touchstart", () => setIsInteracting(true))
// container.removeEventListener("touchend", () => setIsInteracting(false))
// }
// }, [updateMousePosition])
// useEffect(() => {
// if (materialRef.current) {
// materialRef.current.uniforms.u_color1.value.set(config.color1)
// materialRef.current.uniforms.u_color2.value.set(config.color2)
// materialRef.current.uniforms.u_color3.value.set(config.color3)
// materialRef.current.uniforms.u_color4.value.set(config.color4)
// materialRef.current.uniforms.u_invertMouse.value = config.invertMouse
// materialRef.current.defines.VAR = config.variation
// materialRef.current.needsUpdate = true
// }
// }, [config])
// return (
// <motion.div
// ref={containerRef}
// initial={{ opacity: 0 }}
// animate={{ opacity: 1 }}
// transition={{ duration: 0.5 }}
// className="relative"
// style={{
// width: config.width,
// height: config.height,
// backgroundColor: "transparent",
// borderRadius: "8px",
// }}
// >
// <canvas ref={canvasRef} className="w-full h-full touch-none" />
// <div className="absolute bottom-4 left-4 text-white text-sm">
// {config.enableHover
// ? `${
// config.invertMouse ? "Inverted mouse" : "Normal mouse"
// } interaction`
// : "Interact with the animation"}{" "}
// using mouse or touch
// </div>
// </motion.div>
// )
// }
// export default ShaderLensBlur