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
+1 -1
View File
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- **Liquid aurora hero**: WebGL2 domain-warped FBM shader — slow green/teal/violet fluid, dithering, pointer pull, offscreen pause, and reduced-motion static frame (CSS gradient fallback). / **液态极光 Hero**WebGL2 domain-warped FBM 着色器——缓慢绿/青/紫流体、抖动去色带、轻微指针牵引、离屏暂停、减少动态模式下静态帧(CSS 渐变回退 - **Wave Gradient hero**: Stripe-style animated mesh gradient (vendored MIT `wave-gradient`) with OSG green/teal/violet palettes, reduced-motion freeze, and CSS fallback. / **Wave Gradient Hero**:Stripe 风格动态网格渐变(本地托管 MIT `wave-gradient`),配 OSG 绿/青/紫配色,支持减少动态模式冻结与 CSS 回退。
- **App Store download badges**: official badges on bilingual READMEs (hero + Get sections) and the website, with China-store `/cn/` links for Chinese and regionless App Store links for English. / **App Store 下载徽章**:中英文 README(顶部与获取区)与官网挂载官方徽章;中文链 `/cn/`,英文用无地区链接。 - **App Store download badges**: official badges on bilingual READMEs (hero + Get sections) and the website, with China-store `/cn/` links for Chinese and regionless App Store links for English. / **App Store 下载徽章**:中英文 README(顶部与获取区)与官网挂载官方徽章;中文链 `/cn/`,英文用无地区链接。
- **Website SEO & AI discovery**: richer meta/JSON-LD/FAQ, `llms.txt`, expanded sitemap, English landing, plus install / compare / Mac-dictation content pages. / **官网 SEO 与 AI 发现**:强化 meta/JSON-LD/FAQ、`llms.txt`、sitemap,并新增英文落地页与安装 / 对比 / Mac 听写专题页。 - **Website SEO & AI discovery**: richer meta/JSON-LD/FAQ, `llms.txt`, expanded sitemap, English landing, plus install / compare / Mac-dictation content pages. / **官网 SEO 与 AI 发现**:强化 meta/JSON-LD/FAQ、`llms.txt`、sitemap,并新增英文落地页与安装 / 对比 / Mac 听写专题页。
- **Context-aware polish safeguards**: polish can use a redacted cursor-neighborhood snapshot for natural continuation, validates protected terms and identifiers, retries once, and falls back to a conservative local cleanup when needed. / **上下文润色护栏**:润色可使用经截断脱敏的光标附近文字自然衔接,并校验受保护词与标识符;失败时重试一次,仍不合格则降级为本地保守清理。 - **Context-aware polish safeguards**: polish can use a redacted cursor-neighborhood snapshot for natural continuation, validates protected terms and identifiers, retries once, and falls back to a conservative local cleanup when needed. / **上下文润色护栏**:润色可使用经截断脱敏的光标附近文字自然衔接,并校验受保护词与标识符;失败时重试一次,仍不合格则降级为本地保守清理。
+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; position: absolute;
inset: 0; inset: 0;
background: background:
linear-gradient(180deg, transparent 52%, var(--hero-wash) 94%), linear-gradient(180deg, transparent 58%, var(--hero-wash) 96%),
radial-gradient(ellipse 80% 55% at 50% -5%, transparent 35%, var(--hero-wash) 100%); radial-gradient(ellipse 90% 60% at 50% -8%, transparent 40%, color-mix(in srgb, var(--hero-wash) 55%, transparent) 100%);
opacity: 0.9; opacity: 0.72;
pointer-events: none; pointer-events: none;
} }
.hero .wrap { position: relative; z-index: 1; } .hero .wrap { position: relative; z-index: 1; }
@@ -854,6 +854,7 @@
.hero-aurora-canvas { transition: none; } .hero-aurora-canvas { transition: none; }
} }
</style> </style>
<script src="assets/js/wave-gradient.js" defer></script>
</head> </head>
<body> <body>
<header class="nav"> <header class="nav">
@@ -1575,317 +1576,98 @@
}); });
} }
// Liquid Aurora — domain-warped FBM WebGL2 shader (vanilla, no deps) // Stripe-style Wave Gradient (vendored MIT: sa3dany/wave-gradient)
const liquidAurora = (() => { const waveHero = (() => {
const wrap = document.getElementById("heroAurora"); const wrap = document.getElementById("heroAurora");
const canvas = document.getElementById("heroAuroraCanvas"); 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 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 = { const palettes = {
dark: { dark: ["#0b1c14", "#1f7a45", "#3ecf8e", "#2ec4b6", "#6e8cff"],
base: [0.02, 0.035, 0.04], light: ["#e7f3eb", "#2f9a52", "#5ec4a8", "#4aa8d8", "#8b7cf0"]
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
}
}; };
let palette = palettes.dark; let gradient = null;
let raf = 0;
let running = false;
let visible = true; let visible = true;
let start = performance.now(); let currentMode = "dark";
let pointer = [0.5, 0.45];
let targetPointer = [0.5, 0.45];
const dprCap = reduceMotion ? 1 : Math.min(window.devicePixelRatio || 1, 1.5);
function applyUniforms(timeSec) { function destroy() {
gl.uniform2f(uni.res, canvas.width, canvas.height); if (!gradient) return;
gl.uniform1f(uni.time, timeSec); try { gradient.destroy(); } catch (_) {}
gl.uniform1f(uni.speed, reduceMotion ? 0.0 : 0.22); gradient = null;
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 resize() { function create(mode) {
const rect = wrap.getBoundingClientRect(); if (typeof window.WaveGradient !== "function") return false;
const w = Math.max(1, Math.floor(rect.width * dprCap)); currentMode = mode;
const h = Math.max(1, Math.floor(rect.height * dprCap)); const colors = palettes[mode] || palettes.dark;
if (canvas.width !== w || canvas.height !== h) { // Ensure layout size before WebGL reads clientWidth/Height
canvas.width = w; canvas.style.width = "100%";
canvas.height = h; 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) { function setPalette(mode) {
palette = mode === "light" ? palettes.light : palettes.dark; if (mode === currentMode && gradient) return;
if (reduceMotion) { create(mode === "light" ? "light" : "dark");
resize();
applyUniforms(8.5);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
} }
resize(); function boot() {
setPalette(resolvedTheme()); const ok = create(resolvedTheme());
applyUniforms(reduceMotion ? 8.5 : 0); if (!ok) return;
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");
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(() => { document.addEventListener("visibilitychange", () => {
resize(); if (!gradient || reduceMotion) return;
if (reduceMotion) { gradient.shouldRender = visible && !document.hidden;
applyUniforms(8.5); if (gradient.shouldRender) {
gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame((now) => gradient.render(now));
} }
}); });
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 (document.readyState === "loading") {
if (document.hidden) stopLoop(); document.addEventListener("DOMContentLoaded", boot, { once: true });
else if (visible && !reduceMotion) startLoop(); } else {
}); // wave-gradient.js is deferred; wait a tick for global
setTimeout(boot, 0);
}
return { return { setPalette };
setPalette,
destroy() {
stopLoop();
ro.disconnect();
io.disconnect();
hero.removeEventListener("pointermove", onPointer);
}
};
})(); })();
function applyTheme(theme) { function applyTheme(theme) {
@@ -1896,7 +1678,7 @@ void main(){
} }
try { localStorage.setItem("osg-theme", theme); } catch (_) {} try { localStorage.setItem("osg-theme", theme); } catch (_) {}
applyShots(); applyShots();
liquidAurora.setPalette(resolvedTheme()); waveHero.setPalette(resolvedTheme());
} }
function cycleTheme() { function cycleTheme() {
@@ -1969,7 +1751,7 @@ void main(){
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if ((document.documentElement.getAttribute("data-theme") || "system") === "system") { if ((document.documentElement.getAttribute("data-theme") || "system") === "system") {
applyShots(); applyShots();
liquidAurora.setPalette(resolvedTheme()); waveHero.setPalette(resolvedTheme());
} }
}); });
} catch (_) {} } catch (_) {}