Creating a 3D Slideshow with Three.js
By: Prince Anuragi
Tags: 3d, Element, Threejs, UI, Slideshow

In recent years browsers have become very smart from old browsers such as Netscape, now you can do almost anything on the browser itself, starting from 3D designing to doing analytics tasks and also live trading. With technologies such as web3, webgl and wasm (Web Assembly) browsers are bound to become smarter and much more efficient too. Having such powerful tools at our disposal make it simple to create super cool UIs with brand new technologies like we will be using three.js in this tutorial.

Photo by Annie Spratt on Unsplash

In this tutorial we will create a simple 3D Photo Gallery with zoom and slideshow functionalities. All you require to get started with a tutorial is just a code editor (I use VSCode because of it’s modular nature) and that’s all.

To Get started just create a new folder and inside it create one file named `index.html`

This is how our starting point looks like.

Now we will add cdn’s to external libraries like three.js which we will be using later. So let’s go ahead and add those libraries inside the body tag.

<script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" integrity=”sha512-dLxUelApnYxpLt6K2iomGngnHO83iUvZytA3YjDUCjT0HDOHKXnVYdf3hU4JjM8uEhxf9nD1/ey98U3t2vZ0qQ==” crossorigin=”anonymous” referrerpolicy=”no-referrer”></script> <script src=”https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js" integrity=”sha512-lIKG1kC5TMb1Zx32vdz1n31YJMZRknVY20U9MJ28hD3y2c0OKN0Ce5NhJji78v8zX5UOSsm+MTBOcJt7yMBnSg==” crossorigin=”anonymous” referrerpolicy=”no-referrer”></script>

Now we will create a renderer to render our 3D Scene, a Scene to be in which our photos will be and a camera which we will zoom in and zoom out based on the active image, Let us also create a size object to keep track of our browser tab width and height. A Simple Texture loader instance to load our images as three.js requires a Texture instance to be loaded before rendering it in the frame.

Go Ahead and Put Below Code in a new Script tag below previously loaded scripts.

<script> // Creating Necessary Instances and Initializations /** * Sizes */ const sizes = { width: window.innerWidth, height: window.innerHeight, }; const root = document.getElementById(“app”); var renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(sizes.width, sizes.height); root.appendChild(renderer.domElement); var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera( 75, sizes.width / sizes.height, 0.1, 1000 ); camera.position.z = 10; // const interactionManager = new InteractionManager( // renderer, // camera, // renderer.domElement // ); const loader = new THREE.TextureLoader(); let activeImage = 0; </script>

Now before we proceed, let us create a function which will load and position our images in the scene. In this Slideshow we will be using Plane Geometry as we need only 2 Dimensions to load our images.

function createPlaneWTexture(h, w, texture, x, y, z){ const geometry = new THREE.PlaneGeometry( h, w ); const material = new THREE.MeshBasicMaterial( {map: texture} ); const mesh = new THREE.Mesh( geometry, material ); mesh.position.x = x; mesh.position.y = y; mesh.position.z = z; return mesh; }

After function definition let us create an object named `images` to keep all images which we will be adding to our scene, and Add those to our Plane Textures to our scene using `images` object.

const images = { p1: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1628191081071-a2b761bf21d9"), 0, 0, 0), p2: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1606787620819-8bdf0c44c293"), -1, -5, 0), p3: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1518811829466-1372392d4544"), 2, 2, 0), p4: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1578763918454-d0deb5469071"), -5, 5, 0), } Object.keys(images).forEach(key => { scene.add(images[key]) })

The images are sample images from unsplash, if you want you can take any images or local images too. Now before we get into animating a camera to zoom in and out let us go ahead and create an animation loop to generate our scene in the browser.

function animate(callback) { function loop(time) { callback(time); requestAnimationFrame(loop); } requestAnimationFrame(loop); } animate((time) => { renderer.render(scene, camera); });

Now we will have something like this on our screen. Good, our images show up.

Now let us create some helper function which will help us later when we will create our slideshow. One function to scale our images bigger when we go near an active image, one function to send the camera to some coordinates in the scene and finally one more to reset images to original scale except the active one.

function TweenScaleTo(objMesh, w, h) { const scale = { w: objMesh.geometry.parameters.width, h: objMesh.geometry.parameters.height, }; new TWEEN.Tween(scale) .to({ w: w, h: h }) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { objMesh.geometry = new THREE.PlaneGeometry(scale.w, scale.h); }) .start(); } function cameraTo(x, y, z) { const coords = { x: camera.position.x, y: camera.position.y, z: camera.position.z, }; new TWEEN.Tween(coords) .to({ x: x, y: y, z: z + 2 }) .easing(TWEEN.Easing.Quadratic.Out) .onUpdate(() => camera.position.set(coords.x, coords.y, coords.z)) .start(); } function resetImages(images, except) { Object.keys(images).forEach((key) => { if (key !== except) { TweenScaleTo(images[key], 1.6, 0.9); } }); }

Now we have utility functions which simply help us to scale our images from scale (1.6, 0.9) to scale (16, 9) to and fro. And also one to zoom in our camera now let us create an actual timer which will focus and change the active image each 5 seconds. Let us call a variable to keep track of seconds from current.

let seconds = 0;

Also a function which will tell our timer when triggered to zoom into which image based on active image using a simple switch statement.

function makeZoomAccordingToActive(active) { switch (active) { case 0: resetImages(images, “p1”); TweenScaleTo(images.p1, 16, 9); cameraTo(images.p1.position.x, images.p1.position.y, images.p1.position.z + 8); break; case 1: resetImages(images, “p2”); TweenScaleTo(images.p2, 16, 9); cameraTo(images.p2.position.x, images.p2.position.y, images.p2.position.z + 8); break; case 2: resetImages(images, “p3”); TweenScaleTo(images.p3, 16, 9); cameraTo(images.p3.position.x, images.p3.position.y, images.p3.position.z + 8); break; case 3: resetImages(images, “p4”); TweenScaleTo(images.p4, 16, 9); cameraTo(images.p4.position.x, images.p4.position.y, images.p4.position.z + 8); break; default: resetImages(images, “p”) cameraTo(0, 0, 10); } }

In default case , meaning when the active index is out of bounds it will reset all images and send our camera back to its original position.

Now we are at final step where we will create a function which will check for when 5 seconds happened and based on activeImage display our slides. And let us make use of setInterval to ping our checkSeconds function each seconds. We could have used 5 seconds in setInterval itself but having an extra variable gives us more granular control over what we can do for out slideshow timing and add more stuff.

function checkSeconds() { seconds += 1; if (seconds > 5) { seconds = 0; if (activeImage + 1 > 4) { activeImage = 0; } else { activeImage += 1; } makeZoomAccordingToActive(activeImage) } } setInterval(checkSeconds, 1000);

So after saving and running your script again you will have a working cool slideshow with 3D Effects. It’s really easy to get started with three.js and 3D Animation with it. I hope you will find this article useful and use this in your projects too.

Here’s the final full version of the code.

<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8"> <meta http-equiv=”X-UA-Compatible” content=”IE=edge”> <meta name=”viewport” content=”width=device-width, initial-scale=1.0"> <title>Three JS Gallery</title> </head> <body style=”background-color: black;”> <div id=”app”></div> <script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" integrity=”sha512-dLxUelApnYxpLt6K2iomGngnHO83iUvZytA3YjDUCjT0HDOHKXnVYdf3hU4JjM8uEhxf9nD1/ey98U3t2vZ0qQ==”crossorigin=”anonymous” referrerpolicy=”no-referrer”></script> <script src=”https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js" integrity=”sha512-lIKG1kC5TMb1Zx32vdz1n31YJMZRknVY20U9MJ28hD3y2c0OKN0Ce5NhJji78v8zX5UOSsm+MTBOcJt7yMBnSg==”crossorigin=”anonymous” referrerpolicy=”no-referrer”></script> <script> // Creating Necessary Instances and Initializations /** * Sizes */ const sizes = { width: window.innerWidth, height: window.innerHeight, }; const root = document.getElementById(“app”); var renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(sizes.width, sizes.height); root.appendChild(renderer.domElement); var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera( 75, sizes.width / sizes.height, 0.1, 1000 ); camera.position.z = 10; const loader = new THREE.TextureLoader(); let activeImage = 0; function createPlaneWTexture(h, w, texture, x, y, z){ const geometry = new THREE.PlaneGeometry( h, w ); const material = new THREE.MeshBasicMaterial( {map: texture} ); const mesh = new THREE.Mesh( geometry, material ); mesh.position.x = x; mesh.position.y = y; mesh.position.z = z; return mesh; } const images = { p1: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1628191081071-a2b761bf21d9"), 0, 0, 0), p2: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1606787620819-8bdf0c44c293"), -1, -5, 0), p3: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1518811829466-1372392d4544"), 2, 2, 0), p4: createPlaneWTexture(1.6, 0.9, loader.load(“https://images.unsplash.com/photo-1578763918454-d0deb5469071"), -5, 5, 0), } Object.keys(images).forEach(key => { scene.add(images[key]) }) function TweenScaleTo(objMesh, w, h) { const scale = { w: objMesh.geometry.parameters.width, h: objMesh.geometry.parameters.height, }; new TWEEN.Tween(scale) .to({ w: w, h: h }) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { objMesh.geometry = new THREE.PlaneGeometry(scale.w, scale.h); }) .start(); } function cameraTo(x, y, z) { const coords = { x: camera.position.x, y: camera.position.y, z: camera.position.z, }; new TWEEN.Tween(coords) .to({ x: x, y: y, z: z + 2 }) .easing(TWEEN.Easing.Quadratic.Out) .onUpdate(() => camera.position.set(coords.x, coords.y, coords.z)) .start(); } function resetImages(images, except) { Object.keys(images).forEach((key) => { if (key !== except) { TweenScaleTo(images[key], 1.6, 0.9); } }); } function animate(callback) { function loop(time) { callback(time); requestAnimationFrame(loop); } requestAnimationFrame(loop); } animate((time) => { renderer.render(scene, camera); TWEEN.update(time); }); // Helper to auto zoom and Focus Images let seconds = 0; function makeZoomAccordingToActive(active) { switch (active) { case 0: resetImages(images, “p1”); TweenScaleTo(images.p1, 16, 9); cameraTo(images.p1.position.x, images.p1.position.y, images.p1.position.z + 8); break; case 1: resetImages(images, “p2”); TweenScaleTo(images.p2, 16, 9); cameraTo(images.p2.position.x, images.p2.position.y, images.p2.position.z + 8); break; case 2: resetImages(images, “p3”); TweenScaleTo(images.p3, 16, 9); cameraTo(images.p3.position.x, images.p3.position.y, images.p3.position.z + 8); break; case 3: resetImages(images, “p4”); TweenScaleTo(images.p4, 16, 9); cameraTo(images.p4.position.x, images.p4.position.y, images.p4.position.z + 8); break; default: resetImages(images, “p”) cameraTo(0, 0, 10); } } function checkSeconds() { seconds += 1; if (seconds > 5) { seconds = 0; if (activeImage + 1 > 4) { activeImage = 0; } else { activeImage += 1; } makeZoomAccordingToActive(activeImage) } } setInterval(checkSeconds, 1000); </script> </body> </html>

Thank you and I hope you have a good day.