Generating Three.js canvases dynamically based on requirements (implemented with classes)

In my scenario, I have an asset inventory containing multiple assets. I am looking to implement a feature where whenever a user hovers over the assets, it triggers rendering with an OrbitController (Trackball is preferred but not feasible due to a bug). The challenge lies in the fact that the "this" variable of the class instance cannot be passed into the render method of the class:

import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/build/three.module.js';
import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/examples/jsm/controls/OrbitControls.js';

class Main {

  constructor(canvasId)
  {
    this.canvas = document.querySelector(canvasId);

    this.renderer = new THREE.WebGLRenderer({canvas: this.canvas});

    this.fov = 75;
    this.aspect = 2;  // the canvas default
    this.near = 0.1;
    this.far = 5;
    this.camera = new THREE.PerspectiveCamera(this.fov, this.aspect, this.near, this.far);
    this.camera.position.z = 2;

    this.controls = new OrbitControls(this.camera, this.canvas);
    this.controls.target.set(0, 0, 0);
    this.controls.update();

    this.scene = new THREE.Scene();

    this.color = 0xFFFFFF;
    this.intensity = 1;
    this.light = new THREE.DirectionalLight(this.color, this.intensity);
    this.light.position.set(-1, 2, 4);
    this.scene.add(this.light);

    this.boxWidth = 1;
    this.boxHeight = 1;
    this.boxDepth = 1;
    this.geometry = new THREE.BoxGeometry(this.boxWidth, this.boxHeight, this.boxDepth);
  }

  makeInstance(geometry, color, x){

    let material = new THREE.MeshPhongMaterial(color);

    let cube = new THREE.Mesh(geometry, material);
    this.scene.add(cube);
    cube.position.x = x;

    return cube;
  }

   
  resizeRendererToDisplaySize(renderer) {
    this.canvas = this.renderer.domElement;
    let width = this.canvas.clientWidth;
    let height = this.canvas.clientHeight;
    let needResize = this.canvas.width !== width || this.canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }

    return needResize;
  }

  render() {
    
    // this -> control object or window whereas it should be the class instance   
    if (this.resizeRendererToDisplaySize(this.renderer)) {
      this.canvas = this.renderer.domElement;
      this.camera.aspect = this.canvas.clientWidth / this.canvas.clientHeight;
      this.camera.updateProjectionMatrix();
    }
    
    this.renderer.render(this.scene, this.camera);
    
  }

  starter() {

    this.makeInstance(this.geometry, 0x44aa88,  0);
    this.makeInstance(this.geometry, 0x8844aa, -2);
    this.makeInstance(this.geometry, 0xaa8844,  2);

    this.render();

    this.controls.addEventListener('change', this.render);

    window.addEventListener('resize', this.render);

    // note: this is a workaround for an OrbitControls issue
    // in an iframe. Will remove once the issue is fixed in
    // three.js

    window.addEventListener('mousedown', (e) => {
      e.preventDefault();
      window.focus();
    });

    window.addEventListener('keydown', (e) => {
      e.preventDefault();
    });
  }
}

let main = new Main('#a');
main.starter();

To address the issue, I had to replace "this" with the "main" instance in the render method. However, this approach compromises the class structure and restricts the generation of other instances like main2. For a live demo, please refer to the following CodePen link: https://codepen.io/jimver04/pen/XWjLPLo

Answer №1

You are passing this.render to the event handlers. this.render actually points to the prototype function (Main.prototype.render).

When the handler is triggered, the context will depend on how the handler is executed. In most scenarios, the context for the handler will be that of the origin (or possibly null). For instance,

window.addEventListener( 'resize', handler );
will trigger resize events by calling something similar to handler.call( window, event );

There are a few ways to work around this issue.

Arrow Functions and their implicit this

Arrow functions use the parent context's this. If an arrow function is defined within a class's member function, and that member function is executed within an instance of the class, then this inside the arrow function will refer to the instance.

class Test{

  constructor(){

    this.name = "Test"

    // Arrow function defined in the context of an instance:
    window.addEventListener( 'resize', () => {
      console.log( this.name ) // prints: "Test"
    } )

    // Normal function will have "window" as its "this"
    window.addEventListener( 'resize', function() {
      console.log( this.name ) // results may vary depending on the browser, but "this" is not an instance of Test
    } )

  }

}

Binding a function to a specific context

Using bind can assign a function to a specified context. The bind function creates a new function permanently bound to the given context.

class Test{

  constructor(){

    this.name = "Test"

    this.boundRender = this.render.bind( this ) // returns "render" permanently bound to "this"

    window.addEventListener( 'resize', this.boundRender )

  }

  render(){
    console.log( this.name )
  }

}

In the example above, "Test" will still be displayed in the console. Although the resize event intends to run the handler with the window context, the bind function has overridden that and ensured the handler executes within the context of the bound instance.

Answer №2

Appreciate the help, TheJim01! Below is the finalized solution utilizing the bind technique, which is effective (unlike the arrow solution). You can also find it analytically at https://codepen.io/jimver04/pen/XWjLPLo

import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/build/three.module.js';
import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/examples/jsm/controls/OrbitControls.js';

class Main {

  constructor(canvasId)
  {
    this.canvas = document.querySelector(canvasId);

    this.renderer = new THREE.WebGLRenderer({canvas: this.canvas});

    this.fov = 75;
    this.aspect = 2;  // the canvas default
    this.near = 0.1;
    this.far = 5;
    this.camera = new THREE.PerspectiveCamera(this.fov, this.aspect, this.near, this.far);
    this.camera.position.z = 2;

    this.controls = new OrbitControls(this.camera, this.canvas);
    this.controls.target.set(0, 0, 0);
    this.controls.update();

    this.scene = new THREE.Scene();

    this.color = 0xFFFFFF;
    this.intensity = 1;
    this.light = new THREE.DirectionalLight(this.color, this.intensity);
    this.light.position.set(-1, 2, 4);
    this.scene.add(this.light);

    this.boxWidth = 1;
    this.boxHeight = 1;
    this.boxDepth = 1;
    this.geometry = new THREE.BoxGeometry(this.boxWidth, this.boxHeight, this.boxDepth);

    this.boundRender = this.render.bind(this);
  }

  makeInstance(geometry, color, x){

    let material = new THREE.MeshPhongMaterial(color);

    let cube = new THREE.Mesh(geometry, material);
    this.scene.add(cube);
    cube.position.x = x;

    return cube;
  }

  resizeRendererToDisplaySize(renderer) {
    this.canvas = this.renderer.domElement;
    let width = this.canvas.clientWidth;
    let height = this.canvas.clientHeight;
    let needResize = this.canvas.width !== width || this.canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }

    return needResize;
  }

  render() {

    if (this.resizeRendererToDisplaySize(this.renderer)) {
      this.canvas = this.renderer.domElement;
      this.camera.aspect = this.canvas.clientWidth / this.canvas.clientHeight;
      this.camera.updateProjectionMatrix();
    }

    this.renderer.render(this.scene, this.camera);

  }

  starter() {

    this.makeInstance(this.geometry, 0x44aa88,  0);
    this.makeInstance(this.geometry, 0x8844aa, -2);
    this.makeInstance(this.geometry, 0xaa8844,  2);

    this.render();

    this.controls.addEventListener('change', this.boundRender);

    window.addEventListener('resize', this.boundRender);

    // note: this is a workaround for an OrbitControls issue
    // in an iframe. Will remove once the issue is fixed in
    // three.js

    window.addEventListener('mousedown', (e) => {
      e.preventDefault();
      window.focus();
    });

    window.addEventListener('keydown', (e) => {
      e.preventDefault();
    });
  }
}

let main = new Main('#a');
main.starter();

let main2 = new Main('#b');
main2.starter();

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Notification from HTML code within Django

In my Django project, I am trying to implement an alert. I am sending data from views.py to singup.html in order to display an alert based on certain conditions. However, initially the alert variable in HTML is not assigned a value. It is only after click ...

Having trouble with querySelector or getElementById not functioning properly?

Currently, I am in the midst of developing an experimental web application that features a quiz component. For this project, I have implemented a Python file to handle the questions and quiz functionalities. However, I have encountered an issue with the Ja ...

What is the best way to retain selection while clicking on another item using jQuery?

There is a specific issue I am facing with text selection on the page. When I apply this code snippet, the selected text does not get de-selected even after clicking .container: jQuery('.container').on("mousedown", function(){ jQuery('. ...

Some inquiries regarding the fundamentals of JavaScript - the significance of the dollar sign ($) and the error message "is not a function"

Teaching myself JavaScript without actually doing any explicit research on it (crazy, right?) has led to a few mysteries I can't quite crack. One of these mysteries involves the elusive dollar sign. From what I gather, it's supposed to be a con ...

What is the best way to hide the input field when there are multiple parent classes present?

I am currently implementing dynamic fields with jQuery and everything is functioning correctly. The problem arises when I attempt to remove these fields. After searching on Google and browsing past questions on StackOverflow, I noticed that everyone seems ...

Ensure that the Promise is resolved upon the event firing, without the need for multiple event

I'm currently working on a solution where I need to handle promise resolution when an EventEmitter event occurs. In the function containing this logic, an argument is passed and added to a stack. Later, items are processed from the stack with differe ...

properties remain unchanged even after the state has been updated

Within my application, I have a component called Player. This component generates a div element containing an image and has properties named x and y. The values of these properties determine the left and top position of the div rendered by Player. Outside ...

I am looking to implement screen reader capabilities for both the select-2 combo box and bootstrap's datetime-picker. How can I go about doing

On my existing website, I have select-2 combo boxes for drop-down lists and a Bootstrap Calendar widget. While these features work well, they lack accessibility for users with disabilities. In an effort to make the website inclusive for everyone, I am work ...

Exploring the bind() method in the latest version of jQuery,

After upgrading my JQuery version, one of the plugins I was using stopped working. Despite trying to use the migrate plugin and changing all instances of bind() to on(), I still couldn't get it to work properly. The plugin in question is jQuery Paral ...

Tips on maintaining the content of an element within a directive template

I'm currently facing an issue with adding the ng-click directive to a button. Here's my HTML: <button class="btn">clicky</button> This is the directive I am using: angular.module('app').directive('btn', function ...

Buttons and links with intricate geometries

I am currently developing a web application using Java/JSF and I am interested in finding out about technologies that would allow me to define intricate shapes as clickable buttons or links. Right now, my only option is to split an image into smaller trans ...

The useEffect hook is failing to trigger

Currently, I am attempting to retrieve data from an API using Redux for state management. Despite trying to dispatch the action within the useEffect hook, it does not seem to be functioning properly. I suspect that the issue lies with the dependencies, but ...

Problem with Node JS controller when using async/await

While working on my Node API controller, I encountered an issue with my 'error-handler' middleware related to using an asynchronous function. TypeError: fn is not a function at eval (webpack:///./app/middleware/errorHandler.js?:16:21) T ...

Stable header that jumps to the top when scrolled

I have implemented the JavaScript code below to set the header to a fixed position when it reaches the top of the page so that it remains visible while the user scrolls. Everything appears to be functional, but the header movement is abrupt and not smooth. ...

"Implementing a nested drawer and appbar design using Material UI, along with incorporating tabs within

I am currently working on an app that includes tabs for different files, each of which requires its own drawer and appbar. I found a codesandbox example that is similar to what I am trying to implement. Here is the link to the codesandbox One issue I hav ...

Is there a way to adjust the timepicker value directly using the keyboard input?

Is there a way to control the material-ui-time-picker input using the keyboard instead of clicking on the clock? This is my current code: import React, { Component } from "react"; import { TimePicker } from "material-ui-time-picker"; import { Input as Ti ...

Angular 4 application encountering 'Access-Control-Allow-Origin' issue

Attempting to reach the following URL: Manually accessing works without issue. However, trying to access via an Angular 4 request results in: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://loca ...

What is causing my background image to move upward when I include this JavaScript code for FlashGallery?

There's an issue on my website where adding a flash gallery script is causing the background image to shift unexpectedly. You can view the affected page here: The culprit seems to be the "swfobject.js" script. I've identified this by toggling t ...

Changing an HTML table into an array using JavaScript

Is it possible to transform an HTML table into a JavaScript array? <table id="cartGrid"> <thead> <tr> <th>Item Description</th> <th>Qty</th> <th>Unit Price</th> ...

Running various callbacks consecutively from an array in JavaScript

I have a series of functions in an array, each one calling a callback once it's finished. For example: var queue = [ function (done) { console.log('Executing first job'); setTimeout(done, 1000); // actually an AJAX call ...