| name | webgl-laser |
| description | 创建一个固定的全屏 WebGL 激光背景特效:一根极细的白热垂直核心、克制的品牌色光晕,以及环绕光束的柔和烟雾。仅用于激光背景特效,不用于整页布局、文案、通用主视觉场景、粒子或无关的动效系统。 |
WebGL 激光
范围
- 仅应用于激光背景特效。
- 使用位于 DOM 之后的固定全屏 canvas。
- 在 canvas 上设置
pointer-events: none。
- 让页面内容处于更高的堆叠上下文。
- 让光晕与烟雾匹配页面的主强调色或最强强调色。
视觉目标
- 极细垂直光束:清晰的白热内核、狭窄的彩色光晕。
- 氛围烟雾:集中在光束周围的柔和云状散碎。
- 暗色电影场:克制、品牌染色,且在内容背后可读。
- 缓慢脉动:辉光轻柔呼吸;无激进闪烁或颜色循环。
- 光刃质感:狭窄而精确,绝非粗厚的霓虹柱。
分层
<canvas class="laser-canvas" data-webgl-laser></canvas>
<main class="page-content">
...
</main>
.laser-canvas {
position: fixed;
inset: 0;
z-index: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
}
.page-content {
position: relative;
z-index: 1;
}
品牌色
用产品强调色作为源色。着色器让核心接近白色,并由该色推导出光晕/烟雾。
function hexToRgb01(hex) {
const clean = hex.replace("#", "").trim();
const value = clean.length === 3
? clean.split("").map((char) => char + char).join("")
: clean;
return [
parseInt(value.slice(0, 2), 16) / 255,
parseInt(value.slice(2, 4), 16) / 255,
parseInt(value.slice(4, 6), 16) / 255,
];
}
const accent = getComputedStyle(document.documentElement)
.getPropertyValue("--brand-accent")
.trim() || "#ff4d8d";
原生 WebGL 初始化
除非当前文件已使用其他渲染器,否则优先用带全屏四边形的原生 WebGL。
const laserVertexShader = `
attribute vec2 a_position;
varying vec2 v_uv;
void main() {
v_uv = a_position * 0.5 + 0.5;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const laserFragmentShader = `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec3 u_color;
uniform float u_xOffset;
uniform float u_coreWidth;
uniform float u_glowWidth;
uniform float u_smokeDensity;
varying vec2 v_uv;
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);
vec2 u = f * f * (3.0 - 2.0 * f);
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));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 5; i++) {
value += amplitude * noise(p);
p *= 2.02;
amplitude *= 0.5;
}
return value;
}
void main() {
vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);
vec2 p = (v_uv - 0.5) * aspect;
float x = p.x - u_xOffset;
float distanceToBeam = abs(x);
float core = exp(-pow(distanceToBeam / u_coreWidth, 2.0));
float glow = exp(-pow(distanceToBeam / u_glowWidth, 1.45));
float scatter = exp(-pow(distanceToBeam / (u_glowWidth * 5.5), 1.25));
float pulse = 0.9 + 0.1 * sin(u_time * 1.15);
vec2 fogUv = p * 3.1 + vec2(0.0, -u_time * 0.035);
fogUv.x += sin(p.y * 3.5 + u_time * 0.11) * 0.14;
float fogBase = fbm(fogUv);
float fogFine = fbm(p * 8.0 + vec2(sin(u_time * 0.07) * 0.35, u_time * 0.05));
float fog = smoothstep(0.30, 0.86, fogBase * 0.72 + fogFine * 0.28);
float smoke = fog * scatter * u_smokeDensity;
vec3 brand = clamp(u_color, 0.0, 1.0);
vec3 haloColor = mix(brand, vec3(1.0), 0.16);
vec3 smokeColor = mix(brand, vec3(0.55), 0.28) * 0.55;
vec3 hotCore = vec3(1.0, 0.96, 0.90);
vec3 color = vec3(0.006, 0.007, 0.010);
color += smokeColor * smoke;
color += haloColor * glow * 0.46 * pulse;
color += hotCore * core * 1.35;
float vignette = smoothstep(1.25, 0.18, length(p));
color *= vignette;
float alpha = clamp(smoke * 0.72 + glow * 0.68 + core, 0.0, 1.0);
gl_FragColor = vec4(color, alpha);
}
`;
初始化器
在 resize 时保持 u_resolution 同步,并通过 u_time 驱动动画。
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
}
return shader;
}
function createProgram(gl, vertexSource, fragmentSource) {
const program = gl.createProgram();
gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexSource));
gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fragmentSource));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
}
return program;
}
function initWebGLLaser(canvas, options = {}) {
if (!canvas) return () => {};
const gl = canvas.getContext(, {
: ,
: ,
: ,
});
(!gl) {};
program = (gl, laserVertexShader, laserFragmentShader);
positionBuffer = gl.();
positions = ([
-, -,
, -,
-, ,
-, ,
, -,
, ,
]);
gl.(gl., positionBuffer);
gl.(gl., positions, gl.);
gl.(program);
positionLocation = gl.(program, );
uniforms = {
: gl.(program, ),
: gl.(program, ),
: gl.(program, ),
: gl.(program, ),
: gl.(program, ),
: gl.(program, ),
: gl.(program, ),
};
gl.(positionLocation);
gl.(positionLocation, , gl., , , );
gl.(gl.);
gl.(gl., gl.);
color = options. || (accent);
reduceMotion = .().;
width = ;
height = ;
rafId = ;
() {
dpr = .(. || , options. || );
width = .(, .);
height = .(, .);
canvas. = .(width * dpr);
canvas. = .(height * dpr);
gl.(, , canvas., canvas.);
}
() {
gl.(program);
gl.(uniforms., canvas., canvas.);
gl.(uniforms., time * );
gl.(uniforms., color[], color[], color[]);
gl.(uniforms., options. || );
gl.(uniforms., options. || );
gl.(uniforms., options. || );
gl.(uniforms., options. || );
gl.(, , , );
gl.(gl.);
gl.(gl., , );
(!reduceMotion) rafId = (render);
}
() {
();
();
}
();
();
.(, handleResize);
{
(rafId);
.(, handleResize);
gl.(positionBuffer);
gl.(program);
};
}
cleanupLaser = (.(), {
: (accent),
: ,
: ,
: ,
: ,
: ,
});
调节旋钮
- 光束位置:调整
xOffset;保持在保持宽高比的居中 UV 空间内。
- 光束粗细:把
coreWidth 与 glowWidth 分开调;让核心极细。
- 颜色:由品牌强调色推导
color,再在着色器里柔化光晕与烟雾。
- 烟雾密度:调节
smokeDensity、FBM 缩放、漂移速度、散射宽度与边缘衰减。
- 性能:先减少 FBM 倍频或限制
maxDpr,再改动视觉结构。
审美准则
- 最热的光束核心保持接近白色。
- 光晕与烟雾使用设计的主强调色或最强强调色。
- 烟雾在光束附近绽放并向外消散。
- 脉动仅影响辉光;避免快速闪烁。
- 内容可读性优先于 bloom、雾霾或电影感戏剧性。
避免
- 在设计使用其他主色时硬编码蓝色。
- 把光束做得粗到读作发光条。
- 不集中在光束周围的通用全屏雾。
- 把特效变成 Three.js 场景、粒子爆炸或多彩霓虹背景。
- 让 canvas 拦截指针事件。
- 用密雾或极端 bloom 冲淡前景 UI。