Magnetic Field Voronoi Tension Boundaries
Generated by GridFlow AI | Tags: voronoi, magnetic-field, mineral, generative-art, tension-boundaries, metallic, additive-blending
💡 AI 提示词
Create a visually stunning p5.js artwork exploring magnetic field boundaries through Voronoi tessellation with metallic mineral aesthetics.🔧 核心算法要点
- Compute Voronoi diagram per-frame by calculating nearest seed point for each pixel using distance comparison
- Use multi-layer compositing with separate offscreen buffers for base, Voronoi cells, glow effects, and edge highlights
- Apply blendMode(ADD) for glow layer to create luminous tension boundaries between Voronoi cells
- Generate seed point movement using Perlin noise combined with mouse attraction/repulsion forces
- Color cells using a mineral palette of metallic gold, copper patina, oxidized green, and slate with tension-based modulation
- Draw organic curves using beginShape/curveVertex around high-tension seed points for silk ribbon effects
🎨 原始代码
var sketch = function(p) {
var canvas, container;
var voronoiBuffer, glowBuffer, baseBuffer, edgeBuffer;
var seedPoints = [];
var numSeeds = 48;
var time = 0;
var pixelStep = 4;
var gold, copper, oxidizedGreen, slate, deepSlate, patina;
var mouseAttract = false;
var mouseStrength = 0;
var keyMode = 0;
var lastKeyTime = 0;
p.setup = function() {
container = document.getElementById('p5-wrapper');
canvas = p.createCanvas(container.offsetWidth, container.offsetHeight).parent(container);
p.colorMode(p.RGB, 255, 255, 255, 255);
gold = p.color(212, 175, 55, 200);
copper = p.color(185, 111, 66, 180);
oxidizedGreen = p.color(82, 128, 96, 160);
slate = p.color(77, 87, 102, 255);
deepSlate = p.color(32, 38, 48, 255);
patina = p.color(95, 130, 120, 140);
voronoiBuffer = p.createGraphics(p.width, p.height);
glowBuffer = p.createGraphics(p.width, p.height);
baseBuffer = p.createGraphics(p.width, p.height);
edgeBuffer = p.createGraphics(p.width, p.height);
baseBuffer.colorMode(p.RGB, 255, 255, 255, 255);
glowBuffer.colorMode(p.RGB, 255, 255, 255, 255);
edgeBuffer.colorMode(p.RGB, 255, 255, 255, 255);
initSeeds();
p.noSmooth();
};
function initSeeds() {
seedPoints = [];
var colors = [gold, copper, oxidizedGreen, patina];
for (var i = 0; i < numSeeds; i++) {
var angle = (i / numSeeds) * p.TWO_PI;
var radius = p.min(p.width, p.height) * 0.35;
seedPoints.push({
x: p.width / 2 + p.cos(angle) * radius * p.random(0.5, 1.2),
y: p.height / 2 + p.sin(angle) * radius * p.random(0.5, 1.2),
vx: 0,
vy: 0,
col: colors[i % colors.length],
noiseOffset: p.random(1000),
tension: 0
});
}
}
function updateSeeds() {
var nx = p.mouseX / p.width;
var ny = p.mouseY / p.height;
for (var i = 0; i < seedPoints.length; i++) {
var s = seedPoints[i];
var n = p.noise(s.x * 0.003, s.y * 0.003, time * 0.4 + s.noiseOffset);
var angle = n * p.TWO_PI * 3;
s.vx += p.cos(angle) * 0.8;
s.vy += p.sin(angle) * 0.8;
var dx = p.mouseX - s.x;
var dy = p.mouseY - s.y;
var dist = p.sqrt(dx * dx + dy * dy);
if (mouseAttract && dist > 10) {
var force = mouseStrength * 800 / (dist * dist + 100);
s.vx += (dx / dist) * force;
s.vy += (dy / dist) * force;
}
if (keyMode === 1) {
var centerX = p.width / 2;
var centerY = p.height / 2;
var toCenterX = centerX - s.x;
var toCenterY = centerY - s.y;
s.vx += toCenterX * 0.001;
s.vy += toCenterY * 0.001;
}
s.vx *= 0.92;
s.vy *= 0.92;
s.x += s.vx;
s.y += s.vy;
if (s.x < -50) s.x = p.width + 50;
if (s.x > p.width + 50) s.x = -50;
if (s.y < -50) s.y = p.height + 50;
if (s.y > p.height + 50) s.y = -50;
var speed = p.sqrt(s.vx * s.vx + s.vy * s.vy);
s.tension = p.constrain(speed * 0.15, 0, 1);
}
}
function computeVoronoi() {
voronoiBuffer.background(deepSlate);
glowBuffer.background(0, 0, 0, 0);
edgeBuffer.background(0, 0, 0, 0);
var cellColors = [];
var edges = [];
for (var x = 0; x < p.width; x += pixelStep) {
for (var y = 0; y < p.height; y += pixelStep) {
var minDist = Infinity;
var secondDist = Infinity;
var closestIdx = 0;
for (var i = 0; i < seedPoints.length; i++) {
var dx = x - seedPoints[i].x;
var dy = y - seedPoints[i].y;
var d = dx * dx + dy * dy;
if (d < minDist) {
secondDist = minDist;
minDist = d;
closestIdx = i;
} else if (d < secondDist) {
secondDist = d;
}
}
var edgeThreshold = 400;
var tension = seedPoints[closestIdx].tension;
edgeThreshold *= (1 - tension * 0.5);
if (minDist < edgeThreshold) {
var col = seedPoints[closestIdx].col;
voronoiBuffer.noStroke();
var r = p.red(col) + tension * 30;
var g = p.green(col) + tension * 20;
var b = p.blue(col) - tension * 10;
var a = p.alpha(col);
if (keyMode === 2) {
var depth = 1 - (p.sqrt(minDist) / edgeThreshold);
r = p.lerp(p.red(deepSlate), r, depth);
g = p.lerp(p.green(deepSlate), g, depth);
b = p.lerp(p.blue(deepSlate), b, depth);
}
voronoiBuffer.fill(r, g, b, a);
voronoiBuffer.rect(x, y, pixelStep, pixelStep);
}
var edgeStrength = secondDist - minDist;
if (edgeStrength > 0 && edgeStrength < 600) {
var glowIntensity = (600 - edgeStrength) / 600;
glowIntensity *= glowIntensity;
var baseCol = seedPoints[closestIdx].col;
var glowR = p.red(baseCol) + 63;
var glowG = p.green(baseCol) + 55;
var glowB = p.blue(baseCol) + 30;
glowBuffer.noStroke();
glowBuffer.fill(glowR, glowG, glowB, glowIntensity * 120);
glowBuffer.rect(x, y, pixelStep, pixelStep);
if (edgeStrength < 200) {
edgeBuffer.stroke(gold);
edgeBuffer.strokeWeight(1.5);
edgeBuffer.point(x, y);
}
}
}
}
}
function drawBaseLayer() {
baseBuffer.background(deepSlate);
baseBuffer.noStroke();
for (var i = 0; i < 80; i++) {
var nx = p.noise(i * 0.1, time * 0.05);
var ny = p.noise(i * 0.1 + 500, time * 0.05);
var x = nx * p.width;
var y = ny * p.height;
var s = p.noise(i * 0.05, time * 0.08) * 200 + 50;
baseBuffer.fill(55, 65, 75, 15);
baseBuffer.ellipse(x, y, s, s * 0.6);
}
var noiseScale = 0.008;
for (var x = 0; x < p.width; x += 20) {
for (var y = 0; y < p.height; y += 20) {
var n = p.noise(x * noiseScale, y * noiseScale, time * 0.1);
var alpha = n * 20;
baseBuffer.fill(90, 100, 115, alpha);
baseBuffer.rect(x, y, 20, 20);
}
}
}
function drawTextureOverlay() {
p.noFill();
for (var i = 0; i < 12; i++) {
var n = p.noise(i * 0.3, time * 0.15);
var startX = n * p.width;
var startY = p.noise(i * 0.3 + 100, time * 0.15) * p.height;
p.stroke(212, 175, 55, 12);
p.strokeWeight(1);
p.beginShape();
for (var t = 0; t < 1; t += 0.02) {
var px = startX + p.noise(t * 3 + i, time * 0.2) * 100 - 50;
var py = startY + t * 80 - 40;
p.curveVertex(px, py);
if (t > 0) p.curveVertex(px, py);
}
p.endShape();
}
}
function drawOrganicForms() {
p.noFill();
for (var i = 0; i < seedPoints.length; i++) {
var s = seedPoints[i];
var tensionVal = s.tension;
if (tensionVal > 0.3) {
var r = p.red(gold) + tensionVal * 40;
var g = p.green(gold) + tensionVal * 30;
var b = p.blue(gold) + tensionVal * 20;
p.stroke(r, g, b, tensionVal * 150);
p.strokeWeight(2 + tensionVal * 3);
p.beginShape();
for (var a = 0; a < p.TWO_PI; a += 0.1) {
var n = p.noise(p.cos(a) * 2 + s.noiseOffset, p.sin(a) * 2 + s.noiseOffset, time);
var radius = 20 + n * 40 + tensionVal * 30;
var x = s.x + p.cos(a) * radius;
var y = s.y + p.sin(a) * radius;
p.curveVertex(x, y);
}
p.endShape(p.CLOSE);
}
}
}
function composite() {
p.image(baseBuffer, 0, 0);
p.blendMode(p.ADD);
p.image(glowBuffer, 0, 0);
p.blendMode(p.BLEND);
p.image(voronoiBuffer, 0, 0);
p.push();
p.blendMode(p.ADD);
p.image(edgeBuffer, 0, 0);
p.pop();
drawTextureOverlay();
drawOrganicForms();
p.noFill();
p.stroke(212, 175, 55, 30);
p.strokeWeight(1);
var borderInset = 15;
p.rect(borderInset, borderInset, p.width - borderInset * 2, p.height - borderInset * 2);
}
p.draw = function() {
updateSeeds();
computeVoronoi();
drawBaseLayer();
composite();
time += 0.012;
};
p.mousePressed = function() {
mouseAttract = true;
mouseStrength = 1;
for (var i = 0; i < seedPoints.length; i++) {
var s = seedPoints[i];
var dx = p.mouseX - s.x;
var dy = p.mouseY - s.y;
var dist = p.sqrt(dx * dx + dy * dy);
if (dist < 100) {
s.vx += (p.random() - 0.5) * 15;
s.vy += (p.random() - 0.5) * 15;
}
}
};
p.mouseReleased = function() {
mouseAttract = false;
mouseStrength = 0;
};
p.mouseDragged = function() {
mouseAttract = true;
mouseStrength = 0.5;
};
p.keyPressed = function() {
if (p.key === 'm' || p.key === 'M') {
keyMode = (keyMode + 1) % 3;
} else if (p.key === 'r' || p.key === 'R') {
initSeeds();
} else if (p.key === 'p' || p.key === 'P') {
p.save('magnetic-voronoi.png');
} else if (p.key === ' ') {
for (var i = 0; i < seedPoints.length; i++) {
seedPoints[i].vx += p.random(-5, 5);
seedPoints[i].vy += p.random(-5, 5);
}
}
};
p.windowResized = function() {
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
voronoiBuffer = p.createGraphics(p.width, p.height);
glowBuffer = p.createGraphics(p.width, p.height);
baseBuffer = p.createGraphics(p.width, p.height);
edgeBuffer = p.createGraphics(p.width, p.height);
baseBuffer.colorMode(p.RGB, 255, 255, 255, 255);
glowBuffer.colorMode(p.RGB, 255, 255, 255, 255);
edgeBuffer.colorMode(p.RGB, 255, 255, 255, 255);
};
};
✨ AI 艺术解读
This artwork visualizes the invisible tension at magnetic field boundaries through Voronoi cell tessellation. Metallic gold, copper, and oxidized green create a mineral specimen aesthetic—like viewing polished malachite or heated bronze under microscopy. The tension between adjacent cells manifests as glowing boundary lines where opposing forces meet, revealing hidden energy structures. Mouse interaction simulates magnetic attraction, pulling the field boundaries and creating visual stress patterns. The piece invites contemplation of invisible forces shaping our physical world, rendered in the aesthetic language of precious geological materials.
📝 补充说明
- Pixel step of 4 balances visual quality with performance for per-pixel Voronoi computation
- Use blendMode(ADD) carefully—excessive additive layers can wash out detail; threshold edge detection prevents this
- Tension values propagate from seed point velocity magnitude, creating natural visual escalation at high-energy areas
- Base layer uses low-alpha noise circles to add atmospheric depth without distracting from main Voronoi structure
- Organic curve drawing only triggers above 0.3 tension threshold, keeping visuals restrained and sophisticated rather than chaotic