001 - VORONOI FLOW FIELD SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gradient Flow Fields</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #000;
overflow: hidden;
transition: background-color 0.5s ease;
}
body.light-mode {
background-color: #ffffff;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="can"></canvas>
<script>
// ---------- THEME SYNC ----------
function syncTheme() {
try {
let isDark = true;
if (window.parent && window.parent.document) {
isDark = window.parent.document.documentElement.classList.contains('dark');
} else {
const theme = localStorage.getItem('theme');
isDark = theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
if (!isDark) {
document.body.classList.add('light-mode');
} else {
document.body.classList.remove('light-mode');
}
} catch (e) {
console.warn("Could not sync theme", e);
}
}
syncTheme();
if (window.parent && window.parent.document) {
try {
const observer = new MutationObserver(syncTheme);
observer.observe(window.parent.document.documentElement, { attributes: true, attributeFilter: ['class'] });
} catch(e) {}
}
window.addEventListener('storage', syncTheme);
const can = document.getElementById('can');
const c = can.getContext('2d');
const CONFIG = {
speed: 22,
density: 40,
bandWidth: 6,
bandGap: 45,
colorTransitionDuration: 4000,
backgroundWaveSpeed: 0.00015
};
let isAnimating = true;
let animationId = null;
let resolutionScale = 0.75;
let time = 0;
let points = [];
let pointVelocities = [];
let colorSets = [];
let currentColorSet = 0;
let targetColorSet = 1;
let colorTransitionStartTime = 0;
let isTransitioning = false;
let colorMix = 0;
let bgPhase = 0;
let lastTimestamp = 0;
function initColorSets() {
colorSets = [];
for (let i = 0; i < 8; i++) {
const hue1 = (i * 60 + time * 0.001) % 360;
const hue2 = (hue1 + 120) % 360;
const hue3 = (hue2 + 120) % 360;
colorSets.push([
hslToRgb(hue1, 75, 50),
hslToRgb(hue2, 80, 45),
hslToRgb(hue3, 65, 55)
]);
}
}
function startColorTransition() {
targetColorSet = (currentColorSet + 1) % colorSets.length;
colorTransitionStartTime = time;
isTransitioning = true;
}
function hslToRgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function interpolateColor(color1, color2, t) {
const easeT = 0.5 - Math.cos(t * Math.PI) * 0.5;
return {
r: color1.r * (1 - easeT) + color2.r * easeT,
g: color1.g * (1 - easeT) + color2.g * easeT,
b: color1.b * (1 - easeT) + color2.b * easeT
};
}
function getGradientColor(x, y, width, height, timestamp) {
const nx = x / width;
const ny = y / height;
const rotation = timestamp * 0.00001;
const rotatedX = (nx - 0.5) * Math.cos(rotation) - (ny - 0.5) * Math.sin(rotation) + 0.5;
const rotatedY = (nx - 0.5) * Math.sin(rotation) + (ny - 0.5) * Math.cos(rotation) + 0.5;
const swirl = Math.sin(rotatedX * Math.PI * 3 + timestamp * 0.0001) *
Math.cos(rotatedY * Math.PI * 3 + timestamp * 0.00008) * 0.15;
const diagonalMix = (rotatedX + rotatedY) * 0.5;
const radialMix = Math.sqrt((rotatedX - 0.5) ** 2 + (rotatedY - 0.5) ** 2) * 1.4;
const wave1 = Math.sin(timestamp * 0.00005 + rotatedX * 2) * 0.1;
const wave2 = Math.cos(timestamp * 0.00004 + rotatedY * 2) * 0.1;
const gradientPos = (diagonalMix * 0.4 + radialMix * 0.3 + swirl * 0.2 + (wave1 + wave2) * 0.1) % 1;
let color1, color2;
const currentSet = colorSets[currentColorSet];
const targetSet = colorSets[targetColorSet];
if (isTransitioning) {
const transitionProgress = Math.min(1, (timestamp - colorTransitionStartTime) / CONFIG.colorTransitionDuration);
if (transitionProgress >= 1) {
isTransitioning = false;
currentColorSet = targetColorSet;
setTimeout(startColorTransition, 1000);
}
if (gradientPos < 0.5) {
const t = gradientPos * 2;
const currentColor = interpolateColor(currentSet[0], currentSet[1], t);
const targetColor = interpolateColor(targetSet[0], targetSet[1], t);
color1 = interpolateColor(currentColor, targetColor, transitionProgress);
} else {
const t = (gradientPos - 0.5) * 2;
const currentColor = interpolateColor(currentSet[1], currentSet[2], t);
const targetColor = interpolateColor(targetSet[1], targetSet[2], t);
color1 = interpolateColor(currentColor, targetColor, transitionProgress);
}
} else {
if (gradientPos < 0.5) {
const t = gradientPos * 2;
color1 = interpolateColor(currentSet[0], currentSet[1], t);
} else {
const t = (gradientPos - 0.5) * 2;
color1 = interpolateColor(currentSet[1], currentSet[2], t);
}
}
const secondaryHueShift = Math.sin(timestamp * 0.00002) * 0.05;
const secondaryColor = {
r: color1.r * (0.9 + secondaryHueShift),
g: color1.g * (0.95 + secondaryHueShift * 0.5),
b: color1.b * (1.1 - secondaryHueShift)
};
const colorBlend = Math.sin(timestamp * 0.00003) * 0.3 + 0.7;
return {
r: color1.r * colorBlend + secondaryColor.r * (1 - colorBlend),
g: color1.g * colorBlend + secondaryColor.g * (1 - colorBlend),
b: color1.b * colorBlend + secondaryColor.b * (1 - colorBlend)
};
}
function getBackgroundColor(x, y, width, height, timestamp) {
const nx = x / width;
const ny = y / height;
const wave1 = Math.sin(nx * Math.PI * 2.5 + timestamp * CONFIG.backgroundWaveSpeed) * 0.04;
const wave2 = Math.cos(ny * Math.PI * 2.5 + timestamp * CONFIG.backgroundWaveSpeed * 0.8) * 0.03;
const wave3 = Math.sin((nx + ny) * Math.PI * 1.5 + timestamp * CONFIG.backgroundWaveSpeed * 1.2) * 0.02;
const combinedWave = (wave1 + wave2 + wave3) * 0.8;
if (document.body.classList.contains('light-mode')) {
const baseLight = 245;
const timeVariation = timestamp * 0.00003;
const lightR = baseLight + Math.sin(timeVariation) * 5 + combinedWave * 10;
const lightG = baseLight + Math.cos(timeVariation * 1.1) * 5 + combinedWave * 8;
const lightB = baseLight + Math.sin(timeVariation * 1.2) * 5 + combinedWave * 12;
return {
r: Math.max(230, Math.min(255, lightR)),
g: Math.max(230, Math.min(255, lightG)),
b: Math.max(230, Math.min(255, lightB))
};
}
const baseDark = 8;
const timeVariation = timestamp * 0.00003;
const darkR = baseDark + Math.sin(timeVariation) * 0.5 + combinedWave * 2;
const darkG = baseDark + Math.cos(timeVariation * 1.1) * 0.5 + combinedWave * 1.5;
const darkB = baseDark + Math.sin(timeVariation * 1.2) * 0.5 + combinedWave * 2.5;
return {
r: Math.max(0, Math.min(20, darkR)),
g: Math.max(0, Math.min(18, darkG)),
b: Math.max(0, Math.min(25, darkB))
};
}
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function initPoints(width, height) {
points = [];
pointVelocities = [];
const pointCount = Math.floor((width * height) / (40000 / CONFIG.density));
for (let i = 0; i < pointCount; i++) {
points.push({
x: rand(0, width),
y: rand(0, height)
});
pointVelocities.push({
x: rand(-CONFIG.speed/15, CONFIG.speed/15),
y: rand(-CONFIG.speed/15, CONFIG.speed/15)
});
}
}
function updatePoints(width, height) {
for (let i = 0; i < points.length; i++) {
let p = points[i];
let v = pointVelocities[i];
v.x += rand(-0.03, 0.03);
v.y += rand(-0.03, 0.03);
const maxSpeed = CONFIG.speed / 12;
const speed = Math.sqrt(v.x * v.x + v.y * v.y);
if (speed > maxSpeed) {
v.x = (v.x / speed) * maxSpeed;
v.y = (v.y / speed) * maxSpeed;
}
p.x += v.x;
p.y += v.y;
if (p.x <= 0 || p.x >= width) {
v.x = -v.x * 0.95;
p.x = p.x <= 0 ? 1 : width - 1;
}
if (p.y <= 0 || p.y >= height) {
v.y = -v.y * 0.95;
p.y = p.y <= 0 ? 1 : height - 1;
}
}
}
function render(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const deltaTime = timestamp - lastTimestamp;
lastTimestamp = timestamp;
time += deltaTime * 0.06;
bgPhase = (bgPhase + deltaTime * 0.001) % 1;
const width = window.innerWidth;
const height = window.innerHeight;
const renderWidth = Math.floor(width * resolutionScale);
const renderHeight = Math.floor(height * resolutionScale);
if (can.width !== renderWidth || can.height !== renderHeight) {
can.width = renderWidth;
can.height = renderHeight;
can.style.width = width + 'px';
can.style.height = height + 'px';
initPoints(renderWidth, renderHeight);
}
updatePoints(renderWidth, renderHeight);
const imageData = c.createImageData(renderWidth, renderHeight);
const data = imageData.data;
const bandGap = CONFIG.bandGap * (0.8 + Math.sin(time * 0.0001) * 0.2);
const bandWidth = CONFIG.bandWidth * resolutionScale;
const pixelStep = 2;
const pointSkip = Math.max(1, Math.floor(points.length / 40));
const isLightMode = document.body.classList.contains('light-mode');
const alpha = isLightMode ? 40 : 255;
for (let y = 0; y < renderHeight; y += pixelStep) {
for (let x = 0; x < renderWidth; x += pixelStep) {
const i = (x + y * renderWidth) * 4;
let closestDist = 999999;
for (let pIdx = 0; pIdx < points.length; pIdx += pointSkip) {
const dx = x - points[pIdx].x;
const dy = y - points[pIdx].y;
const d = dx * dx + dy * dy;
if (d < closestDist) {
closestDist = d;
}
}
const d = Math.sqrt(closestDist);
const band = d % bandGap;
const inBand = d > bandWidth && band <= bandWidth;
if (inBand) {
const gradientColor = getGradientColor(x, y, renderWidth, renderHeight, time);
data[i] = gradientColor.r;
data[i + 1] = gradientColor.g;
data[i + 2] = gradientColor.b;
data[i + 3] = alpha;
} else {
const bgColor = getBackgroundColor(x, y, renderWidth, renderHeight, time);
data[i] = bgColor.r;
data[i + 1] = bgColor.g;
data[i + 2] = bgColor.b;
data[i + 3] = alpha;
}
if (pixelStep > 1) {
for (let dy = 0; dy < pixelStep && y + dy < renderHeight; dy++) {
for (let dx = 0; dx < pixelStep && x + dx < renderWidth; dx++) {
if (dx === 0 && dy === 0) continue;
const ni = ((x + dx) + (y + dy) * renderWidth) * 4;
data[ni] = data[i];
data[ni + 1] = data[i + 1];
data[ni + 2] = data[i + 2];
data[ni + 3] = data[i + 3];
}
}
}
}
}
c.putImageData(imageData, 0, 0);
if (isAnimating) {
animationId = requestAnimationFrame(render);
}
}
function handleResize() {
initPoints(can.width, can.height);
}
function init() {
initColorSets();
startColorTransition();
isAnimating = true;
animationId = requestAnimationFrame(render);
window.addEventListener('resize', handleResize);
}
window.addEventListener('load', init);
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
isAnimating = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
} else {
isAnimating = true;
lastTimestamp = 0;
if (!animationId) {
animationId = requestAnimationFrame(render);
}
}
});
</script>
</body>
</html>