Merge floating particles into WebGL shader and fix color picker clipping
Remove overflow:hidden from .card and .template-card that was clipping the color picker popover. Combine noise field + particle glow into a single GPU shader pass (40 drifting particles as uniforms). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,6 @@ section {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 20px 20px;
|
padding: 12px 20px 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: hidden;
|
|
||||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Ambient background animation — WebGL shader.
|
* Ambient background animation — WebGL shader + floating particles.
|
||||||
*
|
*
|
||||||
* Renders a smooth flowing noise field with accent-colored highlights.
|
* Renders a smooth flowing noise field with accent-colored highlights
|
||||||
* GPU-accelerated, no banding, very lightweight.
|
* plus drifting glowing particles, all in a single GPU shader pass.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const PARTICLE_COUNT = 40;
|
||||||
|
|
||||||
const VERT_SRC = `
|
const VERT_SRC = `
|
||||||
attribute vec2 a_pos;
|
attribute vec2 a_pos;
|
||||||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||||||
@@ -16,8 +18,9 @@ uniform float u_time;
|
|||||||
uniform vec2 u_res;
|
uniform vec2 u_res;
|
||||||
uniform vec3 u_accent;
|
uniform vec3 u_accent;
|
||||||
uniform vec3 u_bg;
|
uniform vec3 u_bg;
|
||||||
|
uniform vec3 u_particles[${PARTICLE_COUNT}]; // xy = position (0-1), z = radius
|
||||||
|
|
||||||
// Simplex-style noise (compact hash-based)
|
// Simplex-style noise
|
||||||
vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
|
vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
|
||||||
vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
|
vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
|
||||||
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
|
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
|
||||||
@@ -59,31 +62,76 @@ void main() {
|
|||||||
|
|
||||||
float n = n1 * 0.5 + n2 * 0.3 + n3 * 0.2;
|
float n = n1 * 0.5 + n2 * 0.3 + n3 * 0.2;
|
||||||
|
|
||||||
// Map noise to a soft glow: only show the bright peaks
|
// Map noise to a soft glow
|
||||||
float glow = smoothstep(0.05, 0.6, n);
|
float glow = smoothstep(0.05, 0.6, n);
|
||||||
|
|
||||||
// Create two accent-derived colors with hue shift
|
// Two accent-derived colors
|
||||||
vec3 col1 = u_accent;
|
vec3 col1 = u_accent;
|
||||||
vec3 col2 = u_accent.gbr; // channel rotation for variety
|
vec3 col2 = u_accent.gbr;
|
||||||
|
|
||||||
// Mix between the two colors based on second noise layer
|
|
||||||
float colorMix = n2 * 0.5 + 0.5;
|
float colorMix = n2 * 0.5 + 0.5;
|
||||||
vec3 color = mix(col1, col2, colorMix);
|
vec3 color = mix(col1, col2, colorMix);
|
||||||
|
|
||||||
// Final: blend colored glow onto background
|
// Noise background
|
||||||
vec3 result = mix(u_bg, u_bg + color * 0.6, glow * 0.14);
|
vec3 result = mix(u_bg, u_bg + color * 0.6, glow * 0.14);
|
||||||
|
|
||||||
|
// Floating particles — accumulate glow from each
|
||||||
|
float particleGlow = 0.0;
|
||||||
|
float particleColor = 0.0;
|
||||||
|
for (int i = 0; i < ${PARTICLE_COUNT}; i++) {
|
||||||
|
vec2 ppos = u_particles[i].xy;
|
||||||
|
float pradius = u_particles[i].z;
|
||||||
|
// Particle position in aspect-corrected space
|
||||||
|
vec2 pp = vec2(ppos.x * aspect, ppos.y);
|
||||||
|
float d = length(p - pp);
|
||||||
|
float g = pradius / (d * d * 800.0 + pradius);
|
||||||
|
particleGlow += g;
|
||||||
|
// Alternate color based on index parity (use float comparison)
|
||||||
|
particleColor += g * step(0.5, fract(float(i) * 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mix particle colors: accent and white-ish accent
|
||||||
|
vec3 pCol = mix(u_accent, mix(u_accent, vec3(1.0), 0.5), particleColor / max(particleGlow, 0.001));
|
||||||
|
result += pCol * particleGlow * 0.5;
|
||||||
|
|
||||||
gl_FragColor = vec4(result, 1.0);
|
gl_FragColor = vec4(result, 1.0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let _canvas, _gl, _prog;
|
let _canvas, _gl, _prog;
|
||||||
let _uTime, _uRes, _uAccent, _uBg;
|
let _uTime, _uRes, _uAccent, _uBg, _uParticles;
|
||||||
let _raf = null;
|
let _raf = null;
|
||||||
let _startTime = 0;
|
let _startTime = 0;
|
||||||
let _accent = [76 / 255, 175 / 255, 80 / 255];
|
let _accent = [76 / 255, 175 / 255, 80 / 255];
|
||||||
let _bgColor = [26 / 255, 26 / 255, 26 / 255];
|
let _bgColor = [26 / 255, 26 / 255, 26 / 255];
|
||||||
|
|
||||||
|
// Particle state (CPU-side, positions in 0..1 UV space)
|
||||||
|
const _particles = [];
|
||||||
|
|
||||||
|
function _initParticles() {
|
||||||
|
_particles.length = 0;
|
||||||
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
|
_particles.push({
|
||||||
|
x: Math.random(),
|
||||||
|
y: Math.random(),
|
||||||
|
vx: (Math.random() - 0.5) * 0.0002,
|
||||||
|
vy: 0.0001 + Math.random() * 0.0003,
|
||||||
|
r: 0.003 + Math.random() * 0.006,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateParticles() {
|
||||||
|
for (const p of _particles) {
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
// Wrap around
|
||||||
|
if (p.y > 1.05) { p.y = -0.05; p.x = Math.random(); }
|
||||||
|
if (p.x < -0.05) p.x = 1.05;
|
||||||
|
if (p.x > 1.05) p.x = -0.05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _compile(gl, type, src) {
|
function _compile(gl, type, src) {
|
||||||
const s = gl.createShader(type);
|
const s = gl.createShader(type);
|
||||||
gl.shaderSource(s, src);
|
gl.shaderSource(s, src);
|
||||||
@@ -126,15 +174,17 @@ function _initGL() {
|
|||||||
_uRes = gl.getUniformLocation(_prog, 'u_res');
|
_uRes = gl.getUniformLocation(_prog, 'u_res');
|
||||||
_uAccent = gl.getUniformLocation(_prog, 'u_accent');
|
_uAccent = gl.getUniformLocation(_prog, 'u_accent');
|
||||||
_uBg = gl.getUniformLocation(_prog, 'u_bg');
|
_uBg = gl.getUniformLocation(_prog, 'u_bg');
|
||||||
|
_uParticles = [];
|
||||||
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
|
_uParticles.push(gl.getUniformLocation(_prog, `u_particles[${i}]`));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resize() {
|
function _resize() {
|
||||||
// Render at half resolution for performance
|
const w = Math.round(window.innerWidth * 0.5);
|
||||||
const dpr = Math.min(window.devicePixelRatio || 1, 1);
|
const h = Math.round(window.innerHeight * 0.5);
|
||||||
const w = Math.round(window.innerWidth * dpr * 0.5);
|
|
||||||
const h = Math.round(window.innerHeight * dpr * 0.5);
|
|
||||||
_canvas.width = w;
|
_canvas.width = w;
|
||||||
_canvas.height = h;
|
_canvas.height = h;
|
||||||
if (_gl) _gl.viewport(0, 0, w, h);
|
if (_gl) _gl.viewport(0, 0, w, h);
|
||||||
@@ -145,12 +195,19 @@ function _draw(time) {
|
|||||||
const gl = _gl;
|
const gl = _gl;
|
||||||
if (!gl) return;
|
if (!gl) return;
|
||||||
|
|
||||||
|
_updateParticles();
|
||||||
|
|
||||||
const t = (time - _startTime) * 0.001;
|
const t = (time - _startTime) * 0.001;
|
||||||
gl.uniform1f(_uTime, t);
|
gl.uniform1f(_uTime, t);
|
||||||
gl.uniform2f(_uRes, _canvas.width, _canvas.height);
|
gl.uniform2f(_uRes, _canvas.width, _canvas.height);
|
||||||
gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]);
|
gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]);
|
||||||
gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]);
|
gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]);
|
||||||
|
|
||||||
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
|
const p = _particles[i];
|
||||||
|
gl.uniform3f(_uParticles[i], p.x, p.y, p.r);
|
||||||
|
}
|
||||||
|
|
||||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +215,7 @@ function _start() {
|
|||||||
if (_raf) return;
|
if (_raf) return;
|
||||||
if (!_gl && !_initGL()) return;
|
if (!_gl && !_initGL()) return;
|
||||||
_resize();
|
_resize();
|
||||||
|
_initParticles();
|
||||||
_startTime = performance.now();
|
_startTime = performance.now();
|
||||||
_raf = requestAnimationFrame(_draw);
|
_raf = requestAnimationFrame(_draw);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user