Black White Blobs

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

·      ·░░▒▒▒▒▒░░░░░░░░▒▒▒▒▒▒░░··     ··░▒▒▓▓▓▓▓▒▒░░······░░░░░░░░·····░░▒▓▓▓█▓▓
       ·░░▒▒▒▒▒░░░░░░░▒▒▒▒▒▒▒░░··     ··░▒▒▓▓▓▓▓▒▒░░·······░░░░░░░·····░░▒▓▓███▓
       ·░░░▒▒▒░░░░░░░░▒▒▓▓▓▒▒░░·      ··░▒▒▓███▓▓▒░░··   ··░░░░░░░░···░░░▒▓▓███▓
·     ··░░░▒▒░░░░░░░░▒▒▒▓▓▓▒▒░░·      ··░▒▒▓▓█▓▓▓▒░░··   ···░░░░░░░░··░░░▒▒▓▓▓▓▓
·    ···░░░░░░░░░░░░░░▒▒▒▓▒▒▒░░··     ··░▒▒▓▓▓▓▓▒▒░░··· ···░░░░░░░░░░░░░░▒▒▒▓▓▓▓
·······░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░········░░░▒▒▒▒▒▒▒▒░░░······░░░░░░░░░░░░░░░▒▒▒▒▒▒
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░···░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░▒▒▒▒░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒▒░░░░░······░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░····
▒▒▒▒▒▒▒░░░░░░░░░░░░░░···  ···░░▒▒▓▓▓▓▒▒░░░··    ··░░▒▒▒▓▓▒▒▒░░░░░░░░░░░░░░······
▒▓▓▓▒▒▒░░░░░░░▒▒▒▒░░░·      ·░░▒▓▓▓▓▓▓▒▒░··      ·░░▒▓▓▓▓▓▓▒░░░····░░░░░░░···   
▒▓▓▓▓▒▒░░░░░░░▒▒▒▒░░░·      ·░░▒▓████▓▒▒░··      ·░░▒▓███▓▓▒░░░····░░░░░░░··    
▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░··      ·░░▒▓████▓▒▒░··      ·░░▒▓████▓▒▒░······░░░░░░··    
▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░··      ·░░▒▓▓██▓▓▒▒░··      ·░░▒▓████▓▒▒░······░░░░░░··    
▒▒▓▓▒▒▒░░░░░░▒▒▒▒▒░░░·      ·░░▒▓▓▓▓▓▓▒░░░·      ·░░▒▓▓██▓▓▒░░░·····░░░░░░···  ·
▒▒▒▒▒▒░░░░░░░▒▒▒▒▒░░░··    ··░░▒▒▒▓▒▒▒▒░░░···· ···░░▒▒▓▓▓▓▒▒░░░·····░░░░░░░·····
░▒▒▒░░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒░░░░░░░·····░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░···
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░··░░░░░░░░░░░░░░░░░░░░░░░
·····░░░░░░░░░····░░░▒▒▒▒▒▒▒▒░░░·····░░░░░░░▒▒▒▒▒░░░········░░░▒▒▒▒░░░░░░░░░▒▒▒▒
······░░░░░░······░░░▒▓▓▓▓▓▒▒░░·······░░░░░▒▒▒▒▒▒▒░░··     ·░░░▒▒▒▒▒▒░░░░░░▒▒▒▒▒
·· ···░░░░░░··· ···░▒▒▓███▓▓▒░░··  ···░░░░░▒▒▒▓▓▒▒░░·      ··░▒▒▒▓▒▒▒░░░░░░▒▒▒▓▒
·   ··░░░░░░··   ··░▒▓▓████▓▒░░··  ···░░░░░▒▒▒▓▓▒▒░░·       ·░▒▒▓▓▓▒▒░░░░░░▒▒▓▓▓
·   ··░░░░░░··   ··░▒▓▓████▓▒░░··  ···░░░░░▒▒▒▓▓▒▒░░·       ·░▒▒▓▓▓▒▒░░░░░░▒▒▓▓▓
·· ···░░░░░░··· ···░▒▒▓███▓▓▒░░··  ···░░░░░▒▒▒▓▓▒▒░░·      ··░▒▒▒▓▒▒▒░░░░░░▒▒▒▓▒
······░░░░░░······░░░▒▓▓▓▓▓▒▒░░·······░░░░░▒▒▒▒▒▒▒░░··     ·░░░▒▒▒▒▒▒░░░░░░▒▒▒▒▒
·····░░░░░░░░░····░░░▒▒▒▒▒▒▒▒░░░·····░░░░░░░▒▒▒▒▒░░░········░░░▒▒▒▒░░░░░░░░░▒▒▒▒
░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░··░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░▒▒▒░░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒░░░░░░░·····░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░···
▒▒▒▒▒▒░░░░░░░▒▒▒▒▒░░░··    ··░░▒▒▒▓▒▒▒▒░░░···· ···░░▒▒▓▓▓▓▒▒░░░·····░░░░░░░·····
▒▒▓▓▒▒▒░░░░░░▒▒▒▒▒░░░·      ·░░▒▓▓▓▓▓▓▒░░░·      ·░░▒▓▓██▓▓▒░░░·····░░░░░░···  ·
▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░··      ·░░▒▓▓██▓▓▒▒░··      ·░░▒▓████▓▒▒░······░░░░░░··    
▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░··      ·░░▒▓████▓▒▒░··      ·░░▒▓████▓▒▒░······░░░░░░··    
▒▓▓▓▓▒▒░░░░░░░▒▒▒▒░░░·      ·░░▒▓████▓▒▒░··      ·░░▒▓███▓▓▒░░░····░░░░░░░··    
▒▓▓▓▒▒▒░░░░░░░▒▒▒▒░░░·      ·░░▒▓▓▓▓▓▓▒▒░··      ·░░▒▓▓▓▓▓▓▒░░░····░░░░░░░···   
▒▒▒▒▒▒▒░░░░░░░░░░░░░░···  ···░░▒▒▓▓▓▓▒▒░░░··    ··░░▒▒▒▓▓▒▒▒░░░░░░░░░░░░░░······
░▒▒▒▒░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒▒░░░░░······░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░····
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░···░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
·······░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░········░░░▒▒▒▒▒▒▒▒░░░······░░░░░░░░░░░░░░░▒▒▒▒▒▒
·    ···░░░░░░░░░░░░░░▒▒▒▓▒▒▒░░··     ··░▒▒▓▓▓▓▓▒▒░░··· ···░░░░░░░░░░░░░░▒▒▒▓▓▓▓
·     ··░░░▒▒░░░░░░░░▒▒▒▓▓▓▒▒░░·      ··░▒▒▓▓█▓▓▓▒░░··   ···░░░░░░░░··░░░▒▒▓▓▓▓▓
       ·░░░▒▒▒░░░░░░░░▒▒▓▓▓▒▒░░·      ··░▒▒▓███▓▓▒░░··   ··░░░░░░░░···░░░▒▓▓███▓
       ·░░▒▒▒▒▒░░░░░░░▒▒▒▒▒▒▒░░··     ··░▒▒▓▓▓▓▓▒▒░░·······░░░░░░░·····░░▒▓▓███▓
1
"use client";
2
3
import { useState, useEffect, useRef, useCallback } from "react";
4
5
interface BlackWhiteBlobsProps {
6
backgroundColor?: string;
7
textColor?: string;
8
animationSpeed?: number;
9
}
10
11
type Pattern = (x: number, y: number, t: number) => number;
12
13
const BlackWhiteBlobs = ({
14
backgroundColor = "#F0EEE6",
15
textColor = "#333",
16
}: BlackWhiteBlobsProps) => {
17
const [frame, setFrame] = useState(0);
18
const [patternType, setPatternType] = useState(0);
19
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
20
const [mouseDown, setMouseDown] = useState(false);
21
const containerRef = useRef<HTMLDivElement>(null);
22
const animationFrameId = useRef<number | null>(null);
23
const [dimensions, setDimensions] = useState({ width: 80, height: 45 });
24
const mouseInfluenceRef = useRef<
25
Array<{ x: number; y: number; time: number; intensity: number }>
26
>([]);
27
28
const slowdownFactor = 12;
29
30
const patterns: Record<string, Pattern> = {
31
balance: (x, y, t) => {
32
const cx = dimensions.width / 2;
33
const cy = dimensions.height / 2;
34
const dx = x - cx;
35
const dy = y - cy;
36
const dist = Math.sqrt(dx * dx + dy * dy);
37
return Math.sin(dx * 0.3 + t * 0.5) * Math.cos(dy * 0.3 + t * 0.3) * Math.sin(dist * 0.1 - t * 0.4);
38
},
39
duality: (x, y, t) => {
40
const cx = dimensions.width / 2;
41
const left = x < cx ? Math.sin(x * 0.2 + t * 0.3) : 0;
42
const right = x >= cx ? Math.cos(x * 0.2 - t * 0.3) : 0;
43
return left + right + Math.sin(y * 0.3 + t * 0.2);
44
},
45
flow: (x, y, t) => {
46
const angle = Math.atan2(y - dimensions.height / 2, x - dimensions.width / 2);
47
const dist = Math.sqrt((x - dimensions.width / 2) ** 2 + (y - dimensions.height / 2) ** 2);
48
return Math.sin(angle * 3 + t * 0.4) * Math.cos(dist * 0.1 - t * 0.3);
49
},
50
chaos: (x, y, t) => {
51
const noise1 = Math.sin(x * 0.5 + t) * Math.cos(y * 0.3 - t);
52
const noise2 = Math.sin(y * 0.4 + t * 0.5) * Math.cos(x * 0.2 + t * 0.7);
53
const noise3 = Math.sin((x + y) * 0.2 + t * 0.8);
54
return noise1 * 0.3 + noise2 * 0.3 + noise3 * 0.4;
55
},
56
};
57
58
const patternTypes = ["balance", "duality", "flow", "chaos"];
59
60
const getMouseInfluence = (x: number, y: number, currentTime: number): number => {
61
let totalInfluence = 0;
62
mouseInfluenceRef.current.forEach((influence) => {
63
const age = currentTime - influence.time;
64
const maxAge = 3000;
65
if (age < maxAge) {
66
const dx = x - influence.x;
67
const dy = y - influence.y;
68
const distance = Math.sqrt(dx * dx + dy * dy);
69
const maxDistance = 15;
70
if (distance < maxDistance) {
71
const strength = (1 - age / maxAge) * influence.intensity;
72
const proximity = 1 - distance / maxDistance;
73
totalInfluence += strength * proximity;
74
}
75
}
76
});
77
return totalInfluence;
78
};
79
80
const generateAsciiArt = useCallback(() => {
81
const { width, height } = dimensions;
82
const t = (frame * Math.PI) / (60 * slowdownFactor);
83
const currentPattern = patterns[patternTypes[patternType]];
84
const currentTime = Date.now();
85
let result = "";
86
87
for (let y = 0; y < height; y++) {
88
for (let x = 0; x < width; x++) {
89
let value = currentPattern(x, y, t);
90
if (mouseDown && containerRef.current) {
91
const rect = containerRef.current.getBoundingClientRect();
92
const dx = x - (mousePos.x / rect.width) * width;
93
const dy = y - (mousePos.y / rect.height) * height;
94
const dist = Math.sqrt(dx * dx + dy * dy);
95
const mouseInfluence = Math.exp(-dist * 0.1) * Math.sin(t * 2);
96
value += mouseInfluence * 0.8;
97
}
98
const clickInfluence = getMouseInfluence(x, y, currentTime);
99
value += clickInfluence * Math.sin(t * 3);
100
101
if (value > 0.8) {
102
result += "█";
103
} else if (value > 0.5) {
104
result += "▓";
105
} else if (value > 0.2) {
106
result += "▒";
107
} else if (value > -0.2) {
108
result += "░";
109
} else if (value > -0.5) {
110
result += "·";
111
} else {
112
result += " ";
113
}
114
}
115
result += "\n";
116
}
117
return result;
118
}, [frame, patternType, mousePos, mouseDown, dimensions, slowdownFactor]);
119
120
useEffect(() => {
121
const resizeObserver = new ResizeObserver((entries) => {
122
if (!entries[0]) return;
123
const { width, height } = entries[0].contentRect;
124
setDimensions({
125
width: Math.floor(width / 10), // adjust cell density
126
height: Math.floor(height / 20),
127
});
128
});
129
if (containerRef.current) {
130
resizeObserver.observe(containerRef.current);
131
}
132
return () => resizeObserver.disconnect();
133
}, []);
134
135
useEffect(() => {
136
const animate = () => {
137
setFrame((f) => (f + 1) % (240 * slowdownFactor));
138
animationFrameId.current = requestAnimationFrame(animate);
139
};
140
animationFrameId.current = requestAnimationFrame(animate);
141
return () => {
142
if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current);
143
};
144
}, [slowdownFactor]);
145
146
return (
147
<div
148
ref={containerRef}
149
className="absolute inset-0 w-full h-full flex items-center justify-center overflow-hidden "
150
style={{
151
backgroundColor,
152
userSelect: "none",
153
}}
154
onMouseMove={(e) => {
155
const rect = containerRef.current?.getBoundingClientRect();
156
if (!rect) return;
157
setMousePos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
158
}}
159
onMouseDown={(e) => {
160
setMouseDown(true);
161
const rect = containerRef.current?.getBoundingClientRect();
162
if (!rect) return;
163
const x = ((e.clientX - rect.left) / rect.width) * dimensions.width;
164
const y = ((e.clientY - rect.top) / rect.height) * dimensions.height;
165
mouseInfluenceRef.current.push({ x, y, time: Date.now(), intensity: 1.5 });
166
mouseInfluenceRef.current = mouseInfluenceRef.current.filter(
167
(inf) => Date.now() - inf.time < 3000
168
);
169
}}
170
onMouseUp={() => setMouseDown(false)}
171
onClick={() => setPatternType((prev) => (prev + 1) % patternTypes.length)}
172
>
173
<pre
174
style={{
175
fontFamily: "monospace",
176
fontSize: "clamp(8px, 1.5vw, 16px)",
177
lineHeight: "1",
178
letterSpacing: "0.05em",
179
color: textColor,
180
margin: 0,
181
padding: 0,
182
whiteSpace: "pre",
183
}}
184
>
185
{generateAsciiArt()}
186
</pre>
187
</div>
188
);
189
};
190
191
export default BlackWhiteBlobs;
192

Props

PropTypeDefaultDescription
backgroundColorstring'#F0EEE6'Background color of the canvas.
textColorstring'#333'Color of the text or visual elements drawn on the canvas.
animationSpeednumberundefinedSpeed of the blob animation. (Can be customized if passed.)