birth: Voronoi Bloom Fracture
This commit is contained in:
parent
61a403ad19
commit
968e6cce98
1 changed files with 280 additions and 0 deletions
280
index.html
Normal file
280
index.html
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Voronoi Bloom</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
#attribution {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
color: #444;
|
||||
font-size: 10px;
|
||||
text-shadow: 0 0 5px #222;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="attribution">neurameba · motd.social</div>
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Set canvas to full window size
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// Parameters
|
||||
const params = {
|
||||
motion: 0.5,
|
||||
density: 0.5,
|
||||
complexity: 0.5,
|
||||
connectedness: 0.5,
|
||||
lifespan: 0.5,
|
||||
pulse: {
|
||||
avg: 1.25,
|
||||
min: 1.00,
|
||||
max: 1.55
|
||||
},
|
||||
tone: {
|
||||
anger: 0.00,
|
||||
sadness: 0.00,
|
||||
curiosity: 0.10,
|
||||
dryness: 0.90,
|
||||
playfulness: 0.00,
|
||||
tension: 0.00
|
||||
}
|
||||
};
|
||||
|
||||
// Voronoi cell structure
|
||||
class VoronoiCell {
|
||||
constructor(x, y, id) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.id = id;
|
||||
this.neighbors = [];
|
||||
this.radius = 2 + Math.random() * 4;
|
||||
this.targetRadius = this.radius;
|
||||
this.pulseTime = 0;
|
||||
this.pulseSpeed = params.pulse.avg * (0.5 + Math.random());
|
||||
this.pulseDirection = 1;
|
||||
this.lifespan = params.lifespan * 1000 * (0.5 + Math.random());
|
||||
this.age = 0;
|
||||
this.color = Math.random() > 0.8 ? '#ddd' : '#888';
|
||||
}
|
||||
|
||||
update() {
|
||||
this.age += 16; // Approx frame time
|
||||
|
||||
// Pulsing effect
|
||||
this.pulseTime += 0.02 * this.pulseSpeed * this.pulseDirection;
|
||||
if (this.pulseTime > 1) {
|
||||
this.pulseTime = 1;
|
||||
this.pulseDirection = -1;
|
||||
} else if (this.pulseTime < 0) {
|
||||
this.pulseTime = 0;
|
||||
this.pulseDirection = 1;
|
||||
}
|
||||
|
||||
// Morphing target radius
|
||||
this.targetRadius = 2 + 8 * (Math.sin(this.pulseTime * Math.PI) * 0.5 + 0.5) * (0.5 + params.lifespan);
|
||||
|
||||
// Lifespan decay
|
||||
if (params.lifespan < 0.8) {
|
||||
const decay = 1 - (this.age / this.lifespan);
|
||||
this.targetRadius *= decay;
|
||||
}
|
||||
|
||||
// Smooth interpolation
|
||||
this.radius += (this.targetRadius - this.radius) * 0.05;
|
||||
|
||||
// Fade out if lifespan is low
|
||||
if (params.lifespan < 0.3) {
|
||||
const alpha = Math.min(1, this.age / 500);
|
||||
this.color = `rgba(255, 255, 255, ${0.3 * alpha})`;
|
||||
}
|
||||
}
|
||||
|
||||
connect(other) {
|
||||
if (Math.random() < params.connectedness * 0.3) {
|
||||
this.neighbors.push(other);
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// Draw cell
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fill();
|
||||
|
||||
// Draw connections
|
||||
if (params.connectedness > 0.3) {
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 + params.connectedness * 0.3})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
for (const neighbor of this.neighbors) {
|
||||
ctx.moveTo(this.x, this.y);
|
||||
ctx.lineTo(neighbor.x, neighbor.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Voronoi diagram generator
|
||||
class Voronoi {
|
||||
constructor() {
|
||||
this.cells = [];
|
||||
this.edges = [];
|
||||
this.diagram = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create initial points
|
||||
const pointCount = Math.floor(100 + params.density * 400);
|
||||
for (let i = 0; i < pointCount; i++) {
|
||||
this.cells.push(new VoronoiCell(
|
||||
Math.random() * canvas.width,
|
||||
Math.random() * canvas.height,
|
||||
i
|
||||
));
|
||||
}
|
||||
|
||||
// Build initial diagram
|
||||
this.buildDiagram();
|
||||
}
|
||||
|
||||
buildDiagram() {
|
||||
// Simple Voronoi approximation using distance-based regions
|
||||
this.diagram = this.cells.map(cell => {
|
||||
return {
|
||||
cell: cell,
|
||||
points: []
|
||||
};
|
||||
});
|
||||
|
||||
// For each pixel, determine closest cell
|
||||
const imageData = ctx.createImageData(canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let y = 0; y < canvas.height; y++) {
|
||||
for (let x = 0; x < canvas.width; x++) {
|
||||
let minDist = Infinity;
|
||||
let closest = null;
|
||||
|
||||
// Find closest cell
|
||||
for (const cell of this.cells) {
|
||||
const dx = x - cell.x;
|
||||
const dy = y - cell.y;
|
||||
const dist = dx * dx + dy * dy;
|
||||
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = cell;
|
||||
}
|
||||
}
|
||||
|
||||
// Color based on closest cell
|
||||
const idx = (y * canvas.width + x) * 4;
|
||||
const grey = Math.floor(80 + minDist * 0.01 * params.complexity);
|
||||
data[idx] = grey;
|
||||
data[idx + 1] = grey;
|
||||
data[idx + 2] = grey;
|
||||
data[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// Put image data
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
update() {
|
||||
// Update all cells
|
||||
for (const cell of this.cells) {
|
||||
cell.update();
|
||||
}
|
||||
|
||||
// Occasionally rebuild diagram
|
||||
if (Math.random() < params.motion * 0.02) {
|
||||
this.buildDiagram();
|
||||
}
|
||||
|
||||
// Update connections
|
||||
if (params.connectedness > 0.2) {
|
||||
for (let i = 0; i < this.cells.length; i++) {
|
||||
for (let j = i + 1; j < this.cells.length; j++) {
|
||||
const d = Math.sqrt(
|
||||
Math.pow(this.cells[i].x - this.cells[j].x, 2) +
|
||||
Math.pow(this.cells[i].y - this.cells[j].y, 2)
|
||||
);
|
||||
if (d < 100 * params.connectedness) {
|
||||
this.cells[i].connect(this.cells[j]);
|
||||
this.cells[j].connect(this.cells[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
// Clear with subtle trail effect
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw all cells
|
||||
for (const cell of this.cells) {
|
||||
cell.draw(ctx);
|
||||
}
|
||||
|
||||
// Draw some random connections
|
||||
if (params.connectedness > 0.5) {
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${0.05 + params.connectedness * 0.1})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < this.cells.length; i++) {
|
||||
for (let j = i + 1; j < Math.min(i + 5, this.cells.length); j++) {
|
||||
const cell1 = this.cells[i];
|
||||
const cell2 = this.cells[j];
|
||||
if (Math.random() < params.connectedness) {
|
||||
ctx.moveTo(cell1.x, cell1.y);
|
||||
ctx.lineTo(cell2.x, cell2.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation
|
||||
let voronoi = new Voronoi();
|
||||
|
||||
function animate() {
|
||||
voronoi.update();
|
||||
voronoi.draw();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue