ビジュアル
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サイズ更新は毎フレームではなくリサイズ時にすると軽い