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

在线体验地址

源码地址

相关推荐
发现一只大呆瓜2 分钟前
Vue3:ref 与 reactive 超全对比
前端·vue.js·面试
lzksword8 分钟前
C++ Builder XE OpenDialog1打开多文件并显示xls与xlsx二种格式文件
java·前端·c++
陈随易16 分钟前
站在普通开发者的角度,我觉得 RollCode 更像是“把 H5 交付这件事重新捋顺了”
前端·后端·程序员
陈随易35 分钟前
RollCode:不只是在做页面,而是在缩短“从需求到上线”的整条链路
前端·后端
炽烈小老头1 小时前
【每天学习一点算法 2026/03/17】括号生成
前端·学习·typescript
大漠_w3cpluscom1 小时前
如何在 CSS 中正确使用 if()
前端
eason_fan1 小时前
踩坑记录:Mac M系列芯片下 pnpm dlx 触发的 esbuild 架构不匹配错误
前端·前端工程化
swipe1 小时前
JavaScript 对象操作进阶:从属性描述符到对象创建模式
前端·javascript·面试
IT_陈寒2 小时前
React开发者都在偷偷用的5个性能优化黑科技,你知道几个?
前端·人工智能·后端
还是大剑师兰特2 小时前
Vue3 前端专属配置(VSCode settings.json + .prettierrc)
前端·vscode·json