Shader powered image transition
A Pen by Szenia Zadvornykh on CodePen.
| <div id="three-container"></div> | |
| <div id="instructions"> | |
| click and drag to control the animation | |
| </div> |
| window.onload = init; | |
| console.ward = function() {}; // what warnings? | |
| function init() { | |
| var root = new THREERoot({ | |
| createCameraControls: !true, | |
| antialias: (window.devicePixelRatio === 1), | |
| fov: 80 | |
| }); | |
| root.renderer.setClearColor(0x000000, 0); | |
| root.renderer.setPixelRatio(window.devicePixelRatio || 1); | |
| root.camera.position.set(0, 0, 60); | |
| var width = 100; | |
| var height = 60; | |
| var slide = new Slide(width, height, 'out'); | |
| var l1 = new THREE.ImageLoader(); | |
| l1.setCrossOrigin('Anonymous'); | |
| l1.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/winter.jpg', function(img) { | |
| slide.setImage(img); | |
| }) | |
| root.scene.add(slide); | |
| var slide2 = new Slide(width, height, 'in'); | |
| var l2 = new THREE.ImageLoader(); | |
| l2.setCrossOrigin('Anonymous'); | |
| l2.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/spring.jpg', function(img) { | |
| slide2.setImage(img); | |
| }) | |
| root.scene.add(slide2); | |
| var tl = new TimelineMax({repeat:-1, repeatDelay:1.0, yoyo: true}); | |
| tl.add(slide.transition(), 0); | |
| tl.add(slide2.transition(), 0); | |
| createTweenScrubber(tl); | |
| window.addEventListener('keyup', function(e) { | |
| if (e.keyCode === 80) { | |
| tl.paused(!tl.paused()); | |
| } | |
| }); | |
| } | |
| //////////////////// | |
| // CLASSES | |
| //////////////////// | |
| function Slide(width, height, animationPhase) { | |
| var plane = new THREE.PlaneGeometry(width, height, width * 2, height * 2); | |
| THREE.BAS.Utils.separateFaces(plane); | |
| var geometry = new SlideGeometry(plane); | |
| geometry.bufferUVs(); | |
| var aAnimation = geometry.createAttribute('aAnimation', 2); | |
| var aStartPosition = geometry.createAttribute('aStartPosition', 3); | |
| var aControl0 = geometry.createAttribute('aControl0', 3); | |
| var aControl1 = geometry.createAttribute('aControl1', 3); | |
| var aEndPosition = geometry.createAttribute('aEndPosition', 3); | |
| var i, i2, i3, i4, v; | |
| var minDuration = 0.8; | |
| var maxDuration = 1.2; | |
| var maxDelayX = 0.9; | |
| var maxDelayY = 0.125; | |
| var stretch = 0.11; | |
| this.totalDuration = maxDuration + maxDelayX + maxDelayY + stretch; | |
| var startPosition = new THREE.Vector3(); | |
| var control0 = new THREE.Vector3(); | |
| var control1 = new THREE.Vector3(); | |
| var endPosition = new THREE.Vector3(); | |
| var tempPoint = new THREE.Vector3(); | |
| function getControlPoint0(centroid) { | |
| var signY = Math.sign(centroid.y); | |
| tempPoint.x = THREE.Math.randFloat(0.1, 0.3) * 50; | |
| tempPoint.y = signY * THREE.Math.randFloat(0.1, 0.3) * 70; | |
| tempPoint.z = THREE.Math.randFloatSpread(20); | |
| return tempPoint; | |
| } | |
| function getControlPoint1(centroid) { | |
| var signY = Math.sign(centroid.y); | |
| tempPoint.x = THREE.Math.randFloat(0.3, 0.6) * 50; | |
| tempPoint.y = -signY * THREE.Math.randFloat(0.3, 0.6) * 70; | |
| tempPoint.z = THREE.Math.randFloatSpread(20); | |
| return tempPoint; | |
| } | |
| for (i = 0, i2 = 0, i3 = 0, i4 = 0; i < geometry.faceCount; i++, i2 += 6, i3 += 9, i4 += 12) { | |
| var face = plane.faces[i]; | |
| var centroid = THREE.BAS.Utils.computeCentroid(plane, face); | |
| // animation | |
| var duration = THREE.Math.randFloat(minDuration, maxDuration); | |
| var delayX = THREE.Math.mapLinear(centroid.x, -width * 0.5, width * 0.5, 0.0, maxDelayX); | |
| var delayY; | |
| if (animationPhase === 'in') { | |
| delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, 0.0, maxDelayY) | |
| } | |
| else { | |
| delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, maxDelayY, 0.0) | |
| } | |
| for (v = 0; v < 6; v += 2) { | |
| aAnimation.array[i2 + v] = delayX + delayY + (Math.random() * stretch * duration); | |
| aAnimation.array[i2 + v + 1] = duration; | |
| } | |
| // positions | |
| endPosition.copy(centroid); | |
| startPosition.copy(centroid); | |
| if (animationPhase === 'in') { | |
| control0.copy(centroid).sub(getControlPoint0(centroid)); | |
| control1.copy(centroid).sub(getControlPoint1(centroid)); | |
| } | |
| else { // out | |
| control0.copy(centroid).add(getControlPoint0(centroid)); | |
| control1.copy(centroid).add(getControlPoint1(centroid)); | |
| } | |
| for (v = 0; v < 9; v += 3) { | |
| aStartPosition.array[i3 + v] = startPosition.x; | |
| aStartPosition.array[i3 + v + 1] = startPosition.y; | |
| aStartPosition.array[i3 + v + 2] = startPosition.z; | |
| aControl0.array[i3 + v] = control0.x; | |
| aControl0.array[i3 + v + 1] = control0.y; | |
| aControl0.array[i3 + v + 2] = control0.z; | |
| aControl1.array[i3 + v] = control1.x; | |
| aControl1.array[i3 + v + 1] = control1.y; | |
| aControl1.array[i3 + v + 2] = control1.z; | |
| aEndPosition.array[i3 + v] = endPosition.x; | |
| aEndPosition.array[i3 + v + 1] = endPosition.y; | |
| aEndPosition.array[i3 + v + 2] = endPosition.z; | |
| } | |
| } | |
| var material = new THREE.BAS.BasicAnimationMaterial( | |
| { | |
| shading: THREE.FlatShading, | |
| side: THREE.DoubleSide, | |
| uniforms: { | |
| uTime: {type: 'f', value: 0} | |
| }, | |
| shaderFunctions: [ | |
| THREE.BAS.ShaderChunk['cubic_bezier'], | |
| //THREE.BAS.ShaderChunk[(animationPhase === 'in' ? 'ease_out_cubic' : 'ease_in_cubic')], | |
| THREE.BAS.ShaderChunk['ease_in_out_cubic'], | |
| THREE.BAS.ShaderChunk['quaternion_rotation'] | |
| ], | |
| shaderParameters: [ | |
| 'uniform float uTime;', | |
| 'attribute vec2 aAnimation;', | |
| 'attribute vec3 aStartPosition;', | |
| 'attribute vec3 aControl0;', | |
| 'attribute vec3 aControl1;', | |
| 'attribute vec3 aEndPosition;', | |
| ], | |
| shaderVertexInit: [ | |
| 'float tDelay = aAnimation.x;', | |
| 'float tDuration = aAnimation.y;', | |
| 'float tTime = clamp(uTime - tDelay, 0.0, tDuration);', | |
| 'float tProgress = ease(tTime, 0.0, 1.0, tDuration);' | |
| //'float tProgress = tTime / tDuration;' | |
| ], | |
| shaderTransformPosition: [ | |
| (animationPhase === 'in' ? 'transformed *= tProgress;' : 'transformed *= 1.0 - tProgress;'), | |
| 'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);' | |
| ] | |
| }, | |
| { | |
| map: new THREE.Texture(), | |
| } | |
| ); | |
| THREE.Mesh.call(this, geometry, material); | |
| this.frustumCulled = false; | |
| } | |
| Slide.prototype = Object.create(THREE.Mesh.prototype); | |
| Slide.prototype.constructor = Slide; | |
| Object.defineProperty(Slide.prototype, 'time', { | |
| get: function () { | |
| return this.material.uniforms['uTime'].value; | |
| }, | |
| set: function (v) { | |
| this.material.uniforms['uTime'].value = v; | |
| } | |
| }); | |
| Slide.prototype.setImage = function(image) { | |
| this.material.uniforms.map.value.image = image; | |
| this.material.uniforms.map.value.needsUpdate = true; | |
| }; | |
| Slide.prototype.transition = function() { | |
| return TweenMax.fromTo(this, 3.0, {time:0.0}, {time:this.totalDuration, ease:Power0.easeInOut}); | |
| }; | |
| function SlideGeometry(model) { | |
| THREE.BAS.ModelBufferGeometry.call(this, model); | |
| } | |
| SlideGeometry.prototype = Object.create(THREE.BAS.ModelBufferGeometry.prototype); | |
| SlideGeometry.prototype.constructor = SlideGeometry; | |
| SlideGeometry.prototype.bufferPositions = function () { | |
| var positionBuffer = this.createAttribute('position', 3).array; | |
| for (var i = 0; i < this.faceCount; i++) { | |
| var face = this.modelGeometry.faces[i]; | |
| var centroid = THREE.BAS.Utils.computeCentroid(this.modelGeometry, face); | |
| var a = this.modelGeometry.vertices[face.a]; | |
| var b = this.modelGeometry.vertices[face.b]; | |
| var c = this.modelGeometry.vertices[face.c]; | |
| positionBuffer[face.a * 3] = a.x - centroid.x; | |
| positionBuffer[face.a * 3 + 1] = a.y - centroid.y; | |
| positionBuffer[face.a * 3 + 2] = a.z - centroid.z; | |
| positionBuffer[face.b * 3] = b.x - centroid.x; | |
| positionBuffer[face.b * 3 + 1] = b.y - centroid.y; | |
| positionBuffer[face.b * 3 + 2] = b.z - centroid.z; | |
| positionBuffer[face.c * 3] = c.x - centroid.x; | |
| positionBuffer[face.c * 3 + 1] = c.y - centroid.y; | |
| positionBuffer[face.c * 3 + 2] = c.z - centroid.z; | |
| } | |
| }; | |
| function THREERoot(params) { | |
| params = utils.extend({ | |
| fov: 60, | |
| zNear: 10, | |
| zFar: 100000, | |
| createCameraControls: true | |
| }, params); | |
| this.renderer = new THREE.WebGLRenderer({ | |
| antialias: params.antialias, | |
| alpha: true | |
| }); | |
| this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1)); | |
| document.getElementById('three-container').appendChild(this.renderer.domElement); | |
| this.camera = new THREE.PerspectiveCamera( | |
| params.fov, | |
| window.innerWidth / window.innerHeight, | |
| params.zNear, | |
| params.zfar | |
| ); | |
| this.scene = new THREE.Scene(); | |
| if (params.createCameraControls) { | |
| this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); | |
| } | |
| this.resize = this.resize.bind(this); | |
| this.tick = this.tick.bind(this); | |
| this.resize(); | |
| this.tick(); | |
| window.addEventListener('resize', this.resize, false); | |
| } | |
| THREERoot.prototype = { | |
| tick: function () { | |
| this.update(); | |
| this.render(); | |
| requestAnimationFrame(this.tick); | |
| }, | |
| update: function () { | |
| this.controls && this.controls.update(); | |
| }, | |
| render: function () { | |
| this.renderer.render(this.scene, this.camera); | |
| }, | |
| resize: function () { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| }; | |
| //////////////////// | |
| // UTILS | |
| //////////////////// | |
| var utils = { | |
| extend: function (dst, src) { | |
| for (var key in src) { | |
| dst[key] = src[key]; | |
| } | |
| return dst; | |
| }, | |
| randSign: function () { | |
| return Math.random() > 0.5 ? 1 : -1; | |
| }, | |
| ease: function (ease, t, b, c, d) { | |
| return b + ease.getRatio(t / d) * c; | |
| }, | |
| fibSpherePoint: (function () { | |
| var vec = {x: 0, y: 0, z: 0}; | |
| var G = Math.PI * (3 - Math.sqrt(5)); | |
| return function (i, n, radius) { | |
| var step = 2.0 / n; | |
| var r, phi; | |
| vec.y = i * step - 1 + (step * 0.5); | |
| r = Math.sqrt(1 - vec.y * vec.y); | |
| phi = i * G; | |
| vec.x = Math.cos(phi) * r; | |
| vec.z = Math.sin(phi) * r; | |
| radius = radius || 1; | |
| vec.x *= radius; | |
| vec.y *= radius; | |
| vec.z *= radius; | |
| return vec; | |
| } | |
| })(), | |
| spherePoint: (function () { | |
| return function (u, v) { | |
| u === undefined && (u = Math.random()); | |
| v === undefined && (v = Math.random()); | |
| var theta = 2 * Math.PI * u; | |
| var phi = Math.acos(2 * v - 1); | |
| var vec = {}; | |
| vec.x = (Math.sin(phi) * Math.cos(theta)); | |
| vec.y = (Math.sin(phi) * Math.sin(theta)); | |
| vec.z = (Math.cos(phi)); | |
| return vec; | |
| } | |
| })() | |
| }; | |
| function createTweenScrubber(tween, seekSpeed) { | |
| seekSpeed = seekSpeed || 0.001; | |
| function stop() { | |
| TweenMax.to(tween, 1, {timeScale:0}); | |
| } | |
| function resume() { | |
| TweenMax.to(tween, 1, {timeScale:1}); | |
| } | |
| function seek(dx) { | |
| var progress = tween.progress(); | |
| var p = THREE.Math.clamp((progress + (dx * seekSpeed)), 0, 1); | |
| tween.progress(p); | |
| } | |
| var _cx = 0; | |
| // desktop | |
| var mouseDown = false; | |
| document.body.style.cursor = 'pointer'; | |
| window.addEventListener('mousedown', function(e) { | |
| mouseDown = true; | |
| document.body.style.cursor = 'ew-resize'; | |
| _cx = e.clientX; | |
| stop(); | |
| }); | |
| window.addEventListener('mouseup', function(e) { | |
| mouseDown = false; | |
| document.body.style.cursor = 'pointer'; | |
| resume(); | |
| }); | |
| window.addEventListener('mousemove', function(e) { | |
| if (mouseDown === true) { | |
| var cx = e.clientX; | |
| var dx = cx - _cx; | |
| _cx = cx; | |
| seek(dx); | |
| } | |
| }); | |
| // mobile | |
| window.addEventListener('touchstart', function(e) { | |
| _cx = e.touches[0].clientX; | |
| stop(); | |
| e.preventDefault(); | |
| }); | |
| window.addEventListener('touchend', function(e) { | |
| resume(); | |
| e.preventDefault(); | |
| }); | |
| window.addEventListener('touchmove', function(e) { | |
| var cx = e.touches[0].clientX; | |
| var dx = cx - _cx; | |
| _cx = cx; | |
| seek(dx); | |
| e.preventDefault(); | |
| }); | |
| } |
| <script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.min.js"></script> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/bas.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/OrbitControls-2.js"></script> |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| } | |
| canvas { | |
| background-image: radial-gradient(#666, #333); | |
| } | |
| #instructions { | |
| position: absolute; | |
| color: #fff; | |
| bottom: 0; | |
| padding-bottom: 6px; | |
| font-family: sans-serif; | |
| width: 100%; | |
| text-align: center; | |
| pointer-events: none; | |
| } |
Shader powered image transition
A Pen by Szenia Zadvornykh on CodePen.