Sebelum memulai tutorial ini, kita akan install library gsap untuk mempermudah coding.
$ npm install gsap
Efek animasi ripple akan dipicu saat mouse hover diatas image. Agar event mouse hover dapat dihubungkan dengan WebGL, diperlukan object Raycaster.
Buka file js/app.js, lalu tambahkan code import gsap.
import gsap from 'gsap';
Pada method constructor tambahkan inisialisasi object Raycaster dan variable untuk menyimpan data mouse.
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
Pada bagian block promise done, tambahkan pemanggilan method mouseMoves() (saat ini belum dibuat).
Promise.all(allDone).then(()=>{
this.scroll = new Scroll();
this.addImages();
this.setPosition();
this.mouseMoves();
this.resize();
this.setupResize();
this.render();
});
Kemudian buat method baru mouseMoves(), yang isinya akan membaca posisi mouse dan menhubungkan dengan object raycaster.
mouseMoves(){
window.addEventListener('mousemove', (event)=>{
this.mouse.x = (event.clientX / this.width) * 2 - 1;
this.mouse.y = - (event.clientY / this.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.scene.children);
if (intersects.length>0){
let obj = intersects[0].object;
obj.material.uniforms.hover.value = intersects[0].uv;
}
}, false);
}
Pada method addImages() tambahkan code untuk membuat shaderMaterial. Berikut isi lengkap method addImage.
addImages(){
this.material = new THREE.ShaderMaterial({
uniforms:{
time: {value: 0},
uImage: {value:0},
hover : {value : new THREE.Vector2(0.5, 0.5)},
hoverState : {value: 0},
//oceanTexture: {value: new THREE.TextureLoader().load(ocean)},
},
side: THREE.DoubleSide,
fragmentShader: fragment,
vertexShader: vertex,
});
this.materials = [];
this.imageStore = this.images.map(img=>{
let bounds = img.getBoundingClientRect();
let geometry = new THREE.PlaneBufferGeometry(bounds.width, bounds.height, 10, 10);
let texture = new THREE.Texture(img);
texture.needsUpdate = true;
let material = this.material.clone();
img.addEventListener('mouseenter', ()=>{
gsap.to(material.uniforms.hoverState, {
duration: 1,
value:1
});
});
img.addEventListener('mouseout', ()=>{
gsap.to(material.uniforms.hoverState, {
duration: 1,
value:0
});
});
this.materials.push(material);
material.uniforms.uImage.value = texture;
let mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
return{
img: img,
mesh: mesh,
top: bounds.top,
left: bounds.left,
width: bounds.width,
height: bounds.height,
}
});
}
Kemudian buka file js/shaders/fragment.glsl, ganti dengan code berikut
varying float vNoise;
varying vec2 vUv;
uniform sampler2D uImage;
uniform float time;
void main(){
vec2 newUV = vUv;
vec4 oceanView = texture2D(uImage, newUV);
gl_FragColor = vec4(vUv,0.,1.);
gl_FragColor = oceanView;
gl_FragColor.rgb += 0.06*vec3(vNoise);
}
kemudian buka file js/shaders/vertex.glsl, ganti dengan code berikut
uniform float time;
uniform vec2 hover;
uniform float hoverState;
varying float vNoise;
varying vec2 vUv;
void main() {
vec3 newPos = position;
float dist = distance(uv, hover);
newPos.z += hoverState * 10.*sin(dist*10. + time);
vNoise = hoverState* sin(dist*10. - time);
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( newPos, 1.0 );
}
Sebagai referensi, berikut isi lengkap file js/app.js
import * as THREE from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
import imagesloaded from 'imagesloaded';
import FontFaceObserver from 'fontfaceobserver';
import gsap from 'gsap';
import Scroll from './scroll';
import fragment from './shaders/fragment.glsl';
import vertex from './shaders/vertex.glsl';
import ocean from './../img/ocean.jpg';
export default class Sketch{
constructor(opt){
this.time = 0;
this.container = opt.dom;
this.scene = new THREE.Scene();
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.camera = new THREE.PerspectiveCamera( 70, this.width / this.height, 100, 2000 );
this.camera.position.z = 600;
this.camera.fov = 2*Math.atan((this.height/2)/600)*(180/Math.PI);
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
this.container.appendChild( this.renderer.domElement );
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.images = [...document.querySelectorAll('img')];
const fontOpen = new Promise(resolve => {
new FontFaceObserver("Open Sans").load().then(() => {
resolve();
});
});
const fontPlayfair = new Promise(resolve => {
new FontFaceObserver("Playfair Display").load().then(() => {
resolve();
});
});
// Preload images
const preloadImages = new Promise((resolve, reject) => {
imagesloaded(document.querySelectorAll("img"), { background: true }, resolve);
});
let allDone = [fontOpen,fontPlayfair,preloadImages]
this.currentScroll = 0;
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
Promise.all(allDone).then(()=>{
this.scroll = new Scroll();
this.addImages();
this.setPosition();
this.mouseMoves();
this.resize();
this.setupResize();
this.render();
});
}
mouseMoves(){
window.addEventListener('mousemove', (event)=>{
this.mouse.x = (event.clientX / this.width) * 2 - 1;
this.mouse.y = - (event.clientY / this.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.scene.children);
if (intersects.length>0){
let obj = intersects[0].object;
obj.material.uniforms.hover.value = intersects[0].uv;
}
}, false);
}
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.camera.updateProjectionMatrix();
}
render(){
this.time+=0.05;
this.scroll.render();
this.currentScroll = this.scroll.scrollToRender;
this.setPosition();
this.materials.forEach(m=>{
m.uniforms.time.value = this.time;
});
this.renderer.render( this.scene, this.camera );
window.requestAnimationFrame(this.render.bind(this));
}
addImages(){
this.material = new THREE.ShaderMaterial({
uniforms:{
time: {value: 0},
uImage: {value:0},
hover : {value : new THREE.Vector2(0.5, 0.5)},
hoverState : {value: 0},
//oceanTexture: {value: new THREE.TextureLoader().load(ocean)},
},
side: THREE.DoubleSide,
fragmentShader: fragment,
vertexShader: vertex,
});
this.materials = [];
this.imageStore = this.images.map(img=>{
let bounds = img.getBoundingClientRect();
let geometry = new THREE.PlaneBufferGeometry(bounds.width, bounds.height, 10, 10);
let texture = new THREE.Texture(img);
texture.needsUpdate = true;
let material = this.material.clone();
img.addEventListener('mouseenter', ()=>{
gsap.to(material.uniforms.hoverState, {
duration: 1,
value:1
});
});
img.addEventListener('mouseout', ()=>{
gsap.to(material.uniforms.hoverState, {
duration: 1,
value:0
});
});
this.materials.push(material);
material.uniforms.uImage.value = texture;
let mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
return{
img: img,
mesh: mesh,
top: bounds.top,
left: bounds.left,
width: bounds.width,
height: bounds.height,
}
});
}
setPosition(){
this.imageStore.forEach(o=>{
o.mesh.position.y = this.currentScroll -o.top + this.height/2 - o.height/2;
o.mesh.position.x = o.left - this.width/2 + o.width/2;
});
}
}
new Sketch({
dom: document.getElementById('container')
});
Jika dijalankan parcel index.html, maka saat image kita sorot dengan mouse akan beranimasi.
