65.9K
CodeProject 正在变化。 阅读更多。
Home

在 HTML Canvas 上绘制

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2014年4月28日

CPOL

10分钟阅读

viewsIcon

18005

开发一个用于创建和在 Canvas 元素上绘制的 jQuery 插件

引言

随着 HTML5 和 Canvas 元素的广泛使用和普及,Web 上的通信媒介对于开发人员来说变得更容易实现。

在本文中,我计划介绍一个 jQuery 插件的开发,你可以使用它来创建 HTML5 画布元素并在其上绘图。在 dooScrib.com 上有一个可用的示例,你可以查看从一个非常基本的想法可以实现什么。

背景

此解决方案的基本要求是,我们的最终用户使用的是支持 HTML5 以及 canvas 元素的浏览器。幸运的是,现代浏览器都支持此功能,甚至像 iOS 和 Android 设备上的移动浏览器也支持此功能。

可以在页面上处理对 <canvas> 元素的简单检测,并在用户的浏览器不支持时向用户提供信息。

<canvas id="drawingSurface" width="100" height="100"> no canvas support </canvas>

如果还不明显,我想说 <canvas> 元素在我看来是 HTML 最伟大的补充之一。与许多其他 HTML 元素不同,这个元素需要一些 JavaScript 才能真正使其生动起来。

因此,从开发的角度来看,需要 HTML5 和 JavaScript。

此外,由于我是 jQuery 的忠实粉丝和支持者,因此将此开发塑造成一个插件可能更好。

Using the Code

因此,在插件完成后,我的目标是任何开发人员都可以通过简单地在他们的页面中添加类似以下内容来使用它

$('#surface').dooScribPlugin({
    width:300,
    height:400,
    cssClass:'pad',
    penSize:4
});

通过这几行代码,我正在回答一些重要问题。

  • 画布应作为哪个元素的子元素创建?
  • 当画布元素在页面上创建时需要多大?
  • 是否应将任何样式参数 (CSS) 添加到元素中?
  • 当用户开始绘图时,画笔的初始粗细是多少?

那么,在解决了基础知识之后,让我们开始创建插件。

插件的基础知识

有很多关于开发 jQuery 插件的文章,所以我不想花太多时间在编写插件的基础知识上。下面是我为编写插件而整理的基本框架。完全有可能有更好的方法,但多年来,我一直认为这种方法对我很有效。

(function($) {
    // using $.fn.extend allows us to expand on jquery
    $.fn.extend({pluginName:function(options){
        // save a link to your instance
        var plugin = this;
        
        var defaultOptions = {
            // add what you know are default values for your options
        };
        
        // connect your default values to what the user has added
        // I connect everything into the current instance so that it 
        // can be referenced later if needed.
        if (options)
            plugin.Settings = $.extend(defaultOptions, options);
        else
            plugin.Settings = defaultOptions;
            
        // private functions
        function functionName(values){
            // code here
        }
        
        // public functions
        plugin.functionName = function(values){
            // code here
        }
        
        // implement get/set for your object properties
        var variableName;
        plugin.variableName = function(v){
            // validate data sent in
            if(undefined !== v){
                variableName = v;
            }
            
            return variableName;
        }
    
        return this.each(function(){
            // initialization code goes here
        });
    }});
})(jQuery);

那么,既然我们已经完成了大部分基础工作,让我们开始开发插件以实现我们想要的功能。

创建绘图画布

要创建绘图画布,我们必须插入类似以下内容

<canvas id="canvasid" class="className" width="100" height="100"></canvas>

有些人可能想知道为什么高度和宽度被添加为元素的属性。为什么不像其他所有元素一样(应该)在 CSS 中定义它们?

事实证明,canvas 元素“不喜欢”将高度和宽度定义为样式元素。我花了几个小时调试鼠标坐标无法正确传输的问题,直到我发现这一点。我最初注意到但忽略的是所有 Safari 画布文档都明确说明了以这种方式设置高度和宽度。

因此,由于 canvas 元素来自 Apple,并且首次在 WebKit 中引入,我倾向于认为这是一个要求。

我还添加了 ID,以便以后需要时可以选择该元素,然后添加了类以便可以对其进行样式设置。

现在让我们编写插件,使其包含在页面上创建 canvas 元素的功能。

(function($) {
    $.fn.extend({dooScribPlugin:function(options){
        var dooScrib = this;

        var defaultOptions = {
            penSize:2,
            width:100,
            height:100,
            cssClass:''
        };

        if (options)
            dooScrib.Settings = $.extend(defaultOptions, options);
        else
            dooScrib.Settings = defaultOptions;     
            
        if(true === isNaN(dooScrib.Settings.height)){
            dooScrib.Settings.height = 100;
        }
        
        if(true === isNaN(dooScrib.Settings.width)){
            dooScrib.Settings.width = 100;
        }

        var ID = this.attr('ID');
        if ((undefined === ID) || ('' === ID)){
            ID = 'dooScribCanvas'+Math.round($.now()*Math.random());
        }
        else {
            ID = 'dooScribCanvas'+Math.round($.now()*Math.random())+ID;
        }

        return this.each(function(){
            $("<canvas id='"+ID+"' class='"+defaultOptions.cssClass+"' 
            height='"+defaultOptions.height+"' width='"+defaultOptions.width+"'></canvas<").appendTo(dooScrib);

            dooScrib.penSize(defaultOptions.penSize);

            drawingSurface = document.getElementById(ID).getContext('2d');
            drawingSurface.lineWidth = dooScrib.penSize();
            drawingSurface.lineCap = cap;
        }
    }
})(jQuery);

因此,我们仍然有前面讨论过的基本插件,但现在我们添加了一些代码来检测是否为将要创建的画布的高度和宽度输入了任何值。如果没有提供值,我决定将高度和宽度都设置为随机值 100。

接下来,我们继续创建一个值,该值将稍后用作将在页面上创建的 canvas 元素的 ID。

最后是创建 canvas 元素的工作。

正如您所看到的,ID 是自动生成的并与一个随机数关联。我之所以选择这种方式,是因为插件可能与一组元素关联,从而导致创建多个 canvas 元素。

处理用户输入

由于我们正在创建一个绘图表面,我们需要知道用户何时正在绘图,以及何时只是移动光标。为了处理这种用户输入,让我们使用鼠标移动事件,以及鼠标按钮被点击和释放的事件。在我们深入了解使用鼠标事件的代码之前,我想谈谈移动设备。

移动设备

事实证明,对于移动设备,我们通常没有 mousedownmouseupmousemove 等事件。相反,在移动或基于触摸的设备上,我们有“触摸”事件。

是不是很神奇?

有几种不同的方法可以从您的网页检测移动或基于触摸的设备。但是,由于我专注于特定触摸事件的使用,我将查询 window 以查看它是否支持触摸事件。

我将以下代码作为 public 方法添加到插件中,以便使用插件的人也可以了解浏览器支持触摸的能力。

dooScrib.hasTouch = function() {
    return 'ontouchstart' in window;
};

我还将以下代码作为规范化触摸事件的 private 方法添加,以便它们将包含与鼠标事件相同格式的 XY 坐标信息。

function normalizeTouch(e) {
    if (true === dooScrib.hasTouch()) {
        if (['touchstart', 'touchmove', 'touchend'].indexOf(e.type) > -1) {
            e.clientX = event.targetTouches[0].pageX;
            e.clientY = event.targetTouches[0].pageY;
        }
    }

    return e;
}

处理事件

在浏览器检测完成后,我们现在可以开始添加一些代码,这些代码将负责订阅处理用户输入所需的适当事件。以下代码添加在前面为获取绘制到我们创建的 canvas 所需的 context 而添加的代码下方。

if (false === dooScrib.hasTouch()) {
    document.getElementById(ID).addEventListener('mousedown', clickDown, true);
    document.getElementById(ID).addEventListener('mousemove', moved, true);
    document.getElementById(ID).addEventListener('mouseup', clickUp, true);
}
else {
    document.getElementById(ID).addEventListener('touchstart', clickDown, true);
    document.getElementById(ID).addEventListener('touchmove', moved, true);
    document.getElementById(ID).addEventListener('touchend', clickUp, true);
}

在介绍处理事件的代码之前,我还想更新插件的设置。告知插件用户发生的各种事件可能是一个好主意。

以下代码包括用户现在可以传递给插件的所有 options

var defaultOptions = {
    penSize:2,
    width:100,
    height:100,
    cssClass:'',
    onClick: function(e) {},
    onMove: function(e) {},
    onPaint: function(e) {},
    onRelease: function(e) {}
};

是时候开始画画了吗?

所以如果你和我一样缺乏耐心,你可能已经开始想我们什么时候开始真正画一些线条了?嗯,我讨厌在长途旅行中听到父母说的那些名言。“我们快到了。”

在我深入讲解画线的细节之前,让我先回顾一下下面的这段代码。

var moved = function(e) {
if (!e) {
    e = window.event;
}

if (true === dooScrib.hasTouch()) {
    e.preventDefault();
    e = normalizeTouch(e);
}

var offset = $(dooScrib).offset();
var pt = new Point(e.clientX - offset.left, e.clientY - offset.top);

在所有接收到的鼠标/触摸事件中,首先需要做的是验证是否传入了事件。如果函数中没有传入任何内容,并且我们只是被告知存在事件;我们将需要从窗口中提取它。

这在鼠标或触摸事件中发生过吗?我想在我多年的事件处理工作中,我只遇到过一两次这种情况。然而,我花在调试上的时间给我留下了足够的创伤,我现在以这种方式处理它们。

所以接下来要做的是处理基于触摸的事件的情况,这样浏览器就不会认为用户正在开始按住或拖动事件。我们可以通过使用附加到事件对象的 preventDefault() 函数来阻止事件的默认行为。之后,我们对触摸事件进行规范化,使其包含与鼠标事件一致的 XY 坐标。

最后要回顾的代码是正确定义 XY 坐标,使其与 canvas 及其在屏幕上的实际位置相关。我们计算接收到的坐标与 canvas 在浏览器中位置的偏移量之间的差值。

是时候画些线条了

我最初将绘图过程直接集成到鼠标事件中,但是,随着时间的推移,我认为最好让插件用户能够无需触摸或鼠标事件即可重新创建或绘制线条。

所以我将实际的绘图工作移到了以下代码中

dooScrib.drawLine = function(fromX, fromY, toX, toY) {
    if ((undefined !== fromX) && (undefined !== fromY) && 
            (undefined !== toX) && (undefined !== toY)) {
        if((false === isNaN(Number(fromX))) && (false === isNaN(Number(fromY))) && 
                 (false === isNaN(Number(toX))) && (false === isNaN(Number(toY)))) {
            // set all the pen options
            drawingSurface.lineCap = cap;
            drawingSurface.strokeStyle = color;
            drawingSurface.lineWidth = penWidth;

            drawingSurface.beginPath();

            drawingSurface.moveTo(fromX, fromY);

            drawingSurface.lineTo(toX, toY);
            drawingSurface.stroke();
        }
    }
}

我不会深入讨论数据验证代码,因为我希望它已经相当自我说明。所以,让我们讨论一下画笔选项的属性,以及实际绘制线条的代码。

画一条线

一旦设置了属性,在 canvas 上画一条线是一个非常简单的过程,需要四个步骤才能完成。把它想象成一个 Bob Ross 绘画教程。

首先,拿起你要用的画笔。

设置线条的起始点。

设置线条的结束点。

画出实际的线条。

  • beginPath
  • moveTo
  • lineTo
  • stroke

好吧,也许我的 Bob Ross 模仿还需要一些练习,但我希望你能明白我的意思。

扩展用户输入

现在我们已经有了画线功能,让我们将其插入到可以报告给插件用户的用户事件中,并开始画线。

在以下代码示例中,我正在处理用户单击鼠标按钮或触摸其移动设备屏幕的事件。由于我们已经讨论了基本的事件处理、规范化触摸事件以及定义与 canvas 位置相关的坐标,因此我已从接下来的示例中删除了该代码。

如以下代码所示,处理鼠标点击或触摸事件,基本上是记录位置并设置一个标志,表明绘图已开始。在所有工作完成后,如果插件用户提供了事件处理程序,它会通知他们。值得指出的是,在这些函数中的每一个中,代码还返回 false 以阻止事件进一步处理。

var clickDown = function(e) {
    prevPoint = pt;

    dooScrib.drawing = true;
    dooScrib.Settings.onClick(pt);

    return false;
};

当用户释放鼠标按钮或将手指从屏幕上抬起时,我们会通过 clickUp 函数收到通知。此时,我们只需要保存这些坐标,清除绘图标志,然后通过 onRelease 回调通知订阅者。

var clickUp = function(e) {
    dooScrib.Settings.onRelease(pt);
    dooScrib.drawing = false;

    return false;            
};

无论用户是否点击鼠标,插件都会收到移动事件。因此,当收到这些事件时,我们将测试绘图标志是否已设置。

如果插件当前处于绘图模式,那么我们将使用之前的坐标和新的坐标来绘制一条线。最后,请务必保存当前坐标,以备后续调用,这些调用可能需要之前的点来完成一条新线。

移动事件将通过 OnPaint 事件或 onMove 事件通知插件用户。

需要指出的一点是,对于移动设备,插件只会在绘图模式下接收移动事件。

var moved = function(e) {
    if (true === dooScrib.isDrawing()) {
        dooScrib.drawLine(prevPoint.X, prevPoint.Y, pt.X, pt.Y);
        prevPoint = pt;

        dooScrib.Settings.onPaint(pt);
    }
    else {
        dooScrib.Settings.onMove(pt);
    }

    return false;
};

关注点

在过去的一年里,我一直专注于 Objective-C 的开发,但我对这个插件的长期开发包括使用 Node.jsExpresssocket.iodooScrib 网站上创建一个数字共享 canvas

历史

  • 2013 年 3 月 10 日:原始 dooscrib 文章在此撰写
  • 2014 年 4 月 27 日:本文作为前一篇文章的清理而撰写
© . All rights reserved.