Laser
A laser beam sits near the bottom of the viewport. Scroll and new content prints in from behind it, hot and shimmering.
Install
npx shadcn@latest add @canvas-ui/laser-reactOr copy the source below into your project.
Code
"use client";
import {
useEffect,
useRef,
useState,
useSyncExternalStore,
type ReactNode,
} from "react";
export interface LaserOptions {
/** Animation speed of the beam wave, flicker, and sparkle. 1 is normal. */
speed?: number;
/** Distance of the beam from the bottom edge in CSS pixels. */
offset?: number;
/** Laser glow color as RGB in the 0 to 1 range. */
color?: [number, number, number];
/** Thickness of the white-hot beam core in CSS pixels. */
thickness?: number;
/** Intensity of the white beam core (0 to 2). 0 removes it. */
core?: number;
/** Reach of the colored glow around the beam in CSS pixels. */
radius?: number;
/** Brightness of the colored glow (0 to 3). 0 removes it. */
glow?: number;
/** Amplitude of the slow beam waviness in CSS pixels. */
wave?: number;
/** Beam length as a fraction of the content width (0 to 1). */
width?: number;
/** Random intensity flicker of the beam (0 to 1). */
flicker?: number;
/** Height of the hot reveal band above the beam in CSS pixels. */
reveal?: number;
/** How strongly freshly revealed content glows (0 to 1.5). */
heat?: number;
/** Heat shimmer displacement of freshly revealed content in CSS pixels. */
shimmer?: number;
/** Animated sparkle texture inside the reveal band (0 to 2). */
sparkle?: number;
/** How much scrolling boosts the beam and the reveal glow (0 to 3). */
reactivity?: number;
}
export interface LaserElements {
/** Canvas with layoutsubtree that hosts the HTML content. */
source: HTMLCanvasElement;
/** The element inside the source canvas that gets captured. */
content: HTMLElement;
/** Canvas the WebGL effect renders to. */
output: HTMLCanvasElement;
}
export interface LaserInstance {
/** Update effect options live. */
setOptions: (options: LaserOptions) => void;
/** Re-read canvas size. Call when the element is resized. */
resize: () => void;
/** Stop the loop and release all GPU resources. */
destroy: () => void;
}
const DEFAULTS: Required<LaserOptions> = {
speed: 0.3,
offset: 140,
color: [0.05, 0.35, 1],
thickness: 6,
core: 1,
radius: 20,
glow: 2,
wave: 10,
width: 0.55,
flicker: 0.2,
reveal: 400,
heat: 1.5,
shimmer: 12,
sparkle: 0.25,
reactivity: 1,
};
type PaintableCanvas = HTMLCanvasElement & {
onpaint?: (() => void) | null;
requestPaint?: () => void;
};
type ElementImageContext = CanvasRenderingContext2D & {
drawElementImage?: (element: Element, x: number, y: number) => void;
};
const VERT = `#version 300 es
precision highp float;
layout(location = 0) in vec2 aPos;
out vec2 vUv;
void main () {
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}`;
const FRAG = `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 outColor;
uniform sampler2D uContent;
uniform vec2 uResolution;
uniform float uTime;
uniform float uBeamY;
uniform float uWaveAmp;
uniform float uBeamCX;
uniform float uBeamHalfW;
uniform float uHalfCore;
uniform float uCore;
uniform float uRadius;
uniform float uGlow;
uniform vec3 uColor;
uniform float uBright;
uniform float uRevealH;
uniform float uHeat;
uniform float uShimmer;
uniform float uSparkle;
uniform vec3 uBezel;
uniform float uMaxX;
uniform float uHasContent;
float hash (vec2 v) {
return fract(sin(dot(v, vec2(89.44, 19.36))) * 22189.22);
}
float iHash (vec2 v, vec2 r) {
float h00 = hash(floor(v * r + vec2(0.0, 0.0)) / r);
float h10 = hash(floor(v * r + vec2(1.0, 0.0)) / r);
float h01 = hash(floor(v * r + vec2(0.0, 1.0)) / r);
float h11 = hash(floor(v * r + vec2(1.0, 1.0)) / r);
vec2 ip = smoothstep(vec2(0.0), vec2(1.0), mod(v * r, 1.0));
return (h00 * (1.0 - ip.x) + h10 * ip.x) * (1.0 - ip.y)
+ (h01 * (1.0 - ip.x) + h11 * ip.x) * ip.y;
}
float noise (vec2 v) {
float sum = 0.0;
float s = 2.0;
for (int i = 1; i < 7; i++) {
sum += iHash(v + vec2(i), vec2(2.0 * s)) / s;
s *= 2.0;
}
return sum;
}
vec3 permute (vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }
float snoise (vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m = m * m;
m = m * m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
float fbm (vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 3; i++) {
v += a * snoise(p);
p = mat2(1.6, 1.2, -1.2, 1.6) * p + 11.7;
a *= 0.5;
}
return v * 0.5 + 0.5;
}
float smokeField (vec2 p, float t) {
vec2 rise = vec2(t * 0.04, -t * 0.35);
vec2 q = vec2(
fbm(p + rise),
fbm(p + rise * 0.85 + vec2(5.2, 1.3)));
return fbm(p + 0.55 * q + rise);
}
vec4 page (vec2 p) {
p.x = clamp(p.x, 0.0005, uMaxX - 0.0005);
p.y = clamp(p.y, 0.0005, 0.9995);
return texture(uContent, vec2(p.x, 1.0 - p.y));
}
void main () {
vec2 uv = vUv;
if (uv.x > uMaxX) {
outColor = vec4(0.0);
return;
}
float t = uTime;
float nx = (uv.x - uBeamCX) / max(uBeamHalfW, 1e-4);
float env = pow(max(1.0 - nx * nx, 0.0), 1.5);
float bend = 0.0;
if (uWaveAmp > 0.0) {
bend = (noise(vec2(uv.x * 2.5 + t * 0.6, t * 0.4)) - 0.5) * 2.0 * uWaveAmp;
}
float yb = uBeamY + bend;
float dy = uv.y - yb;
float pxd = abs(dy) * uResolution.y;
vec3 col;
float alpha;
if (dy < 0.0 && uHasContent > 0.5) {
col = uBezel;
alpha = 1.0;
} else {
col = vec3(0.0);
alpha = 0.0;
}
if (dy >= 0.0 && dy < uRevealH && uRevealH > 0.0) {
float k = dy / uRevealH;
float w = exp(-3.0 * k) * (1.0 - smoothstep(0.55, 1.0, k));
vec2 sp = uv * uResolution / uResolution.y;
if (uShimmer > 0.0 && env > 0.0 && uHasContent > 0.5) {
float dx = snoise(vec2(sp.x * 5.0, sp.y * 12.0 - t * 1.3))
* uShimmer * w * env;
float sa = w * env;
col = page(vec2(uv.x + dx, uv.y)).rgb * sa;
alpha = sa;
}
if (uHeat > 0.0 && env > 0.0) {
float s = smoothstep(0.3, 1.05, smokeField(sp * vec2(2.4, 3.4), t));
float heat = w * uHeat * env * (0.4 + 0.9 * s);
if (uSparkle > 0.0) {
float f = fbm(sp * vec2(9.0, 13.0) + vec2(0.0, -t * 1.4));
heat *= 1.0 + uSparkle * (f - 0.5) * 1.3;
}
heat = max(heat, 0.0);
vec3 emission = uColor * heat * uBright + vec3(heat * heat * 0.35);
vec3 toned = (1.0 - exp(-emission)) * env;
float ea = max(max(toned.r, toned.g), toned.b);
col = toned + col * (1.0 - ea);
alpha = ea + alpha * (1.0 - ea);
}
}
vec3 beam = vec3(0.0);
if (env > 0.0) {
float pd = pxd / max(env, 0.18);
if (uCore > 0.0) {
beam += 10.0 * uCore * smoothstep(uHalfCore, uHalfCore * 0.3, pd) * vec3(1.0);
}
if (uGlow > 0.0) {
float g = pow(uRadius / max(pd, 0.75), 0.9) * exp(-0.55 * pd / uRadius);
beam += uGlow * g * uColor;
}
beam *= uBright;
}
vec3 beamToned = (1.0 - exp(-beam)) * env;
float ba = max(max(beamToned.r, beamToned.g), beamToned.b);
if (ba > 0.003) {
col = beamToned + col * (1.0 - ba);
alpha = ba + alpha * (1.0 - ba);
}
outColor = vec4(col, clamp(alpha, 0.0, 1.0));
}`;
export function supportsHtmlInCanvas(): boolean {
if (typeof document === "undefined") return false;
const probe = document.createElement("canvas") as PaintableCanvas;
const ctx = probe.getContext("2d") as ElementImageContext | null;
return Boolean(
ctx &&
typeof ctx.drawElementImage === "function" &&
typeof probe.requestPaint === "function",
);
}
export function createLaser(
elements: LaserElements,
options: LaserOptions = {},
): LaserInstance | null {
const config = { ...DEFAULTS, ...options };
const { source, content, output } = elements;
const gl = output.getContext("webgl2", {
alpha: true,
depth: false,
stencil: false,
antialias: false,
premultipliedAlpha: true,
});
if (!gl || gl.isContextLost()) return null;
const sourceCtx = source.getContext("2d") as ElementImageContext | null;
const paintable = source as PaintableCanvas;
const htmlInCanvas = Boolean(
sourceCtx &&
typeof sourceCtx.drawElementImage === "function" &&
typeof paintable.requestPaint === "function",
);
let contentDirty = false;
let wake = () => {};
if (htmlInCanvas) {
paintable.onpaint = () => {
try {
sourceCtx!.reset();
sourceCtx!.drawElementImage!(content, 0, 0);
contentDirty = true;
wake();
} catch {}
};
}
function compile(type: number, text: string): WebGLShader {
const shader = gl!.createShader(type)!;
gl!.shaderSource(shader, text);
gl!.compileShader(shader);
if (!gl!.getShaderParameter(shader, gl!.COMPILE_STATUS)) {
console.error("Laser shader error:", gl!.getShaderInfoLog(shader));
}
return shader;
}
const vertexShader = compile(gl.VERTEX_SHADER, VERT);
const fragmentShader = compile(gl.FRAGMENT_SHADER, FRAG);
const program = gl.createProgram()!;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const uniforms: Record<string, WebGLUniformLocation> = {};
const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < count; i++) {
const info = gl.getActiveUniform(program, i)!;
uniforms[info.name] = gl.getUniformLocation(program, info.name)!;
}
const quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
const contentTexture = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, contentTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
1,
1,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 0, 0]),
);
let contentMaxX = 1;
let beamCX = 0.5;
let beamSpan = 1;
let bezel: [number, number, number] = [0, 0, 0];
const bezelProbe = document.createElement("canvas");
bezelProbe.width = bezelProbe.height = 1;
const bezelCtx = bezelProbe.getContext("2d", { willReadFrequently: true });
function syncBezelColor() {
if (!bezelCtx) return;
let el: Element | null = content;
while (el) {
const bg = getComputedStyle(el).backgroundColor;
if (bg && bg !== "transparent") {
bezelCtx.clearRect(0, 0, 1, 1);
bezelCtx.fillStyle = bg;
bezelCtx.fillRect(0, 0, 1, 1);
const [r, g, b, a] = bezelCtx.getImageData(0, 0, 1, 1).data;
if (a > 0) {
bezel = [r / 255, g / 255, b / 255];
return;
}
}
el = el.parentElement;
}
bezel = [0, 0, 0];
}
function syncCanvasSize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.max(1, Math.round(output.clientWidth * dpr));
const height = Math.max(1, Math.round(output.clientHeight * dpr));
if (output.width !== width || output.height !== height) {
output.width = width;
output.height = height;
}
contentMaxX = Math.min(
1,
Math.max(0.05, content.clientWidth / Math.max(output.clientWidth, 1)),
);
const viewW = Math.max(output.clientWidth, 1);
beamCX = contentMaxX * 0.5;
beamSpan = contentMaxX;
const child = content.firstElementChild;
if (child instanceof HTMLElement) {
const childRect = child.getBoundingClientRect();
const outputRect = output.getBoundingClientRect();
const style = getComputedStyle(child);
const left =
childRect.left - outputRect.left + (parseFloat(style.paddingLeft) || 0);
const right =
childRect.right -
outputRect.left -
(parseFloat(style.paddingRight) || 0);
if (right - left > 48) {
beamCX = ((left + right) * 0.5) / viewW;
beamSpan = (right - left) / viewW;
}
}
if (htmlInCanvas) {
const cssWidth = Math.max(1, Math.round(source.clientWidth));
const cssHeight = Math.max(1, Math.round(source.clientHeight));
if (source.width !== cssWidth * dpr || source.height !== cssHeight * dpr) {
source.width = cssWidth * dpr;
source.height = cssHeight * dpr;
}
paintable.requestPaint!();
}
}
syncCanvasSize();
syncBezelColor();
function uploadContent() {
if (!htmlInCanvas || !contentDirty) return;
contentDirty = false;
syncBezelColor();
gl!.bindTexture(gl!.TEXTURE_2D, contentTexture);
gl!.texImage2D(
gl!.TEXTURE_2D,
0,
gl!.RGBA,
gl!.RGBA,
gl!.UNSIGNED_BYTE,
source,
);
}
let time = 0;
let activity = 0;
const fract = (x: number) => x - Math.floor(x);
const hash2 = (x: number, y: number) =>
fract(Math.sin(x * 89.44 + y * 19.36) * 22189.22);
const smooth01 = (x: number) => x * x * (3 - 2 * x);
function iHashCpu(vx: number, vy: number, r: number) {
const fx = Math.floor(vx * r);
const fy = Math.floor(vy * r);
const h00 = hash2(fx / r, fy / r);
const h10 = hash2((fx + 1) / r, fy / r);
const h01 = hash2(fx / r, (fy + 1) / r);
const h11 = hash2((fx + 1) / r, (fy + 1) / r);
const ix = smooth01(fract(vx * r));
const iy = smooth01(fract(vy * r));
return (
(h00 * (1 - ix) + h10 * ix) * (1 - iy) + (h01 * (1 - ix) + h11 * ix) * iy
);
}
function noiseCpu(vx: number, vy: number) {
let sum = 0;
let s = 2;
for (let i = 1; i < 7; i++) {
sum += iHashCpu(vx + i, vy + i, 2 * s) / s;
s *= 2;
}
return sum;
}
function render() {
uploadContent();
gl!.useProgram(program);
gl!.activeTexture(gl!.TEXTURE0);
gl!.bindTexture(gl!.TEXTURE_2D, contentTexture);
gl!.uniform1i(uniforms.uContent, 0);
gl!.uniform2f(uniforms.uResolution, output.width, output.height);
gl!.uniform1f(uniforms.uTime, time);
const dpr = output.width / Math.max(output.clientWidth, 1);
const clientH = Math.max(output.clientHeight, 1);
gl!.uniform1f(
uniforms.uBeamY,
Math.min(Math.max(config.offset, 0) / clientH, 0.95),
);
gl!.uniform1f(uniforms.uWaveAmp, Math.max(config.wave, 0) / clientH);
gl!.uniform1f(uniforms.uBeamCX, beamCX);
gl!.uniform1f(
uniforms.uBeamHalfW,
Math.min(Math.max(config.width, 0.05), 1) * beamSpan * 0.5,
);
gl!.uniform1f(
uniforms.uHalfCore,
Math.max(config.thickness, 0.5) * dpr * 0.5,
);
gl!.uniform1f(uniforms.uCore, Math.max(config.core, 0));
gl!.uniform1f(uniforms.uRadius, Math.max(config.radius, 0.5) * dpr);
gl!.uniform1f(uniforms.uGlow, Math.max(config.glow, 0));
gl!.uniform3f(
uniforms.uColor,
config.color[0],
config.color[1],
config.color[2],
);
const flick =
1 - Math.min(Math.max(config.flicker, 0), 1) * noiseCpu(time * 1.8, 3.7);
const boost = 1 + Math.max(config.reactivity, 0) * activity * 1.6;
gl!.uniform1f(uniforms.uBright, flick * boost);
gl!.uniform1f(uniforms.uRevealH, Math.max(config.reveal, 0) / clientH);
gl!.uniform1f(
uniforms.uHeat,
Math.max(config.heat, 0) * (0.3 + 0.7 * activity),
);
gl!.uniform1f(
uniforms.uShimmer,
(Math.max(config.shimmer, 0) / Math.max(output.clientWidth, 1)) *
(0.25 + 0.75 * activity),
);
gl!.uniform1f(uniforms.uSparkle, Math.max(config.sparkle, 0));
gl!.uniform3f(uniforms.uBezel, bezel[0], bezel[1], bezel[2]);
gl!.uniform1f(uniforms.uMaxX, contentMaxX);
gl!.uniform1f(uniforms.uHasContent, htmlInCanvas ? 1 : 0);
gl!.bindFramebuffer(gl!.FRAMEBUFFER, null);
gl!.viewport(0, 0, output.width, output.height);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
}
let raf = 0;
let lastTime = performance.now();
let destroyed = false;
let running = false;
let visible = true;
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
let reducedMotion = motionQuery.matches;
function frame(now: number) {
if (destroyed) return;
if (!visible) {
running = false;
return;
}
const delta = Math.min((now - lastTime) / 1000, 1 / 30);
lastTime = now;
if (!reducedMotion) {
time += delta * config.speed;
activity *= Math.exp(-delta * 2.4);
}
render();
if (reducedMotion && !contentDirty) {
running = false;
return;
}
raf = requestAnimationFrame(frame);
}
function start() {
if (destroyed || running || !visible) return;
running = true;
lastTime = performance.now();
raf = requestAnimationFrame(frame);
}
wake = start;
start();
let lastScrollTop = content.scrollTop;
function onScroll() {
const top = content.scrollTop;
if (!reducedMotion) {
activity = Math.min(1, activity + Math.abs(top - lastScrollTop) / 600);
}
lastScrollTop = top;
start();
}
content.addEventListener("scroll", onScroll, { passive: true });
function onMotionChange() {
reducedMotion = motionQuery.matches;
if (reducedMotion) activity = 0;
start();
}
motionQuery.addEventListener("change", onMotionChange);
const observer = new ResizeObserver(() => {
syncCanvasSize();
start();
});
observer.observe(output);
observer.observe(content);
const intersection = new IntersectionObserver((entries) => {
visible = entries[entries.length - 1]?.isIntersecting ?? true;
if (visible) start();
});
intersection.observe(output);
return {
setOptions(next) {
if (
!Object.entries(next).some(
([key, value]) => config[key as keyof LaserOptions] !== value,
)
)
return;
Object.assign(config, next);
start();
},
resize() {
syncCanvasSize();
start();
},
destroy() {
destroyed = true;
cancelAnimationFrame(raf);
content.removeEventListener("scroll", onScroll);
observer.disconnect();
intersection.disconnect();
motionQuery.removeEventListener("change", onMotionChange);
gl!.deleteTexture(contentTexture);
gl!.deleteProgram(program);
gl!.deleteShader(vertexShader);
gl!.deleteShader(fragmentShader);
gl!.deleteBuffer(quad);
if (htmlInCanvas) paintable.onpaint = null;
},
};
}
export interface LaserProps extends LaserOptions {
children: ReactNode;
className?: string;
style?: React.CSSProperties;
}
const emptySubscribe = () => () => {};
export function Laser({ children, className, style, ...options }: LaserProps) {
const sourceRef = useRef<HTMLCanvasElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const outputRef = useRef<HTMLCanvasElement>(null);
const instanceRef = useRef<LaserInstance | null>(null);
const [initialOptions] = useState(options);
const [failed, setFailed] = useState(false);
const supported = useSyncExternalStore(
emptySubscribe,
supportsHtmlInCanvas,
() => false,
);
const native = supported && !failed;
useEffect(() => {
const source = sourceRef.current;
const content = contentRef.current;
const output = outputRef.current;
if (!source || !content || !output) return;
instanceRef.current = createLaser(
{ source, content, output },
initialOptions,
);
if (native && !instanceRef.current) setFailed(true);
return () => {
instanceRef.current?.destroy();
instanceRef.current = null;
};
}, [initialOptions, native]);
useEffect(() => {
instanceRef.current?.setOptions(options);
});
return (
<div className={className} style={{ position: "relative", ...style }}>
<canvas
ref={sourceRef}
// @ts-expect-error experimental html-in-canvas attribute
layoutsubtree="true"
suppressHydrationWarning
style={
native
? { position: "absolute", inset: 0, width: "100%", height: "100%" }
: { display: "none" }
}
>
{native ? (
<div
ref={contentRef}
style={{
position: "relative",
width: "100%",
height: "100%",
overflow: "auto",
}}
>
{children}
</div>
) : null}
</canvas>
{!native ? (
<div
ref={contentRef}
style={{
position: "relative",
width: "100%",
height: "100%",
overflow: "auto",
}}
>
{children}
</div>
) : null}
<canvas
ref={outputRef}
aria-hidden
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
}}
/>
</div>
);
}
export default Laser;
API reference
| Property | Type | Default |
|---|---|---|
speedAnimation speed of the beam wave, flicker, and sparkle. 1 is normal. | number | 0.3 |
offsetDistance of the beam from the bottom edge in CSS pixels. | number | 140 |
colorLaser glow color as RGB in the 0 to 1 range. | [number, number, number] | [0.05, 0.35, 1] |
thicknessThickness of the white-hot beam core in CSS pixels. | number | 6 |
coreIntensity of the white beam core (0 to 2). 0 removes it. | number | 1 |
radiusReach of the colored glow around the beam in CSS pixels. | number | 20 |
glowBrightness of the colored glow (0 to 3). 0 removes it. | number | 2 |
waveAmplitude of the slow beam waviness in CSS pixels. | number | 10 |
widthBeam length as a fraction of the content width (0 to 1). The beam centers on the visible content column and tapers into a fine point at both ends. | number | 0.55 |
flickerRandom intensity flicker of the beam (0 to 1). | number | 0.2 |
revealHeight of the hot reveal band above the beam in CSS pixels. | number | 400 |
heatHow strongly freshly revealed content glows (0 to 1.5). | number | 1.5 |
shimmerHeat shimmer displacement of freshly revealed content in CSS pixels. | number | 12 |
sparkleAnimated sparkle texture inside the reveal band (0 to 2). | number | 0.25 |
reactivityHow much scrolling boosts the beam and the reveal glow (0 to 3). | number | 1 |
classNameClasses applied to the wrapper element. | string |