关于canvas的学习心得(二)

本文探讨了使用Canvas制作手绘板的过程,特别关注了如何实现毛笔笔锋效果。通过分析书道和人民网手写功能,提出通过手写速度控制线条粗细,并解释了如何计算速度。遇到的问题是找到两圆外公切线来画出四边形,目前在寻求解决方案。同时,介绍了手写重播的实现方法,强调了JavaScript的运行机制在其中的作用。最后,分享了完整的代码,邀请读者交流改进。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

任务描述:

1>制作一个手机端的手绘板

2>要求能有毛笔的笔锋

3>画线能有轻重感

4>能重播手写过程

5>能提交保存


前面把一切准备工作都做好了,现在就涉及到了笔锋的设计了,经过多方查看,

如书道:http://www.theshodo.com/Write

人民网的手写送祝福:http://t.people.com.cn/wx/chunjie?from=singlemessage&isappinstalled=0

书道使用的是贴图,一个毛笔的点然后循环将这个点的贴图贴到canvas面板上,而人民网的则是使用圆点笔锋。

让我疑惑的是,我用radius获取不到手指的触摸面积,那他们是怎么做到画线轻重感的呢?

如果你正好进入了这两个网站,可以跟我一起试试。如果你开始慢慢的来,然后突然加速,是不是发现慢慢来的笔画粗些,而突然加速后的笔画细些?对了,就是通过手写速度控制笔画的,那么速度怎么获取呢?如果您仔细看了我前面的代码,肯定会发现有一个方法的说明"计算两点之间的距离函数,方便任务三的完成,请看下面分析"不知道您发现这句话了没有,对,就是通过距离获取速度,速度的定义是:单位时间内移动的距离,单位时间是由计算机自定义的,我们无法更改。那么久只能通过距离来获取了。


通过测试“10/dis+10”这个数据是比较靠近真实毛笔画的粗细的。

下面上具体代码:

function onTouchMove(event) {
    try
    {
      	event.preventDefault();
		var last1X=event.touches[0].pageX-canvasPosition.x;
		var last1Y=event.touches[0].pageY-canvasPosition.y;
		var rax=document.getElementById("raX");
		var ray=document.getElementById('raY');
		var dis=distance(lastX,lastY,last1X,last1Y);
		lastLineWidth=ctx.lineWidth;
		ctx.lineWidth=10/dis+10;
      	drawLine(ctx,lastX, lastY, last1X, last1Y );
		rax.value=lastX-event.targetTouches[0].pageX+canvasPosition.x;
		ray.value=lastY-event.targetTouches[0].pageY+canvasPosition.y;
		lastX=event.touches[0].pageX-canvasPosition.x;
		lastY=event.touches[0].pageY-canvasPosition.y;
		putNewOne(lastX,lastY,10,dis);
    }
    catch(err){
        alert(err.description);
    }
}
将上面这段函数覆盖上一篇的同名函数即可。这样实现了画笔的宽度设置,但是如果你去做了实验,就会发现这样的粗细变化很突兀。考虑到要求,想到了一个解决方案,两点之间画线,那是不是可以像下面的图这样呢?


第一个圆的位置我们都已经知道了,笔画的宽度也就是圆的半径,第二个圆的位置我们也清楚,半径也一样能获取到,那么能不能画两圆之间的外公切线并连上这四个点画个四边形,然后填充颜色不就解决这个问题了吗?

可是事情不像想象的那么美好啊,首先就这四个点的位置问题,经过多方查询,这个公式比较复杂,目前正在解决这个问题。所以如果可以欢迎大家来此提意见,或者算法有谁能做到的,也请各位大神不吝赐教。


下面是手写重播的说明了。

我们每次通过触摸来画线是不是都获取到了相应的X,Y坐标、W画笔的宽度和每次移动的距离D?

将这些数据整合起来,方法如下:

function putNewOne(x,y,w,dis){
	var arr={X:x,Y:y,W:w,D:dis};
	saveArr.push(arr);
}
获取到了这些数据,然后如何将它重播出来呢?

我这里是用了另外一个canvas来重播

<span style="font-size:14px;">function drawOnHead(){
	var headCtx=head.getContext("2d");				//head的笔
	initCtx(headCtx,0.5,0.5,"#0FA");								//初始化head的笔
	for(var i=0;saveArr.length>0;i++){
		var arr=saveArr.shift();
		headCtx.lineWidth=arr.W;
		var X=arr.X;
		var Y=arr.Y;
		var D=arr.D;
		if(D==0){
			var lx=X;
			var ly=Y;
			setTimeout(function (X,Y,lx,ly,headCtx){return function (){
				//drawRound(headCtx,lx,ly);
				drawRound(headCtx,lx,ly);
			};}(X,Y,lx,ly,headCtx)
			,30*i);
		}else{
			setTimeout(function (X,Y,lx,ly,headCtx){return function (){
				//drawRound(headCtx,lx,ly);
				drawLine(headCtx,X,Y,lx,ly);
			};}(X,Y,lx,ly,headCtx)
			,30*i);
			lx=X;ly=Y;
		}
	}
}</span>
在第三行有一个初始化函数,如下:

function initCtx(headCtx,sx,sy,color){<span style="white-space:pre">		</span>//第一个参数是画笔,第二个是X轴上缩放的倍数如果不缩放可以填1,第三个是Y轴上缩放的倍数,第四个是画笔的颜色
	headCtx.scale(sx,sy);
	headCtx.strokeStyle = color;//"#000";
}
第十三行有一个setTimeOut函数,如果不懂的朋友可以去百度一下。

在这里顺便提一下,js的运行机制,js是逐行扫面运行,如果遇到了函数,不会等到函数运行完成再执行下句,而是直接执行下句,所以setTimeOut的等待时间不能是固定的,而是变化的,如果在C语言中for中增加一个sleep方法会等待sleep返回后才执行下一句,而js不是js是分多线程同时执行sleep,然后它主线程会继续往下执行。所以一定要注意这里。

下面就是最后一个任务了。

function convertCanvasToImage(canvas) {
  var image = new Image();
  image.src = canvas.toDataURL("image/png");<span style="white-space:pre">			</span>//将canvas上的图形转换为计算数据并赋值给image的源
  var img=document.getElementById("img");<span style="white-space:pre">			</span>//获取页面中ID为img的组件
  img.value=image.src;<span style="white-space:pre">						</span>//将值传给img
}

function canvasToImage(){
	convertCanvasToImage(document.getElementById('canvas'));
}
由此form表单中的隐藏组件img有了值,点击提交按钮便可以上传到后台,后台是设计到php的内容就不在这个帖子提及了。如有需要,我会在另外的帖子中贴出来的。


下面防止代码不完整,贴上整个全部的代码。欢迎大家评论,改善。

<pre name="code" class="html"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<title>Canvas</title>
</head>

<body>
<canvas id="head"></canvas>
<div><canvas style="border:medium #000" id="canvas" >您的浏览器不支持本网页</canvas><div></div></div>
<form action="{:U('Index/upload',array('uid'=>$userid))}" method="post">
    <input id="img" type="hidden" name="img" /><br />
    <input type="submit" value="提交" />
  	<input id="clear" type="button" value="清空" />
  	<input id="addNew" type="button" value="增加"  />
</form>

<input type="text" id="raX" />
<input type="text" id="raY" />
</body>
<script type="text/javascript" src="public/js/jquery-1.11.2.min.js"></script>
<script type="text/javascript" charset="utf-8">
/**************************************常量区***************************************/
var canvas = document.getElementById("canvas");
canvas.width=(window.innerWidth-10)/3*2;
canvas.height=(window.innerHeight)/2-50;
canvas.align="center";

var head=document.getElementById('head');
head.width=(window.innerWidth-10);
head.height=(window.innerHeight)/2-50;

var canvasPosition={x:canvas.offsetLeft,y:canvas.offsetTop};

//获取id为clear的按钮值,并增加监听点击事件。
var clear=document.getElementById('clear');
clear.addEventListener('click',clearAll,false);

//获取id为addNew的按钮值,并增加监听点击事件。
var add=document.getElementById('addNew');
add.addEventListener('click',addNew,false);

//划过的点的坐标以及各种参数
var saveArr=new Array();
//上一次触摸坐标
var lastX;
var lastY;
var startlindwidth=10;
var startsudu=1;
var ctx =canvas.getContext("2d");			//canvas的笔
ctx.lineWidth=startlindwidth;//画笔粗细
ctx.strokeStyle = "#000";//画笔颜色
//ctx.setAntiAlias(true);		//抗锯齿
ctx.fillStyle = ctx.strokeStyle;

var touchable = 'createTouch' in document;
if (touchable) {
    canvas.addEventListener('touchstart', onTouchStart, false);
    canvas.addEventListener('touchmove', onTouchMove, false);
	canvas.addEventListener('touchend', onTouchEnd, false);
}
else
{
	document.onmousedown=onMouseDown;
    //document.onmousemove=onMouseMove;
	document.onmouseup=onMouseUp;
}


/**************************************函数区***************************************/
/******************基本功能函数*********************/
//画圆
function drawRound(ctx,x,y){
    ctx.beginPath();
    ctx.arc(x,y,(ctx.lineWidth-1)/2,0,Math.PI*2,true);
    ctx.closePath();
    ctx.fill();
}

//画线
function drawLine(ctx,startX,startY,endX,endY){
    ctx.beginPath();
    ctx.lineCap="round";
    ctx.moveTo(startX,startY);
    ctx.lineTo(endX,endY);
    ctx.stroke();
}

function drawLine(ctx,startX,startY,startW,endX,endY,endW){
	
}

function convertCanvasToImage(canvas) {
  var image = new Image();
  image.src = canvas.toDataURL("image/png");
  var img=document.getElementById("img");
  img.value=image.src;
}

function canvasToImage(){
	convertCanvasToImage(document.getElementById('canvas'));
}

function distance(x1,y1,x2,y2) {
	var xdiff = x2 - x1;
	var ydiff = y2 - y1;
	return Math.pow((xdiff * xdiff + ydiff * ydiff), 0.5);
}

/******************监听事件函数*********************/
//鼠标被压下时事件
function onMouseDown(event){
	lastX=event.clientX-canvasPosition.x;
    lastY=event.clientY-canvasPosition.y;
	drawRound(ctx,lastX,lastY);
	putNewOne(lastX,lastY,startlindwidth,0);
	document.onmousemove=onMouseMove;
}


//鼠标按钮被压下后拖动事件
function onMouseMove(event){
	try{
		event.preventDefault();
		drawLine(ctx,lastX,lastY,event.clientX-canvasPosition.x,event.clientY-canvasPosition.y);
		lastX=event.clientX-canvasPosition.x;
      	lastY=event.clientY-canvasPosition.y;
		putNewOne(lastX,lastY,thisLineWidth,dis);
	}catch(err){
		alert(err.description);
	}
}
//鼠标松开后的时间执行
function onMouseUp(event){
	document.onmousemove=null;
	canvasToImage();
}

//触摸开始事件
function onTouchStart(event) {
	ctx.lineWidth=startlindwidth;
	initFlag=1;
    event.preventDefault();
    lastX=event.touches[0].pageX-canvasPosition.x;
    lastY=event.touches[0].pageY-canvasPosition.y;
	putNewOne(lastX,lastY,startlindwidth,0);
    drawRound(ctx,lastX,lastY);
}


//触摸滑动事件
function onTouchMove(event) {
    try
    {
	//cxt.lineWidth;
      	event.preventDefault();
		var last1X=event.touches[0].pageX-canvasPosition.x;
		var last1Y=event.touches[0].pageY-canvasPosition.y;
		var rax=document.getElementById("raX");
		var ray=document.getElementById('raY');
		var dis=distance(lastX,lastY,last1X,last1Y);
		lastLineWidth=ctx.lineWidth;
		ctx.lineWidth=10/dis+10;
      	drawLine(ctx,lastX, lastY, last1X, last1Y );
		rax.value=lastX-event.targetTouches[0].pageX+canvasPosition.x;
		ray.value=lastY-event.targetTouches[0].pageY+canvasPosition.y;
		lastX=event.touches[0].pageX-canvasPosition.x;
		lastY=event.touches[0].pageY-canvasPosition.y;
		putNewOne(lastX,lastY,10,dis);
    }
    catch(err){
        alert(err.description);
    }
}

function onTouchEnd(event){
	drawRound(ctx,lastX,lastY);
	canvasToImage();
	//alert(saveArr);
}
/******************其他函数*********************/
function drawOnHead(){
	var headCtx=head.getContext("2d");				//head的笔
	initCtx(headCtx,0.5,0.5,"#0FA");								//初始化head的笔
	for(var i=0;saveArr.length>0;i++){
		var arr=saveArr.shift();
		headCtx.lineWidth=arr.W;
		var X=arr.X;
		var Y=arr.Y;
		var D=arr.D;
		if(D==0){
			var lx=X;
			var ly=Y;
			setTimeout(function (X,Y,lx,ly,headCtx){return function (){
				//drawRound(headCtx,lx,ly);
				drawRound(headCtx,lx,ly);
			};}(X,Y,lx,ly,headCtx)
			,30*i);
		}else{
			setTimeout(function (X,Y,lx,ly,headCtx){return function (){
				//drawRound(headCtx,lx,ly);
				drawLine(headCtx,X,Y,lx,ly);
			};}(X,Y,lx,ly,headCtx)
			,30*i);
			lx=X;ly=Y;
		}
	}
}

function putNewOne(x,y,w,dis){
	var arr={X:x,Y:y,W:w,D:dis};
	saveArr.push(arr);
}

function initCtx(headCtx,sx,sy,color){
	headCtx.scale(sx,sy);
	headCtx.strokeStyle = color;//"#000";
}

//增加canvas字
function addNew(){
	var board=document.getElementById('board');
	var img = document.createElement('img');
	img.src=canvas.toDataURL("image/png");
	img.width=(window.innerWidth-10)/4;
	var object=board.appendChild(img);
	//alert(canvas.offsetLeft+canvas.offsetTop);
}

//清空canvas面板
function clearAll(name){
	drawOnHead();
	ctx.clearRect(0,0,window.innerWidth-10,window.innerHeight-100);
	saveArr=new Array();
}
</script>

</html>




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值