Connect with us

Technology

Creating an Infinite Auto-Scrolling Gallery utilizing WebGL with OGL and GLSL Shaders | Codrops


From our sponsor: Elevate all of your advertising and marketing with Mailchimp Smarts.

Howdy everybody, introducing myself a little bit bit first, I’m Luis Henrique Bizarro, I’m a Senior Artistic Developer at Energetic Idea based mostly in São Paulo, Brazil. It’s at all times a pleasure to me having the chance to collaborate with Codrops to assist different builders be taught new issues, so I hope everybody enjoys this tutorial!

On this tutorial I’ll clarify you easy methods to create an auto scrolling infinite picture gallery. The picture grid can also be scrollable be person interplay, making it an attention-grabbing design factor to showcase works. It’s based mostly on this nice animation seen on Oneshot.finance made by Jesper Landberg.

I’ve been utilizing the strategy of styling photos first with HTML + CSS after which creating an abstraction of those parts inside WebGL utilizing some digital camera and viewport calculations in a number of web sites, so that is the strategy we’re going to make use of on this tutorial.

The advantage of this implementation is that it may be reused throughout any WebGL library, so if you happen to’re extra accustomed to Three.js or Babylon.js than OGL, you’ll additionally be capable of obtain the identical outcomes utilizing an analogous code, when it’s about shading and scaling the aircraft meshes.

So let’s get into it!

Implementing our HTML markup

Step one is implementing our HTML markup. We’re going to make use of <determine> and <img> parts, nothing particular right here, simply the usual:

<div class="demo-1__gallery">
  <determine class="demo-1__gallery__figure">
    <img class="demo-1__gallery__image" src="https://tympanus.web/codrops/2021/01/05/creating-an-infinite-auto-scrolling-gallery-using-webgl-with-ogl-and-glsl-shaders/photos/demo-1/1.jpg">
  </determine>

  <!-- Repeating the identical markup till 12.jpg. -->
</div>

Setting our CSS kinds

The second step is styling our parts utilizing CSS. One of many first issues I do in a web site is defining the font-size of the html factor as a result of I take advantage of rem to assist with the responsive breakpoints.

This is useful if you happen to’re doing inventive web sites that solely require two or three completely different breakpoints, so I extremely advocate beginning utilizing it if you happen to haven’t adopted rem but.

One factor I’m additionally utilizing is calc() with the dimensions of the designs. In our tutorial we’re going to make use of 1920 as our major width, scaling our font-size relying on the display measurement of 100vw. This leads to 10px at a 1920px display, for instance:

html {
  font-size: calc(100vw / 1920 * 10);
}

Now let’s type our grid of photos. We need to freely place our photos throughout the display utilizing absolute positioning, so we’re simply going to set the top, width and left/prime kinds throughout all our demo-1 lessons:

.demo-1__gallery {
  top: 295rem;
  place: relative;
  visibility: hidden;
}

.demo-1__gallery__figure {
  place: absolute;
 
  &:nth-child(1) {
    top: 40rem;
    width: 70rem;
  }
 
  &:nth-child(2) {
    top: 50rem;
    left: 85rem;
    prime: 30rem;
    width: 40rem;
  }
 
  &:nth-child(3) {
    top: 50rem;
    left: 15rem;
    prime: 60rem;
    width: 60rem;
  }
 
  &:nth-child(4) {
    top: 30rem;
    proper: 0;
    prime: 10rem;
    width: 50rem;
  }
 
  &:nth-child(5) {
    top: 60rem;
    proper: 15rem;
    prime: 55rem;
    width: 40rem;
  }
 
  &:nth-child(6) {
    top: 75rem;
    left: 5rem;
    prime: 120rem;
    width: 57.5rem;
  }
 
  &:nth-child(7) {
    top: 70rem;
    proper: 0;
    prime: 130rem;
    width: 50rem;
  }
 
  &:nth-child(8) {
    top: 50rem;
    left: 85rem;
    prime: 95rem;
    width: 40rem;
  }
 
  &:nth-child(9) {
    top: 65rem;
    left: 75rem;
    prime: 155rem;
    width: 50rem;
  }
 
  &:nth-child(10) {
    top: 43rem;
    proper: 0;
    prime: 215rem;
    width: 30rem;
  }
 
  &:nth-child(11) {
    top: 50rem;
    left: 70rem;
    prime: 235rem;
    width: 80rem;
  }
 
  &:nth-child(12) {
    left: 0;
    prime: 210rem;
    top: 70rem;
    width: 50rem;
  }
}
 
.demo-1__gallery__image {
  top: 100%;
  left: 0;
  object-fit: cowl;
  place: absolute;
  prime: 0;
  width: 100%;
}

Notice that we’re hiding the visibility of our HTML, as a result of it’s not going to be seen for the customers since we’re going to load these photos contained in the <canvas> factor. However beneath you will discover a screenshot of what the consequence will seem like.

Creating our OGL 3D setting

Now it’s time to get began with the WebGL implementation utilizing OGL. First let’s create an App class that’s going to be the entry level of our demo and inside it, let’s additionally create the preliminary strategies: createRenderer, createCamera, createScene, onResize and our requestAnimationFrame loop with replace.

import { Renderer, Digital camera, Remodel } from 'ogl'

class App {
  constructor () {
    this.createRenderer()
    this.createCamera()
    this.createScene()
 
    this.onResize()
 
    this.replace()
 
    this.addEventListeners()
  }
 
  createRenderer () {
    this.renderer = new Renderer({
      alpha: true
    })
 
    this.gl = this.renderer.gl
 
    doc.physique.appendChild(this.gl.canvas)
  }
 
  createCamera () {
    this.digital camera = new Digital camera(this.gl)
    this.digital camera.fov = 45
    this.digital camera.place.z = 5
  }
 
  createScene () {
    this.scene = new Remodel()
  }
 
  /**
   * Wheel.
   */
  onWheel (occasion) {
 
  }
 
  /**
   * Resize.
   */
  onResize () {
    this.display = {
      top: window.innerHeight,
      width: window.innerWidth
    }
 
    this.renderer.setSize(this.display.width, this.display.top)
 
    this.digital camera.perspective({
      facet: this.gl.canvas.width / this.gl.canvas.top
    })
 
    const fov = this.digital camera.fov * (Math.PI / 180)
    const top = 2 * Math.tan(fov / 2) * this.digital camera.place.z
    const width = top * this.digital camera.facet
 
    this.viewport = {
      top,
      width
    }
  }
 
  /**
   * Replace.
   */
  replace () {
    this.renderer.render({
      scene: this.scene,
      digital camera: this.digital camera
    })
 
    window.requestAnimationFrame(this.replace.bind(this))
  }
 
  /**
   * Listeners.
   */
  addEventListeners () {
    window.addEventListener('resize', this.onResize.bind(this))
 
    window.addEventListener('mousewheel', this.onWheel.bind(this))
    window.addEventListener('wheel', this.onWheel.bind(this))
  }
}

new App()

Explaining some a part of our App.js file

In our createRenderer technique, we’re initializing one renderer with alpha enabled, storing our GL context (this.renderer.gl) reference within the this.gl variable and appending our <canvas> factor to our doc.physique.

In our createCamera technique, we’re simply creating a brand new Digital camera and setting a few of its attributes: fov and its z place.

In our createScene technique, we’re utilizing the Remodel class, that’s the illustration of a brand new scene that’s going to comprise all our planes that symbolize our photos within the WebGL setting.

The onResize technique is an important a part of our preliminary setup. It’s liable for three various things:

  1. Ensuring we’re at all times resizing the <canvas> factor with the proper viewport sizes.
  2. Updating our this.digital camera perspective dividing the width and top of the viewport.
  3. Storing within the variable this.viewport, the worth representations that can assist to remodel pixels into 3D setting sizes through the use of the fov from the digital camera.

The strategy of utilizing the digital camera.fov to remodel pixels in 3D setting sizes is an strategy used fairly often in a number of WebGL implementations. Principally what it does is ensuring that if we do one thing like: this.mesh.scale.x = this.viewport.width; it’s going to make our mesh match the complete display width, behaving like width: 100%, however in 3D house.

And eventually in our replace, we’re setting our requestAnimationFrame loop and ensuring we maintain rendering our scene.

Create our reusable geometry occasion

It’s a very good apply to maintain reminiscence utilization low by at all times reusing the identical geometry reference it doesn’t matter what WebGL library you’re utilizing. To symbolize all our photos, we’re going to make use of a Airplane geometry, so let’s create a brand new technique and retailer this new geometry contained in the this.planeGeometry variable.

import { Renderer, Digital camera, Remodel, Airplane } from 'ogl'
 
createGeometry () {
  this.planeGeometry = new Airplane(this.gl)
}

Choose all photos and create a brand new class for every one

Now it’s time to make use of doc.querySelector to pick out all our photos and create one reusable class that’s going to symbolize our photos. (We’re going to create a single Media.js file later.)

createMedias () {
  this.mediasElements = doc.querySelectorAll('.demo-1__gallery__figure')
  this.medias = Array.from(this.mediasElements).map(factor => {
    let media = new Media({
      factor,
      geometry: this.planeGeometry,
      gl: this.gl,
      scene: this.scene,
      display: this.display,
      viewport: this.viewport
    })
 
    return media
  })
}

As you possibly can see, we’re simply deciding on all .demo-1__gallery__figure parts, going via them and producing an array of `this.medias` with new cases of Media.

Now it’s essential to begin attaching this array in essential items of our setup code.

Let’s first embody all our media inside the strategy onResize and in addition name media.onResize for every one in all these new cases:

if (this.medias) {
  this.medias.forEach(media => media.onResize({
    display: this.display,
    viewport: this.viewport
  }))
}

And inside our replace technique, we’re going to name media.replace() as effectively:

if (this.medias) {
  this.medias.forEach(media => media.replace())
}

Organising our Media.js file and sophistication

Our Media class goes to make use of Mesh, Program and Texture lessons from OGL to create a 3D aircraft and attribute a texture to it, which in our case goes to be our photos.

In our constructor, we’re going to retailer all variables that we want and that had been handed within the new Media() initialization from index.js:

import { Mesh, Program, Texture } from 'ogl'
 
import fragment from 'shaders/fragment.glsl'
import vertex from 'shaders/vertex.glsl'
 
export default class {
  constructor ({ factor, geometry, gl, scene, display, viewport }) {
    this.factor = factor
    this.picture = this.factor.querySelector('img')
 
    this.geometry = geometry
    this.gl = gl
    this.scene = scene
    this.display = display
    this.viewport = viewport
 
    this.createMesh()
    this.createBounds()
 
    this.onResize()
  }
}

In our createMesh technique, we’ll load the picture texture utilizing the this.picture.src attribute, then create a brand new Program, which is principally a illustration of the fabric we’re making use of to our Mesh. So our technique seems like this:

createMesh () {
  const picture = new Picture()
  const texture = new Texture(this.gl)

  picture.src = this.picture.src
  picture.onload = _ => {
    texture.picture = picture
  }

  const program = new Program(this.gl, {
    fragment,
    vertex,
    uniforms: {
      tMap: { worth: texture },
      uScreenSizes: { worth: [0, 0] },
      uImageSizes: { worth: [0, 0] }
    },
    clear: true
  })

  this.aircraft = new Mesh(this.gl, {
    geometry: this.geometry,
    program
  })

  this.aircraft.setParent(this.scene)
}

Seems to be fairly easy, proper? After we generate a brand new Mesh, we’re setting the aircraft as kids of this.scene, so we’re together with our mesh inside our major scene.

As you’ve most likely seen, our Program receives fragment and vertex. These each symbolize the shaders we’re going to make use of on our planes. For now, we’re simply utilizing easy implementations of each.

In our vertex.glsl file we’re getting the uv and place attributes, and ensuring we’re rendering our planes in the proper 3D world place.

attribute vec2 uv;
attribute vec3 place;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
various vec2 vUv;
 
void major() {
  vUv = uv;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
}

In our fragment.glsl file, we’re receiving a tMap texture, as you possibly can see within the tMap: { worth: texture } declaration, and rendering it in our aircraft geometry:

precision highp float;
 
uniform sampler2D tMap;
 
various vec2 vUv;
 
void major() {
  gl_FragColor.rgb = texture2D(tMap, vUv).rgb;
  gl_FragColor.a = 1.0;
}

The createBounds technique is essential to verify we’re positioning and scaling our planes within the right DOM parts positions, so it’s principally going to name for this.factor.getBoundingClientRect() to get the proper place of our planes, after which after that utilizing these values to calculate the 3D values of our aircraft.

createBounds () {
  this.bounds = this.factor.getBoundingClientRect()

  this.updateScale()
  this.updateX()
  this.updateY()
}

updateScale () {
  this.aircraft.scale.x = this.viewport.width * this.bounds.width / this.display.width
  this.aircraft.scale.y = this.viewport.top * this.bounds.top / this.display.top
}

updateX (x = 0) {
  this.aircraft.place.x = -(this.viewport.width / 2) + (this.aircraft.scale.x / 2) + ((this.bounds.left - x) / this.display.width) * this.viewport.width
}

updateY (y = 0) {
  this.aircraft.place.y = (this.viewport.top / 2) - (this.aircraft.scale.y / 2) - ((this.bounds.prime - y) / this.display.top) * this.viewport.top
}

replace (y) {
  this.updateScale()
  this.updateX()
  this.updateY(y)
}

As you’ve most likely seen, the calculations for scale.x and scale.y are going to stretch our aircraft to make it the identical width and top of the <img> parts. And the place.x and place.y takes the offset from the factor and makes our translate our planes to the proper x and y axis in 3D.

And let’s not neglect our onResize technique, which is principally going to name createBounds once more to refresh our getBoundingClientRect values and ensure we maintain our 3D implementation responsive as effectively.

onResize (sizes) {
  if (sizes) {
    const { display, viewport } = sizes
 
    if (display) this.display = display
    if (viewport) this.viewport = viewport
  }
 
  this.createBounds()
}

That is the consequence we’ve acquired thus far.

Implement cowl habits in fragment shaders

As you’ve most likely seen, our photos are stretched. It occurs as a result of we have to make correct calculations within the fragment shaders as a way to have a habits like object-fit: cowl; or background-size: cowl; in WebGL.

I like to make use of an strategy to move the photographs’ actual sizes and do some ratio calculations contained in the fragment shader, so let’s adapt our code to this strategy. So in our Program, we’re going to move two new uniforms known as uPlaneSizes and uImageSizes:

const program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { worth: texture },
    uPlaneSizes: { worth: [0, 0] },
    uImageSizes: { worth: [0, 0] }
  },
  clear: true
})

Now we have to replace our fragment.glsl and use these values to calculate our photos ratios:

precision highp float;
 
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
 
various vec2 vUv;
 
void major() {
  vec2 ratio = vec2(
    min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
    min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
  );
 
  vec2 uv = vec2(
    vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
    vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
  );
 
  gl_FragColor.rgb = texture2D(tMap, uv).rgb;
  gl_FragColor.a = 1.0;
}

After which we additionally want replace our picture.onload technique to move naturalWidth and naturalHeight to uImageSizes:

picture.onload = _ => {
  program.uniforms.uImageSizes.worth = [image.naturalWidth, image.naturalHeight]
  texture.picture = picture
}

And createBounds to replace the uPlaneSizes uniforms:

createBounds () {
  this.bounds = this.factor.getBoundingClientRect()
 
  this.updateScale()
  this.updateX()
  this.updateY()
 
  this.aircraft.program.uniforms.uPlaneSizes.worth = [this.plane.scale.x, this.plane.scale.y]
}

That’s it! Now now we have correctly scaled photos.

Implementing clean scrolling

Earlier than we implement our infinite logic, it’s good to begin making scrolling work correctly. In our setup code, now we have included a onWheel technique, which goes for use to lerp some variables and make our scroll butter clean.

In our constructor from index.js, let’s create the this.scroll object with these variables:

this.scroll = {
  ease: 0.05,
  present: 0,
  goal: 0,
}

Now let’s replace our onWheel implementation. When working with wheel occasions, it’s at all times essential to normalize it, as a result of it behaves in a different way based mostly on the browser, I’ve been utilizing normalize-wheel library to assist on it:

import NormalizeWheel from 'normalize-wheel'

onWheel (occasion) {
  const normalized = NormalizeWheel(occasion)
  const pace = normalized.pixelY
 
  this.scroll.goal += pace * 0.5
}

Let’s additionally create our lerp utility operate contained in the file utils/math.js:

export operate lerp (p1, p2, t) {
  return p1 + (p2 - p1) * t
}

And now we simply must lerp from the this.scroll.present to the this.scroll.goal contained in the replace technique. And eventually move it to the media.replace() strategies:

replace () {
  this.scroll.present = lerp(this.scroll.present, this.scroll.goal, this.scroll.ease)
 
  if (this.medias) {
    this.medias.forEach(media => media.replace(this.scroll.present))
  }
}

After that we have already got a consequence like this.

Making our clean scrolling infinite

The strategy of creating an infinite scrolling logic is principally repeating the identical grid again and again whereas the person retains scrolling your web page. Because the person can scroll up or down, you additionally must take into account what course is being scrolled, so general the algorithm ought to work this manner:

  • Should you’re scrolling down, your parts transfer up — when your first factor isn’t on the display anymore, you must transfer it to the tip of the listing.
  • Should you’re scrolling up, your parts transfer to down — when your final factor isn’t on the display anymore, you must transfer it to the beginning of the listing.

To clarify it in a visible means, let’s say we’re scrolling down and the purple space is our viewport and the blue parts should not within the viewport anymore.

Once we are on this state, we simply want to maneuver the blue parts to the tip of our gallery grid, which is the complete top of our gallery: 295rem.

Let’s embody the logic for it then. First, we have to create a brand new variable known as this.scroll.final to retailer the final worth of our scroll, that is going to be checked to provide us up or down strings:

this.scroll = {
  ease: 0.05,
  present: 0,
  goal: 0,
  final: 0
}

In our replace technique, we have to embody the next strains of validations and move this.course to our this.medias parts.

replace () {
  this.scroll.present = lerp(this.scroll.present, this.scroll.goal, this.scroll.ease)
 
  if (this.scroll.present > this.scroll.final) {
    this.course = 'down'
  } else if (this.scroll.present < this.scroll.final) {
    this.course = 'up'
  }
 
  if (this.medias) {
    this.medias.forEach(media => media.replace(this.scroll.present, this.course))
  }
 
  this.renderer.render({
    scene: this.scene,
    digital camera: this.digital camera
  })
 
  this.scroll.final = this.scroll.present
 
  window.requestAnimationFrame(this.replace.bind(this))
}

Then we have to get the overall gallery top and remodel it to 3D dimensions, so let’s embody a querySelector of .demo-1__gallery and name the createGallery technique in our index.js constructor.

createGallery () {
  this.gallery = doc.querySelector('.demo-1__gallery')
}

It’s time to do the true calculations utilizing this selector, so in our onResize technique, we have to embody the next strains:

this.galleryBounds = this.gallery.getBoundingClientRect()
this.galleryHeight = this.viewport.top * this.galleryBounds.top / this.display.top

The this.galleryHeight variable now could be storing the 3D measurement of the complete grid, now we have to move it to each onResize and new Media() calls:

if (this.medias) {
  this.medias.forEach(media => media.onResize({
    top: this.galleryHeight,
    display: this.display,
    viewport: this.viewport
  }))
}
this.medias = Array.from(this.mediasElements).map(factor => {
  let media = new Media({
    factor,
    geometry: this.planeGeometry,
    gl: this.gl,
    top: this.galleryHeight,
    scene: this.scene,
    display: this.display,
    viewport: this.viewport
  })
 
  return media
})

After which inside our Media class, we have to retailer the top as effectively within the constructor and in addition within the onResize strategies:

constructor ({ factor, geometry, gl, top, scene, display, viewport }) {
  this.top = top
}
onResize (sizes) {
  if (sizes) {
    const { top, display, viewport } = sizes
 
    if (top) this.top = top
    if (display) this.display = display
    if (viewport) this.viewport = viewport
  }
}

Now we’re going to incorporate the logic to maneuver our parts based mostly on their viewport place, identical to our visible illustration of the purple and blue rectangles.

If the concept is to maintain summing up a price based mostly on the scroll and factor place, we are able to obtain this by simply creating a brand new variable known as this.further = 0, that is going to retailer how a lot we have to sum (or subtract) of our media, so in our constructor let’s embody it:

constructor ({ factor, geometry, gl, top, scene, display, viewport }) {
    this.further = 0
}

And let’s reset it on resizing the browser, to make all values constant so it doesn’t break when customers resizes their viewport:

onResize (sizes) {
  this.further = 0
}

And in our updateY technique, we’re going to incorporate it as effectively:

updateY (y = 0) {
  this.aircraft.place.y = ((this.viewport.top / 2) - (this.aircraft.scale.y / 2) - ((this.bounds.prime - y) / this.display.top) * this.viewport.top) - this.further
}

Lastly, the one factor left now could be updating the this.further variable inside our replace technique, ensuring we’re including or subtracting the this.top relying on the course.

const planeOffset = this.aircraft.scale.y / 2
const viewportOffset = this.viewport.top / 2
 
this.isBefore = this.aircraft.place.y + planeOffset < -viewportOffset
this.isAfter = this.aircraft.place.y - planeOffset > viewportOffset
 
if (course === 'up' && this.isBefore) {
  this.further -= this.top
 
  this.isBefore = false
  this.isAfter = false
}
 
if (course === 'down' && this.isAfter) {
  this.further += this.top
 
  this.isBefore = false
  this.isAfter = false
}

Since we’re working in 3D house, we’re coping with cartesian coordinates, that’s why you possibly can discover we’re dividing most issues by two (ex: this.viewport.heighht / 2). In order that’s additionally the rationale why we needed to do a special logic for the this.isBefore and this.isAfter checks.

Superior, we’re virtually ending our demo! That’s the way it seems now, fairly cool to have it infinite proper?

Together with contact occasions

Let’s additionally embody contact occasions, so this demo might be extra attentive to person interactions! In our addEventListeners technique, let’s embody some window.addEventListener calls:

window.addEventListener('mousedown', this.onTouchDown.bind(this))
window.addEventListener('mousemove', this.onTouchMove.bind(this))
window.addEventListener('mouseup', this.onTouchUp.bind(this))
 
window.addEventListener('touchstart', this.onTouchDown.bind(this))
window.addEventListener('touchmove', this.onTouchMove.bind(this))
window.addEventListener('touchend', this.onTouchUp.bind(this))

Then we simply must implement easy contact occasions calculations, together with the three strategies: onTouchDown, onTouchMove and onTouchUp.

onTouchDown (occasion) {
  this.isDown = true

  this.scroll.place = this.scroll.present
  this.begin = occasion.touches ? occasion.touches[0].clientY : occasion.clientY
}

onTouchMove (occasion) {
  if (!this.isDown) return

  const y = occasion.touches ? occasion.touches[0].clientY : occasion.clientY
  const distance = (this.begin - y) * 2

  this.scroll.goal = this.scroll.place + distance
}

onTouchUp (occasion) {
  this.isDown = false
}

Completed! Now we even have contact occasions assist enabled for our gallery.

Implementing direction-aware auto scrolling

Let’s additionally implement auto scrolling to make our interplay even higher. As a way to obtain that we simply must create a brand new variable that can retailer our pace based mostly on the course the person is scrolling.

So let’s create a variable known as this.pace in our index.js file:

constructor () {
  this.pace = 2
}

This variable goes to be modified by our down and up validations now we have in our replace loop, so if the person is scrolling down, we’re going to maintain the pace as 2, if the person is scrolling up, we’re going to switch it with -2, and earlier than that we are going to sum this.pace to the this.scroll.goal variable:

replace () {
  this.scroll.goal += this.pace

  this.scroll.present = lerp(this.scroll.present, this.scroll.goal, this.scroll.ease)

  if (this.scroll.present > this.scroll.final) {
    this.course = 'down'
    this.pace = 2
  } else if (this.scroll.present < this.scroll.final) {
    this.course = 'up'
    this.pace = -2
  }
}

Implementing distortion shaders

Now let’s make every part much more attention-grabbing, it’s time to play a little bit bit with shaders and deform our planes whereas the person is scrolling via our web page.

First, let’s replace our replace technique from index.js, ensuring we expose each present and final scroll values to all our medias, we’re going to do a easy calculation with them.

replace () {
  if (this.medias) {
    this.medias.forEach(media => media.replace(this.scroll, this.course))
  }
}

And now let’s create two uniforms for our Program shader: uOffset and uViewportSizes, and move them:

const program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { worth: texture },
    uPlaneSizes: { worth: [0, 0] },
    uImageSizes: { worth: [0, 0] },
    uViewportSizes: { worth: [this.viewport.width, this.viewport.height] },
    uStrength: { worth: 0 }
  },
  clear: true
})

As you possibly can most likely discover, we’re going to want to set uViewportSizes in our onResize technique as effectively, since this.viewport adjustments after we resize, so to maintain this.viewport.width and this.viewport.top updated, we additionally want to incorporate the next strains of code in onResize:

onResize (sizes) {
  if (sizes) {
    const { top, display, viewport } = sizes
 
    if (top) this.top = top
    if (display) this.display = display
    if (viewport) {
      this.viewport = viewport
 
      this.aircraft.program.uniforms.uOffset.worth = [this.viewport.width, this.viewport.height]
    }
  }
}

Bear in mind the this.scroll replace we’ve constituted of index.js? Now it’s time to incorporate a small trick to generate a pace worth inside our Media.js:

replace (y, course) {
  this.updateY(y.present)
 
  this.aircraft.program.uniforms.uStrength.worth = ((y.present - y.final) / this.display.width) * 10
}

We’re principally checking the distinction between the present and final values, which returns us some sort of “pace” of the scrolling, and dividing it by the this.display.width, to maintain our impact worth behaving appropriately independently of the width of our display.

Lastly now it’s time to play a little bit bit with our vertex shader. We’re going to bend our planes a little bit bit whereas the person is scrolling via the web page. So let’s replace our vertex.glsl file with this new code:

#outline PI 3.1415926535897932384626433832795
 
precision highp float;
precision highp int;
 
attribute vec3 place;
attribute vec2 uv;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
uniform float uStrength;
uniform vec2 uViewportSizes;
 
various vec2 vUv;
 
void major() {
  vec4 newPosition = modelViewMatrix * vec4(place, 1.0);
 
  newPosition.z += sin(newPosition.y / uViewportSizes.y * PI + PI / 2.0) * -uStrength;
 
  vUv = uv;
 
  gl_Position = projectionMatrix * newPosition;
}

That’s it! Now we’re additionally bending our photos creating an distinctive kind of impact!

Explaining a little bit little bit of the shader logic: principally what’s applied within the newPosition.z line is taking into account the uViewportSize.y, which is our top from the viewport and the present place.y of our aircraft, getting the division of each and multiplying by PI that we outlined on the very prime of our shader file. After which we use the uStrength which is the power of the bending, that’s tight with our scrolling values, making it bend based mostly on how quicker you scroll the demo.

That’s the ultimate results of our demo! I hope this tutorial was helpful to you and don’t neglect to remark you probably have any questions!

Pictures used within the demos by Planete Elevene and Jayson Hinrichsen.



Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *