birth: Tendrils in Wavering Currents
This commit is contained in:
parent
3e9b7c1c67
commit
30b90f1e2f
1 changed files with 218 additions and 0 deletions
218
index.html
Normal file
218
index.html
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Flow Field Tendrils</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #ccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
canvas {
|
||||
flex: 1;
|
||||
}
|
||||
#attribution {
|
||||
text-align: right;
|
||||
padding: 8px 16px;
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</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');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// Parameters derived from input
|
||||
const params = {
|
||||
motion: 0.5,
|
||||
density: 0.5,
|
||||
complexity: 0.5,
|
||||
connectedness: 0.5,
|
||||
lifespan: 0.5,
|
||||
pulse: { avg: 1.07, min: 1.0, max: 1.1 },
|
||||
tone: { curiosity: 0.7, dryness: 0.8, playfulness: 0.3 }
|
||||
};
|
||||
|
||||
// Flow field parameters
|
||||
const fieldWidth = 32;
|
||||
const fieldHeight = 32;
|
||||
const segmentCount = Math.floor(150 * params.density + 20);
|
||||
const maxParticles = 500;
|
||||
const decayRate = 0.995 * params.lifespan + 0.005;
|
||||
const complexityRatio = params.complexity * 0.8 + 0.2;
|
||||
const motionFactor = params.motion * 1.5 + 0.5;
|
||||
const connectednessFactor = params.connectedness;
|
||||
const colorDryness = params.tone.dryness;
|
||||
|
||||
// Flow field vectors (curl noise)
|
||||
const vectors = Array(fieldHeight).fill().map((_, y) =>
|
||||
Array(fieldWidth).fill().map((_, x) => {
|
||||
const noiseX = x / fieldWidth * 4 + performance.now() * 0.0005 * motionFactor;
|
||||
const noiseY = y / fieldHeight * 4 + performance.now() * 0.0003 * motionFactor;
|
||||
const angle = Math.atan2(
|
||||
Math.cos(noiseX * 2) * Math.sin(noiseY * 3),
|
||||
Math.sin(noiseX * 3) * Math.cos(noiseY * 2)
|
||||
) + Math.PI/2;
|
||||
const length = 0.8 + Math.sin(noiseX) * 0.3 + Math.cos(noiseY) * 0.2;
|
||||
return { x: Math.cos(angle) * length, y: Math.sin(angle) * length };
|
||||
})
|
||||
);
|
||||
|
||||
// Particle system
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.reset();
|
||||
this.life = params.lifespan > 0.7 ? 1000 : Math.random() * 200 + 50;
|
||||
this.width = params.dryness > 0.7 ? 0.5 : 1 + Math.random() * 2;
|
||||
this.trail = [];
|
||||
this.trailLength = Math.floor(params.complexity * 20 + 5);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.pos = {
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height
|
||||
};
|
||||
this.vel = { x: 0, y: 0 };
|
||||
this.size = params.density > 0.5 ?
|
||||
(1 + Math.random() * 2) * (0.5 + params.connectedness * 0.5) :
|
||||
(0.5 + Math.random()) * (0.5 + params.connectedness * 0.5);
|
||||
this.hue = 180 + Math.random() * 30;
|
||||
this.saturation = 60 + params.tone.curiosity * 20;
|
||||
this.lightness = 70;
|
||||
this.noiseOffset = Math.random() * 100;
|
||||
this.angle = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
// Get flow field vector
|
||||
const fieldX = Math.floor(this.pos.x / canvas.width * fieldWidth) % fieldWidth;
|
||||
const fieldY = Math.floor(this.pos.y / canvas.height * fieldHeight) % fieldHeight;
|
||||
const fieldVec = vectors[fieldY][fieldX];
|
||||
|
||||
// Add turbulence based on complexity
|
||||
const turbulence = params.complexity * 0.3;
|
||||
const noiseX = this.pos.x * 0.01 + this.noiseOffset;
|
||||
const noiseY = this.pos.y * 0.01 + this.noiseOffset;
|
||||
const turbulenceX = Math.cos(noiseX * 5 + performance.now() * 0.001) * turbulence;
|
||||
const turbulenceY = Math.sin(noiseY * 5 + performance.now() * 0.001) * turbulence;
|
||||
|
||||
// Update velocity with field influence
|
||||
this.vel.x += (fieldVec.x + turbulenceX) * motionFactor * 0.1;
|
||||
this.vel.y += (fieldVec.y + turbulenceY) * motionFactor * 0.1;
|
||||
|
||||
// Add connectedness behavior (network-like attraction)
|
||||
if (connectednessFactor > 0.3) {
|
||||
const closest = particles.filter(p => p !== this && p.size > 1)
|
||||
.sort((a, b) => {
|
||||
const distA = Math.hypot(a.pos.x - this.pos.x, a.pos.y - this.pos.y);
|
||||
const distB = Math.hypot(b.pos.x - this.pos.x, b.pos.y - this.pos.y);
|
||||
return distA - distB;
|
||||
})[0];
|
||||
|
||||
if (closest) {
|
||||
const dist = Math.hypot(closest.pos.x - this.pos.x, closest.pos.y - this.pos.y);
|
||||
if (dist < 200) {
|
||||
const influence = connectednessFactor * (1 - dist/200);
|
||||
this.vel.x += (closest.pos.x - this.pos.x) * influence * 0.001;
|
||||
this.vel.y += (closest.pos.y - this.pos.y) * influence * 0.001;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update position
|
||||
this.pos.x += this.vel.x;
|
||||
this.pos.y += this.vel.y;
|
||||
|
||||
// Boundary check
|
||||
if (this.pos.x < 0 || this.pos.x > canvas.width) this.vel.x *= -0.5;
|
||||
if (this.pos.y < 0 || this.pos.y > canvas.height) this.vel.y *= -0.5;
|
||||
|
||||
// Trail capture
|
||||
this.trail.push({...this.pos});
|
||||
if (this.trail.length > this.trailLength) {
|
||||
this.trail.shift();
|
||||
}
|
||||
|
||||
// Lifespan decay
|
||||
this.life--;
|
||||
this.size *= decayRate;
|
||||
this.lightness = Math.max(30, this.lightness - 0.1);
|
||||
|
||||
// Reset if dead
|
||||
if (this.life <= 0 || this.size < 0.1) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.strokeStyle = `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`;
|
||||
ctx.lineWidth = this.width;
|
||||
ctx.beginPath();
|
||||
|
||||
// Draw trail
|
||||
if (this.trail.length > 1) {
|
||||
ctx.moveTo(this.trail[0].x, this.trail[0].y);
|
||||
for (let i = 1; i < this.trail.length; i++) {
|
||||
const point = this.trail[i];
|
||||
const next = this.trail[i + 1] || this.pos;
|
||||
const controlX = (point.x + next.x) / 2;
|
||||
const controlY = (point.y + next.y) / 2;
|
||||
|
||||
ctx.quadraticCurveTo(
|
||||
point.x, point.y,
|
||||
controlX, controlY
|
||||
);
|
||||
}
|
||||
ctx.lineTo(this.pos.x, this.pos.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let particles = Array(maxParticles).fill().map(() => new Particle());
|
||||
|
||||
function animate() {
|
||||
// Pulsing density
|
||||
const pulseFactor = params.pulse.avg + Math.sin(performance.now() * 0.002) * (params.pulse.max - params.pulse.min) * 0.5;
|
||||
|
||||
// Clear with fade effect
|
||||
ctx.fillStyle = `rgba(10, 10, 10, ${0.05 * params.dryness + 0.05})`;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Update and draw particles
|
||||
particles.forEach(p => {
|
||||
p.update();
|
||||
p.draw();
|
||||
});
|
||||
|
||||
// Occasionally add new particles based on density
|
||||
if (Math.random() < (0.01 * pulseFactor * params.density)) {
|
||||
particles[Math.floor(Math.random() * particles.length)].reset();
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue