When taking things out of constructor, OrbitControl stops working for some reason

Hi! I am new to using Angular and Three js so this may just be a silly mistake. When I am attempting to use Orbit Controls to move the camera, it works if I have everything inside the constructor as shown below:

constructor() {
//create the scene
const scene = new THREE.Scene();

//create the camera
let focalLength = 75;
let aspectRatio = window.innerWidth / window.innerHeight;
let near = 0.1;
let far = 1000;
const camera = new THREE.PerspectiveCamera(focalLength,aspectRatio,near,far);

//create and set the renderer
const renderer = new THREE.WebGLRenderer(); //maybe try GL1?
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

//creating the orbit controls
const controls = new OrbitControls(camera, renderer.domElement);

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

function animate() {
  requestAnimationFrame( animate );
  renderer.render( scene, camera );
}
animate();

}

However, the moment I take things out of the constructor e.g. the animate function, renderer, camera and scene, I am unable to rotate the camera and/or zoom using the Orbit Controls. I am not sure what exactly I am doing wrong. Any help would be appreciated! Here is where things go wrong:

renderer! : THREE.WebGLRenderer;
scene! : THREE.Scene;
camera! : THREE.Camera;

constructor() {

//create the scene
this.scene = new THREE.Scene();

//create the camera
let focalLength = 75;
let aspectRatio = window.innerWidth / window.innerHeight;
let near = 0.1;
let far = 1000;
this.camera = new THREE.PerspectiveCamera(focalLength,aspectRatio,near,far);

//create and set the renderer
this.renderer = new THREE.WebGLRenderer(); //maybe try GL1?
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);

//creating the orbit controls
const controls = new OrbitControls(this.camera, this.renderer.domElement);

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
this.scene.add( cube );

this.camera.position.z = 5;
this.animate();

}
animate() {
requestAnimationFrame( this.animate );
this.renderer.render( this.scene, this.camera );
}

Long story short - replace that line with:

requestAnimationFrame(() => this.animate());

And, if you’d be interested about why - by passing the function directly, you let requestAnimationFrame call it with the context within requestAnimationFrame - not within the class (ie. this in the called callback function will point to the “parent” of requestAnimationFrame which is window, instead of the class.)
If you replace that line with a callback that then calls the animate, the context of animate will keep the class as this (you could also solve this by using bind - but messing with context overall is a bit of a risky thing in JS.)

requestAnimationFrame(this.animate); // Within `animate` - `this` will point to context of `requestAnimationFrame`, which is `window` iirc. And, for example, `this.renderer` will equal `window.renderer`, which is undefined.

requestAnimationFrame(() => {
  this.animate(); // Since the wrapper function is created within the class context, `this.animate()` retains this context. So `this.renderer` will point to the Renderer.
});

// Or alternatively:
this.animate = this.animate.bind(this); // Call this for example in the class constructor
requestAnimationFrame(this.animate); // Since `animate` was manually bound, it will keep the correct context of the class
2 Likes

Ah that makes a lot of sense, thanks very much!