第一步:画一个米字格,先画一个矩形,再画中间的米字。
<script> window.onload = function(){ var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); canvas.width = 600; canvas.height = canvas.width; var color ="black"; //画出田字格 drawGrid(); //田字格 function drawGrid(){ context.save(); context.strokeStyle = "rgb(230,11,9)"; context.beginPath(); context.moveTo(3,3); context.lineTo(canvas.width - 3,3); context.lineTo(canvas.width - 3,canvas.height -3); context.lineTo(3,canvas.height -3); context.closePath(); context.lineWidth = 6; context.stroke(); context.beginPath(); context.moveTo(0,0); context.lineTo(canvas.width,canvas.height); context.moveTo(canvas.width,0); context.lineTo(0,canvas.height); context.moveTo(canvas.width/2,0); context.lineTo(canvas.width/2,canvas.height); context.moveTo(0,canvas.width/2); context.lineTo(canvas.width,canvas.height/2); context.lineWidth=1; context.stroke(); context.restore(); } }
</script>
第二步.鼠标的四种状态:onmousedown、onmouseup、onmouseout、onmousemove。
根据分析写字的主要操作操作在onmousemove事件下进行的。鼠标onmouseup、onmouseout的时候应该停止写字。鼠标onmousedown触发写字。
所以需先判断鼠标是否按下,如果按下则onmousemove开始执行写字操作。否则不执行。
var isMouseDown = false; //初始化鼠标是否按下 canvas.onmousedown=function(e){//鼠标按下 e.preventDefault(); isMouseDown = true; console.log("...onmousedown"); } canvas.onmouseup=function(e){//鼠标起来 e.preventDefault(); isMouseDown = false; console.log("...onmouseup"); } canvas.onmouseout=function(e){//鼠标离开 e.preventDefault(); isMouseDown = false; console.log("...onmouseout"); } canvas.onmousemove=function(e){//鼠标移动 e.preventDefault(); if(isMouseDown){ console.log("...onmousemove"); } }
第三步:在canvas中写字,相当于鼠标移动的时候不停地画直线。那么问题来了,画直线就需要获得起始坐标。e.clientX,e.clientY只能获得当前屏幕的坐标,而我们需要的是canvas里面的坐标。
接下来我们需要想办法得到canvas的坐标了。在canvas中有一个方法getBoundingClientRect()可以获得canvas距离屏幕的距离。
我们可以通过获得光标屏幕坐标 - canvas距离屏幕的距离来得到光标在canvas中的坐标。
但是怎么确认哪个是起始位置,哪个是结束位置呢?所以一开始会初始化一个一开始的位置,lastLoc = {x:0,y:0};当鼠标落下,记录光标位置赋值给lastLoc。鼠标移动的时候获得当前坐标curLoc作为结束位置。
绘制结束后,将curLoc的值赋给lastLoc。所以每一次鼠标移动画直线的起始坐标为上一次的结束坐标,结束坐标为当前鼠标坐标。
var isMouseDown = false; //鼠标是否按下
var lastLoc = {x:0,y:0};//初始化鼠标上一次所在位置
canvas.onmousedown=function(e){
e.preventDefault();
isMouseDown = true;
lastLoc = windowToCanvas(e.clientX,e.clientY);//上一次的坐标
}
canvas.onmousemove=function(e){ e.preventDefault(); if(isMouseDown){ //draw var curLoc = windowToCanvas(e.clientX,e.clientY);//获得当前坐标 var lineWidth = 5; context.lineWidth=lineWidth; context.beginPath(); context.moveTo(lastLoc.x,lastLoc.y);//起始位置为鼠标落下的位置 context.lineTo(curLoc.x,curLoc.y);//结束位置为当前位置 context.strokeStyle=color; context.stroke(); lastLoc = curLoc;//将当前坐标赋值给上一次坐标 lastLineWidth = lineWidth; } } //获得canvas坐标 function windowToCanvas(x,y){ var bbox = canvas.getBoundingClientRect(); return {x:Math.round(x-bbox.left),y:Math.round(y-bbox.top)}; }
现在写字功能已经完成了,但是我们需要对他进行优化。
优化一:当把context.lineWidth改大一些的时候,我们会发现,写字功能变得很不光滑了。
这是什么原因呢,我们可以尝试画两条宽度很大的直线看一下,两条直线之间确实是有缺口存在的,并且跟线的宽度有关。所以学写一个字会出现毛糙现象。
解决方法:设定线段端点的形状(线帽)
canvas.onmousemove=function(e){ e.preventDefault(); if(isMouseDown){ //draw var curLoc = windowToCanvas(e.clientX,e.clientY);//获得当前坐标 var lineWidth = 30; context.lineWidth=lineWidth; context.beginPath(); context.moveTo(lastLoc.x,lastLoc.y); context.lineTo(curLoc.x,curLoc.y); context.strokeStyle=color; context.lineCap = "round" context.lineJoin = "round" context.stroke(); lastLoc = curLoc; lastTimestamp = curTimestamp; lastLineWidth = lineWidth; } }
优化二:可以选择字的颜色,在页面上做一个色盘。
优化三:我们的字lineWidth是固定的,不能够向真正的毛笔字一样有粗细之分。
解决方法:通过运笔速度设置lineWidth的大小,运笔速度=距离 / 时间。时间可以通过时间戳获得。做法类似于lastLoc。
距离=当前坐标 - 上一次坐标。根据两点之间距离公式
设置一个做大lineWidth和一个最小的lineWidth,
var isMouseDown = false; //鼠标是否按下 var lastLoc = {x:0,y:0};//鼠标上一次所在位置 var lastTimestamp = 0;//时间戳 var lastLineWidth=-1;//上一次线条宽度 canvas.onmousemove=function(e){ e.preventDefault(); if(isMouseDown){ //draw var curLoc = windowToCanvas(e.clientX,e.clientY);//获得当前坐标 var curTimestamp = new Date().getTime();//当前时间 var s = calcDistance(curLoc,lastLoc);//获得运笔距离 var t = curTimestamp-lastTimestamp;//运笔时间 var lineWidth = calcLineWidth(t,s); var lineWidth = 30; context.lineWidth=lineWidth; context.beginPath(); context.moveTo(lastLoc.x,lastLoc.y); context.lineTo(curLoc.x,curLoc.y); context.strokeStyle=color; context.lineCap = "round" context.lineJoin = "round" context.stroke(); lastLoc = curLoc; lastLineWidth = lineWidth; } } //获得canvas坐标 function windowToCanvas(x,y){ var bbox = canvas.getBoundingClientRect(); return {x:Math.round(x-bbox.left),y:Math.round(y-bbox.top)}; } //求两点之间距离 function calcDistance(loc1,loc2){ return Math.sqrt((loc1.x - loc2.x)*(loc1.x - loc2.x)+(loc1.y - loc2.y)*(loc1.y - loc2.y)); } //求速度 function calcLineWidth(t,s){ var v = s/t; var resultLineWidth; if(v<=0.1){ resultLineWidth=30; }else if(v>=10){ resultLineWidth=1; }else{ resultLineWidth=30-(v-0.1)/(10-0.1)*(30-1); } if(lastLineWidth==-1){ return resultLineWidth; } return lastLineWidth*2/3+resultLineWidth*1/3; }
优化三:将项目改为移动端,touchstart,touchmove,touchend。函数封装,手机端跟pc端获得屏幕位置的方法不一样
//函数封装--开始 function beginStroke(point){ isMouseDown = true //console.log("mouse down!") lastLoc = windowToCanvas(point.x, point.y) lastTimestamp = new Date().getTime(); } function endStroke(){ isMouseDown = false } function moveStroke(point){ var curLoc = windowToCanvas(point.x , point.y);//获得当前坐标 var curTimestamp = new Date().getTime();//当前时间 var s = calcDistance(curLoc,lastLoc);//获得运笔距离 var t = curTimestamp-lastTimestamp;//运笔时间 var lineWidth = calcLineWidth(t,s); context.lineWidth=lineWidth; context.beginPath(); context.moveTo(lastLoc.x,lastLoc.y); context.lineTo(curLoc.x,curLoc.y); context.strokeStyle=color; context.lineCap = "round" context.lineJoin = "round" context.stroke(); lastLoc = curLoc; lastTimestamp = curTimestamp; lastLineWidth = lineWidth; } //手机端事件 canvas.addEventListener('touchstart',function(e){ e.preventDefault() touch = e.touches[0] //获得坐标位置 beginStroke( {x: touch.pageX , y: touch.pageY} ) }); canvas.addEventListener('touchmove',function(e){ e.preventDefault() if( isMouseDown ){ touch = e.touches[0] moveStroke({x: touch.pageX , y: touch.pageY}) } }); canvas.addEventListener('touchend',function(e){ e.preventDefault() endStroke() });
源码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>学写一个字</title> <meta name="viewport" content="height=device-height, width = device-width, initial-scale = 1.0, minimum-scale = 1.0, maxmum - scale = 1.0, user - scalable =no"/> <style> ul{ overflow:hidden; cursor:pointer; 400px; text-align:center; margin:20px auto; } ul li{ float:left; 40px; height:40px; border-radius:50%; margin-right:10px; border:4px solid transparent; list-style:none; } ul li:hover{ border:4px solid violet; } .red{ background-color:red; } .black{ background-color:black; } .green{ background-color:green; } .yellow{ background-color:yellow; } .blue{ background-color:blue; } button{ 90px; height:40px; line-height:40px; border:none; background:#ddd; margin-left:50px; } img{ 100px; margin-top:20px; text-align:left; } </style> </head> <body style="text-align:center;"> <canvas id="canvas" style="border:1px solid #ddd;"></canvas> <!---取色盘----> <ul> <li class="red" name="red"></li> <li class="black" name="black"></li> <li class="green" name="green"></li> <li class="yellow" name="yellow"></li> <li class="blue" name="blue"></li> </ul> <div style="text-align: center;"><button class="save" >保存</button><button class="clear">清除</button></div> <div class="img"></div> </body> <script src="js/jquery-2.1.4.min.js"></script> <script> window.onload = function(){ var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); var isMouseDown = false; //鼠标是否按下 var lastLoc = {x:0,y:0};//鼠标上一次所在位置 var lastTimestamp = 0;//时间戳 var lastLineWidth=-1;//上一次线条宽度 canvas.width = Math.min( 600 , window.innerWidth - 20 ); canvas.height = canvas.width; var color ="black"; //画出田字格 drawGrid(); //选择颜色 $('ul').on('click','li',function(){ color = $(this).attr('name'); }); //清除田字格的内容 $('body').on('click','button.clear',function(){ context.clearRect( 0 , 0 , canvas.width, canvas.height ); drawGrid(); }); //将canvas保存成图片 $('body').on('click','button.save',function(){ var dataurl = canvas.toDataURL('image/png'); var a = document.createElement('a'); a.href = dataurl; a.download = "我的书法"; a.click(); $('.img').append('<img src="'+dataurl+'"/>'); }); //函数封装--开始 function beginStroke(point){ isMouseDown = true //console.log("mouse down!") lastLoc = windowToCanvas(point.x, point.y) lastTimestamp = new Date().getTime(); } function endStroke(){ isMouseDown = false } function moveStroke(point){ var curLoc = windowToCanvas(point.x , point.y);//获得当前坐标 var curTimestamp = new Date().getTime();//当前时间 var s = calcDistance(curLoc,lastLoc);//获得运笔距离 var t = curTimestamp-lastTimestamp;//运笔时间 var lineWidth = calcLineWidth(t,s); context.lineWidth=lineWidth; context.beginPath(); context.moveTo(lastLoc.x,lastLoc.y); context.lineTo(curLoc.x,curLoc.y); context.strokeStyle=color; context.lineCap = "round" context.lineJoin = "round" context.stroke(); lastLoc = curLoc; lastTimestamp = curTimestamp; lastLineWidth = lineWidth; } //手机端事件 canvas.addEventListener('touchstart',function(e){ e.preventDefault() touch = e.touches[0] //获得坐标位置 beginStroke( {x: touch.pageX , y: touch.pageY} ) }); canvas.addEventListener('touchmove',function(e){ e.preventDefault() if( isMouseDown ){ touch = e.touches[0] moveStroke({x: touch.pageX , y: touch.pageY}) } }); canvas.addEventListener('touchend',function(e){ e.preventDefault() endStroke() }); canvas.onmousedown=function(e){ e.preventDefault(); beginStroke( {x: e.clientX , y: e.clientY} ) } canvas.onmouseup = function(e){ e.preventDefault(); endStroke(); } canvas.onmouseout = function(e){ e.preventDefault(); endStroke(); } canvas.onmousemove = function(e){ e.preventDefault(); if(isMouseDown){ //draw var curLoc = windowToCanvas(e.clientX,e.clientY);//获得当前坐标 moveStroke({x: e.clientX , y: e.clientY}) } } //获得canvas坐标 function windowToCanvas(x,y){ var bbox = canvas.getBoundingClientRect(); return {x:Math.round(x-bbox.left),y:Math.round(y-bbox.top)}; } //求两点之间距离 function calcDistance(loc1,loc2){ return Math.sqrt((loc1.x - loc2.x)*(loc1.x - loc2.x)+(loc1.y - loc2.y)*(loc1.y - loc2.y)); } //求速度 function calcLineWidth(t,s){ var v = s/t; var resultLineWidth; if(v<=0.1){ resultLineWidth=30; }else if(v>=10){ resultLineWidth=1; }else{ resultLineWidth=30-(v-0.1)/(10-0.1)*(30-1); } if(lastLineWidth==-1){ return resultLineWidth; } return lastLineWidth*2/3+resultLineWidth*1/3; } //田字格 function drawGrid(){ context.save(); context.strokeStyle = "rgb(230,11,9)"; context.beginPath(); context.moveTo(3,3); context.lineTo(canvas.width - 3,3); context.lineTo(canvas.width - 3,canvas.height -3); context.lineTo(3,canvas.height -3); context.closePath(); context.lineWidth = 6; context.stroke(); context.beginPath(); context.moveTo(0,0); context.lineTo(canvas.width,canvas.height); context.moveTo(canvas.width,0); context.lineTo(0,canvas.height); context.moveTo(canvas.width/2,0); context.lineTo(canvas.width/2,canvas.height); context.moveTo(0,canvas.width/2); context.lineTo(canvas.width,canvas.height/2); context.lineWidth=1; context.stroke(); context.restore(); } } </script> </html>