使用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.在线体验及源码地址

在线体验地址

源码地址

相关推荐
慧一居士3 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead5 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说7 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409198 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app