Skip to content

Instantly share code, notes, and snippets.

@kobitoDevelopment
Last active September 12, 2025 13:27
Show Gist options
  • Select an option

  • Save kobitoDevelopment/4b0e244cacdfd5c64742058be55dbf74 to your computer and use it in GitHub Desktop.

Select an option

Save kobitoDevelopment/4b0e244cacdfd5c64742058be55dbf74 to your computer and use it in GitHub Desktop.
canvas.water {
display: block;
width: 100%;
height: 100vh;
}
<canvas class="water"></canvas>
// ============================================================================
// 1. 初期設定・定数定義
// ============================================================================
// DOM要素とWebGLコンテキストの基本設定
const el = document.querySelector("canvas.water");
let gl;
// ============================================================================
// 2. WebGL設定・拡張機能の検出
// ============================================================================
// WebGL拡張機能とテクスチャ形式の互換性を確認
function loadConfig() {
const canvas = document.createElement("canvas");
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
let extensions = {};
// 必要な拡張機能をロード
["OES_texture_float", "OES_texture_half_float", "OES_texture_float_linear", "OES_texture_half_float_linear"].forEach(function (name) {
const extension = gl.getExtension(name);
if (extension) {
extensions[name] = extension;
}
});
const configs = [];
// 各テクスチャ形式の設定を作成
function createConfig(type, glType, arrayType) {
const name = "OES_texture_" + type,
nameLinear = name + "_linear",
linearSupport = nameLinear in extensions,
configExtensions = [name];
if (linearSupport) {
configExtensions.push(nameLinear);
}
return {
type: glType,
arrayType: arrayType,
linearSupport: linearSupport,
extensions: configExtensions,
};
}
// FLOAT形式を追加
configs.push(createConfig("float", gl.FLOAT, Float32Array));
// HALF_FLOAT形式が利用可能なら追加
if (extensions.OES_texture_half_float) {
configs.push(createConfig("half_float", extensions.OES_texture_half_float.HALF_FLOAT_OES, null));
}
// テクスチャとフレームバッファーを作成してテスト
const texture = gl.createTexture();
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 使用可能な設定を見つけるまでテスト
let config = null;
for (let i = 0; i < configs.length; i++) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 32, 32, 0, gl.RGBA, configs[i].type, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
config = configs[i];
break;
}
}
return config;
}
// ============================================================================
// 3. ユーティリティ関数
// ============================================================================
// object-fit: coverの計算(縦横比を維持して大きい方に合わせる)
function calculateCoverScale(imageWidth, imageHeight, containerWidth, containerHeight) {
const scaleX = containerWidth / imageWidth;
const scaleY = containerHeight / imageHeight;
return Math.max(scaleX, scaleY); // 大きい方のスケールを使用してcover効果
}
// カラー文字列を解析してRGBA値を取得
function parseColor(colorString) {
// 簡単なカラーパース(#hex、rgb()、rgba()に対応)
if (colorString.startsWith("#")) {
const hex = colorString.slice(1);
const r = parseInt(hex.slice(0, 2), 16) / 255;
const g = parseInt(hex.slice(2, 4), 16) / 255;
const b = parseInt(hex.slice(4, 6), 16) / 255;
return [r, g, b, 1.0];
}
// デフォルトは青色
return [0.37, 0.76, 0.87, 1.0]; // #5fc3dd相当
}
// ============================================================================
// 4. シェーダープログラム作成
// ============================================================================
// シェーダープログラムをコンパイル・リンクして作成
function createProgram(vertexSource, fragmentSource, uniformValues) {
// シェーダーソースをコンパイル
function compileSource(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
const program = {};
program.id = gl.createProgram();
// 頂点シェーダーとフラグメントシェーダーをアタッチ
gl.attachShader(program.id, compileSource(gl.VERTEX_SHADER, vertexSource));
gl.attachShader(program.id, compileSource(gl.FRAGMENT_SHADER, fragmentSource));
gl.linkProgram(program.id);
program.uniforms = {};
program.locations = {};
gl.useProgram(program.id);
gl.enableVertexAttribArray(0);
// uniform変数の場所を自動取得
let match;
let name;
const regex = /uniform (\w+) (\w+)/g;
const shaderCode = vertexSource + fragmentSource;
while ((match = regex.exec(shaderCode)) != null) {
name = match[2];
program.locations[name] = gl.getUniformLocation(program.id, name);
}
return program;
}
// テクスチャをバインド
function bindTexture(texture, unit) {
gl.activeTexture(gl.TEXTURE0 + (unit || 0));
gl.bindTexture(gl.TEXTURE_2D, texture);
}
// ============================================================================
// 5. 初期化
// ============================================================================
// WebGL設定を取得
const config = loadConfig();
// ============================================================================
// 6. createRipples関数 - メインファクトリー関数
// ============================================================================
function createRipples(element, options) {
// 基本設定の初期化
const state = {
el: element,
interactive: options.interactive,
resolution: options.resolution, // 波紋計算用の固定解像度 (通常400x400)
textureDelta: new Float32Array([1 / options.resolution, 1 / options.resolution]),
// 波紋パラメータの設定
perturbance: options.perturbance, // 波の強度 (通常0.01)
dropRadius: options.dropRadius, // 波紋の半径 (通常15)
// 背景設定(画像または色)
backgroundType: options.backgroundType || "image", // 'image' または 'color'
imageUrl: options.imageUrl || "assets/images/1.jpg", // デフォルト画像
backgroundColor: options.backgroundColor || "#5fc3dd", // デフォルト色
crossOrigin: options.crossOrigin,
// Canvas要素
canvas: element,
context: null,
// ダブルバッファリング用
textures: [],
framebuffers: [],
bufferWriteIndex: 0,
bufferReadIndex: 1,
// 状態フラグ
visible: true,
running: true,
inited: true,
destroyed: false,
// シェーダープログラム
dropProgram: null,
updateProgram: null,
renderProgram: null,
// その他
quad: null,
backgroundTexture: null,
imageSource: null,
backgroundWidth: null,
backgroundHeight: null,
};
// Canvas初期化
state.canvas.width = element.offsetWidth;
state.canvas.height = element.offsetHeight;
// WebGLコンテキストの取得と拡張機能の有効化
state.context = gl = element.getContext("webgl") || element.getContext("experimental-webgl");
config.extensions.forEach(function (name) {
gl.getExtension(name);
});
// ダブルバッファリング用のテクスチャとフレームバッファの準備
const arrayType = config.arrayType;
const textureData = arrayType ? new arrayType(state.resolution * state.resolution * 4) : null;
// 2つのテクスチャとフレームバッファを作成(ダブルバッファリング用)
for (let i = 0; i < 2; i++) {
const texture = gl.createTexture();
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// テクスチャの設定
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, config.linearSupport ? gl.LINEAR : gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, config.linearSupport ? gl.LINEAR : gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, state.resolution, state.resolution, 0, gl.RGBA, config.type, textureData);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
state.textures.push(texture);
state.framebuffers.push(framebuffer);
}
// 全画面描画用の四角形バッファの作成
state.quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, state.quad);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, +1, -1, +1, +1, -1, +1]), gl.STATIC_DRAW);
// ------------------------------------------------------------------------
// 内部関数の定義
// ------------------------------------------------------------------------
// ダブルバッファリング用のインデックスを交換
function swapBufferIndices() {
state.bufferWriteIndex = 1 - state.bufferWriteIndex;
state.bufferReadIndex = 1 - state.bufferReadIndex;
}
// 全画面四角形を描画(-1〜+1の正規化座標)
function drawQuad() {
gl.bindBuffer(gl.ARRAY_BUFFER, state.quad);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
// シェーダープログラムの初期化
function initShaders() {
// 共通の頂点シェーダー(-1〜+1の正規化座標を0〜1のテクスチャ座標に変換)
const vertexShader = [
"attribute vec2 vertex;", // 頂点属性(-1〜+1)
"varying vec2 coord;", // フラグメントシェーダーに渡すテクスチャ座標
"void main() {",
"coord = vertex * 0.5 + 0.5;", // -1〜+1 を 0〜1 に変換
"gl_Position = vec4(vertex, 0.0, 1.0);",
"}",
].join("\n");
// 1. 波紋生成シェーダープログラム(dropProgram)
state.dropProgram = createProgram(
vertexShader,
[
"precision highp float;",
"const float PI = 3.141592653589793;",
"uniform sampler2D texture;", // 現在の波紋状態
"uniform vec2 center;", // 波紋の中心位置
"uniform float radius;", // 波紋の半径
"uniform float strength;", // 波紋の強度
"varying vec2 coord;",
"void main() {",
"vec4 info = texture2D(texture, coord);",
// 中心からの距離に基づいて波紋の強度を計算
"float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);",
// コサイン関数でなめらかな波形を作成
"drop = 0.5 - cos(drop * PI) * 0.5;",
// 既存の波紋データに新しい波紋を加算
"info.r += drop * strength;",
"gl_FragColor = info;",
"}",
].join("\n")
);
// 2. 波紋更新シェーダープログラム(updateProgram)
state.updateProgram = createProgram(
vertexShader,
[
"precision highp float;",
"uniform sampler2D texture;", // 現在の波紋状態
"uniform vec2 delta;", // テクセル間のオフセット(1/resolution)
"varying vec2 coord;",
"void main() {",
"vec4 info = texture2D(texture, coord);", // 現在のピクセルの状態
"vec2 dx = vec2(delta.x, 0.0);", // X方向のオフセット
"vec2 dy = vec2(0.0, delta.y);", // Y方向のオフセット
// 上下左右の隣接ピクセルの高度の平均を計算(ラプラシアン演算子の近似)
"float average = (",
"texture2D(texture, coord - dx).r +", // 左
"texture2D(texture, coord - dy).r +", // 上
"texture2D(texture, coord + dx).r +", // 右
"texture2D(texture, coord + dy).r", // 下
") * 0.25;",
// 波動方程式: 速度 += (平均高度 - 現在高度) * 復元力
"info.g += (average - info.r) * 2.0;",
// 減衰効果を適用(エネルギー保存を防ぎ、波が永続しないように)
"info.g *= 0.995;",
// 新しい高度 = 現在高度 + 速度
"info.r += info.g;",
"gl_FragColor = info;",
"}",
].join("\n")
);
// テクセル間のオフセットをユニフォーム変数に設定
gl.uniform2fv(state.updateProgram.locations.delta, state.textureDelta);
// 3. 最終レンダリングシェーダープログラム(renderProgram)
state.renderProgram = createProgram(
// 頂点シェーダー(背景とリップルの座標系を分けて処理)
[
"precision highp float;",
"attribute vec2 vertex;", // 頂点属性
"uniform vec2 topLeft;", // 背景テクスチャの左上座標
"uniform vec2 bottomRight;", // 背景テクスチャの右下座標
"uniform vec2 containerRatio;", // アスペクト比補正
"varying vec2 ripplesCoord;", // 波紋テクスチャ用座標
"varying vec2 backgroundCoord;", // 背景テクスチャ用座標
"void main() {",
// 背景テクスチャの座標を計算(CSS背景画像の位置・サイズに対応)
"backgroundCoord = mix(topLeft, bottomRight, vertex * 0.5 + 0.5);",
"backgroundCoord.y = 1.0 - backgroundCoord.y;", // Y軸を反転
// 波紋テクスチャの座標を計算(正方形にアスペクト比を補正)
"ripplesCoord = vec2(vertex.x, -vertex.y) * containerRatio * 0.5 + 0.5;",
"gl_Position = vec4(vertex.x, -vertex.y, 0.0, 1.0);",
"}",
].join("\n"),
// フラグメントシェーダー(波紋による屈折効果とスペキュラーハイライトを計算)
[
"precision highp float;",
"uniform sampler2D samplerBackground;", // 背景画像テクスチャ
"uniform sampler2D samplerRipples;", // 波紋データテクスチャ
"uniform vec2 delta;", // テクセル間のオフセット
"uniform float perturbance;", // 屈折の強度
"varying vec2 ripplesCoord;",
"varying vec2 backgroundCoord;",
"void main() {",
// 現在のピクセルとその隣接ピクセルの波紋高度を取得
"float height = texture2D(samplerRipples, ripplesCoord).r;",
"float heightX = texture2D(samplerRipples, vec2(ripplesCoord.x + delta.x, ripplesCoord.y)).r;",
"float heightY = texture2D(samplerRipples, vec2(ripplesCoord.x, ripplesCoord.y + delta.y)).r;",
// 法線ベクトルを計算(高度差から勾配を求める)
"vec3 dx = vec3(delta.x, heightX - height, 0.0);",
"vec3 dy = vec3(0.0, heightY - height, delta.y);",
"vec2 offset = -normalize(cross(dy, dx)).xz;", // 外積で法線を求めXZ成分を使用
// スペキュラーハイライトを計算(光源方向: -0.6, 1.0)
"float specular = pow(max(0.0, dot(offset, normalize(vec2(-0.6, 1.0)))), 4.0);",
// 背景画像を法線方向にオフセットして屈折効果を表現し、スペキュラーを加算
"gl_FragColor = texture2D(samplerBackground, backgroundCoord + offset * perturbance) + specular;",
"}",
].join("\n")
);
// テクセル間のオフセットをユニフォーム変数に設定
gl.uniform2fv(state.renderProgram.locations.delta, state.textureDelta);
}
// 背景テクスチャの初期化
function initTexture() {
state.backgroundTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, state.backgroundTexture);
// Y軸を反転してWebGLの座標系と一致させる
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 線形補間を有効化してなめらかな描画を実現
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
// 単色テクスチャを作成(背景色指定の場合)
function createColorTexture() {
gl.bindTexture(gl.TEXTURE_2D, state.backgroundTexture);
// 指定された色でピクセルデータを作成
const colorRgba = parseColor(state.backgroundColor);
const colorData = new Uint8Array(4);
colorData[0] = Math.round(colorRgba[0] * 255); // R
colorData[1] = Math.round(colorRgba[1] * 255); // G
colorData[2] = Math.round(colorRgba[2] * 255); // B
colorData[3] = Math.round(colorRgba[3] * 255); // A
// 1x1の単色テクスチャを作成
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, colorData);
// 色の場合は画像サイズをキャンバスサイズと同じに設定
state.backgroundWidth = state.canvas.width;
state.backgroundHeight = state.canvas.height;
updateCoverMapping();
}
// object-fit: cover相当のテクスチャマッピング計算
function updateCoverMapping() {
if (!state.renderProgram) {
return; // シェーダープログラムが未初期化の場合は何もしない
}
if (state.backgroundType === "color") {
// 単色の場合は全体をカバー
state.renderProgram.uniforms.topLeft = new Float32Array([0.0, 0.0]);
state.renderProgram.uniforms.bottomRight = new Float32Array([1.0, 1.0]);
} else {
// 画像の場合:object-fit: cover計算
const canvasWidth = state.canvas.width;
const canvasHeight = state.canvas.height;
const scale = calculateCoverScale(state.backgroundWidth, state.backgroundHeight, canvasWidth, canvasHeight);
// スケール後のサイズ
const scaledWidth = state.backgroundWidth * scale;
const scaledHeight = state.backgroundHeight * scale;
// 中央配置でのオフセット計算
const offsetX = (scaledWidth - canvasWidth) * 0.5;
const offsetY = (scaledHeight - canvasHeight) * 0.5;
// テクスチャ座標の計算(0.0〜1.0)
state.renderProgram.uniforms.topLeft = new Float32Array([offsetX / scaledWidth, offsetY / scaledHeight]);
state.renderProgram.uniforms.bottomRight = new Float32Array([state.renderProgram.uniforms.topLeft[0] + canvasWidth / scaledWidth, state.renderProgram.uniforms.topLeft[1] + canvasHeight / scaledHeight]);
}
// アスペクト比補正用の比率計算(波紋用)
const maxSide = Math.max(state.canvas.width, state.canvas.height);
state.renderProgram.uniforms.containerRatio = new Float32Array([state.canvas.width / maxSide, state.canvas.height / maxSide]);
}
// 背景の読み込み処理(画像または色)
function loadBackground() {
gl = state.context;
if (state.backgroundType === "color") {
// 色指定の場合:単色テクスチャを作成
createColorTexture();
return;
}
// 画像指定の場合:画像を読み込み
const newImageSource = state.imageUrl;
// 同じ画像の場合は処理をスキップ
if (newImageSource === state.imageSource) {
return;
}
state.imageSource = newImageSource;
// 画像が指定されていない場合は色テクスチャを設定
if (!state.imageSource) {
createColorTexture();
return;
}
// 画像要素を作成して読み込み処理を開始
const image = new Image();
// 画像読み込み成功時の処理
image.onload = function () {
gl = state.context;
// 画像サイズが2の累乗かを判定(テクスチャラッピング方法の決定用)
function isPowerOfTwo(x) {
return (x & (x - 1)) == 0;
}
// 2の累乗の場合はREPEAT、そうでなければCLAMP_TO_EDGEを使用
const wrapping = isPowerOfTwo(image.width) && isPowerOfTwo(image.height) ? gl.REPEAT : gl.CLAMP_TO_EDGE;
// 背景テクスチャに画像を設定
gl.bindTexture(gl.TEXTURE_2D, state.backgroundTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapping);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapping);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 画像サイズを記録してobject-fit:cover計算に使用
state.backgroundWidth = image.width;
state.backgroundHeight = image.height;
updateCoverMapping();
};
// 画像読み込み失敗時の処理
image.onerror = function () {
gl = state.context;
createColorTexture();
};
// CORS設定
if (state.crossOrigin) {
image.crossOrigin = state.crossOrigin;
}
image.src = state.imageSource;
}
// 波紋シミュレーション更新
function update() {
// 波紋計算用の解像度でビューポートを設定
gl.viewport(0, 0, state.resolution, state.resolution);
// 書き込み先フレームバッファーにバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffers[state.bufferWriteIndex]);
// 読み取り元テクスチャをバインド
bindTexture(state.textures[state.bufferReadIndex]);
// 波紋更新シェーダーを使用
gl.useProgram(state.updateProgram.id);
// 波の伝播計算を実行
drawQuad();
// ダブルバッファリング: 読み書きバッファを交換
swapBufferIndices();
}
// 最終画面レンダリング
function render() {
// 画面バッファに描画
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, state.canvas.width, state.canvas.height);
// ブレンディングを有効化
gl.enable(gl.BLEND);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// レンダリングシェーダーを使用
gl.useProgram(state.renderProgram.id);
// テクスチャをバインド: 0=背景画像, 1=波紋データ
bindTexture(state.backgroundTexture, 0);
bindTexture(state.textures[0], 1);
// シェーダーユニフォーム変数を設定
gl.uniform1f(state.renderProgram.locations.perturbance, state.perturbance); // 波の強度
gl.uniform2fv(state.renderProgram.locations.topLeft, state.renderProgram.uniforms.topLeft); // テクスチャ座標の左上
gl.uniform2fv(state.renderProgram.locations.bottomRight, state.renderProgram.uniforms.bottomRight); // テクスチャ座標の右下
gl.uniform2fv(state.renderProgram.locations.containerRatio, state.renderProgram.uniforms.containerRatio); // アスペクト比
gl.uniform1i(state.renderProgram.locations.samplerBackground, 0); // 背景テクスチャ
gl.uniform1i(state.renderProgram.locations.samplerRipples, 1); // 波紋テクスチャ
// 四角形を描画して画面全体を覆う
drawQuad();
// ブレンディングを無効化
gl.disable(gl.BLEND);
}
// メインレンダリングループ
function step() {
gl = state.context;
// 非表示の場合は描画をスキップ
if (!state.visible) {
return;
}
// テクスチャマッピングの更新(object-fit: cover相当)
updateCoverMapping();
// アニメーションが有効な場合は波紋の更新処理を実行
if (state.running) {
update(); // 波紋シミュレーションの更新
}
// 最終的な画面描画
render();
}
// 指定した座標に波紋を生成(コアロジック)
function drop(x, y, radius, strength) {
gl = state.context;
// 要素のサイズを取得
const elWidth = state.el.offsetWidth;
const elHeight = state.el.offsetHeight;
const longestSide = Math.max(elWidth, elHeight); // 正方形基準で正規化するため
// 半径を正規化(0〜1の範囲)
radius = radius / longestSide;
// 座標を正規化座標系に変換(-1〜+1の範囲)
const dropPosition = new Float32Array([
(2 * x - elWidth) / longestSide, // X: 0〜幅 → -1〜+1
(elHeight - 2 * y) / longestSide, // Y: 0〜高さ → +1〜-1 (上下反転)
]);
// 波紋計算用の固定解像度でビューポートを設定
gl.viewport(0, 0, state.resolution, state.resolution);
// 書き込み先フレームバッファーにバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffers[state.bufferWriteIndex]);
// 読み取り元テクスチャをバインド
bindTexture(state.textures[state.bufferReadIndex]);
// 波紋生成シェーダーを使用
gl.useProgram(state.dropProgram.id);
// シェーダーのユニフォーム変数を設定
gl.uniform2fv(state.dropProgram.locations.center, dropPosition); // 波紋の中心位置
gl.uniform1f(state.dropProgram.locations.radius, radius); // 波紋の半径
gl.uniform1f(state.dropProgram.locations.strength, strength); // 波紋の強度
// 波紋を描画してバッファーを入れ替え
drawQuad();
swapBufferIndices();
}
// ポインター(マウス/タッチ)の位置に波紋を生成
function dropAtPointer(pointer, radius, strength) {
// 要素のボーダー幅を考慮して正確な相対座標を計算
const borderLeft = parseInt(window.getComputedStyle(state.el).borderLeftWidth) || 0;
const borderTop = parseInt(window.getComputedStyle(state.el).borderTopWidth) || 0;
// ページ座標から要素内の相対座標に変換してdropメソッドを呼び出し
drop(pointer.pageX - state.el.offsetLeft - borderLeft, pointer.pageY - state.el.offsetTop - borderTop, radius, strength);
}
// ウィンドウリサイズ時にキャンバスサイズを更新
function updateSize() {
const newWidth = state.el.offsetWidth;
const newHeight = state.el.offsetHeight;
// サイズが変更された場合のみキャンバスを更新
if (newWidth != state.canvas.width || newHeight != state.canvas.height) {
state.canvas.width = newWidth;
state.canvas.height = newHeight;
// 色背景の場合はサイズを更新
if (state.backgroundType === "color") {
state.backgroundWidth = newWidth;
state.backgroundHeight = newHeight;
}
// テクスチャマッピングを再計算
updateCoverMapping();
}
}
// マウス・タッチイベント設定
function setupPointerEvents() {
// ポインターイベントが有効かどうかを判定
function pointerEventsEnabled() {
return state.visible && state.running && state.interactive;
}
// ポインター位置に波紋を生成
function dropAtPointerEvent(pointer, big) {
if (pointerEventsEnabled()) {
// big=true: クリック/タッチ開始時(強い波紋: 半径1.5倍、強度0.14)
// big=false: 移動時(弱い波紋: 通常半径、強度0.01)
dropAtPointer(pointer, state.dropRadius * (big ? 1.5 : 1), big ? 0.14 : 0.01);
}
}
// マウスイベントの設定
state.el.addEventListener("mousedown", function (e) {
dropAtPointerEvent(e, true); // 強い波紋(クリック時)
});
state.el.addEventListener("mousemove", function (e) {
dropAtPointerEvent(e); // 弱い波紋(移動時)
});
// タッチイベントの設定
state.el.addEventListener("touchmove", function (e) {
const touches = e.changedTouches;
for (let i = 0; i < touches.length; i++) {
dropAtPointerEvent(touches[i]); // 弱い波紋
}
});
state.el.addEventListener("touchstart", function (e) {
const touches = e.changedTouches;
for (let i = 0; i < touches.length; i++) {
dropAtPointerEvent(touches[i], true); // 強い波紋(タッチ開始時)
}
});
}
// プロパティの動的更新用メソッド
function set(property, value) {
if (property === "dropRadius" || property === "perturbance" || property === "interactive" || property === "crossOrigin") {
state[property] = value;
} else if (property === "imageUrl") {
state.imageUrl = value;
state.backgroundType = "image";
loadBackground(); // 新しい画像を読み込み直し
} else if (property === "backgroundColor") {
state.backgroundColor = value;
state.backgroundType = "color";
loadBackground(); // 新しい色を設定
} else if (property === "backgroundType") {
state.backgroundType = value;
loadBackground(); // 背景を再読み込み
}
}
// 初期化処理
initShaders();
initTexture();
loadBackground();
// WebGLの基本設定
gl.clearColor(0, 0, 0, 0);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// マウス・タッチイベントの設定
setupPointerEvents();
// リサイズイベントの設定
window.addEventListener("resize", updateSize);
// レンダリングループの開始
function animationLoop() {
if (!state.destroyed) {
step();
requestAnimationFrame(animationLoop);
}
}
requestAnimationFrame(animationLoop);
// パブリックAPI
return {
drop: drop,
updateSize: updateSize,
set: set,
destroy: function () {
state.destroyed = true;
window.removeEventListener("resize", updateSize);
},
};
}
// ============================================================================
// 7. デフォルト設定値
// ============================================================================
const RIPPLES_DEFAULTS = {
backgroundType: "image", // 背景タイプ ('image' または 'color')
imageUrl: "assets/images/1.jpg", // 背景画像のURL
backgroundColor: "#5fc3dd", // 背景色
resolution: 256, // 波紋計算用の解像度
dropRadius: 20, // 波紋の半径
perturbance: 0.03, // 波の強度
interactive: true, // マウス・タッチ操作の有効性
crossOrigin: "", // CORS設定
};
// ============================================================================
// 8. 初期化実行
// ============================================================================
// DOM読み込み完了後にRipplesインスタンスを作成
document.addEventListener("DOMContentLoaded", function () {
const el = document.querySelector("canvas.water");
if (el) {
// 背景画像を使用する場合の設定
const ripplesInstance = createRipples(el, {
backgroundType: "image",
imageUrl: "assets/images/1.jpg",
resolution: 400,
dropRadius: 15,
interactive: true,
perturbance: 0.01,
});
// 背景色を使用する場合の設定
// const ripplesInstance = createRipples(el, {
// backgroundType: "color",
// backgroundColor: "#5fc3dd",
// resolution: 400,
// dropRadius: 15,
// interactive: true,
// perturbance: 0.01,
// });
// インスタンスを要素に関連付け(互換性のため)
el.ripplesInstance = ripplesInstance;
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment