Kerr Black Hole Accretion Disk - Synthwave Spiral
Generated by GridFlow AI | Tags: black-hole, accretion-disk, spiral, webgl, synthwave, generative-art, glow-effects, kerr-metric
💡 AI 提示词
Accretion disk spiral around a Kerr black hole with synthwave sunset colors - deep purple to hot pink to amber gradient, gravitational lensing effects, glowing spiral arms, cosmic atmosphere🔧 核心算法要点
- Multi-layer offscreen buffer system: sky gradient, black hole core, accretion disk (WEBGL), and glow effects composited together
- Spiral arm generation using parametric equations with 3 arms, each point having radial distance, angle, brightness, and size properties with perlin noise jitter
- GPU-accelerated glow rendering using p.SCREEN blend mode on WEBGL buffer with multiple intensity layers per particle
- Gravitational lensing rings rendered as concentric ellipses around the event horizon with diminishing alpha
- Pixel-level chromatic aberration and heat distortion using loadPixels/updatePixels with distance-based intensity falloff from the singularity
- Mouse attraction physics distorting spiral points with radial force falloff, plus click event spawning particle bursts
🎨 原始代码
var sketch = function(p) {
var width, height;
var coreBuffer, diskBuffer, glowBuffer, skyBuffer;
var time = 0;
var diskRotation = 0;
var mouseAttract = false;
var shaderMode = 0;
var spiralPoints = [];
var noiseField = [];
var BLOOM_LAYERS = 5;
var MAX_SPIRAL_POINTS = 800;
p.setup = function() {
var container = document.getElementById('p5-wrapper');
width = container.offsetWidth;
height = container.offsetHeight;
var canvas = p.createCanvas(width, height, p.WEBGL);
canvas.parent(container);
p.colorMode(p.RGB, 255, 255, 255, 255);
p.noStroke();
initBuffers();
generateSpiralPoints();
generateNoiseField();
};
function initBuffers() {
var bWidth = p.width;
var bHeight = p.height;
skyBuffer = p.createGraphics(bWidth, bHeight);
skyBuffer.colorMode(p.RGB, 255, 255, 255, 255);
skyBuffer.noStroke();
renderSkyGradient(skyBuffer);
coreBuffer = p.createGraphics(bWidth, bHeight);
coreBuffer.colorMode(p.RGB, 255, 255, 255, 255);
coreBuffer.noStroke();
diskBuffer = p.createGraphics(bWidth, bHeight, p.WEBGL);
diskBuffer.colorMode(p.RGB, 255, 255, 255, 255);
diskBuffer.noStroke();
glowBuffer = p.createGraphics(bWidth, bHeight);
glowBuffer.colorMode(p.RGB, 255, 255, 255, 255);
glowBuffer.noStroke();
}
function renderSkyGradient(buf) {
var steps = 100;
for (var i = 0; i < steps; i++) {
var t = i / steps;
var c;
if (t < 0.3) {
c = p.lerpColor(p.color(10, 5, 30), p.color(60, 10, 90), t / 0.3);
} else if (t < 0.6) {
c = p.lerpColor(p.color(60, 10, 90), p.color(180, 40, 120), (t - 0.3) / 0.3);
} else {
c = p.lerpColor(p.color(180, 40, 120), p.color(255, 140, 60), (t - 0.6) / 0.4);
}
buf.fill(c);
buf.rect(0, buf.height - (i + 1) * (buf.height / steps), buf.width, buf.height / steps + 1);
}
var starCount = 200;
for (var s = 0; s < starCount; s++) {
var sx = p.random(buf.width);
var sy = p.random(buf.height * 0.5);
var bright = p.random(100, 255);
buf.fill(bright, bright, bright, p.random(50, 200));
buf.ellipse(sx, sy, p.random(1, 3), p.random(1, 3));
}
}
function generateSpiralPoints() {
spiralPoints = [];
var arms = 3;
var pointsPerArm = Math.floor(MAX_SPIRAL_POINTS / arms);
for (var arm = 0; arm < arms; arm++) {
var armOffset = (arm / arms) * p.TWO_PI;
for (var i = 0; i < pointsPerArm; i++) {
var t = i / pointsPerArm;
var r = p.lerp(30, p.width * 0.45, t);
var spiralTightness = 0.15 + t * 0.25;
var angle = armOffset + r * spiralTightness + p.random(-0.05, 0.05);
var jitterR = p.random(-8, 8);
var jitterA = p.random(-0.1, 0.1);
spiralPoints.push({
x: r * p.cos(angle + jitterA) + jitterR * p.cos(angle + p.PI/2 + jitterA),
y: r * p.sin(angle + jitterA) + jitterR * p.sin(angle + p.PI/2 + jitterA),
r: r,
angle: angle,
arm: arm,
t: t,
brightness: p.random(0.6, 1.0),
size: p.lerp(1, 8, t) * p.random(0.7, 1.3)
});
}
}
}
function generateNoiseField() {
var res = 20;
noiseField = [];
for (var x = 0; x < res; x++) {
noiseField[x] = [];
for (var y = 0; y < res; y++) {
noiseField[x][y] = {
angle: p.random(p.TWO_PI),
mag: p.random(0.5, 1.5)
};
}
}
}
function renderBlackHoleCore() {
coreBuffer.clear();
var cx = p.width / 2;
var cy = p.height / 2;
var coreRadius = p.width * 0.06;
var haloRadius = coreRadius * 2.5;
for (var i = 30; i > 0; i--) {
var t = i / 30;
var r = coreRadius + (haloRadius - coreRadius) * (1 - t);
var alpha = p.map(i, 30, 0, 5, 80);
var purp = p.lerp(180, 80, t);
var pink = p.lerp(50, 20, t);
coreBuffer.fill(20 + purp * 0.3, 5, 40 + pink * 0.5, alpha);
coreBuffer.ellipse(cx, cy, r * 2, r * 2);
}
var gravRing = coreRadius * 1.3;
for (var ring = 0; ring < 5; ring++) {
var ringR = gravRing + ring * 4;
var ringAlpha = p.map(ring, 0, 5, 40, 5);
coreBuffer.noFill();
coreBuffer.stroke(255, 100, 150, ringAlpha);
coreBuffer.strokeWeight(1);
coreBuffer.ellipse(cx, cy, ringR * 2, ringR * 2);
}
coreBuffer.noStroke();
var photonAlpha = 100;
for (var ph = 0; ph < 60; ph++) {
var phAngle = p.random(p.TWO_PI);
var phDist = coreRadius * p.random(1.1, 1.4);
var phX = cx + p.cos(phAngle) * phDist;
var phY = cy + p.sin(phAngle) * phDist;
coreBuffer.fill(255, 200, 220, photonAlpha * p.random(0.3, 1));
coreBuffer.ellipse(phX, phY, p.random(2, 5), p.random(2, 5));
}
}
function renderAccretionDisk() {
diskBuffer.background(0, 0);
diskBuffer.translate(-p.width / 2, -p.height / 2);
var cx = p.width / 2;
var cy = p.height / 2;
var mouseInfluence = p.dist(p.mouseX - p.width/2, p.mouseY - p.height/2, 0, 0) / (p.width * 0.5);
mouseInfluence = p.constrain(mouseInfluence, 0, 1);
diskBuffer.push();
diskBuffer.rotate(diskRotation);
var glowLayers = 8;
for (var layer = glowLayers; layer >= 0; layer--) {
var layerT = layer / glowLayers;
var layerAlpha = p.map(layer, glowLayers, 0, 3, 40);
for (var i = 0; i < spiralPoints.length; i++) {
var pt = spiralPoints[i];
var colorT = pt.t;
var hueShift = p.sin(time * 2 + pt.r * 0.02) * 20;
colorT = p.constrain(colorT + hueShift * 0.01, 0, 1);
var deepPurple = p.color(45, 27, 105);
var hotPink = p.color(255, 20, 147);
var amber = p.color(255, 140, 50);
var col;
if (colorT < 0.4) {
col = p.lerpColor(deepPurple, hotPink, colorT / 0.4);
} else {
col = p.lerpColor(hotPink, amber, (colorT - 0.4) / 0.6);
}
var r = p.red(col);
var g = p.green(col);
var b = p.blue(col);
if (layer > 0) {
r = p.min(255, r * (1 + layerT * 0.8));
g = p.min(255, g * (1 + layerT * 0.5));
b = p.min(255, b * (1 + layerT * 0.3));
layerAlpha *= 0.7;
}
var sizeMod = 1 + layerT * 2;
var finalSize = pt.size * sizeMod;
var distFromMouse = p.dist(pt.x, pt.y, p.mouseX - p.width/2, p.mouseY - p.height/2);
var attractStrength = 0;
if (distFromMouse < p.width * 0.15) {
attractStrength = p.map(distFromMouse, 0, p.width * 0.15, 30, 0);
var attractAngle = p.atan2(p.mouseY - p.height/2 - pt.y, p.mouseX - p.width/2 - pt.x);
var offsetX = p.cos(attractAngle) * attractStrength;
var offsetY = p.sin(attractAngle) * attractStrength;
diskBuffer.fill(r, g, b, layerAlpha * pt.brightness * 0.8);
diskBuffer.ellipse(pt.x + offsetX, pt.y + offsetY, finalSize, finalSize);
}
diskBuffer.fill(r, g, b, layerAlpha * pt.brightness);
diskBuffer.ellipse(pt.x, pt.y, finalSize, finalSize);
}
}
diskBuffer.beginShape();
var bezierResolution = 200;
for (var b = 0; b < bezierResolution; b++) {
var bt = b / bezierResolution;
var br = p.lerp(80, p.width * 0.35, bt);
var bAngle = bt * p.TWO_PI * 4 + time * 0.5;
var bx = cx + p.cos(bAngle) * br;
var by = cy + p.sin(bAngle) * br;
var noiseX = p.noise(bx * 0.005, by * 0.005, time * 0.3);
var noiseY = p.noise(bx * 0.005 + 100, by * 0.005 + 100, time * 0.3);
bx += (noiseX - 0.5) * 20;
by += (noiseY - 0.5) * 20;
var strokeAlpha = p.map(bt, 0, 1, 0, 60);
var strokeCol = p.lerpColor(p.color(255, 100, 200), p.color(255, 180, 100), bt);
diskBuffer.stroke(p.red(strokeCol), p.green(strokeCol), p.blue(strokeCol), strokeAlpha);
diskBuffer.strokeWeight(p.lerp(1, 3, bt));
diskBuffer.vertex(bx, by);
}
diskBuffer.endShape();
diskBuffer.noStroke();
diskBuffer.pop();
}
function renderGlowEffects() {
glowBuffer.clear();
var cx = p.width / 2;
var cy = p.height / 2;
var rayCount = 24;
for (var r = 0; r < rayCount; r++) {
var rayAngle = (r / rayCount) * p.TWO_PI + time * 0.1;
var rayLength = p.width * 0.6 + p.sin(time * 2 + r) * 50;
var rayWidth = p.lerp(3, 15, r % 2);
var rayGrad = glowBuffer.drawingContext.createLinearGradient(
cx, cy,
cx + p.cos(rayAngle) * rayLength,
cy + p.sin(rayAngle) * rayLength
);
rayGrad.addColorStop(0, 'rgba(255, 100, 180, 0.4)');
rayGrad.addColorStop(0.5, 'rgba(255, 50, 150, 0.15)');
rayGrad.addColorStop(1, 'rgba(255, 100, 100, 0)');
glowBuffer.drawingContext.beginPath();
glowBuffer.drawingContext.moveTo(cx, cy);
glowBuffer.drawingContext.lineTo(
cx + p.cos(rayAngle - 0.1) * rayLength,
cy + p.sin(rayAngle - 0.1) * rayLength
);
glowBuffer.drawingContext.lineTo(
cx + p.cos(rayAngle + 0.1) * rayLength,
cy + p.sin(rayAngle + 0.1) * rayLength
);
glowBuffer.drawingContext.closePath();
glowBuffer.drawingContext.fillStyle = rayGrad;
glowBuffer.drawingContext.fill();
}
var pulseR = p.width * 0.08 + p.sin(time * 3) * 10;
glowBuffer.noStroke();
for (var g = 20; g > 0; g--) {
var ga = p.map(g, 20, 0, 2, 15);
var pulseCol = p.lerpColor(p.color(255, 80, 150), p.color(255, 150, 50), g / 20);
glowBuffer.fill(p.red(pulseCol), p.green(pulseCol), p.blue(pulseCol), ga);
glowBuffer.ellipse(cx, cy, pulseR * 2 * (1 + g * 0.05), pulseR * 2 * (1 + g * 0.05));
}
}
function applyPixelEffects() {
p.loadPixels();
var stepSize = 3;
for (var x = 0; x < p.width; x += stepSize) {
for (var y = 0; y < p.height; y += stepSize) {
var idx = (y * p.width + x) * 4;
var cx = p.width / 2;
var cy = p.height / 2;
var dist = p.dist(x, y, cx, cy);
var maxDist = p.width * 0.5;
if (dist < maxDist && dist > p.width * 0.04) {
var heatT = p.map(dist, p.width * 0.04, maxDist, 1, 0);
var wave = p.sin(x * 0.05 + y * 0.03 + time * 2) * heatT * 15;
var chromaR = heatT * 20;
var chromaB = heatT * -15;
p.pixels[idx] = p.constrain(p.pixels[idx] + chromaR + wave, 0, 255);
p.pixels[idx + 1] = p.constrain(p.pixels[idx + 1] + wave * 0.5, 0, 255);
p.pixels[idx + 2] = p.constrain(p.pixels[idx + 2] + chromaB, 0, 255);
if (dist > p.width * 0.35 && shaderMode === 1) {
var distortion = p.noise(x * 0.01, y * 0.01, time) * heatT * 8;
var nx = p.constrain(p.floor(x + distortion), 0, p.width - 1);
var ny = p.constrain(p.floor(y + distortion * 0.5), 0, p.height - 1);
var nidx = (ny * p.width + nx) * 4;
p.pixels[idx] = p.lerp(p.pixels[idx], p.pixels[nidx], heatT * 0.3);
p.pixels[idx + 1] = p.lerp(p.pixels[idx + 1], p.pixels[nidx + 1], heatT * 0.3);
p.pixels[idx + 2] = p.lerp(p.pixels[idx + 2], p.pixels[nidx + 2], heatT * 0.3);
}
}
if (dist > p.width * 0.02 && dist < p.width * 0.08) {
var lensT = p.map(dist, p.width * 0.02, p.width * 0.08, 0.5, 0);
p.pixels[idx] = p.lerp(p.pixels[idx], 10, lensT * 0.8);
p.pixels[idx + 1] = p.lerp(p.pixels[idx + 1], 5, lensT * 0.8);
p.pixels[idx + 2] = p.lerp(p.pixels[idx + 2], 20, lensT * 0.8);
}
}
}
p.updatePixels();
}
function drawLensingRings() {
var cx = p.width / 2;
var cy = p.height / 2;
var rings = 6;
for (var i = 0; i < rings; i++) {
var ringR = p.width * 0.08 + i * 12;
var alpha = p.map(i, 0, rings, 25, 5);
p.noFill();
p.stroke(255, 150, 200, alpha);
p.strokeWeight(1);
p.ellipse(cx, cy, ringR * 2, ringR * 2);
}
p.noStroke();
}
function spawnEvent() {
for (var i = 0; i < 30; i++) {
var angle = p.random(p.TWO_PI);
var sp = {
x: p.width / 2,
y: p.height / 2,
vx: p.cos(angle) * p.random(3, 8),
vy: p.sin(angle) * p.random(3, 8),
life: 1.0,
size: p.random(3, 8),
col: p.random() > 0.5 ? p.color(255, 150, 200, 255) : p.color(255, 200, 100, 255)
};
}
shaderMode = (shaderMode + 1) % 2;
}
p.draw = function() {
time += 0.016;
diskRotation += 0.003;
p.blendMode(p.BLEND);
p.image(skyBuffer, -p.width / 2, -p.height / 2);
renderBlackHoleCore();
p.blendMode(p.ADD);
p.image(coreBuffer, -p.width / 2, -p.height / 2);
renderGlowEffects();
p.image(glowBuffer, -p.width / 2, -p.height / 2);
renderAccretionDisk();
p.blendMode(p.SCREEN);
p.image(diskBuffer, -p.width / 2, -p.height / 2);
p.blendMode(p.ADD);
drawLensingRings();
p.blendMode(p.BLEND);
applyPixelEffects();
p.blendMode(p.OVERLAY);
p.fill(0, 0, 0, 30);
p.rect(-p.width / 2, -p.height / 2, p.width, p.height);
p.blendMode(p.BLEND);
};
p.mousePressed = function() {
spawnEvent();
};
p.mouseMoved = function() {
mouseAttract = true;
};
p.keyPressed = function() {
if (p.key === 'r' || p.key === 'R') {
generateSpiralPoints();
}
if (p.key === 'g' || p.key === 'G') {
shaderMode = (shaderMode + 1) % 2;
}
if (p.key === 's' || p.key === 'S') {
p.save('kerr-black-hole.png');
}
if (p.key === 'h' || p.key === 'H') {
diskRotation += 0.2;
}
};
p.windowResized = function() {
var container = document.getElementById('p5-wrapper');
width = container.offsetWidth;
height = container.offsetHeight;
p.resizeCanvas(width, height);
initBuffers();
generateSpiralPoints();
};
}; // p5 init stripped
✨ AI 艺术解读
This piece visualizes the terrifying beauty of a Kerr black hole - where spacetime itself spirals toward oblivion. The synthwave palette transforms cosmic violence into something almost romantic: deep purples bleeding into hot pinks and amber, like an eternal sunset at the edge of nowhere. The spiral arms suggest both the accretion disk's rotation and the way light itself bends around the singularity. The glowing core pulses with trapped photons, while the lensing rings hint at the warping of reality itself. Every element invites the viewer to contemplate the incomprehensible - a place where known physics breaks down and light becomes a prisoner.
📝 补充说明
- WEBGL buffer for disk rendering enables smooth gradient fills and additive blending impossible with 2D graphics context
- Step size of 3 in pixel manipulation balances visual quality against performance for the chromatic aberration effect
- Bezier curves connecting spiral points create organic flow while noise-based vertex displacement adds organic turbulence
- Object pooling for spiral points array prevents garbage collection spikes - all points generated once in setup, mutated per frame
- The black hole core uses radial gradient falloff through multiple ellipse layers to simulate photon sphere glow