Starling Flock Topological Transitions
Generated by GridFlow AI | Tags: verlet-integration, cloth-simulation, bezier-curves, synthwave, generative-art, topological-flow, multi-layer-compositing, interactive-art
💡 AI 提示词
Starling flock murmuration patterns represented as flowing cloth rope simulations transitioning through topological states with synthwave sunset colors🔧 核心算法要点
- Verlet integration physics with position-based dynamics for rope segment simulation
- Multi-pass constraint solving with 3 iterations per frame for stable cloth-like behavior
- Bezier curve ribbon rendering using curveVertex with normal-based ribbon width modulation
- Perlin noise field driving flock-like collective motion patterns across all rope anchor points
- Multi-layer compositing: background gradient, additive glow layer, screen-blended curves, and pixel shimmer
- Radial gradient blooms at select nodes creating bioluminescent starling effect with pulsing opacity
🎨 原始代码
var sketch = function(p) {
var ropes = [];
var numRopes = 12;
var nodesPerRope = 45;
var bgBuffer, glowBuffer, curveBuffer;
var time = 0;
var mode = 0;
var mouseInfluence = 0;
var showGuide = true;
var noiseZ = 0;
var Rope = function(x, y, length, segCount) {
this.segments = [];
this.restLength = length / segCount;
this.damping = 0.97;
this.stiffness = 0.4;
for (var i = 0; i < segCount; i++) {
this.segments.push({
x: x,
y: y + i * this.restLength,
oldX: x,
oldY: y + i * this.restLength,
pinned: i === 0
});
}
};
Rope.prototype.update = function(targetX, targetY, noiseOffset) {
var segs = this.segments;
var segCount = segs.length;
for (var i = 0; i < segCount; i++) {
var seg = segs[i];
if (seg.pinned) {
seg.x += (targetX - seg.x) * 0.08;
seg.y += (targetY - seg.y) * 0.08;
continue;
}
var vx = (seg.x - seg.oldX) * this.damping;
var vy = (seg.y - seg.oldY) * this.damping;
seg.oldX = seg.x;
seg.oldY = seg.y;
var noiseScale = 0.003;
var noiseX = p.noise(seg.x * noiseScale + noiseOffset, seg.y * noiseScale, noiseZ) - 0.5;
var noiseY = p.noise(seg.x * noiseScale + 100, seg.y * noiseScale + noiseOffset, noiseZ + 50) - 0.5;
var flockNoise = p.noise(seg.x * 0.01 + time * 0.3, seg.y * 0.01, noiseZ * 2);
var angle = flockNoise * p.TWO_PI * 4 + noiseOffset * 0.1;
var flockForceX = p.cos(angle) * 0.3;
var flockForceY = p.sin(angle) * 0.3;
seg.x += vx + noiseX * 2 + flockForceX;
seg.y += vy + noiseY * 2 + flockForceY;
var mouseDist = p.dist(seg.x, seg.y, p.mouseX, p.mouseY);
if (mouseDist < 200) {
var force = (200 - mouseDist) / 200 * mouseInfluence;
var angle = p.atan2(seg.y - p.mouseY, seg.x - p.mouseX);
seg.x += p.cos(angle) * force * 3;
seg.y += p.sin(angle) * force * 3;
}
}
for (var iter = 0; iter < 3; iter++) {
for (var i = 0; i < segCount - 1; i++) {
var seg1 = segs[i];
var seg2 = segs[i + 1];
var dx = seg2.x - seg1.x;
var dy = seg2.y - seg1.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var diff = (this.restLength - dist) / dist * this.stiffness;
var offsetX = dx * diff * 0.5;
var offsetY = dy * diff * 0.5;
if (!seg1.pinned) {
seg1.x -= offsetX;
seg1.y -= offsetY;
}
seg2.x += offsetX;
seg2.y += offsetY;
}
}
};
Rope.prototype.render = function(buffer, colorIndex, thickness) {
var segs = this.segments;
var len = segs.length;
if (len < 4) return;
buffer.push();
buffer.noFill();
var t = time * 0.001;
var palette = getSynthwaveColor(colorIndex, t);
buffer.stroke(palette.r, palette.g, palette.b, palette.a * 0.3);
buffer.strokeWeight(thickness + 8);
drawBezierRibbon(buffer, segs, len, 0.6);
buffer.stroke(palette.r, palette.g, palette.b, palette.a * 0.5);
buffer.strokeWeight(thickness + 4);
drawBezierRibbon(buffer, segs, len, 0.8);
buffer.stroke(palette.r * 1.2, palette.g * 1.1, palette.b * 0.9, palette.a);
buffer.strokeWeight(thickness);
drawBezierRibbon(buffer, segs, len, 1.0);
buffer.pop();
};
function drawBezierRibbon(buffer, segs, len, widthMult) {
buffer.beginShape();
var step = 3;
for (var i = 0; i < len - 3; i += step) {
var p0 = segs[Math.max(0, i - 1)];
var p1 = segs[i];
var p2 = segs[i + 1];
var p3 = segs[Math.min(len - 1, i + 2)];
var tVal = (i + step * 0.5) / len;
var width = widthMult * (0.5 + 0.5 * p.sin(tVal * p.PI));
var dx1 = p2.x - p0.x;
var dy1 = p2.y - p0.y;
var dx2 = p3.x - p1.x;
var dy2 = p3.y - p1.y;
var len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) + 0.001;
var len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) + 0.001;
var nx1 = -dy1 / len1 * width * 8;
var ny1 = dx1 / len1 * width * 8;
var nx2 = -dy2 / len2 * width * 8;
var ny2 = dx2 / len2 * width * 8;
if (i === 0) {
buffer.curveVertex(p1.x + nx1, p1.y + ny1);
}
var cp1x = p1.x + dx1 * 0.3 + nx1;
var cp1y = p1.y + dy1 * 0.3 + ny1;
var cp2x = p2.x - dx2 * 0.3 + nx2;
var cp2y = p2.y - dy2 * 0.3 + ny2;
buffer.curveVertex(cp1x, cp1y);
buffer.curveVertex(p2.x + nx2 * 0.5, p2.y + ny2 * 0.5);
buffer.curveVertex(cp2x, cp2y);
if (i >= len - 4) {
buffer.curveVertex(p2.x + nx2, p2.y + ny2);
}
}
for (var i = len - 4; i >= 0; i -= step) {
var p0 = segs[Math.max(0, i - 1)];
var p1 = segs[i];
var p2 = segs[i + 1];
var p3 = segs[Math.min(len - 1, i + 2)];
var tVal = (i + step * 0.5) / len;
var width = widthMult * (0.5 + 0.5 * p.sin(tVal * p.PI));
var dx1 = p2.x - p0.x;
var dy1 = p2.y - p0.y;
var dx2 = p3.x - p1.x;
var dy2 = p3.y - p1.y;
var len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) + 0.001;
var len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) + 0.001;
var nx1 = dy1 / len1 * width * 8;
var ny1 = -dx1 / len1 * width * 8;
var nx2 = dy2 / len2 * width * 8;
var ny2 = -dx2 / len2 * width * 8;
buffer.curveVertex(p2.x + nx2, p2.y + ny2);
buffer.curveVertex(p2.x - nx2 * 0.5, p2.y - ny2 * 0.5);
buffer.curveVertex(p1.x - nx1 * 0.5, p1.y - ny1 * 0.5);
buffer.curveVertex(p1.x + nx1, p1.y + ny1);
}
buffer.endShape(p.CLOSE);
}
function getSynthwaveColor(index, t) {
var colors = [
{ r: 75, g: 0, b: 130 },
{ r: 139, g: 0, b: 139 },
{ r: 199, g: 21, b: 133 },
{ r: 255, g: 105, b: 180 },
{ r: 255, g: 165, b: 0 },
{ r: 255, g: 215, b: 0 }
];
var c1 = colors[index % colors.length];
var c2 = colors[(index + 1) % colors.length];
var blend = (p.sin(t * 0.5) + 1) * 0.5;
return {
r: c1.r + (c2.r - c1.r) * blend,
g: c1.g + (c2.g - c1.g) * blend,
b: c1.b + (c2.b - c1.b) * blend,
a: 180 + 75 * p.sin(t + index)
};
}
function renderBackground() {
bgBuffer.push();
bgBuffer.noStroke();
var gradient = bgBuffer.drawingContext.createLinearGradient(0, 0, 0, p.height);
gradient.addColorStop(0, '#1a0a2e');
gradient.addColorStop(0.3, '#4b1060');
gradient.addColorStop(0.5, '#c41e7f');
gradient.addColorStop(0.7, '#ff6b35');
gradient.addColorStop(1, '#ffb347');
bgBuffer.drawingContext.fillStyle = gradient;
bgBuffer.drawingContext.fillRect(0, 0, p.width, p.height);
var gridAlpha = 20 + 10 * p.sin(time * 0.001);
bgBuffer.stroke(255, 255, 255, gridAlpha);
bgBuffer.strokeWeight(0.5);
var gridSpacing = 60;
var perspective = 0.002;
var horizonY = p.height * 0.55;
for (var y = horizonY; y < p.height; y += 15) {
var t = (y - horizonY) / (p.height - horizonY);
var spacing = gridSpacing * (1 + t * 3);
var alpha = gridAlpha * (0.3 + 0.7 * t);
bgBuffer.stroke(255, 255, 255, alpha);
bgBuffer.line(0, y, p.width, y);
}
for (var x = 0; x < p.width; x += gridSpacing) {
var horizonX = p.width / 2;
bgBuffer.stroke(255, 255, 255, gridAlpha * 0.5);
bgBuffer.line(x, horizonY, horizonX + (x - horizonX) * 0.1, p.height);
}
bgBuffer.pop();
}
function renderGlow() {
glowBuffer.clear();
for (var i = 0; i < ropes.length; i++) {
var rope = ropes[i];
var segs = rope.segments;
for (var j = 2; j < segs.length - 2; j += 4) {
var seg = segs[j];
var pulse = 0.5 + 0.5 * p.sin(time * 0.003 + i * 0.5 + j * 0.1);
var size = 20 + 30 * pulse;
var gradient = glowBuffer.drawingContext.createRadialGradient(
seg.x, seg.y, 0,
seg.x, seg.y, size
);
var palette = getSynthwaveColor(i, time * 0.001);
gradient.addColorStop(0, `rgba(${palette.r}, ${palette.g}, ${palette.b}, ${0.4 * pulse})`);
gradient.addColorStop(0.5, `rgba(${palette.r}, ${palette.g}, ${palette.b}, ${0.1 * pulse})`);
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
glowBuffer.drawingContext.fillStyle = gradient;
glowBuffer.drawingContext.beginPath();
glowBuffer.drawingContext.arc(seg.x, seg.y, size, 0, p.TWO_PI);
glowBuffer.drawingContext.fill();
}
}
}
function renderPixelNoise() {
curveBuffer.loadPixels();
var d = curveBuffer.pixelDensity();
var w = curveBuffer.width * d;
var h = curveBuffer.height * d;
var step = 3;
for (var y = 0; y < h; y += step) {
for (var x = 0; x < w; x += step) {
var idx = (y * w + x) * 4;
var noiseVal = p.noise(x * 0.01, y * 0.01, noiseZ * 3);
var shimmer = 0.9 + 0.1 * noiseVal;
curveBuffer.pixels[idx] = Math.min(255, curveBuffer.pixels[idx] * shimmer);
curveBuffer.pixels[idx + 1] = Math.min(255, curveBuffer.pixels[idx + 1] * shimmer);
curveBuffer.pixels[idx + 2] = Math.min(255, curveBuffer.pixels[idx + 2] * shimmer);
if (noiseVal > 0.7) {
var boost = (noiseVal - 0.7) * 3;
curveBuffer.pixels[idx] = Math.min(255, curveBuffer.pixels[idx] + 30 * boost);
curveBuffer.pixels[idx + 1] = Math.min(255, curveBuffer.pixels[idx + 1] + 15 * boost);
curveBuffer.pixels[idx + 2] = Math.min(255, curveBuffer.pixels[idx + 2] + 20 * boost);
}
}
}
curveBuffer.updatePixels();
}
function initializeRopes() {
ropes = [];
var centerX = p.width / 2;
var centerY = p.height / 2;
for (var i = 0; i < numRopes; i++) {
var angle = (i / numRopes) * p.TWO_PI;
var radius = 50 + i * 15;
var startX = centerX + p.cos(angle) * radius;
var startY = centerY + p.sin(angle) * radius;
var length = 150 + p.random(100);
ropes.push(new Rope(startX, startY, length, nodesPerRope));
}
}
p.setup = function() {
var container = document.getElementById('p5-wrapper');
p.createCanvas(container.offsetWidth, container.offsetHeight).parent(container);
bgBuffer = p.createGraphics(p.width, p.height);
glowBuffer = p.createGraphics(p.width, p.height);
curveBuffer = p.createGraphics(p.width, p.height);
bgBuffer.pixelDensity(p.pixelDensity());
glowBuffer.pixelDensity(p.pixelDensity());
curveBuffer.pixelDensity(p.pixelDensity());
p.noSmooth();
bgBuffer.noSmooth();
glowBuffer.noSmooth();
curveBuffer.noSmooth();
initializeRopes();
};
p.draw = function() {
time = p.frameCount;
noiseZ = time * 0.005;
mouseInfluence = p.map(p.mouseX, 0, p.width, 0, 2);
renderBackground();
curveBuffer.clear();
var centerX = p.width / 2;
var centerY = p.height / 2;
var flockTime = time * 0.001;
for (var i = 0; i < ropes.length; i++) {
var rope = ropes[i];
var angle = (i / ropes.length) * p.TWO_PI * 3 + flockTime;
var radius = 80 + 150 * p.noise(i * 0.3, time * 0.001);
if (mode === 0) {
var targetX = centerX + p.cos(angle) * radius;
var targetY = centerY + p.sin(angle) * radius * 0.7;
} else if (mode === 1) {
var wave = p.sin(time * 0.002 + i * 0.5) * 100;
var targetX = centerX + wave + p.cos(angle) * radius;
var targetY = centerY + p.sin(angle) * radius * 0.7;
} else {
var starAngle = angle + p.noise(i * 0.5, time * 0.001) * p.PI;
var targetX = centerX + p.cos(starAngle) * radius * 1.5;
var targetY = centerY + p.sin(starAngle * 2) * radius * 0.8;
}
rope.update(targetX, targetY, i * 0.1 + time * 0.001);
rope.render(curveBuffer, i % 6, 1.5 + p.sin(time * 0.002 + i) * 0.5);
}
renderGlow();
renderPixelNoise();
p.image(bgBuffer, 0, 0);
p.blendMode(p.ADD);
p.image(glowBuffer, 0, 0);
p.blendMode(p.SCREEN);
p.tint(255, 220);
p.image(curveBuffer, 0, 0);
p.blendMode(p.BLEND);
p.tint(255, 255);
if (showGuide) {
renderUI();
}
};
function renderUI() {
p.push();
p.noStroke();
p.fill(255, 255, 255, 30);
p.textSize(11);
p.textAlign(p.LEFT, p.BOTTOM);
p.text('MOUSE: Influence flock | CLICK: Burst | SPACE: Toggle UI | 1-3: Morph modes | R: Reset', 10, p.height - 10);
var modeNames = ['Spiral', 'Wave', 'Star'];
p.fill(255, 255, 255, 100);
p.textAlign(p.RIGHT, p.TOP);
p.textSize(14);
p.text('Mode: ' + modeNames[mode], p.width - 15, 15);
var pulse = 0.5 + 0.5 * p.sin(time * 0.005);
var barWidth = 100;
var barHeight = 3;
var barX = p.width - 15 - barWidth;
var barY = 35;
p.fill(100, 100, 100, 100);
p.rect(barX, barY, barWidth, barHeight, 2);
var palette = getSynthwaveColor(Math.floor(time / 60) % 6, time * 0.001);
p.fill(palette.r, palette.g, palette.b, 200);
p.rect(barX, barY, barWidth * pulse, barHeight, 2);
p.pop();
}
p.mousePressed = function() {
for (var i = 0; i < ropes.length; i++) {
var rope = ropes[i];
var segs = rope.segments;
var burstDist = 150;
for (var j = 1; j < segs.length; j++) {
var seg = segs[j];
var dx = seg.x - p.mouseX;
var dy = seg.y - p.mouseY;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < burstDist) {
var force = (burstDist - dist) / burstDist * 15;
var angle = p.atan2(dy, dx);
seg.x += p.cos(angle) * force;
seg.y += p.sin(angle) * force;
seg.oldX = seg.x - p.cos(angle) * force * 0.5;
seg.oldY = seg.y - p.sin(angle) * force * 0.5;
}
}
}
};
p.keyPressed = function() {
if (p.key === ' ') {
showGuide = !showGuide;
} else if (p.key === '1') {
mode = 0;
} else if (p.key === '2') {
mode = 1;
} else if (p.key === '3') {
mode = 2;
} else if (p.key === 'r' || p.key === 'R') {
initializeRopes();
}
};
p.windowResized = function() {
var container = document.getElementById('p5-wrapper');
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
bgBuffer.resizeCanvas(p.width, p.height);
glowBuffer.resizeCanvas(p.width, p.height);
curveBuffer.resizeCanvas(p.width, p.height);
initializeRopes();
};
}; // p5 init stripped
✨ AI 艺术解读
This piece transforms the mathematical beauty of starling murmurations into flowing fabric-like forms that pulse and breathe like living creatures. The ropes undulate between spiral, wave, and star configurations representing topological phase transitions in collective motion. The synthwave palette evokes nostalgic futurism while the smooth bezier ribbons create organic silk-like trails that dance across a gradient horizon. Each rope responds to mouse proximity creating ripples of influence through the flock.
📝 补充说明
- Verlet integration provides stable, fast physics without explicit velocity storage - positions alone determine motion
- Bezier ribbon rendering requires both edges tracked in synchronized vertex sequences for proper fill closure
- Multi-layer blend modes create depth impossible in single-pass rendering - screen mode adds luminosity to bright regions
- Pixel-level noise shimmer adds photographic quality with minimal performance impact at step size 3
- Mode switching morphs anchor point motion functions creating smooth topological transitions in flock behavior