002 - 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>Voronoi Animations</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000;
font-family: sans-serif;
transition: background-color 0.5s ease;
}
body.light-mode {
background-color: #ffffff;
}
.container {
position: relative;
width: 100vw;
height: 100vh;
}
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.5s ease;
}
canvas.active {
opacity: 1;
}
.nav-buttons {
position: absolute;
bottom: 40px;
right: 40px;
left: auto;
transform: none;
display: flex;
gap: 12px;
z-index: 100;
}
.nav-button {
width: 28px;
height: 28px;
background: transparent;
border: 1px solid #fff;
border-radius: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 2px 2px 0px 0px rgba(255,255,255,0.3);
}
body.light-mode .nav-button {
border: 1px solid #000;
box-shadow: 2px 2px 0px 0px rgba(0,0,0,0.8);
background: transparent;
}
.nav-button:hover {
background: #fff;
transform: translate(2px, 2px);
box-shadow: none;
}
body.light-mode .nav-button:hover {
background: #000;
transform: translate(2px, 2px);
box-shadow: none;
}
.nav-button:active {
transform: translate(2px, 2px) scale(0.95);
}
.nav-arrow {
width: 8px;
height: 8px;
border: solid #fff;
border-width: 0 1px 1px 0;
display: inline-block;
transition: border-color 0.2s ease;
}
.nav-button:hover .nav-arrow {
border-color: #000;
}
body.light-mode .nav-arrow {
border-color: #000;
}
body.light-mode .nav-button:hover .nav-arrow {
border-color: #fff;
}
.arrow-left {
transform: rotate(135deg);
margin-left: 2px;
}
.arrow-right {
transform: rotate(-45deg);
margin-right: 2px;
}
</style>
</head>
<body>
<div class="container">
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
<canvas id="canvas3"></canvas>
<div class="nav-buttons">
<div class="nav-button" id="prevBtn">
<div class="nav-arrow arrow-left"></div>
</div>
<div class="nav-button" id="nextBtn">
<div class="nav-arrow arrow-right"></div>
</div>
</div>
</div>
<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);
// Global state
let currentAnimation = 0;
const animations = [
{
name: "Deep Space",
canvas: null,
ctx: null,
points: [],
time: 0,
isActive: false,
animationId: null
},
{
name: "Evolving Colors",
canvas: null,
ctx: null,
points: [],
time: 0,
isActive: false,
animationId: null
},
{
name: "Organic Flow",
canvas: null,
ctx: null,
points: [],
time: 0,
isActive: false,
animationId: null
}
];
// Initialize canvases
function initCanvases() {
animations[0].canvas = document.getElementById('canvas1');
animations[0].ctx = animations[0].canvas.getContext('2d', { alpha: true });
animations[0].canvas.classList.add('active');
animations[0].isActive = true;
animations[1].canvas = document.getElementById('canvas2');
animations[1].ctx = animations[1].canvas.getContext('2d', { alpha: true });
animations[2].canvas = document.getElementById('canvas3');
animations[2].ctx = animations[2].canvas.getContext('2d', { alpha: true });
resizeCanvases();
}
// Resize all canvases
function resizeCanvases() {
animations.forEach(anim => {
if (anim.canvas) {
anim.canvas.width = window.innerWidth;
anim.canvas.height = window.innerHeight;
}
});
// Reinitialize points if needed
animations.forEach((anim, i) => {
if (anim.points.length > 0) initPoints(i);
});
}
// Point class for animations 1 and 2
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
this.baseX = x;
this.baseY = y;
this.vx = (Math.random() - 0.5) * 1.5;
this.vy = (Math.random() - 0.5) * 1.5;
this.phase = Math.random() * Math.PI * 2;
this.orbitRadius = 30 + Math.random() * 50;
this.orbitSpeed = 0.3 + Math.random() * 0.5;
}
update(t, width, height) {
const orbitX = Math.cos(t * this.orbitSpeed + this.phase) * this.orbitRadius;
const orbitY = Math.sin(t * this.orbitSpeed + this.phase) * this.orbitRadius;
this.baseX += this.vx;
this.baseY += this.vy;
if (this.baseX < 0 || this.baseX > width) {
this.vx *= -1;
this.baseX = Math.max(0, Math.min(width, this.baseX));
}
if (this.baseY < 0 || this.baseY > height) {
this.vy *= -1;
this.baseY = Math.max(0, Math.min(height, this.baseY));
}
this.x = this.baseX + orbitX;
this.y = this.baseY + orbitY;
}
}
// Point class for animation 3 (Organic Flow)
class FlowPoint {
constructor(x, y) {
this.x = x;
this.y = y;
this.baseX = x;
this.baseY = y;
this.angle = Math.random() * Math.PI * 2;
this.speed = 0.5 + Math.random() * 1;
this.noiseOffsetX = Math.random() * 1000;
this.noiseOffsetY = Math.random() * 1000;
this.phase = Math.random() * Math.PI * 2;
}
update(t, width, height) {
// Perlin-like noise simulation using multiple sine waves
const noiseX = Math.sin(t * 0.3 + this.noiseOffsetX) *
Math.cos(t * 0.2 + this.phase) * 2;
const noiseY = Math.cos(t * 0.25 + this.noiseOffsetY) *
Math.sin(t * 0.35 + this.phase) * 2;
this.baseX += noiseX * this.speed;
this.baseY += noiseY * this.speed;
// Soft boundaries - pull back toward center
const centerX = width / 2;
const centerY = height / 2;
const pullStrength = 0.002;
if (this.baseX < 50) {
this.baseX += (centerX - this.baseX) * pullStrength;
} else if (this.baseX > width - 50) {
this.baseX += (centerX - this.baseX) * pullStrength;
}
if (this.baseY < 50) {
this.baseY += (centerY - this.baseY) * pullStrength;
} else if (this.baseY > height - 50) {
this.baseY += (centerY - this.baseY) * pullStrength;
}
this.x = this.baseX;
this.y = this.baseY;
}
}
// Initialize points for specific animation
function initPoints(animIndex) {
const anim = animations[animIndex];
anim.points = [];
const numPoints = animIndex === 2 ? 45 : 60;
const centerX = anim.canvas.width / 2;
const centerY = anim.canvas.height / 2;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const radius = Math.random() * Math.min(anim.canvas.width, anim.canvas.height) * 0.4;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
if (animIndex === 2) {
anim.points.push(new FlowPoint(x, y));
} else {
anim.points.push(new Point(x, y));
}
}
}
// Color function for Animation 1 (Deep Space)
function getColor1(index, total, t, isLightMode) {
const cycle = (t * 0.1 + index / total) % 1;
const alpha = isLightMode ? 0.2 : 1;
if (cycle < 0.33) {
const hue = 280 + Math.sin(t * 0.2 + index) * 40;
const sat = 70 + Math.cos(t * 0.3 + index * 0.5) * 20;
const light = 25 + Math.sin(t * 0.15 + index * 0.3) * 15;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
} else if (cycle < 0.66) {
const hue = 180 + Math.sin(t * 0.25 + index * 0.7) * 30;
const sat = 80 + Math.cos(t * 0.2 + index) * 15;
const light = 35 + Math.sin(t * 0.18 + index * 0.4) * 20;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
} else {
const hue = 220 + Math.sin(t * 0.15 + index * 0.6) * 25;
const sat = 65 + Math.cos(t * 0.25 + index * 0.3) * 25;
const light = 30 + Math.sin(t * 0.12 + index * 0.5) * 18;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
}
}
// Color function for Animation 2 (Evolving Colors)
function getColor2(index, total, t, isLightMode) {
const baseHue = (index / total * 360 + t * 15) % 360;
const wave = Math.sin(t * 0.5 + index * 0.2) * 0.5 + 0.5;
const saturation = 60 + wave * 40;
const lightness = 45 + Math.sin(t * 0.3 + index * 0.15) * 15;
const alpha = isLightMode ? 0.2 : 1;
return `hsla(${baseHue}, ${saturation}%, ${lightness}%, ${alpha})`;
}
// Color function for Animation 3 (Organic Flow)
function getColor3(index, total, t, isLightMode) {
// Warm sunset gradient with oranges, pinks, and purples
const progress = index / total;
const timeShift = Math.sin(t * 0.1) * 0.3;
const adjustedProgress = (progress + timeShift) % 1;
const alpha = isLightMode ? 0.2 : 1;
if (adjustedProgress < 0.5) {
// Orange to pink
const hue = 15 + adjustedProgress * 60 + Math.sin(t * 0.2 + index * 0.3) * 15;
const sat = 85 + Math.cos(t * 0.15 + index * 0.4) * 10;
const light = 55 + Math.sin(t * 0.18 + index * 0.2) * 10;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
} else {
// Pink to purple
const hue = 320 + (adjustedProgress - 0.5) * 80 + Math.cos(t * 0.25 + index * 0.35) * 20;
const sat = 75 + Math.sin(t * 0.2 + index * 0.5) * 15;
const light = 50 + Math.cos(t * 0.12 + index * 0.3) * 12;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
}
}
// Draw Voronoi for specific animation
function drawVoronoi(animIndex) {
const anim = animations[animIndex];
const ctx = anim.ctx;
const width = anim.canvas.width;
const height = anim.canvas.height;
const points = anim.points;
const time = anim.time;
const isLightMode = document.body.classList.contains('light-mode');
// Clear with fade effect
if (isLightMode) {
ctx.fillStyle = animIndex === 2 ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 0.2)';
} else {
ctx.fillStyle = animIndex === 2 ? 'rgba(10, 5, 15, 0.08)' : 'rgba(0, 0, 0, 0.05)';
}
ctx.fillRect(0, 0, width, height);
const step = 8; // Increased step slightly for performance, adjust as needed
let getColor;
if (animIndex === 0) getColor = getColor1;
else if (animIndex === 1) getColor = getColor2;
else getColor = getColor3;
// Simple grid-based rendering
for (let y = 0; y < height; y += step) {
for (let x = 0; x < width; x += step) {
let minDist = Infinity;
let closestIndex = 0;
// Find closest point
for (let i = 0; i < points.length; i++) {
const dx = points[i].x - x;
const dy = points[i].y - y;
const d = dx * dx + dy * dy;
if (d < minDist) {
minDist = d;
closestIndex = i;
}
}
// Color based on closest point
const color = getColor(closestIndex, points.length, time, isLightMode);
ctx.fillStyle = color;
ctx.fillRect(x, y, step, step);
}
}
}
// Animation loop for specific animation
function animate(animIndex) {
const anim = animations[animIndex];
if (!anim.isActive) return;
anim.time += 0.016;
// Update all points
anim.points.forEach(p => p.update(anim.time, anim.canvas.width, anim.canvas.height));
// Draw the Voronoi diagram
drawVoronoi(animIndex);
anim.animationId = requestAnimationFrame(() => animate(animIndex));
}
// Switch to a specific animation
function switchAnimation(index) {
// Stop all animations first
animations.forEach(anim => {
if (anim.animationId) {
cancelAnimationFrame(anim.animationId);
anim.animationId = null;
}
anim.canvas.classList.remove('active');
anim.isActive = false;
});
// Update current index
currentAnimation = index;
// Start the new animation
const newAnim = animations[index];
newAnim.isActive = true;
newAnim.canvas.classList.add('active');
// Start animation if not already initialized
if (newAnim.points.length === 0) {
initPoints(index);
}
animate(index);
}
// Next animation
function nextAnimation() {
const nextIndex = (currentAnimation + 1) % animations.length;
switchAnimation(nextIndex);
}
// Previous animation
function prevAnimation() {
const prevIndex = (currentAnimation - 1 + animations.length) % animations.length;
switchAnimation(prevIndex);
}
// Initialize everything
function init() {
initCanvases();
animations.forEach((anim, i) => initPoints(i));
switchAnimation(0);
// Event listeners
document.getElementById('nextBtn').addEventListener('click', nextAnimation);
document.getElementById('prevBtn').addEventListener('click', prevAnimation);
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') nextAnimation();
if (e.key === 'ArrowLeft') prevAnimation();
});
// Window resize
window.addEventListener('resize', () => {
resizeCanvases();
});
}
// Start everything when page loads
window.addEventListener('load', init);
</script>
</body>
</html>