デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
暗い部屋に差し込む光を、CSSだけで作った背景です。光源を軸にした細長い帯を扇状に並べ、clip-path で先端が狭まる形に切り抜き、外側をぼかしてから screen の合成で重ねています。ぼかしと切り抜きを同じ要素にかけると輪郭が硬くなるので、二重の要素に分けています。
筋の角度や太さ、明るさは帯ごとに違う値を持たせ、周波数を整数にした揺れと明滅で動かしています。塵は光の扇の中にだけ散らし、小さな弧を描きながら漂わせ、光源に近いほど明るくしています。
背景の飾りなので、aria-hidden を付けて読み上げからは外しています。動きを減らす設定では、光と塵を最初の位置のまま止め、静止した画面として見せます。
角度や本数、色を変えるだけで、朝の光にも舞台の照明にも寄せられます。カフェ・ホテル・ドキュメンタリーなど、落ち着いた暗いヒーローの背景に向いています。
コード
<section class="rays-hero">
<!-- 光の筋と塵は飾りなので、JS が組み立てて読み上げから外す -->
<div class="rays-hero__scene" aria-hidden="true"></div>
<div class="rays-hero__inner">
<p class="rays-hero__eyebrow">SINCE 2014</p>
<h1 class="rays-hero__title">朝の光と、<br>一杯のコーヒー。</h1>
<p class="rays-hero__lead">ここに説明文が入ります。</p>
<a class="rays-hero__button" href="/menu/">メニューを見る</a>
</div>
</section>
.rays-hero {
--paper: #eeece6;
--night: #111111;
--light: 210, 185, 153; /* 光の色(r, g, b)。グレージュ */
--dust: 224, 211, 192; /* 塵の色(r, g, b)。グレージュを明るくしたもの */
--gutter: clamp(24px, 7vw, 96px);
position: relative;
display: grid;
align-items: center;
min-height: min(90vh, 640px);
padding: 0 var(--gutter);
overflow: hidden;
color: var(--paper);
background: linear-gradient(165deg, #231e19 0%, #151311 50%, #0b0a09 100%);
}
.rays-hero__scene {
position: absolute;
inset: 0;
isolation: isolate; /* screen の重ね合わせをこの中だけに閉じる */
}
.rays-hero__ray {
position: absolute;
transform-origin: 0 50%;
mix-blend-mode: screen;
}
.rays-hero__ray-fill {
width: 100%;
height: 100%;
/* 先広がりの形に切り抜く */
clip-path: polygon(0 46%, 100% 0, 100% 100%, 0 54%);
}
.rays-hero__glow {
position: absolute;
left: 4%;
top: -18%;
width: 70%;
height: 100%;
transform: translate(-50%, -50%);
border-radius: 50%;
mix-blend-mode: screen;
}
.rays-hero__dust {
position: absolute;
border-radius: 50%;
mix-blend-mode: screen;
transform: translate(-50%, -50%);
}
.rays-hero__inner {
position: relative;
max-width: 30em;
text-shadow: 0 2px 24px rgba(7, 4, 12, 0.85);
}
.rays-hero__eyebrow {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
opacity: 0.8;
}
.rays-hero__title {
margin: 24px 0 0;
font-size: clamp(36px, 7vw, 60px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
}
.rays-hero__lead {
margin: 24px 0 0;
font-size: 18px;
line-height: 1.8;
word-break: auto-phrase; /* 語の途中で折り返さない */
}
.rays-hero__button {
display: inline-grid;
place-items: center;
height: 48px;
margin-top: 32px;
padding: 0 32px;
border-radius: 999px;
background: var(--paper);
color: var(--night);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-decoration: none;
}
.rays-hero__button:focus-visible { outline: 2px solid var(--paper); outline-offset: 4px; }
import { gsap } from "gsap";
const DURATION = 10; // 揺れと明滅がひと巡りする秒数
const SOURCE = [4, -18]; // 光源の位置(コンテナに対する %。左上の外)
const LENGTH = 190; // 筋の長さ(コンテナ幅に対する %)
const DUST = 60;
// angle: 光源から見た向き(度)/ spread: 太さ(高さに対する %)/ alpha: 明るさ / sway: 揺れ角(度)/ f: 周波数(整数)/ blur: ぼかし(高さに対する比)
const RAYS = [
{ angle: 43, spread: 110, alpha: 0.12, sway: 0.4, f: 1, phase: 0.0, blur: 0.08 },
{ angle: 24, spread: 10, alpha: 0.68, sway: 1.2, f: 1, phase: 0.0, blur: 0.012 },
{ angle: 30, spread: 26, alpha: 0.38, sway: 0.9, f: 2, phase: 1.1, blur: 0.02 },
{ angle: 36, spread: 7, alpha: 0.72, sway: 1.4, f: 1, phase: 2.3, blur: 0.01 },
{ angle: 42, spread: 20, alpha: 0.42, sway: 0.8, f: 3, phase: 3.0, blur: 0.018 },
{ angle: 49, spread: 9, alpha: 0.6, sway: 1.2, f: 2, phase: 4.2, blur: 0.012 },
{ angle: 56, spread: 30, alpha: 0.28, sway: 1.0, f: 1, phase: 5.1, blur: 0.024 },
{ angle: 63, spread: 8, alpha: 0.55, sway: 1.1, f: 2, phase: 0.7, blur: 0.012 },
];
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".rays-hero").forEach((hero) => {
const scene = hero.querySelector(".rays-hero__scene");
// 光と塵の色は CSS 変数から読む
const style = getComputedStyle(hero);
const light = style.getPropertyValue("--light").trim();
const dustColor = style.getPropertyValue("--dust").trim();
const rays = RAYS.map((ray) => {
const outer = document.createElement("div");
outer.className = "rays-hero__ray";
outer.style.cssText = `left:${SOURCE[0]}%;top:${SOURCE[1] - ray.spread / 2}%;width:${LENGTH}%;height:${ray.spread}%;filter:blur(${Math.round(ray.blur * scene.clientHeight)}px);`;
const inner = document.createElement("div");
inner.className = "rays-hero__ray-fill";
inner.style.background = `linear-gradient(90deg, rgba(${light}, ${ray.alpha}), rgba(${light}, ${ray.alpha * 0.55}) 40%, rgba(${light}, 0))`;
outer.append(inner);
scene.append(outer);
return { node: outer, ray };
});
const glow = document.createElement("div");
glow.className = "rays-hero__glow";
glow.style.background = `radial-gradient(circle, rgba(${light}, 0.5), rgba(${light}, 0.14) 35%, rgba(${light}, 0) 70%)`;
scene.append(glow);
// 位置・大きさ・周波数は組み立てるときに決めて、以後は変えない
const dust = Array.from({ length: DUST }, () => {
const angle = ((22 + Math.random() * 43) * Math.PI) / 180;
const dist = 25 + Math.random() * 95;
const size = 2 + Math.random() * 3;
const node = document.createElement("div");
node.className = "rays-hero__dust";
node.style.cssText = `left:${SOURCE[0]}%;top:${SOURCE[1]}%;width:${size}px;height:${size}px;background:rgb(${dustColor});box-shadow:0 0 ${size * 2}px rgba(${dustColor}, 0.8);`;
scene.append(node);
return {
node,
x: SOURCE[0] + Math.cos(angle) * dist,
y: SOURCE[1] + Math.sin(angle) * dist,
ax: 0.5 + Math.random() * 2,
ay: 1 + Math.random() * 3,
fx: 1 + Math.floor(Math.random() * 2),
fy: 1 + Math.floor(Math.random() * 2),
twinkle: 1 + Math.floor(Math.random() * 3),
phase: Math.random() * Math.PI * 2,
alpha: (0.35 + Math.random() * 0.65) * (1.1 - dist / 130),
};
});
function shine(t) {
const a = (t / DURATION) * Math.PI * 2;
for (const { node, ray } of rays) {
gsap.set(node, {
rotation: ray.angle + ray.sway * Math.sin(a * ray.f + ray.phase),
opacity: 0.65 + 0.35 * Math.sin(a * ray.f + ray.phase * 1.7),
});
}
gsap.set(glow, { opacity: 0.8 + 0.2 * Math.sin(a * 2) });
for (const d of dust) {
gsap.set(d.node, {
left: `${d.x + d.ax * Math.sin(a * d.fx + d.phase)}%`,
top: `${d.y + d.ay * Math.sin(a * d.fy + d.phase * 1.3)}%`,
opacity: Math.max(0, d.alpha * (0.55 + 0.45 * Math.sin(a * d.twinkle + d.phase * 2.1))),
});
}
}
shine(0);
// 動きを減らす設定では、静止した光の絵のまま
if (reduceMotion) return;
gsap.ticker.add((time) => shine(time % DURATION));
});