利用canvas实现毛笔字帖(二)

时间:2022-06-06
本文章向大家介绍利用canvas实现毛笔字帖(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上接javascript

上接 利用canvas实现毛笔字帖(一)

二、 根据功能需要完善代码

2. 第2部分write.js 第二部分决定先介绍write部分,因为controller部分必须要结合write部分才能看到效果。 针对write.js部分,前面有介绍,是用来实现通过鼠标(手指)写字的核心部分。 分析一下要做的事情。

  • 当鼠标按下(mousedown)时,我们认为是毛笔落在纸上了。
  • 当鼠标移动(mousemove)时,我们认为是毛笔在移动。
  • 当鼠标放开(mouseup)时,我们认为是毛笔收起了。
  • 当鼠标移出canvas范围时,我们认为毛笔移出字帖的范围了,也认为是收笔了。
1. 事件监听设置

针对以上的想法,我们可以写下以下的代码了。 主要是一系列的事件监听,兼顾到pc端的 mouse事件 和移动端的 touch事件。 其中,我们在代码里设置了isWriting属性用作状态标识,只有在 isWriting==true 的情况下,writing()方法才会实行

var write = {
        canvas: null, //html中的canvas对象,主要标签
        context: null, //canvas对象获取的context,用于绘图
        isWriting: false,//状态属性,标识是否正在下笔写字

        init: function (canvas) {
            this.canvas = canvas;//接收外界canvas,赋值给自己的属性``canvas``,在下面的其他方法中需要用到
            this.context = canvas.getContext('2d');//通过canvas获取context,赋值给自己的属性``context``,在下面的其他方法中需要用到

            //事件监听
            this.bindEvent();
        },

        bindEvent: function () {
            var self = this;
            //pc端
            //下笔
            self.canvas.onmousedown = function (e) {
                e.preventDefault();
                self.startWrite(self.getCo(e.clientX, e.clientY))
            };

            //移动,在鼠标移动期间不断执行。
            self.canvas.onmousemove = function (e) {
                e.preventDefault();
                if(self.isWriting){
                    self.writing(self.getCo(e.clientX, e.clientY));
                }
            };

            //收笔
            self.canvas.onmouseup = function (e) {
                e.preventDefault();
                if(self.isWriting) {
                    self.endWrite();
                }
            };

            //出界
            self.canvas.onmouseout = function (e) {
                e.preventDefault();
                if(self.isWriting) {
                    self.endWrite();
                }
            };

            //手机端
            //下笔
            self.canvas.addEventListener('touchstart', function (e) {
                e.preventDefault();
                var touch = e.touches[0];//第一个触摸手指
                self.startWrite(self.getCo(touch.clientX, touch.clientY))
            });

            //移动
            self.canvas.addEventListener('touchmove', function (e) {
                e.preventDefault();
                if(self.isWriting){
                    var touch = e.touches[0];
                    self.writing(self.getCo(touch.clientX, touch.clientY));
                }
            });

            //收笔
            self.canvas.addEventListener('touchend', function (e) {
                e.preventDefault();
                self.endWrite();
            })
        },
        //描绘区
        startWrite: function (co) {
            this.isWriting = true;
            //···
        },
        writing: function (co) {
            //···
        },
        endWrite: function(){
            this.isWriting = false;
        },
    };

同样的,在外界通过调用write.jsinit()方法完成其初始化,并让其运行起来,如下。

var canvas = document.getElementById('cnavas');
write.init(canvas);
2. 完善绘制操作的方法

下面我们来完善绘制操作的方法。startWrite writing endWrite 分别表示开始下笔,正在写字,结束收笔。 其实,实际上要在canvas是实现写字效果, 就是针对鼠标移动,不断的根据鼠标上一次移动的位置和下一次移动的位置,调用canvas的stroke() api 画密密麻麻衔接的线段,看起来就像沿着鼠标描线一样。 为此,我们需要设置一些辅助的属性和方法。如下。

var write = {
    //···
    lastX: 0,//画笔上次停留坐标x轴值
    lastY: 0,//画笔上次停留坐标y轴值
    //···
    //辅助函数区
    //根据传进来的鼠标坐标,return 当前点相对于canvas(米字格字帖)的左上角的左边位置。
    getCo: function (clientX, clientY) {
        var canvasLT = this.canvas.getBoundingClientRect();
        return {x: clientX - canvasLT.left, y: clientY - canvasLT.top};
    }
}

如此,我们可以开始写下动作操作了

//描绘区
    startWrite: function (co) {
        this.isWriting = true;
        this.lastX = co.x;
        this.lastY = co.y;
    },
    writing: function (co) {
            this.context.beginPath();
            this.context.moveTo(this.lastX, this.lastY);
            this.context.lineTo(co.x, co.y);
            this.context.stroke();

            //维护更新鼠标的上一次位置为当前位置,供下一次writing使用
            this.lastX = co.x;
            this.lastY = co.y;
        }
    },
    endWrite: function(){
        this.isWriting = false;
    },

在我们的html文件中做以下调用,一个简单的写字笔效果已经出来了,太神奇了

<script src="js/paper.js"></script>
<script src="js/write.js"></script>
<script>
    window.onload = function () {
        var canvas = document.getElementById('canvas');
        paper.init(canvas);
        write.init(canvas);
    };
</script>

目前效果已经有了,但是,很明显,笔画线条太细,不是我们想要的毛笔字,我们先尝试给一个比较粗的笔画试一试, 设置lineWidth为canvas宽度的1/20

this.context.lineWidth = canvas.width/20;

效果如图:

很多毛刺的艺术感,这是因为我们画了很多不同方向的直线,而无法衔接造成的,所以,我们要对线段做平滑处理

//描边处理,使笔画圆滑
    this.context.lineCap = 'round';
    this.context.lineJoin = 'round';

效果如图:

毛笔字感觉出来了!!!

3. 根据移笔速度处理线条粗细

毛笔字感觉出来了,但是,我们仔细观察会发现,字体的线条大小是一致的,我们要精益求精,对线条粗细做一些处理。 我们知道,毛笔字的线条粗细跟下笔的力度,速度等有关系,在浏览器中,我们目前没办法获知下笔力度,但是可以计算出速度 v=s/t。 所以,我们用速度来计算线条粗细。

根据公式 v=s/t,我们需要知道 st才能拿到v,s可以根据坐标计算,t则要用到Date()。 我们需要用到新的辅助属性和方法,如下

var write = {
//···
    lineWidthMax: 0, //画笔最大粗细
    lineWidthMin: 1, //画笔最小粗细
    lastTime: 0, //上次笔时间

    init: function(canvas){
        //this.context.lineWidth = canvas.width/20;
        this.lineWidthMax = canvas.width/20;
    },

//···
//描绘区
//修改startWrite,主要增加了时间
    startWrite: function (co) {
        this.isWriting = true;
        this.lastX = co.x;
        this.lastY = co.y;
        //设置当前时间
        this.lastTime = new Date().getTime();
    },
//修改writing,主要增加了时间计算和笔画粗细设置
    writing: function (co) {
        var curTime = new Date().getTime();//获取当前时间戳(毫秒级)
        if(curTime != this.lastTime){
            this.context.beginPath();
            //设置笔画宽度,根据getLineWidth计算出来
            this.context.lineWidth = this.getLineWidth(this.getS(this.lastX, this.lastY, co.x, co.y), curTime - this.lastTime);
            this.context.moveTo(this.lastX, this.lastY);
            this.context.lineTo(co.x, co.y);
            this.context.stroke();

            //维护更新鼠标的上一次位置为当前位置,供下一次writing使用
            this.lastX = co.x;
            this.lastY = co.y;
            //维护更新鼠标的上一次写笔时间为当前时间,供下一次writing使用
            this.lastTime = curTime;
        }
    },
 //辅助函数区
 //新增下面函数
    //根据坐标计算距离
    getS: function (sx, sy, ex, ey) {
        return Math.sqrt((ex - sx)*(ex - sx) + (ey - sy)*(ey - sy))
    },
    //根据 距离s 和 时间t 计算笔画粗细
    getLineWidth: function (s, t) {
        var v = s/t;
        var resultLineWidth = 0;
        if(v < 0.1){//速度到达某个最小值时,笔画很大,这里的8和0.1是我自己随便调的数,有兴趣的朋友可以自己找到更合理的方式和数值
            resultLineWidth = this.lineWidthMax;
        }
        else if(v >8){//速度到达某个最大值时,笔画很小
            resultLineWidth = this.lineWidthMin;
        }
        else{ // 根据速度赋予线条宽度值,速度比例和笔画宽度比例的计算
            resultLineWidth = this.lineWidthMax - (v-0.1)/(8-0.1)*(this.lineWidthMax - this.lineWidthMin)
        }

        return resultLineWidth;
    }
};

这时,我们再看看效果图

oh, 不,笔画粗细虽然转换了,但是有时会转换得非常突然,这是因为前一刻和下一刻的速度相差很大(笔记计算机响应mousemove的时间我们没办法控制), 我们必须继续优化。 我们的想法是,下一次绘制的笔画粗细必须收到上一次笔画粗细的控制,我做了下面的修改。

var write = {
    //···
    lastLineWidth: 0,
    init: function (canvas) {
        //···
        this.lastLineWidth = this.lineWidthMax /2;
        //···
    }
    //···
    //描绘区
    //修改startWrite,主要增加线条宽度
    startWrite: function (co) {
        this.isWriting = true;
        this.lastX = co.x;
        this.lastY = co.y;
        //设置当前时间
        this.lastTime = new Date().getTime();
        //设置落笔的最近线条宽度 lastWidth
        this.lastLineWidth = this.lineWidthMax /2;
    },
    //辅助函数区
    //修改getLineWidth,优化线条宽度,受lastLineWidth限制
    getLineWidth: function (s, t) {
        var v = s/t;
        var resultLineWidth = 0;
        if(v < 0.1){
            resultLineWidth = this.lineWidthMax;
        }
        else if(v >8){
            resultLineWidth = this.lineWidthMin;
        }
        else{ // 根据速度赋予线条宽度值
            resultLineWidth = this.lineWidthMax - (v-0.1)/(8-0.1)*(this.lineWidthMax - this.lineWidthMin)
        }

        //防止变化突然,使线条平滑,借鉴上次线条粗细取值
        resultLineWidth = this.lastLineWidth * 3/5 + resultLineWidth * 2/5;
        this.lastLineWidth = resultLineWidth;

        return resultLineWidth;
    }
};

最终效果完成。大家可以试一试了。我对粗细的把控不是很好,大家可以发挥自己的才智想想怎么做更加真实的模范。

完整的代码 write.js

var write = {
        canvas: null, //html中的canvas对象,主要标签
        context: null, //canvas对象获取的context,用于绘图
        isWriting: false,//是否正在下笔写字
        lineWidthMax: 0, //画笔最大粗细
        lineWidthMin: 1, //画笔最小粗细

        lastX: 0,//画笔上次停留位置
        lastY: 0,
        lastTime: 0, //上次笔时间
        lastLineWidth: 0,

        init: function (canvas) {

            this.canvas = canvas;
            this.context = this.canvas.getContext('2d');

            this.lineWidthMax = canvas.width/20;
            this.lastLineWidth = this.lineWidthMax /2;
            //描边处理,使笔画圆滑
            this.context.lineCap = 'round';
            this.context.lineJoin = 'round';

            //事件绑定
            this.bindEvent();
        },

        bindEvent: function () {
            var self = this;
            //pc端
            //下笔
            self.canvas.onmousedown = function (e) {
                e.preventDefault();
                self.startWrite(self.getCo(e.clientX, e.clientY))
            };

            //移动
            self.canvas.onmousemove = function (e) {
                e.preventDefault();
                if(self.isWriting){
                    self.writing(self.getCo(e.clientX, e.clientY));
                }
            };

            //收笔
            self.canvas.onmouseup = function (e) {
                e.preventDefault();
                self.endWrite();
            };

            //出界
            self.canvas.onmouseout = function (e) {
                e.preventDefault();
                if(self.isWriting) {
                    self.endWrite();
                }
            };

            //下笔
            self.canvas.addEventListener('touchstart', function (e) {
                e.preventDefault();
                var touch = e.touches[0];
                self.startWrite(self.getCo(touch.clientX, touch.clientY))
            });

            //移动
            self.canvas.addEventListener('touchmove', function (e) {
                e.preventDefault();
                if(self.isWriting){
                    var touch = e.touches[0];
                    self.writing(self.getCo(touch.clientX, touch.clientY));
                }
            });

            //收笔
            self.canvas.addEventListener('touchend', function (e) {
                e.preventDefault();
                self.endWrite();
            })
        },

        //描绘区
        startWrite: function (co) {
            this.isWriting = true;
            this.lastX = co.x;
            this.lastY = co.y;
            this.lastTime = new Date().getTime();
            this.lastLineWidth = this.lineWidthMax /2;
        },
        writing: function (co) {
            var curTime = new Date().getTime();
            if(curTime != this.lastTime){
                this.context.beginPath();
                this.context.lineWidth = this.getLineWidth(this.getS(this.lastX, this.lastY, co.x, co.y), curTime - this.lastTime);
                this.context.moveTo(this.lastX, this.lastY);
                this.context.lineTo(co.x, co.y);
                this.context.stroke();

                this.lastX = co.x;
                this.lastY = co.y;
                this.lastTime = curTime;
            }
        },

        endWrite: function(){
            this.isWriting = false;
        },

        //辅助函数区
        getCo: function (clientX, clientY) {
            var canvasLT = this.canvas.getBoundingClientRect();
            return {x: clientX - canvasLT.left, y: clientY - canvasLT.top};
        },
        getS: function (sx, sy, ex, ey) {
            return Math.sqrt((ex - sx)*(ex - sx) + (ey - sy)*(ey - sy))
        },
        getLineWidth: function (s, t) {
            var v = s/t;
            var resultLineWidth = 0;
            if(v < 0.1){
                resultLineWidth = this.lineWidthMax;
            }
            else if(v >8){
                resultLineWidth = this.lineWidthMin;
            }
            else{ // 根据速度赋予线条宽度值
                resultLineWidth = this.lineWidthMax - (v-0.1)/(8-0.1)*(this.lineWidthMax - this.lineWidthMin)
            }

            //防止变化突然,使线条平滑,借鉴上次线条粗细取值
            resultLineWidth = this.lastLineWidth * 3/5 + resultLineWidth * 2/5;

            this.lastLineWidth = resultLineWidth;
            return resultLineWidth;
        }
    };

这是绘制的第二部分,我们在接下来的一篇博客里再讲第三部分,请期待 利用canvas实现毛笔字帖(三), 跟大家一起将这一字帖的控制部件的功能完善起来,同时,将模块改造成 requirejs的形式