Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save kobitoDevelopment/f87fefd23667dd0a35c9abe4f0a53f95 to your computer and use it in GitHub Desktop.
.canvas {
display: block;
width: 100%;
height: 100vh;
}
<canvas class="canvas"></canvas>
// ============================================================================
// 1. キャンバスとWebGLの基本設定
// ============================================================================
// HTMLから取得したcanvas要素とWebGLコンテキスト
const canvas = document.querySelector(".canvas");
let gl;
// ============================================================================
// 2. 流体シミュレーションの設定パラメータ
// ============================================================================
// 流体の物理的特性と計算品質を制御するパラメータ
const config = {
SIM_RESOLUTION: 128, // シミュレーション解像度
DYE_RESOLUTION: 512, // 染料の描画解像度
DENSITY_DISSIPATION: 1, // 染料の消散率(大きいほど早く消える)
VELOCITY_DISSIPATION: 0.2, // 速度の減衰率
PRESSURE_ITERATIONS: 20, // 圧力計算の反復回数
SPLAT_RADIUS: 0.25, // 流体発生時の半径
SPLAT_FORCE: 6000, // 流体発生時の力の強さ
COLORFUL: true, // カラフルな色を使用するか
COLOR_UPDATE_SPEED: 10, // 色の更新速度
PAUSED: false, // シミュレーションの一時停止フラグ
};
// ============================================================================
// 3. マウス・タッチ入力の管理
// ============================================================================
// ポインター(マウス・タッチ)の状態を作成する関数
function createPointer() {
return {
id: -1, // ポインターID
texcoordX: 0, // テクスチャ座標X(0~1)
texcoordY: 0, // テクスチャ座標Y(0~1)
prevTexcoordX: 0, // 前フレームのX座標
prevTexcoordY: 0, // 前フレームのY座標
deltaX: 0, // X方向の移動量
deltaY: 0, // Y方向の移動量
down: false, // 押下状態
moved: false, // 移動フラグ
color: [30, 0, 300] // 流体の色
};
}
const pointers = []; // ポインターの配列
const splatStack = []; // 流体発生のキュー
pointers.push(createPointer()); // メインポインターを初期化
// ============================================================================
// 4. WebGLコンテキストの初期化と拡張機能の検出
// ============================================================================
// WebGL2/WebGL1の判別と必要な拡張機能の取得
function getWebGLContext(canvas) {
const params = {
alpha: true, // アルファブレンドを有効化
depth: false, // 深度バッファは不要
stencil: false, // ステンシルバッファは不要
antialias: false, // アンチエイリアシングは無効
preserveDrawingBuffer: false, // 描画バッファの保持は不要
};
gl = canvas.getContext("webgl2", params);
const isWebGL2 = !!gl; // WebGL2のサポートチェック
if (!isWebGL2) {
// WebGL2がサポートされていない場合はWebGL1を使用
gl = canvas.getContext("webgl", params) || canvas.getContext("experimental-webgl", params);
}
let halfFloat;
let supportLinearFiltering;
if (isWebGL2) {
// WebGL2用の浮動小数点テクスチャサポート
gl.getExtension("EXT_color_buffer_float");
supportLinearFiltering = gl.getExtension("OES_texture_float_linear");
} else {
// WebGL1用のハーフフロートテクスチャサポート
halfFloat = gl.getExtension("OES_texture_half_float");
supportLinearFiltering = gl.getExtension("OES_texture_half_float_linear");
}
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 背景色を黒に設定
const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES;
let formatRGBA, formatRG, formatR; // テクスチャフォーマットの変数
if (isWebGL2) {
formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType);
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);
} else {
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
}
return {
gl: gl,
ext: {
formatRGBA: formatRGBA,
formatRG: formatRG,
formatR: formatR,
halfFloatTexType: halfFloatTexType,
supportLinearFiltering: supportLinearFiltering,
},
};
}
function getSupportedFormat(gl, internalFormat, format, type) {
if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
if (internalFormat === gl.R16F) {
return getSupportedFormat(gl, gl.RG16F, gl.RG, type);
} else if (internalFormat === gl.RG16F) {
return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type);
} else {
return null;
}
}
return {
internalFormat: internalFormat,
format: format,
};
}
function supportRenderTextureFormat(gl, internalFormat, format, type) {
const texture = gl.createTexture();
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);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
return status == gl.FRAMEBUFFER_COMPLETE;
}
// ============================================================================
// 5. シェーダープログラムの管理クラス
// ============================================================================
// シェーダーのコンパイル・リンクとユニフォーム変数の管理
function createShaderProgram(vertexShader, fragmentShader) {
const program = createProgram(vertexShader, fragmentShader);
const uniforms = getUniforms(program);
return {
program: program,
uniforms: uniforms,
bind() {
gl.useProgram(program);
}
};
}
function createProgram(vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw gl.getProgramInfoLog(program);
}
return program;
}
function getUniforms(program) {
const uniforms = [];
const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
const uniformName = gl.getActiveUniform(program, i).name;
uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
}
return uniforms;
}
function compileShader(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(shader);
}
return shader;
}
// ============================================================================
// 6. WebGL初期化とシェーダーのコンパイル
// ============================================================================
// WebGLコンテキストを最初に初期化(シェーダーコンパイルに必要)
resizeCanvas();
const ref = getWebGLContext(canvas);
gl = ref.gl;
const ext = ref.ext;
// ------------------------------------------------------------------------
// 6.1 基本頂点シェーダー(全シェーダー共通)
// ------------------------------------------------------------------------
// 正規化座標(-1~1)をテクスチャ座標(0~1)に変換し、隣接ピクセル座標も計算
const baseVertexShader = compileShader(
gl.VERTEX_SHADER,
`
precision highp float;
attribute vec2 aPosition; // 頂点座標
varying vec2 vUv; // メインテクスチャ座標
varying vec2 vL; // 左隣接ピクセル座標
varying vec2 vR; // 右隣接ピクセル座標
varying vec2 vT; // 上隣接ピクセル座標
varying vec2 vB; // 下隣接ピクセル座標
uniform vec2 texelSize; // 1ピクセルのサイズ
void main () {
vUv = aPosition * 0.5 + 0.5; // -1~1 を 0~1 に変換
vL = vUv - vec2(texelSize.x, 0.0); // 左隣接ピクセル
vR = vUv + vec2(texelSize.x, 0.0); // 右隣接ピクセル
vT = vUv + vec2(0.0, texelSize.y); // 上隣接ピクセル
vB = vUv - vec2(0.0, texelSize.y); // 下隣接ピクセル
gl_Position = vec4(aPosition, 0.0, 1.0); // 頂点座標を設定
}
`
);
// 染料テクスチャをスクリーンに描画するシェーダー
const displayShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv; // テクスチャ座標
uniform sampler2D uTexture; // 染料テクスチャ
void main () {
vec3 c = texture2D(uTexture, vUv).rgb; // 染料の色を取得
float a = max(c.r, max(c.g, c.b)); // 最大色値をアルファ値として使用
gl_FragColor = vec4(c, a); // 最終色を出力
}
`
);
// ガウシアン分布で流体を発生させるシェーダー
const splatShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv; // テクスチャ座標
uniform sampler2D uTarget; // 対象テクスチャ(速度または染料)
uniform float aspectRatio; // アスペクト比補正
uniform vec3 color; // 添加する色または力
uniform vec2 point; // 発生位置
uniform float radius; // 発生範囲の半径
void main () {
vec2 p = vUv - point.xy; // 発生位置からの距離ベクトル
p.x *= aspectRatio; // アスペクト比補正
vec3 splat = exp(-dot(p, p) / radius) * color; // ガウシアン分布で強度計算
vec3 base = texture2D(uTarget, vUv).xyz; // 既存の値を取得
gl_FragColor = vec4(base + splat, 1.0); // 既存値に新しい値を加算
}
`
);
// 移流(物質を速度場に沿って運搬)するシェーダー
const advectionShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv; // テクスチャ座標
uniform sampler2D uVelocity; // 速度場テクスチャ
uniform sampler2D uSource; // 移流させる対象テクスチャ
uniform vec2 texelSize; // 1ピクセルのサイズ
uniform float dt; // フレーム間の時間
uniform float dissipation; // 減衰率
void main () {
// 速度を逆に辿って前フレームの物質の位置を推定
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
vec4 result = texture2D(uSource, coord); // 推定位置から値をサンプル
float decay = 1.0 + dissipation * dt; // 減衰係数
gl_FragColor = result / decay; // 減衰を適用して出力
}
`
);
// 発散(速度場の圧縮性)を計算するシェーダー
const divergenceShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv; // メインテクスチャ座標
varying highp vec2 vL; // 左隣接ピクセル座標
varying highp vec2 vR; // 右隣接ピクセル座標
varying highp vec2 vT; // 上隣接ピクセル座標
varying highp vec2 vB; // 下隣接ピクセル座標
uniform sampler2D uVelocity; // 速度場テクスチャ
void main () {
float L = texture2D(uVelocity, vL).x; // 左の速度X成分
float R = texture2D(uVelocity, vR).x; // 右の速度X成分
float T = texture2D(uVelocity, vT).y; // 上の速度Y成分
float B = texture2D(uVelocity, vB).y; // 下の速度Y成分
vec2 C = texture2D(uVelocity, vUv).xy; // 中央の速度
// 境界条件の処理(外側は速度を反転)
if (vL.x < 0.0) { L = -C.x; }
if (vR.x > 1.0) { R = -C.x; }
if (vT.y > 1.0) { T = -C.y; }
if (vB.y < 0.0) { B = -C.y; }
float div = 0.5 * (R - L + T - B); // 発散を計算
gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
}
`
);
// Jacobi反復法で圧力場を計算するシェーダー
const pressureShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv; // メインテクスチャ座標
varying highp vec2 vL; // 左隣接ピクセル座標
varying highp vec2 vR; // 右隣接ピクセル座標
varying highp vec2 vT; // 上隣接ピクセル座標
varying highp vec2 vB; // 下隣接ピクセル座標
uniform sampler2D uPressure; // 現在の圧力場
uniform sampler2D uDivergence; // 発散値
void main () {
float L = texture2D(uPressure, vL).x; // 左隣接の圧力
float R = texture2D(uPressure, vR).x; // 右隣接の圧力
float T = texture2D(uPressure, vT).x; // 上隣接の圧力
float B = texture2D(uPressure, vB).x; // 下隣接の圧力
float divergence = texture2D(uDivergence, vUv).x; // 発散値
// Jacobi反復法で圧力を更新
float pressure = (L + R + B + T - divergence) * 0.25;
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
}
`
);
const gradientSubtractShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uPressure;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uPressure, vL).x;
float R = texture2D(uPressure, vR).x;
float T = texture2D(uPressure, vT).x;
float B = texture2D(uPressure, vB).x;
vec2 velocity = texture2D(uVelocity, vUv).xy;
velocity.xy -= vec2(R - L, T - B);
gl_FragColor = vec4(velocity, 0.0, 1.0);
}
`
);
const clearShader = compileShader(
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
uniform sampler2D uTexture;
uniform float value;
void main () {
gl_FragColor = value * texture2D(uTexture, vUv);
}
`
);
// シェーダープログラム
const displayProgram = createShaderProgram(baseVertexShader, displayShader);
const splatProgram = createShaderProgram(baseVertexShader, splatShader);
const advectionProgram = createShaderProgram(baseVertexShader, advectionShader);
const divergenceProgram = createShaderProgram(baseVertexShader, divergenceShader);
const pressureProgram = createShaderProgram(baseVertexShader, pressureShader);
const gradientSubtractProgram = createShaderProgram(baseVertexShader, gradientSubtractShader);
const clearProgram = createShaderProgram(baseVertexShader, clearShader);
// フレームバッファ
let dye;
let velocity;
let divergence;
let pressure;
// フルスクリーン四角形のジオメトリ
const blit = (() => {
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
return (destination) => {
gl.bindFramebuffer(gl.FRAMEBUFFER, destination);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
};
})();
// FBOユーティリティ
function createFBO(w, h, internalFormat, format, type, param) {
gl.activeTexture(gl.TEXTURE0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
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, internalFormat, w, h, 0, format, type, null);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.viewport(0, 0, w, h);
gl.clear(gl.COLOR_BUFFER_BIT);
const texelSizeX = 1.0 / w;
const texelSizeY = 1.0 / h;
return {
texture: texture,
fbo: fbo,
width: w,
height: h,
texelSizeX: texelSizeX,
texelSizeY: texelSizeY,
attach(id) {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(gl.TEXTURE_2D, texture);
return id;
},
};
}
function createDoubleFBO(w, h, internalFormat, format, type, param) {
const fbo1 = createFBO(w, h, internalFormat, format, type, param);
const fbo2 = createFBO(w, h, internalFormat, format, type, param);
const api = {
width: w,
height: h,
texelSizeX: fbo1.texelSizeX,
texelSizeY: fbo1.texelSizeY,
read: fbo1,
write: fbo2,
swap() {
const temp = api.read;
api.read = api.write;
api.write = temp;
},
};
return api;
}
function initFramebuffers() {
const simRes = getResolution(config.SIM_RESOLUTION);
const dyeRes = getResolution(config.DYE_RESOLUTION);
const texType = ext.halfFloatTexType;
const rgba = ext.formatRGBA;
const rg = ext.formatRG;
const r = ext.formatR;
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering);
velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering);
divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);
pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);
}
// ------------------------------------------------------------------------
// 7. 流体シミュレーションのメイン計算ステップ
// ------------------------------------------------------------------------
// Navier-Stokes方程式に基づく流体の物理シミュレーション
function step(dt) {
// ブレンド無効化(計算フェーズのため)
gl.disable(gl.BLEND);
// 速度場の解像度でビューポート設定
gl.viewport(0, 0, velocity.width, velocity.height);
// 7.1 発散(Divergence)計算 - 流体の圧縮性を測定
divergenceProgram.bind();
gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
blit(divergence.fbo);
// 7.2 圧力場の初期化(前回の圧力に減衰係数を適用)
clearProgram.bind();
gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0));
gl.uniform1f(clearProgram.uniforms.value, 0.8);
blit(pressure.write.fbo);
pressure.swap();
// 7.3 圧力場の計算(Jacobi反復法でPoisson方程式を解く)
pressureProgram.bind();
gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0));
// 反復計算により圧力場を収束させる
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1));
blit(pressure.write.fbo);
pressure.swap();
}
// 7.4 圧力勾配除去 - 速度場から圧力成分を取り除き非圧縮性を実現
gradientSubtractProgram.bind();
gl.uniform2f(gradientSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(gradientSubtractProgram.uniforms.uPressure, pressure.read.attach(0));
gl.uniform1i(gradientSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));
blit(velocity.write.fbo);
velocity.swap();
// 7.5 速度の移流(Advection) - 速度場を自分自身で運搬
advectionProgram.bind();
gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
const velocityId = velocity.read.attach(0);
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);
gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);
gl.uniform1f(advectionProgram.uniforms.dt, dt);
gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);
blit(velocity.write.fbo);
velocity.swap();
// 7.6 染料の移流 - 色素を速度場に沿って運搬・減衰
gl.viewport(0, 0, dye.width, dye.height);
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
blit(dye.write.fbo);
dye.swap();
}
// ------------------------------------------------------------------------
// 8. 最終描画処理
// ------------------------------------------------------------------------
// 染料テクスチャをスクリーンに描画
function render() {
// アルファブレンドで透明度を考慮した描画
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
// キャンバス全体をビューポートとして設定
const width = gl.drawingBufferWidth;
const height = gl.drawingBufferHeight;
gl.viewport(0, 0, width, height);
// 染料テクスチャをスクリーンに描画
displayProgram.bind();
gl.uniform1i(displayProgram.uniforms.uTexture, dye.read.attach(0));
blit(null);
}
// ------------------------------------------------------------------------
// 9. 流体の新規発生処理(Splat)
// ------------------------------------------------------------------------
// 指定座標に速度と色素をガウシアン分布で添加
function splat(x, y, dx, dy, color) {
// 9.1 速度場への力の付与
gl.viewport(0, 0, velocity.width, velocity.height);
splatProgram.bind();
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);
gl.uniform2f(splatProgram.uniforms.point, x, y);
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0);
gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0));
blit(velocity.write.fbo);
velocity.swap();
// 9.2 染料場への色素の添加
gl.viewport(0, 0, dye.width, dye.height);
gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
blit(dye.write.fbo);
dye.swap();
}
// ユーティリティ関数
function resizeCanvas() {
const width = scaleByPixelRatio(canvas.clientWidth);
const height = scaleByPixelRatio(canvas.clientHeight);
if (canvas.width != width || canvas.height != height) {
canvas.width = width;
canvas.height = height;
return true;
}
return false;
}
function correctRadius(radius) {
const aspectRatio = canvas.width / canvas.height;
if (aspectRatio > 1) radius *= aspectRatio;
return radius;
}
function generateColor() {
const c = HSVtoRGB(Math.random(), 1.0, 1.0);
c.r *= 0.15;
c.g *= 0.15;
c.b *= 0.15;
return c;
}
function HSVtoRGB(h, s, v) {
let r, g, b, i, f, p, q, t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
const colorIndex = i % 6;
if (colorIndex === 0) {
(r = v), (g = t), (b = p);
} else if (colorIndex === 1) {
(r = q), (g = v), (b = p);
} else if (colorIndex === 2) {
(r = p), (g = v), (b = t);
} else if (colorIndex === 3) {
(r = p), (g = q), (b = v);
} else if (colorIndex === 4) {
(r = t), (g = p), (b = v);
} else if (colorIndex === 5) {
(r = v), (g = p), (b = q);
}
return { r: r, g: g, b: b };
}
function getResolution(resolution) {
let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;
if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio;
const min = Math.round(resolution);
const max = Math.round(resolution * aspectRatio);
if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min };
else return { width: min, height: max };
}
function scaleByPixelRatio(input) {
const pixelRatio = window.devicePixelRatio || 1;
return Math.floor(input * pixelRatio);
}
// 入力処理
function updatePointerDownData(pointer, id, posX, posY) {
pointer.id = id;
pointer.down = true;
pointer.moved = false;
pointer.texcoordX = posX / canvas.width;
pointer.texcoordY = 1.0 - posY / canvas.height;
pointer.prevTexcoordX = pointer.texcoordX;
pointer.prevTexcoordY = pointer.texcoordY;
pointer.deltaX = 0;
pointer.deltaY = 0;
pointer.color = generateColor();
}
function updatePointerMoveData(pointer, posX, posY) {
pointer.prevTexcoordX = pointer.texcoordX;
pointer.prevTexcoordY = pointer.texcoordY;
pointer.texcoordX = posX / canvas.width;
pointer.texcoordY = 1.0 - posY / canvas.height;
pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);
pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);
pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
}
function correctDeltaX(delta) {
const aspectRatio = canvas.width / canvas.height;
if (aspectRatio < 1) delta *= aspectRatio;
return delta;
}
function correctDeltaY(delta) {
const aspectRatio = canvas.width / canvas.height;
if (aspectRatio > 1) delta /= aspectRatio;
return delta;
}
function splatPointer(pointer) {
const dx = pointer.deltaX * config.SPLAT_FORCE;
const dy = pointer.deltaY * config.SPLAT_FORCE;
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
}
// イベントリスナー
canvas.addEventListener("mousedown", (e) => {
const posX = scaleByPixelRatio(e.offsetX);
const posY = scaleByPixelRatio(e.offsetY);
let pointer = pointers.find((p) => p.id == -1);
if (pointer == null) pointer = createPointer();
updatePointerDownData(pointer, -1, posX, posY);
});
canvas.addEventListener("mousemove", (e) => {
const pointer = pointers[0];
const posX = scaleByPixelRatio(e.offsetX);
const posY = scaleByPixelRatio(e.offsetY);
if (!pointer.down) {
// マウスが動いている場合は新しいポインターを作成
updatePointerDownData(pointer, -1, posX, posY);
}
updatePointerMoveData(pointer, posX, posY);
});
window.addEventListener("mouseup", () => {
pointers[0].down = false;
});
canvas.addEventListener("touchstart", (e) => {
e.preventDefault();
const touches = e.targetTouches;
while (touches.length >= pointers.length) {
pointers.push(createPointer());
}
for (let i = 0; i < touches.length; i++) {
const posX = scaleByPixelRatio(touches[i].pageX);
const posY = scaleByPixelRatio(touches[i].pageY);
updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY);
}
});
canvas.addEventListener(
"touchmove",
(e) => {
e.preventDefault();
const touches = e.targetTouches;
for (let i = 0; i < touches.length; i++) {
const pointer = pointers[i + 1];
if (!pointer.down) continue;
const posX = scaleByPixelRatio(touches[i].pageX);
const posY = scaleByPixelRatio(touches[i].pageY);
updatePointerMoveData(pointer, posX, posY);
}
},
false
);
window.addEventListener("touchend", (e) => {
const touches = e.changedTouches;
for (let i = 0; i < touches.length; i++) {
const pointer = pointers.find((p) => p.id == touches[i].identifier);
if (pointer == null) continue;
pointer.down = false;
}
});
// ------------------------------------------------------------------------
// 10. アニメーションループとメイン更新処理
// ------------------------------------------------------------------------
let lastUpdateTime = Date.now();
// 毎フレーム呼び出されるメイン更新処理
function update() {
// 10.1 フレームレート計算とキャンバササイズ変更のチェック
const dt = calcDeltaTime();
if (resizeCanvas()) {
initFramebuffers();
}
// 10.2 ポインター入力の処理(マウスやタッチ)
pointers.forEach((p) => {
if (p.moved) {
p.moved = false;
splatPointer(p);
}
});
// 10.3 物理シミュレーションの実行と描画
if (!config.PAUSED) {
step(dt);
}
render();
// 10.4 次のフレームをリクエスト
requestAnimationFrame(update);
}
function calcDeltaTime() {
const now = Date.now();
let dt = (now - lastUpdateTime) / 1000;
dt = Math.min(dt, 0.016666);
lastUpdateTime = now;
return dt;
}
// ------------------------------------------------------------------------
// 11. シミュレーションの開始
// ------------------------------------------------------------------------
// フレームバッファを初期化し、アニメーションループを開始
initFramebuffers();
update();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment