docs(site): switch hero to React Bits–style beams

Replace the wave gradient with a vanilla Three.js beams field using the
shared demo preset and OSG green lighting for a more dimensional look.
This commit is contained in:
Rocky
2026-07-29 23:12:49 +08:00
parent bf4492ee2f
commit 38a76e9217
6 changed files with 438 additions and 402 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
- **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 回退 - **Beams hero**: radiant noise-warped beam array (React Bitsstyle, vanilla Three.js) with the shared demo preset (26 beams, 208° rotation) and OSG green light. / **Beams Hero**:放射状噪声光束阵列(React Bits 风格,原生 Three.js),采用分享的演示预设(26 束、208° 旋转)与 OSG 绿色灯光
- **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. / **上下文润色护栏**:润色可使用经截断脱敏的光标附近文字自然衔接,并校验受保护词与标识符;失败时重试一次,仍不合格则降级为本地保守清理。
+373
View File
@@ -0,0 +1,373 @@
/*!
* OSGKeyboard hero beams — vanilla Three.js recreation inspired by
* React Bits "Beams" (https://reactbits.dev/backgrounds/beams).
* Parameters match the shared demo preset.
* three.js is MIT; this file is project code.
*/
(function (global) {
"use strict";
const NOISE = `
float random (in vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
float noise (in vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(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;
}
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
float cnoise(vec3 P){
vec3 Pi0 = floor(P);
vec3 Pi1 = Pi0 + vec3(1.0);
Pi0 = mod(Pi0, 289.0);
Pi1 = mod(Pi1, 289.0);
vec3 Pf0 = fract(P);
vec3 Pf1 = Pf0 - vec3(1.0);
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
vec4 iy = vec4(Pi0.yy, Pi1.yy);
vec4 iz0 = Pi0.zzzz;
vec4 iz1 = Pi1.zzzz;
vec4 ixy = permute(permute(ix) + iy);
vec4 ixy0 = permute(ixy + iz0);
vec4 ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 / 7.0;
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
vec4 sz0 = step(gz0, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
vec4 gx1 = ixy1 / 7.0;
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
vec4 sz1 = step(gz1, vec4(0.0));
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));
g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;
vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));
g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;
float n000 = dot(g000, Pf0);
float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));
float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));
float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));
float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));
float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));
float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));
float n111 = dot(g111, Pf1);
vec3 fade_xyz = fade(Pf0);
vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);
vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);
float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);
return 2.2 * n_xyz;
}
`;
function hexToNormalizedRGB(hex) {
const clean = String(hex).replace("#", "");
const full = clean.length === 3
? clean.split("").map((c) => c + c).join("")
: clean;
return [
parseInt(full.substring(0, 2), 16) / 255,
parseInt(full.substring(2, 4), 16) / 255,
parseInt(full.substring(4, 6), 16) / 255
];
}
function extendMaterial(THREE, BaseMaterial, cfg) {
const physical = THREE.ShaderLib.physical;
const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;
const baseDefines = physical.defines || {};
const uniforms = THREE.UniformsUtils.clone(baseUniforms);
const defaults = new BaseMaterial(cfg.material || {});
if (defaults.color) uniforms.diffuse.value = defaults.color;
if ("roughness" in defaults) uniforms.roughness.value = defaults.roughness;
if ("metalness" in defaults) uniforms.metalness.value = defaults.metalness;
if ("envMap" in defaults) uniforms.envMap.value = defaults.envMap;
if ("envMapIntensity" in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;
Object.entries(cfg.uniforms || {}).forEach(([key, u]) => {
uniforms[key] = u !== null && typeof u === "object" && "value" in u ? u : { value: u };
});
let vert = `${cfg.header}\n${cfg.vertexHeader || ""}\n${baseVert}`;
let frag = `${cfg.header}\n${cfg.fragmentHeader || ""}\n${baseFrag}`;
for (const [inc, code] of Object.entries(cfg.vertex || {})) {
vert = vert.replace(inc, `${inc}\n${code}`);
}
for (const [inc, code] of Object.entries(cfg.fragment || {})) {
frag = frag.replace(inc, `${inc}\n${code}`);
}
return new THREE.ShaderMaterial({
defines: { ...baseDefines },
uniforms,
vertexShader: vert,
fragmentShader: frag,
lights: true,
fog: !!(cfg.material && cfg.material.fog)
});
}
function createStackedPlanesBufferGeometry(THREE, n, width, height, spacing, heightSegments) {
const geometry = new THREE.BufferGeometry();
const numVertices = n * (heightSegments + 1) * 2;
const numFaces = n * heightSegments * 2;
const positions = new Float32Array(numVertices * 3);
const indices = new Uint32Array(numFaces * 3);
const uvs = new Float32Array(numVertices * 2);
let vertexOffset = 0;
let indexOffset = 0;
let uvOffset = 0;
const totalWidth = n * width + (n - 1) * spacing;
const xOffsetBase = -totalWidth / 2;
for (let i = 0; i < n; i++) {
const xOffset = xOffsetBase + i * (width + spacing);
const uvXOffset = Math.random() * 300;
const uvYOffset = Math.random() * 300;
for (let j = 0; j <= heightSegments; j++) {
const y = height * (j / heightSegments - 0.5);
positions.set([xOffset, y, 0, xOffset + width, y, 0], vertexOffset * 3);
const uvY = j / heightSegments;
uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);
if (j < heightSegments) {
const a = vertexOffset;
const b = vertexOffset + 1;
const c = vertexOffset + 2;
const d = vertexOffset + 3;
indices.set([a, b, c, c, b, d], indexOffset);
indexOffset += 6;
}
vertexOffset += 2;
uvOffset += 4;
}
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeVertexNormals();
return geometry;
}
/**
* @param {HTMLCanvasElement} canvas
* @param {object} options
*/
function create(canvas, options) {
const THREE = global.THREE;
if (!THREE || !canvas) throw new Error("THREE or canvas missing");
const {
beamWidth = 4.5,
beamHeight = 6,
beamNumber = 26,
lightColor = "#7CFFB2",
speed = 4.5,
noiseIntensity = 2.65,
scale = 0.62,
rotation = 208,
background = "#000000",
reduceMotion = false,
heightSegments = 100
} = options || {};
const scene = new THREE.Scene();
scene.background = new THREE.Color(background);
const camera = new THREE.PerspectiveCamera(30, 1, 0.1, 100);
camera.position.set(0, 0, 20);
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: false,
powerPreference: "low-power"
});
renderer.setClearColor(background, 1);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const material = extendMaterial(THREE, THREE.MeshStandardMaterial, {
header: `
varying vec3 vEye;
varying float vNoise;
varying vec2 vUv;
varying vec3 vPosition;
uniform float time;
uniform float uSpeed;
uniform float uNoiseIntensity;
uniform float uScale;
${NOISE}`,
vertexHeader: `
float getPos(vec3 pos) {
vec3 noisePos =
vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;
return cnoise(noisePos);
}
vec3 getCurrentPos(vec3 pos) {
vec3 newpos = pos;
newpos.z += getPos(pos);
return newpos;
}
vec3 getNormal(vec3 pos) {
vec3 curpos = getCurrentPos(pos);
vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));
vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));
vec3 tangentX = normalize(nextposX - curpos);
vec3 tangentZ = normalize(nextposZ - curpos);
return normalize(cross(tangentZ, tangentX));
}`,
vertex: {
"#include <begin_vertex>": "transformed.z += getPos(transformed.xyz);",
"#include <beginnormal_vertex>": "objectNormal = getNormal(position.xyz);"
},
fragment: {
"#include <dithering_fragment>": `
float randomNoise = noise(gl_FragCoord.xy);
gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`
},
material: { fog: true },
uniforms: {
diffuse: new THREE.Color(...hexToNormalizedRGB("#000000")),
time: { value: 0 },
roughness: 0.3,
metalness: 0.3,
uSpeed: { value: speed },
envMapIntensity: 10,
uNoiseIntensity: { value: noiseIntensity },
uScale: { value: scale }
}
});
const geometry = createStackedPlanesBufferGeometry(
THREE,
beamNumber,
beamWidth,
beamHeight,
0,
heightSegments
);
const mesh = new THREE.Mesh(geometry, material);
const group = new THREE.Group();
group.rotation.z = THREE.MathUtils.degToRad(rotation);
group.add(mesh);
const dir = new THREE.DirectionalLight(lightColor, 1);
dir.position.set(0, 3, 10);
group.add(dir);
scene.add(group);
scene.add(new THREE.AmbientLight(0xffffff, 1));
let playing = !reduceMotion;
let visible = true;
let raf = 0;
let last = performance.now();
let disposed = false;
function resize() {
const parent = canvas.parentElement || canvas;
const rect = parent.getBoundingClientRect();
const w = Math.max(1, Math.floor(rect.width));
const h = Math.max(1, Math.floor(rect.height));
const dpr = Math.min(window.devicePixelRatio || 1, reduceMotion ? 1 : 1.75);
renderer.setPixelRatio(dpr);
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function frame(now) {
if (disposed) return;
raf = requestAnimationFrame(frame);
if (!playing || !visible || document.hidden) return;
const delta = Math.min(0.05, (now - last) / 1000);
last = now;
material.uniforms.time.value += 0.1 * delta;
renderer.render(scene, camera);
}
function renderOnce() {
renderer.render(scene, camera);
}
resize();
// Seed a bit of displacement so the first frame isn't flat
material.uniforms.time.value = reduceMotion ? 2.4 : 0.2;
renderOnce();
if (!reduceMotion) {
last = performance.now();
raf = requestAnimationFrame(frame);
}
const ro = new ResizeObserver(() => {
resize();
renderOnce();
});
ro.observe(canvas.parentElement || canvas);
return {
setPlaying(next) {
playing = !!next && !reduceMotion;
if (playing && !raf) {
last = performance.now();
raf = requestAnimationFrame(frame);
}
},
setVisible(next) {
visible = !!next;
if (visible && playing && !raf) {
last = performance.now();
raf = requestAnimationFrame(frame);
}
if (visible) renderOnce();
},
setLightColor(hex) {
dir.color.set(hex);
renderOnce();
},
setBackground(hex) {
scene.background = new THREE.Color(hex);
renderer.setClearColor(hex, 1);
renderOnce();
},
resize,
destroy() {
disposed = true;
cancelAnimationFrame(raf);
raf = 0;
ro.disconnect();
geometry.dispose();
material.dispose();
renderer.dispose();
}
};
}
global.OSGBeamsHero = { create };
})(typeof window !== "undefined" ? window : globalThis);
@@ -1,6 +1,6 @@
MIT License The MIT License
Copyright (c) 2022 Mohamed ElSaadany Copyright © 2010-2023 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in
copies or substantial portions of the Software. all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
SOFTWARE. THE SOFTWARE.
+7
View File
File diff suppressed because one or more lines are too long
-341
View File
@@ -1,341 +0,0 @@
/*! 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;
+51 -54
View File
@@ -355,14 +355,14 @@
.hero-aurora.is-ready .hero-aurora-canvas { opacity: 1; } .hero-aurora.is-ready .hero-aurora-canvas { opacity: 1; }
.hero-aurora-fallback { .hero-aurora-fallback {
position: absolute; position: absolute;
inset: -20% -10%; inset: -10%;
background: background:
radial-gradient(ellipse 55% 45% at 18% 28%, var(--aurora-a), transparent 70%), linear-gradient(208deg, #000 0%, #04140c 35%, #0a1f14 55%, #000 100%),
radial-gradient(ellipse 50% 40% at 82% 22%, var(--aurora-b), transparent 68%), repeating-linear-gradient(208deg,
radial-gradient(ellipse 48% 42% at 52% 62%, var(--aurora-c), transparent 72%), transparent 0 18px,
radial-gradient(ellipse 40% 35% at 30% 78%, var(--aurora-d), transparent 70%); rgba(124, 255, 178, 0.08) 18px 22px,
filter: blur(40px); transparent 22px 40px);
opacity: 0.85; opacity: 0.95;
} }
.hero-aurora.is-ready .hero-aurora-fallback { opacity: 0; } .hero-aurora.is-ready .hero-aurora-fallback { opacity: 0; }
.hero-aurora::after { .hero-aurora::after {
@@ -854,7 +854,8 @@
.hero-aurora-canvas { transition: none; } .hero-aurora-canvas { transition: none; }
} }
</style> </style>
<script src="assets/js/wave-gradient.js" defer></script> <script src="assets/js/three.min.js" defer></script>
<script src="assets/js/beams-hero.js" defer></script>
</head> </head>
<body> <body>
<header class="nav"> <header class="nav">
@@ -1576,65 +1577,69 @@
}); });
} }
// Stripe-style Wave Gradient (vendored MIT: sa3dany/wave-gradient) // React Bitsstyle Beams (vanilla Three.js), preset from shared demo URL
const waveHero = (() => { const beamsHero = (() => {
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() {} }; if (!wrap || !canvas) return { setPalette() {} };
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const isMobile = window.matchMedia("(max-width: 720px)").matches;
const palettes = { const palettes = {
dark: ["#0b1c14", "#1f7a45", "#3ecf8e", "#2ec4b6", "#6e8cff"], dark: { light: "#7CFFB2", bg: "#000000" },
light: ["#e7f3eb", "#2f9a52", "#5ec4a8", "#4aa8d8", "#8b7cf0"] light: { light: "#2f9a52", bg: "#0a1210" }
}; };
let gradient = null; let instance = null;
let visible = true; let currentMode = "";
let currentMode = "dark";
function destroy() { function destroy() {
if (!gradient) return; if (!instance) return;
try { gradient.destroy(); } catch (_) {} try { instance.destroy(); } catch (_) {}
gradient = null; instance = null;
} }
function create(mode) { function create(mode) {
if (typeof window.WaveGradient !== "function") return false; if (typeof window.OSGBeamsHero?.create !== "function" || typeof window.THREE === "undefined") {
return false;
}
currentMode = mode; currentMode = mode;
const colors = palettes[mode] || palettes.dark; const palette = palettes[mode] || palettes.dark;
// Ensure layout size before WebGL reads clientWidth/Height
canvas.style.width = "100%"; canvas.style.width = "100%";
canvas.style.height = "100%"; canvas.style.height = "100%";
try { try {
destroy(); destroy();
gradient = new window.WaveGradient(canvas, { instance = window.OSGBeamsHero.create(canvas, {
colors, beamNumber: 26,
seed: mode === "light" ? 3 : 7, beamWidth: 4.5,
speed: reduceMotion ? 0 : 0.55, beamHeight: 6,
fps: reduceMotion ? 1 : 24, speed: 4.5,
amplitude: 280, noiseIntensity: 2.65,
density: [0.06, 0.16], scale: 0.62,
time: reduceMotion ? 420000 : 0 rotation: 208,
lightColor: palette.light,
background: palette.bg,
reduceMotion,
heightSegments: isMobile ? 64 : 100
}); });
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"); wrap.classList.add("is-ready");
return true; return true;
} catch (err) { } catch (err) {
console.warn("[wave-gradient]", err); console.warn("[beams]", err);
destroy(); destroy();
return false; return false;
} }
} }
function setPalette(mode) { function setPalette(mode) {
if (mode === currentMode && gradient) return; const next = mode === "light" ? "light" : "dark";
create(mode === "light" ? "light" : "dark"); if (next === currentMode && instance) {
const palette = palettes[next];
instance.setLightColor(palette.light);
instance.setBackground(palette.bg);
return;
}
create(next);
} }
function boot() { function boot() {
@@ -1642,28 +1647,20 @@
if (!ok) return; if (!ok) return;
const io = new IntersectionObserver((entries) => { const io = new IntersectionObserver((entries) => {
visible = entries.some((e) => e.isIntersecting); const visible = entries.some((e) => e.isIntersecting);
if (!gradient || reduceMotion) return; instance?.setVisible(visible);
gradient.shouldRender = visible && !document.hidden; instance?.setPlaying(visible && !document.hidden && !reduceMotion);
if (gradient.shouldRender && typeof requestAnimationFrame === "function") {
requestAnimationFrame((now) => gradient.render(now));
}
}, { threshold: 0.01 }); }, { threshold: 0.01 });
io.observe(wrap); io.observe(wrap);
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (!gradient || reduceMotion) return; instance?.setPlaying(!document.hidden && !reduceMotion);
gradient.shouldRender = visible && !document.hidden;
if (gradient.shouldRender) {
requestAnimationFrame((now) => gradient.render(now));
}
}); });
} }
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true }); document.addEventListener("DOMContentLoaded", () => setTimeout(boot, 0), { once: true });
} else { } else {
// wave-gradient.js is deferred; wait a tick for global
setTimeout(boot, 0); setTimeout(boot, 0);
} }
@@ -1678,7 +1675,7 @@
} }
try { localStorage.setItem("osg-theme", theme); } catch (_) {} try { localStorage.setItem("osg-theme", theme); } catch (_) {}
applyShots(); applyShots();
waveHero.setPalette(resolvedTheme()); beamsHero.setPalette(resolvedTheme());
} }
function cycleTheme() { function cycleTheme() {
@@ -1751,7 +1748,7 @@
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();
waveHero.setPalette(resolvedTheme()); beamsHero.setPalette(resolvedTheme());
} }
}); });
} catch (_) {} } catch (_) {}