Skip to content

Instantly share code, notes, and snippets.

@jesperlandberg
Created February 26, 2019 20:44
Show Gist options
  • Select an option

  • Save jesperlandberg/050145a5cb53471aff19f5754b2363f3 to your computer and use it in GitHub Desktop.

Select an option

Save jesperlandberg/050145a5cb53471aff19f5754b2363f3 to your computer and use it in GitHub Desktop.
import * as twgl from 'twgl.js'
import TweenMax from 'gsap'
import math from '../utils/math'
import bindAll from '../utils/bindAll'
import config from '../config'
import Bullets from './Bullets'
import EventBus from '../utils/EventBus'
import { Events as GlobalRAFEvents } from '../utils/GlobalRAF'
import { Events as GlobalResizeEvents } from '../utils/GlobalResize'
import { Events as ScrollControllerEvents } from '../utils/ScrollController'
import { Events as GlobalMouseEvents } from '../utils/GlobalMouse'
const vert = `
precision mediump float;
attribute vec3 position;
attribute vec2 texcoord;
uniform mat4 uMatrix;
uniform mat4 uTMatrix;
uniform float uTime;
uniform vec2 uRes;
uniform vec2 uOffset;
uniform float uPower;
uniform float uStrength;
varying vec2 vTexcoord;
void main() {
vec3 pos = position.xzy;
float dist = distance(uOffset, vec2(pos.x, pos.y));
float rippleEffect = cos(uStrength * (dist - (uTime / 60.0)));
float distortionEffect = rippleEffect * uPower;
pos.x += (distortionEffect / 15.0 * (uRes.x / uRes.y) * (uOffset.x - pos.x));
pos.y += distortionEffect / 15.0 * (uOffset.y - pos.y);
gl_Position = uMatrix * vec4(pos, 1.0);
vTexcoord = (uTMatrix * vec4(texcoord - vec2(.5), 0, 1)).xy + vec2(.5);
}
`
const frag = `
precision mediump float;
uniform sampler2D uTex;
varying vec2 vTexcoord;
void main() {
vec2 uv = vTexcoord;
vec4 tex = texture2D(uTex, uv);
gl_FragColor = tex;
}
`
const fragSlider = `
precision mediump float;
uniform sampler2D uTexOne;
uniform sampler2D uTexTwo;
uniform float uProgress;
varying vec2 vTexcoord;
void main() {
vec2 uv = vTexcoord;
vec4 color = vec4(1.0);
vec4 texOne = texture2D(uTexOne, uv);
vec4 texTwo = texture2D(uTexTwo, uv);
float effect = step(uv.x, uProgress);
color = mix(texOne, texTwo, effect);
gl_FragColor = color;
}
`
class MainGl {
constructor() {
bindAll(this, ['render', 'event', 'getPos'])
this.canvas = document.getElementById('gl-bg')
this.gl = this.canvas.getContext('webgl')
if (!this.gl) return
this.images = document.querySelectorAll('[data-gl-texture]')
this.slider = document.querySelector('[data-gl-slider]')
this.programInfo = twgl.createProgramInfo(this.gl, [vert, frag])
this.programInfoSlider = twgl.createProgramInfo(this.gl, [vert, fragSlider])
this.bufferInfo = twgl.primitives.createPlaneBufferInfo(this.gl, 1, 1, 15, 15)
this.data = {
target: 0,
current: 0,
threshold: 100,
ease: config.isDevice ? 0.1 : 0.135,
translate: 0,
scrollDiff: 0
}
this.mouse = {
x: 0,
y: 0
}
this.bounds = {
width: this.gl.canvas.clientWidth,
height: this.gl.canvas.clientHeight,
res: [this.gl.canvas.clientWidth, this.gl.canvas.clientHeight]
}
this.time = 0
this.infos = null
this.hovers = null
this.matrix = null
this.time = 0
this.init()
}
/**
* Creates cached array with plane objects
* @return {Array} Array with cached plane objects
*/
getTextureInfo() {
this.data.current = this.data.target = 0
this.images = document.querySelectorAll('[data-gl-texture]')
if (!this.images) return
this.infos = []
this.hovers = []
this.images.forEach((target, index) => {
const bounds = target.getBoundingClientRect()
const parallax = target.dataset.parallax || 1.0
const offset = (1.0 / parallax)
const src = target.dataset.glTexture
const info = {
// Target element
el: target,
// Texture
texture: null,
type: src.split('.').pop() === 'mp4' ? 'video' : 'image',
srcElement: null,
needsUpdate: false,
// Bounds
srcHeight: bounds.height,
srcWidth: bounds.width,
width: bounds.width,
height: bounds.height,
x: bounds.width / 2 + bounds.x,
y: (bounds.height / 2 + bounds.y) * offset,
top: bounds.y > config.height ? bounds.y : config.height,
// Scroll & Animation
translate: 0,
parallax: parallax,
time: 0,
strength: 7.5,
// Hovers
hover: {
tl: null,
state: false
},
// Transition
transition: {
state: false
}
}
info.texture = this.createTexture(src, info)
if (target.dataset.glHoverable != undefined) {
this.addHover(info)
}
if (target.dataset.glTransition != undefined) {
this.heroTransition(info)
}
this.infos.push(info)
})
}
/**
* Cache slider plane information
* @return {object} Cached slider plane
*/
getSliderInfo() {
this.slider = document.querySelector('[data-gl-slider]')
if (!this.slider) return
const bounds = this.slider.getBoundingClientRect()
const images = JSON.parse(`[${this.slider.dataset.glSlider}]`)
const bulletNav = this.slider.parentNode.querySelector('.js-bullet-nav')
this.sliderInfo = {
// Target element
el: this.slider,
// Texture
type: 'image',
srcElement: null,
needsUpdate: false,
// Bounds
srcHeight: bounds.height,
srcWidth: bounds.width,
width: bounds.width,
height: bounds.height,
x: bounds.width / 2 + bounds.x,
y: bounds.height / 2 + bounds.y,
top: bounds.y > config.height ? bounds.y : config.height,
// Textures
textures: [],
total: images.length - 1,
current: 0,
next: 1,
progress: 0,
// Scroll & Animation
translate: 0,
parallax: 1.0,
time: 0,
strength: 7.5,
offset: [0, 0],
// Bullets
bullets: new Bullets(bulletNav)
}
images.forEach(image => {
const texture = this.createTexture(image, this.sliderInfo)
this.sliderInfo.textures.push(texture)
})
this.addSliderClick(this.sliderInfo)
}
/**
* Deletes textures and resets planes
*/
deleteTextures() {
// Delete regular planes
this.infos.forEach(info => {
this.gl.deleteTexture(info.texture)
if (info.type === 'video') {
info.srcElement.pause()
}
info.srcElement = null
info = null
})
this.infos = null
this.images = null
// Delete slider plane
if (this.sliderInfo) {
this.sliderInfo.textures.forEach(texture => {
this.gl.deleteTexture(texture)
})
this.sliderInfo = null
}
}
/**
* Get mouse pixel coordinates from global mousemove listener
* @param {number} options.x [x mouse coordinate]
* @param {[type]} options.y [y mouse coordinate]
*/
getPos({ x, y }) {
this.mouse.x = x
this.mouse.y = y
}
/**
* Get virtual scroll y value
* @param {number} options.y [y virtual scroll]
*/
event({ y }) {
this.data.target += y
this.clamp()
}
clamp() {
this.data.target = Math.round(Math.min(Math.max(this.data.target, 0), config.docHeight))
}
/**
* Checks if plane is in viewport
* @param {number} h plane height
* @param {number} y plane rect.y position
* @return {boolean}
*/
inView(h, y) {
const start = y - this.data.current
const end = (y + h) - this.data.current
const isVisible = start < (this.data.threshold + config.height) && end > -this.data.threshold
return isVisible
}
/**
* Draws plane on correct coordinates
* @param {[type]} info plane object with cached data
*/
drawTexture(info) {
info.translate = (info.y + this.data.translate) * info.parallax
const isVisible = this.inView(info.height, info.top)
if (!isVisible) return
if (info.needsUpdate && info.srcElement.readyState === info.srcElement.HAVE_ENOUGH_DATA) {
if (!info.srcElement.didRenderLastFrame) {
this.updateTexture(info.texture, info.srcElement)
info.srcElement.didRenderLastFrame = true
} else {
info.srcElement.didRenderLastFrame = false
}
}
const matrix = twgl.m4.identity()
const tMatrix = twgl.m4.identity()
const texAspect = info.width / info.height
const imgAspect = info.srcWidth / info.srcHeight
let scaleY = 0
let scaleX = 0
if (imgAspect < texAspect) {
scaleY = 1
scaleX = imgAspect / texAspect
} else if (imgAspect > texAspect) {
scaleY = texAspect / imgAspect
scaleX = 1
}
twgl.m4.scale(tMatrix, [scaleY, scaleX, 0], tMatrix)
twgl.m4.ortho(0, this.bounds.width, this.bounds.height, 0, -1, 1, matrix)
twgl.m4.translate(matrix, [info.x, info.translate.toFixed(2), 1], matrix)
twgl.m4.scale(matrix, [info.width, info.height, 1], matrix)
if (!info.textures) {
this.gl.useProgram(this.programInfo.program)
twgl.setBuffersAndAttributes(this.gl, this.programInfo, this.bufferInfo)
twgl.setUniforms(this.programInfo, {
uMatrix: matrix,
uTMatrix: tMatrix,
uTex: info.texture,
uOffset: [0, 0],
uTime: this.time,
uPower: info.time,
uStrength: info.strength,
uRes: this.bounds.res
})
} else {
this.gl.useProgram(this.programInfoSlider.program)
twgl.setBuffersAndAttributes(this.gl, this.programInfoSlider, this.bufferInfo)
twgl.setUniforms(this.programInfoSlider, {
uMatrix: matrix,
uTMatrix: tMatrix,
uTexOne: info.textures[info.current],
uTexTwo: info.textures[info.next],
uOffset: info.offset,
uTime: this.time,
uPower: info.time,
uStrength: info.strength,
uProgress: info.progress,
uRes: this.bounds.res
})
}
twgl.drawBufferInfo(this.gl, this.bufferInfo)
}
/**
* Loop and call drawtexture on each plane
*/
drawTextures() {
// Loop an draw regular planes
for (let i = 0, n = this.infos.length; i < n; ++i) {
const info = this.infos[i]
this.drawTexture(info)
}
// Draw slider plane
if (this.sliderInfo) {
this.drawTexture(this.sliderInfo)
}
}
/**
* the main render loop
*/
render() {
if (!config.gl || !this.infos) return
this.data.current += (this.data.target - this.data.current) * this.data.ease
this.data.translate = -this.data.current
this.data.scrollDiff = this.data.target - this.data.current
twgl.resizeCanvasToDisplaySize(this.gl.canvas)
this.time++
this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight)
this.gl.clearColor(0, 0, 0, 0)
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
this.drawTextures()
}
/**
* Ripple on page transition
* @param {object} info Plane object
*/
heroTransition(info) {
info.transition.state = true
TweenMax.set(info, { time: 2 })
TweenMax.to(info, 3, {
time: 0,
ease: Expo.easeOut,
onComplete: () => {
info.transition.state = false
}
})
}
onMouseEnter(data) {
data.hover.state = true
data.hover.tl.restart()
}
addHover(data) {
data.hover.tl = new TimelineMax({ paused: true, onComplete: () => { data.hover.state = false } })
const tl = new TimelineLite({ paused: true })
tl
.to(data, 1, { time: 0.65, ease: Linear.easeNone })
.to(data, 1, { time: 0, ease: Linear.easeNone })
data.hover.tl
.to(tl, 2, { progress: 1, ease: Power3.easeOut })
data.el.addEventListener('mouseenter', () => this.onMouseEnter(data))
}
/**
* Add click listeners to plane.
* Update timeline and texture index
* @param {object} info Plane object
*/
addSliderClick(info) {
info.tl = new TimelineMax({
paused: true,
onComplete: () => {
info.isAnimating = false
info.progress = 0
this.changeSliderIndex(info)
}
})
const tl = new TimelineLite({ paused: true })
tl
.to(info, 0.5, { time: 0.5, ease: Linear.easeNone })
.to(info, 1, { time: 0, ease: Linear.easeNone })
info.tl
.to(tl, 1.25, { progress: 1, ease: Power2.easeInOut })
.to(info, 1.1, { progress: 1, ease: Expo.easeInOut }, 0)
info.el.addEventListener('click', () => this.onSliderClick(info))
}
/**
* Updates texture index
* @param {object} info Plane object
*/
changeSliderIndex(info) {
info.current = info.next
info.next = info.next === info.total ? 0 : info.next + 1
}
/**
* Plays slide transition and updates mouse to plane coords
* @param {object} info plane cache information
*/
onSliderClick(info) {
if (info.isAnimating) return
info.offset = [
this.mouse.x / this.gl.canvas.width * 2 - 1,
this.mouse.y / this.gl.canvas.height * 2 - 1
]
info.bullets.animate(info.next)
info.isAnimating = true
info.tl.restart()
}
/**
* Creates a webgl texture
* @param {url} src image url
* @param {object} info plane cache
* @return {object} webgl texture
*/
createTexture(src, info) {
let texture = this.gl.createTexture()
this.gl.bindTexture(this.gl.TEXTURE_2D, texture)
const level = 0
const internalFormat = this.gl.RGB
const width = 1
const height = 1
const border = 0
const srcFormat = this.gl.RGB
const srcType = this.gl.UNSIGNED_BYTE
const pixel = new Uint8Array([0, 0, 0, 0])
this.gl.texImage2D(this.gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, pixel)
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
if (info.type === 'image') {
const image = document.createElement('img')
image.addEventListener('load', () => {
this.gl.bindTexture(this.gl.TEXTURE_2D, texture)
this.gl.texImage2D(this.gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image)
info.srcWidth = image.width
info.srcHeight = image.height
})
image.src = src
} else if (info.type === 'video') {
info.srcElement = document.createElement('video')
info.srcElement.autoplay = true
info.srcElement.muted = true
info.srcElement.loop = true
info.srcElement.controls = false
info.srcElement.didRenderLastFrame = false
info.srcElement.addEventListener('loadeddata', () => {
info.needsUpdate = true
info.srcWidth = info.srcElement.videoWidth
info.srcHeight = info.srcElement.videoHeight
})
info.srcElement.src = src
info.srcElement.play()
}
return texture
}
/**
* Update a texture if needsUpdate = true
* @param {object} texture Webgl texture
* @param {node} srcElement Node element
*/
updateTexture(texture, srcElement) {
const level = 0
const internalFormat = this.gl.RGB
const srcFormat = this.gl.RGB
const srcType = this.gl.UNSIGNED_BYTE
this.gl.bindTexture(this.gl.TEXTURE_2D, texture)
this.gl.texImage2D(this.gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, srcElement)
}
/**
* Add global event listeners
*/
addListeners() {
EventBus.on(GlobalRAFEvents.TICK, this.render)
EventBus.on(ScrollControllerEvents.SCROLL, this.event)
EventBus.on(GlobalMouseEvents.MOVE, this.getPos)
}
/**
* Initalise
*/
init() {
this.addListeners()
}
}
export default MainGl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment