Search code examples
vue.jsthree.jsresponsive

Get correct mouseover interaction in a ThreeJS VueJS app changing the window


I'm quite proud of what I've done: I have a menu which comprises 4 shapes. When you hover a shape, it's chaging color, growing and pushing the other shapes on top, while the rotation gets slower.

I read the ThreeJS docs and follow the advices of StackOverflow members.

I'm struggling with mouse interactions and window resizing: when I first open the browser, the mouseover doesn't seem to be called exactly when the mouse is over. And when I resize the window, it's clearly messed up. If anybody has a clue on what I'm doing wrong, thanks in advance :)

Here is my component:

<template>
  <v-container>
    <div @click="onClick" @mousemove="onMouseMove" id="menu3D" style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;"></div>
    <v-row class="text-center">

      <v-col
        class="mb-5"
        cols="12"
      >
        <h2 class="headline font-weight-bold mb-3">
          Accueil
        </h2>

        <v-row justify="center">

          <p>
            THIS IS ONLY A TEST
          </p>
        </v-row>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>

  import * as Three from 'three'

  export default {
    name: 'Home',
    mounted() {
      this.init();
    },
    methods: {
      init: function() {
       this.createScene();
       this.createCamera();
       this.userData.formes.forEach(x=>this.createShape(x))
       this.addSpotlight(16777215/*'#fdffab'*/);
       this.addAmbientLight();
       this.animate();
       window.addEventListener('resize', this.onResize())
      },
      onResize: function() {
       let container = document.getElementById('menu3D');
       this.renderer.setSize(container.clientWidth, container.clientHeight);
       this.camera.aspect = container.clientWidth / container.clientHeight;
       this.camera.updateProjectionMatrix();
      },
      createScene: function() {

       this.renderer = new Three.WebGLRenderer({
         antialias: true,
         alpha: true
       });
       let container = document.getElementById('menu3D');
       this.renderer.setSize(container.clientWidth, container.clientHeight);
       this.renderer.setPixelRatio(window.devicePixelRatio);
       this.renderer.setClearColor(0xffffff,0);
       container.appendChild(this.renderer.domElement);
      },

      createCamera: function() {
        //let container = document.getElementById('container');
       this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);
       this.camera.position.set(0, 5, 20);
       this.camera.zoom = 1;
      },

      createShape: function(shape) {

       let material = new Three.MeshStandardMaterial({
        "color": '#0000ff'/*16777215*/,
        "roughness": 1,
        "metalness": 0.5,
        "emissive": 0,
        "depthFunc": 3,
        "depthTest": true,
        "depthWrite": true,
        "stencilWrite": false,
        "stencilWriteMask": 255,
        "stencilFunc": 519,
        "stencilRef": 0,
        "stencilFuncMask": 255,
        "stencilFail": 7680,
        "stencilZFail": 7680,
        "stencilZPass": 7680
       })
       switch (shape.nom) {
         case "Box": {
           this.geometry = new Three.BoxBufferGeometry(1.8,1.8,1.8)
           break;
         }
         case "Sphere": {
           this.geometry = new Three.SphereBufferGeometry(1,8,6,0,6.283185,0, 3.141593)
           break;
         }
         case "Dodecahedron": {
           this.geometry = new Three.DodecahedronBufferGeometry(1.2,0)
           break;
         }
         case "Icosahedron": {
           this.geometry = new Three.IcosahedronBufferGeometry(1.5,0)
           break;
         }
       }
       this.mesh = new Three.Mesh(this.geometry, material)
       this.mesh.name = shape.nom
       this.mesh.userData = shape.userData
       this.mesh.receiveShadow = true
       this.mesh.castShadow = true
       this.mesh.position.set(0, shape.userData.position.y, 0)
       this.scene.add(this.mesh)
      },

      addSpotlight: function(color) {
       const light = new Three.SpotLight(color, 2, 1000)
       light.position.set(0, 0, 30)
       this.scene.add(light)
      },

      addAmbientLight: function() {
       const light = new Three.AmbientLight('#fff', 0.5)
       this.scene.add(light)
      },

      verifForme: function(e) {
        let t = this
        let elt = t.scene.getObjectByName(e);
            t.intersects = t.raycaster.intersectObject(elt);
            if (t.intersects.length !== 0) {
                // if it's not in the array, we put it at the beginning
                if (t.userData.souris.indexOf(e)<0) {
                    t.userData.souris.unshift(e);
                    console.log(t.userData.souris[0] + " survolé!");
                }
                if (t.userData.souris[0] == e) {
                    let obj = t.intersects[0].object;
                    obj.material.color.set('#'+elt.userData.couleurs[1]);
                    obj.scale.set(obj.scale.x<1.4?obj.scale.x+t.VITESSE_ZOOM:obj.scale.x,obj.scale.y<1.4?obj.scale.y+t.VITESSE_ZOOM:obj.scale.y,obj.scale.z<1.4?obj.scale.z+t.VITESSE_ZOOM:obj.scale.z);
                    obj.rotation.y += t.VITESSE_ROTATION/t.RALENTISSEMENT
                    t.replacer(obj,obj.userData.position.y+obj.userData.decalage)

                }
                else {
                    t.retrecir(e,elt);
                }
        }
            else {
                if (t.userData.souris.indexOf(e)>=0) {
                    t.userData.souris = t.userData.souris.filter(forme => forme != e);
                }
                t.retrecir(e,elt);
            }
      },

      onClick: function ( event ) {
        event.preventDefault();
            if (this.userData.souris.length >0 ) { console.log(this.userData.souris[0] + " clicked!"); }
        else {
          console.log("click outside!")
        }
        },

      onMouseMove: function(event){
        let container = document.getElementById('menu3D');
        this.mouse.x = ( event.clientX / container.clientWidth ) * 2 - 1;
        this.mouse.y = - ( event.clientY / container.clientHeight ) * 2 + 1;
        //console.log(JSON.stringify(this.mouse))
      },

      replacer: function(e,py) {
            // next line to prevent shaking
            if (Math.abs(e.position.y - py) < 0.05) { return true }
            let rhesus = 10*this.VITESSE_ZOOM
        if (this.userData.souris[0] != e.name) { rhesus *= 3 }
        //console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
            if (e.position.y > py) { rhesus = -1 }
            e.position.set(0,Math.trunc(10*e.position.y+rhesus)/10,0)
        },

      retrecir: function (n,e) {
            // checking if the clicked element is on top
            let dec = 0
        let elt = this
            if ((elt.userData.souris.length > 0) && (elt.userData.formes.map(x=>x.nom).indexOf(n)<elt.userData.formes.map(x=>x.nom).indexOf(elt.userData.souris[0]))) {
                dec = Math.trunc(10*e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage*2.1)/10;
            }
            e.material.color.set('#'+e.userData.couleurs[0]);
            e.rotation.y += elt.VITESSE_ROTATION
            e.scale.set(e.scale.x>1?e.scale.x-elt.VITESSE_ZOOM:e.scale.x,e.scale.y>1?e.scale.y-elt.VITESSE_ZOOM:e.scale.y,e.scale.z>1?e.scale.z-elt.VITESSE_ZOOM:e.scale.z);
            let newY = e.userData.position.y+dec
            if (e.position.y != newY) {
                elt.replacer(e,newY)
            }
        },

      animate: function() {
         let elt = this
         requestAnimationFrame(this.animate);
         this.raycaster.setFromCamera(this.mouse, this.camera);
         this.userData.formes.map(x=>x.nom).forEach(x=>elt.verifForme(x))
         if (this.userData.souris.length >0 ) { document.body.style.cursor = "pointer"; }
         else { document.body.style.cursor = "default"; }
         this.camera.updateProjectionMatrix();
         this.renderer.render(this.scene, this.camera);
      }
    },
    data: () => ({
      scene: new Three.Scene(),
      camera: null,
      renderer: Three.WebGLRenderer,
      mesh: new Three.Mesh,
      factor:0,
      mouse : new Three.Vector2(1, 1),
      raycaster : new Three.Raycaster(),
      intersects : [],
      VITESSE_ROTATION: 0.05,
      VITESSE_ZOOM: 0.1,
      RALENTISSEMENT: 3,
      userData: {
        "souris": [],
        "formes": [
          {
            "nom": "Box",
            "userData": {
              "position": {
                "x": 0,
                "y": 7.8,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "095256"
              ],
              "decalage": 0.5
            }
          },
          {
            "nom": "Icosahedron",
            "userData": {
              "position": {
                "x": 0,
                "y": 5.5,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "087F8C"
              ],
              "decalage": 0.5
            }
          },
          {
            "nom": "Dodecahedron",
            "userData": {
              "position": {
                "x": 0,
                "y": 3.1,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "5AAA95"
              ],
              "decalage": 0.4
            }
          },
          {
            "nom": "Sphere",
            "userData": {
              "position": {
                "x": 0,
                "y": 1,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "86A873"
              ],
              "decalage": 0.2
            }
          }
        ]
      }
    }),
  }
</script>

Solution

  • I investigated the problem using the code you provided and fixed it locally so I'm hoping it also works for you. The issues are the following:

    1. A tiny hard to find typo in the attachment of the resize event: instead of window.addEventListener('resize', this.onResize()) you need to use window.addEventListener('resize', this.onResize); removing the () because you don't want to call the function at the time of the attachment, you want it called each time the event is triggered.
    2. Due to the first issue, as the resize function wasn't getting called when you expected, I guess this is what led you to use a hard-coded value (1.686275) in the camera instantiation instead of the recommended formula container.clientWidth / container.clientHeight so you need to change that back to

    createCamera: function () {
        let container = document.getElementById('menu3D');
        this.camera = new Three.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.01, 1000);
        ...

    1. Also as the 3D container div is not in the root level of the HTML body due to VueJS requirements, in onMouseMove() you need to consume the offset coordinates instead of the client ones as follows:

    onMouseMove: function (event) {
      let container = document.getElementById('menu3D');
      this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
      this.mouse.y = - (event.offsetY / container.clientHeight) * 2 + 1;
      ...