DooScrib - 一个用于创建简单绘图画布的 jQuery 插件






4.11/5 (2投票s)
本系列文章的开篇,最终目标是创建一个人们可以在网页上使用的共享画布。
引言
正如人类最早的沟通形式之一包括描绘迁徙模式的洞穴壁画一样,在当今世界,我们仍然将图画作为一种交流方式。我仍然记得在我大学时代,一位教授在描述无限循环以及它们可能带来的破坏时,画了一幅飓风的图。随着时间的推移,每种形式都以更简单的方式传达了一个想法或一幅图画。
随着 HTML5 和 Canvas 元素的使用和可用性的不断扩展,Web 上的通信媒介对于开发人员来说变得更加容易实现。
在本系列文章中,我计划开发一个用于共享数字画布的工作解决方案,让用户可以实时协作。随着每篇文章的撰写,我将扩展 dooScrib.com,最终创建一个完整的可用示例。
背景
我还记得在 70 年代,当我还是个小男孩时,和父亲一起去 GTE 交换机房,听到所有交换机的咔哒声。我睁大眼睛,惊奇地看着,父亲用最简单的方式解释了通过电话连接一个客户到另一个客户的过程。在家里,我和父亲会用我的 150 合一电子套件在一个简单的平台上构建复杂的东西,以帮助他进一步阐述他工作中展示给我的想法。
让复杂的事物看起来简单,这始终是我从父亲那里学到的信息。即使在今天,我也对技术以一种简单化的方式进行开发和交付感到惊叹。
所以,当我在一个晚上看着我的妻子玩 Draw Something 时,我并没有感到惊讶,我当时心想,这是一个多么简单的想法。
嘿,我可以做一个类似的东西,并在过程中展示它的简单性,以便其他人可以在此基础上进行扩展。
要求
该解决方案的基本要求是,我们的最终用户使用的是支持 HTML5 和 canvas 元素的浏览器。幸运的是,我们开发者使用的大多数现代浏览器都支持这些功能,甚至移动浏览器,如 iOS 和 Android 设备上的浏览器,也都支持。
<canvas id="drawingSurface" width="100" height="100"> no canvas support </canvas>
在我看来,<canvas>
元素是 HTML 最伟大的新增功能之一。与许多其他 HTML 元素不同,这个元素需要一些 JavaScript 才能真正让它“活”起来。
因此,从开发角度来看,需要 HTML5 和 JavaScript。由于我非常喜欢并且支持 jQuery,我将尽可能多地围绕一个自定义插件来构建这个功能。我还将加入在移动设备上运行的要求。
第一步 - jQuery 插件
我喜欢简洁,并且倾向于尽可能地隐藏复杂性。所以,从使用的角度来看,我认为我想要一些足够简单,开发人员可以添加以下代码
$('#surface').dooScribPlugin({
width:300,
height:400,
cssClass:'pad',
penSize:4
});
需要回答几个问题;我想将此附加到页面上的哪个元素,我想要应用任何特殊的样式元素,线条应该有多粗,以及画布的大小应该是多少?我知道有些人会说宽度和高度是样式元素,通常我也会同意,但在这种情况下,它们根本不是。
插件基础
有很多关于编写 jQuery 插件的文章,我不想花太多时间讲解编写插件的基础知识。下面是我用于开始编写 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 文档都明确说明了要以这种方式设置高度和宽度。由于 canvas 元素来自 Apple,并且最初是在 WebKit 中引入的,所以我认为这是一个必需项。
我添加了 ID,以便以后需要时可以轻松选择该元素,然后添加了 class,以便可以对其进行样式设置。
好的,既然我们已经具备了一些基础知识,现在就开始编写插件吧。
(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);
}
}
})(jQuery);
基础部分已经完成。你实际上可以运行上面的代码并将其附加到一个或多个页面元素上,最终结果是,它将为附加到它的任何元素添加一个 canvas 元素。
正如你所见,ID 是自动生成的,并与一个随机数相关联。我添加了这个,以防插件与元素集合相关联,导致创建多个 Canvas 元素。
添加了 Canvas 之后,这仍然不够。如果我们想在上面进行任何绘图,我们需要创建一个 context 来进行绘制。
可以在添加 Canvas 后立即添加以下代码片段,它将用于创建我们需要的 context 对象。这里引用了一些其他方法和变量(penSize、cap),我将在文章后面讨论。
dooScrib.penSize(defaultOptions.penSize);
drawingSurface = document.getElementById(ID).getContext('2d');
drawingSurface.lineWidth = dooScrib.penSize();
drawingSurface.lineCap = cap;
现在我们创建了一个 canvas,并且我们已经有了实际绘制线条和其他图形所需的绘图 context。弄清楚了这些,我们就可以开始添加一些 JavaScript 来让 canvas 焕发活力了。
第三步 - 处理用户输入
由于我们正在创建一个绘图表面,我们需要知道用户何时在绘制,何时只是移动光标。为了处理用户输入,让我们捕获鼠标移动、点击和释放的事件。在我们开始编写处理鼠标事件的代码之前,我们应该谈谈移动设备。
移动设备
结果发现,对于移动设备,我们没有 mousedown、mouseup 或 mousemove 等事件。相反,在移动设备或基于触摸的设备上,我们有“触摸”事件。真令人惊叹,不是吗?
有几种方法可以从你的网页检测到移动设备或基于触摸的设备。由于我专注于消耗特定的触摸事件,我将查询 window,看看它是否支持触摸事件。
我将以下代码作为插件的一个公共方法添加,以便使用插件的人也能了解浏览器的触摸支持能力。
dooScrib.hasTouch = function() {
return 'ontouchstart' in window;
};
我还将以下代码作为私有方法添加,用于规范化触摸事件,以便它们包含与鼠标事件相同的 X 和 Y 坐标信息。
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);
}
在介绍处理事件的代码之前,我想更新插件的设置。最好能告知插件用户不同的事件发生情况。这非常适合我计划开始共享绘图画布给多个用户以后的开发。
以下代码包含用户可以传递给插件的所有选项。
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() 函数来阻止事件的默认行为来实现。然后,我们规范化触摸事件,使其包含与鼠标事件格式一致的 X 和 Y 坐标。
需要审查的最后一段代码是如何正确定义 X 和 Y 坐标相对于 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();
}
}
}
我将不详细介绍数据验证代码,因为我希望它已经相当自明了。所以,让我们来讨论笔刷选项的属性,以及实际绘制线条的代码。
笔刷属性
lineCap
strokeStyle
lineWidth
设置线条结束的样式。可以是 butt、rounded 或 square。我有一个私有变量来存储值,可以通过我稍后将介绍的公共函数进行修改。
设置要绘制线条的颜色。同样,我创建了一个私有变量来存储值,可以通过我稍后将介绍的公共函数进行修改。
设置要绘制线条的宽度。与前两个属性一样,我创建了一个私有变量来存储值,可以通过我稍后将介绍的公共函数进行修改。
绘制线条
一旦设置了属性,在 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;
};
第五步 - 添加一些“花哨”的功能
细黑线画久了会变得枯燥,所以我添加了以下内容,以增加控件的可用性和用户的创造力。
penSize(value)
lineCap(value)
lineColor(value)
允许你获取或设置绘制时使用的笔刷宽度。
允许你获取或设置线条结束的形状。有效值为 butt、round 或 square。
允许你设置绘制线条时使用的笔刷颜色。值是经过 CSS 验证的,这意味着你可以使用 #------ 值或英文可读值,如 Red、Green、Blue 等。
第六步 - 使用它
这部分很大程度上取决于你。下载代码,玩玩我包含的示例,或者将其集成到你自己的项目中。
下一篇文章
在下一篇文章中,我计划将该插件集成到一个使用 Node.js、Express 和 socket.io 的项目中,以开始在互联网上创建共享 Canvas。
历史
- 2013 年 3 月 10 日:初始版本。