Hover Image Ripple with GLSL
Visit Site
References
Vertex Shader
// シェーダーに渡される一時変数
uniform float iTime; // 現在の時間
uniform vec2 iResolution; // 画面の解像度
uniform vec2 iMouse; // マウスの位置
// ジオメトリのUVマッピング座標をフラグメントシェーダーに渡す
varying vec2 vUv;
// JavaScriptから渡される変数
uniform vec2 uMeshSize; // メッシュ(画像)のサイズ
uniform vec2 uMediaSize; // メディア(実際の画像)のサイズ
uniform vec2 uOffset; // オフセット量(マウス移動による)
uniform float uOpacity; // 不透明度
uniform float uMouseEnter; // マウスがエンターしているかどうかのフラグ
uniform float uMouseEnterMask; // マウスエンター時のマスク効果
// ダミー変数(デバッグ用)
varying vec2 vDummy;
const float PI = 3.14159265359;
// 画像のスケールを変更する関数
vec2 scale(in vec2 st, in vec2 s, in vec2 center) {
return (st - center) * s + center;
}
// 指定した値を0.0から1.0の間に制限する関数
float saturate(float a) {
return clamp(a, 0., 1.);
}
// 画像の変形効果を計算する関数
vec3 deformationCurve(vec3 position, vec2 uv, vec2 offset) {
position.x = position.x + (sin(uv.y * PI) * offset.x);
position.y = position.y + (sin(uv.x * PI) * offset.y);
return position;
}
// UVスケールを計算する関数
vec2 getUVScale() {
float d = length(uMeshSize);
float longEdge = max(uMeshSize.x, uMeshSize.y);
float dRatio = d / longEdge;
float mRatio = uMeshSize.x / uMeshSize.y;
return vec2(mRatio / dRatio);
}
// アニメーションの進行度を計算する関数
float getProgress(float activation, float latestStart, float progress, float progressLimit) {
float startAt = activation * latestStart;
float pr = smoothstep(startAt, 1., progress);
float p = min(saturate(pr / progressLimit), saturate((1. - pr) / (1. - progressLimit)));
return p;
}
// 頂点の変形を計算する関数
vec3 distort(vec3 p) {
vec2 uvDistortion = uv;
vec2 uvScale = getUVScale();
uvDistortion = scale(uvDistortion, uvScale, vec2(.5));
uvDistortion = (uvDistortion - .5) * 2.;
float d = length(uvDistortion);
float pr = getProgress(d, .8, uMouseEnter, .75) * .08;
p.xy *= (1. + pr);
return p;
}
// メイン関数
void main() {
vec3 p = position; // 頂点の初期位置
p = deformationCurve(p, uv, uOffset); // 変形効果を適用
p = distort(p); // 更に変形
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.); // 最終的な頂点の位置
vUv = uv; // UV座標をフラグメントシェーダーに渡す
}
Fragment Shader
// シェーダーに渡される一時変数
uniform float iTime; // 現在の時間
uniform vec2 iResolution; // 画面の解像度
uniform vec2 iMouse; // マウスの位置
// 頂点シェーダーから受け取るUV座標
varying vec2 vUv;
// テクスチャ情報
uniform sampler2D iChannel0; // 画像のテクスチャ
// JavaScriptから渡される変数
uniform vec2 uMeshSize; // メッシュ(画像)のサイズ
uniform vec2 uMediaSize; // メディア(実際の画像)のサイズ
uniform vec2 uOffset; // オフセット量(マウス移動による)
uniform float uOpacity; // 不透明度
uniform float uMouseEnter; // マウスがエンターしているかどうかのフラグ
uniform float uMouseEnterMask; // マウスエンター時のマスク効果
// ダミー変数(デバッグ用)
varying vec2 vDummy;
// テクスチャをメッシュにフィットさせるための計算
vec2 cover(vec2 s, vec2 i, vec2 uv) {
float rs = s.x / s.y;
float ri = i.x / i.y;
vec2 new = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x);
vec2 offset = (rs < ri ? vec2((new.x - s.x) / 2., 0.) : vec2(0., (new.y - s.y) / 2.)) / new;
uv = uv * s / new + offset;
return uv;
}
// UV座標をスケールする関数
vec2 scale(in vec2 st, in vec2 s, in vec2 center) {
return (st - center) * s + center;
}
// アスペクト比に基づいてUV座標を調整する関数
vec2 ratio2(in vec2 v, in vec2 s) {
return mix(vec2(v.x, v.y * (s.y / s.x)),
vec2((v.x * s.x / s.y), v.y),
step(s.x, s.y));
}
// UV座標に歪みを加える関数
vec2 distort(vec2 uv) {
uv -= .5;
float mRatio = uMeshSize.x / uMeshSize.y;
float pUvX = pow(uv.x * mRatio, 2.);
float pUvY = pow(uv.y, 2.);
float pSum = pUvX + pUvY;
float multiplier = 10. * (1. - uMouseEnter);
float strength = 1. - multiplier * pSum;
uv *= strength;
uv += .5;
return uv;
}
// メイン関数
void main() {
vec2 uv = vUv; // UV座標
uv = cover(uMeshSize, uMediaSize.xy, uv); // メッシュにフィットさせる
float d = getMaskDist(uv); // マスクの距離を計算
float mask = 1. - step(uMouseEnterMask, d); // マスクの適用
uv = scale(uv, vec2(1. / (1. + (1. - uMouseEnter) * .25)), vec2(.5)); // スケール調整
uv = distort(uv); // 歪みを適用
vec4 tex = texture(iChannel0, uv); // テクスチャを取得
vec3 color = tex.rgb; // 色情報
float alpha = mask * uOpacity; // 不透明度を適用
gl_FragColor = vec4(color, alpha); // 最終的なピクセルの色
}
JavaScript
// 必要なライブラリをインポートしています。
import * as kokomi from "https://esm.sh/kokomi.js";
import * as THREE from "https://esm.sh/three";
import gsap from "https://esm.sh/gsap";
// 頂点シェーダーとフラグメントシェーダーのコードは省略します。
const vertexShader = /* glsl */ `...`;
const fragmentShader = /* glsl */ `...`;
// Sketchクラスを定義します。このクラスは、kokomi.jsライブラリを使って、
// 3Dエフェクトの画像ギャラリーをウェブページ上に作成するための処理をまとめています。
class Sketch extends kokomi.Base {
async create() {
// 画像エフェクトに関する設定を定義します。
const config = {
offsetAmount: 1 // マウス移動に応じたエフェクトの強さ
};
// ギャラリーの要素と各画像項目を取得します。
const galleryEl = document.querySelector(".gallery");
const galleryItems = [...document.querySelectorAll(".gallery-item")];
const hoverImg = document.querySelector(".hover-img");
// ギャラリーの画像に関する情報をリストとして準備します。
const resourceList = galleryItems.map((item, i) => ({
name: item.dataset["glImgName"], // 画像の名前
type: "texture", // リソースのタイプ(テクスチャ)
path: item.dataset["glImg"] // 画像のパス
}));
// アセットマネージャーを作成し、画像リソースをプリロードします。
const am = new kokomi.AssetManager(this, resourceList);
// すべてのリソースが読み込まれた後に実行される処理を定義します。
am.on("ready", async () => {
// ローダースクリーンを非表示にします。
document.querySelector(".loader-screen")?.classList.add("hollow");
// 画面全体をカバーするカメラを設定します。
const screenCamera = new kokomi.ScreenCamera(this);
screenCamera.addExisting();
// 画像を表示するためのメッシュ(平面)を作成します。
const geometry = new THREE.PlaneGeometry(1, 1, 64, 64);
const uj = new kokomi.UniformInjector(this);
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
transparent: true, // 透明度を有効にします。
uniforms: {
...uj.shadertoyUniforms,
iChannel0: {
value: null // テクスチャは後で設定されます。
},
// その他のuniform変数も設定しますが、初期値はここで設定されます。
}
});
const mesh = new THREE.Mesh(geometry, material);
mesh.scale.set(hoverImg.clientWidth, hoverImg.clientHeight, 1);
this.scene.add(mesh);
// マウスの位置に基づいて、エフェクトのオフセットを計算します。
let targetX = 0;
let targetY = 0;
let offsetX = 0;
let offsetY = 0;
// レンダリングループで繰り返し実行される処理を定義します。
this.update(() => {
// 現在のマウスの位置を取得し、オフセットを計算します。
targetX = this.interactionManager.mouse.x;
targetY = this.interactionManager.mouse.y;
offsetX = THREE.MathUtils.lerp(offsetX, targetX, 0.1);
offsetY = THREE.MathUtils.lerp(offsetY, targetY, 0.1);
// オフセットに基づいてシェーダーのuniform変数を更新します。
material.uniforms.uOffset.value = new THREE.Vector2(
(targetX - offsetX) * config.offsetAmount,
(targetY - offsetY) * config.offsetAmount
);
// マウスの位置に合わせてメッシュとホバー画像を動かします。
gsap.to(mesh.position, {
x: (this.interactionManager.mouse.x * window.innerWidth) / 2,
y: (this.interactionManager.mouse.y * window.innerHeight) / 2
});
gsap.to(hoverImg, {
x: this.iMouse.mouseDOM.x - hoverImg.clientWidth / 2,
y: this.iMouse.mouseDOM.y - hoverImg.clientHeight / 2
});
});
// 画像項目にマウスを乗せたときの処理を定義します。
const doTransition = (item) => {
if (item) {
hoverImg.src = item.dataset["glImg"];
const tex = am.items[item.dataset["glImgName"]];
material.uniforms.iChannel0.value = tex;
// シェーダーに必要な情報を更新します。
material.uniforms.uMeshSize.value = new THREE.Vector2(
mesh.scale.x,
mesh.scale.y
);
material.uniforms.uMediaSize.value = new THREE.Vector2(
tex.image.width,
tex.image.height
);
// 不透明度をアニメーションで変更します。
gsap.to(material.uniforms.uOpacity, {
value: 1,
duration: 0.3
});
// マウスエンター効果のアニメーションを設定します。
gsap.fromTo(
material.uniforms.uMouseEnter,
{
value: 0
},
{
value: 1,
duration: 1.2,
ease: "power2.out"
}
);
gsap.fromTo(
material.uniforms.uMouseEnterMask,
{
value: 0
},
{
value: 1,
duration: 0.7,
ease: "power2.out"
}
);
} else {
// マウスが離れた場合は不透明度を0に戻します。
gsap.to(material.uniforms.uOpacity, {
value: 0,
duration: 0.3
});
}
};
// ギャラリー項目にイベントリスナーを追加します。
let currentItem = null;
galleryItems.forEach((item) => {
item.addEventListener("mouseenter", () => {
currentItem = item;
doTransition(currentItem);
});
});
galleryEl.addEventListener("mouseleave", () => {
currentItem = null;
doTransition(currentItem);
});
});
}
}
// Sketchクラスのインスタンスを作成し、ギャラリーを表示します。
const sketch = new Sketch("#sketch");
sketch.create();