フローフィールド(流れ場)
三角関数で作った疑似ノイズの流れ場に沿って大量の粒子が漂い、繊細な流線アートを描く生成的ビジュアル。アンビエントな背景に最適です。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW:ブランドストーリー(湯気のような流れ場の背景) -->
<section class="mb-story">
<!-- 主役:アンビエントなフローフィールド -->
<canvas class="mb-story__fx" id="mbFlow"></canvas>
<!-- 前景UI:コンセプト文 -->
<div class="mb-story__inner">
<span class="mb-story__tag">OUR STORY</span>
<h1 class="mb-story__title">一杯ごとに、<br>立ちのぼる物語。</h1>
<p class="mb-story__lead">産地の畑から焙煎の香りまで。湯気のようにゆらめく時間を、あなたのテーブルへ。</p>
<div class="mb-story__meta">
<span># 自家焙煎</span>
<span># 直輸入豆</span>
<span># 月夜のカフェ</span>
</div>
<a class="mb-story__btn" href="#">こだわりを読む</a>
</div>
</section>
CSS
/* MOON BREW:流れ場のアンビエント背景+ブランドストーリー */
:root {
--cream: #f5ede1;
--brown: #2b1d12;
--amber: #c98a3b;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
.mb-story {
position: relative;
height: 400px;
overflow: hidden;
background:
radial-gradient(700px 360px at 75% 0%, #3a2718, transparent),
linear-gradient(160deg, #2b1d12, #1c130b);
}
/* 主役:流線アートの背景 */
.mb-story__fx {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
z-index: 1;
}
.mb-story__inner {
position: relative;
z-index: 2;
padding: 40px 32px;
max-width: 420px;
color: var(--cream);
pointer-events: none;
}
.mb-story__inner a { pointer-events: auto; }
.mb-story__tag {
display: inline-block;
font-size: 10px;
letter-spacing: 0.32em;
color: var(--amber);
font-weight: 700;
}
.mb-story__title {
margin: 13px 0 14px;
font-size: 31px;
line-height: 1.42;
font-weight: 700;
font-family: "Hiragino Mincho ProN", "Yu Mincho", serif;
}
.mb-story__lead {
margin: 0 0 20px;
font-size: 13px;
line-height: 1.9;
color: rgba(245,237,225,0.82);
max-width: 340px;
}
.mb-story__meta { display: flex; gap: 14px; margin-bottom: 24px; flex-wrap: wrap; }
.mb-story__meta span {
font-size: 11.5px;
color: rgba(245,237,225,0.7);
}
.mb-story__btn {
display: inline-block;
padding: 11px 24px;
border-radius: 999px;
background: var(--amber);
color: #fff;
font-size: 13px;
font-weight: 700;
text-decoration: none;
box-shadow: 0 10px 24px rgba(201,138,59,0.4);
transition: transform 0.2s ease;
}
.mb-story__btn:hover { transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) {
.mb-story__btn { transition: none; }
}
JavaScript
// MOON BREW:三角関数の疑似ノイズで作る流れ場(湯気のような流線)
(() => {
const canvas = document.getElementById('mbFlow');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
let w = 0, h = 0, raf = 0, running = true, t = 0, agents = [];
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const scale = 0.006; // 流れ場の細かさ
function resize() {
const r = canvas.getBoundingClientRect();
w = r.width; h = r.height;
canvas.width = Math.max(1, w * dpr);
canvas.height = Math.max(1, h * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// 背景を一度暗く塗っておく
ctx.fillStyle = '#1c130b';
ctx.fillRect(0, 0, w, h);
}
function makeAgents() {
const count = Math.max(120, Math.min(320, Math.floor((w * h) / 2600)));
agents = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
hue: 28 + Math.random() * 18 // 琥珀〜クリーム系
}));
}
// 座標から流れの角度を求める(疑似ノイズ)
function angleAt(x, y) {
return (
Math.sin(x * scale + t * 0.002) +
Math.cos(y * scale - t * 0.0015) +
Math.sin((x + y) * scale * 0.6)
) * Math.PI;
}
resize();
makeAgents();
function step() {
// ごく薄く塗り重ねて余韻を残す(流線が積層)
ctx.fillStyle = 'rgba(28,19,11,0.04)';
ctx.fillRect(0, 0, w, h);
for (const a of agents) {
const ang = angleAt(a.x, a.y);
const nx = a.x + Math.cos(ang) * 1.1;
const ny = a.y + Math.sin(ang) * 1.1;
ctx.strokeStyle = `hsla(${a.hue},55%,62%,0.18)`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(nx, ny);
ctx.stroke();
a.x = nx;
a.y = ny;
// 画面外や寿命でランダム再配置
if (a.x < 0 || a.x > w || a.y < 0 || a.y > h || Math.random() < 0.005) {
a.x = Math.random() * w;
a.y = Math.random() * h;
}
}
t += 1;
raf = requestAnimationFrame(step);
}
function start() {
if (running) return;
running = true;
raf = requestAnimationFrame(step);
}
function stop() {
running = false;
cancelAnimationFrame(raf);
}
window.addEventListener('resize', () => { resize(); makeAgents(); });
document.addEventListener('visibilitychange', () => {
document.hidden ? stop() : start();
});
running = false;
start();
})();
コード
HTML
<!-- フローフィールド(流れ場に沿って漂う粒子) -->
<div class="stage">
<canvas id="flowCanvas"></canvas>
<div class="label">Flow Field</div>
</div>
CSS
/* フローフィールド */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
width: 100%;
height: 360px;
overflow: hidden;
font-family: "Segoe UI", system-ui, sans-serif;
background: #07070d;
}
#flowCanvas { display: block; width: 100%; height: 100%; }
.label {
position: absolute;
left: 16px; bottom: 12px;
color: rgba(255, 255, 255, .4);
font-size: 12px;
letter-spacing: .3em;
text-transform: uppercase;
pointer-events: none;
}
JavaScript
// フローフィールドデモ(疑似ノイズの流れ場に沿って粒子が流れる)
(() => {
const canvas = document.getElementById('flowCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let w = 0, h = 0, t = 0;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let particles = [];
function resize() {
const r = canvas.getBoundingClientRect();
w = r.width; h = r.height;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = '#07070d';
ctx.fillRect(0, 0, w, h); // 初期化
}
function makeParticles() {
const count = Math.max(120, Math.min(320, Math.floor((w * h) / 3000)));
particles = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
hue: 180 + Math.random() * 120
}));
}
// 三角関数を組み合わせた滑らかな疑似ノイズ角度
function angleAt(x, y) {
const s = 0.004;
return (
Math.sin(x * s + t) +
Math.cos(y * s - t * 0.8) +
Math.sin((x + y) * s * 0.6 + t * 0.5)
) * Math.PI;
}
resize();
makeParticles();
window.addEventListener('resize', () => { resize(); makeParticles(); });
function step() {
// ごく薄く暗幕をかけて軌跡をゆっくり消す
ctx.fillStyle = 'rgba(7,7,13,0.04)';
ctx.fillRect(0, 0, w, h);
for (const p of particles) {
const a = angleAt(p.x, p.y);
p.x += Math.cos(a) * 1.4;
p.y += Math.sin(a) * 1.4;
ctx.beginPath();
ctx.arc(p.x, p.y, 1, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue},85%,62%,0.7)`;
ctx.fill();
// 画面外に出たら再配置
if (p.x < 0 || p.x > w || p.y < 0 || p.y > h) {
p.x = Math.random() * w;
p.y = Math.random() * h;
}
}
if (!reduced) t += 0.002;
requestAnimationFrame(step);
}
requestAnimationFrame(step);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「フローフィールド(流れ場)」の効果を追加してください。
# 追加してほしい効果
フローフィールド(流れ場)(Canvas エフェクト)
三角関数で作った疑似ノイズの流れ場に沿って大量の粒子が漂い、繊細な流線アートを描く生成的ビジュアル。アンビエントな背景に最適です。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- フローフィールド(流れ場に沿って漂う粒子) -->
<div class="stage">
<canvas id="flowCanvas"></canvas>
<div class="label">Flow Field</div>
</div>
【CSS】
/* フローフィールド */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
width: 100%;
height: 360px;
overflow: hidden;
font-family: "Segoe UI", system-ui, sans-serif;
background: #07070d;
}
#flowCanvas { display: block; width: 100%; height: 100%; }
.label {
position: absolute;
left: 16px; bottom: 12px;
color: rgba(255, 255, 255, .4);
font-size: 12px;
letter-spacing: .3em;
text-transform: uppercase;
pointer-events: none;
}
【JavaScript】
// フローフィールドデモ(疑似ノイズの流れ場に沿って粒子が流れる)
(() => {
const canvas = document.getElementById('flowCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let w = 0, h = 0, t = 0;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let particles = [];
function resize() {
const r = canvas.getBoundingClientRect();
w = r.width; h = r.height;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = '#07070d';
ctx.fillRect(0, 0, w, h); // 初期化
}
function makeParticles() {
const count = Math.max(120, Math.min(320, Math.floor((w * h) / 3000)));
particles = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
hue: 180 + Math.random() * 120
}));
}
// 三角関数を組み合わせた滑らかな疑似ノイズ角度
function angleAt(x, y) {
const s = 0.004;
return (
Math.sin(x * s + t) +
Math.cos(y * s - t * 0.8) +
Math.sin((x + y) * s * 0.6 + t * 0.5)
) * Math.PI;
}
resize();
makeParticles();
window.addEventListener('resize', () => { resize(); makeParticles(); });
function step() {
// ごく薄く暗幕をかけて軌跡をゆっくり消す
ctx.fillStyle = 'rgba(7,7,13,0.04)';
ctx.fillRect(0, 0, w, h);
for (const p of particles) {
const a = angleAt(p.x, p.y);
p.x += Math.cos(a) * 1.4;
p.y += Math.sin(a) * 1.4;
ctx.beginPath();
ctx.arc(p.x, p.y, 1, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue},85%,62%,0.7)`;
ctx.fill();
// 画面外に出たら再配置
if (p.x < 0 || p.x > w || p.y < 0 || p.y > h) {
p.x = Math.random() * w;
p.y = Math.random() * h;
}
}
if (!reduced) t += 0.002;
requestAnimationFrame(step);
}
requestAnimationFrame(step);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。