Canvas手写板曲线平滑的解决方案

972 阅读1分钟

最近在做一款教育类的直播系统,分为学生版本和教师版本,在制作教师版本的时候,涉及到白板模块,版本模块就有标记,曲线绘制,椭圆绘制,直线,矩形等,就类似于截图那一套。在最开始的鼠标绘制能很好的适配,但是由于教师的习惯并不是使用鼠标,而是使用速写版来进行写,画操作,因此这就涉及到压感,曲线圆滑度,曲线流畅度,曲线清晰度等一系列问题。本文主要讨论一下曲线原画度的几种方式。

一、采用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>

效果图如下:

以上就是实现曲线的三种方式,具体的圆滑度效果,需要大家通过手绘板去体验。更高级的圆滑算法欢迎大家讨论交流,如文章有错误之处,欢迎大家指出。