docs(site): switch hero to Stripe-style wave gradient

Replace the custom liquid aurora shader with the MIT wave-gradient
mesh animation, vendored locally and tinted for OSG brand colors.
This commit is contained in:
Rocky
2026-07-29 22:27:36 +08:00
parent 85455ab575
commit 4b378d1b90
4 changed files with 439 additions and 295 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Mohamed ElSaadany
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+341
View File
@@ -0,0 +1,341 @@
/*! wave-gradient (MIT) https://github.com/sa3dany/wave-gradient — Copyright (c) 2022 Mohamed ElSaadany */
"use strict";
var WaveGradientModule = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/wave-gradient.js
var wave_gradient_exports = {};
__export(wave_gradient_exports, {
WaveGradient: () => WaveGradient
});
// src/shaders.js
var vert = `#version 300 es
vec3 o(vec3 i,vec3 c,float r){return c*r+i*(1.-r);}vec3 o(vec3 n){return n-floor(n*(1./289.))*289.;}vec4 o(vec4 n){return n-floor(n*(1./289.))*289.;}vec4 e(vec4 n){return o((n*34.+1.)*n);}vec4 v(vec4 y){return 1.79284291400159-.85373472095314*y;}float t(vec3 l){const vec2 s=vec2(1./6.,1./3.);const vec4 u=vec4(0.,.5,1.,2.);vec3 a=floor(l+dot(l,s.yyy)),x=l-a+dot(a,s.xxx),d=step(x.yzx,x.xyz),f=1.-d,z=min(d.xyz,f.zxy),w=max(d.xyz,f.zxy),m=x-z+s.xxx,C=x-w+s.yyy,p=x-u.yyy;a=o(a);vec4 P=e(e(e(a.z+vec4(0.,z.z,w.z,1.))+a.y+vec4(0.,z.y,w.y,1.))+a.x+vec4(0.,z.x,w.x,1.));vec3 S=.142857142857*u.wyz-u.xzx;vec4 L=P-49.*floor(P*S.z*S.z),F=floor(L*S.z),R=floor(L-7.*F),n=F*S.x+S.yyyy,W=R*S.x+S.yyyy,b=1.-abs(n)-abs(W),G=vec4(n.xy,W.xy),q=vec4(n.zw,W.zw),h=floor(G)*2.+1.,g=floor(q)*2.+1.,O=-step(b,vec4(0.)),B=G.xzyw+h.xzyw*O.xxyy,A=q.xzyw+g.xzyw*O.zzww;vec3 E=vec3(B.xy,b.x),Z=vec3(B.zw,b.y),Y=vec3(A.xy,b.z),X=vec3(A.zw,b.w);vec4 V=v(vec4(dot(E,E),dot(Z,Z),dot(Y,Y),dot(X,X)));E*=V.x;Z*=V.y;Y*=V.z;X*=V.w;vec4 U=max(.6-vec4(dot(x,x),dot(m,m),dot(C,C),dot(p,p)),0.);U=U*U;return 42.*dot(U*U,vec4(dot(E,x),dot(Z,m),dot(Y,C),dot(X,p)));}uniform mediump vec2 u_Resolution;uniform float u_Amplitude,u_Realtime,u_Seed;uniform vec3 u_BaseColor;uniform int u_LayerCount;uniform struct WaveLayers{float noiseCeil;float noiseFloor;float noiseFlow;float noiseSeed;float noiseSpeed;vec2 noiseFreq;vec3 color;} u_WaveLayers[9];in vec3 a_Position;out vec3 v_Color;void main(){float T=u_Realtime*5e-6;vec2 Q=vec2(.00014,.00029),N=u_Resolution*a_Position.xy*Q;float M=u_Amplitude*(2./u_Resolution.y),K=t(vec3(N.x*3.+T*3.,N.y*4.,T*10.+u_Seed));K*=1.-pow(abs(a_Position.y),2.);K=max(0.,K);gl_Position=vec4(a_Position.x,a_Position.y+K*M,a_Position.z,1.);v_Color=u_BaseColor;for(int a=0;a<u_LayerCount;a++){WaveLayers J=u_WaveLayers[a];float K=t(vec3(N.x*J.noiseFreq.x+T*J.noiseFlow,N.y*J.noiseFreq.y,T*J.noiseSpeed+J.noiseSeed));K=K/2.+.5;K=smoothstep(J.noiseFloor,J.noiseCeil,K);v_Color=o(v_Color,J.color,pow(K,4.));}}
`;
var frag = `#version 300 es
precision mediump float;uniform vec2 u_Resolution;uniform float u_ShadowPower;in vec3 v_Color;out vec4 color;void main(){vec2 I=gl_FragCoord.xy/u_Resolution.xy;color=vec4(v_Color,1.);color.y-=pow(I.y+sin(-12.)*I.x,u_ShadowPower)*.4;}
`;
// src/wave-gradient.js
function parseRGB(hex) {
const result = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) || hex.match(/^#?([a-f\d])([a-f\d])([a-f\d])$/i);
return result ? result.slice(1, 4).map((c) => {
return parseInt(c.length < 2 ? c + c : c, 16) / 255;
}) : null;
}
var ClipSpace = class {
static createPlaneGeometry(widthSegments, depthSegments) {
const gridX = Math.ceil(widthSegments);
const gridZ = Math.ceil(depthSegments);
const vertexCount = 3 * (gridX + 1) * (gridZ + 1);
const indexCount = 3 * 2 * gridX * gridZ;
const positions = new ArrayBuffer(4 * vertexCount);
const indices = new ArrayBuffer(4 * indexCount);
for (let z = gridZ, i = 0, view = new DataView(positions); z >= 0; z--) {
const v = z / gridZ;
const clipY = v * 2 - 1;
for (let x = gridX; x >= 0; x--, i += 3) {
const clipX = x / gridX * 2 - 1;
view.setFloat32((i + 0) * 4, clipX, true);
view.setFloat32((i + 1) * 4, clipY, true);
view.setFloat32((i + 2) * 4, v, true);
}
}
const verticesAcross = gridX + 1;
for (let z = 0, i = 0, view = new DataView(indices); z < gridZ; z++) {
for (let x = 0; x < gridX; x++, i += 6) {
view.setUint32((i + 0) * 4, (z + 0) * verticesAcross + x, true);
view.setUint32((i + 1) * 4, (z + 0) * verticesAcross + x + 1, true);
view.setUint32((i + 2) * 4, (z + 1) * verticesAcross + x, true);
view.setUint32((i + 3) * 4, (z + 0) * verticesAcross + x + 1, true);
view.setUint32((i + 4) * 4, (z + 1) * verticesAcross + x + 1, true);
view.setUint32((i + 5) * 4, (z + 1) * verticesAcross + x, true);
}
}
return { positions, indices, count: indexCount };
}
static prefixName(name, prefix) {
return `${prefix}${name[0].toUpperCase()}${name.slice(1)}`;
}
constructor(config) {
this.gl = config.gl;
this.program = this.createProgram(config.shaders);
this._attributes = {};
this.setupAttributes(config.attributes);
this._elementBuffer;
this.setElements(config.elements);
this._uniforms = {};
this.setupUniforms(config.uniforms);
}
compileShader(type, source) {
const { gl } = this;
let shader = gl.createShader(type);
if (!shader)
throw new Error("can't create shader");
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
debugProgram(program) {
const { gl } = this;
const [vs, fs] = gl.getAttachedShaders(program) ?? [];
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(
`can't link WebGL program.
${gl.getProgramInfoLog(program)}
${gl.getShaderInfoLog(vs)}
${gl.getShaderInfoLog(fs)}`
);
}
}
createProgram(shaders) {
const { gl } = this;
const [vs, fs] = [
this.compileShader(gl.VERTEX_SHADER, shaders[0]),
this.compileShader(gl.FRAGMENT_SHADER, shaders[1])
];
const program = gl.createProgram();
if (!program)
throw new Error("can't create WebGL program");
gl.attachShader(program, vs);
gl.attachShader(program, fs);
try {
gl.linkProgram(program);
this.debugProgram(program);
} catch (linkError) {
gl.deleteProgram(program);
throw linkError;
} finally {
gl.deleteShader(vs);
gl.deleteShader(fs);
}
gl.useProgram(program);
return program;
}
createBuffer() {
const { gl } = this;
const buffer = gl.createBuffer();
if (!buffer)
throw new Error("can't create buffer");
return buffer;
}
setupAttributes(attributes) {
const { gl, program } = this;
for (const [name, dataBuffer] of Object.entries(attributes)) {
const prefixedName = ClipSpace.prefixName(name, "a_");
const buffer = this.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, dataBuffer, gl.STATIC_DRAW);
const location = gl.getAttribLocation(program, prefixedName);
gl.enableVertexAttribArray(location);
gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
this._attributes[name] = { buffer, location };
}
}
setAttribute(attributeName, dataBuffer) {
const { gl } = this;
gl.bufferData(gl.ARRAY_BUFFER, dataBuffer, gl.STATIC_DRAW);
}
setElements(elements) {
const { gl } = this;
if (!this._elementBuffer) {
const buffer = this.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
this._elementBuffer = buffer;
}
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, elements, gl.STATIC_DRAW);
}
createUniformSetter(name, type, initialValue) {
const { gl, program } = this;
const uniformX = `uniform${type}`;
const location = gl.getUniformLocation(program, name);
const setter = (value) => {
Array.isArray(value) ? gl[uniformX](location, ...value) : gl[uniformX](location, value);
};
if (initialValue)
setter(initialValue);
return setter;
}
setupUniforms(uniforms) {
for (const [name, uniform] of Object.entries(uniforms)) {
const prefixedName = ClipSpace.prefixName(name, "u_");
switch (uniform.type) {
case void 0:
Array.isArray(uniform.value) && uniform.value.forEach(
(member, i) => {
const structName = name;
const prefixedStructName = prefixedName;
for (const [name2, uniform2] of Object.entries(member)) {
const key = `${structName}[${i}].${name2}`;
const prefixedKey = `${prefixedStructName}[${i}].${name2}`;
this._uniforms[key] = this.createUniformSetter(
prefixedKey,
uniform2.type,
uniform2.value
);
}
}
);
break;
default:
this._uniforms[name] = this.createUniformSetter(
prefixedName,
uniform.type,
uniform.value
);
}
}
}
setUniform(uniformName, newValue) {
this._uniforms[uniformName](newValue);
}
delete() {
const { gl } = this;
gl.deleteProgram(this.program);
for (const [, attribute] of Object.entries(this._attributes)) {
this.gl.deleteBuffer(attribute.buffer);
}
}
};
var WaveGradient = class {
constructor(canvas, options) {
const gl = canvas.getContext("webgl2", {
antialias: true,
depth: false,
powerPreference: "low-power"
});
if (!gl)
throw new Error("can't get WebGL2 context");
const {
amplitude = 320,
colors = ["#ef008f", "#6ec3f4", "#7038ff", "#ffba27"],
density = [0.06, 0.16],
fps = 24,
seed = 0,
speed = 1.25,
time = 0,
wireframe = false
} = options ?? {};
const { clientWidth, clientHeight } = canvas;
canvas.width = clientWidth;
canvas.height = clientHeight;
gl.viewport(0, 0, clientWidth, clientHeight);
gl.enable(gl.CULL_FACE);
gl.disable(gl.DITHER);
gl.disable(gl.DEPTH_TEST);
const geometry = ClipSpace.createPlaneGeometry(
clientWidth * density[0],
clientHeight * density[1]
);
const clipSpace = new ClipSpace({
gl,
shaders: [vert, frag],
attributes: { position: geometry.positions },
elements: geometry.indices,
uniforms: {
amplitude: { value: amplitude, type: "1f" },
baseColor: { value: parseRGB(colors[0]), type: "3f" },
realtime: { value: time, type: "1f" },
resolution: { value: [clientWidth, clientHeight], type: "2f" },
seed: { value: seed, type: "1f" },
shadowPower: { value: 6, type: "1f" },
layerCount: { value: colors.length - 1, type: "1i" },
waveLayers: {
value: colors.slice(1).map((color, i, array) => {
const r = (i + 1) / array.length + 1;
return {
noiseCeil: { value: 0.63 + 0.07 * (i + 1), type: "1f" },
noiseFloor: { value: 0.1, type: "1f" },
noiseFlow: { value: 6.5 + 0.3 * (i + 1), type: "1f" },
noiseSeed: { value: seed + 10 * (i + 1), type: "1f" },
noiseSpeed: { value: 11 + 0.3 * (i + 1), type: "1f" },
noiseFreq: { value: [2 + r, 3 + r], type: "2f" },
color: { value: parseRGB(color), type: "3f" }
};
})
}
}
});
this.gl = gl;
this.clipSpace = clipSpace;
this.density = density;
this.speed = speed;
this.frameInterval = 1e3 / fps;
this.lastFrameTime = 0;
this.shouldRender = true;
this.drawMode = wireframe ? this.gl.LINES : this.gl.TRIANGLES;
this.drawCount = geometry.count;
this.time = time;
requestAnimationFrame((now) => {
this.render(now);
});
}
resize() {
const { gl, gl: { canvas }, clipSpace } = this;
const { width, clientWidth, height, clientHeight } = canvas;
const resized = width !== clientWidth || height !== clientHeight;
if (resized) {
canvas.width = clientWidth;
canvas.height = clientHeight;
gl.viewport(0, 0, clientWidth, clientHeight);
this.clipSpace.setUniform("resolution", [clientWidth, clientHeight]);
const geometry = ClipSpace.createPlaneGeometry(
clientWidth * this.density[0],
clientHeight * this.density[1]
);
clipSpace.setAttribute("position", geometry.positions);
clipSpace.setElements(geometry.indices);
this.drawCount = geometry.count;
}
}
render(now) {
if (this.shouldRender) {
requestAnimationFrame((now2) => {
this.render(now2);
});
} else {
return;
}
const delta = now - this.lastFrameTime;
if (delta < this.frameInterval) {
if (Math.random() > 0.75 === true)
this.resize();
return;
}
this.lastFrameTime = now - delta % this.frameInterval;
this.time += Math.min(delta, this.frameInterval) * this.speed;
this.clipSpace.setUniform("realtime", this.time);
this.gl.drawElements(
this.drawMode,
this.drawCount,
this.gl.UNSIGNED_INT,
0
);
}
destroy() {
this.clipSpace.delete();
delete this.gl;
this.shouldRender = false;
}
};
return __toCommonJS(wave_gradient_exports);
})();
window.WaveGradient = WaveGradientModule.WaveGradient;
+76 -294
View File
@@ -370,9 +370,9 @@
position: absolute;
inset: 0;
background:
linear-gradient(180deg, transparent 52%, var(--hero-wash) 94%),
radial-gradient(ellipse 80% 55% at 50% -5%, transparent 35%, var(--hero-wash) 100%);
opacity: 0.9;
linear-gradient(180deg, transparent 58%, var(--hero-wash) 96%),
radial-gradient(ellipse 90% 60% at 50% -8%, transparent 40%, color-mix(in srgb, var(--hero-wash) 55%, transparent) 100%);
opacity: 0.72;
pointer-events: none;
}
.hero .wrap { position: relative; z-index: 1; }
@@ -854,6 +854,7 @@
.hero-aurora-canvas { transition: none; }
}
</style>
<script src="assets/js/wave-gradient.js" defer></script>
</head>
<body>
<header class="nav">
@@ -1575,317 +1576,98 @@
});
}
// Liquid Aurora — domain-warped FBM WebGL2 shader (vanilla, no deps)
const liquidAurora = (() => {
// Stripe-style Wave Gradient (vendored MIT: sa3dany/wave-gradient)
const waveHero = (() => {
const wrap = document.getElementById("heroAurora");
const canvas = document.getElementById("heroAuroraCanvas");
if (!wrap || !canvas) return { setPalette() {}, destroy() {} };
if (!wrap || !canvas) return { setPalette() {} };
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const gl = canvas.getContext("webgl2", {
alpha: true,
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: true,
powerPreference: "low-power"
});
if (!gl) return { setPalette() {}, destroy() {} };
const VERT = `#version 300 es
in vec2 a_pos;
void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }`;
const FRAG = `#version 300 es
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_contrast;
uniform float u_warp;
uniform float u_scale;
uniform float u_intensity;
uniform vec2 u_pointer;
uniform float u_pointerStrength;
uniform vec3 u_base;
uniform vec3 u_deep;
uniform vec3 u_colorA;
uniform vec3 u_colorB;
uniform vec3 u_hot;
out vec4 fragColor;
float hash(vec2 p){
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float noise(vec2 p){
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
float fbm(vec2 p){
float v = 0.0;
float a = 0.5;
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for (int i = 0; i < 5; i++) {
v += a * noise(p);
p = m * p;
a *= 0.5;
}
return v;
}
void main(){
vec2 uv = gl_FragCoord.xy / u_res;
float aspect = u_res.x / max(u_res.y, 1.0);
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
p *= u_scale;
// Soft pointer pull
vec2 ptr = (u_pointer - 0.5) * vec2(aspect, 1.0);
p += (ptr - p) * u_pointerStrength * 0.08;
float t = u_time * u_speed;
// Domain warp — liquid marble / aurora veins
vec2 q = vec2(fbm(p + vec2(0.0, t * 0.15)), fbm(p + vec2(5.2, -t * 0.12)));
vec2 r = vec2(
fbm(p + u_warp * q + vec2(1.7, 9.2) + t * 0.07),
fbm(p + u_warp * q + vec2(8.3, 2.8) - t * 0.05)
);
float f = fbm(p + u_warp * r);
// Extra shimmer ribbon
float ribbon = fbm(p * 1.4 + r * 1.8 + vec2(t * 0.04, -t * 0.06));
float veins = smoothstep(0.35, 0.78, f + ribbon * 0.25);
veins = pow(veins, mix(1.8, 0.85, clamp(u_contrast - 0.6, 0.0, 1.0)));
vec3 col = u_base;
col = mix(col, u_deep, smoothstep(0.15, 0.55, f));
col = mix(col, u_colorA, veins * 0.95);
col = mix(col, u_colorB, smoothstep(0.55, 0.92, ribbon) * veins);
col = mix(col, u_hot, pow(smoothstep(0.72, 0.98, f * ribbon), 2.2) * 0.65);
// Soft vignette so content stays readable
float vig = 1.0 - smoothstep(0.35, 1.15, length((uv - 0.5) * vec2(1.05, 1.25)));
col *= mix(0.55, 1.0, vig);
col *= u_intensity;
// Blue-noise-ish dither to kill banding
float dither = (hash(gl_FragCoord.xy + fract(t * 13.7)) - 0.5) / 255.0;
col += dither;
// Fade toward bottom for hero fade-out
float bottom = smoothstep(1.0, 0.55, uv.y);
float alpha = clamp(0.35 + veins * 0.75, 0.0, 0.95) * bottom;
fragColor = vec4(col * alpha, alpha);
}`;
function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.warn("[aurora]", gl.getShaderInfoLog(s));
gl.deleteShader(s);
return null;
}
return s;
}
const vs = compile(gl.VERTEX_SHADER, VERT);
const fs = compile(gl.FRAGMENT_SHADER, FRAG);
if (!vs || !fs) return { setPalette() {}, destroy() {} };
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.warn("[aurora]", gl.getProgramInfoLog(program));
return { setPalette() {}, destroy() {} };
}
gl.useProgram(program);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, 1, -1, -1, 1,
-1, 1, 1, -1, 1, 1
]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(program, "a_pos");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const uni = {
res: gl.getUniformLocation(program, "u_res"),
time: gl.getUniformLocation(program, "u_time"),
speed: gl.getUniformLocation(program, "u_speed"),
contrast: gl.getUniformLocation(program, "u_contrast"),
warp: gl.getUniformLocation(program, "u_warp"),
scale: gl.getUniformLocation(program, "u_scale"),
intensity: gl.getUniformLocation(program, "u_intensity"),
pointer: gl.getUniformLocation(program, "u_pointer"),
pointerStrength: gl.getUniformLocation(program, "u_pointerStrength"),
base: gl.getUniformLocation(program, "u_base"),
deep: gl.getUniformLocation(program, "u_deep"),
colorA: gl.getUniformLocation(program, "u_colorA"),
colorB: gl.getUniformLocation(program, "u_colorB"),
hot: gl.getUniformLocation(program, "u_hot")
};
const palettes = {
dark: {
base: [0.02, 0.035, 0.04],
deep: [0.04, 0.12, 0.14],
colorA: [0.18, 0.78, 0.52],
colorB: [0.15, 0.72, 0.88],
hot: [0.72, 0.62, 1.0],
intensity: 1.15,
contrast: 1.35
},
light: {
base: [0.93, 0.95, 0.94],
deep: [0.78, 0.90, 0.86],
colorA: [0.22, 0.62, 0.42],
colorB: [0.25, 0.68, 0.72],
hot: [0.55, 0.48, 0.85],
intensity: 0.92,
contrast: 1.15
}
dark: ["#0b1c14", "#1f7a45", "#3ecf8e", "#2ec4b6", "#6e8cff"],
light: ["#e7f3eb", "#2f9a52", "#5ec4a8", "#4aa8d8", "#8b7cf0"]
};
let palette = palettes.dark;
let raf = 0;
let running = false;
let gradient = null;
let visible = true;
let start = performance.now();
let pointer = [0.5, 0.45];
let targetPointer = [0.5, 0.45];
const dprCap = reduceMotion ? 1 : Math.min(window.devicePixelRatio || 1, 1.5);
let currentMode = "dark";
function applyUniforms(timeSec) {
gl.uniform2f(uni.res, canvas.width, canvas.height);
gl.uniform1f(uni.time, timeSec);
gl.uniform1f(uni.speed, reduceMotion ? 0.0 : 0.22);
gl.uniform1f(uni.contrast, palette.contrast);
gl.uniform1f(uni.warp, 1.55);
gl.uniform1f(uni.scale, 1.85);
gl.uniform1f(uni.intensity, palette.intensity);
gl.uniform2f(uni.pointer, pointer[0], pointer[1]);
gl.uniform1f(uni.pointerStrength, reduceMotion ? 0.0 : 0.55);
gl.uniform3fv(uni.base, palette.base);
gl.uniform3fv(uni.deep, palette.deep);
gl.uniform3fv(uni.colorA, palette.colorA);
gl.uniform3fv(uni.colorB, palette.colorB);
gl.uniform3fv(uni.hot, palette.hot);
function destroy() {
if (!gradient) return;
try { gradient.destroy(); } catch (_) {}
gradient = null;
}
function resize() {
const rect = wrap.getBoundingClientRect();
const w = Math.max(1, Math.floor(rect.width * dprCap));
const h = Math.max(1, Math.floor(rect.height * dprCap));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
function create(mode) {
if (typeof window.WaveGradient !== "function") return false;
currentMode = mode;
const colors = palettes[mode] || palettes.dark;
// Ensure layout size before WebGL reads clientWidth/Height
canvas.style.width = "100%";
canvas.style.height = "100%";
try {
destroy();
gradient = new window.WaveGradient(canvas, {
colors,
seed: mode === "light" ? 3 : 7,
speed: reduceMotion ? 0 : 0.55,
fps: reduceMotion ? 1 : 24,
amplitude: 280,
density: [0.06, 0.16],
time: reduceMotion ? 420000 : 0
});
if (reduceMotion) {
// Freeze after first paints by destroying the loop's continuation
setTimeout(() => {
if (!gradient) return;
gradient.shouldRender = false;
}, 80);
}
wrap.classList.add("is-ready");
return true;
} catch (err) {
console.warn("[wave-gradient]", err);
destroy();
return false;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
function frame(now) {
if (!running) return;
raf = requestAnimationFrame(frame);
if (!visible || document.hidden) return;
pointer[0] += (targetPointer[0] - pointer[0]) * 0.04;
pointer[1] += (targetPointer[1] - pointer[1]) * 0.04;
const t = (now - start) / 1000;
applyUniforms(reduceMotion ? 8.5 : t);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function startLoop() {
if (running) return;
running = true;
start = performance.now() - (reduceMotion ? 8500 : 0);
raf = requestAnimationFrame(frame);
}
function stopLoop() {
running = false;
cancelAnimationFrame(raf);
}
function setPalette(mode) {
palette = mode === "light" ? palettes.light : palettes.dark;
if (reduceMotion) {
resize();
applyUniforms(8.5);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
if (mode === currentMode && gradient) return;
create(mode === "light" ? "light" : "dark");
}
resize();
setPalette(resolvedTheme());
applyUniforms(reduceMotion ? 8.5 : 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);
wrap.classList.add("is-ready");
function boot() {
const ok = create(resolvedTheme());
if (!ok) return;
if (!reduceMotion) startLoop();
const io = new IntersectionObserver((entries) => {
visible = entries.some((e) => e.isIntersecting);
if (!gradient || reduceMotion) return;
gradient.shouldRender = visible && !document.hidden;
if (gradient.shouldRender && typeof requestAnimationFrame === "function") {
requestAnimationFrame((now) => gradient.render(now));
}
}, { threshold: 0.01 });
io.observe(wrap);
const ro = new ResizeObserver(() => {
resize();
if (reduceMotion) {
applyUniforms(8.5);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
});
ro.observe(wrap);
const io = new IntersectionObserver((entries) => {
visible = entries.some((e) => e.isIntersecting);
if (!reduceMotion) {
if (visible) startLoop();
else stopLoop();
}
}, { threshold: 0.01 });
io.observe(wrap);
const hero = wrap.closest(".hero") || wrap;
const onPointer = (e) => {
const rect = hero.getBoundingClientRect();
targetPointer[0] = (e.clientX - rect.left) / Math.max(rect.width, 1);
targetPointer[1] = 1.0 - (e.clientY - rect.top) / Math.max(rect.height, 1);
};
if (!reduceMotion && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
hero.addEventListener("pointermove", onPointer, { passive: true });
document.addEventListener("visibilitychange", () => {
if (!gradient || reduceMotion) return;
gradient.shouldRender = visible && !document.hidden;
if (gradient.shouldRender) {
requestAnimationFrame((now) => gradient.render(now));
}
});
}
document.addEventListener("visibilitychange", () => {
if (document.hidden) stopLoop();
else if (visible && !reduceMotion) startLoop();
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
// wave-gradient.js is deferred; wait a tick for global
setTimeout(boot, 0);
}
return {
setPalette,
destroy() {
stopLoop();
ro.disconnect();
io.disconnect();
hero.removeEventListener("pointermove", onPointer);
}
};
return { setPalette };
})();
function applyTheme(theme) {
@@ -1896,7 +1678,7 @@ void main(){
}
try { localStorage.setItem("osg-theme", theme); } catch (_) {}
applyShots();
liquidAurora.setPalette(resolvedTheme());
waveHero.setPalette(resolvedTheme());
}
function cycleTheme() {
@@ -1969,7 +1751,7 @@ void main(){
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if ((document.documentElement.getAttribute("data-theme") || "system") === "system") {
applyShots();
liquidAurora.setPalette(resolvedTheme());
waveHero.setPalette(resolvedTheme());
}
});
} catch (_) {}