ビジュアル

CSSとJSで作る、ふわふわ揺れるバブル型の写真

投稿日2026/08/24

更新日2026/8/15

画像を丸みのあるバブルの中に配置し、光や輪郭、ゆっくりとした動きを組み合わせたビジュアル表現です。

SVGのアニメーションでバブル自体をゆらめかせながら、JavaScriptでマウスの動きに合わせてそれぞれの要素を異なる速度で動かすことで、奥行きのあるパララックス表現を作っています。

Code コード

<div class="kumonosu-container">
	<section class="kumonosu-visual" id="kumonosu-visual-id">
		<div class="kumonosu-lighting" aria-hidden="true">
			<span class="kumonosu-glow kumonosu-glow--ambient"></span>
			<span class="kumonosu-glow kumonosu-glow--core"></span>
			<span class="kumonosu-glow kumonosu-glow--sun"></span>
		</div>
		<svg class="kumonosu-liquid-scene" viewBox="0 0 1100 1000" preserveAspectRatio="xMidYMid meet">
			<defs>
				<!-- IDs updated to kumonosu- prefix -->
				<path id="kumonosu-main-path" d="M 380 80 C 600 40 850 110 920 340 C 980 580 820 740 550 770 C 280 800 180 610 210 340 C 240 70 320 100 380 80 Z">
					<animate attributeName="d" dur="18s" repeatCount="indefinite" calcMode="spline" keyTimes="0; .5; 1" keySplines=".42 0 .58 1; .42 0 .58 1" values="M 380 80 C 600 40 850 110 920 340 C 980 580 820 740 550 770 C 280 800 180 610 210 340 C 240 70 320 100 380 80 Z;
                                M 420 110 C 650 60 900 160 880 410 C 850 660 750 780 500 740 C 250 700 150 510 200 310 C 250 110 350 140 420 110 Z;
                                M 380 80 C 600 40 850 110 920 340 C 980 580 820 740 550 770 C 280 800 180 610 210 340 C 240 70 320 100 380 80 Z" />
				</path>
				<path id="kumonosu-secondary-path" d="M 120 680 C 320 610 520 650 550 780 C 580 910 420 980 270 950 C 120 920 50 810 120 680 Z">
					<animate attributeName="d" dur="12s" begin="-2s" repeatCount="indefinite" calcMode="spline" keyTimes="0; .5; 1" keySplines=".42 0 .58 1; .42 0 .58 1" values="M 120 680 C 320 610 520 650 550 780 C 580 910 420 980 270 950 C 120 920 50 810 120 680 Z;
                                M 150 650 C 350 640 490 680 520 810 C 550 940 390 950 250 920 C 110 890 80 780 150 650 Z;
                                M 120 680 C 320 610 520 650 550 780 C 580 910 420 980 270 950 C 120 920 50 810 120 680 Z" />
				</path>
				<path id="kumonosu-drop-path" d="M 730 720 C 800 700 870 740 880 820 C 890 900 830 970 750 950 C 670 930 660 820 730 720 Z">
					<animate attributeName="d" dur="10s" begin="-5s" repeatCount="indefinite" calcMode="spline" keyTimes="0; .5; 1" keySplines=".42 0 .58 1; .42 0 .58 1" values="M 730 720 C 800 700 870 740 880 820 C 890 900 830 970 750 950 C 670 930 660 820 730 720 Z;
                                M 750 740 C 830 720 900 770 890 850 C 880 930 800 940 730 920 C 660 900 670 820 750 740 Z;
                                M 730 720 C 800 700 870 740 880 820 C 890 900 830 970 750 950 C 670 930 660 820 730 720 Z" />
				</path>
				<clipPath id="kumonosu-main-clip">
					<use href="#kumonosu-main-path" />
				</clipPath>
				<clipPath id="kumonosu-secondary-clip">
					<use href="#kumonosu-secondary-path" />
				</clipPath>
				<clipPath id="kumonosu-drop-clip">
					<use href="#kumonosu-drop-path" />
				</clipPath>
				<linearGradient id="kumonosu-glass-stroke" x1="0" y1="0" x2="1" y2="1">
					<stop offset="0%" stop-color="#ffffff" />
					<stop offset="100%" stop-color="#ffffff" stop-opacity="0.2" />
				</linearGradient>
				<radialGradient id="kumonosu-main-milk" cx="48%" cy="16%" r="70%">
					<stop offset="0%" stop-color="#ffffff" stop-opacity=".34" />
					<stop offset="100%" stop-color="#ffffff" stop-opacity="0" />
				</radialGradient>
			</defs>
			<!-- MAIN BUBBLE -->
			<g id="kumonosu-main-parallax">
				<g>
					<animateTransform attributeName="transform" type="translate" dur="8.5s" repeatCount="indefinite" calcMode="spline" keyTimes="0; .3; .65; 1" keySplines=".42 0 .58 1; .42 0 .58 1; .42 0 .58 1" values="0 0; 7 -24; -5 -10; 0 0" />
					<g clip-path="url(#kumonosu-main-clip)">
						<image href="https://kumonosu.net/wp-content/uploads/2026/01/176d.jpg.webp" x="180" y="20" width="800" height="800" preserveAspectRatio="xMidYMid slice" />
						<rect x="0" y="0" width="1100" height="1000" fill="url(#kumonosu-main-milk)" />
					</g>
					<g class="kumonosu-marquee">
						<text class="kumonosu-marquee-text" dy="-14">
							<textPath href="#kumonosu-main-path" startOffset="0%"> With great power comes great responsibility With great power comes great responsibility With great power comes great responsibility
								<animate attributeName="startOffset" from="0%" to="100%" dur="25s" repeatCount="indefinite" />
							</textPath>
						</text>
						<text class="kumonosu-marquee-text" dy="-14">
							<textPath href="#kumonosu-main-path" startOffset="-100%"> With great power comes great responsibility With great power comes great responsibility With great power comes great responsibility
								<animate attributeName="startOffset" from="-100%" to="0%" dur="25s" repeatCount="indefinite" />
							</textPath>
						</text>
					</g>
					<use href="#kumonosu-main-path" class="kumonosu-bubble-edge kumonosu-bubble-edge-main" />
				</g>
			</g>
			<!-- SECONDARY BUBBLE -->
			<g id="kumonosu-secondary-parallax">
				<g>
					<animateTransform attributeName="transform" type="translate" dur="7s" begin="-2s" repeatCount="indefinite" calcMode="spline" keyTimes="0; .5; 1" keySplines=".42 0 .58 1; .42 0 .58 1" values="0 0; -14 -30; 0 0" />
					<g clip-path="url(#kumonosu-secondary-clip)">
						<image href="https://kumonosu.net/wp-content/uploads/2026/01/176b.jpg.webp" x="80" y="620" width="500" height="400" preserveAspectRatio="xMidYMid slice" />
					</g>
					<use href="#kumonosu-secondary-path" class="kumonosu-bubble-edge" />
				</g>
			</g>
			<!-- DROP BUBBLE -->
			<g id="kumonosu-drop-parallax">
				<g>
					<animateTransform attributeName="transform" type="translate" dur="6s" begin="-4s" repeatCount="indefinite" calcMode="spline" keyTimes="0; .5; 1" keySplines=".42 0 .58 1; .42 0 .58 1" values="0 0; 15 -34; 0 0" />
					<g clip-path="url(#kumonosu-drop-clip)">
						<image href="https://kumonosu.net/wp-content/uploads/2026/01/176c.jpg.webp" x="650" y="700" width="300" height="300" preserveAspectRatio="xMidYMid slice" />
					</g>
					<use href="#kumonosu-drop-path" class="kumonosu-bubble-edge" />
				</g>
			</g>
		</svg>
	</section>
</div>
* {
	box-sizing: border-box;
}
html, body {
	width: 100%;
	min-height: 100%;
	margin: 0;
	padding: 0;
	overflow: hidden;
	background: #f9fafb;
	font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.kumonosu-container {
	position: relative;
	width: 100vw;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	background: radial-gradient(ellipse 28% 32% at 68% 35%,
			rgba(255, 255, 255, 0.95) 0%,
			rgba(248, 250, 252, 0.6) 23%,
			rgba(241, 245, 249, 0.3) 48%,
			transparent 74%),
		linear-gradient(116deg, #fdfdfd 0%, #f9fafb 37%, #f3f4f6 75%, #e5e7eb 100%);
}
.kumonosu-visual {
	position: relative;
	z-index: 10;
	width: min(95vw, 1020px);
	aspect-ratio: 1020 / 578;
	pointer-events: none;
	isolation: isolate;
}
.kumonosu-lighting {
	position: absolute;
	z-index: -10;
	inset: -10% -10%;
	overflow: visible;
	pointer-events: none;
}
.kumonosu-glow {
	position: absolute;
	display: block;
	border-radius: 50%;
	pointer-events: none;
	transform: translateZ(0);
	will-change: transform;
}
.kumonosu-glow--ambient {
	left: 10%;
	top: 10%;
	width: 80%;
	height: 80%;
	background: radial-gradient(circle at center, rgba(255, 255, 255, 0.8) 0%, transparent 70%);
	filter: blur(60px);
}
.kumonosu-liquid-scene {
	position: relative;
	z-index: 10;
	display: block;
	width: 100%;
	height: 100%;
	overflow: visible;
}
.kumonosu-bubble-edge {
	fill: rgba(255, 255, 255, 0.02);
	stroke: url(#kumonosu-glass-stroke);
	stroke-width: 2;
	vector-effect: non-scaling-stroke;
	filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.98)) drop-shadow(0 4px 12px rgba(86, 70, 63, 0.08));
}
.kumonosu-bubble-edge-main {
	stroke-width: 3.5;
	filter: drop-shadow(0 0 2px rgba(255, 255, 255, 1)) drop-shadow(0 8px 20px rgba(86, 70, 63, 0.12));
}
.kumonosu-marquee-text {
	fill: #1e293b;
	font-family: Inter, sans-serif;
	font-size: 20px;
	font-weight: 500;
	letter-spacing: 4px;
	text-transform: uppercase;
}
const kumonosuMain = document.querySelector("#kumonosu-main-parallax");
const kumonosuSecondary = document.querySelector("#kumonosu-secondary-parallax");
const kumonosuDrop = document.querySelector("#kumonosu-drop-parallax");
const PARALLAX_INTENSITY = 0.5;
const PARALLAX_SMOOTHNESS = 0.025;
let targetX = 0,
	targetY = 0,
	currentX = 0,
	currentY = 0;
window.addEventListener("pointermove", (event) => {
	const normalizedX = event.clientX / window.innerWidth - 0.5;
	const normalizedY = event.clientY / window.innerHeight - 0.5;
	targetX = normalizedX * 180 * PARALLAX_INTENSITY;
	targetY = normalizedY * 130 * PARALLAX_INTENSITY;
});
document.documentElement.addEventListener("mouseleave", () => {
	targetX = 0;
	targetY = 0;
});

function render() {
	currentX += (targetX - currentX) * PARALLAX_SMOOTHNESS;
	currentY += (targetY - currentY) * PARALLAX_SMOOTHNESS;
	kumonosuMain.style.transform = `translate(${currentX}px, ${currentY}px)`;
	kumonosuSecondary.style.transform = `translate(${currentX * 1.55}px, ${currentY * 1.55}px)`;
	kumonosuDrop.style.transform = `translate(${currentX * 2.15}px, ${currentY * 2.15}px)`;
	requestAnimationFrame(render);
}
render();

Explanation 詳しい説明

基本構造

SVGをベースに、画像を切り抜いて表示する複数のバブルを配置しています。

メインとなる大きなバブルに加えて、左下と右下にも異なるサイズのバブルを配置。それぞれに画像を設定し、SVGのclipPathを使って画像をバブルの形に切り抜いています。

バブルの輪郭にはグラデーションのSVGストロークとドロップシャドウを組み合わせ、透明感のある光の表現を追加しています。

また、SVGのanimateanimateTransformを使用して、バブルの形状や位置を一定の周期でゆっくり変化させています。

JavaScriptではpointermoveを取得し、マウスの位置に応じて各バブルを移動させています。メイン・サブ・小さなバブルで移動量を変えることで、レイヤーごとの奥行きを表現しています。

仕様とカスタム

バブルの形状はSVGのpathで定義しています。d属性の値を変更することで、丸型や歪んだ形など、好きな形状に変更できます。

画像はSVG内のimage要素に設定されているため、hrefを変更することで使用する画像を差し替えられます。

バブルの動きはanimatedurで調整できます。値を大きくするとゆっくりとした動きになり、小さくすると動きが速くなります。

マウスによるパララックスの強さはJavaScriptのPARALLAX_INTENSITYで調整できます。さらに、各バブルには異なる倍率が設定されているため、currentXcurrentYに掛ける数値を変更することで奥行き感を調整できます。

例えばメインバブルは1、セカンダリバブルは1.55、小さなバブルは2.15の倍率で動くため、手前にある要素ほど大きく動かすことで立体的な動きを作っています。

注意点

SVGのclipPathimageを使用しているため、画像を変更する場合は画像のサイズや配置も合わせて調整する必要があります。

また、SVG内のアニメーションとJavaScriptによるパララックスを同時に使用しているため、動きを大きくしすぎると全体が落ち着かない印象になる場合があります。PARALLAX_INTENSITYや各要素の移動倍率を調整し、ゆっくりとしたSVGアニメーションとのバランスを取るのがおすすめです。

JavaScriptではrequestAnimationFrameを使って常に位置を更新しているため、画面上に大量の要素を追加すると負荷が高くなる可能性があります。バブルの数を増やす場合は、パララックス処理やSVGアニメーションの数にも注意が必要です。