868 lines
32 KiB
HTML
868 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Nebula Genesis - Generative Art</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--anthropic-dark: #141413;
|
|
--anthropic-light: #faf9f5;
|
|
--anthropic-mid-gray: #b0aea5;
|
|
--anthropic-light-gray: #e8e6dc;
|
|
--anthropic-orange: #d97757;
|
|
--anthropic-blue: #6a9bcc;
|
|
--anthropic-green: #788c5d;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Poppins', sans-serif;
|
|
background: linear-gradient(135deg, var(--anthropic-light) 0%, #0a0a0f 100%);
|
|
min-height: 100vh;
|
|
color: var(--anthropic-light);
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 320px;
|
|
flex-shrink: 0;
|
|
background: rgba(20, 20, 25, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
padding: 24px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.sidebar h1 {
|
|
font-family: 'Lora', serif;
|
|
font-size: 24px;
|
|
font-weight: 500;
|
|
color: var(--anthropic-light);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.sidebar .subtitle {
|
|
color: var(--anthropic-mid-gray);
|
|
font-size: 14px;
|
|
margin-bottom: 32px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.control-section {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.control-section h3 {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--anthropic-light);
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.control-section h3::before {
|
|
content: '•';
|
|
color: #ff6b4a;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.seed-input {
|
|
width: 100%;
|
|
background: rgba(255,255,255,0.1);
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
margin-bottom: 12px;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
text-align: center;
|
|
color: var(--anthropic-light);
|
|
}
|
|
|
|
.seed-input:focus {
|
|
outline: none;
|
|
border-color: #ff6b4a;
|
|
box-shadow: 0 0 0 2px rgba(255, 107, 74, 0.2);
|
|
background: rgba(255,255,255,0.15);
|
|
}
|
|
|
|
.seed-controls {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.button {
|
|
background: linear-gradient(135deg, #ff6b4a 0%, #d97757 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 16px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
width: 100%;
|
|
}
|
|
|
|
.button:hover {
|
|
background: linear-gradient(135deg, #ff8570 0%, #e98868 100%);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.button.secondary {
|
|
background: linear-gradient(135deg, #4a90c2 0%, #6a9bcc 100%);
|
|
}
|
|
|
|
.button.secondary:hover {
|
|
background: linear-gradient(135deg, #5aa0d2 0%, #7aabdc 100%);
|
|
}
|
|
|
|
.button.tertiary {
|
|
background: linear-gradient(135deg, #5d7c5d 0%, #788c5d 100%);
|
|
}
|
|
|
|
.button.tertiary:hover {
|
|
background: linear-gradient(135deg, #6d8c6d 0%, #889c6d 100%);
|
|
}
|
|
|
|
.control-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.control-group label {
|
|
display: block;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--anthropic-light);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.slider-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.slider-container input[type="range"] {
|
|
flex: 1;
|
|
height: 4px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 2px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.slider-container input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #ff6b4a;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.slider-container input[type="range"]::-webkit-slider-thumb:hover {
|
|
transform: scale(1.1);
|
|
background: #ff8570;
|
|
}
|
|
|
|
.slider-container input[type="range"]::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #ff6b4a;
|
|
border-radius: 50%;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.value-display {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
color: var(--anthropic-mid-gray);
|
|
min-width: 60px;
|
|
text-align: right;
|
|
}
|
|
|
|
.color-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.color-group label {
|
|
display: block;
|
|
font-size: 12px;
|
|
color: var(--anthropic-mid-gray);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.color-picker-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.color-picker-container input[type="color"] {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.color-value {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
color: var(--anthropic-mid-gray);
|
|
}
|
|
|
|
.button-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.button-row .button {
|
|
flex: 1;
|
|
}
|
|
|
|
.canvas-area {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 0;
|
|
}
|
|
|
|
#canvas-container {
|
|
width: 100%;
|
|
max-width: 1000px;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
background: #000;
|
|
}
|
|
|
|
#canvas-container canvas {
|
|
display: block;
|
|
width: 100% !important;
|
|
height: auto !important;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
color: var(--anthropic-mid-gray);
|
|
min-height: 400px;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.container {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 100%;
|
|
}
|
|
|
|
.canvas-area {
|
|
padding: 20px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="sidebar">
|
|
<h1>Nebula Genesis</h1>
|
|
<div class="subtitle">Stellar nursery in algorithmic form. Witness gas clouds collapse into stellar embryos.</div>
|
|
|
|
<div class="control-section">
|
|
<h3>Seed</h3>
|
|
<input type="number" id="seed-input" class="seed-input" value="12345" onchange="updateSeed()">
|
|
<div class="seed-controls">
|
|
<button class="button secondary" onclick="previousSeed()">← Prev</button>
|
|
<button class="button secondary" onclick="nextSeed()">Next →</button>
|
|
</div>
|
|
<button class="button tertiary" onclick="randomSeedAndUpdate()">↻ Random</button>
|
|
</div>
|
|
|
|
<div class="control-section">
|
|
<h3>Particles</h3>
|
|
|
|
<div class="control-group">
|
|
<label>Particle Count</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="particleCount" min="1000" max="8000" step="500" value="4000" oninput="updateParam('particleCount', this.value)">
|
|
<span class="value-display" id="particleCount-value">4000</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Flow Speed</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="flowSpeed" min="0.1" max="3.0" step="0.1" value="1.0" oninput="updateParam('flowSpeed', this.value)">
|
|
<span class="value-display" id="flowSpeed-value">1.0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Particle Life</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="particleLife" min="50" max="500" step="25" value="200" oninput="updateParam('particleLife', this.value)">
|
|
<span class="value-display" id="particleLife-value">200</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-section">
|
|
<h3>Gravity & Structure</h3>
|
|
|
|
<div class="control-group">
|
|
<label>Gravity Strength</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="gravityStrength" min="0.1" max="2.0" step="0.1" value="0.8" oninput="updateParam('gravityStrength', this.value)">
|
|
<span class="value-display" id="gravityStrength-value">0.8</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Noise Scale</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="noiseScale" min="0.002" max="0.02" step="0.001" value="0.008" oninput="updateParam('noiseScale', this.value)">
|
|
<span class="value-display" id="noiseScale-value">0.008</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Turbulence</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="turbulence" min="0.1" max="2.0" step="0.1" value="1.0" oninput="updateParam('turbulence', this.value)">
|
|
<span class="value-display" id="turbulence-value">1.0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Star Density</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="starDensity" min="1" max="8" step="1" value="4" oninput="updateParam('starDensity', this.value)">
|
|
<span class="value-display" id="starDensity-value">4</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-section">
|
|
<h3>Visuals</h3>
|
|
|
|
<div class="control-group">
|
|
<label>Trail Opacity</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="trailOpacity" min="2" max="25" step="1" value="8" oninput="updateParam('trailOpacity', this.value)">
|
|
<span class="value-display" id="trailOpacity-value">8</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Core Brightness</label>
|
|
<div class="slider-container">
|
|
<input type="range" id="coreBrightness" min="50" max="200" step="10" value="120" oninput="updateParam('coreBrightness', this.value)">
|
|
<span class="value-display" id="coreBrightness-value">120</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-section">
|
|
<h3>Colors</h3>
|
|
|
|
<div class="color-group">
|
|
<label>Core Color (Hydrogen)</label>
|
|
<div class="color-picker-container">
|
|
<input type="color" id="colorCore" value="#ff4532" onchange="updateColor('colorCore', this.value)">
|
|
<span class="color-value" id="colorCore-value">#ff4532</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<label>Mid Layer (Sulfur/Oxygen)</label>
|
|
<div class="color-picker-container">
|
|
<input type="color" id="colorMid" value="#9b59b6" onchange="updateColor('colorMid', this.value)">
|
|
<span class="color-value" id="colorMid-value">#9b59b6</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<label>Edge Color (Reflection)</label>
|
|
<div class="color-picker-container">
|
|
<input type="color" id="colorEdge" value="#3498db" onchange="updateColor('colorEdge', this.value)">
|
|
<span class="color-value" id="colorEdge-value">#3498db</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<label>Star Color</label>
|
|
<div class="color-picker-container">
|
|
<input type="color" id="colorStar" value="#fff8e7" onchange="updateColor('colorStar', this.value)">
|
|
<span class="color-value" id="colorStar-value">#fff8e7</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-section">
|
|
<h3>Actions</h3>
|
|
<div class="button-row">
|
|
<button class="button" onclick="resetParameters()">Reset</button>
|
|
<button class="button tertiary" onclick="downloadCanvas()">↓ PNG</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="canvas-area">
|
|
<div id="canvas-container">
|
|
<div class="loading">Initializing stellar nursery...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// NEBULA GENESIS - Generative Art Algorithm
|
|
// A meticulously crafted algorithm simulating stellar formation
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
let params = {
|
|
seed: 12345,
|
|
particleCount: 4000,
|
|
flowSpeed: 1.0,
|
|
particleLife: 200,
|
|
gravityStrength: 0.8,
|
|
noiseScale: 0.008,
|
|
turbulence: 1.0,
|
|
starDensity: 4,
|
|
trailOpacity: 8,
|
|
coreBrightness: 120,
|
|
colors: {
|
|
core: '#ff4532',
|
|
mid: '#9b59b6',
|
|
edge: '#3498db',
|
|
star: '#fff8e7'
|
|
}
|
|
};
|
|
|
|
let defaultParams = {...params};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// PARTICLE SYSTEM
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
let particles = [];
|
|
let gravityWells = [];
|
|
let cols, rows;
|
|
let scl = 8;
|
|
|
|
function setup() {
|
|
let canvas = createCanvas(1200, 1200);
|
|
canvas.parent('canvas-container');
|
|
|
|
initializeSystem();
|
|
|
|
document.querySelector('.loading').style.display = 'none';
|
|
}
|
|
|
|
function initializeSystem() {
|
|
randomSeed(params.seed);
|
|
noiseSeed(params.seed);
|
|
|
|
particles = [];
|
|
gravityWells = [];
|
|
|
|
// Initialize gravity wells at strategic positions
|
|
initializeGravityWells();
|
|
|
|
// Initialize particles
|
|
for (let i = 0; i < params.particleCount; i++) {
|
|
particles.push(new NebulaParticle());
|
|
}
|
|
|
|
// Calculate flow field dimensions
|
|
cols = floor(width / scl);
|
|
rows = floor(height / scl);
|
|
|
|
// Clear background
|
|
background(5, 5, 15);
|
|
}
|
|
|
|
function initializeGravityWells() {
|
|
let numWells = params.starDensity;
|
|
let centerX = width / 2;
|
|
let centerY = height / 2;
|
|
let maxRadius = min(width, height) * 0.35;
|
|
|
|
// Main central well
|
|
gravityWells.push({
|
|
x: centerX + random(-50, 50),
|
|
y: centerY + random(-50, 50),
|
|
strength: random(0.8, 1.2),
|
|
radius: random(maxRadius * 0.15, maxRadius * 0.25)
|
|
});
|
|
|
|
// Additional wells in spiral pattern
|
|
for (let i = 1; i < numWells; i++) {
|
|
let angle = (i / (numWells - 1)) * TWO_PI * random(1.5, 3);
|
|
let radius = maxRadius * random(0.3, 0.9);
|
|
let spiralFactor = 1 - (i / numWells);
|
|
|
|
gravityWells.push({
|
|
x: centerX + cos(angle) * radius * spiralFactor,
|
|
y: centerY + sin(angle) * radius * spiralFactor,
|
|
strength: random(0.4, 0.9),
|
|
radius: random(maxRadius * 0.08, maxRadius * 0.15)
|
|
});
|
|
}
|
|
}
|
|
|
|
function generateFlowField(x, y, scale, time) {
|
|
// Multi-layered noise for complex flow patterns
|
|
let n1 = noise(x * scale, y * scale, time * 0.1);
|
|
let n2 = noise(x * scale * 2.3 + 100, y * scale * 2.3 + 100, time * 0.15);
|
|
let n3 = noise(x * scale * 0.4 + 200, y * scale * 0.4 + 200, time * 0.08);
|
|
|
|
let combined = (n1 + n2 * 0.5 + n3 * 0.25) / 1.75;
|
|
return combined * TWO_PI * params.turbulence;
|
|
}
|
|
|
|
function draw() {
|
|
// Very subtle fade for accumulation effect
|
|
noStroke();
|
|
|
|
// Update and draw particles
|
|
for (let p of particles) {
|
|
p.update();
|
|
p.display();
|
|
}
|
|
|
|
// Occasionally respawn particles that have died
|
|
let deadCount = particles.filter(p => p.isDead()).length;
|
|
if (deadCount > particles.length * 0.1) {
|
|
for (let i = 0; i < min(deadCount, 50); i++) {
|
|
let deadIndex = particles.findIndex(p => p.isDead());
|
|
if (deadIndex !== -1) {
|
|
particles[deadIndex] = new NebulaParticle();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// NEBULA PARTICLE CLASS
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
class NebulaParticle {
|
|
constructor() {
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
// Spawn at edges or randomly
|
|
if (random() < 0.4) {
|
|
// Spawn at edges
|
|
let edge = floor(random(4));
|
|
switch(edge) {
|
|
case 0: this.pos = createVector(random(width), random(5)); break;
|
|
case 1: this.pos = createVector(random(width), height - 5); break;
|
|
case 2: this.pos = createVector(random(5), random(height)); break;
|
|
case 3: this.pos = createVector(width - 5, random(height)); break;
|
|
}
|
|
} else {
|
|
// Spawn randomly with bias towards edges
|
|
let angle = random(TWO_PI);
|
|
let r = random(width * 0.6);
|
|
this.pos = createVector(width/2 + cos(angle) * r, height/2 + sin(angle) * r);
|
|
}
|
|
|
|
this.vel = createVector(0, 0);
|
|
this.acc = createVector(0, 0);
|
|
this.life = params.particleLife;
|
|
this.maxLife = params.particleLife;
|
|
this.history = [];
|
|
this.maxHistory = 8;
|
|
|
|
// Individual particle variation
|
|
this.noiseOffset = random(1000);
|
|
this.sizeFactor = random(0.5, 1.5);
|
|
}
|
|
|
|
update() {
|
|
let scale = params.noiseScale;
|
|
let time = frameCount * 0.01;
|
|
|
|
// Calculate flow field force
|
|
let flowAngle = generateFlowField(this.pos.x, this.pos.y, scale, time);
|
|
let flowForce = p5.Vector.fromAngle(flowAngle);
|
|
flowForce.mult(params.flowSpeed * 0.3);
|
|
|
|
// Calculate gravity well forces
|
|
let gravityForce = createVector(0, 0);
|
|
for (let well of gravityWells) {
|
|
let d = dist(this.pos.x, this.pos.y, well.x, well.y);
|
|
if (d < well.radius * 3 && d > 5) {
|
|
let forceDir = createVector(well.x - this.pos.x, well.y - this.pos.y);
|
|
forceDir.normalize();
|
|
let forceMag = well.strength * params.gravityStrength * 50 / (d * d + 100);
|
|
forceDir.mult(forceMag);
|
|
gravityForce.add(forceDir);
|
|
}
|
|
}
|
|
|
|
// Add subtle turbulence
|
|
let turbAngle = noise(this.pos.x * 0.01 + time, this.pos.y * 0.01 + time) * TWO_PI;
|
|
let turbForce = p5.Vector.fromAngle(turbAngle);
|
|
turbForce.mult(0.1);
|
|
|
|
this.acc.add(flowForce);
|
|
this.acc.add(gravityForce);
|
|
this.acc.add(turbForce);
|
|
|
|
this.vel.add(this.acc);
|
|
this.vel.limit(4);
|
|
this.pos.add(this.vel);
|
|
this.acc.mult(0);
|
|
|
|
// Store history for trail effect
|
|
this.history.push(this.pos.copy());
|
|
if (this.history.length > this.maxHistory) {
|
|
this.history.shift();
|
|
}
|
|
|
|
this.life--;
|
|
|
|
// Respawn if out of bounds or dead
|
|
if (this.isOutOfBounds() || this.isDead()) {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
isOutOfBounds() {
|
|
return this.pos.x < -50 || this.pos.x > width + 50 ||
|
|
this.pos.y < -50 || this.pos.y > height + 50;
|
|
}
|
|
|
|
isDead() {
|
|
return this.life <= 0;
|
|
}
|
|
|
|
display() {
|
|
// Calculate velocity magnitude for color mapping
|
|
let speed = this.vel.mag();
|
|
let lifeRatio = this.life / this.maxLife;
|
|
|
|
// Determine which color to use based on speed and position
|
|
let color = this.selectColor(speed);
|
|
|
|
// Calculate opacity based on life and position
|
|
let alpha = map(this.trailOpacity / 255 * lifeRatio, 0, 1, 3, 20);
|
|
|
|
// Draw trail
|
|
noFill();
|
|
beginShape();
|
|
for (let i = 0; i < this.history.length; i++) {
|
|
let pos = this.history[i];
|
|
let t = i / this.history.length;
|
|
let trailAlpha = alpha * t;
|
|
|
|
// Blend color along trail
|
|
let c = color;
|
|
c.setAlpha(trailAlpha);
|
|
stroke(c);
|
|
strokeWeight(1.5 * this.sizeFactor * t);
|
|
vertex(pos.x, pos.y);
|
|
}
|
|
endShape();
|
|
|
|
// Draw current position with glow effect
|
|
noStroke();
|
|
|
|
// Outer glow
|
|
let glowSize = map(speed, 0, 4, 3, 8) * this.sizeFactor;
|
|
let glowColor = color;
|
|
glowColor.setAlpha(alpha * 0.3);
|
|
fill(glowColor);
|
|
ellipse(this.pos.x, this.pos.y, glowSize * 2, glowSize * 2);
|
|
|
|
// Core
|
|
let coreColor = this.selectCoreColor(speed);
|
|
coreColor.setAlpha(alpha * 0.8);
|
|
fill(coreColor);
|
|
ellipse(this.pos.x, this.pos.y, glowSize * 0.6, glowSize * 0.6);
|
|
}
|
|
|
|
selectColor(speed) {
|
|
// Calculate distance to nearest gravity well
|
|
let minDist = Infinity;
|
|
for (let well of gravityWells) {
|
|
let d = dist(this.pos.x, this.pos.y, well.x, well.y);
|
|
minDist = min(minDist, d);
|
|
}
|
|
|
|
// Map to color palette
|
|
let color1 = color(params.colors.core);
|
|
let color2 = color(params.colors.mid);
|
|
let color3 = color(params.colors.edge);
|
|
|
|
let maxDist = min(width, height) * 0.4;
|
|
let normalizedDist = constrain(minDist / maxDist, 0, 1);
|
|
let normalizedSpeed = constrain(speed / 3, 0, 1);
|
|
|
|
// Blend based on distance and speed
|
|
if (normalizedDist < 0.3) {
|
|
return lerpColor(color1, color2, normalizedDist * 3);
|
|
} else {
|
|
return lerpColor(color2, color3, (normalizedDist - 0.3) * 1.4);
|
|
}
|
|
}
|
|
|
|
selectCoreColor(speed) {
|
|
let starColor = color(params.colors.star);
|
|
let midColor = color(params.colors.mid);
|
|
|
|
let normalizedSpeed = constrain(speed / 3, 0, 1);
|
|
return lerpColor(starColor, midColor, normalizedSpeed * 0.7);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// UI CONTROL HANDLERS
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function updateParam(paramName, value) {
|
|
value = parseFloat(value);
|
|
params[paramName] = value;
|
|
document.getElementById(paramName + '-value').textContent = value;
|
|
|
|
// Some params need reinitialization
|
|
if (paramName === 'particleCount' || paramName === 'starDensity') {
|
|
initializeSystem();
|
|
}
|
|
}
|
|
|
|
function updateColor(colorId, value) {
|
|
let paramName = colorId.replace('color', '').toLowerCase();
|
|
params.colors[paramName] = value;
|
|
document.getElementById(colorId + '-value').textContent = value;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// SEED CONTROL FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function updateSeedDisplay() {
|
|
document.getElementById('seed-input').value = params.seed;
|
|
}
|
|
|
|
function updateSeed() {
|
|
let input = document.getElementById('seed-input');
|
|
let newSeed = parseInt(input.value);
|
|
if (newSeed && newSeed > 0) {
|
|
params.seed = newSeed;
|
|
initializeSystem();
|
|
} else {
|
|
updateSeedDisplay();
|
|
}
|
|
}
|
|
|
|
function previousSeed() {
|
|
params.seed = Math.max(1, params.seed - 1);
|
|
updateSeedDisplay();
|
|
initializeSystem();
|
|
}
|
|
|
|
function nextSeed() {
|
|
params.seed = params.seed + 1;
|
|
updateSeedDisplay();
|
|
initializeSystem();
|
|
}
|
|
|
|
function randomSeedAndUpdate() {
|
|
params.seed = Math.floor(Math.random() * 999999) + 1;
|
|
updateSeedDisplay();
|
|
initializeSystem();
|
|
}
|
|
|
|
function resetParameters() {
|
|
params = {...defaultParams};
|
|
|
|
// Update UI elements
|
|
document.getElementById('particleCount').value = params.particleCount;
|
|
document.getElementById('particleCount-value').textContent = params.particleCount;
|
|
document.getElementById('flowSpeed').value = params.flowSpeed;
|
|
document.getElementById('flowSpeed-value').textContent = params.flowSpeed;
|
|
document.getElementById('particleLife').value = params.particleLife;
|
|
document.getElementById('particleLife-value').textContent = params.particleLife;
|
|
document.getElementById('gravityStrength').value = params.gravityStrength;
|
|
document.getElementById('gravityStrength-value').textContent = params.gravityStrength;
|
|
document.getElementById('noiseScale').value = params.noiseScale;
|
|
document.getElementById('noiseScale-value').textContent = params.noiseScale;
|
|
document.getElementById('turbulence').value = params.turbulence;
|
|
document.getElementById('turbulence-value').textContent = params.turbulence;
|
|
document.getElementById('starDensity').value = params.starDensity;
|
|
document.getElementById('starDensity-value').textContent = params.starDensity;
|
|
document.getElementById('trailOpacity').value = params.trailOpacity;
|
|
document.getElementById('trailOpacity-value').textContent = params.trailOpacity;
|
|
document.getElementById('coreBrightness').value = params.coreBrightness;
|
|
document.getElementById('coreBrightness-value').textContent = params.coreBrightness;
|
|
|
|
// Reset colors
|
|
document.getElementById('colorCore').value = params.colors.core;
|
|
document.getElementById('colorCore-value').textContent = params.colors.core;
|
|
document.getElementById('colorMid').value = params.colors.mid;
|
|
document.getElementById('colorMid-value').textContent = params.colors.mid;
|
|
document.getElementById('colorEdge').value = params.colors.edge;
|
|
document.getElementById('colorEdge-value').textContent = params.colors.edge;
|
|
document.getElementById('colorStar').value = params.colors.star;
|
|
document.getElementById('colorStar-value').textContent = params.colors.star;
|
|
|
|
updateSeedDisplay();
|
|
initializeSystem();
|
|
}
|
|
|
|
function downloadCanvas() {
|
|
saveCanvas('nebula-genesis-' + params.seed, 'png');
|
|
}
|
|
|
|
// Initialize UI on load
|
|
window.addEventListener('load', function() {
|
|
updateSeedDisplay();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|