190 lines
No EOL
6.1 KiB
HTML
190 lines
No EOL
6.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Serpentine Echoes</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #0a0a1a;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
}
|
|
#attribution {
|
|
position: fixed;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
color: rgba(255, 255, 255, 0.3);
|
|
font-size: 10px;
|
|
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
|
}
|
|
</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();
|
|
|
|
// Strange attractor parameters
|
|
const params = {
|
|
a: 1.5,
|
|
b: 0.2,
|
|
c: 2.5,
|
|
d: 0.5,
|
|
scale: 10,
|
|
motion: 0.5,
|
|
density: 0.5,
|
|
complexity: 0.5,
|
|
connectedness: 0.5,
|
|
lifespan: 0.5,
|
|
pulse: {
|
|
avg: 1.12,
|
|
min: 0.9,
|
|
max: 1.3
|
|
},
|
|
tone: {
|
|
anger: 0,
|
|
sadness: 0,
|
|
curiosity: 0.7,
|
|
dryness: 0.8,
|
|
playfulness: 0.1,
|
|
tension: 0
|
|
}
|
|
};
|
|
|
|
// Color palette based on tone
|
|
function getColor() {
|
|
const curiosity = params.tone.curiosity;
|
|
const dryness = params.tone.dryness;
|
|
const playfulness = params.tone.playfulness;
|
|
const tension = params.tone.tension;
|
|
|
|
// Base teal with variations
|
|
const teal = `hsl(${180 + curiosity * 60}, ${100 * curiosity}%, ${30 + 10 * dryness}%)`;
|
|
const white = `hsl(0, 0%, ${100 * (1 - tension)}%)`;
|
|
|
|
// Add some playfulness if present
|
|
const hueVariation = playfulness * (10 + Math.sin(Date.now() / 1000) * 10);
|
|
return teal.replace('hsl(', `hsl(${hueVariation},`);
|
|
}
|
|
|
|
// Points array
|
|
let points = [];
|
|
const maxPoints = 200;
|
|
|
|
// Initialize points
|
|
function initPoints() {
|
|
points = [];
|
|
for (let i = 0; i < maxPoints * params.density; i++) {
|
|
points.push({
|
|
x: Math.random() * 2 - 1,
|
|
y: Math.random() * 2 - 1,
|
|
color: getColor(),
|
|
life: 0,
|
|
maxLife: 100 + Math.random() * 50
|
|
});
|
|
}
|
|
}
|
|
|
|
initPoints();
|
|
|
|
// Strange attractor function
|
|
function attractor(x, y, a, b, c, d) {
|
|
const xn = Math.sin(a * y) + c * Math.cos(a * x);
|
|
const yn = Math.sin(b * x) + d * Math.cos(b * y);
|
|
return [xn, yn];
|
|
}
|
|
|
|
// Main animation loop
|
|
function animate() {
|
|
// Calculate pulse effect
|
|
const pulse = params.pulse.avg + Math.sin(Date.now() / 500) * (params.pulse.max - params.pulse.min) / 2;
|
|
|
|
// Clear with semi-transparent background
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Update and draw points
|
|
points.forEach((point, i) => {
|
|
// Attractor movement
|
|
const [x, y] = attractor(point.x, point.y,
|
|
params.a, params.b, params.c * pulse, params.d * pulse);
|
|
|
|
// Update position with motion parameter
|
|
point.x += (x - point.x) * 0.01 * params.motion;
|
|
point.y += (y - point.y) * 0.01 * params.motion;
|
|
|
|
// Random small variations for complexity
|
|
point.x += (Math.random() - 0.5) * 0.01 * params.complexity;
|
|
point.y += (Math.random() - 0.5) * 0.01 * params.complexity;
|
|
|
|
// Wrap around edges
|
|
if (point.x > 1) point.x = -1;
|
|
if (point.x < -1) point.x = 1;
|
|
if (point.y > 1) point.y = -1;
|
|
if (point.y < -1) point.y = 1;
|
|
|
|
// Update life for lifespan effect
|
|
point.life += 1;
|
|
if (point.life > point.maxLife * (1 - params.lifespan)) {
|
|
point.life = 0;
|
|
// Reinitialize position
|
|
point.x = Math.random() * 2 - 1;
|
|
point.y = Math.random() * 2 - 1;
|
|
}
|
|
|
|
// Draw with alpha based on life
|
|
const alpha = (point.life / point.maxLife) * (0.5 + 0.5 * params.lifespan);
|
|
ctx.strokeStyle = point.color.replace('hsl', 'hsla').replace(')', `, ${alpha})`);
|
|
ctx.lineWidth = 1 + 2 * params.density * (1 - alpha);
|
|
|
|
// Draw line to previous point for connectedness
|
|
if (i > 0 && params.connectedness > 0.3) {
|
|
const prev = points[i - 1];
|
|
ctx.beginPath();
|
|
ctx.moveTo(
|
|
(prev.x + 1) * canvas.width / 2,
|
|
(prev.y + 1) * canvas.height / 2
|
|
);
|
|
ctx.lineTo(
|
|
(point.x + 1) * canvas.width / 2,
|
|
(point.y + 1) * canvas.height / 2
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Draw point
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
(point.x + 1) * canvas.width / 2,
|
|
(point.y + 1) * canvas.height / 2,
|
|
params.density * 2,
|
|
0,
|
|
Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
});
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
// Start animation
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html> |