50 components available

Dots Shader

May 2024
A canvas shader that renders animated colored dots with customizable opacities, dot sizes and colors.
Playground
<DotsShader  
  colors={[
    [226, 67, 41],
    [252, 109, 38],
    [252, 163, 38, 1],
  ]}
  opacities={[0.4, 0.4, 0.6, 0.6, 0.6, 0.8, 0.8, 0.8, 0.8, 1]}
  totalSize={5}
  dotSize={1} 
/>
Source Code
MUI
Tailwind
dots-shader.tsx
1import { useEffect, useMemo, useRef } from 'react';
2
3export interface DotsShaderProps {
4 opacities?: number[];
5 colors?: number[][];
6 totalSize?: number;
7 dotSize?: number;
8 maxFps?: any;
9}
10
11function createShader(
12 gl: WebGL2RenderingContext,
13 type: number,
14 source: string
15) {
16 let shader = gl.createShader(type);
17 if (!shader) {
18 return console.error('Failed to create shader');
19 }
20
21 // Send the source to the shader object
22 gl.shaderSource(shader, source);
23
24 // Compile the shader program
25 gl.compileShader(shader);
26
27 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
28 console.error('Failed to create shader: ' + gl.getShaderInfoLog(shader));
29 gl.deleteShader(shader);
30 return null;
31 }
32
33 return shader;
34}
35
36function createBuffer(gl: WebGL2RenderingContext, arr: any) {
37 let buffer = gl.createBuffer();
38 let bufferType =
39 arr instanceof Uint16Array || arr instanceof Uint32Array
40 ? gl.ELEMENT_ARRAY_BUFFER
41 : gl.ARRAY_BUFFER;
42
43 gl.bindBuffer(bufferType, buffer);
44 gl.bufferData(bufferType, arr, gl.STATIC_DRAW);
45
46 return buffer;
47}
48
49const DEFAULT_SHADER_SOURCE = `
50float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15);
51opacity *= step(intro_offset, u_time);
52opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time)) * 1.25, 1.0, 1.25);
53`;
54
55const DotsShader = (props: DotsShaderProps) => {
56 const {
57 colors = [[93, 227, 255]],
58 opacities = [0.4, 0.4, 0.6, 0.6, 0.6, 0.8, 0.8, 0.8, 0.8, 1],
59 totalSize = 3,
60 dotSize = 1,
61 maxFps = 30,
62 } = props;
63 const source = DEFAULT_SHADER_SOURCE;
64 const canvasRef = useRef<HTMLCanvasElement>(null);
65 const center = ['x', 'y'];
66 const fragmentSource = `#version 300 es
67 precision mediump float;
68
69 in vec2 fragCoord;
70
71 uniform float u_time;
72 uniform float u_opacities[10];
73 uniform vec3 u_colors[6];
74 uniform float u_total_size;
75 uniform float u_dot_size;
76 uniform vec2 u_resolution;
77
78 out vec4 fragColor;
79 float PHI = 1.61803398874989484820459;
80 float random(vec2 xy) {
81 return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
82 }
83
84 float map(float value, float min1, float max1, float min2, float max2) {
85 return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
86 }
87
88 void main() {
89 vec2 st = fragCoord.xy;
90 \n
91 `
92 .concat(
93 center.includes('x')
94 ? 'st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));'
95 : '',
96 '\n '
97 )
98 .concat(
99 center.includes('y')
100 ? 'st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));'
101 : '',
102 '\n\n float opacity = step(0.0, st.x);\n opacity *= step(0.0, st.y);\n\n vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));\n\n float frequency = 5.0;\n float show_offset = random(st2);\n // Without the +1.0 the first column is all the same opacity\n float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0);\n opacity *= u_opacities[int(rand * 10.0)];\n opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));\n opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));\n\n vec3 color = u_colors[int(show_offset * 6.0)];\n\n '
103 )
104 .concat(
105 source,
106 '\n\n fragColor = vec4(color, opacity);\n fragColor.rgb *= fragColor.a;\n}\n'
107 );
108
109 const uniforms = useMemo(() => {
110 let e =
111 colors.length === 2
112 ? [colors[0], colors[0], colors[0], colors[1], colors[1], colors[1]]
113 : colors.length === 3
114 ? [colors[0], colors[0], colors[1], colors[1], colors[2], colors[2]]
115 : [colors[0], colors[0], colors[0], colors[0], colors[0], colors[0]];
116
117 return {
118 u_colors: {
119 value: e.map(e => [e[0] / 255, e[1] / 255, e[2] / 255]),
120 type: 'uniform3fv',
121 },
122 u_opacities: {
123 value: opacities,
124 type: 'uniform1fv',
125 },
126 u_total_size: {
127 value: totalSize,
128 type: 'uniform1f',
129 },
130 u_dot_size: {
131 value: dotSize,
132 type: 'uniform1f',
133 },
134 };
135 }, [colors, opacities, totalSize, dotSize]);
136
137 useEffect(() => {
138 const windowDpr = window.devicePixelRatio;
139 const canvas = canvasRef.current!;
140 const glCanvas = document.createElement('canvas');
141 const dpr = Math.max(1, Math.min(windowDpr ?? 1, 2));
142 let raf: any;
143
144 canvas.width = canvas.offsetWidth * dpr;
145 canvas.height = canvas.offsetHeight * dpr;
146 glCanvas.width = canvas.offsetWidth * dpr;
147 glCanvas.height = canvas.offsetHeight * dpr;
148
149 const gl = glCanvas.getContext('webgl2');
150 const ctx2d = canvas.getContext('2d');
151
152 if (!gl || !ctx2d) {
153 return;
154 }
155
156 const vertexShader = createShader(
157 gl,
158 gl.VERTEX_SHADER,
159 `#version 300 es
160
161 precision mediump float;
162
163 in vec2 coordinates;
164
165 uniform vec2 u_resolution;
166
167 out vec2 fragCoord;
168
169 void main(void) {
170 gl_Position = vec4(coordinates, 0.0, 1.0);
171 fragCoord = (coordinates + 1.0) * 0.5 * u_resolution;
172 fragCoord.y = u_resolution.y - fragCoord.y;
173 }
174 `
175 );
176 const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
177
178 if (!vertexShader || !fragmentShader) {
179 return;
180 }
181
182 const glProgram = gl.createProgram()!;
183 gl.attachShader(glProgram, vertexShader);
184 gl.attachShader(glProgram, fragmentShader);
185
186 gl.linkProgram(glProgram);
187
188 if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) {
189 throw `Failed to compile WebGL program: \n\n${gl.getProgramInfoLog(
190 glProgram
191 )}`;
192 }
193 gl.useProgram(glProgram);
194
195 const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
196 const positionsBuffer = createBuffer(gl, positions);
197
198 let coordinatesAttrLocation = gl.getAttribLocation(
199 glProgram,
200 'coordinates'
201 );
202 gl.enableVertexAttribArray(coordinatesAttrLocation);
203 gl.vertexAttribPointer(coordinatesAttrLocation, 2, gl.FLOAT, false, 0, 0);
204
205 //prettier-ignore
206 const resolutionAttrLocation = gl.getUniformLocation(glProgram,'u_resolution');
207 const timeAttrLocation = gl.getUniformLocation(glProgram, 'u_time');
208 const scrollAttrLocation = gl.getUniformLocation(glProgram, 'u_scroll');
209
210 for (let key in uniforms) {
211 const uniformLocation = gl.getUniformLocation(glProgram, key);
212 //@ts-ignore
213 const uniform = uniforms[key];
214
215 switch (uniform.type) {
216 case 'uniform1f':
217 gl.uniform1f(uniformLocation, uniform.value);
218 break;
219 case 'uniform3f':
220 // @ts-ignore
221 gl.uniform3f(uniformLocation, ...uniform.value);
222 break;
223 case 'uniform1fv':
224 gl.uniform1fv(uniformLocation, uniform.value);
225 break;
226 case 'uniform3fv':
227 gl.uniform3fv(uniformLocation, uniform.value.flat());
228 break;
229 default:
230 return uniform;
231 }
232 }
233
234 gl.uniform2f(
235 resolutionAttrLocation,
236 canvas.width / dpr,
237 canvas.height / dpr
238 );
239 gl.enable(gl.BLEND);
240 gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
241 gl.disable(gl.DEPTH_TEST);
242
243 let lastSecondPassed: number | null = null;
244 let timePassed = 0;
245
246 function run(e: number) {
247 if (!gl) {
248 return;
249 }
250
251 let secondsPassed = e / 1e3;
252
253 if (lastSecondPassed === null) {
254 lastSecondPassed = secondsPassed;
255 }
256
257 if (maxFps !== Infinity) {
258 if (e - timePassed < 1000 / maxFps) {
259 raf = window.requestAnimationFrame(run);
260 return;
261 }
262 timePassed = e;
263 }
264
265 const time = secondsPassed - lastSecondPassed;
266 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
267 gl.uniform1f(timeAttrLocation, time);
268 gl.uniform1f(scrollAttrLocation, window.scrollY);
269 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
270 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
271 ctx2d!.clearRect(0, 0, canvas.width, canvas.height);
272 ctx2d!.drawImage(glCanvas, 0, 0);
273 raf = window.requestAnimationFrame(run);
274 }
275
276 raf = window.requestAnimationFrame(run);
277
278 const resizeObserver = new window.ResizeObserver(() => {
279 canvas.width = canvas.offsetWidth * dpr;
280 canvas.height = canvas.offsetHeight * dpr;
281 glCanvas.width = canvas.offsetWidth * dpr;
282 glCanvas.height = canvas.offsetHeight * dpr;
283 gl.uniform2f(
284 resolutionAttrLocation,
285 canvas.width / dpr,
286 canvas.height / dpr
287 );
288 });
289
290 return (
291 resizeObserver.observe(canvas),
292 () => {
293 window.cancelAnimationFrame(raf),
294 resizeObserver.disconnect(),
295 gl &&
296 (gl.deleteShader(vertexShader),
297 gl.deleteShader(fragmentShader),
298 gl.deleteProgram(glProgram),
299 gl.deleteBuffer(positionsBuffer));
300 }
301 );
302 }, [fragmentSource, uniforms, maxFps]);
303
304 return <canvas ref={canvasRef} />;
305};
306
307export default DotsShader;