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>