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

在线体验地址

源码地址

相关推荐
Tandy12356_6 分钟前
js逆向——webpack实战案例(一)
前端·javascript·安全·webpack
TonyH20028 分钟前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
你会发光哎u12 分钟前
Webpack模式-Resolve-本地服务器
服务器·前端·webpack
王小二(海阔天空)13 分钟前
个人文章合集 - 前端相关
前端·css·vue·jquery
老华带你飞22 分钟前
公寓管理系统|SprinBoot+vue夕阳红公寓管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot·课程设计
gopher951139 分钟前
HTML详解
前端·html
Tiny201740 分钟前
前端模块化CommonJs、ESM、AMD总结
前端
吕永强42 分钟前
CSS相关属性和显示模式
前端·css·css3
结衣结衣.1 小时前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
全栈技术负责人1 小时前
前端提升方向
前端