Black White Blobs
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
· ·░░▒▒▒▒▒░░░░░░░░▒▒▒▒▒▒░░·· ··░▒▒▓▓▓▓▓▒▒░░······░░░░░░░░·····░░▒▓▓▓█▓▓ ·░░▒▒▒▒▒░░░░░░░▒▒▒▒▒▒▒░░·· ··░▒▒▓▓▓▓▓▒▒░░·······░░░░░░░·····░░▒▓▓███▓ ·░░░▒▒▒░░░░░░░░▒▒▓▓▓▒▒░░· ··░▒▒▓███▓▓▒░░·· ··░░░░░░░░···░░░▒▓▓███▓ · ··░░░▒▒░░░░░░░░▒▒▒▓▓▓▒▒░░· ··░▒▒▓▓█▓▓▓▒░░·· ···░░░░░░░░··░░░▒▒▓▓▓▓▓ · ···░░░░░░░░░░░░░░▒▒▒▓▒▒▒░░·· ··░▒▒▓▓▓▓▓▒▒░░··· ···░░░░░░░░░░░░░░▒▒▒▓▓▓▓ ·······░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░········░░░▒▒▒▒▒▒▒▒░░░······░░░░░░░░░░░░░░░▒▒▒▒▒▒ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░···░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░▒▒▒▒░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒▒░░░░░······░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░···· ▒▒▒▒▒▒▒░░░░░░░░░░░░░░··· ···░░▒▒▓▓▓▓▒▒░░░·· ··░░▒▒▒▓▓▒▒▒░░░░░░░░░░░░░░······ ▒▓▓▓▒▒▒░░░░░░░▒▒▒▒░░░· ·░░▒▓▓▓▓▓▓▒▒░·· ·░░▒▓▓▓▓▓▓▒░░░····░░░░░░░··· ▒▓▓▓▓▒▒░░░░░░░▒▒▒▒░░░· ·░░▒▓████▓▒▒░·· ·░░▒▓███▓▓▒░░░····░░░░░░░·· ▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░·· ·░░▒▓████▓▒▒░·· ·░░▒▓████▓▒▒░······░░░░░░·· ▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░·· ·░░▒▓▓██▓▓▒▒░·· ·░░▒▓████▓▒▒░······░░░░░░·· ▒▒▓▓▒▒▒░░░░░░▒▒▒▒▒░░░· ·░░▒▓▓▓▓▓▓▒░░░· ·░░▒▓▓██▓▓▒░░░·····░░░░░░··· · ▒▒▒▒▒▒░░░░░░░▒▒▒▒▒░░░·· ··░░▒▒▒▓▒▒▒▒░░░···· ···░░▒▒▓▓▓▓▒▒░░░·····░░░░░░░····· ░▒▒▒░░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒░░░░░░░·····░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░··· ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░··░░░░░░░░░░░░░░░░░░░░░░░ ·····░░░░░░░░░····░░░▒▒▒▒▒▒▒▒░░░·····░░░░░░░▒▒▒▒▒░░░········░░░▒▒▒▒░░░░░░░░░▒▒▒▒ ······░░░░░░······░░░▒▓▓▓▓▓▒▒░░·······░░░░░▒▒▒▒▒▒▒░░·· ·░░░▒▒▒▒▒▒░░░░░░▒▒▒▒▒ ·· ···░░░░░░··· ···░▒▒▓███▓▓▒░░·· ···░░░░░▒▒▒▓▓▒▒░░· ··░▒▒▒▓▒▒▒░░░░░░▒▒▒▓▒ · ··░░░░░░·· ··░▒▓▓████▓▒░░·· ···░░░░░▒▒▒▓▓▒▒░░· ·░▒▒▓▓▓▒▒░░░░░░▒▒▓▓▓ · ··░░░░░░·· ··░▒▓▓████▓▒░░·· ···░░░░░▒▒▒▓▓▒▒░░· ·░▒▒▓▓▓▒▒░░░░░░▒▒▓▓▓ ·· ···░░░░░░··· ···░▒▒▓███▓▓▒░░·· ···░░░░░▒▒▒▓▓▒▒░░· ··░▒▒▒▓▒▒▒░░░░░░▒▒▒▓▒ ······░░░░░░······░░░▒▓▓▓▓▓▒▒░░·······░░░░░▒▒▒▒▒▒▒░░·· ·░░░▒▒▒▒▒▒░░░░░░▒▒▒▒▒ ·····░░░░░░░░░····░░░▒▒▒▒▒▒▒▒░░░·····░░░░░░░▒▒▒▒▒░░░········░░░▒▒▒▒░░░░░░░░░▒▒▒▒ ░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░··░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░▒▒▒░░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒░░░░░░░·····░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░··· ▒▒▒▒▒▒░░░░░░░▒▒▒▒▒░░░·· ··░░▒▒▒▓▒▒▒▒░░░···· ···░░▒▒▓▓▓▓▒▒░░░·····░░░░░░░····· ▒▒▓▓▒▒▒░░░░░░▒▒▒▒▒░░░· ·░░▒▓▓▓▓▓▓▒░░░· ·░░▒▓▓██▓▓▒░░░·····░░░░░░··· · ▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░·· ·░░▒▓▓██▓▓▒▒░·· ·░░▒▓████▓▒▒░······░░░░░░·· ▒▓▓▓▓▒▒░░░░░░▒▒▒▒▒▒░·· ·░░▒▓████▓▒▒░·· ·░░▒▓████▓▒▒░······░░░░░░·· ▒▓▓▓▓▒▒░░░░░░░▒▒▒▒░░░· ·░░▒▓████▓▒▒░·· ·░░▒▓███▓▓▒░░░····░░░░░░░·· ▒▓▓▓▒▒▒░░░░░░░▒▒▒▒░░░· ·░░▒▓▓▓▓▓▓▒▒░·· ·░░▒▓▓▓▓▓▓▒░░░····░░░░░░░··· ▒▒▒▒▒▒▒░░░░░░░░░░░░░░··· ···░░▒▒▓▓▓▓▒▒░░░·· ··░░▒▒▒▓▓▒▒▒░░░░░░░░░░░░░░······ ░▒▒▒▒░░░░░░░░░░░░░░░░░······░░░░▒▒▒▒▒▒░░░░░······░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░···· ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░···░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ·······░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░········░░░▒▒▒▒▒▒▒▒░░░······░░░░░░░░░░░░░░░▒▒▒▒▒▒ · ···░░░░░░░░░░░░░░▒▒▒▓▒▒▒░░·· ··░▒▒▓▓▓▓▓▒▒░░··· ···░░░░░░░░░░░░░░▒▒▒▓▓▓▓ · ··░░░▒▒░░░░░░░░▒▒▒▓▓▓▒▒░░· ··░▒▒▓▓█▓▓▓▒░░·· ···░░░░░░░░··░░░▒▒▓▓▓▓▓ ·░░░▒▒▒░░░░░░░░▒▒▓▓▓▒▒░░· ··░▒▒▓███▓▓▒░░·· ··░░░░░░░░···░░░▒▓▓███▓ ·░░▒▒▒▒▒░░░░░░░▒▒▒▒▒▒▒░░·· ··░▒▒▓▓▓▓▓▒▒░░·······░░░░░░░·····░░▒▓▓███▓
1"use client";23import { useState, useEffect, useRef, useCallback } from "react";45interface BlackWhiteBlobsProps {6backgroundColor?: string;7textColor?: string;8animationSpeed?: number;9}1011type Pattern = (x: number, y: number, t: number) => number;1213const BlackWhiteBlobs = ({14backgroundColor = "#F0EEE6",15textColor = "#333",16}: BlackWhiteBlobsProps) => {17const [frame, setFrame] = useState(0);18const [patternType, setPatternType] = useState(0);19const [mousePos, setMousePos] = useState({ x: 0, y: 0 });20const [mouseDown, setMouseDown] = useState(false);21const containerRef = useRef<HTMLDivElement>(null);22const animationFrameId = useRef<number | null>(null);23const [dimensions, setDimensions] = useState({ width: 80, height: 45 });24const mouseInfluenceRef = useRef<25Array<{ x: number; y: number; time: number; intensity: number }>26>([]);2728const slowdownFactor = 12;2930const patterns: Record<string, Pattern> = {31balance: (x, y, t) => {32const cx = dimensions.width / 2;33const cy = dimensions.height / 2;34const dx = x - cx;35const dy = y - cy;36const dist = Math.sqrt(dx * dx + dy * dy);37return 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},39duality: (x, y, t) => {40const cx = dimensions.width / 2;41const left = x < cx ? Math.sin(x * 0.2 + t * 0.3) : 0;42const right = x >= cx ? Math.cos(x * 0.2 - t * 0.3) : 0;43return left + right + Math.sin(y * 0.3 + t * 0.2);44},45flow: (x, y, t) => {46const angle = Math.atan2(y - dimensions.height / 2, x - dimensions.width / 2);47const dist = Math.sqrt((x - dimensions.width / 2) ** 2 + (y - dimensions.height / 2) ** 2);48return Math.sin(angle * 3 + t * 0.4) * Math.cos(dist * 0.1 - t * 0.3);49},50chaos: (x, y, t) => {51const noise1 = Math.sin(x * 0.5 + t) * Math.cos(y * 0.3 - t);52const noise2 = Math.sin(y * 0.4 + t * 0.5) * Math.cos(x * 0.2 + t * 0.7);53const noise3 = Math.sin((x + y) * 0.2 + t * 0.8);54return noise1 * 0.3 + noise2 * 0.3 + noise3 * 0.4;55},56};5758const patternTypes = ["balance", "duality", "flow", "chaos"];5960const getMouseInfluence = (x: number, y: number, currentTime: number): number => {61let totalInfluence = 0;62mouseInfluenceRef.current.forEach((influence) => {63const age = currentTime - influence.time;64const maxAge = 3000;65if (age < maxAge) {66const dx = x - influence.x;67const dy = y - influence.y;68const distance = Math.sqrt(dx * dx + dy * dy);69const maxDistance = 15;70if (distance < maxDistance) {71const strength = (1 - age / maxAge) * influence.intensity;72const proximity = 1 - distance / maxDistance;73totalInfluence += strength * proximity;74}75}76});77return totalInfluence;78};7980const generateAsciiArt = useCallback(() => {81const { width, height } = dimensions;82const t = (frame * Math.PI) / (60 * slowdownFactor);83const currentPattern = patterns[patternTypes[patternType]];84const currentTime = Date.now();85let result = "";8687for (let y = 0; y < height; y++) {88for (let x = 0; x < width; x++) {89let value = currentPattern(x, y, t);90if (mouseDown && containerRef.current) {91const rect = containerRef.current.getBoundingClientRect();92const dx = x - (mousePos.x / rect.width) * width;93const dy = y - (mousePos.y / rect.height) * height;94const dist = Math.sqrt(dx * dx + dy * dy);95const mouseInfluence = Math.exp(-dist * 0.1) * Math.sin(t * 2);96value += mouseInfluence * 0.8;97}98const clickInfluence = getMouseInfluence(x, y, currentTime);99value += clickInfluence * Math.sin(t * 3);100101if (value > 0.8) {102result += "█";103} else if (value > 0.5) {104result += "▓";105} else if (value > 0.2) {106result += "▒";107} else if (value > -0.2) {108result += "░";109} else if (value > -0.5) {110result += "·";111} else {112result += " ";113}114}115result += "\n";116}117return result;118}, [frame, patternType, mousePos, mouseDown, dimensions, slowdownFactor]);119120useEffect(() => {121const resizeObserver = new ResizeObserver((entries) => {122if (!entries[0]) return;123const { width, height } = entries[0].contentRect;124setDimensions({125width: Math.floor(width / 10), // adjust cell density126height: Math.floor(height / 20),127});128});129if (containerRef.current) {130resizeObserver.observe(containerRef.current);131}132return () => resizeObserver.disconnect();133}, []);134135useEffect(() => {136const animate = () => {137setFrame((f) => (f + 1) % (240 * slowdownFactor));138animationFrameId.current = requestAnimationFrame(animate);139};140animationFrameId.current = requestAnimationFrame(animate);141return () => {142if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current);143};144}, [slowdownFactor]);145146return (147<div148ref={containerRef}149className="absolute inset-0 w-full h-full flex items-center justify-center overflow-hidden "150style={{151backgroundColor,152userSelect: "none",153}}154onMouseMove={(e) => {155const rect = containerRef.current?.getBoundingClientRect();156if (!rect) return;157setMousePos({ x: e.clientX - rect.left, y: e.clientY - rect.top });158}}159onMouseDown={(e) => {160setMouseDown(true);161const rect = containerRef.current?.getBoundingClientRect();162if (!rect) return;163const x = ((e.clientX - rect.left) / rect.width) * dimensions.width;164const y = ((e.clientY - rect.top) / rect.height) * dimensions.height;165mouseInfluenceRef.current.push({ x, y, time: Date.now(), intensity: 1.5 });166mouseInfluenceRef.current = mouseInfluenceRef.current.filter(167(inf) => Date.now() - inf.time < 3000168);169}}170onMouseUp={() => setMouseDown(false)}171onClick={() => setPatternType((prev) => (prev + 1) % patternTypes.length)}172>173<pre174style={{175fontFamily: "monospace",176fontSize: "clamp(8px, 1.5vw, 16px)",177lineHeight: "1",178letterSpacing: "0.05em",179color: textColor,180margin: 0,181padding: 0,182whiteSpace: "pre",183}}184>185{generateAsciiArt()}186</pre>187</div>188);189};190191export default BlackWhiteBlobs;192
Prop | Type | Default | Description |
---|---|---|---|
backgroundColor | string | '#F0EEE6' | Background color of the canvas. |
textColor | string | '#333' | Color of the text or visual elements drawn on the canvas. |
animationSpeed | number | undefined | Speed of the blob animation. (Can be customized if passed.) |