安装方式
命令行安装
在项目根目录执行以下命令,完成 Skill 安装。
npx bzskills add anthropics/skills --skill algorithmic-art 使用 p5.js 创建具有种子随机性和交互式参数探索的算法艺术。当用户请求使用代码创作艺术、生成艺术、算法艺术、流场或粒子系统时,使用此方法。创作原创算法艺术,而非复制现有艺术家作品,以避免版权侵权。
38.9k
下载量
命令行安装
在项目根目录执行以下命令,完成 Skill 安装。
npx bzskills add anthropics/skills --skill algorithmic-art name: algorithmic-art
description: 使用 p5.js 创建具有种子随机性和交互式参数探索的算法艺术。当用户请求使用代码创作艺术、生成艺术、算法艺术、流场或粒子系统时,使用此方法。创作原创算法艺术,而非复制现有艺术家作品,以避免版权侵权。
license: Complete terms in LICENSE.txt# Void Symmetries
This movement emerges from the tension between nothingness and pattern—a meditation on how perfect symmetry can arise from chaotic seeds. The algorithm does not impose form; it cultivates it, allowing order to crystallize from the void through deterministic processes guided by mathematical constraints.
At its core, Void Symmetries relies on **noise-driven vector fields** where every point in space carries a directed force. Particles are born at random positions and follow these forces, their paths accumulating into delicate traceries. The field itself is constructed from layered Perlin noise octaves, each scaled to reveal structure at different resolutions. A masterful twist: the field is mirrored across multiple axes, creating repeating kaleidoscopic symmetries that feel both organic and architectural. The symmetry order becomes a parametric key—turning a chaotic flow into a sacred geometry.
The **conceptual seed** is a quiet tribute to phyllotaxis—the spiral arrangement of leaves dictated by the golden angle. Invisible within the code, the golden ratio (φ) governs the relationship between noise scales and the angular velocity of particles. The algorithm does not announce this; it simply hums with the same mathematical resonance found in sunflowers and pinecones. Only those who look for the Fibonacci sequence in the spiral densities will sense the homage. To everyone else, it remains a hypnotic dance of lines.
**Craftsmanship is paramount.** Every parameter has been tuned through countless iterations by a computational artist at the absolute top of their field. The particle lifetime, the noise scale ratios, the feedback between symmetry and randomness—all are the product of deep expertise. The algorithm does not rely on brute force; it is a delicate instrument where slight changes in seed produce wholly unique yet equally beautiful compositions. The result feels inevitable, as if the void itself chose to speak in patterns.
The beauty lives in the **process**, not the final frame. Each run is a performance: particles born, flowing, dying, their trails fading and coalescing. The system evolves until a state of visual equilibrium is reached, then freezes—a single breath held eternally. The viewer is invited to change the seed, adjust the symmetry, or alter the color palette, and witness how the same mathematical soul manifests in infinite variations. This is generative art as a living philosophy.
**Void Symmetries** is a manifesto for algorithmic expression: that from nothingness, through rigorously tuned computational processes, can emerge beauty that feels both ancient and unrepeatable. The algorithm is the brush, the seed is the wind, and the canvas is the void—willing to become anything.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void Symmetries — Generative Art</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Lora:ital@0;1&display=swap" rel="stylesheet">
<style>
/* ---------- RESET & BASE ---------- */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #fdf6f0 0%, #f3e9e0 100%);
min-height: 100vh;
display: flex;
}
/* ---------- LAYOUT ---------- */
#app {
display: flex;
width: 100%;
}
/* ---------- SIDEBAR ---------- */
#sidebar {
width: 340px;
min-width: 340px;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(8px);
border-right: 1px solid rgba(0,0,0,0.06);
padding: 24px 20px;
overflow-y: auto;
height: 100vh;
}
#sidebar h1 {
font-size: 20px;
font-weight: 600;
color: #3a3a3a;
margin-bottom: 2px;
}
#sidebar .subtitle {
font-family: 'Lora', serif;
font-style: italic;
color: #8a7a6f;
font-size: 14px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 12px;
}
.sidebar-section {
margin-bottom: 24px;
}
.sidebar-section h2 {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #7a6a5f;
margin-bottom: 12px;
}
/* ---------- SEED CONTROLS ---------- */
.seed-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.seed-row .seed-value {
font-weight: 600;
font-size: 18px;
color: #3a3a3a;
min-width: 80px;
text-align: center;
}
.btn {
background: #e8ddd5;
border: none;
padding: 6px 14px;
border-radius: 6px;
font-family: 'Poppins', sans-serif;
font-size: 13px;
font-weight: 500;
color: #3a3a3a;
cursor: pointer;
transition: background 0.15s;
}
.btn:hover { background: #d7c8bd; }
.btn-primary {
background: #8b7a6e;
color: white;
}
.btn-primary:hover { background: #6e5f54; }
.seed-row input[type="number"] {
width: 70px;
padding: 4px 6px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: 'Poppins', sans-serif;
font-size: 13px;
text-align: center;
}
/* ---------- PARAMETER CONTROLS ---------- */
.control-group {
margin-bottom: 14px;
}
.control-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #4a3a2f;
margin-bottom: 4px;
}
.control-group input[type="range"] {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #ddd;
border-radius: 3px;
outline: none;
}
.control-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #8b7a6e;
border-radius: 50%;
cursor: pointer;
}
.control-group .value-display {
font-size: 12px;
color: #8a7a6f;
margin-left: 6px;
}
.control-group input[type="color"] {
width: 40px;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
background: none;
padding: 0;
}
/* ---------- ACTIONS ---------- */
.actions-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.actions-row .btn {
flex: 1;
min-width: 90px;
text-align: center;
}
/* ---------- CANVAS AREA ---------- */
#canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
#canvas-container canvas {
max-width: 100%;
max-height: calc(100vh - 40px);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.08);
}
/* ---------- RESPONSIVE ---------- */
@media (max-width: 800px) {
#app { flex-direction: column; }
#sidebar {
width: 100%;
min-width: unset;
height: auto;
max-height: 50vh;
border-right: none;
border-bottom: 1px solid rgba(0,0,0,0.06);
}
#canvas-container { padding: 10px; }
}
</style>
</head>
<body>
<div id="app">
<!-- SIDEBAR -->
<div id="sidebar">
<h1>Void Symmetries</h1>
<div class="subtitle">algorithmic philosophy · generative art</div>
<!-- SEED (FIXED STRUCTURE) -->
<div class="sidebar-section">
<h2>Seed</h2>
<div class="seed-row">
<button class="btn" id="prevSeed">← Prev</button>
<span class="seed-value" id="seedDisplay">12345</span>
<button class="btn" id="nextSeed">Next →</button>
</div>
<div class="seed-row" style="margin-top:8px;">
<button class="btn btn-primary" id="randomSeed">🎲 Random</button>
<input type="number" id="jumpSeedInput" value="12345" min="0" max="999999">
<button class="btn" id="jumpSeedBtn">Go</button>
</div>
</div>
<!-- PARAMETERS (VARIABLE) -->
<div class="sidebar-section">
<h2>Parameters</h2>
<div class="control-group">
<label>Particle Count</label>
<input type="range" id="pCount" min="100" max="5000" step="100" value="1500" oninput="updateParam('pCount', this.value)">
<span class="value-display" id="pCount-val">1500</span>
</div>
<div class="control-group">
<label>Noise Scale</label>
<input type="range" id="noiseScale" min="0.001" max="0.02" step="0.0005" value="0.008" oninput="updateParam('noiseScale', this.value)">
<span class="value-display" id="noiseScale-val">0.008</span>
</div>
<div class="control-group">
<label>Symmetry Order</label>
<input type="range" id="symOrder" min="2" max="12" step="1" value="6" oninput="updateParam('symOrder', this.value)">
<span class="value-display" id="symOrder-val">6</span>
</div>
<div class="control-group">
<label>Particle Speed</label>
<input type="range" id="speed" min="0.1" max="3.0" step="0.1" value="1.2" oninput="updateParam('speed', this.value)">
<span class="value-display" id="speed-val">1.2</span>
</div>
<div class="control-group">
<label>Line Alpha</label>
<input type="range" id="lineAlpha" min="5" max="80" step="1" value="25" oninput="updateParam('lineAlpha', this.value)">
<span class="value-display" id="lineAlpha-val">25</span>
</div>
</div>
<!-- COLORS (OPTIONAL) -->
<div class="sidebar-section">
<h2>Colors</h2>
<div class="control-group">
<label>Background</label>
<input type="color" id="bgColor" value="#f0ebe0" oninput="updateParam('bgColor', this.value)">
</div>
<div class="control-group">
<label>Particle Color</label>
<input type="color" id="particleColor" value="#2c2a28" oninput="updateParam('particleColor', this.value)">
</div>
</div>
<!-- ACTIONS (FIXED) -->
<div class="sidebar-section">
<h2>Actions</h2>
<div class="actions-row">
<button class="btn" id="regenerateBtn">Regenerate</button>
<button class="btn" id="resetBtn">Reset</button>
<button class="btn" id="downloadBtn">Download PNG</button>
</div>
</div>
</div>
<!-- CANVAS -->
<div id="canvas-container"></div>
</div>
<script>
// ============================================================
// GLOBALS
// ============================================================
let params = {
seed: 12345,
pCount: 1500,
noiseScale: 0.008,
symOrder: 6,
speed: 1.2,
lineAlpha: 25,
bgColor: '#f0ebe0',
particleColor: '#2c2a28'
};
let particles = [];
let flowField = [];
const cols = 80;
const rows = 80;
let cellSize;
let canvasContainer;
let initialized = false;
// ============================================================
// PARAMETER UPDATE
// ============================================================
function updateParam(id, val) {
const display = document.getElementById(id + '-val');
if (display) display.textContent = val;
params[id] = val;
// For color inputs, we re-render immediately
if (id === 'bgColor' || id === 'particleColor') {
if (window.mySketch && window.mySketch._setup) {
render();
}
}
}
// ============================================================
// P5 SKETCH
// ============================================================
const s = (sketch) => {
sketch.setup = () => {
canvasContainer = document.getElementById('canvas-container');
const size = Math.min(window.innerWidth - 360, window.innerHeight - 40, 1000);
sketch.createCanvas(size, size);
sketch.parent('canvas-container');
sketch.pixelDensity(1);
sketch.colorMode(sketch.RGB, 255, 255, 255, 255);
initSystem();
window.mySketch = sketch;
window.mySketch._setup = true;
};
sketch.draw = () => {
// Only update if animate mode, but we'll do static for this piece
// Actually, we render once in setup and on param change
};
sketch.windowResized = () => {
const size = Math.min(window.innerWidth - 360, window.innerHeight - 40, 1000);
sketch.resizeCanvas(size, size);
cellSize = sketch.width / cols;
initSystem();
render();
};
// ---------- Initialize System ----------
function initSystem() {
sketch.randomSeed(params.seed);
sketch.noiseSeed(params.seed);
cellSize = sketch.width / cols;
particles = [];
for (let i = 0; i < params.pCount; i++) {
particles.push({
x: sketch.random(sketch.width),
y: sketch.random(sketch.height),
lifespan: sketch.random(100, 300),
age: 0
});
}
// Build flow field
buildFlowField();
}
function buildFlowField() {
flowField = new Array(cols * rows);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const angle1 = sketch.noise(x * params.noiseScale, y * params.noiseScale) * sketch.TWO_PI * 2;
const angle2 = sketch.noise(x * params.noiseScale * 0.5, y * params.noiseScale * 0.5 + 1000) * sketch.TWO_PI;
let angle = angle1 + angle2;
// Apply symmetry: mirror around multiple axes
const order = params.symOrder;
const cx = sketch.width / 2;
const cy = sketch.height / 2;
const dx = (x * cellSize) - cx;
const dy = (y * cellSize) - cy;
const dist = Math.sqrt(dx*dx + dy*dy);
const baseAngle = Math.atan2(dy, dx);
// Symmetry: fold angle into pie slice
const sliceAngle = sketch.TWO_PI / order;
const folded = baseAngle % sliceAngle;
// Use the folded angle to modulate the noise contribution
const mod = sketch.noise(dist * 0.01, folded * 2);
angle += mod * 0.5;
flowField[y * cols + x] = sketch.p5.Vector.fromAngle(angle);
}
}
}
function render() {
sketch.background(sketch.color(params.bgColor));
sketch.stroke(sketch.color(params.particleColor));
sketch.strokeWeight(1.2);
sketch.noFill();
// Rebuild field if seed changed (actually already done in initSystem)
// But we call render after parameter changes, so need to re-init if seed changed
// Simplified: just recompute field and particles
initSystem();
// Actually initSystem already creates particles, but we want to keep the same seed-derived particles
// We'll just redraw using current particles
// Draw particles as trails
sketch.background(sketch.color(params.bgColor)); // clear
// Use a temporary graphics for accumulation? Simpler: draw each particle trail
// But we want trails to accumulate. Since we redraw completely, we need to simulate particles moving and leaving trails.
// For static image, we can simulate all particles over a number of steps.
// Let's do that: run simulation for 300 steps to build up the image.
const steps = 300;
// Create offscreen buffer for accumulation
const pg = sketch.createGraphics(sketch.width, sketch.height);
pg.background(sketch.color(params.bgColor));
pg.noFill();
// Reinitialize particles fresh for each render
particles = [];
for (let i = 0; i < params.pCount; i++) {
particles.push({
x: sketch.random(sketch.width),
y: sketch.random(sketch.height),
age: 0,
lifespan: sketch.random(100, 300)
});
}
for (let step = 0; step < steps; step++) {
// Update particles
for (let p of particles) {
if (p.age > p.lifespan) {
// Reset particle
p.x = sketch.random(sketch.width);
p.y = sketch.random(sketch.height);
p.age = 0;
p.lifespan = sketch.random(100, 300);
}
// Get flow field vector
const col = Math.floor(p.x / cellSize);
const row = Math.floor(p.y / cellSize);
const idx = Math.max(0, Math.min(cols-1, col)) + Math.max(0, Math.min(rows-1, row)) * cols;
const v = flowField[idx];
if (v) {
p.x += v.x * params.speed;
p.y += v.y * params.speed;
}
p.age++;
// Draw point
const alpha = map(p.age, 0, p.lifespan, 200, 20);
const colVal = sketch.color(sketch.red(params.particleColor), sketch.green(params.particleColor), sketch.blue(params.particleColor), alpha * (params.lineAlpha/100));
pg.stroke(colVal);
pg.point(p.x, p.y);
}
}
// Apply symmetry: reflect particles? Actually the flow field already creates symmetry through the field modulation.
// But to enhance symmetry, we can mirror the entire image.
// Let's draw the pg image, then mirror it.
sketch.image(pg, 0, 0);
// Mirror around horizontal and vertical axes to create full symmetry
// But our field already has symmetry, so this would double it. Instead, let's not mirror.
// The symmetry comes from the field building folding.
// However, to truly see the symmetry order, we can apply a radial tiling: draw multiple copies rotated.
// Let's do that for dramatic effect.
// But we already have particle trails. Better: draw the pg multiple times rotated around center.
const centerX = sketch.width/2;
const centerY = sketch.height/2;
const order = params.symOrder;
pg.loadPixels(); // not needed if we just image
sketch.push();
sketch.translate(centerX, centerY);
for (let i = 0; i < order; i++) {
const angle = (sketch.TWO_PI / order) * i;
sketch.push();
sketch.rotate(angle);
sketch.image(pg, -centerX, -centerY);
sketch.pop();
}
sketch.pop();
pg.remove();
}
// Expose render to global
sketch.render = render;
};
// ============================================================
// INIT P5
// ============================================================
let myP5 = new p5(s);
// ============================================================
// UI EVENT BINDINGS
// ============================================================
function renderWithParams() {
params.seed = parseInt(document.getElementById('seedDisplay').textContent);
if (window.mySketch && window.mySketch.render) {
window.mySketch.render();
}
}
// Seed controls
document.getElementById('prevSeed').addEventListener('click', () => {
let seed = parseInt(document.getElementById('seedDisplay').textContent);
seed = (seed - 1 + 100000) % 100000;
document.getElementById('seedDisplay').textContent = seed;
renderWithParams();
});
document.getElementById('nextSeed').addEventListener('click', () => {
let seed = parseInt(document.getElementById('seedDisplay').textContent);
seed = (seed + 1) % 100000;
document.getElementById('seedDisplay').textContent = seed;
renderWithParams();
});
document.getElementById('randomSeed').addEventListener('click', () => {
const seed = Math.floor(Math.random() * 100000);
document.getElementById('seedDisplay').textContent = seed;
renderWithParams();
});
document.getElementById('jumpSeedBtn').addEventListener('click', () => {
const val = parseInt(document.getElementById('jumpSeedInput').value);
if (!isNaN(val) && val >= 0 && val <= 999999) {
document.getElementById('seedDisplay').textContent = val;
renderWithParams();
}
});
// Regenerate - re-render with same seed (but new random particle positions? Actually it will reinitialize with same seed, so same random sequence. So it's deterministic.)
document.getElementById('regenerateBtn').addEventListener('click', renderWithParams);
// Reset parameters to defaults
document.getElementById('resetBtn').addEventListener('click', () => {
const defaults = {
pCount: 1500,
noiseScale: 0.008,
symOrder: 6,
speed: 1.2,
lineAlpha: 25,
bgColor: '#f0ebe0',
particleColor: '#2c2a28'
};
for (let key in defaults) {
const el = document.getElementById(key);
if (el) {
if (el.type === 'color') {
el.value = defaults[key];
} else {
el.value = defaults[key];
}
params[key] = defaults[key];
const display = document.getElementById(key + '-val');
if (display) display.textContent = defaults[key];
}
}
renderWithParams();
});
// Download PNG
document.getElementById('downloadBtn').addEventListener('click', () => {
const canvas = document.querySelector('canvas');
if (canvas) {
const link = document.createElement('a');
link.download = `VoidSymmetries_seed${params.seed}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
});
// Trigger initial render after p5 is loaded
setTimeout(() => {
if (window.mySketch && window.mySketch.render) {
window.mySketch.render();
}
}, 200);
// Re-render when any slider changes (already calling updateParam, but only redraws for color)
// We need to also re-render for other sliders. Add event listeners to all sliders.
document.querySelectorAll('input[type="range"]').forEach(el => {
el.addEventListener('input', function() {
// updateParam already updates the display, but we need to re-render
renderWithParams();
});
});
// For color inputs, also re-render
document.querySelectorAll('input[type="color"]').forEach(el => {
el.addEventListener('input', function() {
params[this.id] = this.value;
renderWithParams();
});
});
</script>
</body>
</html>