CSSとJSで作る、マウスに反応して揺らぐ雲やオーロラのようなアニメーション

ビジュアル

CSSとJSで作る、マウスに反応して揺らぐ雲やオーロラのようなアニメーション

投稿日2026/02/22

更新日2026/2/14

ヒーローエリアに、静止画像では出せない光のうねりを入れたい。

このサンプルは、WebGLシェーダーで抽象的な流れ模様と発光のハイライトを描き、マウス操作に反応して雲やオーロラのような、揺らぐアニメーションを作ります。

キャンバスは全面固定、見出しは別レイヤーで重ねる構成です。

Preview プレビュー

Code コード

<canvas id="kumonosu-gl-canvas"></canvas>
<div class="kumonosu-content-layer">
	<h1 class="text-5xl md:text-8xl font-extrabold"> KUMONOSU <br>
		<span class="text-3xl md:text-5xl opacity-70">Generative Art</span>
	</h1>
</div>
@import url(https://fonts.googleapis.com/css2?family=Inter:wght@800&display=swap);
body {
	font-family: 'Inter', sans-serif;
	background-color: black;
	margin: 0;
	overflow: hidden;
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 100vh;
}
#kumonosu-gl-canvas {
	position: fixed;
	top: 0;
	left: 0;
	width: 100vw;
	height: 100vh;
	z-index: 0;
}
.kumonosu-content-layer {
	position: relative;
	z-index: 10;
	pointer-events: none;
	text-align: center;
}
h1 {
	color: white;
	line-height: 0.85;
	letter-spacing: -0.05em;
	user-select: none;
	opacity: 0.95;
	text-transform: uppercase;
}
const canvas = document.getElementById('kumonosu-gl-canvas');
const gl = canvas.getContext('webgl');
// --- Shaders ---
const vertexShaderSource = `
            attribute vec2 position;
            void main() {
                gl_Position = vec4(position, 0.0, 1.0);
            }
        `;
const fragmentShaderSource = `
            precision highp float;
            uniform float u_time;
            uniform vec2 u_resolution;
            uniform vec2 u_mouse;

            mat2 rot(float a) {
                float s = sin(a), c = cos(a);
                return mat2(c, s, -s, c);
            }

            float noise(vec2 p) {
                return sin(p.x) * sin(p.y);
            }

            float fbm(vec2 p) {
                float v = 0.0;
                float amp = 0.5;
                for (int i = 0; i < 6; i++) {
                    p *= rot(0.5);
                    v += amp * noise(p);
                    p *= 2.1;
                    amp *= 0.45;
                }
                return v;
            }

            void main() {
                vec2 uv = gl_FragCoord.xy / u_resolution.xy;
                vec2 mouse = u_mouse / u_resolution;
                float dist = distance(uv, mouse);
                
                vec2 p = (uv * 2.0 - 1.0);
                p.x *= u_resolution.x / u_resolution.y;

                float t = u_time * 0.12;
                vec2 flow = p;
                
                // Subtle mouse pull
                vec2 dir = normalize(uv - mouse);
                float strength = 0.35 / (dist + 0.45);
                flow += dir * strength * 0.15;

                for(int i = 0; i < 3; i++) {
                    flow.x += 0.35 * sin(flow.y * 1.4 + t + float(i));
                    flow.y += 0.25 * cos(flow.x * 1.3 + t * 0.7 + float(i));
                }

                float pattern = fbm(flow * 1.4 + t);
                float pattern2 = fbm(flow * 2.8 - t * 0.4);
                
                float sheen = smoothstep(0.25, 0.0, abs(pattern - 0.12));
                sheen += smoothstep(0.35, 0.0, abs(pattern2 - 0.18)) * 0.6;

                // Deep color palette: Midnight Navy, Web-purple, Sunset-gold
                vec3 blue = vec3(0.02, 0.15, 0.6);
                vec3 purple = vec3(0.4, 0.0, 0.6);
                vec3 orange = vec3(0.9, 0.45, 0.1);
                vec3 black = vec3(0.01, 0.01, 0.04);

                vec3 baseColor = mix(black, blue, clamp(pattern * 2.2 + 0.4, 0.0, 1.0));
                baseColor = mix(baseColor, purple, clamp(pattern2 * 1.6, 0.0, 1.0));
                
                float goldMask = smoothstep(0.12, 0.48, pattern * pattern2);
                baseColor = mix(baseColor, orange, goldMask * 0.65);

                vec3 finalColor = baseColor + sheen * mix(blue, orange, uv.x) * 0.45;
                
                // Glow around the "web" threads
                finalColor += (1.0 - smoothstep(0.0, 0.55, dist)) * vec3(0.06, 0.06, 0.18);

                float mask = smoothstep(1.6, 0.0, length(p));
                finalColor *= mask;
                
                gl_FragColor = vec4(pow(finalColor * 1.9, vec3(1.15)), 1.0);
            }
        `;
// --- WebGL Setup ---
const createShader = (gl, type, source) => {
	const shader = gl.createShader(type);
	gl.shaderSource(shader, source);
	gl.compileShader(shader);
	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
		console.error(gl.getShaderInfoLog(shader));
		gl.deleteShader(shader);
		return null;
	}
	return shader;
};
const program = gl.createProgram();
gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexShaderSource));
gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource));
gl.linkProgram(program);
gl.useProgram(program);
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const position = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
const timeLoc = gl.getUniformLocation(program, 'u_time');
const resLoc = gl.getUniformLocation(program, 'u_resolution');
const mouseLoc = gl.getUniformLocation(program, 'u_mouse');
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
window.addEventListener('mousemove', (e) => {
	mouseX = e.clientX;
	mouseY = window.innerHeight - e.clientY;
});

function render(time) {
	canvas.width = window.innerWidth;
	canvas.height = window.innerHeight;
	gl.viewport(0, 0, canvas.width, canvas.height);
	gl.uniform1f(timeLoc, time * 0.001);
	gl.uniform2f(resLoc, canvas.width, canvas.height);
	gl.uniform2f(mouseLoc, mouseX, mouseY);
	gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
	requestAnimationFrame(render);
}
window.addEventListener('resize', () => {
	canvas.width = window.innerWidth;
	canvas.height = window.innerHeight;
});
requestAnimationFrame(render);
https://cdn.tailwindcss.com

Explanation 詳しい説明

仕様

画面全体に固定したcanvasへWebGLで全面描画し、板ポリゴン相当(TRIANGLE_STRIP)にフラグメントシェーダーを適用しています。

見た目はすべてシェーダー内で生成し、u_timeで時間変化、u_mouseでマウス位置の影響を加えています。

模様はsinベースの簡易ノイズを重ねるfbmと、座標をゆらすフロー変形(sin/cosの反復)で作っています。

そこから特定の値域を拾ってハイライト(sheen)を作り、深い青〜紫〜橙のパレットで合成することで、帯状の光が流れるオーロラっぽい質感になりました。

マウス周辺には距離に応じた淡いグローを足し、触っている感を補強しています。

  • フルスクリーンcanvasにWebGLで全面描画
  • u_timeで流れ、u_mouseで反応を付与
  • fbm+座標変形で光の帯のうねりを生成
  • ハイライト抽出+カラーパレット合成で発光表現
  • テキストは上レイヤーに重ねて表示

カスタム

印象を変えるなら「速度」「発光の強さ」「色」「マウス反応」を調整すると効きます。

  • 動きの速さ:float t = u_time * 0.12; の係数
  • オーロラの細かさ:fbm(flow * 1.4...) / flow * 2.8 の倍率
  • 発光の強さ:sheenの閾値(0.12/0.18など)や加算係数(* 0.45
  • マウス反応量:strength* 0.15 の係数
  • 色味:blue / purple / orange / black のRGBを差し替え

中央の広がりは mask = smoothstep(1.6, 0.0, length(p)); で調整できます。

注意点

WebGL依存のため、WebGL非対応環境では表示できません。常時requestAnimationFrameで描画するので、端末によっては負荷が上がります。

背景用途では、必要に応じて解像度を落とす(dpr制限)・非表示時に停止するなどの対策が有効です。

また、このコードは毎フレームcanvas.width/heightを更新しているため、環境によっては余計なコストになります。運用ではリサイズ時だけ更新する形にすると安定しやすいです。

  • WebGL非対応環境では動作しない
  • 常時描画で負荷が出る場合がある(dpr制限など推奨)
  • canvasサイズ更新は毎フレームではなくリサイズ時にすると軽い