tendrils-in-wavering-curren.../index.html

218 lines
No EOL
8.4 KiB
HTML

<!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>