最近在做一款教育类的直播系统,分为学生版本和教师版本,在制作教师版本的时候,涉及到白板模块,版本模块就有标记,曲线绘制,椭圆绘制,直线,矩形等,就类似于截图那一套。在最开始的鼠标绘制能很好的适配,但是由于教师的习惯并不是使用鼠标,而是使用速写版来进行写,画操作,因此这就涉及到压感,曲线圆滑度,曲线流畅度,曲线清晰度等一系列问题。本文主要讨论一下曲线原画度的几种方式。
一、采用moveTo+lineTo进行绘制曲线
为了大家测试,直接贴出单个实例的源码,方便大家测试。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas曲线绘制</title> <style> * { padding: 0; margin: 0; } #canvas { width: 100%; height: 100%; touch-action: none; position: fixed; top: 0; left: 0; z-index: 999; } </style></head><body> <canvas id="canvas" width="1920" height="720"></canvas></body><script> let touchStart = false, startPoint = null; let canvas = document.querySelector('#canvas'); let ctx = canvas.getContext('2d'); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = `rgba(0,0,0,255)`; canvas.addEventListener('pointerdown', e => { touchStart = true; startPoint = { x: e.offsetX, y: e.offsetY }; }); canvas.addEventListener('pointermove', e => { if (!touchStart ) return; setContextLineWidth(ctx, e.pressure || 0.65); coustomDrawLine(ctx, startPoint.x, startPoint.y, e.offsetX, e.offsetY); startPoint = { x: e.offsetX, y: e.offsetY }; }); canvas.addEventListener('pointerup', e => { if (touchStart) touchStart = false; }); function coustomDrawLine(ctx, x, y, a, b) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(a, b); ctx.stroke(); ctx.closePath(); } function setContextLineWidth(ctx, pressure) { ctx.lineWidth = (pressure * 20 + 1) * 0.25; }</script></html>
上面的事件使用的是pointer的相关事件,这类事件整合了mouse以及touch的相关事件,基本类似于mouse事件,兼容了touch的部分事件,由于可以获取到压感和基本能兼容其他,所以采用pointer,更多pointer解释可以参考pointerevent。
效果图如下:
二、采用moveTo+二次贝塞尔曲线进行绘制
第二种方式是采用二次贝塞尔进行绘制。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas曲线绘制</title> <style> * { padding: 0; margin: 0; } #canvas { width: 100%; height: 100%; touch-action: none; position: fixed; top: 0; left: 0; z-index: 999; } </style></head><body> <canvas id="canvas" width="1920" height="720"></canvas></body><script> let points = [], mouseDown = false, startPoint = null; let canvas = document.querySelector('#canvas'); let ctx = canvas.getContext('2d'); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = `rgba(0,0,0,255)`; canvas.addEventListener('pointerdown', e => { mouseDown = true; startPoint = { x: e.offsetX, y: e.offsetY }; addPoint(startPoint); }) canvas.addEventListener('pointermove', e => { if (!mouseDown) return; setContextLineWidth(ctx, e.pressure || 0.65); updateCurveDrawing(ctx, { x: e.offsetX, y: e.offsetY }); }) canvas.addEventListener('pointerup', e => { if (mouseDown) { points = []; mouseDown = false; } }) function addPoint(params) { points.push({ ...params }); } function updateCurveDrawing(ctx, params) { addPoint(params); let ccp = computedControlPoint(points); if (ccp) { drawingCurve(ctx, startPoint, ccp.controlPoint, ccp.endPoint); startPoint = ccp.endPoint; } } function computedControlPoint(points) { if (points.length > 3) { const lastTwoPoints = points.slice(-2); const controlPoint = lastTwoPoints[0]; const endPoint = lastTwoPoints[1]; return { controlPoint, endPoint } } return false; } function setContextLineWidth(ctx, pressure) { ctx.lineWidth = (pressure * 20 + 1) * 0.25; } function drawingCurve(ctx, startPoint, controlPoint, endPoint) { ctx.beginPath(); ctx.moveTo(startPoint.x, startPoint.y); ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); ctx.stroke(); ctx.closePath(); }</script></html>
效果图如下:
三、采用moveTo+三次贝塞尔曲线进行绘制
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas曲线绘制</title> <style> * { padding: 0; margin: 0; } #canvas { width: 100%; height: 100%; touch-action: none; position: fixed; top: 0; left: 0; z-index: 999; } </style></head><body> <canvas id="canvas" width="1920" height="720"></canvas></body><script> let points = [], tension = 0.65, touchStart = false; let canvas = document.querySelector('#canvas'); let ctx = canvas.getContext('2d'); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = `rgba(66, 118, 255,255)`; canvas.addEventListener('pointerdown', e => { touchStart = true; points.push({ x: e.offsetX, y: e.offsetY }) }) canvas.addEventListener('pointermove', e => { if (!touchStart) return; points.push({ x: e.offsetX, y: e.offsetY }) setContextLineWidth(ctx, e.pressure || 0.65); drawCurve(points, 0.5); }) canvas.addEventListener('pointerup', e => { if (touchStart) { points = []; touchStart = false; } }) function drawCurve(points, tension) { ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); var t = (tension != null) ? tension : 1; for (var i = 0; i < points.length - 1; i++) { var p0 = (i > 0) ? points[i - 1] : points[0]; var p1 = points[i]; var p2 = points[i + 1]; var p3 = (i != points.length - 2) ? points[i + 2] : p2; var cp1x = p1.x + (p2.x - p0.x) / 6 * t; var cp1y = p1.y + (p2.y - p0.y) / 6 * t; var cp2x = p2.x - (p3.x - p1.x) / 6 * t; var cp2y = p2.y - (p3.y - p1.y) / 6 * t; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y); } ctx.stroke(); ctx.closePath(); points.splice(0, 1); } function setContextLineWidth(ctx, pressure) { ctx.lineWidth = (pressure * 20 + 1) * 0.25; }</script></html>
效果图如下:
以上就是实现曲线的三种方式,具体的圆滑度效果,需要大家通过手绘板去体验。更高级的圆滑算法欢迎大家讨论交流,如文章有错误之处,欢迎大家指出。