フレネル発光の球
フレネル項で縁だけが光るシェーダーマテリアルの球が回転。内側は暗く縁が明るいリムライト風で、暗い背景に映えます。テック系の象徴ビジュアルに向きます。
外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura:縁が光る球をペンライトの光に見立てたライブ告知ヒーロー -->
<section class="sk-live" aria-label="Sakura ライブ">
<!-- 背景:フレネルで縁が光る球(ライブの光の象徴) -->
<canvas id="scene" class="sk-live__canvas" aria-hidden="true"></canvas>
<div class="sk-live__fallback" id="sk-fallback" hidden></div>
<header class="sk-bar">
<span class="sk-logo">🌸 Sakura</span>
<span class="sk-bar__tag">LIVE TOUR 2026</span>
</header>
<div class="sk-live__body">
<span class="sk-kicker">SPRING ONEMAN LIVE</span>
<h1 class="sk-title">桜、満開ツアー</h1>
<p class="sk-lead">全国5都市をめぐる春のワンマンツアー。<br>チケット最速先行、本日より受付開始。</p>
<ul class="sk-dates">
<li><span class="sk-dates__city">東京</span><span class="sk-dates__day">4.18 SAT</span></li>
<li><span class="sk-dates__city">大阪</span><span class="sk-dates__day">4.25 SAT</span></li>
<li><span class="sk-dates__city">名古屋</span><span class="sk-dates__day">5.02 SAT</span></li>
</ul>
<button class="sk-btn" type="button">先行に申し込む</button>
</div>
</section>
CSS
/* Sakura:暗めの背景に光る球が映えるライブ告知 */
:root {
--pink: #ffd1e0;
--pink-deep: #ff8fb3;
--white: #ffffff;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
background: #2a1320;
}
.sk-live {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
/* ライブ会場の暗がりに桜色の光 */
background:
radial-gradient(90% 90% at 78% 45%, #5a2540 0%, #361426 55%, #220c18 100%);
color: #fff;
}
/* 光る球は右寄りの背景に */
.sk-live__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.sk-live__fallback {
position: absolute;
right: 16%;
top: 50%;
width: 180px;
height: 180px;
transform: translateY(-50%);
border-radius: 50%;
background: radial-gradient(circle, transparent 45%, rgba(255, 143, 179, 0.85) 70%, transparent 80%);
filter: blur(2px);
}
.sk-bar {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 26px;
}
.sk-logo { font-size: 18px; font-weight: 800; letter-spacing: 0.04em; }
.sk-bar__tag {
font-size: 11px;
letter-spacing: 0.22em;
font-weight: 700;
color: var(--pink);
}
.sk-live__body {
position: relative;
z-index: 2;
max-width: 420px;
padding: 22px 26px;
}
.sk-kicker {
font-size: 11px;
letter-spacing: 0.26em;
font-weight: 700;
color: var(--pink-deep);
}
.sk-title {
margin: 12px 0 12px;
font-size: 34px;
font-weight: 800;
letter-spacing: 0.02em;
text-shadow: 0 2px 18px rgba(255, 143, 179, 0.4);
}
.sk-lead {
margin: 0 0 16px;
font-size: 13.5px;
line-height: 1.85;
color: rgba(255, 255, 255, 0.82);
}
.sk-dates {
margin: 0 0 20px;
padding: 0;
list-style: none;
display: flex;
gap: 8px;
}
.sk-dates li {
flex: 1;
text-align: center;
padding: 9px 4px;
border-radius: 12px;
background: rgba(255, 209, 224, 0.1);
border: 1px solid rgba(255, 143, 179, 0.35);
}
.sk-dates__city {
display: block;
font-size: 13px;
font-weight: 700;
color: var(--pink);
}
.sk-dates__day {
display: block;
font-size: 10px;
margin-top: 3px;
color: rgba(255, 255, 255, 0.7);
}
.sk-btn {
font: inherit;
font-size: 14px;
font-weight: 800;
color: #fff;
background: linear-gradient(135deg, var(--pink-deep), #ff6f9c);
border: none;
padding: 12px 28px;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 8px 24px rgba(255, 111, 156, 0.5);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.sk-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(255, 111, 156, 0.65); }
.sk-btn:active { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
.sk-btn { transition: none; }
}
JavaScript
// Sakura ライブ告知ヒーロー:フレネル項で縁だけが桜色に光る球(ライブの光の象徴)
(function () {
"use strict";
const canvas = document.getElementById("scene");
const fallback = document.getElementById("sk-fallback");
// THREE未読込やcanvas不在なら安全に終了し、フォールバックを表示
if (!canvas || typeof THREE === "undefined") {
if (fallback) fallback.hidden = false;
return;
}
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let renderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
} catch (e) {
if (fallback) fallback.hidden = false;
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0, 4);
// フレネル発光のシェーダーマテリアル
const uniforms = {
glowColor: { value: new THREE.Color(0xff8fb3) }, // 桜色のリム
coreColor: { value: new THREE.Color(0x3a1626) }, // 暗い内側
power: { value: 2.6 },
};
const vertexShader = `
varying vec3 vNormal;
varying vec3 vView;
void main() {
vNormal = normalize(normalMatrix * normal);
vec4 mv = modelViewMatrix * vec4(position, 1.0);
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}
`;
const fragmentShader = `
precision highp float;
varying vec3 vNormal;
varying vec3 vView;
uniform vec3 glowColor;
uniform vec3 coreColor;
uniform float power;
void main() {
// 視線と法線の角度差から縁を明るく(フレネル)
float f = pow(1.0 - abs(dot(vNormal, vView)), power);
vec3 col = mix(coreColor, glowColor, f);
gl_FragColor = vec4(col, 1.0);
}
`;
const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });
const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 64, 64), material);
sphere.position.x = 1.4; // 前景テキストを避けて右へ
scene.add(sphere);
function resize() {
const w = canvas.clientWidth || 1;
const h = canvas.clientHeight || 1;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
window.addEventListener("resize", resize);
let raf = 0;
let running = true;
let t = 0;
function animate() {
if (!reduceMotion) {
t += 0.02;
sphere.rotation.y += 0.005;
// 光の脈動でライブの高揚感を演出
uniforms.power.value = 2.6 + Math.sin(t) * 0.5;
}
renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
}
animate();
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (running) { cancelAnimationFrame(raf); running = false; }
} else if (!running) {
running = true;
raf = requestAnimationFrame(animate);
}
});
window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();
コード
HTML
<!-- フレネル項で縁が光る回転球。リムライト風 -->
<div class="stage">
<canvas id="fresnel" aria-label="フレネル発光の球"></canvas>
<div class="fallback" id="fr-fallback" hidden>WebGL を表示できない環境です</div>
<div class="caption">
<span class="badge">Fresnel</span>
<h2>Glowing Sphere</h2>
<p>縁だけが光るリムライト風シェーダーの球</p>
</div>
</div>
CSS
/* 配色変数 */
:root {
--ink: #eaf2ff;
--accent: #6fe9ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
.stage {
position: relative;
width: 100%;
height: 360px;
/* 暗い背景で縁の発光を際立たせる */
background: radial-gradient(circle at 50% 42%, #0a1326 0%, #050a16 60%, #02040a 100%);
}
#fresnel {
display: block;
width: 100%;
height: 100%;
}
/* WebGL非対応時のフォールバック表示 */
.fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--ink);
font-size: 14px;
letter-spacing: .06em;
text-shadow: 0 2px 10px rgba(0, 0, 0, .6);
}
/* hidden 属性を尊重(CSSの display 指定が [hidden] を上書きしないように) */
.fallback[hidden] { display: none; }
.caption {
position: absolute;
left: 28px;
bottom: 24px;
color: var(--ink);
text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
pointer-events: none;
}
.badge {
display: inline-block;
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: rgba(111, 233, 255, .15);
border: 1px solid rgba(111, 233, 255, .45);
color: var(--accent);
margin-bottom: 10px;
}
.caption h2 {
font-size: 22px;
font-weight: 700;
}
.caption p {
margin-top: 4px;
font-size: 13px;
opacity: .75;
}
JavaScript
// フレネル発光の球:縁ほど強く光るリムライト風シェーダー
(function () {
"use strict";
const canvas = document.getElementById("fresnel");
const fallback = document.getElementById("fr-fallback");
// THREE未読込やcanvas不在なら安全に終了し、フォールバックを表示
if (!canvas || typeof THREE === "undefined") {
if (fallback) fallback.hidden = false;
return;
}
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// WebGL初期化(失敗時はフォールバック)
let renderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
} catch (e) {
if (fallback) fallback.hidden = false;
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.z = 4;
// フレネル項(視線と法線の角度)で縁の発光量を決めるシェーダー
const uniforms = {
time: { value: 0 },
glowColor: { value: new THREE.Color(0x6fe9ff) }, // 縁の発光色
coreColor: { value: new THREE.Color(0x081326) }, // 内側の暗色
power: { value: 2.6 }, // フレネルの鋭さ
};
const vertexShader = `
varying vec3 vNormal;
varying vec3 vView;
void main() {
// ビュー空間の法線と視線ベクトルを渡す
vec4 mv = modelViewMatrix * vec4(position, 1.0);
vNormal = normalize(normalMatrix * normal);
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}
`;
const fragmentShader = `
precision highp float;
varying vec3 vNormal;
varying vec3 vView;
uniform float time;
uniform vec3 glowColor;
uniform vec3 coreColor;
uniform float power;
void main() {
// フレネル:正面ほど0、縁ほど1
float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
f = pow(f, power);
// ゆるく脈動させる
f *= 0.85 + 0.15 * sin(time * 1.5);
// 内側暗・縁明をブレンド
vec3 col = mix(coreColor, glowColor, clamp(f, 0.0, 1.0));
// 縁を加算でさらに発光
col += glowColor * f * 0.6;
gl_FragColor = vec4(col, 1.0);
}
`;
const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });
const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 64, 64), material);
scene.add(sphere);
// 縁の光をさらに広げる外殻(裏面描画の加算ハロー)
const haloMat = new THREE.ShaderMaterial({
uniforms,
vertexShader,
fragmentShader: `
precision highp float;
varying vec3 vNormal;
varying vec3 vView;
uniform vec3 glowColor;
uniform float power;
void main() {
float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
f = pow(f, power * 0.6);
gl_FragColor = vec4(glowColor, f * 0.5);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
side: THREE.BackSide,
depthWrite: false,
});
const halo = new THREE.Mesh(new THREE.SphereGeometry(1.45, 48, 48), haloMat);
scene.add(halo);
function resize() {
const w = canvas.clientWidth || 1;
const h = canvas.clientHeight || 1;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
window.addEventListener("resize", resize);
let raf = 0;
let running = true;
const start = performance.now();
function animate(now) {
const t = (now - start) * 0.001;
uniforms.time.value = t;
// 縁の発光色を時間でゆっくり巡回させ、変化をはっきり見せる(無地の球でも動きが分かる)
uniforms.glowColor.value.setHSL((t * 0.08) % 1, 0.85, 0.62);
if (!reduceMotion) {
sphere.rotation.y = t * 0.4;
sphere.rotation.x = t * 0.15;
}
renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
}
animate(start);
// タブ非表示で停止、復帰で再開
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (running) { cancelAnimationFrame(raf); running = false; }
} else if (!running) {
running = true;
raf = requestAnimationFrame(animate);
}
});
window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「フレネル発光の球」の効果を追加してください。
# 追加してほしい効果
フレネル発光の球(WebGL / Three.js)
フレネル項で縁だけが光るシェーダーマテリアルの球が回転。内側は暗く縁が明るいリムライト風で、暗い背景に映えます。テック系の象徴ビジュアルに向きます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- フレネル項で縁が光る回転球。リムライト風 -->
<div class="stage">
<canvas id="fresnel" aria-label="フレネル発光の球"></canvas>
<div class="fallback" id="fr-fallback" hidden>WebGL を表示できない環境です</div>
<div class="caption">
<span class="badge">Fresnel</span>
<h2>Glowing Sphere</h2>
<p>縁だけが光るリムライト風シェーダーの球</p>
</div>
</div>
【CSS】
/* 配色変数 */
:root {
--ink: #eaf2ff;
--accent: #6fe9ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
.stage {
position: relative;
width: 100%;
height: 360px;
/* 暗い背景で縁の発光を際立たせる */
background: radial-gradient(circle at 50% 42%, #0a1326 0%, #050a16 60%, #02040a 100%);
}
#fresnel {
display: block;
width: 100%;
height: 100%;
}
/* WebGL非対応時のフォールバック表示 */
.fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--ink);
font-size: 14px;
letter-spacing: .06em;
text-shadow: 0 2px 10px rgba(0, 0, 0, .6);
}
/* hidden 属性を尊重(CSSの display 指定が [hidden] を上書きしないように) */
.fallback[hidden] { display: none; }
.caption {
position: absolute;
left: 28px;
bottom: 24px;
color: var(--ink);
text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
pointer-events: none;
}
.badge {
display: inline-block;
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: rgba(111, 233, 255, .15);
border: 1px solid rgba(111, 233, 255, .45);
color: var(--accent);
margin-bottom: 10px;
}
.caption h2 {
font-size: 22px;
font-weight: 700;
}
.caption p {
margin-top: 4px;
font-size: 13px;
opacity: .75;
}
【JavaScript】
// フレネル発光の球:縁ほど強く光るリムライト風シェーダー
(function () {
"use strict";
const canvas = document.getElementById("fresnel");
const fallback = document.getElementById("fr-fallback");
// THREE未読込やcanvas不在なら安全に終了し、フォールバックを表示
if (!canvas || typeof THREE === "undefined") {
if (fallback) fallback.hidden = false;
return;
}
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// WebGL初期化(失敗時はフォールバック)
let renderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
} catch (e) {
if (fallback) fallback.hidden = false;
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.z = 4;
// フレネル項(視線と法線の角度)で縁の発光量を決めるシェーダー
const uniforms = {
time: { value: 0 },
glowColor: { value: new THREE.Color(0x6fe9ff) }, // 縁の発光色
coreColor: { value: new THREE.Color(0x081326) }, // 内側の暗色
power: { value: 2.6 }, // フレネルの鋭さ
};
const vertexShader = `
varying vec3 vNormal;
varying vec3 vView;
void main() {
// ビュー空間の法線と視線ベクトルを渡す
vec4 mv = modelViewMatrix * vec4(position, 1.0);
vNormal = normalize(normalMatrix * normal);
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}
`;
const fragmentShader = `
precision highp float;
varying vec3 vNormal;
varying vec3 vView;
uniform float time;
uniform vec3 glowColor;
uniform vec3 coreColor;
uniform float power;
void main() {
// フレネル:正面ほど0、縁ほど1
float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
f = pow(f, power);
// ゆるく脈動させる
f *= 0.85 + 0.15 * sin(time * 1.5);
// 内側暗・縁明をブレンド
vec3 col = mix(coreColor, glowColor, clamp(f, 0.0, 1.0));
// 縁を加算でさらに発光
col += glowColor * f * 0.6;
gl_FragColor = vec4(col, 1.0);
}
`;
const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });
const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 64, 64), material);
scene.add(sphere);
// 縁の光をさらに広げる外殻(裏面描画の加算ハロー)
const haloMat = new THREE.ShaderMaterial({
uniforms,
vertexShader,
fragmentShader: `
precision highp float;
varying vec3 vNormal;
varying vec3 vView;
uniform vec3 glowColor;
uniform float power;
void main() {
float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
f = pow(f, power * 0.6);
gl_FragColor = vec4(glowColor, f * 0.5);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
side: THREE.BackSide,
depthWrite: false,
});
const halo = new THREE.Mesh(new THREE.SphereGeometry(1.45, 48, 48), haloMat);
scene.add(halo);
function resize() {
const w = canvas.clientWidth || 1;
const h = canvas.clientHeight || 1;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
window.addEventListener("resize", resize);
let raf = 0;
let running = true;
const start = performance.now();
function animate(now) {
const t = (now - start) * 0.001;
uniforms.time.value = t;
// 縁の発光色を時間でゆっくり巡回させ、変化をはっきり見せる(無地の球でも動きが分かる)
uniforms.glowColor.value.setHSL((t * 0.08) % 1, 0.85, 0.62);
if (!reduceMotion) {
sphere.rotation.y = t * 0.4;
sphere.rotation.x = t * 0.15;
}
renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
}
animate(start);
// タブ非表示で停止、復帰で再開
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (running) { cancelAnimationFrame(raf); running = false; }
} else if (!running) {
running = true;
raf = requestAnimationFrame(animate);
}
});
window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();
# 外部ライブラリ
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。