Three.js パーティクル背景
光の粒子がゆっくり漂うセクション背景に、キャッチコピーを重ねて表示できます。
コード・使い方を見る
<div class="tp-wrap">
<canvas class="tp-canvas" aria-hidden="true"></canvas>
<div class="tp-content">
<p class="tp-eyebrow">みなと総合クリニック</p>
<h2 class="tp-title">安心と信頼を、<br class="tp-br">これからも。</h2>
<p class="tp-lead">地域のみなさまに寄り添う診療を、これからも大切にしてまいります。</p>
</div>
</div> .tp-wrap {
--tp-bg-from: #0f3f47; /* 背景グラデーション開始色 */
--tp-bg-to: #114b52; /* 背景グラデーション終了色 */
--tp-text: #ffffff; /* 見出し・本文の文字色 */
position: relative;
width: 100%;
min-height: 420px;
overflow: hidden;
background: linear-gradient(150deg, var(--tp-bg-from), var(--tp-bg-to));
border-radius: 14px;
font-family: "Noto Sans JP", "Hiragino Kaku Gothic ProN", sans-serif;
box-sizing: border-box;
}
.tp-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.tp-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 420px;
padding: 2.5em 1.5em;
color: var(--tp-text);
}
.tp-eyebrow {
margin: 0 0 .6em;
font-size: .82rem;
letter-spacing: .2em;
opacity: .8;
}
.tp-title {
margin: 0 0 .7em;
font-size: clamp(1.5rem, 4vw, 2.4rem);
font-weight: 700;
line-height: 1.5;
}
.tp-lead {
margin: 0;
font-size: .95rem;
line-height: 1.9;
opacity: .9;
max-width: 32em;
}
@media (max-width: 480px) {
.tp-wrap, .tp-content { min-height: 340px; }
.tp-content { padding: 2em 1.2em; }
.tp-br { display: none; }
}
/* THREE.js が読み込めない環境向けの静的フォールバック(script.js が .tp-static を付与) */
.tp-wrap.tp-static .tp-canvas { display: none; } (function () {
var wraps = document.querySelectorAll('.tp-wrap');
if (!wraps.length) return;
if (typeof THREE === 'undefined') {
console.warn('threejs-particles: THREE が読み込まれていないため、静的背景で表示します。');
wraps.forEach(function (wrap) { wrap.classList.add('tp-static'); });
return;
}
var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
wraps.forEach(function (wrap) {
var canvas = wrap.querySelector('.tp-canvas');
if (!canvas) return;
var width = wrap.clientWidth || 1;
var height = wrap.clientHeight || 1;
var renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(width, height);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 1000);
camera.position.z = 60;
var COUNT = 260;
var positions = new Float32Array(COUNT * 3);
var speeds = new Float32Array(COUNT);
for (var i = 0; i < COUNT; i++) {
positions[i * 3] = (Math.random() - 0.5) * 90;
positions[i * 3 + 1] = (Math.random() - 0.5) * 60;
positions[i * 3 + 2] = (Math.random() - 0.5) * 60;
speeds[i] = 0.05 + Math.random() * 0.12;
}
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
var material = new THREE.PointsMaterial({
color: 0xbfe9e4, /* 粒子の色(クリニックのテーマ色に合わせて変更可) */
size: 1.6,
transparent: true,
opacity: 0.75,
depthWrite: false,
});
var points = new THREE.Points(geometry, material);
scene.add(points);
function renderStatic() {
renderer.render(scene, camera);
}
var rafId = null;
var running = false;
function animate() {
if (!running) return;
var pos = geometry.attributes.position.array;
for (var j = 0; j < COUNT; j++) {
pos[j * 3 + 1] += speeds[j];
if (pos[j * 3 + 1] > 30) pos[j * 3 + 1] = -30;
}
geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
rafId = requestAnimationFrame(animate);
}
function start() {
if (running || reduceMotion) { renderStatic(); return; }
running = true;
animate();
}
function stop() {
running = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
}
function handleResize() {
var w = wrap.clientWidth || 1;
var h = wrap.clientHeight || 1;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
if (!running) renderStatic();
}
window.addEventListener('resize', handleResize);
if ('IntersectionObserver' in window) {
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) start(); else stop();
});
}, { threshold: 0.05 });
observer.observe(wrap);
} else {
start();
}
if (reduceMotion) {
renderStatic();
}
});
})();