{"id":3581,"date":"2021-08-29T08:19:24","date_gmt":"2021-08-29T08:19:24","guid":{"rendered":"https:\/\/rengga.dev\/blog\/?p=3581"},"modified":"2023-02-24T13:08:14","modified_gmt":"2023-02-24T13:08:14","slug":"three-js-tutorial-shader-powered-image-transition-with-three-js","status":"publish","type":"post","link":"https:\/\/rengga.dev\/blog\/three-js-tutorial-shader-powered-image-transition-with-three-js\/","title":{"rendered":"Three JS Tutorial &#8211; Shader Powered Image Transition with three.js"},"content":{"rendered":"<p><span style=\"color: #ef3207;\"><a style=\"color: #ef3207;\" href=\"https:\/\/rengga.dev\/\" target=\"_blank\" rel=\"noopener\"><strong>Rengga Dev<\/strong><\/a> <\/span>&#8211; Shader powered image transition with\u00a0<strong>three.js<\/strong>. Click and drag to control the animation.<\/p>\n<p><iframe style=\"width: 100%;\" title=\"Untitled\" src=\"https:\/\/codepen.io\/renggagumilar\/embed\/XWYbQge?default-tab=result&amp;theme-id=dark\" height=\"500\" frameborder=\"no\" scrolling=\"no\" allowfullscreen=\"allowfullscreen\"><br \/>\nSee the Pen <a href=\"https:\/\/codepen.io\/renggagumilar\/pen\/XWYbQge\"><br \/>\nUntitled<\/a> by Rengga Gumilar (<a href=\"https:\/\/codepen.io\/renggagumilar\">@renggagumilar<\/a>)<br \/>\non <a href=\"https:\/\/codepen.io\">CodePen<\/a>.<br \/>\n<\/iframe><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">window.onload = init;\r\nconsole.ward = function() {}; \/\/ what warnings?\r\n\r\nfunction init() {\r\n  var root = new THREERoot({\r\n    createCameraControls: !true,\r\n    antialias: (window.devicePixelRatio === 1),\r\n    fov: 80\r\n  });\r\n\r\n  root.renderer.setClearColor(0x000000, 0);\r\n  root.renderer.setPixelRatio(window.devicePixelRatio || 1);\r\n  root.camera.position.set(0, 0, 60);\r\n\r\n  var width = 100;\r\n  var height = 60;\r\n\r\n  var slide = new Slide(width, height, 'out');\r\n    var l1 = new THREE.ImageLoader();\r\n    l1.setCrossOrigin('Anonymous');\r\n    l1.load('https:\/\/s3-us-west-2.amazonaws.com\/s.cdpn.io\/175711\/winter.jpg', function(img) {\r\n      slide.setImage(img);\r\n    })\r\n  root.scene.add(slide);\r\n\r\n  var slide2 = new Slide(width, height, 'in');\r\n  var l2 = new THREE.ImageLoader();\r\n    l2.setCrossOrigin('Anonymous');\r\n    l2.load('https:\/\/s3-us-west-2.amazonaws.com\/s.cdpn.io\/175711\/spring.jpg', function(img) {\r\n        slide2.setImage(img);\r\n    })\r\n    \r\n  root.scene.add(slide2);\r\n\r\n  var tl = new TimelineMax({repeat:-1, repeatDelay:1.0, yoyo: true});\r\n\r\n  tl.add(slide.transition(), 0);\r\n  tl.add(slide2.transition(), 0);\r\n\r\n  createTweenScrubber(tl);\r\n\r\n  window.addEventListener('keyup', function(e) {\r\n    if (e.keyCode === 80) {\r\n      tl.paused(!tl.paused());\r\n    }\r\n  });\r\n}\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ CLASSES\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\r\nfunction Slide(width, height, animationPhase) {\r\n  var plane = new THREE.PlaneGeometry(width, height, width * 2, height * 2);\r\n\r\n  THREE.BAS.Utils.separateFaces(plane);\r\n\r\n  var geometry = new SlideGeometry(plane);\r\n\r\n  geometry.bufferUVs();\r\n\r\n  var aAnimation = geometry.createAttribute('aAnimation', 2);\r\n  var aStartPosition = geometry.createAttribute('aStartPosition', 3);\r\n  var aControl0 = geometry.createAttribute('aControl0', 3);\r\n  var aControl1 = geometry.createAttribute('aControl1', 3);\r\n  var aEndPosition = geometry.createAttribute('aEndPosition', 3);\r\n\r\n  var i, i2, i3, i4, v;\r\n\r\n  var minDuration = 0.8;\r\n  var maxDuration = 1.2;\r\n  var maxDelayX = 0.9;\r\n  var maxDelayY = 0.125;\r\n  var stretch = 0.11;\r\n\r\n  this.totalDuration = maxDuration + maxDelayX + maxDelayY + stretch;\r\n\r\n  var startPosition = new THREE.Vector3();\r\n  var control0 = new THREE.Vector3();\r\n  var control1 = new THREE.Vector3();\r\n  var endPosition = new THREE.Vector3();\r\n\r\n  var tempPoint = new THREE.Vector3();\r\n\r\n  function getControlPoint0(centroid) {\r\n    var signY = Math.sign(centroid.y);\r\n\r\n    tempPoint.x = THREE.Math.randFloat(0.1, 0.3) * 50;\r\n    tempPoint.y = signY * THREE.Math.randFloat(0.1, 0.3) * 70;\r\n    tempPoint.z = THREE.Math.randFloatSpread(20);\r\n\r\n    return tempPoint;\r\n  }\r\n\r\n  function getControlPoint1(centroid) {\r\n    var signY = Math.sign(centroid.y);\r\n\r\n    tempPoint.x = THREE.Math.randFloat(0.3, 0.6) * 50;\r\n    tempPoint.y = -signY * THREE.Math.randFloat(0.3, 0.6) * 70;\r\n    tempPoint.z = THREE.Math.randFloatSpread(20);\r\n\r\n    return tempPoint;\r\n  }\r\n\r\n  for (i = 0, i2 = 0, i3 = 0, i4 = 0; i &lt; geometry.faceCount; i++, i2 += 6, i3 += 9, i4 += 12) {\r\n    var face = plane.faces[i];\r\n    var centroid = THREE.BAS.Utils.computeCentroid(plane, face);\r\n\r\n    \/\/ animation\r\n    var duration = THREE.Math.randFloat(minDuration, maxDuration);\r\n    var delayX = THREE.Math.mapLinear(centroid.x, -width * 0.5, width * 0.5, 0.0, maxDelayX);\r\n    var delayY;\r\n\r\n    if (animationPhase === 'in') {\r\n      delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, 0.0, maxDelayY)\r\n    }\r\n    else {\r\n      delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, maxDelayY, 0.0)\r\n    }\r\n\r\n    for (v = 0; v &lt; 6; v += 2) {\r\n      aAnimation.array[i2 + v]     = delayX + delayY + (Math.random() * stretch * duration);\r\n      aAnimation.array[i2 + v + 1] = duration;\r\n    }\r\n\r\n    \/\/ positions\r\n\r\n    endPosition.copy(centroid);\r\n    startPosition.copy(centroid);\r\n\r\n    if (animationPhase === 'in') {\r\n      control0.copy(centroid).sub(getControlPoint0(centroid));\r\n      control1.copy(centroid).sub(getControlPoint1(centroid));\r\n    }\r\n    else { \/\/ out\r\n      control0.copy(centroid).add(getControlPoint0(centroid));\r\n      control1.copy(centroid).add(getControlPoint1(centroid));\r\n    }\r\n\r\n    for (v = 0; v &lt; 9; v += 3) {\r\n      aStartPosition.array[i3 + v]     = startPosition.x;\r\n      aStartPosition.array[i3 + v + 1] = startPosition.y;\r\n      aStartPosition.array[i3 + v + 2] = startPosition.z;\r\n\r\n      aControl0.array[i3 + v]     = control0.x;\r\n      aControl0.array[i3 + v + 1] = control0.y;\r\n      aControl0.array[i3 + v + 2] = control0.z;\r\n\r\n      aControl1.array[i3 + v]     = control1.x;\r\n      aControl1.array[i3 + v + 1] = control1.y;\r\n      aControl1.array[i3 + v + 2] = control1.z;\r\n\r\n      aEndPosition.array[i3 + v]     = endPosition.x;\r\n      aEndPosition.array[i3 + v + 1] = endPosition.y;\r\n      aEndPosition.array[i3 + v + 2] = endPosition.z;\r\n    }\r\n  }\r\n\r\n  var material = new THREE.BAS.BasicAnimationMaterial(\r\n    {\r\n      shading: THREE.FlatShading,\r\n      side: THREE.DoubleSide,\r\n      uniforms: {\r\n        uTime: {type: 'f', value: 0}\r\n      },\r\n      shaderFunctions: [\r\n        THREE.BAS.ShaderChunk['cubic_bezier'],\r\n        \/\/THREE.BAS.ShaderChunk[(animationPhase === 'in' ? 'ease_out_cubic' : 'ease_in_cubic')],\r\n        THREE.BAS.ShaderChunk['ease_in_out_cubic'],\r\n        THREE.BAS.ShaderChunk['quaternion_rotation']\r\n      ],\r\n      shaderParameters: [\r\n        'uniform float uTime;',\r\n        'attribute vec2 aAnimation;',\r\n        'attribute vec3 aStartPosition;',\r\n        'attribute vec3 aControl0;',\r\n        'attribute vec3 aControl1;',\r\n        'attribute vec3 aEndPosition;',\r\n      ],\r\n      shaderVertexInit: [\r\n        'float tDelay = aAnimation.x;',\r\n        'float tDuration = aAnimation.y;',\r\n        'float tTime = clamp(uTime - tDelay, 0.0, tDuration);',\r\n        'float tProgress = ease(tTime, 0.0, 1.0, tDuration);'\r\n        \/\/'float tProgress = tTime \/ tDuration;'\r\n      ],\r\n      shaderTransformPosition: [\r\n        (animationPhase === 'in' ? 'transformed *= tProgress;' : 'transformed *= 1.0 - tProgress;'),\r\n        'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);'\r\n      ]\r\n    },\r\n    {\r\n      map: new THREE.Texture(),\r\n    }\r\n  );\r\n\r\n  THREE.Mesh.call(this, geometry, material);\r\n\r\n  this.frustumCulled = false;\r\n}\r\nSlide.prototype = Object.create(THREE.Mesh.prototype);\r\nSlide.prototype.constructor = Slide;\r\nObject.defineProperty(Slide.prototype, 'time', {\r\n  get: function () {\r\n    return this.material.uniforms['uTime'].value;\r\n  },\r\n  set: function (v) {\r\n    this.material.uniforms['uTime'].value = v;\r\n  }\r\n});\r\n\r\nSlide.prototype.setImage = function(image) {\r\n  this.material.uniforms.map.value.image = image;\r\n  this.material.uniforms.map.value.needsUpdate = true;\r\n};\r\n\r\nSlide.prototype.transition = function() {\r\n  return TweenMax.fromTo(this, 3.0, {time:0.0}, {time:this.totalDuration, ease:Power0.easeInOut});\r\n};\r\n\r\n\r\nfunction SlideGeometry(model) {\r\n  THREE.BAS.ModelBufferGeometry.call(this, model);\r\n}\r\nSlideGeometry.prototype = Object.create(THREE.BAS.ModelBufferGeometry.prototype);\r\nSlideGeometry.prototype.constructor = SlideGeometry;\r\nSlideGeometry.prototype.bufferPositions = function () {\r\n  var positionBuffer = this.createAttribute('position', 3).array;\r\n\r\n  for (var i = 0; i &lt; this.faceCount; i++) {\r\n    var face = this.modelGeometry.faces[i];\r\n    var centroid = THREE.BAS.Utils.computeCentroid(this.modelGeometry, face);\r\n\r\n    var a = this.modelGeometry.vertices[face.a];\r\n    var b = this.modelGeometry.vertices[face.b];\r\n    var c = this.modelGeometry.vertices[face.c];\r\n\r\n    positionBuffer[face.a * 3]     = a.x - centroid.x;\r\n    positionBuffer[face.a * 3 + 1] = a.y - centroid.y;\r\n    positionBuffer[face.a * 3 + 2] = a.z - centroid.z;\r\n\r\n    positionBuffer[face.b * 3]     = b.x - centroid.x;\r\n    positionBuffer[face.b * 3 + 1] = b.y - centroid.y;\r\n    positionBuffer[face.b * 3 + 2] = b.z - centroid.z;\r\n\r\n    positionBuffer[face.c * 3]     = c.x - centroid.x;\r\n    positionBuffer[face.c * 3 + 1] = c.y - centroid.y;\r\n    positionBuffer[face.c * 3 + 2] = c.z - centroid.z;\r\n  }\r\n};\r\n\r\n\r\nfunction THREERoot(params) {\r\n  params = utils.extend({\r\n    fov: 60,\r\n    zNear: 10,\r\n    zFar: 100000,\r\n\r\n    createCameraControls: true\r\n  }, params);\r\n\r\n  this.renderer = new THREE.WebGLRenderer({\r\n    antialias: params.antialias,\r\n    alpha: true\r\n  });\r\n  this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));\r\n  document.getElementById('three-container').appendChild(this.renderer.domElement);\r\n\r\n  this.camera = new THREE.PerspectiveCamera(\r\n    params.fov,\r\n    window.innerWidth \/ window.innerHeight,\r\n    params.zNear,\r\n    params.zfar\r\n  );\r\n\r\n  this.scene = new THREE.Scene();\r\n\r\n  if (params.createCameraControls) {\r\n    this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);\r\n  }\r\n\r\n  this.resize = this.resize.bind(this);\r\n  this.tick = this.tick.bind(this);\r\n\r\n  this.resize();\r\n  this.tick();\r\n\r\n  window.addEventListener('resize', this.resize, false);\r\n}\r\nTHREERoot.prototype = {\r\n  tick: function () {\r\n    this.update();\r\n    this.render();\r\n    requestAnimationFrame(this.tick);\r\n  },\r\n  update: function () {\r\n    this.controls &amp;&amp; this.controls.update();\r\n  },\r\n  render: function () {\r\n    this.renderer.render(this.scene, this.camera);\r\n  },\r\n  resize: function () {\r\n    this.camera.aspect = window.innerWidth \/ window.innerHeight;\r\n    this.camera.updateProjectionMatrix();\r\n\r\n    this.renderer.setSize(window.innerWidth, window.innerHeight);\r\n  }\r\n};\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ UTILS\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\r\nvar utils = {\r\n  extend: function (dst, src) {\r\n    for (var key in src) {\r\n      dst[key] = src[key];\r\n    }\r\n\r\n    return dst;\r\n  },\r\n  randSign: function () {\r\n    return Math.random() &gt; 0.5 ? 1 : -1;\r\n  },\r\n  ease: function (ease, t, b, c, d) {\r\n    return b + ease.getRatio(t \/ d) * c;\r\n  },\r\n  fibSpherePoint: (function () {\r\n    var vec = {x: 0, y: 0, z: 0};\r\n    var G = Math.PI * (3 - Math.sqrt(5));\r\n\r\n    return function (i, n, radius) {\r\n      var step = 2.0 \/ n;\r\n      var r, phi;\r\n\r\n      vec.y = i * step - 1 + (step * 0.5);\r\n      r = Math.sqrt(1 - vec.y * vec.y);\r\n      phi = i * G;\r\n      vec.x = Math.cos(phi) * r;\r\n      vec.z = Math.sin(phi) * r;\r\n\r\n      radius = radius || 1;\r\n\r\n      vec.x *= radius;\r\n      vec.y *= radius;\r\n      vec.z *= radius;\r\n\r\n      return vec;\r\n    }\r\n  })(),\r\n  spherePoint: (function () {\r\n    return function (u, v) {\r\n      u === undefined &amp;&amp; (u = Math.random());\r\n      v === undefined &amp;&amp; (v = Math.random());\r\n\r\n      var theta = 2 * Math.PI * u;\r\n      var phi = Math.acos(2 * v - 1);\r\n\r\n      var vec = {};\r\n      vec.x = (Math.sin(phi) * Math.cos(theta));\r\n      vec.y = (Math.sin(phi) * Math.sin(theta));\r\n      vec.z = (Math.cos(phi));\r\n\r\n      return vec;\r\n    }\r\n  })()\r\n};\r\n\r\nfunction createTweenScrubber(tween, seekSpeed) {\r\n  seekSpeed = seekSpeed || 0.001;\r\n\r\n  function stop() {\r\n    TweenMax.to(tween, 1, {timeScale:0});\r\n  }\r\n\r\n  function resume() {\r\n    TweenMax.to(tween, 1, {timeScale:1});\r\n  }\r\n\r\n  function seek(dx) {\r\n    var progress = tween.progress();\r\n    var p = THREE.Math.clamp((progress + (dx * seekSpeed)), 0, 1);\r\n\r\n    tween.progress(p);\r\n  }\r\n\r\n  var _cx = 0;\r\n\r\n  \/\/ desktop\r\n  var mouseDown = false;\r\n  document.body.style.cursor = 'pointer';\r\n\r\n  window.addEventListener('mousedown', function(e) {\r\n    mouseDown = true;\r\n    document.body.style.cursor = 'ew-resize';\r\n    _cx = e.clientX;\r\n    stop();\r\n  });\r\n  window.addEventListener('mouseup', function(e) {\r\n    mouseDown = false;\r\n    document.body.style.cursor = 'pointer';\r\n    resume();\r\n  });\r\n  window.addEventListener('mousemove', function(e) {\r\n    if (mouseDown === true) {\r\n      var cx = e.clientX;\r\n      var dx = cx - _cx;\r\n      _cx = cx;\r\n\r\n      seek(dx);\r\n    }\r\n  });\r\n  \/\/ mobile\r\n  window.addEventListener('touchstart', function(e) {\r\n    _cx = e.touches[0].clientX;\r\n    stop();\r\n    e.preventDefault();\r\n  });\r\n  window.addEventListener('touchend', function(e) {\r\n    resume();\r\n    e.preventDefault();\r\n  });\r\n  window.addEventListener('touchmove', function(e) {\r\n    var cx = e.touches[0].clientX;\r\n    var dx = cx - _cx;\r\n    _cx = cx;\r\n\r\n    seek(dx);\r\n    e.preventDefault();\r\n  });\r\n}\r\n<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Rengga Dev &#8211; Shader powered image transition with\u00a0three.js. Click and drag to <a class=\"read-more\" href=\"https:\/\/rengga.dev\/blog\/three-js-tutorial-shader-powered-image-transition-with-three-js\/\" title=\"Three JS Tutorial &#8211; Shader Powered Image Transition with three.js\" itemprop=\"url\"><\/a><\/p>\n","protected":false},"author":1,"featured_media":3587,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[20,12],"tags":[382,264,263,384,352,103,227],"newstopic":[],"class_list":{"0":"post-3581","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-javascript","8":"category-web-development","9":"tag-bas-js","10":"tag-javascript-tutorial","11":"tag-js-tutorial","12":"tag-three-js","13":"tag-tweenmax-js","14":"tag-web-design","15":"tag-web-designer"},"_links":{"self":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3581","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/comments?post=3581"}],"version-history":[{"count":1,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3581\/revisions"}],"predecessor-version":[{"id":3582,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3581\/revisions\/3582"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/media\/3587"}],"wp:attachment":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/media?parent=3581"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/categories?post=3581"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/tags?post=3581"},{"taxonomy":"newstopic","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/newstopic?post=3581"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}