9

在HTML5备受期待和瞩目的今天,越来越多的人已经感受到它带来的无限魅力与震撼力,许多的技术人员、设计者、互联网爱好者们纷纷加入了HTML5的研究与设计中。

首先我先为大家介绍一下一个功能很强大的HTML5在线绘画应用,它还拥有多种笔刷和滤镜,具有类似于photoshop的图层功能,可调节透明度隐藏等,还有渐变、油漆桶、拾色器、选择工具,大家一定会为此感到惊讶吧。

clipboard.png

但这样复杂的应用并没有使用flash实现,在canvas标记还没有出现之前,要想实现复杂的网页应用,或者直接在网页上进行绘图,只能借助于第三方的插件,比如Flash或Java,而现在,借助于canvas标记,我们可以实现图像显示和处理了,那么现在就让我抛砖引玉,讲解一下我的一些开发思路吧。

想要制作一个简单的画板并不是太难,但我建议您掌握一定的canvas基础和javascript基础,这样更便于理解和学习本教程。而如果你canvas技术比较好的话,你一定会觉得本教程又长又啰嗦,但是教程不可能顾及到所有的阅读者,所以麻烦你跳过你了解的部分,只关注重要的部分就好了。

首先,我讲解一下我的开发思路。我们需要在页面中添加一个canvas标记作为我们的画布,也就是我们将来要绘画的画板。由于需要用户使用鼠标点击、滑动、释放鼠标等操作来实现绘画,所以我们也必须要使用鼠标的几个基本的监听事件mousemove、mouseup、mousedown。

document.addEventListener('mousemove', mouseMove, false);
document.addEventListener('mousedown', mouseDown, false);
document.addEventListener('mouseup', mouseUp, false);

为了使绘画出来的线条更流畅,兼顾性能问题,我们可以采用setInterval来设置监听事件的时间间隔。 setInterval(函数名,1000/60); 其中1000/60为时间间隔。

setInterval(loop, 1000 / 60);
function loop() {
    $pos_display.innerHTML='你当前鼠标的位置为('+pos.x+','+pos.y+')';
    if (isMouseDown) draw(context);}

loop为循环执行的函数。

当然,你也可以采用requestAnimationFrame(如果不了解该属性可以自行百度^_^)。这取决于你的习惯。

那么现在我们需要获取用户鼠标点击的位置,在这里我们需要区分pageX,clientX,offsetX,layerX等概念 ,这里有篇文章讲解,你可以看看http://www.funnyhao.com/pagex-clientx-offsetx-layerx-of-those-things/

由于我们现在画布直接放在页面上左上部,padding和margin都为0,因此我们直接用clientX和clientY即可.当用户第一次点击鼠标时,我们设置isMouseDown为true,开启绘画模式。

function mouseDown(e) {
    isMouseDown = true;
}

获取了用户点击的位置后,我们在约定的时间间隔后(1/60秒)再次获取用户所在的位置,并进行更新

function loop() {
    if (isMouseDown) draw(context);//绘制鼠标点击位置
}
function mouseMove(e) {
    pos.x=e.clientX;//设置x坐标
    pos.y=e.clientY;//设置y坐标
    $pos_display.innerHTML='你当前点击鼠标的位置为('+pos.x+','+pos.y+')';//更新当前鼠标点击的位置
}

接下来我们就可以绘制了

function draw(ctx) {
    ctx.save();//保存当前绘图状态
    ctx.fillStyle = DEFAULT_BRUSH_COLOR;//设置填充的背景颜色
    ctx.lineWidth =DEFAULT_BRUSH_SIZE;  //设置画笔的大小
    ctx.lineCap = "round"; //设置线条,让线条边缘更圆滑
    ctx.beginPath();
    ctx.arc(pos.x,pos.y,DEFAULT_BRUSH_SIZE,0,Math.PI * 2,true);
    /****
    *context.arc(x, y, radius, startAngle, endAngle, anticlockwise)
    *参数 x,y表示圆心
    *radius半径
    *startAngle起始弧度
    *endAngle终止弧度
    *anticlockwise是否为逆时针方向
    ***/
    ctx.fill();//填充绘画路径
    ctx.restore();//恢复绘画状态
}

似乎这样的大功告成了。看这里的演示代码:DEMO1(http://runjs.cn/detail/gxeeyocw)

clipboard.png

当我们画画时,如果绘制笔移动的较快的时候,就会发现出现了断断续续的情况,这是怎么回事呢?原来我们只设置了一个点每过1/60秒就更新一下位置,当我们绘图时如果画笔移动的速度够快时绘制的不够密集,绘制的点久不能连接起来,从而引起断续的现象。

可能会有些人说可以设置时间间隔更小,比如设置为1/1000秒,也就是将页面中的代码

setInterval(loop, 1000 / 60);

改为

setInterval(loop, 1000 / 1000);

甚至无穷小,这样不就解决了吗。但是相信很多人都不会推荐这样的方法,因为这不仅仅会影响到页面的效率,而且也没有从根本上解决问题,setinterval调用间隔的时间往往会有诸多限制,所以这样的方法是行不通的。

要让线连贯起来最简单的方法:那就用线连起来吧。(旁白:废话,⊙﹏⊙b汗)我们知道两点确定一条直线,所以只要我们确定两个点的坐标即可。亦即每个时间间隔单位,我们获取一次当前点的坐标就好了。然后使用canvas的moveTo函数移动下一个点,记录当前点坐标和上一个点的坐标,并使用canvas的lineTo函数将线连起来,然后不要忘了用stroke函数绘制出来,具体看这里的代码:DEMO2(http://runjs.cn/detail/r52qaltg)。

clipboard.png

我们通过表格比较一下这两种方案的区别:

clipboard.png

表格中明显看出方案一都是孤立的点,而方案二每个点都会有两种状态,将这两种状态下的点连起来就会形成衔接的较好的效果。

因为基础的内容在上面已经讲述了,所以在这里我也不重复了,需要注意的是当前点与上一个点重复时需要做一下处理,否则页面无法绘制出来。

if(pos.x==next_pos.x&&pos.y==next_pos.y){
    ctx.arc(pos.x,pos.y,DEFAULT_BRUSH_SIZE/1.7,0,Math.PI * 2,true);
    ctx.fill();//填充绘画路径}
else{
    ctx.moveTo(pos.x,pos.y);
    ctx.lineTo(next_pos.x,next_pos.y);
    ctx.stroke();
}

为了方便讲解,我这里采用的都是面向过程的方法,在比较大的应用中,我们要尽可能采用面向对象的方法,好处是不言而喻的,不仅能让代码条理清晰,更有较好的扩展性,方便二次开发和模块复用。使用面向对象方法的代码请查看这里(这里会使用了point函数类,覆盖了set和update等方法)请查看DEMO3_1(http://runjs.cn/detail/gvfyrswu)。

研究技术的时候,我们需要举一反三,显然现在的方法还是不够完善。能不能将所有的点都记录下来,因为每个时间间隔单位,都会损失掉很多的点,为了让画出来的图更加圆滑,我们要将所有的点都记录下来,并且效率又能得到优化,我在这里提出一个解决方案。

用数组记录下所有的路径,然后用堆栈的push方法将点添加到数组中,为了达到更好的效率,我们可以采用一维数组,分别用两个数组记录横坐标和纵坐标,具体的实现我就不贴代码了,大家有余力的话可以当作一个小小的作业,参照我的这个页面例子自己编写代码实现,页面中会有代码注释的。

我们实现了绘制功能,我们还需要对绘制的图片进行擦除。不要尝试采用transparent或者rgba(x,x,x,0)这样的颜色值绘制,因为这样页面便不会绘制出任何东西,最实用的方法就是绘制背景颜色,如果背景是图片,那就重绘背景图片,然后就原来的内容的其他部分绘制到画布中。具体查看demo3_2(http://runjs.cn/detail/jywf4qv1

clipboard.png

那如果我们要实现蜡笔的效果。要怎么处理呢,如果我们将蜡笔画放大后看就会知道那是一些很小的分散颗粒状大小的粒子,这样我们就有了思路了。我们还是沿用DEMO3的例子,在其基础上进行开发,需要注意的一点是粒子的分布问题,如何才能将粒子均匀的分布呢,不知道大伙们这么久没学数学是不是都将知识还给老师了。这里我们会用到一些基本的数学知识, 具体思路请看下图,

clipboard.png

请参照源代码

draw: function(ctx) {
        var v = this.subtract(this._latest);//当前点与下一个点的距离的横纵坐标
        var s = Math.ceil(this.size / 2);       //算出粒子的单位长度
        var stepNum = Math.floor(v.length() / s) + 1;   //算出步长  v.length()为斜线长度
        v.normalize(s);//当前点与下一个点的

        var sep = 1.5; // 分割数  控制画笔的浓密程度  关键所在
        //粒子的大小 根据画笔描绘的速度(画笔的停留时间)进行调整
        var dotSize = sep * Math.min(this.inkAmount / this._latestStrokeLength * 3, 1);
        var dotNum = Math.ceil(this.size * sep);
        var range = this.size / 2;
        var i, j, p, r, c, x, y;
        $whitemode_display.innerHTML="绘制的画笔颜色是"+brush_color;
        ctx.save();
        ctx.fillStyle = currentColor;
        $pos_display.innerHTML='你上一点鼠标的位置为('+this.x+','+this.y+').你当前鼠标的位置为('+this._latest.x+','+this._latest.y+')';//更新当前鼠标点击的位置
        ctx.beginPath();
        if(wmode=="擦除模式"){
            ctx.strokeStyle=brush_color;
            ctx.lineWidth =DEFAULT_BRUSH_SIZE;
            ctx.lineCap = "round";
            ctx.beginPath();
            p = this._latest;//获取下一个点位置
            ctx.moveTo(this.x,this.y);
            ctx.lineTo(p.x,p.y);
            ctx.stroke();
        }
        else{
            for (i = 0; i < dotNum; i++) {
                for (j = 0; j < stepNum; j++) {
                    p = this._latest.add(v.scale(j));
                    r = random(range);
                    c = random(Math.PI * 2);
                    w = random(dotSize, dotSize / 2);
                    h = random(dotSize, dotSize / 2);
                    x = p.x + r * Math.sin(c) - w / 2;
                    y = p.y + r * Math.cos(c) - h / 2;
                    ctx.rect(x, y, w, h);//边缘不要太平滑,不要使用arc
                }
            }
        }
        ctx.fill();
        ctx.restore();
    }

  });

进行分析比较。思路的重点是在一定间隔后对粒子进行随机分散排布,并能处理在画笔移动的比较快的时候的的绘制问题。为了得到更好的展示效果,我们一般还会控制透明度进行调整。

处理完这些后,我们如果喜欢这样的图片,还可以使用图片导出功能,方法也挺简单。去掉监听事件,使用canvas的toDataURL内置函数,然后展示到新打开的窗口中。

我们就可以运行一下源代码看看带擦出功能和图片导出功能的实际效果如何:

clipboard.png

温馨提示:在键盘上输入p键可以导出图片,图片导出功能由于在新窗口中打开,请使用全屏预览模式并允许窗口弹出。

其实我们还可以有更多变化,只要你去构想,去思考。很多时候你都要去尝试,往往多次尝试才会有新的ideas。接下来我们可是做出以下特殊的画笔,比如说钢笔效果,边缘会比较笔触比较重的。钢笔效果需要将画笔尺寸调小,减弱抽丝的效果,并且边缘的锯齿会比较明显,所以需要做一下阴影模糊处理,让其过渡更平滑。

具体源码看这里:demo5(http://runjs.cn/detail/df5u6cb5

clipboard.png

之前曾在这里见过一个毛笔画网站,所以也模拟了一下毛笔的效果,毛笔的特点是笔触比较大,当收笔较快时边缘要凹凸不平,笔尖写字锋棱易出。当收笔有停顿时则会圆润而浑厚。有个类似的线上应用,大家可以去研究研究:http://www.theshodo.com/Write

![clipboard.pn
clipboard.png
C)

![图片上传中...]

根据这些特点,我给大家提供一个DEMO源码,是在钢笔效果的基础上做些小小的调整的。

如果大家还觉得这样的效果是否还可以添加点特效什么的,对,可以做杂点斑点的效果,还有墨水过多而流下的效果。

具体可以参看DEMO6(http://runjs.cn/detail/ully3puv)和DEMO7。

clipboard.png

clipboard.png

源码我就不进行分析了,留待大家自己去研究,将画笔颜色调为黑色就差不多可以模拟出毛笔的效果了。在我看来,技术的专研不是一味的让别人教你,而是让你自己去领悟的。只有那样你才真正学到技术,领略到不一样的乐趣。

说些设计以外的东西,设计、编程都需要有自己的思想和灵魂,真正让别人也能感受到你的思路,而这一切都需要磨练,需要举一反三。不应该满足于现状,我在这里只列举了其中一些效果,我相信还有很多效果可以实现,比如说类似于这样的喷雾效果,铅笔字效果,艺术画效果,等等。既然说HTML5创意画板,那就要尝试脱离这个画板的束缚,学到更多的东西,比如说你可以用这个画板做什么,可以做一个记事本、涂鸦工具、处理和分享图片,个性签名,你还可以做一些小游戏,涂鸦类的游戏,你画我猜(需要采用websocket实现服务器端双向通信)等等,甚至可以做一些canvas动画,这些基础上做些修改和调整,完全是可以实现的。有了目标和思路,那就沿着这个方向去学习,我相信你一定会有所收获的。

这次的HTML5绘图教程就到这里,大家还可以尝试为此添加更多的个性化的功能,同时欢迎大家留言提问或者提出批评建议。

源码下载:

runJS: http://runjs.cn/detail/spxs2kxq

微盘:http://vdisk.weibo.com/s/otSnZ

百度网盘:http://pan.baidu.com/share/link?shareid=194573&uk=3744164386

扩展阅读:

在线毛笔画板:http://www.theshodo.com/Write

来自deviants的在线画板:http://sta.sh/muro/

如果想查看之前的FLASH版本,可以点击这里http://www.inzrb.com/blog/?page_id=211

via sina udc


江小湖Laker
6.9k 声望371 粉丝

进击的程序媛快去创造奇迹~