「海」を表現する
波・光・泡・きらめき。海の空気感を写真素材に頼らず、CSS/SVGの軽量な表現で再現するテクニック。
このページについて
- 「海の表現」とは:波の動き・水中の光・泡など、海を連想させる視覚要素。実写を使わずに抽象表現で「海らしさ」を伝えると、軽量で世界観の統一もしやすい。
- こんな時に使う:マリンスポーツ・リゾート・水族館・夏キャンペーン・ダイビングショップ・湘南エリアの店舗サイトなど。
- 関連タグ:🌿 癒し(準備中)❄️ 清涼感(準備中)☁️ 空(準備中)
- AIへの伝え方のコツ:「海っぽく」では曖昧。「多層の波」「上から差す光芒」「泡の上昇」など、このページの具体的な表現名で指示する。
表現の引き出し
🎨 重なる波
📝 表現の語彙
💬 AIへの指示テンプレ
🎯 似合うシーン
リゾート・マリンスポーツ・水族館・夏のキャンペーンLP・ビーチ系飲食店
🔧 実装手法
不規則な波長・振幅のSVG path × 4層の横ループ + 層ごとに振幅・周期を変えた上下うねり + feTurbulence/feDisplacementMapによる揺らぎ(最前面のみ)
📄 コード例を見る
<div class="wave-stage">
<!-- 揺らぎフィルター(負荷が高いため最前面の波にのみ適用) -->
<svg width="0" height="0" aria-hidden="true">
<filter id="waveTurbulence">
<!-- numOctaves=1 + 低い baseFrequency で、ギザつかない大きくなだらかな揺らぎにする -->
<feTurbulence type="fractalNoise" baseFrequency="0.005 0.012"
numOctaves="1" seed="3" result="noise">
<animate attributeName="baseFrequency" dur="17s"
values="0.005 0.012; 0.006 0.016; 0.005 0.012" repeatCount="indefinite" />
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="5" />
</filter>
</svg>
<!-- 4層構成。波形は波長・振幅を不規則にした Q カーブの連続で作る -->
<!-- 各層は「上下うねり」ラッパー ×「同一path×2連結の横ループ」 -->
<div class="wave-bob wave-bob-1">
<div class="wave-layer wave-1">
<svg viewBox="0 0 800 120" preserveAspectRatio="none">
<path d="M0,58 Q55,48 110,56 Q150,62 210,52 Q270,42 330,54 Q380,64 440,54
Q500,42 560,56 Q610,66 670,56 Q730,46 800,58 L800,120 L0,120 Z"
fill="#dbeafe" />
</svg>
<svg><!-- 同じpath --></svg>
</div>
</div>
<div class="wave-bob wave-bob-2"><!-- 第2層 --></div>
<div class="wave-bob wave-bob-3"><!-- 第3層(波長を細かく) --></div>
<div class="wave-bob wave-bob-4"><!-- 最前面(さらに細かい波長、filter適用) --></div>
</div>.wave-stage {
position: relative;
height: 180px;
border-radius: 8px;
overflow: hidden;
background: linear-gradient(180deg, #7dd3fc 0%, #38bdf8 35%, #0284c7 70%, #075985 100%);
}
/* 上下のうねり。奥は小さくゆっくり、手前は大きく速く(素数的な周期差) */
.wave-bob { position: absolute; left: 0; width: 100%; }
/* うねりや揺らぎで下端が浮いて余白が出ないよう、揺れ幅ぶん下にはみ出させる */
.wave-bob-1 { height: 118px; bottom: -5px; animation: waveBob1 13s ease-in-out infinite alternate; }
.wave-bob-2 { height: 100px; bottom: -6px; animation: waveBob2 11s ease-in-out infinite alternate; animation-delay: -5s; }
.wave-bob-3 { height: 84px; bottom: -7px; animation: waveBob3 9s ease-in-out infinite alternate; animation-delay: -2s; }
.wave-bob-4 { height: 66px; bottom: -12px; animation: waveBob4 7s ease-in-out infinite alternate; animation-delay: -3.5s; }
@keyframes waveBob1 { from { transform: translateY(0); } to { transform: translateY(-4px); } }
@keyframes waveBob2 { from { transform: translateY(0); } to { transform: translateY(-5px); } }
@keyframes waveBob3 { from { transform: translateY(0); } to { transform: translateY(-6px); } }
@keyframes waveBob4 { from { transform: translateY(0); } to { transform: translateY(-8px); } }
/* 横の流れ。-50% 移動で継ぎ目なくループ(水の流れは等速でよい) */
.wave-layer { display: flex; width: 200%; height: 100%; }
.wave-layer svg { width: 50%; height: 100%; flex-shrink: 0; }
.wave-1 { animation: waveDrift 34s linear infinite; opacity: .3; }
.wave-2 { animation: waveDrift 26s linear infinite; opacity: .45; }
.wave-3 { animation: waveDrift 19s linear infinite; opacity: .65; }
.wave-4 { animation: waveDrift 13s linear infinite; opacity: .9;
filter: url(#waveTurbulence); } /* 揺らぎは最前面のみ */
@keyframes waveDrift {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}🎨 海中の光芒
📝 表現の語彙
💬 AIへの指示テンプレ
🎯 似合うシーン
ダイビング・水族館・スパ・瞑想/リラクゼーション・深海テーマのクリエイティブサイト
🔧 実装手法
コア+グローの2種の光芒(screen合成・支点回転)+ 奥行き2段の岸壁シルエット + 深度の霧グラデーション + 微粒子の多重周期浮遊
📄 コード例を見る
<div class="ray-stage">
<div class="ray-surface"></div>
<!-- 岸壁のシルエット。奥(blur+薄め)と手前(シャープ)の2段で奥行きを出す -->
<svg class="ray-rocks" viewBox="0 0 400 180" preserveAspectRatio="none">
<path class="rock-far" d="M0,180 L0,58 Q36,74 28,100 Q48,122 40,148 Q54,164 48,180 Z" fill="#0a2b42" />
<path class="rock-far" d="M400,180 L400,46 Q364,62 372,90 Q352,114 362,140 Q346,160 352,180 Z" fill="#0a2b42" />
<path d="M0,180 L0,98 Q26,110 18,132 Q38,148 30,166 Q42,174 38,180 Z" fill="#03141f" />
<path d="M400,180 L400,90 Q376,102 384,124 Q366,140 374,158 Q360,170 364,180 Z" fill="#03141f" />
</svg>
<!-- 光芒。明るいコアの光と、幅広で淡いグロー光を混ぜる -->
<div class="ray" style="left: 6%; width: 44px; opacity: .55; --tilt: -9deg; --dur: 11s; filter: blur(4px)"></div>
<div class="ray" style="left: 22%; width: 90px; opacity: .28; --tilt: -5deg; --dur: 23s; filter: blur(10px)"></div>
<!-- ...コア5本+グロー2本の計7本... -->
<!-- 深度の霧。下に行くほど光が届かない -->
<div class="ray-depth"></div>
<!-- 漂う微粒子。上昇と左右の揺れは別アニメで合成 -->
<div class="mote" style="left: 20%; width: 2px; height: 2px; --dur: 23s; --sway: 3.7s; --mo: 0.4">
<div class="mote-dot"></div>
</div>
</div>.ray-stage {
position: relative;
height: 180px;
border-radius: 8px;
overflow: hidden;
background: linear-gradient(180deg, #0c4a6e 0%, #082f49 60%, #041e33 100%);
}
/* 水面の明るさの気配 */
.ray-surface {
position: absolute;
top: 0; left: 0; right: 0;
height: 30px;
background: linear-gradient(180deg, rgba(186,230,253,0.25), transparent);
}
.ray-rocks { position: absolute; inset: 0; width: 100%; height: 100%; }
.rock-far { filter: blur(2px); opacity: .8; } /* 奥の岩は霞ませる */
/* 上が狭く下に広がる光のカーテン。screen合成で岸壁にも光が当たる */
.ray {
position: absolute;
top: -12px;
height: 200px;
clip-path: polygon(38% 0, 62% 0, 100% 100%, 0 100%);
background: linear-gradient(180deg,
rgba(224,242,254,0.65) 0%, rgba(186,230,253,0.25) 45%, transparent 85%);
mix-blend-mode: screen;
transform-origin: top center;
transform: rotate(var(--tilt));
animation: raySway var(--dur) ease-in-out infinite alternate;
}
@keyframes raySway {
from { transform: rotate(calc(var(--tilt) - 3deg)); }
to { transform: rotate(calc(var(--tilt) + 3deg)); }
}
/* 深度の霧:下に行くほど暗く、光芒が溶けて消える */
.ray-depth {
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 35%, rgba(2,10,18,0.6) 100%);
}
/* 微粒子:ゆっくり上昇(19〜29s)×短周期の左右揺れ */
.mote {
position: absolute;
bottom: -10px;
animation: moteRise var(--dur) ease-in-out infinite;
}
@keyframes moteRise {
0% { transform: translateY(0); opacity: 0; }
12% { opacity: var(--mo); }
88% { opacity: var(--mo); }
100% { transform: translateY(-200px); opacity: 0; }
}
.mote-dot {
width: 100%; height: 100%;
border-radius: 9999px;
background: #e0f2fe;
animation: moteSway var(--sway) ease-in-out infinite alternate;
}
@keyframes moteSway {
from { transform: translateX(-6px); }
to { transform: translateX(6px); }
}🎨 泡の上昇
📝 表現の語彙
💬 AIへの指示テンプレ
🎯 似合うシーン
炭酸飲料・ビール・水族館・子供向けコンテンツ・洗剤/シャンプー系LP
🔧 実装手法
depth(遠近)を軸に速度・サイズ・濃さ・ボケを連動させた泡 + 色味3種とscaleXのつぶれで個性 + 上昇と横揺れの2軸合成
📄 コード例を見る
// 泡12個。depth(奥行き)を軸に速度・サイズ・濃さ・ボケを連動させる
const BUBBLE_TINTS = [
// 白 / シアン寄り / 青寄り --- 泡ごとに色の個性を出す
"radial-gradient(circle at 30% 30%, rgba(255,255,255,0.85), rgba(255,255,255,0.15) 45%, rgba(255,255,255,0.05) 70%)",
"radial-gradient(circle at 32% 28%, rgba(236,254,255,0.9), rgba(165,243,252,0.18) 48%, rgba(165,243,252,0.05) 72%)",
"radial-gradient(circle at 28% 32%, rgba(239,246,255,0.8), rgba(191,219,254,0.2) 42%, rgba(191,219,254,0.06) 68%)",
];
const bubbles = Array.from({ length: 12 }, (_, i) => {
const j1 = ((i * 7919) % 100) / 100; // depth: 0=奥, 1=手前
const j2 = ((i * 104729) % 100) / 100;
const j3 = ((i * 1299709) % 100) / 100;
const depth = j1;
return {
left: 4 + ((i * 9973) % 90), // %
size: 5 + depth * 24, // 手前ほど大きい
dur: 13 - depth * 8, // 手前は速く(5s)、奥は遅く(13s)
delay: -(j2 * 13),
sway: 2.3 + j2 * 1.4,
amp: 2 + (1 - depth) * 7, // 小さい泡ほど大きく揺れる
opacity: 0.45 + depth * 0.55, // 奥は薄い
blur: (1 - depth) * 1.2, // 奥はわずかにボケる
squish: 0.92 + j3 * 0.16, // 真円でない微妙なつぶれ
tint: i % 3,
};
});
<div className="bubble-stage">
{bubbles.map((b, i) => (
<div key={i} className="bubble-rise"
style={{
left: `${b.left}%`, width: b.size, height: b.size,
filter: `blur(${b.blur}px)`,
"--dur": `${b.dur}s`, "--sway": `${b.sway}s`,
"--amp": `${b.amp}px`, "--bo": b.opacity, "--squish": b.squish,
animationDelay: `${b.delay}s`,
}}>
<div className="bubble" style={{ background: BUBBLE_TINTS[b.tint] }} />
</div>
))}
</div>.bubble-stage {
position: relative;
height: 180px;
border-radius: 8px;
overflow: hidden;
background: linear-gradient(180deg, #38bdf8 0%, #0369a1 100%);
}
/* 上昇は ease-in(浮力で加速)。上端で薄く拡大して消える */
.bubble-rise {
position: absolute;
bottom: -32px;
animation: bubbleRise var(--dur) ease-in infinite;
}
@keyframes bubbleRise {
0% { transform: translateY(0) scale(1); opacity: 0; }
10% { opacity: var(--bo); }
88% { opacity: var(--bo); transform: translateY(-196px) scale(1); }
100% { transform: translateY(-214px) scale(1.3); opacity: 0; }
}
/* 左上ハイライトで球体感。横揺れ+微妙なつぶれは内側要素の別アニメ */
.bubble {
width: 100%; height: 100%;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.35);
animation: bubbleSway var(--sway) ease-in-out infinite alternate;
}
@keyframes bubbleSway {
from { transform: translateX(calc(var(--amp) * -1)) scaleX(var(--squish)); }
to { transform: translateX(var(--amp)) scaleX(1); }
}🎨 水面のきらめき
📝 表現の語彙
💬 AIへの指示テンプレ
🎯 似合うシーン
リゾートホテル・クルーズ・夕方の演出・サマーキャンペーン・ウェディング(海辺)
🔧 実装手法
境界を波立たせた2色の青ストライプ(blur+ゆっくり横揺れ、奥ほど波小さく)+ レンズ形きらめきを全面散布+太陽下に密度集中 + 明滅と波に乗る上下動の2アニメ合成
📄 コード例を見る
// ストライプの波立つ上端(サインカーブ的なQの連続。±20pxはみ出させて揺れの隙間を防ぐ)
function waveEdgePath(y, amp, wl, startUp) {
let d = `M-20,${y}`;
let up = startUp;
for (let x = -20; x < 430; x += wl / 2) {
d += ` Q${x + wl / 4},${y + (up ? -amp : amp)} ${x + wl / 2},${y}`;
up = !up;
}
return `${d} L430,130 L-20,130 Z`;
}
// 海は2色の青のランダムな太さのストライプ。境界はぼかし、波立たせる
const seaStripes = (() => {
const stripes = [];
let y = 0;
let i = 0;
while (y < 122) {
const j1 = ((i * 7919) % 100) / 100;
const j2 = ((i * 104729) % 100) / 100;
const j3 = ((i * 1299709) % 100) / 100;
const depth = y / 120; // 0=水平線, 1=手前
const t = (1.5 + depth * 12) * (0.6 + j1 * 0.8); // 奥は細く、手前は太く
const amp = (0.3 + depth * 2.4) * (0.7 + j3 * 0.6); // 奥に行くほど波は小さく
const wl = 46 + j2 * 50; // 波長もランダム
stripes.push({
d: waveEdgePath(y, amp, wl, i % 2 === 0),
color: i % 2 ? "#0369a1" : "#0ea5e9",
blur: 0.4 + (1 - depth) * 1.4, // 全境界をぼかす(奥ほど強く)
// 線ごとにゆっくり左右へ往復。向き(符号)・振幅・周期・位相をすべてバラす
swayAmp: (i % 2 ? 1 : -1) * (5 + depth * 9) * (0.8 + j3 * 0.4),
swayDur: 7 + j2 * 10,
swayDelay: -(j1 * 12),
});
y += t;
i++;
}
return stripes;
})();
// きらめき56個。水面全体に散らしつつ、太陽の下に密度を寄せる
const glints = Array.from({ length: 56 }, (_, i) => {
const j1 = ((i * 7919) % 100) / 100;
const j2 = ((i * 104729) % 100) / 100;
const j3 = ((i * 1299709) % 100) / 100;
const depth = j1; // 0=水平線際, 1=手前
// 1/3は水面全体に散らし、残りは太陽の反射帯の近くに集める
const x =
i % 3 === 0 ? 8 + j2 * 384 : 200 + (j2 - 0.5) * 2 * (25 + j3 * j3 * 140);
const y = 5 + depth * 108;
const len = 2 + depth * 22; // 奥は短く、手前は長く
const h = 0.5 + depth * 1.4; // 中央の太さ(奥は細い)
const sag = (j3 - 0.5) * depth * 2; // わずかな湾曲
return {
// 両端が緩やかに尖るレンズ形。中央が最も太く、端に行くほど細い
d: `M${x - len / 2},${y} Q${x},${y + sag - h} ${x + len / 2},${y}
Q${x},${y + sag + h} ${x - len / 2},${y} Z`,
color: ["#ffffff", "#fef9c3", "#fde68a"][i % 3],
peak: 0.45 + j1 * 0.5,
dur: 1.3 + j2 * 2.4, // 明滅
delay: j3 * 3,
bob: 0.6 + depth * 1.8, // 波に乗る上下動(手前ほど大きい)
bobDur: 2.9 + j2 * 3.2,
bobDelay: -(j1 * 4),
};
});
<div className="glitter-stage">
<div className="glitter-sun" />
<svg viewBox="0 0 400 120" className="glitter-sea" preserveAspectRatio="none">
{seaStripes.map((s, i) => (
<path key={i} className="sea-stripe" d={s.d} fill={s.color}
style={{
filter: `blur(${s.blur}px)`,
"--samp": `${s.swayAmp}px`, "--dur": `${s.swayDur}s`,
animationDelay: `${s.swayDelay}s`,
}} />
))}
</svg>
<div className="glitter-horizon" />
<div className="glitter-band" />
<svg viewBox="0 0 400 120" className="glitter-lines" preserveAspectRatio="none">
{glints.map((g, i) => (
<path key={i} d={g.d} fill={g.color}
style={{
"--peak": g.peak, "--bob": `${g.bob}px`,
animationDuration: `${g.dur}s, ${g.bobDur}s`,
animationDelay: `${g.delay}s, ${g.bobDelay}s`,
}} />
))}
</svg>
</div>.glitter-stage {
position: relative;
height: 180px;
border-radius: 8px;
overflow: hidden;
/* 上1/3が夕方の空。海はストライプのSVGで描く */
background: linear-gradient(180deg, #fef3c7 0%, #fcd34d 31%, #0369a1 33%, #0369a1 100%);
}
/* 海のストライプ。境界の波がゆっくり横に揺れる */
.glitter-sea { position: absolute; top: 33%; left: 0; width: 100%; height: 67%; }
.sea-stripe { animation: stripeSway var(--dur) ease-in-out infinite alternate; }
@keyframes stripeSway {
from { transform: translateX(calc(var(--samp) * -1)); }
to { transform: translateX(var(--samp)); }
}
.glitter-sun {
position: absolute;
top: 14px; left: 50%;
width: 30px; height: 30px; margin-left: -15px;
border-radius: 9999px;
background: #fde68a;
filter: blur(2px);
box-shadow: 0 0 22px 8px rgba(253, 230, 138, 0.55);
}
.glitter-horizon {
position: absolute;
top: 33%; left: 0; right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.5);
}
/* 太陽の反射の帯。overlay合成でゆっくり明滅 */
.glitter-band {
position: absolute;
top: 33%; bottom: 0; left: 50%;
width: 44px; margin-left: -22px;
background: linear-gradient(180deg, rgba(253,230,138,0.8), rgba(253,230,138,0.08));
mix-blend-mode: overlay;
filter: blur(2px);
animation: bandPulse 5s ease-in-out infinite alternate;
}
@keyframes bandPulse {
from { opacity: .55; }
to { opacity: 1; }
}
.glitter-lines { position: absolute; top: 33%; left: 0; width: 100%; height: 67%; }
/* レンズ形(fill)のきらめき。明滅(glint)と波に乗る上下動(glintBob)を別アニメで合成 */
.glitter-lines path {
opacity: var(--peak);
animation-name: glint, glintBob;
animation-timing-function: ease-in-out, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-direction: normal, alternate;
}
@keyframes glint {
0%, 100% { opacity: 0; }
50% { opacity: var(--peak); }
}
@keyframes glintBob {
from { transform: translateY(calc(var(--bob) * -1)); }
to { transform: translateY(var(--bob)); }
}よくある質問
写真素材と抽象表現、どちらを使うべき?
実物の質感が必要なら写真、世界観の統一・軽量さ・他要素との馴染みを重視するなら抽象表現が向きます。写真の上に本ページの表現(光芒・きらめき)を重ねるハイブリッドも効果的で、写真だけよりも「動きのある海」を演出できます。
波のアニメーションは重くなりませんか?
SVG pathのtransform移動は軽量ですが、feTurbulenceフィルターはGPU負荷が高めです。フィルターは最前面の1層だけに絞る、モバイルではフィルターを外す(メディアクエリ)、画面外では停止する(Intersection Observer)などの工夫で実用範囲に収まります。
「海らしさ」を一番手軽に出すには?
1つだけ選ぶなら「重なる波」をセクション境界(区切り)に使うのが最も手軽で効果的です。ページ下部やフッター上に波を1セット置くだけで、サイト全体が海の世界観になります。
AIに頼むとき、複数の表現を組み合わせるコツは?
「背景は深海の光芒、その上に泡を5個ゆっくり上昇させる」のように、レイヤー構造(背景→中景→アクセント)を明示して指示すると破綻しにくいです。一度に全部頼まず、まず背景、次に泡、と段階的に重ねるのも有効です。