003 - 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 3</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #0a0a0a;
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.8s 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: "Electric Dreams",
canvas: null,
ctx: null,
points: [],
time: 0,
isActive: false,
animationId: null
},
{
name: "Neon Pulse",
canvas: null,
ctx: null,
points: [],
time: 0,
isActive: false,
animationId: null
},
{
name: "Cyber Drift",
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);
});
}
// Electric Point class - fast, sharp movements
class ElectricPoint {
constructor(x, y) {
this.x = x;
this.y = y;
this.baseX = x;
this.baseY = y;
this.vx = (Math.random() - 0.5) * 3;
this.vy = (Math.random() - 0.5) * 3;
this.jitter = Math.random() * 5;
}
update(t, width, height) {
// Sharp, erratic movements
const jitterX = (Math.random() - 0.5) * this.jitter;
const jitterY = (Math.random() - 0.5) * this.jitter;
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 + jitterX;
this.y = this.baseY + jitterY;
}
}
// Pulse Point class - rhythmic expansion/contraction
class PulsePoint {
constructor(x, y) {
this.x = x;
this.y = y;
this.centerX = x;
this.centerY = y;
this.phase = Math.random() * Math.PI * 2;
this.pulseSpeed = 0.8 + Math.random() * 0.4;
this.pulseRadius = 40 + Math.random() * 60;
}
update(t, width, height) {
const pulse = Math.sin(t * this.pulseSpeed + this.phase);
const offsetX = Math.cos(this.phase) * pulse * this.pulseRadius;
const offsetY = Math.sin(this.phase) * pulse * this.pulseRadius;
this.x = this.centerX + offsetX;
this.y = this.centerY + offsetY;
// Slow drift
this.centerX += Math.sin(t * 0.1 + this.phase) * 0.3;
this.centerY += Math.cos(t * 0.1 + this.phase) * 0.3;
// Keep in bounds
this.centerX = Math.max(50, Math.min(width - 50, this.centerX));
this.centerY = Math.max(50, Math.min(height - 50, this.centerY));
}
}
// Drift Point class - smooth, flowing movements
class DriftPoint {
constructor(x, y) {
this.x = x;
this.y = y;
this.angle = Math.random() * Math.PI * 2;
this.speed = 0.3 + Math.random() * 0.7;
this.spiralFactor = 0.01 + Math.random() * 0.02;
this.radius = Math.sqrt(x * x + y * y);
}
update(t, width, height) {
this.angle += this.spiralFactor;
this.radius += Math.sin(t * 0.5) * this.speed;
const centerX = width / 2;
const centerY = height / 2;
this.x = centerX + Math.cos(this.angle) * this.radius;
this.y = centerY + Math.sin(this.angle) * this.radius;
// Reset if too far
const maxDist = Math.max(width, height);
if (this.radius > maxDist * 0.6 || this.radius < 50) {
this.radius = 100 + Math.random() * 200;
}
}
}
// Initialize points for specific animation
function initPoints(animIndex) {
const anim = animations[animIndex];
anim.points = [];
const numPoints = animIndex === 0 ? 50 : animIndex === 1 ? 40 : 55;
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.35;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
if (animIndex === 0) {
anim.points.push(new ElectricPoint(x, y));
} else if (animIndex === 1) {
anim.points.push(new PulsePoint(x, y));
} else {
anim.points.push(new DriftPoint(x - centerX, y - centerY));
}
}
}
// Color function for Animation 1 (Electric Dreams)
function getColor1(index, total, t, isLightMode) {
const hue = (index / total * 120 + t * 20) % 120 + 160; // Cyan to electric blue
const lightness = 50 + Math.sin(t * 2 + index * 0.5) * 20;
const saturation = 90 + Math.cos(t * 1.5 + index) * 10;
const alpha = isLightMode ? 0.2 : 1;
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
}
// Color function for Animation 2 (Neon Pulse)
function getColor2(index, total, t, isLightMode) {
const pulse = Math.sin(t * 1.2 + index * 0.3) * 0.5 + 0.5;
const alpha = isLightMode ? 0.2 : 1;
// Alternate between hot pink and electric green
if ((index + Math.floor(t * 2)) % 2 === 0) {
const hue = 320 + pulse * 20; // Hot pink to magenta
const sat = 95;
const light = 55 + pulse * 20;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
} else {
const hue = 140 + pulse * 30; // Electric green
const sat = 90;
const light = 50 + pulse * 25;
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
}
}
// Color function for Animation 3 (Cyber Drift)
function getColor3(index, total, t, isLightMode) {
const progress = index / total;
const shift = Math.sin(t * 0.3) * 0.2;
const adjustedProgress = (progress + shift) % 1;
const alpha = isLightMode ? 0.2 : 1;
// Purple to cyan gradient
const hue = 280 - adjustedProgress * 100 + Math.sin(t * 0.5 + index) * 15;
const sat = 85 + Math.cos(t * 0.4 + index * 0.6) * 10;
const light = 45 + Math.sin(t * 0.6 + index * 0.3) * 15;
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 strong fade for sharper look
if (isLightMode) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
} else {
ctx.fillStyle = 'rgba(10, 10, 10, 0.15)';
}
ctx.fillRect(0, 0, width, height);
const step = 8; // Adjust step for performance
let getColor;
if (animIndex === 0) getColor = getColor1;
else if (animIndex === 1) getColor = getColor2;
else getColor = getColor3;
// Grid-based rendering with glow effect
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);
// Add subtle glow near boundaries
if (minDist > 5000 && Math.random() > 0.7) {
ctx.globalAlpha = 0.3;
ctx.fillRect(x, y, step, step);
ctx.globalAlpha = 1;
}
}
}
}
// 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);
// Button 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>