在vue 项目中用 three.js 帮我实现点击任意模型弹框,弹框显示在模型正上方,弹框可以自定义宽高样式,弹框内显示自定义的文字内容
首先在vue项目中引入three.js库:
import * as THREE from 'three'
然后在vue组件中定义一个three.js场景,加载模型并添加点击事件:
<template>
<div ref="container" style="width: 100%; height: 100%;"></div>
</template>
<script>
import * as THREE from 'three'
export default {
data() {
return {
scene: null,
camera: null,
renderer: null,
raycaster: new THREE.Raycaster(),
mouse: new THREE.Vector2(),
intersected: null,
popup: null,
popupText: 'Hello world!'
}
},
mounted() {
this.initScene()
this.initCamera()
this.initRenderer()
this.loadModel()
this.renderScene()
window.addEventListener('resize', this.onWindowResize, false)
this.$refs.container.addEventListener('mousemove', this.onMouseMove, false)
this.$refs.container.addEventListener('click', this.onClick, false)
},
methods: {
initScene() {
this.scene = new THREE.Scene()
},
initCamera() {
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000)
this.camera.position.set(0, 0, 5)
},
initRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.$refs.container.appendChild(this.renderer.domElement)
},
loadModel() {
const loader = new THREE.GLTFLoader()
loader.load('/path/to/model.gltf', gltf => {
const model = gltf.scene
model.position.set(0, 0, 0)
model.scale.set(1, 1, 1)
this.scene.add(model)
this.intersected = model
})
},
renderScene() {
this.renderer.render(this.scene, this.camera)
requestAnimationFrame(this.renderScene)
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
},
onMouseMove(event) {
event.preventDefault()
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
},
onClick(event) {
event.preventDefault()
if (this.intersected) {
const intersects = this.raycaster.intersectObject(this.intersected, true)
if (intersects.length > 0) {
this.showPopup()
}
}
},
showPopup() {
if (!this.popup) {
this.popup = document.createElement('div')
this.popup.style.position = 'absolute'
this.popup.style.top = '0'
this.popup.style.left = '0'
this.popup.style.width = '200px'
this.popup.style.height = '200px'
this.popup.style.background = 'white'
this.popup.style.border = '1px solid black'
this.popup.style.padding = '10px'
this.popup.style.textAlign = 'center'
this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
this.popup.innerHTML = this.popupText
this.$refs.container.appendChild(this.popup)
}
const position = this.intersected.getWorldPosition(new THREE.Vector3())
const screenPosition = position.clone().project(this.camera)
const x = (screenPosition.x + 1) / 2 * window.innerWidth
const y = -(screenPosition.y - 1) / 2 * window.innerHeight
this.popup.style.top = `${y}px`
this.popup.style.left = `${x}px`
}
}
}
</script>
在这个例子中,我们创建了一个three.js场景,并加载了一个模型。我们在模型上添加了鼠标点击事件,当用户点击模型时,我们会显示一个弹框。弹框的内容可以自定义,弹框的位置会显示在模型的正上方。
我们在vue组件的data中定义了一些变量,包括场景、相机、渲染器、射线、鼠标位置、被选中的模型、弹框和弹框内容。我们还定义了一些方法来初始化场景、相机
、渲染器、加载模型、渲染场景、处理窗口大小改变事件、处理鼠标移动事件、处理鼠标点击事件和显示弹框。
在加载模型时,我们使用了GLTFLoader来加载模型文件。一旦模型加载完成,我们将其添加到场景中,并将其保存为被选中的模型。
当用户在模型上点击鼠标时,我们使用射线检测来检测是否有模型被选中。如果有,我们将显示一个弹框。
在显示弹框时,我们首先检查是否已经存在一个弹框。如果没有,我们创建一个新的div元素作为弹框,并设置一些样式属性。我们还设置弹框的内容为我们在data中定义的popupText变量的值。
然后,我们需要确定弹框的位置。我们使用被选中的模型的世界位置来计算屏幕上的位置。我们将这个位置映射到屏幕坐标系,并使用这个坐标来设置弹框的位置。最后,我们将弹框添加到vue组件的DOM元素中。
需要注意的是,在实际应用中,我们需要通过CSS来控制弹框的样式,而不是直接设置style属性。我们在这个例子中直接设置style属性只是为了演示方便。
最后,我们需要在vue组件销毁时清理所有事件和元素:
beforeDestroy() {
window.removeEventListener('resize', this.onWindowResize)
this.$refs.container.removeEventListener('mousemove', this.onMouseMove)
this.$refs.container.removeEventListener('click', this.onClick)
if (this.popup) {
this.$refs.container.removeChild(this.popup)
}
},
这样,我们就成功地在vue项目中使用three.js实现了点击任意模型弹框的效果。
如果我们需要在弹框中显示不同的内容,我们可以通过传递参数来实现。例如,我们可以在showPopup方法中传递一个参数来设置弹框内容:
showPopup(content) {
if (!this.popup) {
this.popup = document.createElement('div')
this.popup.style.position = 'absolute'
this.popup.style.top = '0'
this.popup.style.left = '0'
this.popup.style.width = '200px'
this.popup.style.height = '200px'
this.popup.style.background = 'white'
this.popup.style.border = '1px solid black'
this.popup.style.padding = '10px'
this.popup.style.textAlign = 'center'
this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
this.$refs.container.appendChild(this.popup)
}
this.popup.innerHTML = content
// ...
}
然后,在点击事件中调用showPopup方法时,我们传递需要显示的内容,例如:
onClick(event) {
event.preventDefault()
if (this.intersected) {
const intersects = this.raycaster.intersectObject(this.intersected, true)
if (intersects.length > 0) {
this.showPopup('This is some custom content')
}
}
},
这样,我们就可以在弹框中显示不同的内容了。
最后,需要注意的是,在实际应用中,我们需要根据需求来设置弹框的样式和布局,以及处理弹框中的交互事件。在这个例子中,我们只是简单地创建了一个div元素作为弹框,但在实际应用中,我们可能需要使用更复杂的UI组件库来实现弹框。
另外,如果我们需要在弹框中显示更复杂的内容,例如图表或表格等,我们可以考虑使用第三方库来实现。例如,我们可以使用Chart.js来绘制图表,将图表渲染到canvas元素中,然后将canvas元素添加到弹框中:
import Chart from 'chart.js/auto'
showPopup(chartData) {
if (!this.popup) {
this.popup = document.createElement('div')
this.popup.style.position = 'absolute'
this.popup.style.top = '0'
this.popup.style.left = '0'
this.popup.style.width = '400px'
this.popup.style.height = '400px'
this.popup.style.background = 'white'
this.popup.style.border = '1px solid black'
this.popup.style.padding = '10px'
this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
this.$refs.container.appendChild(this.popup)
}
const canvas = document.createElement('canvas')
canvas.width = 380
canvas.height = 380
const ctx = canvas.getContext('2d')
new Chart(ctx, {
type: 'bar',
data: chartData,
options: {}
})
this.popup.innerHTML = ''
this.popup.appendChild(canvas)
// ...
}
在这个例子中,我们使用Chart.js来创建一个柱状图,并将图表渲染到canvas元素中。然后,我们将canvas元素添加到弹框中。这样,我们就可以在弹框中显示复杂的图表了。
需要注意的是,在使用第三方库时,我们需要在vue组件中正确安装和卸载库,以避免内存泄漏和其他问题。具体的实现方法会因库而异,需要查阅库的文档来了解。
你写的代码有错误,怎么点击了没反应,而且保存
非常抱歉,我的代码中有些错误。以下是经过修正的代码:
模板代码:
<template>
<div ref="container" style="width: 100%; height: 100%;"></div>
</template>
脚本代码:
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
export default {
data() {
return {
scene: null,
camera: null,
renderer: null,
raycaster: new THREE.Raycaster(),
mouse: new THREE.Vector2(),
intersected: null,
popup: null,
popupText: 'Hello world!'
}
},
mounted() {
this.initScene()
this.initCamera()
this.initRenderer()
this.loadModel()
this.renderScene()
window.addEventListener('resize', this.onWindowResize, false)
this.$refs.container.addEventListener('mousemove', this.onMouseMove, false)
this.$refs.container.addEventListener('click', this.onClick, false)
},
methods: {
initScene() {
this.scene = new THREE.Scene()
},
initCamera() {
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000)
this.camera.position.set(0, 0, 5)
},
initRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.$refs.container.appendChild(this.renderer.domElement)
},
loadModel() {
const loader = new GLTFLoader()
loader.load('/path/to/model.gltf', gltf => {
const model = gltf.scene
model.position.set(0, 0, 0)
model.scale.set(1, 1, 1)
this.scene.add(model)
this.intersected = model
})
},
renderScene() {
this.renderer.render(this.scene, this.camera)
requestAnimationFrame(this.renderScene)
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
},
onMouseMove(event) {
event.preventDefault()
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
},
onClick(event) {
event.preventDefault()
if (this.intersected) {
const intersects = this.raycaster.intersectObject(this.intersected, true)
if (intersects.length > 0) {
this.showPopup()
}
}
},
showPopup() {
if (!this.popup) {
this.popup = document.createElement('div')
this.popup.style.position = 'absolute'
this.popup.style.top = '0'
this.popup.style.left = '0'
this.popup.style.width = '200px'
this.popup.style.height = '200px'
this.popup.style.background = 'white'
this.popup.style.border = '1px solid black'
this.popup.style.padding = '10px'
this.popup.style.textAlign = 'center'
this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
this.$refs.container.appendChild(this.popup)
}
const position = this.intersected.getWorldPosition(new THREE.Vector3())
const screenPosition = position.clone().project(this.camera)
const x = (screenPosition.x + 1) / 2 * window.innerWidth
const y = -(screenPosition.y - 1) / 2 * window.innerHeight
this.popup.style.top = `${y}px`
this.popup.style.left = `${x}px`
this.popup.innerHTML = this.popupText
}
},
beforeDestroy() {
window.removeEventListener('resize', this.onWindowResize)
this.$refs.container.removeEventListener('mousemove', this.onMouseMove)
this.$refs.container.removeEventListener('click', this.onClick)
if (this.popup) {
this.$refs.container.removeChild(this.popup)
}
}
}
在这个修正后的代码中,我将GLTFLoader导入语句改为了:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
同时,在loadModel方法中,我将GLTFLoader的实例化方式改为了:
const loader = new GLTFLoader()
这是因为在
为什么运行起来画面是黑色的,也没报错
A server error has occurred
INTERNAL_SERVER_ERROR