天天看点

如何用 JavaScript 作画

<b>本文讲的是如何用 JavaScript 作画,</b>

<b></b>

因为我司给我一个在浏览器中以编程方式来实现绘图的需求,如下图 1.1 所示,我想分享一些用 JavaScript 绘画的要点。实际上,我们画啥呢?答案是任一种图像和图形。

注意:这个项目的版权是我司的,所以并不会向社区 开源 代码。

你可能会问,这样的 <code>path</code> 数据只在 SVG 元素中有效,怎么能绘制其他像 JPG、PNG、或者 GIF 这样的图片呢。这是我们在本文后面将探讨的问题。在那之前,我们先简单绘制一幅 SVG 图像。

什么是 SVG?可伸缩矢量图形,又称为 SVG,是针对二维图形基于 XML 的矢量图片格式,支持动画交互。不支持老旧的 IE 浏览器。如果你是设计师,或者是经常使用 Adobe Illustration 做绘图工具的插画家,也许已经对图形已经有了一定的认知。但与一般图形主要的不同在于,SVG 是可伸缩的无损的,而其他格式的图片不是。

注意:一般来说,SVG 格式的图片被称作 图形,而其他格式的被称为 图像。

正如上文所说,在绘制 SVG 之前,你需要从 SVG 文件中读取数据。这通常是 JavaScript 中 <code>FileReader</code> 这个对象的工作,它的初始化代码片段像下面这样:

作为一个 Web API,<code>FileReader</code> 能够读取本地文件,<code>readAsText</code> 是其中支持读取文本格式内容的方法之一。它可以触发事先定义的 <code>onload</code> 方法,我们能够在事件处理方法内部读取内容。读取内容的代码应该如下所示:

有了阅读监听器,你也许会考虑是否还要用一个按钮来上传文件。现在看来,那是普通没有任何吸引力的交互方式。于此,我们可以通过拖放来优化这类交互。这意味着你能够拖拽任何图形并且放置到读取内容的方框里。因为我的项目的优先技术选型是 Canvas,我将通过设置事件监听器和注册一个 canvas 的 <code>drop</code> 事件来实现这种交互。

现在数据已经存储在 <code>contents</code> 变量里,并且已经能够处理它,数据对我们来说只是文本而已。开始时,我尝试使用常规方法提取路径节点。

但是这个方法有两个缺点:

会丢失整个 SVG 文件结构。

不能创建一个合法的 <code>SVGPathElement</code> DOM 元素。

为了更直白地说明,请看如下代码:

正如你看到的, <code>tmpDiv.childNodes[0]</code> 不是一个 <code>SVGPathElement</code>,所以我们需要创建另一个节点。如果我用另一个方法读取整个 SVG 文件,<code>SVGPath</code> 变量能够以清晰的结构存储整个 SVG 对象,并且可以随意访问:

用递归的方式可以很容易地提取所有 <code>SVGPathElement</code> 并且直接送入 <code>pathNodes</code> 栈。

使用那种方法看起来优雅多了,至少我是这么认为的,尤其是和其他元素一起绘制的时候,我只用 <code>switch</code> 结构就能提取不同元素,而不是使用一些常规表达。一般来说,在一个 SVG 文件里,图形元素除了可以被定义成 <code>path</code>,还可被定义成<code>circle</code>,<code>rect</code>,<code>polyline</code>。所以,我们应该怎么处理他们?答案是用 JavaScript 就能全部转换成 <code>path</code> 元素,这个稍后再说。

我在开发项目的时候有一个问题是到底需要重点关注什么。在一个复合路径中,<code>m</code> 和 <code>M</code> 完全不一样,必须要有至少一个 <code>m</code>或者一个 <code>M</code>,所以你必须把他们分离出来,避免两条路径相互影响。也就是说,如果一条路径属于复合路径,则区分这两个符号:

注意:路径已经在提取出来并存储在本地变量中,下一步要做的是用点绘制出来:

如你所见,<code>pointsArr</code> 是一个二维数组,第一维是路径,第二维是每个路径下的点。当然,这些点是能用 Canvas 画出来的,如下:

试着考虑这样一个问题:如果一条路径包括尽可能多的可绘制点,如何优化绘制方案更快速地绘制?也许,跳着画是解决的简单之法,但是怎么跳着画是另一个关键问题。我还没有发现完美解法,如果你有想法,欢迎交流。

算法是我们需要重点思考的。

随着需求越来越复杂,路径数据无法适应比例缩放,改变大小或者移动图形的场景。

因为你可能要在Canvas当中对图形进行比例缩放、调整尺寸、移动,这就意味着路径数据也应该随着你的改动来变化。但实际上它不能,所以我们才需要校准参数。

<a href="https://github.com/aleen42/PersonalWiki/raw/master/post/how_to_draw/panel.png" target="_blank"></a>

图 2.1所谓面板

图 2.1 展示了一个高亮工作区域,我叫它 面板。在这个面板上,你可以进行拖放,拖拽,调整尺寸或者移动操作。实际上,面板里包含了一个能满足你需求的 Canvas 对象。只需要把 SVG 文件(图 2.2)拖放到面板里,就可以在屏幕上重绘,结果如图 2.3:

如何用 JavaScript 作画

图 2.2绘制的 SVG 文件

富含中国元素的美丽 logo 就生成啦

<a href="https://github.com/aleen42/PersonalWiki/raw/master/post/how_to_draw/1.png" target="_blank"></a>

图 2.3 渲染图形

除了图形操作,其他 SVG 属性也会影响路径数据,比如 <code>width</code>,<code>height</code> 和 <code>viewBox</code>。

所以,校准参数的计算受两个因素影响,属性 和 操作。

计算之前,要了解定义的变量和代表的含义。

首先是图形位置变量:

oriX: 图形初始 <code>x</code> 值

oriY: 图形初始 <code>y</code> 值

moveX: 移动前后 <code>x</code> 的差值.

moveY: 移动前后 <code>y</code> 的差值.

viewBoxX: 图形的 <code>viewBox</code> 属性的 <code>x</code> 值

viewBoxY: 图形的 <code>viewBox</code> 属性的 <code>y</code> 值

然后是图形尺寸变量:

oriW: 图形初始宽度

oriH: 图形初始高度

svgW: SVG 元素的宽度

svgH: SVG 元素的高度

viewBoxW: SVG 元素的 <code>viewBox</code> 属性的宽度

viewBoxH: SVG 元素的 <code>viewBox</code> 属性的高度

curW: 图形的当前宽度

curH: 图形的当前高度

了解变量含义之后,我们可以开始计算校准参数了。

用以下公式计算图形的当前位置:

下面这个公式是用于计算比例:

要记住 <code>viewBox</code> 属性的 <code>x</code> 和 <code>y</code> 值会裁切图形(如图 2.4)。所以,我们需要从初始点值中去掉这部分值。

<a href="https://github.com/aleen42/PersonalWiki/raw/master/post/how_to_draw/2.png" target="_blank"></a>

图 2.4 裁切图形

我只需要边缘点的最大值和最小值。举个栗子,如果点集的位置在图形之外,我就改变 <code>x</code> 或 <code>y</code>,甚至全部改变,重写到图形的边上。

据我所知,当点的数量很大的时候,删除范围外的点要比重写更好。

现在,我们已经知道怎么用 JavaScript 绘制 <code>path</code> 元素。上文说道,在绘制 <code>rect</code>、<code>polyline</code>、<code>circle</code> 等其他元素定义的形状之前,应该先转换成路径。本节,就来介绍一下做法。

圆和椭圆元素是近亲,相同属性如表 2.1 所示:

椭圆

CX

CY

表 2.1相同属性

不同属性如表 2.2 所示:

R

RX

RY

表 2.2不同属性

路径转换方法如下:

对于这些元素,要提取 <code>points</code> 属性。按路径元素的 <code>d</code> 值的特定格式重新组装。

一般来说,<code>line</code> 元素有多个属性用于线的定位:<code>x1</code>,<code>y1</code>,<code>x2</code> 和 <code>y2</code>。

很简单,我们可以这么计算:

矩形也有一些用于定位和决定大小的属性:<code>x</code>,<code>y</code>,<code>width</code> 和 <code>height</code>。

形状转换成 <code>path</code> 的方法已经全部讲解完毕。你可以用这些方法绘制上述图形。

除了 SVG 文件,我们还想绘制像 PNG,JPG,或者 GIF 格式的图像。仅仅由像素数据组成,我们是无法直接使用的。因此,我尝试用计算机视觉领域的一个常见技术,Canny 边缘检测算法。用这种算法,可以简单地找到位图的轮廓。

在处理之前,我们要定义一些通用函数。第一个是 <code>runImg</code> 函数,通常用在从 Canvas 中加载图片时,将其转换成由数组组成的矩阵。

然而,针对 <code>imgData</code> 还有一些操作,<code>imgData</code> 是 Canvas 中 Context 对象的 <code>Context.prototype.getImageData(x, y, width, height)</code> 这 个 prototype 方法的返回值变量。

现在,可以开始找轮廓了,点击 Run 运行 Codepen 上给出的例子。由于有一定的复杂性,要等一会儿才能在屏幕上看到结果。

灰度在维基百科上的定义如下:

在摄影和计算领域,灰度 或者说 灰度 数字图像是每个像素值都是单个采样的图片,即,这样的图片只携带亮度信息。

本节,我们将用两个方法实现灰度处理:

栗子如下:

高斯模糊是增加边缘检测精度的一个方法,也是 Canny 边缘检测的第一步。

如果你想检查效果,改变 sigma 和 size 参数返回演示如下,

在这步,我们将找到图片的亮度梯度(G)。在之前,我们要得到边缘检测器(Roberts,Prewitt,Sobel等)第一步在水平方向(Gx)和垂直方向(Gy)的衍生值。我们用的是 Sobel 探测器。

在处理灰度之前,我们应该导出一个模块,用于操作像素,我们命名为 Pixel。

用 Pixel 开始实现梯度处理:

样例如下:

非极大值抑制应用到 “薄” 边。梯度计算后,从梯度值中提取的边缘仍然很模糊。根据范式 3,边缘只能有一个精确值。所以非极大值抑制能够帮助抑制除了本地极大值之外的其他值,指出亮度值改变最大的位置。

最后一步是计算 <code>dirMap</code> 和 <code>graphMap</code>:

抑制之后,看起来比以前效果要好:

无论如何,这个所谓的 “弱” 边还需要进一步加工。Canny 磁滞是 Canny 边缘检测的改进方法。

从图中删除 “弱” 边之后是什么样的呢?

哇,看起来更完美了。

这幅图只有两种像素:0 和 255,可以通过扫描每个像素生成点路径。算法描述如下:

循环获取像素值, 检测是否被标记为255值.

匹配之后,找出生成最长路径的方向。(当一条路径是由自身组成的,每个像素都会被标记,当一条路径的点有超过一个值,就是一条真实路径,6 ~ 10。)

扫描之后,提取 SVG 的路径数据,当然你还可以绘制路径。

本文详细地讨论了如何用 JavaScript 绘图,不管是 SVG 文件还是其他类型图片,比如 PNG、JPG 和 GIF。核心思想是转换特定格式到路径数据。一旦抽离出这样的数据,我们还可以模进行模拟绘图。

直接绘制 SVG 文件中的 <code>path</code> 元素。

如果是其他元素,例如 <code>rect</code>,需要先转换成 <code>path</code>。

使用 Canny 轮廓检测算法检测位图中的轮廓,这样才可以绘制.

<b>原文发布时间为:2016年12月06日</b>

<b>本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。</b>

继续阅读