第一步:畫一個米字格,先畫一個矩形,再畫中間的米字。
<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;
width:400px;
text-align:center;
margin:20px auto;
}
ul li{
float:left;
width: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{
width:90px;
height:40px;
line-height:40px;
border:none;
background:#ddd;
margin-left:50px;
}
img{
width: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>
