使用Threejs实现鼠标点击画线功能

1.先看下最终效果

2.使用主要的技术及版本

  • threejs 0.155.0
  • react 18.2.0

3.大致思路

鼠标单击第一个点时使用BufferGeometry创建一条line,并add到linegroup。鼠标点击其余点时修改当构成前line的buffer及绘制区域(setDrawRange)。同时在buffer的尾部另外存储一份当前的点击位置(px,py)用于占位,当鼠标在画布上移动时修改用于占位的px,py,并修改绘制区域就实现了鼠标移动时跟随绘制效果。鼠标右键时把当前线用于占位的px,py从buffer中删除,然后再创建一条新的line添加到linegroup.

4.主要步骤

诸如scene,renderer,camera这些元素是实现Threejs渲染图形必不可少的;创建这些元素并做一些基础的设置。注意这里创建的是正交相机,因为绘制的是2d图形用正交相机更合适。

ts 复制代码
//获取dom元素,即将threejs绘制的canvas渲染到这个元素上。这里我使用的是canvas 元素
 const canvasP = document.querySelector('#canvas') as HTMLElement
// 创建render
let renderer = new THREE.WebGLRenderer({
      canvas: canvasP as HTMLCanvasElement,
      antialias: true,
      alpha: true
    });
    //创建场景
    let scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000);
    scene.fog = new THREE.Fog(0x000000, 0, 200); 
    //设置render的像素比,这里的env暂时可以忽略
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth - env.nav_width, window.innerHeight);
   //创建正交相机 
   let camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100)

接下来创建一个group用于存储我们画的多条line.这里之所以单独把line放到group里而不是直接添加到scene里面是因为我们后面要获取我们的line,如果直接放到scene里查找line就变的比较麻烦。

ts 复制代码
const lineGroup :THREE.Group= new THREE.Group()
scene.add(lineGroup)

创建一个函数makeLine,用于创建line.并把line添加到lineGroup。这里创建line使用的是BufferGeometry的方式。方便后期增加新的节点。初始创建的line就一个点无法形成线所以此时绘制区域设置为0;

ts 复制代码
 const  makeLine=() =>{
        //创建存储构成线的点的位置信息
        const positions = new Float32Array(0)
        const geometry = new THREE.BufferGeometry()
         //设置geometry的position,三个数据构成一个点的位置信息(xyz,其中z是0)
        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
        geometry.setDrawRange(0, 0)
        const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xffffff }))
        lineGroup.add(line)
    }

添加鼠标单击canvas事件,首先获取到当前正在绘制的line(即是lineGroup数组里的最后一条line,我们暂称其为curLine)。获取curLine的positionAttribut.并将其转为数组存放到points,在points尾部push鼠标点击位置的坐标信息。这里注意需要将鼠标事件的clientX,clientY转化为threejs坐标数据(pX,pY)。将points数组作为geometry的positionAttribute传进去,并修改setDrawRange即可看到绘制了一条线的一部分。此外为了实现鼠标跟随效果再points的尾部再次push pX,pY.

ts 复制代码
  myCanvas.addEventListener('click', (e: MouseEvent) => {
              //获取当前正在绘制的线条
            const line = lineGroup.children[lineGroup.children.length - 1] as THREE.Line
            const positionAttribute = line.geometry.getAttribute('position') || []
            //将clientX,clientY转换为threejs对应的坐标数据
            const [pX,pY] = getPos(e)
            const points: number[] = [...Array.from(positionAttribute.array)]
            points.push(pX, pY, 0)
            const geometry = line.geometry
            geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(points), 3))
            positionAttribute.needsUpdate = true
            geometry.setDrawRange(0, (points.length)/3)
         //为了实现鼠标move跟随效果再次push
            points.push(pX, pY, 0)
            geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(points), 3))
            //绘制小球 即线体上面的节点
            const sphere1 = smallBoll()
            sphere1.position.set(pX, pY, 0)
            scene.add(sphere1)
        })

坐标位置转换函数

ts 复制代码
 const   getPos=(e:MouseEvent)=>{
        const myCanvas = document.getElementById('canvas') as HTMLCanvasElement
        const { clientX, clientY } = e
        let x = clientX - env.nav_width
        let y = clientY
        let pX = -1 + 2 * x / myCanvas.clientWidth
        let pY = 1 - 2 * y / myCanvas.clientHeight
        return [pX,pY]
    }

鼠标move事件跟鼠标点击事件类似,鼠标点击事件主要是往buffer数组内push坐标位置数据并修改range,鼠标move事件则主要是修改buffer数组内组成最后一个节点的位置数据并修改range.

ts 复制代码
  myCanvas.addEventListener('mousemove', (e) => {
            //获取当前正在绘制的线条
            const line = lineGroup.children[lineGroup.children.length - 1] as THREE.Line
            const positionAttribute = line.geometry.getAttribute('position') || []
            //鼠标clientX,clientY转为threejs坐标
            const [pX,pY] = getPos(e)
          //修改最后一个点的位置信息
            const points: number[] = [...Array.from(positionAttribute.array)]
            points[points.length - 3] =pX
            points[points.length - 2] =pY
            const geometry = line.geometry
            geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(points), 3))
            positionAttribute.needsUpdate = true
            geometry.setDrawRange(0,  (points.length)/3)
            //跟随鼠标位置的小球的位置
            sphere.position.set(pX, pY, 0)
          
        })

鼠标右键主要功能是结束当前绘制的line,并开启一条新的line.其中结束当前line主要是把其最后一个点数据从buffer内删除。另外这里有一点需要注意:阻止浏览器的默认右键事件。

ts 复制代码
     myCanvas.addEventListener('contextmenu', (e) => {
            const line = lineGroup.children[lineGroup.children.length - 1] as THREE.Line
            const positionAttribute = line.geometry.getAttribute('position') || []
            const points: number[] = [...Array.from(positionAttribute.array)]
            if(points.length/3>0){
                points.length = points.length -1
            }
            const geometry = line.geometry
            geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(points), 3))
            //创建一条新的线
            makeLine()
            e.preventDefault()
        })

小球跟随,创建一个全局的SphereGeometry,初始时位置放到屏幕外,鼠标在canvas移动时修改sphere的位置即可实现小球跟随。循环渲染则是用ani函数内requestAnimationFrame(ani)。另外还有html元素用于创建绘图区的canvas.

ts 复制代码
    const smallBoll= ()=>{
        const geometry = new THREE.SphereGeometry(0.01, 32, 32)
        const material = new THREE.MeshBasicMaterial({ color: 0xffffff })
        const sphere = new THREE.Mesh(geometry, material)
        sphere.position.set(-100, 0, 0)
        return sphere
    }
    
function ani() {
            renderer.render(scene, camera)
            requestAnimationFrame(ani)
        }
        ani()
tsx 复制代码
<div style={{ position: 'relative' }}>
        <span style={{ position: 'absolute', color: 'white' }}>鼠标点击画线,右键结束当前画线开启新新线</span>
        <canvas id="canvas" style={{ 'display': 'block' }}></canvas>
    </div>

5.在线体验及源码地址

在线体验地址

源码地址

相关推荐
GDAL2 分钟前
vue3入门教程:ref函数
前端·vue.js·elementui
GISer_Jing10 分钟前
Vue3状态管理——Pinia
前端·javascript·vue.js
好开心3325 分钟前
axios的使用
开发语言·前端·javascript·前端框架·html
Domain-zhuo35 分钟前
Git常用命令
前端·git·gitee·github·gitea·gitcode
菜根Sec1 小时前
XSS跨站脚本攻击漏洞练习
前端·xss
m0_748257181 小时前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
桃园码工1 小时前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲2 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
Anlici2 小时前
three.js建立3D模型展示地球+高亮
前端·数据可视化·canvas
轻口味3 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos