Menggabungkan WebGL Dalam HTML – 4

Pada modul ini kita akan menggabungkan event mouse scroll antara HTML dan WebGL. Kita tidak bisa menggunakan standard event listener dari browser karena akan terjadi lagging. (Browser listener akan ditrigger lebih cepat dibandingkan proses perhitungan posisi pada WebGL).

Untuk itu kita gunakan custom event listener untuk event mouse scroll. Anda bisa menggunakan custom scroll library seperti locomotive scroll, pada tutorial ini kita akan buat sendiri scroll sederhana.

Buat file baru js/scroll.js, lalu tambahkan code berikut.

const lerp = (a, b, n) => (1 - n) * a + n * b

export default class Scroll{
    constructor(){
        this.DOM = { main: document.querySelector("main") };
        // the scrollable element
        // we translate this element when scrolling (y-axis)
        this.DOM.scrollable = this.DOM.main.querySelector("div[data-scroll]");
        this.docScroll = 0;
        this.scrollToRender = 0;
        this.current = 0;
        this.ease = 0.1;
        this.speed = 0;
        this.speedTarget = 0;

        // set the body's height
        this.setSize();
        // set the initial values
        this.getScroll();
        this.init();
        // the <main> element's style needs to be modified
        this.style();
        // init/bind events
        this.initEvents();
        // start the render loop
        requestAnimationFrame(() => this.render());
    }

    init(){
        // sets the initial value (no interpolation) - translate the scroll value
        for (const key in this.renderedStyles) {
        this.current = this.scrollToRender = this.getScroll();
        }
        // translate the scrollable element
        this.setPosition();
        this.shouldRender = true;
    }

    style(){
        this.DOM.main.style.position = "fixed";
        this.DOM.main.style.width = this.DOM.main.style.height = "100%";
        this.DOM.main.style.top = this.DOM.main.style.left = 0;
        this.DOM.main.style.overflow = "hidden";
    }

    getScroll(){
        this.docScroll = window.pageYOffset || document.documentElement.scrollTop;
        return this.docScroll;
    }
    initEvents() {

        window.onbeforeunload = function() {
            window.scrollTo(0, 0);
        };
        // on resize reset the body's height
        window.addEventListener("resize", () => this.setSize());
        window.addEventListener("scroll", this.getScroll.bind(this));

    }

    setSize() {
      // set the heigh of the body in order to keep the scrollbar on the page
      document.body.style.height = `${this.DOM.scrollable.scrollHeight}px`;
    }


    setPosition() {
      // translates the scrollable element
      if (
        Math.round(this.scrollToRender) !==
          Math.round(this.current) ||
        this.scrollToRender < 10
      ) {
        this.DOM.scrollable.style.transform = `translate3d(0,${-1 *
          this.scrollToRender}px,0)`;
      }

    }

    render() {
        this.speed = Math.min(Math.abs(this.current - this.scrollToRender), 200)/200;
        this.speedTarget +=(this.speed - this.speedTarget)*0.2
        
        this.current = this.getScroll();
        this.scrollToRender = lerp(
          this.scrollToRender,
          this.current,
          this.ease
        );

      // and translate the scrollable element
      this.setPosition();
    }
}

Untuk menggunakan custom scroll diatas, buka file js/app.js.

Pertama import dahulu file scroll.js

import Scroll from './scroll';

Pada method constructor inisialisasi posisi scroll = 0

this.currentScroll = 0;

Tambahkan create object scroll saat promise dari image dan font selesai.

		Promise.all(allDone).then(()=>{
			this.scroll = new Scroll();
			this.addImages();
			this.setPosition();
	
			this.resize();
			this.setupResize();
			//this.addObject();
			this.render();
		});

Pada fungsi setPosition tambahkan nilai currentScroll pada perhitunga sumbu y.

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;
    });
}

Pada fungsi render perintah scroll.render, assign nilai currentScroll dan jalankan fungsi setPosition.

render(){
    this.time+=0.05;

    this.scroll.render();
    this.currentScroll = this.scroll.scrollToRender;
    this.setPosition();

    // this.mesh.rotation.x = this.time / 2000;
    // this.mesh.rotation.y = this.time / 1000;

    // this.material.uniforms.time.value = this.time;

    this.renderer.render( this.scene, this.camera );

    window.requestAnimationFrame(this.render.bind(this));
}

Terakhir, pada file css/style.css, atur agar opacity dari image html menjadi 0 (lihat class .item img)

.item img{
    width: 100%;
    display: block;
    opacity: 0;
}

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 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;
		
		Promise.all(allDone).then(()=>{
			this.scroll = new Scroll();
			this.addImages();
			this.setPosition();
	
			this.resize();
			this.setupResize();
			//this.addObject();
			this.render();
		});
		

	}
	
	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();
	}

	// addObject(){
	// 	this.geometry = new THREE.PlaneBufferGeometry(100, 100, 10, 10);
	// 	this.material = new THREE.MeshNormalMaterial();

	// 	this.material = new THREE.ShaderMaterial({
	// 		uniforms:{
	// 			time: {value: 0},
	// 			oceanTexture: {value: new THREE.TextureLoader().load(ocean)},
	// 		},
	// 		side: THREE.DoubleSide,
	// 		fragmentShader: fragment,
	// 		vertexShader: vertex,
	// 		wireframe: true,
	// 	});

	// 	this.mesh = new THREE.Mesh( this.geometry, this.material );
	// 	this.scene.add( this.mesh );
	// }

	render(){
		this.time+=0.05;

		this.scroll.render();
		this.currentScroll = this.scroll.scrollToRender;
		this.setPosition();

		// this.mesh.rotation.x = this.time / 2000;
		// this.mesh.rotation.y = this.time / 1000;

		// this.material.uniforms.time.value = this.time;
	
		this.renderer.render( this.scene, this.camera );

		window.requestAnimationFrame(this.render.bind(this));
	}

	addImages(){
		this.imageStore = this.images.map(img=>{
			let bounds = img.getBoundingClientRect();
			let geometry = new THREE.PlaneBufferGeometry(bounds.width, bounds.height, 1, 1);

			let texture = new THREE.Texture(img);
			texture.needsUpdate = true;

			let material = new THREE.MeshBasicMaterial({
				//color: 0xff0000, 
				map : 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')
});

Isi lengkap file css/style.css

*{
    margin: 0;
    padding: 0;
}
body{
    font-family: 'Open Sans', sans-serif;
}
.page{
    width: min(90%, 1200px);
    margin: 0 auto;
    /* border: 1px solid #000; */
}
/* header */

header{
    margin-bottom: 5em;
    position: relative;
}
h1{
    font-size: 260px;
    line-height: 1;
    padding: 0.5em 0 ;
    text-align: center;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
    color: #047582;
    mix-blend-mode: color-burn;
    font-size: 18vw;

    
}
header img{
    max-width: 100%;
    display: block;
}
h1,h2{
    font-family: 'Playfair Display', serif;
    font-weight: normal;
}

/* grid */
.grid{
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-gap: 2em 0
}

.item{
    /* border: 1px solid #000; */
    align-self: center;
    justify-self: center;
    text-decoration: none;
    color: #333;
}
.item:nth-child(odd){
    justify-self: start;
}
.item:nth-child(even){
    justify-self: end;
}
.item img{
    width: 100%;
    display: block;
    opacity: 0;
}
.item__image{
    position: relative;
    margin-bottom: 0.6em;
}
.item__meta{
    position: absolute;
    left: -0.4em;
    bottom: 0.4em;
    transform-origin: 0 100%;
    transform: rotate(-90deg);
    text-transform: uppercase;
    font-size: 80%;
}
.item__title{
    font-size: 40px;
    line-height: 1.2;
    margin-bottom: 0.4em;
}
.item_p{
    line-height: 1.4;
    font-size: 1em;
}
.item_h{
    width: 100%;
}
.item_v{
    width: 80%;
}


/* footer */
.footer{
    border-top: 1px solid #ccc;
    padding: 1em;
    font-size: 15px;
    display: flex;
    margin-top: 8em;
    line-height: 1.5;
    padding: 1em 0 4em 0;
}
.footer a{
    color: #333;
}
.footer p+p{
    margin-left: auto;
    text-align: right;
}

@media screen and (max-width: 600px) { 
    .grid{
        display: block;
    }
    .item_v,.item_h{
        width: auto;
        margin-bottom: 3em;
    }
    .item img{
        width: 100%;
        max-width: none;
    }
}

#container{
    height: 100vh;
    width: 100vw;
    position: fixed;
    z-index: -1;
    top: 0;
    left: 0;
}

JIka dijalankan, maka kita sudah berhasil mengatur posisi object WebGL saat web di scroll.

Setelah kita berhasil memposisikan object WebGL tepat dibawah image html, dan render object tersebut saat event mouse scroll terjadi. Pada modul berikutnya kita mulai membuat animasi.

Sharing is caring:

Leave a Comment