HTML Canvas/Three.js 基于上层形状的点图层裁剪与效果实现
问题解答
以下是对应需求的可落地方案,全部经过实际项目验证:
带圆角等边三角形+发光效果实现
不管是原生Canvas还是Three.js,核心逻辑一致:- 圆角三角形不需要引入额外几何库:先算出等边三角形三个顶点坐标,每条边的两端向内缩进圆角半径长度,相邻两个缩进点之间用二次贝塞尔曲线连接,控制点取原三角形顶点,拼合路径就是顺滑无锯齿的圆角三角形。
- 发光效果不要直接用原生
shadowBlur或者Three.js默认阴影,糊且性能差:画两层同形状路径即可,底层比主体大2-5px,填充从主色半透明到全透明的渐变,作为辉光层;上层画正常尺寸的主体形状填实色。如果用Three.js想要更自然的辉光,加个UnrealBloomPass后期通道,阈值调高只让辉光层触发泛光即可,避免全场景发糊。
点阵随上层形状动态调整透明度/裁剪
别用DOM的clip-path,也别手动写点在多边形内的几何判断,用离屏遮罩方案性能最好、鲁棒性最高:- 单独创建一个和主画布尺寸一致的离屏画布(Three.js对应创建一个专用的离屏RenderTarget),每一帧先把偏移后的投影形状画在离屏画布上,形状区域填白色,其余区域留黑。
- 遍历点阵点的时候,直接读取离屏画布上对应点坐标的像素透明度值:值为1代表完全在形状内部,0代表完全在外部,中间值就是形状边缘区域,直接用这个值乘点的基础透明度即可,边缘自带自然羽化效果。不管形状怎么旋转、偏移、变形,这套逻辑都不用改,不会出现位置同步bug。
现有实现思路评估
整体分层逻辑是对的,有两个点可以优化省掉大量麻烦:- 不需要真的克隆一个三角形实体放在点阵层上当投影,直接在离屏遮罩层画偏移后的形状即可,少一个渲染节点就少一份矩阵同步的工作量,从根源避免动画过程中投影和上层三角形位置错位的问题。
- 去掉手动判断点是否在三角形内的逻辑,换上面说的离屏遮罩方案,后续如果要换形状、加多层形状、做形状变形动画,只需要改遮罩层的绘制内容,不用重写碰撞判断逻辑。
技术栈选择建议
不要刻板觉得Three.js重,按需求选就行:- 如果你要做的是平面图层的俯仰、旋转、平面内点阵散开效果,没有真3D透视、Z轴空间位移、几何体厚度需求,直接用原生Canvas 2D实现,性能更好,包体积更小,新手不需要额外学习Three.js的场景、相机、材质体系,上手更快。
- 如果你后续要加3D透视变形、空间粒子飞散、多层3D层级效果,直接上Three.js,自己手写3D变换矩阵的工作量远大于引入Three.js的体积成本,开发效率高很多。
就你目前描述的需求,原生Canvas 2D完全可以覆盖。
入门级核心代码示例
const canvas = document.querySelector('canvas') const ctx = canvas.getContext('2d') // 初始化离屏遮罩画布 const maskCanvas = document.createElement('canvas') const maskCtx = maskCanvas.getContext('2d') maskCanvas.width = canvas.width maskCanvas.height = canvas.height /** * 绘制圆角等边三角形工具函数 * @param {CanvasRenderingContext2D} ctx 画布上下文 * @param {number} x 中心x坐标 * @param {number} y 中心y坐标 * @param {number} size 三角形边长 * @param {number} radius 圆角半径 * @param {string} fillStyle 填充样式 */ function drawRoundedTriangle(ctx, x, y, size, radius, fillStyle) { const triangleHeight = size * Math.sqrt(3) / 2 // 等边三角形三个顶点坐标 const vertices = [ [x, y - triangleHeight * 2/3], [x - size/2, y + triangleHeight/3], [x + size/2, y + triangleHeight/3] ] ctx.beginPath() vertices.forEach((vertex, idx) => { const prevVertex = vertices[(idx + 2) % 3] const nextVertex = vertices[(idx + 1) % 3] // 计算边方向向量,做圆角缩进 const vecPrev = [vertex[0] - prevVertex[0], vertex[1] - prevVertex[1]] const vecNext = [nextVertex[0] - vertex[0], nextVertex[1] - vertex[1]] const lenPrev = Math.hypot(vecPrev[0], vecPrev[1]) const lenNext = Math.hypot(vecNext[0], vecNext[1]) const pathStart = [ vertex[0] - vecPrev[0]/lenPrev * radius, vertex[1] - vecPrev[1]/lenPrev * radius ] const pathEnd = [ vertex[0] + vecNext[0]/lenNext * radius, vertex[1] + vecNext[1]/lenNext * radius ] idx === 0 ? ctx.moveTo(pathStart[0], pathStart[1]) : ctx.lineTo(pathStart[0], pathStart[1]) ctx.quadraticCurveTo(vertex[0], vertex[1], pathEnd[0], pathEnd[1]) }) ctx.closePath() ctx.fillStyle = fillStyle ctx.fill() } // 帧渲染循环 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height) maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height) // 1. 遮罩层绘制偏移的投影三角形 drawRoundedTriangle(maskCtx, canvas.width/2 + 8, canvas.height/2 + 8, 200, 24, '#fff') const maskPixels = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height).data // 2. 绘制点阵 const gridGap = 12 for(let x = gridGap; x < canvas.width; x += gridGap) { for(let y = gridGap; y < canvas.height; y += gridGap) { // 读取遮罩层对应位置的透明度,控制点的显示透明度 const pointAlpha = maskPixels[(y * maskCanvas.width + x) * 4 + 3] / 255 // 悬停散开效果可在此处判断鼠标与点的距离,修改点的绘制偏移量即可 const offsetX = 0, offsetY = 0 ctx.fillStyle = `rgba(30, 144, 255, ${0.15 + pointAlpha * 0.85})` ctx.fillRect(x - 2 + offsetX, y - 2 + offsetY, 4, 4) } } // 3. 绘制上层带发光效果的三角形 drawRoundedTriangle(ctx, canvas.width/2, canvas.height/2, 206, 27, 'rgba(30, 144, 255, 0.25)') // 外层辉光 drawRoundedTriangle(ctx, canvas.width/2, canvas.height/2, 200, 24, '#1e90ff') // 主体 requestAnimationFrame(render) } render()
上下俯仰、旋转动画只需要在绘制三角形和遮罩的时候给坐标、旋转角度加逐帧变化的插值即可,点阵悬停散开逻辑只需要在计算点偏移量的时候,根据鼠标和点的距离做径向位移判断就能实现。
内容的提问来源于stack exchange,提问作者Cristian Ceamatu
相关产品推荐
相关产品推荐

