Animating Camera Movement in Three.js
Tags: javascript, threejs, tutorial, animation
Published: 2020-12-16 10:37:17

---
title: Animating Camera Movement in Three.js
published: true
description: I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts
tags: javascript, threejs, tutorial, animation
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/w4th8jv58ouml11vvzhz.png
series: Nuffshell Coding Diary
---

*I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts. Follow me if you're interested in what I've learned along the way about building web apps with React, Tailwind CSS, Firebase, Apollo/GraphQL, three.js and TypeScript.*

# Today's Goal

Let's figure out how we can make a 3D model we build with [three.js](https://threejs.org) interactive – when we click on it, the camera moves to put the clicked object to the center of the screen, like this:

{% youtube iFm-g1DKSFs %}

I currently need this for a project I'm working on, *Nuffshell*, a social network and collaboration tool based on mind maps. 

For this part of my series, however, I'm not going to work on my Nuffshell code, but build something from scratch, to make it easier for you to follow along.

If you *do* want to follow along, I recommend you use [CodeSandbox](https://codesandbox.io/) and create a new project using the “Vanilla” template:

![CodeSandbox Vanilla template](https://dev-to-uploads.s3.amazonaws.com/i/cuqk56k8h9sg4pqv86hd.jpeg)

This already has everything set up for you to work with JavaScript modules.

# Step 1: Creating the 3D Scene

As a first step, I'm going to set up my basic 3D scene that shows four colored cubes, for now, without interactivity or animation.

## Functions, Functions, Everywhere

I find it good practice to factor every part of my program into functions that do one thing exactly. A function should not be longer than a screen length. 

With this in mind, here are the JavaScript modules I create for setting up my 3D scene. Each module contains one function.

*[createRenderer.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/createRenderer.js)*

```javascript
import * as THREE from "three";

export default function createRenderer() {
  const app = document.getElementById("app");
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  app.appendChild(renderer.domElement);
  return renderer;
}
```

My *createRenderer* sets up the 3D [renderer](https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer) and attaches it to the HTML page (through a DOM element called *app*).

I'm setting the size so that the 3D scene fills out the whole browser viewport.

*[createScene.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/createScene.js)*

```javascript
import * as THREE from "three";

export default function createScene() {
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0xffffff);
  return scene;
}
```

In this function, I'm setting up a [scene](https://threejs.org/docs/index.html#api/en/scenes/Scene). Scenes in *three.js* are the top-level containers for all the 3D objects to be rendered.

*[createCamera.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/createCamera.js)*

```javascript
import * as THREE from "three";

export default function createCamera() {
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  camera.position.z = 5;
  return camera;
}
```

Making 3D models in *three.js* is similar to making real-life movies: you need cameras and light sources to “film” it.

This function sets up the [camera](https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera).

Notice how I'm setting the Z-coordinate of the camera to 5. This means that the camera is some distance away from the object it is going to “film”.

![Camera Z coordinate](https://dev-to-uploads.s3.amazonaws.com/i/y4p0hua66fi7skq0yr70.png)

*[createCube.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/createCube.js)*

```javascript
import * as THREE from "three";

export default function createCube({ color, x, y }) {
  const geometry = new THREE.BoxGeometry();
  const material = new THREE.MeshLambertMaterial({ color });
  const cube = new THREE.Mesh(geometry, material);
  cube.position.set(x, y, 0);

  return cube;
}
```

Next up is the function for the objects to be shown in my 3D scene. It creates simple 3D cubes. I can provide a color and X/Y coordinates. The Z coordinate is set to 0 for all cubes.

The 3D objects in *three.js* are made up of geometries and materials.

*[createLight.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/createLight.js)*

```javascript
import * as THREE from "three";

export default function createLight() {
  const light = new THREE.PointLight(0xffffff, 1, 1000);
  light.position.set(0, 0, 10);
  return light;
}
```

Like in real life, we need a light source to be able to see anything in our 3D scene, so this function is for creating that.

*[animate.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/animate.js)*

```javascript
export default function animate(callback) {
  function loop(time) {
    callback(time);
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);
}
```

Since we want to have an animated 3D model, we need to render our model over and over again, 60 times per second, in a never-ending loop. My *animate* function accepts a *callback* argument, which is a function that gets executed as often as the browser can handle. 

I use the browser's [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) function for this, which the standard way to do this in pretty much all browser games.

## Putting It All Together

So now that we have functions to initialize renderer and scene, create a camera, a light source and some cubes and animate them, we can finally use these to create our 3D scene.

*[index.js](https://codesandbox.io/s/step-1-basic-setup-dupth?file=/src/index.js)*

```javascript
import "./styles.css";
import createCube from "./createCube";
import createLight from "./createLight";
import animate from "./animate";
import createCamera from "./createCamera";
import createRenderer from "./createRenderer";
import createScene from "./createScene";

const renderer = createRenderer();
const scene = createScene();
const camera = createCamera();

const cubes = {
  pink: createCube({ color: 0xff00ce, x: -1, y: -1 }),
  purple: createCube({ color: 0x9300fb, x: 1, y: -1 }),
  blue: createCube({ color: 0x0065d9, x: 1, y: 1 }),
  cyan: createCube({ color: 0x00d7d0, x: -1, y: 1 })
};

const light = createLight();

for (const object of Object.values(cubes)) {
  scene.add(object);
}

scene.add(light);

animate(() => {
  renderer.render(scene, camera);
});
```

Note how the camera, light and comes are all added to the scene. 

Our *animate* function makes sure the scene gets rendered over and over again. For now, pretty useless, but it will become important in the next steps.

Here's the project so far:

{% codesandbox dupth %}

# Step 2: Adding a Click Handler

Me need to be able to click on 3D objects to tell the camera where to move to. To achieve this, I'm adding a dependency to the npm package [three.interactive](https://github.com/markuslerner/THREE.Interactive#readme) to my project. 

This library allows us to add event listener to our 3D objects, just like with HTML DOM nodes.

At the beginning of my [index.js](https://codesandbox.io/s/step-2-adding-a-click-handler-wg5ol?file=/src/index.js), I'm adding an import statement to use *three.interactive*:

```javascript
import { InteractionManager } from "three.interactive";
```

In addition to the renderer, scene and camera, I'm creating an interaction manager:

```javascript
const interactionManager = new InteractionManager(
  renderer,
  camera,
  renderer.domElement
);
```

As you can see, the interaction manager needs to be able to control the renderer, camera and the canvas DOM element that the scene is rendered to.

I change the *for* loop that creates the cube objects and adds them to the scene to write a log statement to the console when a cube is clicked, to see if it works:

```javascript
for (const [name, object] of Object.entries(cubes)) {
  object.addEventListener("click", (event) => {
    event.stopPropagation();
    console.log(`${name} cube was clicked`);
  });
  interactionManager.add(object);
  scene.add(object);
}
```

Notice *event.stopPropagation* – this is necessary so that if objects are overlapping, only the top object handles the click. Again, this works just like [click handlers on DOM nodes](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation).

The one thing that we still need to do is edit the *animate* loop so the the interaction manager updates with every iteration:

```javascript
animate(() => {
  renderer.render(scene, camera);
  interactionManager.update();
});
```

Here's the project so far:

{% codesandbox wg5ol %}

When you open up the console on this sandbox (click on “console” in the lower left), then click on the 3D cubes, you'll see the log statements issued by the click handler I've added.

# Step 3: Moving the Camera

Now let's actually move the camera to the position of the cube that was clicked.

This is actually pretty straightforward – I just have to update the camera's position to match the X/Y coordinates of the cube that was clicked.

Here's the updated *for* loop that creates the cubes:

```javascript
for (const [name, object] of Object.entries(cubes)) {
  object.addEventListener("click", (event) => {
    event.stopPropagation();
    console.log(`${name} cube was clicked`);
    const cube = event.target;
    camera.position.set(cube.position.x, cube.position.y, camera.position.z);
  });
  interactionManager.add(object);
  scene.add(object);
}
```

Note that while the camera's X and Y coordinates change, the Z coordinate remains the same – the camera will still have 5 units distance from the objects it is “filming”.

Here's the updated sandbox:

{% codesandbox 9buf3 %}

Click on the cubes to try it out!

# Step 4: Tweening Animation

So far, when a cube is clicked, the camera jumps to the cube's position immediately. This is a step in the right direction, but actually want the camera to move over to the cube's position in a smooth motion (technically, this is called “panning”).

In short, we want to add some proper animation magic!

# What is Tweening?

For creating smooth motions in animations, we use a technique called [inbetweening](https://en.wikipedia.org/wiki/Inbetweening), or “tweeting”, in short. 

This technique is as old as animating itself, it was used by the artists making [Snow White](https://en.wikipedia.org/wiki/Snow_White_and_the_Seven_Dwarfs_(1937_film)) in the 1930s, just as it is used by the artists making animations today.

The basic idea is that you have a start and end state or something to animate (also called “keyframes”), then draw all of the states in between to create the illusion of gradual change.

Consider this bouncing ball animation:

![Bouncing ball](https://dev-to-uploads.s3.amazonaws.com/i/6uryqt0ppft0km0yj6up.png)

Here, we have 3 keyframes:

1. Ball is on the upper left of the screen
1. Ball is in the bottom in the middle
1. Ball is on the upper right

By adding the tweens, it will look like the ball is bouncing on the floor in a smooth notion. Without them, the ball would just kind of jaggedly jump from one place to another.

# JavaScript Library Tween.js

To make our camera move smoothly, we need to use tweening. Like interactivity, *three.js* does not provide this out of the box. We're going to have to add another npm package dependency to our project: [@tweenjs/tween.js](https://www.npmjs.com/package/@tweenjs/tween.js).

This library is not specifically for use with *three.js*. You can use it any time something should be changed over a period of time.

Let's add an import statement to [index.js](https://codesandbox.io/s/step-4-tweened-rxlu6?file=/src/index.js) to use it:

```javascript
import * as TWEEN from "@tweenjs/tween.js";
```

I create a tween in the *for* loop that creates my cubes, to the click handler that is fired when one of the cubes ist clicked:

```javascript
for (const [name, object] of Object.entries(cubes)) {
  object.addEventListener("click", (event) => {
    event.stopPropagation();
    console.log(`${name} cube was clicked`);
    const cube = event.target;
    const coords = { x: camera.position.x, y: camera.position.y };
    new TWEEN.Tween(coords)
      .to({ x: cube.position.x, y: cube.position.y })
      .onUpdate(() =>
        camera.position.set(coords.x, coords.y, camera.position.z)
      )
      .start();
  });
  interactionManager.add(object);
  scene.add(object);
}
```

To add the tween, I just have to instantiate a *Tween* object. The argument I'm passing to the constructor is the data I want to have “tweened”. In my case, this is an object containing X and Y coordinates. At the beginning of the tween, these X/Y coords are the original camera position.

With the *to* method, I tell the tween library what the end state of the tweened data should be. This will be the position of the cube that was clicked.

With the *onUpdate* method, I determine how the data that is being tweened can be used to affect my animation. It is called for every tweening step. I use this to update the position of the camera.

Finally, the *start* method tells the library to start tweening right away.

One more thing – we now have to add a call to the update method of our tweening library to our animation loop:

```javascript
animate((time) => {
  renderer.render(scene, camera);
  interactionManager.update();
  TWEEN.update(time);
});
```

# The End Result

Here's the final version of our project:

{% codesandbox rxlu6 %}

When you click on a cube, the camera pans smoothly over to its position – nice!

# To Be Continued…

This tutorial is part of my project diary. I'm building social media network and collaboration tool, based on mind maps. I will continue to blog about my progress in follow-up articles. Stay tuned!