开始

说到这里,就不得不提SVG的路径操作了,因为ZRender完全的模拟了SVG原生的path元素的用法,很是强大。 关于SVG的Path,请看这里: Path (英文版) 或者 【MDN】SVG教程(5) 路径 [译] (中文版), 很明显的是canvas中的路径没有SVG的用着舒服,那到底ZRender是如何实现的呢,让我给你娓娓道来(不过要想继续进行下去,上面的SVG的PATH必须了解。)。

示例

打开API,shape.path,可以看到,path的配置有MLHVCSQTZ等字母组成的字符串,svg的path也支持小写,也有一个A命令,难道ZRender没有实现? 错,实现了,只是在API上没有写明而已,支持大小写,支持A(圆弧)命令!为了证明我所说,来个示例:

require(
[
'../src/zrender', '../src/shape/Path'
], function( zrender, PathShape )
{ var box = document.getElementById('box');
var zr = zrender.init(box); zr.addShape(new PathShape(
{
style:
{
x: 0,
y: 0,
path: 'M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z',
color: '#F60',
textPosition: 'inside',
textColor: 'red',
strokeColor: 'black'
},
draggable: true
})); zr.addShape(new PathShape(
{
style:
{
x: 0,
y: 0,
path: 'M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z',
color: '#F60',
textPosition: 'inside',
textColor: 'red',
strokeColor: 'black'
},
draggable: true
})); zr.render();
});

得到如下结果:


再用SVG来一个相同配置的:

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<path d="M80 80
A 45 45, 0, 0, 0, 125 125
L 125 80 Z" fill="#F60"/>
<path d="M230 80
A 45 45, 0, 1, 0, 275 125
L 275 80 Z" fill="#F60"/>
</svg>

好吧,得到的结果一模一样,我就不贴图了。不多说了,这就是移植,我喜欢。

_parsePathData

打开zrender/shape/Path,buildPath先调用的就是_parsePathData,作用为:解析path字符串为数组命令,也就是个解析器嘛。

 _parsePathData : function(data) {
if (!data) {
return [];
} // command string
var cs = data; // command chars
var cc = [
'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z',
'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'
]; cs = cs.replace(/-/g, ' -');// M 100 100 L 100 200 L 100-200 Z -> M 100 100 L 100 200 L 100 -200 Z
cs = cs.replace(/ /g, ' ');// M 100 100 L 100 200 L 100 -200 -> M 100 100 L 100 200 L 100 -200 -> M 100 100 L 100 200 L 100 -200
cs = cs.replace(/ /g, ',');// M 100 100 L 100 200 L 100 -200 -> M,100,100,L,100,200,L,100,-200
cs = cs.replace(/,,/g, ',');//如果出现两个逗号,换成一个逗号 -> M,100,100,L,100,200,L,100,-200 //cs = cs.replace(/-/g, ' -').replace(/ /g, ' ').replace(/ /g, ',').replace(/,,/g, ','); 这样写,会不会很帅气,(- var n;
// create pipes so that we can split the data
for (n = 0; n < cc.length; n++) {
cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
} // |M,100,100,|L,100,200,|L,100,-200 // create array
var arr = cs.split('|'); // ['','M,100,100,','L,100,200,','L,100,-200']
var ca = [];
// init context point
var cpx = 0; //cpx和cpy是循环里的全局都在使用,小写命令是累计计算,大写命令是复制计算。
var cpy = 0;
for (n = 1; n < arr.length; n++) { // 从1开始,因为第一个元素肯定为空
var str = arr[n]; // M,100,100,
var c = str.charAt(0); // M
str = str.slice(1); //,100,100,
str = str.replace(new RegExp('e,-', 'g'), 'e-'); var p = str.split(',');// ['','100','100','']
if (p.length > 0 && p[0] === '') {
p.shift();
}
// ['100','100',''] for (var i = 0; i < p.length; i++) {
p[i] = parseFloat(p[i]);
} // [100,100,NaN] while (p.length > 0) {
if (isNaN(p[0])) {
break;
}
var cmd = null;
var points = []; var ctlPtx;
var ctlPty;
var prevCmd; var rx;
var ry;
var psi;
var fa;
var fs; var x1 = cpx;
var y1 = cpy; // convert l, H, h, V, and v to L
switch (c) {
case 'l':
cpx += p.shift();
cpy += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'L':
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
//在l的时候,是直接相加的,而L的时候,是直接赋值的 ,这就说明大小写是不一样的
// L 表示lineTo
case 'm':
cpx += p.shift();
cpy += p.shift();
cmd = 'M';
points.push(cpx, cpy);
c = 'l';
break;
case 'M':
cpx = p.shift();
cpy = p.shift();
cmd = 'M';
points.push(cpx, cpy);
c = 'L';
break;
// M 表示moveTo
case 'h':
cpx += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'H':
cpx = p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
// H 表示水平lineTo,只改变X值
case 'v':
cpy += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'V':
cpy = p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
// H 表示垂直lineTo,只改变Y值
case 'C':
points.push(p.shift(), p.shift(), p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
case 'c':
points.push(
cpx + p.shift(), cpy + p.shift(),
cpx + p.shift(), cpy + p.shift()
);
cpx += p.shift();
cpy += p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
// C表示二次贝塞尔曲线
case 'S':
ctlPtx = cpx;
ctlPty = cpy;
prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'C') {
ctlPtx = cpx + (cpx - prevCmd.points[2]);
ctlPty = cpy + (cpy - prevCmd.points[3]);
}
points.push(ctlPtx, ctlPty, p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 's':
ctlPtx = cpx, ctlPty = cpy;
prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'C') {
ctlPtx = cpx + (cpx - prevCmd.points[2]);
ctlPty = cpy + (cpy - prevCmd.points[3]);
}
points.push(
ctlPtx, ctlPty,
cpx + p.shift(), cpy + p.shift()
);
cpx += p.shift();
cpy += p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
// C表示光滑二次贝塞尔曲线
case 'Q':
points.push(p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
case 'q':
points.push(cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'Q';
points.push(cpx, cpy);
break;
// Q表示三次贝塞尔曲线
case 'T':
ctlPtx = cpx, ctlPty = cpy;
prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'Q') {
ctlPtx = cpx + (cpx - prevCmd.points[0]);
ctlPty = cpy + (cpy - prevCmd.points[1]);
}
cpx = p.shift();
cpy = p.shift();
cmd = 'Q';
points.push(ctlPtx, ctlPty, cpx, cpy);
break;
case 't':
ctlPtx = cpx, ctlPty = cpy;
prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'Q') {
ctlPtx = cpx + (cpx - prevCmd.points[0]);
ctlPty = cpy + (cpy - prevCmd.points[1]);
}
cpx += p.shift();
cpy += p.shift();
cmd = 'Q';
points.push(ctlPtx, ctlPty, cpx, cpy);
break;
// Q表示光滑三次贝塞尔曲线
case 'A':
rx = p.shift(); //椭圆的x轴半径
ry = p.shift(); //椭圆的y轴半径
psi = p.shift();//椭圆的旋转角度
fa = p.shift();//角度大小 0表示小角度,1表示大弧度
fs = p.shift();//弧线方向 0表示从起点到终点沿逆时针画弧,1表示从起点到终点沿顺时针画弧 x1 = cpx, y1 = cpy; //开始的点
cpx = p.shift(), cpy = p.shift(); //结束的点
cmd = 'A';
points = this._convertPoint(
x1, y1, cpx, cpy, fa, fs, rx, ry, psi
);
break;
case 'a':
rx = p.shift();
ry = p.shift();
psi = p.shift();
fa = p.shift();
fs = p.shift(); x1 = cpx, y1 = cpy;
cpx += p.shift();
cpy += p.shift();
cmd = 'A';
points = this._convertPoint(
x1, y1, cpx, cpy, fa, fs, rx, ry, psi
);
break;
// A是啥玩意?
} ca.push({
command : cmd || c,
points : points
});
} //如果是z,z不去分大小写,直接push进入,points为空数组
if (c === 'z' || c === 'Z') {
ca.push({
command : 'z',
points : []
});
}
} return ca;
}
  • 如果没有data,直接返回空数组
  • 将传入的data赋值给cs,将cs进行一系列的replace(将-换成 -,将两个空格换成一个空格,将一个空格换成逗号,将两个逗号换成一个逗号),这些,都是为了兼容SVG的规法和各种不规范的写法
  • 将cs用竖线加命令字符分隔开,便于下一步进行再次分隔
  • 再用竖线将字符串变成数组,声明ca(最后所返回的值),声明cpx和cpy(绘制路径的起点,相对于下面的循环,是一个全局性质的变量)
  • 遍历arr,其中c是命令符,经过处理,最后的点坐标,被赋值到p变量上
  • 开始while循环,真正的往ca中push值,进入switch,如果是命令是大写的cpx直接被赋值为p中的点,如果是小写的,会在原来的cpx和cpy的基础上进行累加。(具体用法可以参见那篇SVG的文章)
  • 这些命令的意思在注释中已经写明,唯一需要说的是A(圆弧),这个比较复杂,需要细细体会,我就不分析了,不过也可以看这里,如果作者有回应的话。 https://github.com/ecomfe/zrender/issues/98
  • 最后返回的ca是一个数组,看下图:

创建路径 buildPath

buildPath : function(ctx, style) {
var path = style.path; var pathArray = this.pathArray || this._parsePathData(path); // 平移坐标
var x = style.x || 0;
var y = style.y || 0; var p;
// 记录边界点,用于判断inside
var pointList = style.pointList = [];
var singlePointList = [];
for (var i = 0, l = pathArray.length; i < l; i++) {
if (pathArray[i].command.toUpperCase() == 'M') { // 如果是M,说明又画了一个新的区域,就把原来的singlePointList塞入到最终结果中,再把singlePointList清空
singlePointList.length > 0
&& pointList.push(singlePointList);
singlePointList = [];
}
p = pathArray[i].points;
for (var j = 0, k = p.length; j < k; j += 2) { //把所有的point点塞入singlePointList
singlePointList.push([p[j] + x, p[j+1] + y]);
}
}
singlePointList.length > 0 && pointList.push(singlePointList); //如果存在点,塞入最终结果里 var c;
for (var i = 0, l = pathArray.length; i < l; i++) {
c = pathArray[i].command;
p = pathArray[i].points;
// 平移变换
for (var j = 0, k = p.length; j < k; j++) { //style.x和style.y是一个参考点
if (j % 2 === 0) {
p[j] += x;
} else {
p[j] += y;
}
}
switch (c) {
case 'L':
ctx.lineTo(p[0], p[1]);
break;
case 'M':
ctx.moveTo(p[0], p[1]);
break;
case 'C':
ctx.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]);
break;
case 'Q':
ctx.quadraticCurveTo(p[0], p[1], p[2], p[3]);
break;
// 这几个做法就比较明显了,调用了原生CanvasAPI,但是A呢,对了,在SVG中,是弧形,
// 文档中也不写,作者好低调,赞!
case 'A':
var cx = p[0];
var cy = p[1];
var rx = p[2];
var ry = p[3];
var theta = p[4];
var dTheta = p[5];
var psi = p[6];
var fs = p[7];
var r = (rx > ry) ? rx : ry;
var scaleX = (rx > ry) ? 1 : rx / ry;
var scaleY = (rx > ry) ? ry / rx : 1; ctx.translate(cx, cy);
ctx.rotate(psi);
ctx.scale(scaleX, scaleY);
ctx.arc(0, 0, r, theta, theta + dTheta, 1 - fs);
ctx.scale(1 / scaleX, 1 / scaleY);
ctx.rotate(-psi);
ctx.translate(-cx, -cy);
break;
case 'z':
ctx.closePath();
break;
}
} return;
},
  • xy是一个绘制路径的参考点,如果用户没有指定,这里默认为0 0
  • 记录边界点,用于判断inside,这里对M的判断主要是处理一个命令画多个区域的问题。而判断inside的作用主要是在Base类里drawText的时候用到
  • 开始遍历pathArray(即上面说的ca),style.x/style.y是一个参考点,所有的坐标都会加上这个参考点,即为平移变换。
  • 进入switch进行真正的canvas原生API绘制路径,最后碰到z,进行closePath
  • A我就不说了,没找到这个算法的相关资料,欢迎大家指导。

热区 getRect

getRect : function(style) {
if (style.__rect) {
return style.__rect;
} var lineWidth;
if (style.brushType == 'stroke' || style.brushType == 'fill') {
lineWidth = style.lineWidth || 1;
}
else {
lineWidth = 0;
} var minX = Number.MAX_VALUE;
var maxX = Number.MIN_VALUE; var minY = Number.MAX_VALUE;
var maxY = Number.MIN_VALUE; // 平移坐标
var x = style.x || 0;
var y = style.y || 0; var pathArray = this.pathArray || this._parsePathData(style.path);
for (var i = 0; i < pathArray.length; i++) {
var p = pathArray[i].points; for (var j = 0; j < p.length; j++) {
if (j % 2 === 0) { // 0,2,4,6,8....为x值
if (p[j] + x < minX) {
minX = p[j] + x;
}
if (p[j] + x > maxX) {
maxX = p[j] + x;
}
}
else { // 1,3,5,7,9...为y值
if (p[j] + y < minY) {
minY = p[j] + y;
}
if (p[j] + y > maxY) {
maxY = p[j] + y;
}
}
}
} var rect;
if (minX === Number.MAX_VALUE
|| maxX === Number.MIN_VALUE
|| minY === Number.MAX_VALUE
|| maxY === Number.MIN_VALUE
) {
rect = {
x : 0,
y : 0,
width : 0,
height : 0
};
}
else {
rect = {
x : Math.round(minX - lineWidth / 2),
y : Math.round(minY - lineWidth / 2),
width : maxX - minX + lineWidth,
height : maxY - minY + lineWidth
};
}
style.__rect = rect;
return rect;
}
  • 关于Number.MAX_VALUE和Number.MIN_VALUE,请看这里:JavaScript Number 对象
  • 获得pathArray(即为上面说的ca),遍历之
  • 加上参考点x/y后分别跟最大值最小值作比较,最后得出靠谱的minX,minY,maxX,maxY,木有什么惊喜
  • 如果minX,minY,maxX,maxY原封未动,那就是pathArray出了问题(没有取到或者什么的),返回一个都是0的对象
  • 如果正常返回x,y,width,height,关于lineWidth的问题,前一篇有解释。

ZRender源码分析6:Shape对象详解之路径的更多相关文章

  1. 死磕 java并发包之AtomicStampedReference源码分析(ABA问题详解)

    问题 (1)什么是ABA? (2)ABA的危害? (3)ABA的解决方法? (4)AtomicStampedReference是什么? (5)AtomicStampedReference是怎么解决AB ...

  2. Laravel源码分析--Laravel生命周期详解

    一.XDEBUG调试 这里我们需要用到php的 xdebug 拓展,所以需要小伙伴们自己去装一下,因为我这里用的是docker,所以就简单介绍下在docker中使用xdebug的注意点. 1.在php ...

  3. spring源码分析之spring-web http详解

    spring-web是spring webmvc的基础,它的功能如下: 1. 封装http协议中client端/server端的request请求和response响应及格式的转换,如json,rss ...

  4. spring源码分析之spring-jms模块详解

    0 概述 spring提供了一个jms集成框架,这个框架如spring 集成jdbc api一样,简化了jms api的使用. jms可以简单的分成两个功能区,消息的生产和消息的消费.JmsTempl ...

  5. spring源码分析之spring-jdbc模块详解

    0 概述 Spring将替我们完成所有使用JDBC API进行开发的单调乏味的.底层细节处理工作.下表描述了哪些是spring帮助我们做好的,哪些是我们要做的. Action  Spring  You ...

  6. jvm源码解读--15 oop对象详解

    (gdb) p obj $15 = (oopDesc *) 0xf3885d08 (gdb) p * obj $16 = { _mark = 0x70dea4e01, _metadata = { _k ...

  7. spring源码分析之spring-messaging模块详解

    0 概述 spring-messaging模块为集成messaging api和消息协议提供支持. 其代码结构为: 其中base定义了消息Message(MessageHeader和body).消息处 ...

  8. ZRender源码分析5:Shape绘图详解

    回顾 上一篇说到:ZRender源码分析4:Painter(View层)-中,这次,来补充一下具体的shape 关于热区的边框 以圆形为例: document.addEventListener('DO ...

  9. ZRender源码分析2:Storage(Model层)

    回顾 上一篇请移步:zrender源码分析1:总体结构 本篇进行ZRender的MVC结构中的M进行分析 总体理解 上篇说到,Storage负责MVC层中的Model,也就是模型,对于zrender来 ...

随机推荐

  1. php.ini配置

    PHP作为一门强大的脚本语言被越来越多的web应用程序采用,不规范的php安全配置可能会带来敏感信息泄漏.SQL注射.远程包含等问题,规范的安全配置可保障最基本的安全环境.下面我们分析几个会引发安全问 ...

  2. PHP基础之 file_get_contents() 函数

    定义和用法 file_get_contents() 函数把整个文件读入一个字符串中. 和 file() 一样,不同的是 file_get_contents() 把文件读入一个字符串. file_get ...

  3. linux学习笔记之套接字

    一.基础知识. 1:套接字基础. 1,是通信端点的抽象. 2,在UNIX类系统中被当作是一种文件描述符. 3,套接字通信域. 域 描述 AF_INET IPV4因特网域 AF_INET6 IPV6因特 ...

  4. C# 读取xml节点类容

    这是一个测试节点类容的获取 这是控制台代码部分 注意的应用文件 :using.system.Xml using System; using System.Collections.Generic; us ...

  5. 关于tomcat的远程调试

    最近做项目开发发现,在本地运行好好的项目发布到测试服务器既然不好使了,很是郁闷,周围的大神们就给了一条明路:远程调试 查看了网上例子太多了,好像自己真的不会使用,就查了一些简单的资料发现其实很简单 下 ...

  6. 使用SQLiteHelper创建数据库并插入数据

    参考<疯狂android讲义>8.4节P424 1.获取SQLiteDatabase实例有2种方法,一是直接new SQLiteDatabase(),另一种使用SQLiteHelper.一 ...

  7. Ring3 和Ring0 解释

    这得从CPU指令系统(用于控制CPU完成各种功能的命令)的特权级别说起.在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃.比如:清内存.设置时钟等.如果所有的程序都能使用这些 ...

  8. python连接redis002

    例子001. 通过StrictRedis模式连接到redis.并调用get命令读取了一个string类型的值. #!/usr/bin/python #!coding:utf-8 import redi ...

  9. Thinking in C++: 第1章 为什么C++会成功(改进了C的缺点,可复用C的知识与库,执行效率相当)

    本文内容摘抄自C++经典书籍:<Thinking in C++>   操作概念:OOP程序像什么 我们已经知道,用C 语言编写的过程程序就是一些数据定义和函数调用.要理解这种程序的含义,程 ...

  10. qt 自动完成LineEdit

    原地址:http://www.cppblog.com/biao/archive/2009/10/31/99873.html     ---------------------------------- ...