004 - 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>Hexagonal 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>
let isLightMode = false;
// ---------- 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');
isLightMode = true;
} else {
document.body.classList.remove('light-mode');
isLightMode = false;
}
} 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', { alpha: true });
const CONFIG = {
speed: 22,
density: 40,
hexSize: 45,
hexSpacing: 1.5,
colorTransitionDuration: 4000,
backgroundWaveSpeed: 0.00015
};
let isAnimating = true;
let animationId = null;
let resolutionScale = 0.35;
let time = 0;
let points = [];
let colorSets = [];
let currentColorSet = 0;
let targetColorSet = 1;
let colorTransitionStartTime = 0;
let isTransitioning = false;
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, 95, 60),
hslToRgb(hue2, 100, 55),
hslToRgb(hue3, 90, 65)
]);
}
}
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;
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);
}
}
return color1;
}
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function initPoints(width, height) {
points = [];
const pointCount = Math.floor((width * height) / (18000 / CONFIG.density));
for (let i = 0; i < pointCount; i++) {
points.push({
x: rand(0, width),
y: rand(0, height),
vx: rand(-CONFIG.speed/15, CONFIG.speed/15),
vy: rand(-CONFIG.speed/15, CONFIG.speed/15)
});
}
}
function updatePoints(width, height) {
for (let i = 0; i < points.length; i++) {
let pt = points[i];
pt.vx += rand(-0.03, 0.03);
pt.vy += rand(-0.03, 0.03);
const maxSpeed = CONFIG.speed / 12;
const speed = Math.sqrt(pt.vx * pt.vx + pt.vy * pt.vy);
if (speed > maxSpeed) {
pt.vx = (pt.vx / speed) * maxSpeed;
pt.vy = (pt.vy / speed) * maxSpeed;
}
pt.x += pt.vx;
pt.y += pt.vy;
if (pt.x <= 0 || pt.x >= width) {
pt.vx = -pt.vx * 0.95;
pt.x = pt.x <= 0 ? 1 : width - 1;
}
if (pt.y <= 0 || pt.y >= height) {
pt.vy = -pt.vy * 0.95;
pt.y = pt.y <= 0 ? 1 : height - 1;
}
}
}
function render(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const deltaTime = timestamp - lastTimestamp;
lastTimestamp = timestamp;
time += deltaTime * 0.06;
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 pixelStep = 1;
const alphaValue = isLightMode ? 38 : 100; // 15% alpha in light mode, ~40% in dark mode
for (let y = 0; y < renderHeight; y += pixelStep) {
for (let x = 0; x < renderWidth; x += pixelStep) {
const i = (x + y * renderWidth) * 4;
let closestDist = Infinity;
let secondClosestDist = Infinity;
let closestPoint = null;
for (let h = 0; h < points.length; h++) {
const pt = points[h];
const dx = x - pt.x;
const dy = y - pt.y;
const dist = dx * dx + dy * dy;
if (dist < closestDist) {
secondClosestDist = closestDist;
closestDist = dist;
closestPoint = pt;
} else if (dist < secondClosestDist) {
secondClosestDist = dist;
}
}
if (closestPoint) {
const distDiff = Math.sqrt(secondClosestDist) - Math.sqrt(closestDist);
const isEdge = distDiff < 2;
if (isEdge) {
data[i] = isLightMode ? 0 : 255;
data[i + 1] = isLightMode ? 0 : 255;
data[i + 2] = isLightMode ? 0 : 255;
data[i + 3] = alphaValue;
} else {
const pointColor = getGradientColor(closestPoint.x, closestPoint.y, renderWidth, renderHeight, time);
data[i] = pointColor.r;
data[i + 1] = pointColor.g;
data[i + 2] = pointColor.b;
data[i + 3] = alphaValue;
}
} else {
data[i] = isLightMode ? 255 : 8;
data[i + 1] = isLightMode ? 255 : 8;
data[i + 2] = isLightMode ? 255 : 10;
data[i + 3] = alphaValue;
}
}
}
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>