zoukankan      html  css  js  c++  java
  • canvas小画板——笔锋效果实现

    效果结尾处可验收。

    画线准备

    准备一个canvas

     <canvas id="canvasId" width="1000" height="800"></canvas>

    使用pointer事件监听,落笔,拖拽,收笔。

           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.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.onpointerup = function (e) {
                if (e.type == "touchend")
                    handwriting.up(e.touches[0].pageX, e.touches[0].pageY);
                else
                    handwriting.up(e.x, e.y);
            }

    主要的逻辑在Handwritinglff 上,存储了当前绘制中的线条的所有点集合,所有绘制过的线条集合pointLines 。

    class Handwritinglff {
    
        constructor(canvas) {
            this.canvas = canvas;
            this.ctx = canvas.getContext("2d")
            this.line = new Line();
            this.pointLines = new Array();//Line数组
            this.k = 0.5;
            this.begin = null;
            this.middle = null;
            this.end = null;
    this.lineWidth = 10;
            this.isDown = false;
        }

    down事件的时候初始化当前绘制线条line;

    move事件的时候将点加入到当前线条line,并开始绘制

    up的时候将点加入绘制线条,并绘制完整一条线。

    需要注意的点:

    加入点的时候,距离太近的点不需要重复添加;

    怎么形成笔锋效果呢

    很简单!就是在一条线段的最后几个点的lineWidth不断减小,我们这里选用了最后6个点,如果只选用六个阶梯变化,效果是很难看的,会看到一节节明显的线条变细的过程,如下图:

    所以我们有个关键的补点过程,我们会再每6个像素之间补一个点,根据线条粗细变化的范围和最后计算出来的点数,就可以知道每两点连线lineWidth的粗细。

    这里的补点过程我们用到了在贝塞尔曲线上补点的算法。具体不明白的可以留言哈

    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);
                    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;
                    }
                    p.x = this.toDecimal(p.x);
                    p.y = this.toDecimal(p.y);
                }
        
                return result;
            }

      部分代码如下;

        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;
            }
        }
    addPoint
    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);
                    insertPoints.splice(0, 1);
                    insertCount += insertPoints.length;
                    var index = i;//插入位置
                    // 把arr2 变成一个适合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);
                }
            }
        }
    draw

     最终效果

    动手试试:拖拽写字即可 

     github地址:https://github.com/fangsmile/penlineHandWriting 

    相关文章:

    平滑曲线

    实现蜡笔荧光笔效果

    实现笔锋效果

    画笔性能优化

    清除canvas画布内容--点擦除+线擦除

  • 相关阅读:
    time模块
    time模块,计算时间差
    re模块
    Python之常用文件操作
    Django运行错误常见问题及解决方法1
    用JetBrains PyCharm 2017.2创建运行Django程序
    wsgi Python的WEB框架
    django模块安装环境变量
    Django
    DOM
  • 原文地址:https://www.cnblogs.com/fangsmile/p/14324460.html
Copyright © 2011-2022 走看看