Created
February 26, 2019 20:44
-
-
Save jesperlandberg/050145a5cb53471aff19f5754b2363f3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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