如何用JS实现类似Excalidraw的SVG激光指针?贝塞尔平滑效果不佳求助
优化SVG鼠标轨迹平滑效果(类似Excalidraw/谷歌幻灯片激光指针)
我想用原生JavaScript实现类似Excalidraw或谷歌幻灯片的SVG激光指针效果,目前通过SVG path元素创建鼠标轨迹,采用贝塞尔曲线平滑处理,但效果不理想。当前逻辑是存储最近20个鼠标坐标点,基于这些点生成SVG路径,代码如下:
<svg id="mouse-trail" width="100%" height="100vh"></svg>
.paths { stroke-width: 1.5; }
const smoothing = 0.10; function line(pointA, pointB) { var lengthX = pointB[0] - pointA[0]; var lengthY = pointB[1] - pointA[1]; var length = Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)); var angle = Math.atan2(lengthY, lengthX); return { length: length, angle: angle }; } function controlPoint(current, previous, next, reverse) { var p = previous || current; var n = next || current; var o = line(p, n); var angle = o.angle + (reverse ? Math.PI : 0); var length = o.length * smoothing; var x = current[0] + Math.cos(angle) * length; var y = current[1] + Math.sin(angle) * length; return [x, y]; } function bezierCommand(point, i, points) { var cps = controlPoint(points[i - 1], points[i - 2], point); var cpe = controlPoint(point, points[i - 1], points[i + 1], true); var command = "C " + cps[0] + "," + cps[1] + " " + cpe[0] + "," + cpe[1] + " " + point[0] + "," + point[1]; return command; } function svgPath(points, command) { var d = ""; for (var i = 0; i < points.length; i++) { var point = points[i]; if (i === 0) { d += "M " + point[0] + "," + point[1]; } else { d += " " + command(point, i, points); } } return '<path d="' + d + '" fill="none" stroke="red" class="paths"/>'; } var lastMousePoints = []; const svg = document.querySelector('#mouse-trail') function addPoint(x, y) { lastMousePoints.push([x, y]); if (lastMousePoints.length > 20) { lastMousePoints.shift(); svg.innerHTML = svgPath(lastMousePoints, bezierCommand) } else if (lastMousePoints.length === 2) { svg.innerHTML = svgPath(lastMousePoints, bezierCommand) } else if (lastMousePoints.length > 2) { svg.innerHTML = svgPath(lastMousePoints, bezierCommand) } } document.addEventListener('mousemove', function(e) { addPoint(e.pageX, e.pageY); });
一、贝塞尔曲线平滑优化方案
1. 动态调整平滑系数与控制点
固定平滑系数易导致曲线僵硬或过度平滑,可根据相邻点距离动态调整:
// 替换原controlPoint函数 function controlPoint(current, previous, next, reverse) { const p = previous || current; const n = next || current; // 计算当前点与前后点的距离,动态调整平滑程度 const distToPrev = Math.hypot(current[0] - p[0], current[1] - p[1]); const distToNext = Math.hypot(current[0] - n[0], current[1] - n[1]); const avgDist = (distToPrev + distToNext) / 2; // 动态平滑系数,距离越小平滑程度越高 const dynamicSmoothing = Math.min(0.3, avgDist * 0.05); const o = line(p, n); const angle = o.angle + (reverse ? Math.PI : 0); const length = avgDist * dynamicSmoothing; return [ current[0] + Math.cos(angle) * length, current[1] + Math.sin(angle) * length ]; }
2. 简化路径生成逻辑
原代码重复判断点数组长度,可简化为统一处理:
function addPoint(x, y) { lastMousePoints.push([x, y]); if (lastMousePoints.length > 20) { lastMousePoints.shift(); } // 点数量≥2时才生成路径 if (lastMousePoints.length >= 2) { svg.innerHTML = svgPath(lastMousePoints, bezierCommand); } }
3. Catmull-Rom曲线转贝塞尔(更自然平滑)
Catmull-Rom曲线天生适合轨迹平滑,转换为SVG贝塞尔命令效果更自然:
// Catmull-Rom转贝塞尔的命令生成函数 function catmullToBezier(point, i, points) { if (i === 0) { return `L ${point[0]},${point[1]}`; } const p0 = points[i-2] || points[i-1]; const p1 = points[i-1]; const p2 = point; const p3 = points[i+1] || point; // 计算贝塞尔控制点 const cp1x = p1[0] + (p2[0] - p0[0]) / 6; const cp1y = p1[1] + (p2[1] - p0[1]) / 6; const cp2x = p2[0] - (p3[0] - p1[0]) / 6; const cp2y = p2[1] - (p3[1] - p1[1]) / 6; return `C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2[0]},${p2[1]}`; } // 调用时替换bezierCommand svg.innerHTML = svgPath(lastMousePoints, catmullToBezier);
4. 过滤冗余采样点
mousemove触发频率过高导致点密集,可通过距离判断过滤:
let lastX = 0, lastY = 0; const minDistance = 3; // 鼠标移动超过3像素才记录 document.addEventListener('mousemove', function(e) { const dx = Math.abs(e.pageX - lastX); const dy = Math.abs(e.pageY - lastY); if (dx >= minDistance || dy >= minDistance) { addPoint(e.pageX, e.pageY); lastX = e.pageX; lastY = e.pageY; } });
二、替代实现方案
1. SVG滤镜实现柔和拖尾
无需复杂曲线计算,用SVG滤镜实现激光指针的模糊拖尾效果:
<svg id="mouse-trail" width="100%" height="100vh"> <defs> <filter id="laserTrail"> <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/> <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5"/> </filter> </defs> <g filter="url(#laserTrail)"> <path id="trailPath" fill="none" stroke="red" stroke-width="1.5"/> </g> </svg>
const trailPath = document.querySelector('#trailPath'); const points = []; function updatePath() { let d = `M ${points[0][0]},${points[0][1]}`; for (let i = 1; i < points.length; i++) { d += ` L ${points[i][0]},${points[i][1]}`; } trailPath.setAttribute('d', d); } document.addEventListener('mousemove', (e) => { points.push([e.pageX, e.pageY]); if (points.length > 15) points.shift(); updatePath(); });
2. Canvas高效实现轨迹平滑
Canvas处理动态轨迹性能更优,用二次贝塞尔曲线实现平滑:
<canvas id="mouse-trail" width="100%" height="100vh"></canvas>
const canvas = document.querySelector('#mouse-trail'); const ctx = canvas.getContext('2d'); const points = []; // 适配窗口大小 function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resizeCanvas(); window.addEventListener('resize', resizeCanvas); document.addEventListener('mousemove', (e) => { points.push({x: e.pageX, y: e.pageY}); if (points.length > 20) points.shift(); drawTrail(); }); function drawTrail() { ctx.clearRect(0, 0, canvas.width, canvas.height); if (points.length < 2) return; ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); // 二次贝塞尔曲线连接点 for (let i = 1; i < points.length - 1; i++) { const midX = (points[i].x + points[i+1].x) / 2; const midY = (points[i].y + points[i+1].y) / 2; ctx.quadraticCurveTo(points[i].x, points[i].y, midX, midY); } ctx.lineTo(points[points.length-1].x, points[points.length-1].y); ctx.strokeStyle = 'red'; ctx.lineWidth = 1.5; ctx.stroke(); }
内容的提问来源于stack exchange,提问作者gowtham manikandan
相关产品推荐
相关产品推荐

