reactjs - 使用 React,我的 three.js 相机在单击事件后未定义
问题描述
我正在使用Three.js和React创建交互式地图。
我的问题是:我的画布顶部有 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
难道我做错了什么?
我提前感谢您的帮助!
解决方案
推荐阅读
- python - 仅当函数接受该参数时才将参数传递给函数 Python
- ios - 在某些情况下,SwiftUI 与 addArc 一起旋转无法正常工作
- apache-camel - 骆驼在hawtio的航班交换中表现出负面
- c# - 解析反序列化的 Json 数据失败 - c#
- android - 错误:错误:找不到与给定名称匹配的资源(在“标签”处,值为“@string/title_gizmos”)
- typescript - 为团队成员编译不同的打字稿
- unix - 用于屏蔽信用卡最多 12 位数字的 Sed 命令
- java - 使用 Swig 在 C 和 java 之间传递数组
- node.js - 无法更改 package.json 中的目录
- django - 使用超过 4000 万行的 Django 和 PostgreSQL 进行慢速查询