-
-
Save kobitoDevelopment/4b0e244cacdfd5c64742058be55dbf74 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| canvas.water { | |
| display: block; | |
| width: 100%; | |
| height: 100vh; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <canvas class="water"></canvas> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================ | |
| // 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