SOURCE

console 命令行工具 X clear

                    
>
console
Array.prototype.clone = function () {
    return [].concat(this);
    //或者 return this.concat();
}
class Point {
    constructor(x, y, time) {
        this.x = x;
        this.y = y;
        this.isControl = false;
        this.time = Date.now();
        this.lineWidth = 0;
        this.isAdd = false;
    }
}

class Line {
    constructor() {
        this.points = new Array();
        this.changeWidthCount = 0;
        this.lineWidth = 10;
    }
}
class HandwritingSelf {

    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext("2d")
        // this.points = new Array();
        this.line = new Line();
        this.pointLines = new Array();//Line数组
        this.k = 0.5;
        this.begin = null;
        this.middle = null;
        this.end = null;
        this.preTime = null;
        this.lineWidth = 8;
        this.isDown = false;
    }
    down(x, y) {
        this.isDown = true;
        this.line = new Line();
        this.line.lineWidth = this.lineWidth;
        let currentPoint = new Point(x, y, Date.now());
        this.addPoint(currentPoint);

        this.preTime = Date.now();
    }
    move(x, y) {
        // console.log("move:",x,y)
        if (this.isDown) {
            let currentPoint = new Point(x, y, Date.now())
            this.addPoint(currentPoint);
            this.draw();
        }
    }
    up(x, y) {
        // if (e.touches.length > 0) {
        let currentPoint = new Point(x, y, Date.now())
        this.addPoint(currentPoint);
        // }
        this.draw(true);

        this.pointLines.push(this.line);

        this.begin = null;
        this.middle = null;
        this.end = null;
        this.isDown = false;
    }
    draw(isUp = false) {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.ctx.strokeStyle = "rgba(255,20,87,1)";


        //绘制不包含this.line的线条
        this.pointLines.forEach((line, index) => {
            let points = line.points;
            this.ctx.beginPath();
            this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
            this.ctx.fill();
            this.ctx.beginPath();
            this.ctx.moveTo(points[0].x, points[0].y);
            let lastW = line.lineWidth;
            this.ctx.lineWidth = line.lineWidth;
            this.ctx.lineJoin = "round";
            this.ctx.lineCap = "round";
            let minLineW = line.lineWidth / 4;
            let isChangeW = false;

            let changeWidthCount = line.changeWidthCount;
            for (let i = 1; i <= points.length; i++) {
                if (i == points.length) {
                    this.ctx.stroke();
                    break;
                }
                if (i > points.length - changeWidthCount) {
                    if (!isChangeW) {
                        this.ctx.stroke();//将之前的线条不变的path绘制完
                        isChangeW = true;
                        if (i > 1 && points[i - 1].isControl)
                            continue;
                    }
                    let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
                    points[i - 1].lineWidth = w;
                    this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍
                    // this.ctx.strokeStyle = "rgba("+Math.random()*255+","+Math.random()*255+","+Math.random()*255+",1)";
                    this.ctx.lineWidth = w;
                    this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点
                    this.ctx.lineTo(points[i].x, points[i].y);
                    this.ctx.stroke();//将之前的线条不变的path绘制完
                } else {
                    if (points[i].isControl && points[i + 1]) {
                        this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
                    } else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制
                    } else
                        this.ctx.lineTo(points[i].x, points[i].y);
                }
            }
        })

      //绘制this.line线条
        let points;
        if (isUp)
            points = this.line.points;
        else
            points = this.line.points.clone();
        //当前绘制的线条最后几个补点 贝塞尔方式增加点
        let count = 0;
        let insertCount = 0;
        let i = points.length - 1;
        let endPoint = points[i];
        let controlPoint;
        let startPoint;
        while (i >= 0) {
            if (points[i].isControl == true) {
                controlPoint = points[i];
                count++;
            } else {
                startPoint = points[i];
            }
            if (startPoint && controlPoint && endPoint) {//使用贝塞尔计算补点
                let dis = this.z_distance(startPoint, controlPoint) + this.z_distance(controlPoint, endPoint);
                let insertPoints = this.BezierCalculate([startPoint, controlPoint, endPoint], Math.floor(dis / 6) + 1);
                insertCount += insertPoints.length;
                var index = i;//插入位置
                // 把insertPoints 变成一个适合splice的数组(包含splice前2个参数的数组) 
                insertPoints.unshift(index, 1);
                Array.prototype.splice.apply(points, insertPoints);

                //补完点后
                endPoint = startPoint;
                startPoint = null;
            }
            if (count >= 6)
                break;
            i--;
        }
        //确定最后线宽变化的点数
        let changeWidthCount = count + insertCount;
        if (isUp)
            this.line.changeWidthCount = changeWidthCount;
      
        //制造椭圆头
        this.ctx.fillStyle = "rgba(255,20,87,1)"
        this.ctx.beginPath();
        this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
        this.ctx.fill();

        this.ctx.beginPath();
        this.ctx.moveTo(points[0].x, points[0].y);
        let lastW = this.line.lineWidth;
        this.ctx.lineWidth = this.line.lineWidth;
        this.ctx.lineJoin = "round";
        this.ctx.lineCap = "round";
        let minLineW = this.line.lineWidth / 4;
        let isChangeW = false;
        for (let i = 1; i <= points.length; i++) {
            if (i == points.length) {
                this.ctx.stroke();
                break;
            }
            //最后的一些点线宽变细
            if (i > points.length - changeWidthCount) {
                if (!isChangeW) {
                    this.ctx.stroke();//将之前的线条不变的path绘制完
                    isChangeW = true;
                    if (i > 1 && points[i - 1].isControl)
                        continue;
                }

                //计算线宽
                let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
                points[i - 1].lineWidth = w;
                this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍
                // this.ctx.strokeStyle = "rgba(" + Math.random() * 255 + "," + Math.random() * 255 + "," + Math.random() * 255 + ",0.5)";
                this.ctx.lineWidth = w;
                this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点
                this.ctx.lineTo(points[i].x, points[i].y);
                this.ctx.stroke();//将之前的线条不变的path绘制完
            } else {
                if (points[i].isControl && points[i + 1]) {
                    this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
                } else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制
                } else
                    this.ctx.lineTo(points[i].x, points[i].y);
            }
        }
    }

    addPoint(p) {
        if (this.line.points.length >= 1) {
            let last_point = this.line.points[this.line.points.length - 1]
            let distance = this.z_distance(p, last_point);
            if (distance < 10) {
                return;
            }
        }

        if (this.line.points.length == 0) {
            this.begin = p;
            p.isControl = true;
            this.pushPoint(p);
        } else {
            this.middle = p;
            let controlPs = this.computeControlPoints(this.k, this.begin, this.middle, null);
            this.pushPoint(controlPs.first);
            this.pushPoint(p);
            p.isControl = true;

            this.begin = this.middle;
        }
    }

    addOtherPoint(p1, p2, w1, w2) {

        let otherPoints = new Array();
        let dis = this.z_distance(p1, p2);
        if (dis >= 25) {
            otherPoints.push(p1);
            let insertPCount = Math.floor(dis / 20);
            for (let j = 0; j < insertPCount; j++) {
                let insertP = new Point(p1.x + (j + 1) / (insertPCount + 1) * (p2.x - p1.x), p1.y + (j + 1) / (insertPCount + 1) * (p2.y - p1.y))
                insertP.isAdd = true;
                otherPoints.push(insertP);
            }
            otherPoints.push(p2);
        }
        let count = otherPoints.length;
        if (count > 0) {
            console.log("addOtherPoint")
            debugger
            let diffW = (w2 - w1) / (count - 1);
            for (let i = 1; i < count; i++) {
                let w = w1 + diffW * i;
                this.ctx.beginPath();
                this.ctx.lineWidth = w;
                this.ctx.moveTo(otherPoints[i - 1].x, otherPoints[i - 1].y);
                this.ctx.lineTo(otherPoints[i].x, otherPoints[i].y)
                this.ctx.stroke();
            }
        }
        return otherPoints
    }
    pushPoint(p) {
        //排除重复点
        if (this.line.points.length >= 1 && this.line.points[this.line.points.length - 1].x == p.x && this.line.points[this.line.points.length - 1].y == p.y)
            return;
        this.line.points.push(p);
    }
    computeControlPoints(k, begin, middle, end) {
        if (k > 0.5 || k <= 0)
            return;

        let diff1 = new Point(middle.x - begin.x, middle.y - begin.y)
        let diff2 = null;
        if (end)
            diff2 = new Point(end.x - middle.x, end.y - middle.y)

        // let l1 = (diff1.x ** 2 + diff1.y ** 2) ** (1 / 2)
        // let l2 = (diff2.x ** 2 + diff2.y ** 2) ** (1 / 2)

        let first = new Point(middle.x - (k * diff1.x), middle.y - (k * diff1.y))
        let second = null;
        if (diff2)
            second = new Point(middle.x + (k * diff2.x), middle.y + (k * diff2.y))
        return { first: first, second: second }
    }
    // W_current = 
    //   W_previous + min( abs(k*s - W_previous), distance * K_width_unit_change) (k * s-W_previous) >= 0
    //   W_previous - min( abs(k*s - W_previous), distance * K_width_unit_change) (k * s-W_previous) < 0
    //   W_current       当前线段的宽度
    //   W_previous    与当前线条相邻的前一条线段的宽度
    //   distance   	      当前线条的长度
    //   w_k        	设定的一个固定阈值,表示:单位距离内, 笔迹的线条宽度可以变化的最大量. 
    //   distance * w_k     即为当前线段的长度内, 笔宽可以相对于前一条线段笔宽的基础上, 最多能够变宽或者可以变窄多少.
    z_linewidth(b, e, bwidth, step) {

        if (e.time == b.time)
            return bwidth;

        let max_speed = 2.0;
        let d = this.z_distance(b, e);
        let s = d / (e.time - b.time);//计算速度
        console.log("s", e.time - b.time, s)
        s = s > max_speed ? max_speed : s;

        // let w = (max_speed - s) / max_speed;
        let w = 0.5 / s;

        let max_dif = d * step;
        console.log(w, bwidth, max_dif)
        if (w < 0.05) w = 0.05;
        if (Math.abs(w - bwidth) > max_dif) {
            if (w > bwidth)
                w = bwidth + max_dif;
            else
                w = bwidth - max_dif;
        }
        // printf("d:%.4f, time_diff:%lld, speed:%.4f, width:%.4f\n", d, e.t-b.t, s, w);
        return w;
    }
    z_distance(b, e) {
        return Math.sqrt(Math.pow(e.x - b.x, 2) + Math.pow(e.y - b.y, 2));
    }
    BezierCalculate(poss, precision) {

        //维度,坐标轴数(二维坐标,三维坐标...)
        let dimersion = 2;

        //贝塞尔曲线控制点数(阶数)
        let number = poss.length;

        //控制点数不小于 2 ,至少为二维坐标系
        if (number < 2 || dimersion < 2)
            return null;

        let result = new Array();

        //计算杨辉三角
        let mi = new Array();
        mi[0] = mi[1] = 1;
        for (let i = 3; i <= number; i++) {

            let t = new Array();
            for (let j = 0; j < i - 1; j++) {
                t[j] = mi[j];
            }

            mi[0] = mi[i - 1] = 1;
            for (let j = 0; j < i - 2; j++) {
                mi[j + 1] = t[j] + t[j + 1];
            }
        }

        //计算坐标点
        for (let i = 0; i < precision; i++) {
            let t = i / precision;
            let p = new Point(0, 0);
            p.isAdd = true;
            result.push(p);
            for (let j = 0; j < dimersion; j++) {
                let temp = 0.0;
                for (let k = 0; k < number; k++) {
                    temp += Math.pow(1 - t, number - k - 1) * (j == 0 ? poss[k].x : poss[k].y) * Math.pow(t, k) * mi[k];
                }
                j == 0 ? p.x = temp : p.y = temp;
            }
        }

        return result;
    }
}


//以下代码为鼠标移动事件部分
        let handwriting = new HandwritingSelf(document.getElementById("canvasId"))
        // document.ontouchstart = document.onmousedown
        document.onpointerdown = function (e) {
            if (e.type == "touchstart")
                handwriting.down(e.touches[0].pageX, e.touches[0].pageY);
            else
                handwriting.down(e.x, e.y);
        }
        // document.ontouchmove = document.onmousemove
        document.onpointermove = function (e) {
            if (e.type == "touchmove")
                handwriting.move(e.touches[0].pageX, e.touches[0].pageY);
            else
                handwriting.move(e.x, e.y);
        }
        // document.ontouchend = document.onmouseup
        document.onpointerup = function (e) {
            if (e.type == "touchend")
                handwriting.up(e.touches[0].pageX, e.touches[0].pageY);
            else
                handwriting.up(e.x, e.y);
        }
<!-- https://github.com/tclyjy/handwriting-weapp -->
<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <title> canvas 手写毛笔字效果 </title>
    <style type="text/css">
        #canvasId {
            background-color: #FFFFcc;
        }
    </style>
</head>

<body style="touch-action:none">

    <canvas id="canvasId" width="800" height="720"></canvas><br />
    <script>
        // window.onload = () => {

        // }
    </script>
</body>

</html>