Custom Code Experiment •
Voronoi 3
Source Code View HTML / CSS / JavaScript
<!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: 'Courier New', monospace;
}
.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;
}
.ui-panel {
position: absolute;
top: 30px;
left: 30px;
background: rgba(0, 0, 0, 0.7);
border: 2px solid #00ff9f;
padding: 20px;
border-radius: 0;
backdrop-filter: blur(10px);
box-shadow: 0 0 20px rgba(0, 255, 159, 0.3);
z-index: 100;
}
.title {
color: #00ff9f;
font-size: 14px;
letter-spacing: 2px;
margin-bottom: 15px;
text-transform: uppercase;
text-shadow: 0 0 10px rgba(0, 255, 159, 0.5);
}
.mode-selector {
display: flex;
flex-direction: column;
gap: 10px;
}
.mode-btn {
background: rgba(0, 255, 159, 0.1);
border: 1px solid rgba(0, 255, 159, 0.3);
color: #00ff9f;
padding: 10px 20px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 12px;
letter-spacing: 1px;
transition: all 0.3s ease;
text-transform: uppercase;
position: relative;
overflow: hidden;
}
.mode-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 255, 159, 0.3), transparent);
transition: left 0.5s ease;
}
.mode-btn:hover::before {
left: 100%;
}
.mode-btn:hover {
background: rgba(0, 255, 159, 0.2);
border-color: #00ff9f;
box-shadow: 0 0 15px rgba(0, 255, 159, 0.4);
}
.mode-btn.active {
background: rgba(0, 255, 159, 0.3);
border-color: #00ff9f;
color: #fff;
box-shadow: 0 0 20px rgba(0, 255, 159, 0.6);
}
.mode-btn.active::after {
content: '◆';
margin-left: 10px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.status-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: rgba(0, 0, 0, 0.8);
border-top: 1px solid #00ff9f;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.status-text {
color: #00ff9f;
font-size: 11px;
letter-spacing: 3px;
text-transform: uppercase;
text-shadow: 0 0 8px rgba(0, 255, 159, 0.5);
}
@media (max-width: 768px) {
.ui-panel {
top: 15px;
left: 15px;
padding: 15px;
}
.title {
font-size: 12px;
}
.mode-btn {
padding: 8px 15px;
font-size: 11px;
}
}
</style>
</head>
<body>
<div class="container">
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
<canvas id="canvas3"></canvas>
<div class="ui-panel">
<div class="title">◆ Voronoi ◆</div>
<div class="mode-selector">
<button class="mode-btn active" data-mode="0">Electric</button>
<button class="mode-btn" data-mode="1">Neon</button>
<button class="mode-btn" data-mode="2">Cyber</button>
</div>
</div>
</div>
<script>
// 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');
animations[0].canvas.classList.add('active');
animations[0].isActive = true;
animations[1].canvas = document.getElementById('canvas2');
animations[1].ctx = animations[1].canvas.getContext('2d');
animations[2].canvas = document.getElementById('canvas3');
animations[2].ctx = animations[2].canvas.getContext('2d');
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) {
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;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
// Color function for Animation 2 (Neon Pulse)
function getColor2(index, total, t) {
const pulse = Math.sin(t * 1.2 + index * 0.3) * 0.5 + 0.5;
// 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 `hsl(${hue}, ${sat}%, ${light}%)`;
} else {
const hue = 140 + pulse * 30; // Electric green
const sat = 90;
const light = 50 + pulse * 25;
return `hsl(${hue}, ${sat}%, ${light}%)`;
}
}
// Color function for Animation 3 (Cyber Drift)
function getColor3(index, total, t) {
const progress = index / total;
const shift = Math.sin(t * 0.3) * 0.2;
const adjustedProgress = (progress + shift) % 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 `hsl(${hue}, ${sat}%, ${light}%)`;
}
// 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;
// Clear with strong fade for sharper look
ctx.fillStyle = 'rgba(10, 10, 10, 0.15)';
ctx.fillRect(0, 0, width, height);
const step = animIndex === 0 ? 3 : 2;
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);
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;
// Update UI
document.querySelectorAll('.mode-btn').forEach((btn, i) => {
btn.classList.toggle('active', i === 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);
}
// Initialize everything
function init() {
initCanvases();
animations.forEach((anim, i) => initPoints(i));
switchAnimation(0);
// Button event listeners
document.querySelectorAll('.mode-btn').forEach((btn, index) => {
btn.addEventListener('click', () => switchAnimation(index));
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
const next = (currentAnimation + 1) % animations.length;
switchAnimation(next);
}
if (e.key === 'ArrowLeft') {
const prev = (currentAnimation - 1 + animations.length) % animations.length;
switchAnimation(prev);
}
if (e.key >= '1' && e.key <= '3') {
switchAnimation(parseInt(e.key) - 1);
}
});
// Window resize
window.addEventListener('resize', () => {
resizeCanvases();
});
}
// Start everything when page loads
window.addEventListener('load', init);
</script>
</body>
</html>