canvas小畫板——(3)筆鋒效果

方帥發表於2021-06-28

畫線準備

準備一個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;
        }

  部分程式碼如下;

canvas小畫板——(3)筆鋒效果
    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
canvas小畫板——(3)筆鋒效果
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

 最終效果

動手試試:拖拽寫字即可 

 

相關文章:

平滑曲線

實現蠟筆熒光筆效果

實現筆鋒效果

畫筆效能優化

清除canvas畫布內容--點擦除+線擦除

相關文章