<style>
.img-distortion {
visibility: hidden;
position: absolute;
width: 100%;
top: 0;
pointer-events: none;
object-fit: cover;
}
.canvasContainer {
position: absolute;
object-fit: cover;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
}
canvas {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.79.0/build/three.min.js"></script>
<script>
var vertex = `uniform float time;
varying vec2 vUv;
varying vec3 vPosition;
uniform vec2 pixels;
float PI = 3.141592653589793238;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`;
var fragment = `uniform float time;
uniform float progress;
uniform sampler2D uDataTexture;
uniform sampler2D uTexture;
uniform vec4 resolution;
varying vec2 vUv;
varying vec3 vPosition;
float PI = 3.141592653589793238;
void main() {
vec2 newUV = (vUv - vec2(0.5))*resolution.zw + vec2(0.5);
vec4 color = texture2D(uTexture, newUV);
vec4 offset = texture2D(uDataTexture, vUv);
gl_FragColor = color;
gl_FragColor = texture2D(uTexture, newUV - 0.02*offset.rg);
}`;
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
class Sketch {
constructor(options) {
this.scene = new THREE.Scene();
this.container = options.dom;
this.img = this.container.querySelector(".img-distortion");
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer = new THREE.WebGLRenderer();
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(this.width, this.height);
this.renderer.setClearColor(0xeeeeee, 1);
this.renderer.physicallyCorrectLights = true;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.container.appendChild(this.renderer.domElement);
this.camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.1,
100
);
var frustumSize = 1;
var aspect = window.innerWidth / window.innerHeight;
this.camera = new THREE.OrthographicCamera(
frustumSize / -2,
frustumSize / 2,
frustumSize / 2,
frustumSize / -2,
-1000,
1000
);
this.camera.position.set(0, 0, 2);
this.time = 0;
this.mouse = {
x: 0,
y: 0,
prevX: 0,
prevY: 0,
vX: 0,
vY: 0
};
this.isPlaying = true;
this.settings();
this.addObjects(options.texturePath);
this.resize();
this.render();
this.setupResize();
this.mouseEvents();
}
getValue(val) {
return parseFloat(this.container.getAttribute("data-" + val));
}
mouseEvents() {
this.container.addEventListener("mousemove", (e) => {
this.mouse.x = (e.clientX - this.container.getBoundingClientRect().left) / this.width;
this.mouse.y = (e.clientY - this.container.getBoundingClientRect().top) / this.height;
this.mouse.vX = this.mouse.x - this.mouse.prevX;
this.mouse.vY = this.mouse.y - this.mouse.prevY;
this.mouse.prevX = this.mouse.x;
this.mouse.prevY = this.mouse.y;
});
}
settings() {
let that = this;
this.settings = {
grid: this.getValue("grid") || 34,
mouse: this.getValue("mouse") || 0.25,
strength: this.getValue("strength") || 1,
relaxation: this.getValue("relaxation") || 0.9,
aspectRatio: this.getValue("aspectRatio") || 1.77
};
}
setupResize() {
window.addEventListener("resize", this.resize.bind(this));
}
resize() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer.setSize(this.width, this.height);
this.camera.aspect = this.width / this.height;
this.imageAspect = 1 / this.settings.aspectRatio;
let a1;
let a2;
if (this.height / this.width > this.imageAspect) {
a1 = (this.width / this.height) * this.imageAspect;
a2 = 1;
} else {
a1 = 1;
a2 = this.height / this.width / this.imageAspect;
}
this.material.uniforms.resolution.value.x = this.width;
this.material.uniforms.resolution.value.y = this.height;
this.material.uniforms.resolution.value.z = a1;
this.material.uniforms.resolution.value.w = a2;
this.camera.updateProjectionMatrix();
this.regenerateGrid();
}
regenerateGrid() {
this.size = this.settings.grid;
const width = this.size;
const height = this.size;
const size = width * height;
const data = new Float32Array(3 * size);
for (let i = 0; i < size; i++) {
let r = Math.random() * 255 - 125;
let r1 = Math.random() * 255 - 125;
const stride = i * 3;
data[stride] = r;
data[stride + 1] = r1;
data[stride + 2] = r;
}
this.texture = new THREE.DataTexture(
data,
width,
height,
THREE.RGBFormat,
THREE.FloatType
);
this.texture.magFilter = this.texture.minFilter = THREE.NearestFilter;
if (this.material) {
this.material.uniforms.uDataTexture.value = this.texture;
this.material.uniforms.uDataTexture.value.needsUpdate = true;
}
}
addObjects(texturePath) {
this.regenerateGrid();
let texture = new THREE.TextureLoader().load(texturePath, function() {
texture.minFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
});
this.material = new THREE.ShaderMaterial({
extensions: {
derivatives: "#extension GL_OES_standard_derivatives : enable"
},
side: THREE.DoubleSide,
uniforms: {
time: {
value: 0
},
resolution: {
value: new THREE.Vector4()
},
uTexture: {
value: texture
},
uDataTexture: {
value: this.texture
}
},
vertexShader: vertex,
fragmentShader: fragment
});
this.geometry = new THREE.PlaneGeometry(1, 1, 1, 1);
this.plane = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.plane);
this.resize();
}
updateDataTexture() {
let data = this.texture.image.data;
for (let i = 0; i < data.length; i += 3) {
data[i] *= this.settings.relaxation;
data[i + 1] *= this.settings.relaxation;
}
let gridMouseX = this.size * this.mouse.x;
let gridMouseY = this.size * (1 - this.mouse.y);
let maxDist = this.size * this.settings.mouse;
let aspect = this.height / this.width;
for (let i = 0; i < this.size; i++) {
for (let j = 0; j < this.size; j++) {
let distance = (gridMouseX - i) ** 2 / aspect + (gridMouseY - j) ** 2;
let maxDistSq = maxDist ** 2;
if (distance < maxDistSq) {
let index = 3 * (i + this.size * j);
let power = maxDist / Math.sqrt(distance);
power = clamp(power, 0, 10);
data[index] += this.settings.strength * 5 * this.mouse.vX * power * this.settings.relaxation;
data[index + 1] -= this.settings.strength * 5 * this.mouse.vY * power * this.settings.relaxation;
}
}
}
this.mouse.vX *= 0.1;
this.mouse.vY *= 0.1;
this.texture.needsUpdate = true;
}
render() {
if (!this.isPlaying) return;
this.time += 0.05;
this.updateDataTexture();
this.material.uniforms.time.value = this.time;
requestAnimationFrame(this.render.bind(this));
this.renderer.render(this.scene, this.camera);
}
}
</script>