SVG绘图-SVG.js

前言

本文是在SVG.js 3.0的前提上,和2.x的API不一致。

官方文档:https://svgjs.dev/docs/3.0/getting-started/

这个库相比原生开发有以下几点优点:

  1. API调用简单
  2. 组件定位方式统一,比如原生圆形设置的是中心点,而矩形就又是左上角,这样设置位置的时候还要判断是什么图形,分别计算设置。

引用

1
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"></script>

或者

安装

1
npm install @svgdotjs/svg.js

引用

1
import { SVG } from '@svgdotjs/svg.js'

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>SVGJS</title>
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"></script>
</head>
<body>
<div id="drawing"></div>
</body>
<script type="text/javascript">
var draw = SVG().size("100%", "100%").addTo("#drawing");
draw.rect(100, 100).attr({ fill: "#f06" });
</script>
<style>
body {
margin: 0;
padding: 0;
}

#drawing {
width: 100vw;
height: 100vh;
}
</style>
</html>

SVG()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建使用该方法
var draw = SVG()
var draw = SVG().addTo('#drawing')

// 这个方法只能获取不能创建
var rect = SVG('#myRectId')
// 不写#也是按ID获取
var rect = SVG('myRectId')

// any css selector will do
var path = SVG('#group1 path.myClass')

// 创建图形
var circle = SVG('<circle>')

// 转换dom为svgjs对象
var obj = SVG(node)

各种图形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>SVGJS</title>
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"></script>
</head>
<body>
<div id="drawing"></div>
</body>
<script type="text/javascript">
var draw = SVG().size("100%", "100%").addTo("#drawing");

// 圆形
var c1 = draw
.circle(60)
.move(0, 0)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });

// 矩形
var r1 = draw
.rect(100, 60)
.move(0, 70)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });
// 圆角矩形
var r2 = draw
.rect(100, 60)
.radius(10)
.move(110, 70)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });
// 两侧椭圆矩形
var r3 = draw
.rect(100, 60)
.radius(30)
.move(220, 70)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });
// 正方形
var r4 = draw
.rect(60, 60)
.move(330, 70)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });

// 椭圆形
var e1 = draw
.ellipse(100, 60)
.move(0, 140)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });
// 菱形
var polygon = draw
.polygon("0,50 50,0 100,50 50,100")
.move(0, 210)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });

// 红心
var p1 = draw
.path(
"M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z"
)
.move(0, 320)
.fill("#ffe7f4")
.stroke({ width: 1, color: "#f883c9" });

// 线
var line = draw
.line(0, 0, 100, 60)
.move(0, 410)
.stroke({ width: 2, color: "#f06" });

// 图片
var image = draw.image("./imgs/jian.jpg").size(100, 100).move(0, 480);

// 文字
var text = draw
.text("码客说")
.font({
family: "Helvetica",
size: 34,
anchor: "middle",
leading: "1.5em"
})
.move(110, 513)
.fill("#ffffff")
.stroke({ width: 1, color: "#f883c9" });
</script>
<style>
body {
margin: 0;
padding: 10px;
background-color: #f3f3f3;
}

#drawing {
width: calc(100vw - 20px);
height: calc(100vh - 20px);
background-color: #ffffff;
}
</style>
</html>

效果

image-20211203232258148

注意

polygon的实例生成后调用move方法会报错,这里建议用g包裹一下。

绘制虚线框

1
2
3
4
5
6
7
8
9
10
11
12
13
var border_color = "#67aeef";
var bg_color = "#e7f7fe";

const half_width = width / 2;
const half_height = height / 2;
const group = svg.group();
group.addClass("xv_kuang");
group
.rect(width, height)
.move(x - half_width, y - half_height)
.fill(bg_color)
.stroke({width: 2,color: border_color})
.attr("stroke-dasharray", "5,15");

主要就是添加属性.attr("stroke-dasharray", "5,15");

更新线

1
2
3
4
5
6
7
8
var line = that.mSvg.findOne(".line_" + lineid);
if (line) {
let line_arr = line.array();
line.plot([
line_arr[0],
[evt.offsetX, evt.offsetY]
]);
}

获取线的数组 line.array(); 这是一个二位数组。

更新线

1
2
3
4
line.plot([
[0,0],
[100, 100]
]);

更新Path

1
2
3
4
var path = draw.path('M10 10L90 90');

// 更新path
path.plot('M10 10L50 50L90 90');

查找

1
2
3
var path = SVG(".line_" + id);
// 更新path
path.plot('M10 10L50 50L90 90');

绘制线与箭头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var path = draw.path("M0 0 A50 50 0 0 1 50 50 A50 50 0 0 0 100 100");

path.fill("none").move(20, 20).stroke({ width: 1, color: "#666" });

path.marker("start", 12, 12, function (add) {
add.path("M2,2 L10,6 L2,10 L5,6 L2,2").fill("#666");
});
path.marker("mid", 12, 12, function (add) {
add.path("M2,2 L10,6 L2,10 L5,6 L2,2").fill("#666");
});
path.marker("end", 12, 12, function (add) {
add.path("M2,2 L10,6 L2,10 L5,6 L2,2");
this.fill("#666");
});

效果

image-20211205231006892

直线和箭头

1
2
3
4
5
6
7
8
9
10
11
12
var line = draw
.line([
[0, 0],
[100, 60]
])
.move(20, 20)
.stroke({ width: 2, color: "#666" });

line.marker("end", 12, 12, function (add) {
add.path("M2,2 L10,6 L2,10 L5,6 L2,2");
this.fill("#666");
});

效果

image-20211205231517549

容器

SVG.G

1
2
3
4
5
var group = draw.group();
group.rect(100, 100).move(0, 0).fill("#f09");
group.circle(100).move(0, 110).fill("#f09");

group.move(100, 0);

SVG.Symbol

Symbol相当于模板,模板中能够添加多个元素。

1
2
3
4
5
var symbol = draw.symbol();
symbol.rect(100, 100).move(0, 0).fill("#f09");
symbol.circle(100).move(0, 110).fill("#f09");

var use = draw.use(symbol).move(110, 0);

SVG.Defs

基本用法

1
2
3
var defs = draw.defs();
var r1 = defs.rect(100, 100).fill("#f09");
var use = draw.use(r1).move(110, 0);

对比

1
2
var r1 = draw.rect(100, 100).fill("#f09");
var use = draw.use(r1).move(110, 0);

我们会发现

前者相当于创建的模板定义,再使用,模板本身是不显示的。

后者是以现存的元素为模板复制了一份,两个元素都会显示。

Defs和Symbol

defssymbol的相同点

  • defs元素用于预定义一个元素使其能够在SVG图像中重复使用。
    symbol元素用于定义可重复使用的符号。
  • 嵌入在defssymbol元素中的图形是不会被直接显示的,除非你使用元素来引用它。

defssymbol的不同点

  • xlink定义了一套标准的在 XML 文档中创建超级链接的方法,可以用它来引用元素或内定义的元素和组。
  • 一个symbol元素可以有preserveAspectRatioviewBox属性。而g元素不能拥有这些属性。

因此相比于在defs元素中使用g的方式来复用图形,使用symbol元素也许是一个更好的选择。

Defs也相当于定义,不同与Symbol,定义本身不能直接use,定义下的元素才能使用,要实现上面的效果还要用group把多个元素包起来。

同样功能两者实现对比:

Symbol

1
2
3
4
5
var symbol = draw.symbol();
symbol.rect(100, 100).move(0, 0).fill("#f09");
symbol.circle(100).move(0, 110).fill("#f09");

var use = draw.use(symbol).move(110, 0);

Defs

1
2
3
4
5
6
var defs = draw.defs();
var group = defs.group();
group.rect(100, 100).move(0, 0).fill("#f09");
group.circle(100).move(0, 110).fill("#f09");

var use = draw.use(group).move(110, 0).size(100, 100);

SVG.A

1
2
var link = draw.link('https://www.psvmc.cn')
var rect = link.rect(100, 100)

事件

1
2
3
4
element.on(['click', 'mouseover'], function(event){
console.info(this);
console.info(event);
})

或者

1
2
3
4
element.on('click mouseover', function(event){
console.info(this);
console.info(event);
})

标记

DOM添加类

1
2
path.addClass("zline");
path.addClass("line_" + id);

获取方式

1
2
3
SVG.find(".zline").on('click', function (event) {
console.info(this.node.classList);
})

可以通过选择器查找

1
this.drawObj.find(".line_" + id);

或者

1
SVG(".line_" + id);

DOM添加ID

1
path.attr({id: "line_" + id});

获取方式

1
2
3
SVG.find(".zline").on('click', function (event) {
console.info(this.node.id);
})

可以通过选择器查找

1
this.mSvg.find("#line_" + id);

或者

1
SVG("#line_" + id);

对象添加ID

下面这种是添加在了对象上,不是DOM上,不能通过选择器来查找

1
path.id = "line_" + id;

获取方式

1
2
3
SVG.find(".zline").on('click', function (event) {
console.info(this.id);
})

查找元素

示例

SVG查找元素时支持按样式查找,所以在查找前,我们要先把元素添加样式元素上添加样式

我推荐的命名方式为,如果分组下的元素

分组的样式为g_{id},里面的元素为text_{id}等。

1
2
3
4
5
6
7
8
9
10
11
12
13
//文字的宽度
var t_w = font_size * text.length;
group
.text(text)
.font({
family: "微软雅黑",
size: font_size,
anchor: "middle",
baseline: "middle",
})
.move(x - half_width + (width - t_w) / 2, y - font_size / 2 - 3)
.fill(text_color)
.addClass('text_' + id);

查找返回数组

1
2
3
4
5
6
7
8
9
10
11
12
watch: {
select_sharp: {
handler: function () {
console.info(this.select_sharp);
var t_arr = this.mSvg.find(".text_" + this.select_sharp.id);
if (t_arr && t_arr.length > 0) {
t_arr[0].text(this.select_sharp.text);
}
},
deep: true
}
}

当然我们在查找时也可以取一个

1
2
3
4
var t_obj = this.mSvg.findOne(".text_" + this.select_sharp.id);
if (t_obj) {
t_obj.text(this.select_sharp.text);
}

也可以这样写

1
SVG(".text_" + this.select_sharp.id).text(this.select_sharp.text);

官方文档

SVG()

returns SVG.Dom返回首个匹配的元素

1
var rect = SVG('rect.my-class').fill('#f06')

element.findOne()

returns SVG.Dom返回首个匹配的元素

1
var rect = group.findOne('rect.my-class').fill('#f06')

SVG.find()

returns SVG.List返回匹配到的元素列表

1
2
3
var list = SVG.find('.someClass')

list.fill('#f06')

可以设置第二个参数来限制搜索范围:

1
2
3
var list = SVG.find('.someClass', group)

list.fill('#f06')

element.find()

returns SVG.List返回匹配到的元素列表

1
2
3
var list = group.find('.someClass')

list.fill('#f06')

常用方法

删除

1
element.remove()

hide()

returns itself

Hide element:

1
element.hide()

show()

returns itself

Show (unhide) element:

1
element.show()

visible()

returns boolean

To check if the element is visible:

1
element.visible()

SVG转图片

https://github.com/exupero/saveSvgAsPng

原生线的绘制

先看看原生的怎么实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<svg
width="500"
height="500"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<marker
id="arrow"
markerUnits="strokeWidth"
markerWidth="12"
markerHeight="12"
viewBox="0 0 12 12"
refX="6"
refY="6"
orient="auto"
>
<path d="M2,2 L10,6 L2,10 L2,2" style="fill: #666" />
</marker>
</defs>
<!--此处的marker-mid无效-->
<line
x1="100"
y1="10"
x2="300"
y2="50"
stroke="#666"
stroke-width="2"
marker-start="url(#arrow)"
marker-mid="url(#arrow)"
marker-end="url(#arrow)"
></line>

<path
d="M10,95 C25,15 105,10 125,95 C145,180 225,185 240,95"
stroke="#666"
stroke-width="2"
fill="none"
marker-start="url(#arrow)"
marker-mid="url(#arrow)"
marker-end="url(#arrow)"
/>
</svg>

效果

image-20211205224331092

三次贝塞尔曲线

我们先看一条贝塞尔曲线

1
2
3
<path d="M10 10 C 50 10, 70 110, 110 110" stroke="1" fill="none"/>

<path d="M10 10 S 70 110, 110 110" stroke="1" fill="none"/>

其中

M 是移动到,这里是曲线的起点。

后面的C/S是贝塞尔曲线的指令。

  • C = curveto
  • S = smooth curveto

三次贝塞尔曲线需要两个控制点。

假如我们要绘制的线的两个控制点(x1,y1)和(x2,y2),曲线的终点(x,y)

使用C

三次贝塞尔曲线指令:

C x1 y1, x2 y2, x y

使用S

C指令有三个坐标参数,而S指令自动对称一个控制点,因此,跟在C指令之后的S指令,只需要2个参数哦,如下:

三次贝塞尔曲线指令:

S x2 y2, x y

S自动生成的另一个辅助点可能跟预想的不一样,建议使用C。

其他命令

其中路径描述包含如下命令:

  • M = moveto 移动到某点。
  • L = lineto 画一条直线到某点。
  • H = horizontal lineto 画一条水平线到某点。
  • V = vertical lineto 画一条垂直线到某点。
  • Q = quadratic Bézier curveto 二次贝塞尔曲线
  • T = smooth quadratic Bézier curveto 平滑二次贝塞尔曲线
  • C = curveto 三次贝塞尔曲线
  • S = smooth curveto 平滑三次贝塞尔曲线
  • A = elliptical Arc 弧形
  • Z = closepath 从结束点到开始点画一条直线,形成一个闭合的区域。

以上所有命令均允许小写字母:

  1. 大写表示绝对定位,绝对的参照点是svg最上角的那一点。
  2. 小写表示相对定位,相对的参照点是上一个位置。