Gyroid Crystal Formation
Generated by GridFlow AI | Tags: gyroid, crystal, voronoi, mineral, generative, glow, additive-blending, procedural
💡 AI 提示词
Gyroid Crystal Formation with mineral aesthetic - metallic gold, copper patina, oxidized green, slate colors. Per-pixel Voronoi computation with edge detection, multi-layer compositing using createGraphics buffers, ADD blend mode for edge glow, interactive mouse distortion, keyboard controls for palette switching and reset.🔧 核心算法要点
- Per-pixel Voronoi distance computation with step=3 for performance, determining closest and second-closest seed points for each pixel
- Edge detection via difference between distances to nearest neighbors, creating crystal boundary detection for glow effects
- Multi-layer offscreen compositing: background noise texture, Voronoi fill buffer, edge glow buffer with different blend modes
- ADD blend mode composite for luminous edge glow effect, layered over main Voronoi rendering
- Crystal struts drawn using bezier-like vertex sequences connecting nearby points with alpha-transparency
- Mouse proximity force field that repels Voronoi seeds, creating dynamic distortion ripples
- Procedural animation of seed points orbiting around original positions with sine-based radius modulation
🎨 原始代码
var sketch = function(p) {
var voronoiBuffer, glowBuffer, bgBuffer;
var points = [];
var time = 0;
var cellCount = 150;
var isMouseActive = false;
var lastMouseX = 0;
var lastMouseY = 0;
var showEdges = true;
var colorMode = 0;
var crystallization = 0;
var palette = {
gold: [212, 175, 55],
copper: [184, 115, 51],
oxidizedGreen: [74, 124, 89],
slate: [74, 85, 104],
deepSlate: [45, 55, 72],
brightGold: [255, 215, 0],
darkCopper: [139, 69, 19]
};
p.setup = function() {
var container = document.getElementById('p5-wrapper');
p.createCanvas(container.offsetWidth, container.offsetHeight).parent(container);
p.colorMode(p.RGB, 255, 255, 255, 255);
p.noStroke();
voronoiBuffer = p.createGraphics(p.width, p.height);
glowBuffer = p.createGraphics(p.width, p.height);
bgBuffer = p.createGraphics(p.width, p.height);
initPoints();
drawBackground();
};
function initPoints() {
points = [];
for (var i = 0; i < cellCount; i++) {
var angle = i * (p.TWO_PI / cellCount) * 3;
var radius = p.random(50, p.min(p.width, p.height) * 0.4);
var baseX = p.width / 2 + p.cos(angle) * radius;
var baseY = p.height / 2 + p.sin(angle) * radius;
points.push({
x: baseX + p.random(-30, 30),
y: baseY + p.random(-30, 30),
origX: baseX,
origY: baseY,
vx: p.random(-0.3, 0.3),
vy: p.random(-0.3, 0.3),
size: p.random(15, 45),
phase: p.random(p.TWO_PI),
layer: Math.floor(p.random(3))
});
}
}
function drawBackground() {
bgBuffer.background(palette.deepSlate[0], palette.deepSlate[1], palette.deepSlate[2]);
bgBuffer.loadPixels();
var step = 4;
for (var x = 0; x < bgBuffer.width; x += step) {
for (var y = 0; y < bgBuffer.height; y += step) {
var n = p.noise(x * 0.002, y * 0.002, time * 0.05);
var n2 = p.noise(x * 0.008, y * 0.008, time * 0.1);
var idx = (y * bgBuffer.width + x) * 4;
var baseVal = n * 40;
var detail = n2 * 20;
bgBuffer.pixels[idx] = palette.slate[0] + baseVal;
bgBuffer.pixels[idx + 1] = palette.slate[1] + baseVal;
bgBuffer.pixels[idx + 2] = palette.slate[2] + baseVal + detail;
bgBuffer.pixels[idx + 3] = 255;
}
}
bgBuffer.updatePixels();
}
function renderVoronoi() {
voronoiBuffer.background(0, 0, 0, 0);
glowBuffer.background(0, 0, 0, 0);
var step = 3;
var imgData = voronoiBuffer.pixels;
var glowData = glowBuffer.pixels;
voronoiBuffer.loadPixels();
glowBuffer.loadPixels();
for (var x = 0; x < p.width; x += step) {
for (var y = 0; y < p.height; y += step) {
var closest = null;
var secondClosest = null;
var minDist = Infinity;
var secondMinDist = Infinity;
for (var j = 0; j < points.length; j++) {
var px = points[j].x;
var py = points[j].y;
var d = (x - px) * (x - px) + (y - py) * (y - py);
if (d < minDist) {
secondMinDist = minDist;
secondClosest = closest;
minDist = d;
closest = j;
} else if (d < secondMinDist) {
secondMinDist = d;
secondClosest = j;
}
}
var edgeDist = Math.sqrt(secondMinDist) - Math.sqrt(minDist);
var edgeFactor = p.map(edgeDist, 0, 25, 0, 1);
edgeFactor = p.constrain(edgeFactor, 0, 1);
var noiseVal = p.noise(x * 0.004 + points[closest].phase, y * 0.004, time * 0.1);
var heightFactor = p.map(y, 0, p.height, 0, 1);
var cellCol, r, g, b;
switch(colorMode) {
case 0:
var t = (noiseVal + points[closest].layer * 0.3) % 1;
if (t < 0.4) {
cellCol = palette.gold;
} else if (t < 0.7) {
cellCol = palette.copper;
} else {
cellCol = palette.oxidizedGreen;
}
break;
case 1:
cellCol = palette.copper;
if (heightFactor > 0.6) {
cellCol = palette.brightGold;
}
break;
case 2:
cellCol = palette.oxidizedGreen;
if (noiseVal > 0.6) {
cellCol = palette.gold;
}
break;
default:
cellCol = palette.slate;
}
var fillIntensity = p.map(edgeFactor, 0, 1, 1, 0.3);
fillIntensity *= (0.5 + noiseVal * 0.5);
fillIntensity *= crystallization;
r = cellCol[0] * fillIntensity;
g = cellCol[1] * fillIntensity;
b = cellCol[2] * fillIntensity;
for (var dx = 0; dx < step && x + dx < p.width; dx++) {
for (var dy = 0; dy < step && y + dy < p.height; dy++) {
var pixelIdx = ((y + dy) * voronoiBuffer.width + (x + dx)) * 4;
imgData[pixelIdx] = r;
imgData[pixelIdx + 1] = g;
imgData[pixelIdx + 2] = b;
imgData[pixelIdx + 3] = 220;
}
}
var glowIntensity = Math.pow(1 - edgeFactor, 3);
if (showEdges && edgeFactor > 0.85) {
glowIntensity *= 2;
}
glowIntensity *= crystallization;
var gr = Math.min(255, cellCol[0] * glowIntensity * 1.2);
var gg = Math.min(255, cellCol[1] * glowIntensity * 0.8);
var gb = cellCol[2] * glowIntensity * 0.4;
for (var dx = 0; dx < step && x + dx < p.width; dx++) {
for (var dy = 0; dy < step && y + dy < p.height; dy++) {
var pixelIdx = ((y + dy) * glowBuffer.width + (x + dx)) * 4;
glowData[pixelIdx] = gr;
glowData[pixelIdx + 1] = gg;
glowData[pixelIdx + 2] = gb;
glowData[pixelIdx + 3] = glowIntensity * 180;
}
}
}
}
voronoiBuffer.updatePixels();
glowBuffer.updatePixels();
}
function drawCrystalStruts() {
voronoiBuffer.strokeWeight(1.5);
for (var i = 0; i < points.length; i++) {
var alpha = p.map(points[i].layer, 0, 2, 40, 80);
for (var j = i + 1; j < points.length; j++) {
var d = p.dist(points[i].x, points[i].y, points[j].x, points[j].y);
if (d < 120 && d > 30) {
var midX = (points[i].x + points[j].x) / 2;
var midY = (points[i].y + points[j].y) / 2;
var offsetX = p.noise(midX * 0.02, midY * 0.02, time * 0.3) * 40 - 20;
var offsetY = p.noise(midY * 0.02, midX * 0.02, time * 0.3 + 100) * 40 - 20;
var curveAlpha = alpha * (1 - d / 120) * crystallization;
voronoiBuffer.stroke(palette.brightGold[0], palette.brightGold[1], palette.brightGold[2], curveAlpha);
voronoiBuffer.noFill();
voronoiBuffer.beginShape();
voronoiBuffer.vertex(points[i].x, points[i].y);
voronoiBuffer.vertex(midX + offsetX, midY + offsetY);
voronoiBuffer.vertex(points[j].x, points[j].y);
voronoiBuffer.endShape();
}
}
}
}
function drawEdgeHighlights() {
if (!showEdges) return;
var edgePoints = [];
var step = 4;
for (var x = 0; x < p.width; x += step) {
for (var y = 0; y < p.height; y += step) {
var closest = null;
var secondClosest = null;
var minDist = Infinity;
var secondMinDist = Infinity;
for (var j = 0; j < points.length; j++) {
var d = p.dist(x, y, points[j].x, points[j].y);
if (d < minDist) {
secondMinDist = minDist;
secondClosest = closest;
minDist = d;
closest = j;
} else if (d < secondMinDist) {
secondMinDist = d;
secondClosest = j;
}
}
var edgeDist = secondMinDist - minDist;
if (edgeDist < 8 && edgeDist > 2) {
edgePoints.push({x: x, y: y, edge: edgeDist, pointIdx: closest});
}
}
}
voronoiBuffer.strokeWeight(0.8);
for (var i = 0; i < edgePoints.length; i++) {
var e = edgePoints[i];
var n = p.noise(e.x * 0.01, e.y * 0.01, time * 0.2);
var h = p.map(n, 0, 1, 0, 60);
var col;
if (h < 20) {
col = palette.gold;
} else if (h < 40) {
col = palette.copper;
} else {
col = palette.oxidizedGreen;
}
var edgeAlpha = p.map(e.edge, 2, 8, 150, 50) * crystallization;
voronoiBuffer.stroke(col[0], col[1], col[2], edgeAlpha);
voronoiBuffer.point(e.x, e.y);
}
}
p.draw = function() {
time += 0.008;
crystallization = p.lerp(crystallization, 1, 0.02);
for (var i = 0; i < points.length; i++) {
var angle = time * 0.3 + points[i].phase;
var radius = 15 + p.sin(time * 0.5 + i) * 8;
var targetX = points[i].origX + p.cos(angle) * radius;
var targetY = points[i].origY + p.sin(angle) * radius;
points[i].x += (targetX - points[i].x) * 0.03;
points[i].y += (targetY - points[i].y) * 0.03;
if (isMouseActive) {
var dx = points[i].x - p.mouseX;
var dy = points[i].y - p.mouseY;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 150 && dist > 0) {
var force = (150 - dist) / 150 * 2;
points[i].x += (dx / dist) * force;
points[i].y += (dy / dist) * force;
}
}
}
renderVoronoi();
drawCrystalStruts();
drawEdgeHighlights();
p.background(palette.deepSlate);
p.image(bgBuffer, 0, 0);
p.blendMode(p.ADD);
p.image(glowBuffer, 0, 0);
p.blendMode(p.BLEND);
p.image(voronoiBuffer, 0, 0);
if (isMouseActive) {
drawInteractionGlow();
}
};
function drawInteractionGlow() {
var gr = p.createGraphics(p.width, p.height);
gr.noStroke();
var grad = gr.drawingContext.createRadialGradient(
p.mouseX, p.mouseY, 0,
p.mouseX, p.mouseY, 150
);
grad.addColorStop(0, 'rgba(255, 215, 0, 0.4)');
grad.addColorStop(0.5, 'rgba(184, 115, 51, 0.2)');
grad.addColorStop(1, 'rgba(74, 124, 89, 0)');
gr.drawingContext.fillStyle = grad;
gr.drawingContext.fillRect(0, 0, gr.width, gr.height);
p.blendMode(p.ADD);
p.image(gr, 0, 0);
p.blendMode(p.BLEND);
}
p.mousePressed = function() {
crystallization = 0.3;
for (var i = 0; i < points.length; i++) {
var d = p.dist(points[i].x, points[i].y, p.mouseX, p.mouseY);
if (d < 200) {
var angle = p.random(p.TWO_PI);
var force = (200 - d) / 200 * 15;
points[i].origX += p.cos(angle) * force;
points[i].origY += p.sin(angle) * force;
}
}
};
p.mouseMoved = function() {
isMouseActive = true;
lastMouseX = p.mouseX;
lastMouseY = p.mouseY;
};
p.mouseReleased = function() {
isMouseActive = false;
};
p.keyPressed = function() {
if (p.key === 'g' || p.key === 'G') {
colorMode = (colorMode + 1) % 4;
}
if (p.key === 'r' || p.key === 'R') {
initPoints();
crystallization = 0;
}
if (p.key === 'e' || p.key === 'E') {
showEdges = !showEdges;
}
if (p.key === ' ') {
drawBackground();
}
};
p.windowResized = function() {
var container = document.getElementById('p5-wrapper');
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
voronoiBuffer = p.createGraphics(p.width, p.height);
glowBuffer = p.createGraphics(p.width, p.height);
bgBuffer = p.createGraphics(p.width, p.height);
drawBackground();
initPoints();
};
};
✨ AI 艺术解读
This piece captures the mathematical beauty of Voronoi tessellation as a crystalline growth structure, evoking the natural formation of mineral specimens like gold ore deposits or oxidized copper geodes. The layered glow effects and curved struts suggest the underlying lattice structure that gives gyroid surfaces their incredible complexity. Interactive forces let the viewer sculpt the formation in real-time, feeling like they're manipulating molecular bonds within a growing crystal matrix.
📝 补充说明
- Step size of 3 pixels balances visual quality with performance for the per-pixel Voronoi computation
- Crystallization variable creates smooth fade-in effect when points are reinitialized or clicked
- Glow buffer uses pure ADD blend mode for maximum luminosity on crystal edges
- Three distinct color modes create fundamentally different mineral appearances: gold-copper, copper patina, oxidized green, and slate
- Crystal struts only connect points between 30-120 pixels apart to create geometric but organic structures