06Light Shaft Background Fading Layer by Layer
Several light-shaft layers are drawn with their opacity multiplied down by an exponential function of depth, so the near shaft stays crisp while the far one almost dissolves into the background.
Best forSubtle lighting background for a trustworthy SaaS hero
shaftOpacityForDepthLIGHT_SHAFT_DECAYSHAFT_LAYER_COUNT

<main class="stage">
<div class="d3 is-demo" id="d3Root" tabindex="0">
<canvas class="d3__canvas" id="d3Canvas" width="480" height="300"></canvas>
<p class="d3__caption">Several light-shaft layers are drawn with their opacity multiplied down by an exponential function of depth, so the near shaft stays crisp while the far one almost dissolves into the background</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-yellow);
.d3 {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-yellow;
outline: none;
}
.d3__canvas {
display: block;
width: 100%;
height: 100%;
}
.d3__caption {
position: absolute;
left: $sp-3;
bottom: $sp-3;
max-width: 78%;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(23, 20, 26, .78);
color: #fff;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.d3__caption { font-size: 10px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.d3 { cursor: default; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.d3 {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #ffd23f;
outline: none;
}
.d3__canvas {
display: block;
width: 100%;
height: 100%;
}
.d3__caption {
position: absolute;
left: 12px;
bottom: 12px;
max-width: 78%;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.78);
color: #fff;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.d3__caption {
font-size: 10px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.d3 {
cursor: default;
}
}(function () {
// 기법: 겹마다 깊이값(거리값)이 시간에 따라 완만히 흔들리고, shaftOpacityForDepth 가 그 깊이값에
// LIGHT_SHAFT_DECAY 를 지수(exp)로 곱해 불투명도를 구한다 — 거리가 커질수록 불투명도가 기하급수로
// 줄어 먼 겹은 배경에 거의 스며들고 가까운 겹만 선명하게 남는다.
var root = document.getElementById('d3Root');
var canvas = document.getElementById('d3Canvas');
var ctx = canvas.getContext('2d');
var wrap = canvas.parentElement;
var CALC_W = 160, CALC_H = 100;
var SOURCE_X = CALC_W * 0.5, SOURCE_Y = -6;
var SHAFT_LAYER_COUNT = 7;
var LIGHT_SHAFT_DECAY = 0.42;
var MAX_OPACITY = 0.62;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rnd = seedRand(429061);
var shafts = [];
for (var i = 0; i < SHAFT_LAYER_COUNT; i += 1) {
shafts.push({
angleBase: -0.62 + (1.24 / (SHAFT_LAYER_COUNT - 1)) * i,
distanceBase: 1.1 + i * 0.62,
swayRange: 0.6 + rnd() * 0.6,
swaySpeed: 0.0013 + rnd() * 0.0011,
phase: rnd() * Math.PI * 2,
width: 8 + rnd() * 6,
hue: i % 2 === 0 ? '90,60,170' : '40,30,110'
});
}
function depthFor(shaft, t) {
return shaft.distanceBase + Math.sin(t * shaft.swaySpeed + shaft.phase) * shaft.swayRange;
}
function shaftOpacityForDepth(depth) {
var v = Math.exp(-LIGHT_SHAFT_DECAY * depth) * MAX_OPACITY;
return Math.max(0, Math.min(MAX_OPACITY, v));
}
var elapsed = 0;
var cw = 480, ch = 300;
function drawScene(t) {
var sx = cw / CALC_W, sy = ch / CALC_H;
ctx.clearRect(0, 0, cw, ch);
ctx.fillStyle = 'rgba(60,40,120,.55)';
ctx.beginPath();
ctx.arc(SOURCE_X * sx, SOURCE_Y * sy, 10 * sx, 0, Math.PI * 2);
ctx.fill();
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < shafts.length; i += 1) {
var shaft = shafts[i];
var depth = depthFor(shaft, t);
var op = shaftOpacityForDepth(depth);
var angle = shaft.angleBase + Math.sin(t * shaft.swaySpeed * 0.7 + shaft.phase) * 0.13;
var length = 118 + depth * 6;
var endX = SOURCE_X + Math.sin(angle) * length;
var endY = SOURCE_Y + Math.cos(angle) * length;
var grad = ctx.createLinearGradient(SOURCE_X * sx, SOURCE_Y * sy, endX * sx, endY * sy);
grad.addColorStop(0, 'rgba(' + shaft.hue + ',' + op + ')');
grad.addColorStop(1, 'rgba(' + shaft.hue + ',0)');
var perpX = Math.cos(angle) * shaft.width;
var perpY = -Math.sin(angle) * shaft.width;
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo((SOURCE_X - perpX * 0.15) * sx, (SOURCE_Y - perpY * 0.15) * sy);
ctx.lineTo((SOURCE_X + perpX * 0.15) * sx, (SOURCE_Y + perpY * 0.15) * sy);
ctx.lineTo((endX + perpX) * sx, (endY + perpY) * sy);
ctx.lineTo((endX - perpX) * sx, (endY - perpY) * sy);
ctx.closePath();
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
}
function fit() {
var r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas.width = cw; canvas.height = ch;
drawScene(elapsed);
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) root.classList.remove('is-demo');
var warmSteps = reduce ? 0 : 120;
for (var w = 0; w < warmSteps; w += 1) elapsed += 20;
var lastTs = null;
function tick(ts) {
if (lastTs === null) lastTs = ts;
var dt = Math.min(ts - lastTs, 48);
lastTs = ts;
if (!reduce) {
elapsed += dt;
drawScene(elapsed);
}
requestAnimationFrame(tick);
}
fit();
window.addEventListener('resize', fit);
drawScene(elapsed);
requestAnimationFrame(tick);
})();import { useEffect, useRef } from "react";
import styles from "./Depth3dLightShaftDepth.module.scss";
type Shaft = {
angleBase: number;
distanceBase: number;
swayRange: number;
swaySpeed: number;
phase: number;
width: number;
hue: string;
};
export default function Depth3dLightShaftDepth() {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const root = rootRef.current;
const canvas = canvasRef.current;
if (!root || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const wrap = canvas.parentElement as HTMLElement;
const CALC_W = 160, CALC_H = 100;
const SOURCE_X = CALC_W * 0.5, SOURCE_Y = -6;
const SHAFT_LAYER_COUNT = 7;
const LIGHT_SHAFT_DECAY = 0.42;
const MAX_OPACITY = 0.62;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const rnd = seedRand(429061);
const shafts: Shaft[] = [];
for (let i = 0; i < SHAFT_LAYER_COUNT; i += 1) {
shafts.push({
angleBase: -0.62 + (1.24 / (SHAFT_LAYER_COUNT - 1)) * i,
distanceBase: 1.1 + i * 0.62,
swayRange: 0.6 + rnd() * 0.6,
swaySpeed: 0.0013 + rnd() * 0.0011,
phase: rnd() * Math.PI * 2,
width: 8 + rnd() * 6,
hue: i % 2 === 0 ? "90,60,170" : "40,30,110"
});
}
function depthFor(shaft: Shaft, t: number) {
return shaft.distanceBase + Math.sin(t * shaft.swaySpeed + shaft.phase) * shaft.swayRange;
}
function shaftOpacityForDepth(depth: number) {
const v = Math.exp(-LIGHT_SHAFT_DECAY * depth) * MAX_OPACITY;
return Math.max(0, Math.min(MAX_OPACITY, v));
}
let elapsed = 0;
let cw = 480, ch = 300;
function drawScene(t: number) {
const sx = cw / CALC_W, sy = ch / CALC_H;
ctx!.clearRect(0, 0, cw, ch);
ctx!.fillStyle = "rgba(60,40,120,.55)";
ctx!.beginPath();
ctx!.arc(SOURCE_X * sx, SOURCE_Y * sy, 10 * sx, 0, Math.PI * 2);
ctx!.fill();
ctx!.globalCompositeOperation = "lighter";
for (let i = 0; i < shafts.length; i += 1) {
const shaft = shafts[i];
const depth = depthFor(shaft, t);
const op = shaftOpacityForDepth(depth);
const angle = shaft.angleBase + Math.sin(t * shaft.swaySpeed * 0.7 + shaft.phase) * 0.13;
const length = 118 + depth * 6;
const endX = SOURCE_X + Math.sin(angle) * length;
const endY = SOURCE_Y + Math.cos(angle) * length;
const grad = ctx!.createLinearGradient(SOURCE_X * sx, SOURCE_Y * sy, endX * sx, endY * sy);
grad.addColorStop(0, `rgba(${shaft.hue},${op})`);
grad.addColorStop(1, `rgba(${shaft.hue},0)`);
const perpX = Math.cos(angle) * shaft.width;
const perpY = -Math.sin(angle) * shaft.width;
ctx!.fillStyle = grad;
ctx!.beginPath();
ctx!.moveTo((SOURCE_X - perpX * 0.15) * sx, (SOURCE_Y - perpY * 0.15) * sy);
ctx!.lineTo((SOURCE_X + perpX * 0.15) * sx, (SOURCE_Y + perpY * 0.15) * sy);
ctx!.lineTo((endX + perpX) * sx, (endY + perpY) * sy);
ctx!.lineTo((endX - perpX) * sx, (endY - perpY) * sy);
ctx!.closePath();
ctx!.fill();
}
ctx!.globalCompositeOperation = "source-over";
}
function fit() {
const r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas!.width = cw; canvas!.height = ch;
drawScene(elapsed);
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) root.classList.remove("is-demo");
const warmSteps = reduce ? 0 : 120;
for (let w = 0; w < warmSteps; w += 1) elapsed += 20;
let lastTs: number | null = null;
let raf = 0;
function tick(ts: number) {
if (lastTs === null) lastTs = ts;
const dt = Math.min(ts - lastTs, 48);
lastTs = ts;
if (!reduce) {
elapsed += dt;
drawScene(elapsed);
}
raf = requestAnimationFrame(tick);
}
fit();
window.addEventListener("resize", fit);
drawScene(elapsed);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", fit);
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.d3} is-demo`} ref={rootRef} tabIndex={0}>
<canvas className={styles.d3__canvas} ref={canvasRef} width={480} height={300} />
<p className={styles.d3__caption}>빛줄기 여러 겹을 그리고 각 겹의 불투명도를 거리값에 지수함수로 곱해 줄여, 가까운 겹은 선명하고 먼 겹은 배경에 거의 스며든다</p>
</div>
</main>
);
}06 Light Shaft Background Fading Layer by Layer — Measured area 30.528% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS 3D Background Animation Depth Layers
The post this demo lives in
9 CSS 3D Background Animation Depth LayersThis css 3d background animation set has nine layers below. Layer 02 loses focus with distance, layer 07 loses shadow st See all demos in this post
If you like this effect - similar demos
Interactions07 A Two-Layer Crackle Where Grains Split Into Smaller Bursts at the End of Their LifeCSS Fireworks: 9 Bursts, Trails and Reflections
Interactions02 Card stack that shifts by depth3D Hover Effect CSS: 9 Pointer-Following Scenes
Interactions02 A background that follows — three parallax layersMouse Follow Effect CSS — 9 Cursor Chasers