利用canvas实现毛笔字帖(二)
上接javascript
二、 根据功能需要完善代码
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.js
的init()
方法完成其初始化,并让其运行起来,如下。
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
,我们需要知道 s
和t
才能拿到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
的形式