changed behavior of nebula

This commit is contained in:
nvrl committed 2026-02-18 14:51:42 +01:00
1 parent bcbd38ce63
commit 834e830d6e
2 files changed
+108 -81

No files matched your search

+19 -20
View File
@@ -3,13 +3,13 @@
"slug": "nebula",
"description": "An interactive space-themed nebular cloud with fluid movement.",
"settings": {
"particleCount": 350,
"speed": 0.003,
"opacity": 0.75,
"smearRadius": 120,
"turbulence": 0.8,
"cohesion": 0.02,
"returnSpeed": 0.012
"particleCount": 450,
"speed": 0.002,
"opacity": 0.65,
"smearRadius": 140,
"turbulence": 0.5,
"returnSpeed": 0.005,
"starDensity": 180
},
"schema": [
{
@@ -18,7 +18,15 @@
"type": "number",
"min": 100,
"max": 1000,
"description": "Number of particles per color cluster."
"description": "Total number of gas particles. Higher values make the nebula look thicker."
},
{
"key": "starDensity",
"label": "Star Count",
"type": "number",
"min": 0,
"max": 500,
"description": "Number of background stars in the night sky."
},
{
"key": "smearRadius",
@@ -27,25 +35,16 @@
"min": 50,
"max": 300,
"step": 10,
"description": "The width of the path you carve when moving the mouse."
},
{
"key": "turbulence",
"label": "Click Turbulence",
"type": "slider",
"min": 0.1,
"max": 2.0,
"step": 0.1,
"description": "Strength of chaotic motion when clicking the cloud."
"description": "Width of the path carved through the gas."
},
{
"key": "returnSpeed",
"label": "Healing Speed",
"type": "slider",
"min": 0.001,
"max": 0.05,
"max": 0.02,
"step": 0.001,
"description": "How fast the carved path closes and the cloud reforms."
"description": "How fast the gas drifts back. Low values feel like smoke, high values feel like jelly."
},
{
"key": "opacity",
+89 -61
View File
@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useMemo } from 'react';
import { alpha, useTheme } from '@mui/material';
interface NebulaBannerProps {
@@ -12,8 +12,8 @@ interface NebulaBannerProps {
opacity?: number;
smearRadius?: number;
turbulence?: number;
cohesion?: number;
returnSpeed?: number;
starDensity?: number;
};
}
@@ -21,13 +21,13 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
const canvasRef = useRef<HTMLCanvasElement>(null);
const theme = useTheme();
const particleCount = settings?.particleCount ?? 300;
const speed = settings?.speed ?? 0.002;
const globalOpacity = settings?.opacity ?? 0.7;
const smearRadius = settings?.smearRadius ?? 100;
const turbulenceStrength = settings?.turbulence ?? 0.5;
const cohesion = settings?.cohesion ?? 0.02;
const returnSpeed = settings?.returnSpeed ?? 0.01;
const particleCount = settings?.particleCount ?? 400;
const speed = settings?.speed ?? 0.0015;
const globalOpacity = settings?.opacity ?? 0.6;
const smearRadius = settings?.smearRadius ?? 120;
const turbulenceStrength = settings?.turbulence ?? 0.4;
const returnSpeed = settings?.returnSpeed ?? 0.004; // Much lower for smoke-like feel
const starDensity = settings?.starDensity ?? 150;
const getSeed = (str: string) => {
let hash = 0;
@@ -37,6 +37,8 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
return hash;
};
const seed = useMemo(() => getSeed(username), [username]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
@@ -45,7 +47,6 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
if (!ctx) return;
let animationFrameId: number;
const seed = getSeed(username);
const colors = palette.length >= 2 ? palette : [theme.palette.primary.main, theme.palette.secondary.main, theme.palette.info.main];
interface Particle {
@@ -59,28 +60,50 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
color: string;
alpha: number;
targetAlpha: number;
group: number;
phase: number;
}
interface Star {
x: number;
y: number;
size: number;
opacity: number;
}
let particles: Particle[] = [];
let stars: Star[] = [];
const pseudoRandom = (offset: number) => {
const x = Math.sin(seed + offset) * 10000;
return x - Math.floor(x);
};
const initParticles = (width: number, height: number) => {
const initScene = (width: number, height: number) => {
// Init Stars
const newStars = [];
for (let i = 0; i < starDensity; i++) {
newStars.push({
x: pseudoRandom(i + 5000) * width,
y: pseudoRandom(i + 6000) * height,
size: pseudoRandom(i + 7000) * 1.5,
opacity: 0.1 + pseudoRandom(i + 8000) * 0.5
});
}
// Init Gas Clusters
const newParticles = [];
const groupCenters = colors.map((_, i) => ({
x: (pseudoRandom(i * 10) * 0.6 + 0.2) * width,
y: (pseudoRandom(i * 20 + 5) * 0.6 + 0.2) * height
const clusterCount = colors.length * 3; // More clusters
const groupCenters = Array.from({ length: clusterCount }).map((_, i) => ({
x: (pseudoRandom(i * 15) * 0.8 + 0.1) * width,
y: (pseudoRandom(i * 25 + 7) * 0.8 + 0.1) * height
}));
for (let i = 0; i < particleCount; i++) {
const groupIdx = i % colors.length;
const center = groupCenters[groupIdx];
const clusterIdx = i % clusterCount;
const colorIdx = clusterIdx % colors.length;
const center = groupCenters[clusterIdx];
const angle = pseudoRandom(i + 100) * Math.PI * 2;
const dist = pseudoRandom(i + 200) * (width * 0.15);
const dist = pseudoRandom(i + 200) * (width * 0.12);
const bx = center.x + Math.cos(angle) * dist;
const by = center.y + Math.sin(angle) * dist;
@@ -88,14 +111,14 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
x: bx, y: by,
baseX: bx, baseY: by,
vx: 0, vy: 0,
size: 20 + pseudoRandom(i + 300) * 40,
color: colors[groupIdx],
size: 40 + pseudoRandom(i + 300) * 80, // Larger, softer blobs
color: colors[colorIdx],
alpha: 0,
targetAlpha: 0.1 + pseudoRandom(i + 400) * 0.2,
group: groupIdx
targetAlpha: 0.05 + pseudoRandom(i + 400) * 0.15,
phase: pseudoRandom(i + 500) * Math.PI * 2
});
}
return newParticles;
return { stars: newStars, particles: newParticles };
};
const resize = () => {
@@ -108,7 +131,9 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
ctx.scale(dpr, dpr);
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
particles = initParticles(rect.width, rect.height);
const scene = initScene(rect.width, rect.height);
stars = scene.stars;
particles = scene.particles;
}
};
@@ -119,8 +144,7 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
let mouse = { x: -1000, y: -1000, px: -1000, py: -1000, down: false };
const handleMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
mouse.px = mouse.x;
mouse.py = mouse.y;
mouse.px = mouse.x; mouse.py = mouse.y;
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
};
@@ -145,71 +169,75 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
}
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#050508';
// 1. Deep Space Night Sky Gradient
const skyGrad = ctx.createRadialGradient(width/2, height/2, 0, width/2, height/2, width);
skyGrad.addColorStop(0, '#0a0a1a'); // Dark Midnight Blue
skyGrad.addColorStop(0.5, '#05050d'); // Near Black
skyGrad.addColorStop(1, '#020205'); // Deep Space
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, width, height);
// 2. Draw Distant Stars
ctx.fillStyle = '#fff';
stars.forEach(s => {
ctx.globalAlpha = s.opacity * (0.7 + Math.sin(time * 2 + s.x) * 0.3);
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1.0;
time += speed;
// 3. Draw Gas Clouds
particles.forEach((p) => {
// Restoration Force - Pulls back to group center
// Restoration Force (Airy return)
const dxBase = p.baseX - p.x;
const dyBase = p.baseY - p.y;
p.vx += dxBase * returnSpeed;
p.vy += dyBase * returnSpeed;
// Mouse Smear / Path Carving
// Mouse Interaction (Airy Smear)
const dx = mouse.x - p.x;
const dy = mouse.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < smearRadius) {
const force = (smearRadius - dist) / smearRadius;
if (mouse.down) {
// Turbulence on click: Swirl + Random jitter
// High turbulence on click
const angle = Math.atan2(dy, dx);
p.vx += Math.cos(angle + Math.PI/2) * turbulenceStrength;
p.vy += Math.sin(angle + Math.PI/2) * turbulenceStrength;
p.vx += (pseudoRandom(p.x + time) - 0.5) * turbulenceStrength;
p.vy += (pseudoRandom(p.y + time) - 0.5) * turbulenceStrength;
p.vx += Math.cos(angle + Math.PI/2) * turbulenceStrength * 2;
p.vy += Math.sin(angle + Math.PI/2) * turbulenceStrength * 2;
} else {
// Smear: Push away from mouse movement direction
// Smoke-like smear
const mdx = mouse.x - mouse.px;
const mdy = mouse.y - mouse.py;
p.vx += mdx * force * 0.2;
p.vy += mdy * force * 0.2;
// Extra push away to "carve"
p.vx -= (dx / dist) * force * 2;
p.vy -= (dy / dist) * force * 2;
p.vx += mdx * force * 0.15;
p.vy += mdy * force * 0.15;
p.vx -= (dx / dist) * force * 0.8;
p.vy -= (dy / dist) * force * 0.8;
}
}
// Internal drift
p.vx += Math.sin(time + p.x * 0.01) * 0.05;
p.vy += Math.cos(time + p.y * 0.01) * 0.05;
// Friction
p.vx *= 0.92;
p.vy *= 0.92;
// Smooth drift
p.vx += Math.sin(time + p.phase) * 0.03;
p.vy += Math.cos(time * 0.8 + p.phase) * 0.03;
// High friction for gas feel
p.vx *= 0.95;
p.vy *= 0.95;
p.x += p.vx;
p.y += p.vy;
// Screen Wrap
const margin = p.size;
if (p.x < -margin) p.x = width + margin;
if (p.x > width + margin) p.x = -margin;
if (p.y < -margin) p.y = height + margin;
if (p.y > height + margin) p.y = -margin;
if (p.alpha < p.targetAlpha) p.alpha += 0.001;
if (p.alpha < p.targetAlpha) p.alpha += 0.002;
// Render
// Render as very soft additive blob
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size);
const colorAlpha = p.alpha * globalOpacity * (0.7 + Math.sin(time * 2 + p.x) * 0.3);
const colorAlpha = p.alpha * globalOpacity;
grad.addColorStop(0, alpha(p.color, colorAlpha));
grad.addColorStop(0.5, alpha(p.color, colorAlpha * 0.3));
grad.addColorStop(0.6, alpha(p.color, colorAlpha * 0.2));
grad.addColorStop(1, alpha(p.color, 0));
ctx.fillStyle = grad;
@@ -234,7 +262,7 @@ const NebulaBanner: React.FC<NebulaBannerProps> = ({ username, palette, ratingsC
}
cancelAnimationFrame(animationFrameId);
};
}, [username, palette, ratingsCount, followersCount, theme, particleCount, speed, globalOpacity, smearRadius, turbulenceStrength, cohesion, returnSpeed]);
}, [seed, palette, ratingsCount, followersCount, theme, particleCount, speed, globalOpacity, smearRadius, turbulenceStrength, returnSpeed, starDensity]);
return (
<canvas