首页 > 解决方案 > 使用 React,我的 three.js 相机在单击事件后未定义

问题描述

我正在使用Three.jsReact创建交互式地图。

我的问题是:我的画布顶部有 2 个按钮,可以放大我的地图。当我单击一个时,我收到此错误:

TypeError:无法读取未定义的属性“位置” (第 165 行)

我知道我的相机是在我的函数之后初始化的,并且此时它是未定义的。它是一个全局变量,我的函数是一个监听器onClick,所以我不明白这个错误。

在发布此消息之前,我对我的代码进行了多项研究和测试。

让我解释:

为了解决这个错误,我首先尝试在useEffect. 好吧,我的错误发生了变化,这次我遇到了这个错误:

TypeError:无法读取未定义的属性“minDistance” (第 166 行)

这个错误的问题是我必须初始化 中的控件useEffect,因为它使用 value:renderer.domElement外部未定义。

我尝试在外部初始化相机、渲染器和控件,useEffect但以:refMap.current.appendChild(renderer.domElement)内部结束。我没有收到任何错误,并且我的按钮可以正常运行该功能,但是这次我的相机没有缩放,除了我的console.log("test") (line 164)之外什么也没有发生。

所以我回到了起点,并没有找到任何其他的解决方案。除了如果我用我的地图加载页面,使用链接转到另一个页面react-router-dom,然后回到这个页面,以同样的方式,我的按钮工作,我的相机和控件不再未定义,我确实可以放大我的地图

我的印象是这是一个渲染问题,但我不知道如何解决它。

这是我的代码:

import { useEffect, useRef } from "react"
import { Helmet } from "react-helmet-async"
import Layout from "components/Layout"
import PushButton from "components/PushButton"
import { Link } from "react-router-dom"
import * as THREE from "three"
import * as TWEEN from "@tweenjs/tween.js"
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import styles from "./Map.module.sass"
import markers from "./markers.json"

type Marker = {
  id: number,
  x: number,
  y: number,
  link: string
}

type Markers = {
  element: HTMLElement,
  marker: Marker
}

const Map = () => {
  useEffect(() => {
    window.scrollTo(0, 0)
  }, [])

  const refMap = useRef(null)
  const refMarkers = useRef(null)

  let scene: THREE.Scene
  let camera: THREE.PerspectiveCamera
  let renderer: THREE.WebGLRenderer
  let controls: OrbitControls

  // Function to create the camera
  const createCamera = () => {
    camera = new THREE.PerspectiveCamera(
      75, window.innerWidth / window.innerHeight, 0.1, 1000,
    )
    camera.position.z = 0.2
  }

  // Function to create the render
  const createRender = () => {
    renderer = new THREE.WebGLRenderer({ antialias: true })
    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setPixelRatio(window.devicePixelRatio)
    /* @ts-ignore */
    refMap.current.appendChild(renderer.domElement)
  }

  // Function to create the controls
  const createControls = () => {
    controls = new OrbitControls(camera, renderer.domElement)
    controls.enableRotate = false
    controls.enableDamping = true
    controls.mouseButtons = {
      LEFT: THREE.MOUSE.RIGHT, MIDDLE: THREE.MOUSE.MIDDLE, RIGHT: THREE.MOUSE.RIGHT,
    }
    controls.touches = {
      ONE: THREE.TOUCH.PAN, TWO: THREE.TOUCH.DOLLY_PAN,
    }
    controls.minDistance = 0.15
    controls.maxDistance = 0.4
    controls.zoomSpeed = 0.6
    controls.panSpeed = 1.5

    // Limit the pan move
    const minPan = new THREE.Vector3(-0.25, -0.3, 0)
    const maxPan = new THREE.Vector3(0.25, 0.3, 0)
    const vector = new THREE.Vector3()

    controls.addEventListener("change", () => {
      vector.copy(controls.target)
      controls.target.clamp(minPan, maxPan)
      vector.sub(controls.target)
      camera.position.sub(vector)
    })
  }

  // Function to initialize the scene
  const init = () => {
    scene = new THREE.Scene()
    createCamera()
    createRender()
    createControls()
  }

  // Function to create the map
  const createMap = () => {
    const map = new THREE.TextureLoader().load("images/map.jpg")
    const material = new THREE.SpriteMaterial({ map })
    const sprite = new THREE.Sprite(material)

    sprite.scale.set(1, 1, 1) // It's the size of the image
    scene.add(sprite)
  }

  // Function to create markers
  const createMarkers = () => {
    const elements: Markers[] = []
    // @ts-ignore
    const markersDiv = refMarkers.current.querySelectorAll(".marker")

    markers.forEach((marker, index) => {
      const element = markersDiv[index]
      elements.push({ element, marker })
    })

    return elements
  }

  // Function to make markers responsive
  const responsiveMarkers = (markersObject: Markers[]) => {
    if (refMap.current) {
      markersObject.forEach((markerObject) => {
        const { element, marker } = markerObject
        const tempV = new THREE.Vector3(marker.x, marker.y, 0)
        tempV.project(camera)
        /* @ts-ignore */
        const x = (tempV.x * 0.5 + 0.5) * refMap.current.clientWidth
        /* @ts-ignore */
        const y = (tempV.y * -0.5 + 0.5) * refMap.current.clientHeight

        element.style.transform = `translate(${x}px,${y}px)`
      })
    }
  }

  useEffect(() => {
    init()
    createMap()
    const markersObject = createMarkers()

    // Function to render every time the scene
    const animate = (time: number) => {
      controls.update()
      requestAnimationFrame(animate)
      renderer.render(scene, camera)
      TWEEN.update(time)

      responsiveMarkers(markersObject)
    }
    requestAnimationFrame(animate)

    // Function to move the camera when the window is resize
    const onWindowResize = () => {
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
    }
    window.addEventListener("resize", onWindowResize, false)

    return () => {
      window.removeEventListener("resize", onWindowResize, false)
    }
  }, [])

  // Function to zoom in and out with buttons
  const handleClick = (zoom: number) => {
    console.log("test")
    /* @ts-ignore */
    let nextPosition = camera.position.z + zoom
    if (nextPosition < controls.minDistance || nextPosition > controls.maxDistance) {
      // Cancel the zoom
      if (zoom > 0) {
        nextPosition = controls.maxDistance
      } else {
        nextPosition = controls.minDistance
      }
    }

    new TWEEN.Tween(camera.position)
      .to({ x: camera.position.x, y: camera.position.y, z: nextPosition }, 800)
      .easing(TWEEN.Easing.Quadratic.Out)
      .start()
  }

  // Function to change the mouse when grabbing
  const handleMouseDown = () => {
    /* @ts-ignore */
    refMap.current.style.cursor = "grabbing"
  }

  // Function to put back the mouse
  const handleMouseUp = () => {
    /* @ts-ignore */
    refMap.current.style.cursor = "grab"
  }

  return (
    <Layout>
      <Helmet>
        <title>Nihon shinwa | The map</title>
        <meta name="description" content="Discover the map of Japanese mythology, from the story of Yuki Onna, the snow queen to the story of Izanagi, the deity." />
      </Helmet>

      <div className={styles.container}>
        <div className={styles.map} ref={refMap} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} aria-hidden="true" />
        <div className={styles.markers} ref={refMarkers}>
          {markers.map((marker) => (
            <div key={marker.id} className="marker">
              <Link to={marker.link}>
                <div className={styles.marker}>
                  <div className={styles.marker__border} />
                </div>
              </Link>
            </div>
          ))}
        </div>

        <div className={styles.buttons}>
          <div className={styles.plus} onClick={() => handleClick(-0.1)} aria-hidden="true">
            <PushButton>
              <span className={styles.line} />
              <span className={styles.line} />
            </PushButton>
          </div>

          <div className={styles.minus} onClick={() => handleClick(0.1)} aria-hidden="true">
            <PushButton>
              <span className={styles.line} />
            </PushButton>
          </div>
        </div>
      </div>

    </Layout>
  )
}

export default Map

难道我做错了什么?

我提前感谢您的帮助!

标签: reactjsthree.jsonclickuse-effect

解决方案


推荐阅读