"Liquid Scroll Image Effects"
Bootstrap 4.1.1 Snippet by ALIMUL AL RAZY

<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <!------ Include the above in your HEAD tag ----------> <div class="container"> <ul class="image-list"> <li class="image-item"> <a href="" class="image-wrapper"> <img src="https://source.unsplash.com/whOkVvf0_hU/" alt="" /> </a> </li> <li class="image-item"> <a href="" class="image-wrapper"> <img src="https://source.unsplash.com/whOkVvf0_hU/" alt="" /> </a> </li> <li class="image-item"> <a href="" class="image-wrapper"> <img src="https://source.unsplash.com/whOkVvf0_hU/" alt="" /> </a> </li> <li class="image-item"> <a href="" class="image-wrapper"> <img src="https://source.unsplash.com/whOkVvf0_hU/" alt="" /> </a> </li> </ul> </div> <div class="webgl-canvas"> <canvas id="webgl-canvas" class="webgl-canvas__body"></canvas> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <!-- シェーダーをscriptタグ内に書いてJSで読み込む --> <script id="v-shader" type="x-shader/x-vertex"> varying vec2 vUv; uniform float uTime; float PI = 3.1415926535897932384626433832795; void main(){ vUv = uv; vec3 pos = position; // 横方向 float amp = 0.03; // 振幅(の役割) 大きくすると波が大きくなる float freq = 0.01 * uTime; // 振動数(の役割) 大きくすると波が細かくなる // 縦方向 float tension = -0.001 * uTime; // 上下の張り具合 pos.x = pos.x + sin(pos.y * PI * freq) * amp; pos.y = pos.y + (cos(pos.x * PI) * tension); gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } </script> <script id="f-shader" type="x-shader/x-fragment"> varying vec2 vUv; uniform sampler2D uTexture; uniform float uImageAspect; uniform float uPlaneAspect; uniform float uTime; void main(){ // 画像のアスペクトとプレーンオブジェクトのアスペクトを比較し、短い方に合わせる vec2 ratio = vec2( min(uPlaneAspect / uImageAspect, 1.0), min((1.0 / uPlaneAspect) / (1.0 / uImageAspect), 1.0) ); // 計算結果を用いてテクスチャを中央に配置 vec2 fixedUv = vec2( (vUv.x - 0.5) * ratio.x + 0.5, (vUv.y - 0.5) * ratio.y + 0.5 ); vec2 offset = vec2(0.0, uTime * 0.0005); float r = texture2D(uTexture, fixedUv + offset).r; float g = texture2D(uTexture, fixedUv + offset * 0.5).g; float b = texture2D(uTexture, fixedUv).b; vec3 texture = vec3(r, g, b); gl_FragColor = vec4(texture, 1.0); } </script> <script> const canvasEl = document.getElementById('webgl-canvas'); const canvasSize = { w: window.innerWidth, h: window.innerHeight, }; const renderer = new THREE.WebGLRenderer({ canvas: canvasEl }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(canvasSize.w, canvasSize.h); // ウィンドウとwebGLの座標を一致させるため、描画がウィンドウぴったりになるようカメラを調整 const fov = 60; // 視野角 const fovRad = (fov / 2) * (Math.PI / 180); const dist = canvasSize.h / 2 / Math.tan(fovRad); const camera = new THREE.PerspectiveCamera( fov, canvasSize.w / canvasSize.h, 0.1, 1000 ); camera.position.z = dist; const scene = new THREE.Scene(); const loader = new THREE.TextureLoader(); // 画像をテクスチャにしたplaneを扱うクラス class ImagePlane { constructor(mesh, img) { this.refImage = img; this.mesh = mesh; } setParams() { // 参照するimg要素から大きさ、位置を取得してセット const rect = this.refImage.getBoundingClientRect(); this.mesh.scale.x = rect.width; this.mesh.scale.y = rect.height; const x = rect.left - canvasSize.w / 2 + rect.width / 2; const y = -rect.top + canvasSize.h / 2 - rect.height / 2; this.mesh.position.set(x, y, this.mesh.position.z); } update(offset) { this.setParams(); this.mesh.material.uniforms.uTime.value = offset; } } // Planeメッシュを作る関数 const createMesh = (img) => { const texture = loader.load(img.src); const uniforms = { uTexture: { value: texture }, uImageAspect: { value: img.naturalWidth / img.naturalHeight }, uPlaneAspect: { value: img.clientWidth / img.clientHeight }, uTime: { value: 0 }, }; const geo = new THREE.PlaneBufferGeometry(1, 1, 100, 100); // 後から画像のサイズにscaleするので1にしておく const mat = new THREE.ShaderMaterial({ uniforms, vertexShader: document.getElementById('v-shader').textContent, fragmentShader: document.getElementById('f-shader').textContent, }); const mesh = new THREE.Mesh(geo, mat); return mesh; }; // スクロール追従 let targetScrollY = 0; // スクロール位置 let currentScrollY = 0; // 線形補間を適用した現在のスクロール位置 let scrollOffset = 0; // 上記2つの差分 // 開始と終了をなめらかに補間する関数 const lerp = (start, end, multiplier) => { return (1 - multiplier) * start + multiplier * end; }; const updateScroll = () => { // スクロール位置を取得 targetScrollY = document.documentElement.scrollTop; // リープ関数でスクロール位置をなめらかに追従 currentScrollY = lerp(currentScrollY, targetScrollY, 0.1); scrollOffset = targetScrollY - currentScrollY; }; const imagePlaneArray = []; // 毎フレーム呼び出す const loop = () => { updateScroll(); for (const plane of imagePlaneArray) { plane.update(scrollOffset); } renderer.render(scene, camera); requestAnimationFrame(loop); }; // リサイズ処理 let timeoutId = 0; const resize = () => { // three.jsのリサイズ const width = window.innerWidth; const height = window.innerHeight; canvasSize.w = width; canvasSize.h = height; renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); camera.aspect = width / height; camera.updateProjectionMatrix(); // カメラの距離を計算し直す const fov = 60; const fovRad = (fov / 2) * (Math.PI / 180); const dist = canvasSize.h / 2 / Math.tan(fovRad); camera.position.z = dist; }; const main = () => { window.addEventListener('load', () => { const imageArray = [...document.querySelectorAll('img')]; for (const img of imageArray) { const mesh = createMesh(img); scene.add(mesh); const imagePlane = new ImagePlane(mesh, img); imagePlane.setParams(); imagePlaneArray.push(imagePlane); } loop(); }); // リサイズ(負荷軽減のためリサイズが完了してから発火する) window.addEventListener('resize', () => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(resize, 200); }); }; main(); </script>
/* -- リセット系 -- */ * { box-sizing: border-box; margin: 0; padding: 0; } ul, li { list-style: none; } a { text-decoration: none; } img { width: 100%; } /* -- ここまで -- */ body { overscroll-behavior: none; } .webgl-canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; } .webgl-canvas__body { width: 100%; height: 100%; } .wrapper { width: 100%; height: 100vh; position: fixed; top: 0; left: 0; } .scrollable { position: absolute; width: 100%; top: 0; left: 0; } .container { width: 80vw; max-width: 1000px; margin: 0 auto; } .image-list { width: 800px; margin: 0 auto; padding: 180px 0; } .image-item { width: 100%; } .image-item:not(:first-of-type) { margin-top: 180px; } .image-wrapper { display: block; width: 100%; height: 500px; } .image-wrapper > img { height: 100%; object-fit: cover; opacity: 0; }

Related: See More


Questions / Comments: