二维码

图形开发学院(GraphAnywhere.com)

第五章 图形交互操作:平移和缩放

  在二维图形系统中,图形的缩放和平移是最基本的图形交互功能。图形的缩放是指按照一定的比例因子对图形中的要素进行放大或缩小,同时保持其形状不变的操作。图形的平移是指将图形上的所有点按照某个方向做相同距离的移动,而不改变图形本身的形状和大小。

5.1 实现原理

  缩放和平移需对图形对象的坐标值进行的运算,实现图形的缩放与平移的原理是 改变图形的显示范围, 而不是改变图形中各图形对象的坐标值。接下来我们通过学习“全图” 、“图形显示范围” 和 “分辨率” 等几个概念来理解缩放与平移的实现原理。

5.1.1 全图

  图形的全图是指整个图形或图像的全部内容。下面这张图形由20行40列的正方形组成,假设每个正方形的宽和高均为1000米,那么可推算出该图的宽为1000*40=40000米,高为1000*20=20000米。该图的最左上角坐标为(0,0),坐标的单位是米,那么该图X坐标范围为0至40000米,Y坐标范围为0至20000米。这张全图的坐标范围可记录为:[0, 0, 40000, 20000],这个坐标系即为图形坐标系(关于坐标系的概念可参考本系列教程的 第一章 基础知识 )。

运行效果

5.1.2 图形显示范围

  图形显示范围(Extent) 是指图形在屏幕或窗口中的可视区域,是一个由显示图形中最小坐标值(min)和最大坐标值(max)确定的矩形区域,图形显示范围也采用图形坐标来表达,可记录为[minX, minY, maxX, maxY]

  在 AnyGraph 中图形是通过Canvas渲染出来的,Canvas的大小由Canvas的宽和高决定,其单位是像素。此时我们需要面对两个坐标系:图形坐标系和Canvas的像素坐标系。

  假设当前Canvas的宽为1000个像素,高为600个像素,当每个像素显示图形宽度/高度为10米时,则此时Canvas可显示的图形的宽度和高度分别为:

  • 宽度:
  • 高度:

  如果当前显示范围的左上角从C5左上角开始显示,前面讲过每个正方形的宽和高均为1000米,那么Canvas左上角(0,0)像素点对应的图形坐标如下:

  • X坐标为:
  • Y坐标为:

  Canvas右下角像素坐标为(1000,600),当每个像素显示图形的距离为10米时, 对应的图形坐标如下:

  • X坐标为:
  • Y坐标为:

  这个区域就是图形的可视区域,此时的图形显示范围坐标为:[4000, 2000, 14000, 8000],这个区域称之为当前图形显示范围。我们用红色矩形外框表达图形显示范围在全图中的位置,如下图所示:

运行效果

  上图可以理解为是在计算机内存中包含的全图和图形显示范围,我们通过屏幕看到的仅仅包含了图形显示范围中的内容,其效果是下面这个样子的:

运行效果

  重新梳理一下上图的几个关键数据:

  1. 全图的图形坐标范围: [0, 0, 40000, 20000],其坐标系为图形坐标系
  2. Canvas坐标范围: [0, 0, 1000, 600],其坐标系为像素坐标系
  3. 当前的图形显示范围: [4000, 2000, 14000, 8000],其坐标系为图形坐标系

在SVG中,Canvas坐标范围称之为 ViewPort,当前图形显示范围称之为 ViewBox

5.1.3 分辨率

  在图形系统中图形坐标系与像素坐标系的比值称之为“分辨率”英文是 Resolution,上面提到的“当每个像素显示图形的距离为10米时” 用专业术语就是 “当前图形的分辨率为10” 。图形坐标系和屏幕坐标系均属于平面坐标系,可以进行线性变换,因此可使用以下公式计算resolution。

  在下图中包含了两张 “当前图形显示范围”:

运行效果

该图中:

  • 左侧显示的方格数为 , 其坐标范围为
  • 右侧显示的方格数为 , 其坐标范围为

两者的Canvas坐标范围均为:,因此:

  • 左侧的分辨率为:
  • 右侧的分辨率为:

  对比该图中左侧和右侧的显示,可以发现左侧图中的方格大小明显大于右侧图中的方格大小。因此可以得出以下结论:

  • 在同样大小的Canvas中,当分辨率 resolution 增大时,此时Canvas中显示更大的图形范围,能够显示更多的图形对象,同一个图形对象就会更小,这就是缩小操作;
  • 在同样大小的Canvas中,当分辨率 resolution 减少时,此时Canvas中显示更小的图形范围,能够显示更少的图形对象,,图形对象就会更大,这就是放大操作;
  • 在同样大小的Canvas中,当分辨率 resolution 不变时,图形显示范围发生了变换,图形就会出现平移,这就是平移操作;

5.1.4 图形交互操作

  下面两张图是图形平移和图形缩放功能的演示效果:

图形平移

运行效果

上图中:

  • 当图形向上移动时,每移动一次图形显示范围的Y坐标减少了500
  • 当图形向下移动时,每移动一次图形显示范围的Y坐标增加了500
  • 当图形向左移动时,每移动一次图形显示范围的X坐标减少了500
  • 当图形向右移动时,每移动一次图形显示范围的X坐标增加了500

图形缩放

运行效果

上图中:

  • 当图形放大时,每放大一次,其分辨率减少了1,图形的宽和高分别减少了1000
  • 当图形缩小时,每缩小一次,其分辨率增加了1,图形的宽和高分别增加了1000

5.2 边界范围类 (Extent)

  除了图形显示范围外,各种图形对象也有自己的边界范围(Bounding Box),其概念和图形显示范围类似,也是由一个矩形区域包围,由最小值(min)和最大值(max)确定,均可以记录为[minX, minY, maxX, maxY]

  由于边界范围在许多功能中都应用到,因此 AnyGraph 中设计一个 Extent 类,用于实现针对图形显示范围的常用功能,这些功能如下表所示:

名称 说明
getCenter(extent) 计算中心点
getWidth(extent) 计算宽度
getHeight(extent) 计算高度
getArea(extent) 计算面积
buffer(extent, val) 计算空间范围的缓冲区
containsXY(extent, point) 判断点是否在空间范围内
containsExtent(extent1, extent2) 判断extent2是否在extent1内
intersects(extent1, extent2) 判断两个空间范围是否相交

这些功能的实现均比较简单,可通过源代码查看实现过程,其源代码如下:

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
106
107
108
/**
* 边界坐标范围
* 注意:屏幕坐标系(左上角、右下角)的设定:x正方向为自左向右,y正方向为自上向下,常规笛卡尔坐标系(左下角、右上角)与屏幕坐标系y方向相反
*/
class Extent {

/**
* 建立一个边界范围对象
* @param {number} xmin - x方向最小值
* @param {number} ymin - y方向最小值
* @param {number} xmax - x方向最大值
* @param {number} ymax - y方向最大值
* @returns Extent 边界范围值
*/
static create(xmin, ymin, xmax, ymax) {
return [xmin, ymin, xmax, ymax];
}

/**
* 计算中心点
* @param {Extent} extent
* @returns 中心点坐标
*/
static getCenter(extent) {
let center = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
return center;
}

/**
* 计算宽度
* @param {Extent} extent
* @returns width
*/
static getWidth(extent) {
return extent[2] - extent[0];
}

/**
* 计算高度
* @param {Extent} extent
* @returns height
*/
static getHeight(extent) {
return extent[3] - extent[1];
}

/**
* 计算尺寸
* @param {Extent} extent
* @returns [width, height]
*/
static getSize(extent) {
return { "width": Math.abs(extent[2] - extent[0]), "height": Math.abs(extent[3] - extent[1]) };
}

/**
* 计算面积
* @param {Extent} extent
* @returns 面积
*/
static getArea(extent) {
let area = 0;
if (!this.isEmpty(extent)) {
area = this.getWidth(extent) * this.getHeight(extent);
}
return area;
}

/**
* 计算缓冲区范围
* @param {Extent} extent
* @param {number} value
* @returns Extent 边界范围值
*/
static buffer(extent, value) {
return [extent[0] - value, extent[1] - value, extent[2] + value, extent[3] + value];
}

/**
* 判断点是否在空间范围内
* @param {Extent} extent
* @param {Coord} point
* @returns Boolean
*/
static containsXY(extent, point) {
return extent[0] <= point[0] && point[0] <= extent[2] && extent[1] <= point[1] && point[1] <= extent[3];
}

/**
* 判断extent2是否在extent1内
* @param {Extent} extent1
* @param {Extent} extent2
* @returns Boolean
*/
static containsExtent(extent1, extent2) {
return extent1[0] <= extent2[0] && extent2[2] <= extent1[2] && extent1[1] <= extent2[1] && extent2[3] <= extent1[3];
}

/**
* 判断两个空间范围是否相交
* @param {*} extent1
* @param {*} extent2
* @returns Boolean
*/
static intersects(extent1, extent2) {
return extent1[0] <= extent2[2] && extent1[2] >= extent2[0] && extent1[1] <= extent2[3] && extent1[3] >= extent2[1];
}
}

5.3 坐标转换

  接下来,我们继续讲述 图形交互操作-平移和缩放功能实现,在上面的 ‘图形显示范围’ 章节中,我们讲述了当前图形显示范围ViewBox 和 Canvas坐标范围 ViewPort的概念。很明显 ViewBox 的坐标范围和 ViewPort 的坐标范围是不一致的,为了将图形显示范围中的内容绘制到Canvas中,有两种方式可以实现该目的:

  1. 将各个图形对象坐标转换为Canvas画布像素坐标后绘制图形;
  2. 计算图形坐标与Canvas像素坐标之间变换矩阵,对Canvas进行矩阵变换,然后使用图形对象的坐标绘制图形对象。

  这两种方式各有优缺,甚至两者还会结合起来使用,就看我们的应用需求,这一小节我们讲述坐标转换的方法,下一小节中我们将讲述实现缩放与平移功能实现的几种方法。

5.3.1 等比变换(一次函数)

  屏幕像素坐标与图形坐标之间的相互转换仍旧可以借助图形显示范围来计算。其计算过程如下:

  • 已知条件:
    • 像素坐标范围:[0, 0, canvas.width, canvas.height]
    • 对应的的图形显示范围:[minX, minY, maxX, maxY]
    • 图形显示范围坐标[minX, minY]对应的像素坐标为[0, 0],图形显示范围坐标[maxX, maxY]对应的像素坐标为[canvas.width, canvas.height]
  • 问题:计算屏幕中任意一点对应的图形坐标值,或者是计算任意图形坐标值对应的屏幕坐标值?

  图形坐标系和屏幕像素坐标系均属于平面坐标系,可以进行线性变换, 因此这其实是一道使用一次函数求解的数学题目,整个计算过程分为两步,第一步计算两个坐标范围的比值(分辨率),第二步通过比例的性质求解。其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 坐标变换
* @param {*} originalCoord 原坐标值,其格式为[x,y]
* @param {*} originalExtent 原坐标区域,其格式为[minX, minY, maxX, maxY]
* @param {*} destExtent 目标坐标区域,其格式为[minX, minY, maxX, maxY]
* @returns 目标坐标值,其格式为[x,y]
*/
function convert(originalCoord, originalExtent, destExtent) {
let originalWidth = Extent.getWidth(originalExtent);
let destWidth = Extent.getWidth(destExtent);

// 分辨率
let res = destWidth / originalWidth

// 计算目标坐标值
let destX = res * (originalCoord[0] - originalExtent[0]) + destExtent[0];
let destY = res * (originalCoord[1] - originalExtent[1]) + destExtent[1];
return [destX, destY];
}

  当需要进行坐标转换时,可以这么使用:

1
2
3
4
5
6
7
8
let graphExtent = [4000, 2000, 14000, 8000];
let canvasExtent = [0, 0, 1000, 600];

// 将图形坐标转换为Canvas坐标
let pixel = convert([5000, 3000], graphExtent, canvasExtent);

// 将Canvas坐标转换为图形坐标
let coord = convert([120, 200], canvasExtent, graphExtent);

5.3.2 矩阵变换

  在 第一章 基础知识 的数学基础知识中我们梳理了矩阵变换的缩放、平移、旋转等计算公式和方法,并在 程序实现 章节中建立了Transform类, 在该类中实现了使用矩阵变换进行各种变换的坐标转换程序。使用矩阵变换可以很方便的进行两个坐标系之间的坐标转换。

  使用矩阵变换进行两个坐标系之间的坐标转换,其要点是将计算新坐标系变换到与旧坐标系重合的变换矩阵,使得两个坐标系能够重合,然后与源坐标进行相乘,即可得到转换后的坐标。

  对于上述的图形坐标与像素坐标的转换,在矩阵操作中对应了矩阵缩放和矩阵平移两个操作,其计算过程如下:
1、矩阵缩放:这里的缩放比例即为计算两个坐标范围的比值(分辨率);
2、矩阵平移:将原坐标系原点,移动到目标坐标系原点上。

源代码如下:

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
/**
* 坐标变换
* @param {*} originalCoord 原坐标值,其格式为[x,y]
* @param {*} originalExtent 原坐标区域,其格式为[minX, minY, maxX, maxY]
* @param {*} destExtent 目标坐标区域,其格式为[minX, minY, maxX, maxY]
* @returns 目标坐标值,其格式为[x,y]
*/
function convert(originalCoord, originalExtent, destExtent) {
let originalWidth = Extent.getWidth(originalExtent);
let destWidth = Extent.getWidth(destExtent);

// 分辨率
let res = destWidth / originalWidth

// 矩阵初始化
let transform = Transform.create();
Transform.translate(transform, destExtent[0], destExtent[1]);

// 矩阵缩放
Transform.scale(transform, res, res);

// 矩阵旋转, 如果坐标系进行旋转操作,只需添加下面这一行代码即可。
// Transform.rotate(transform, angle);

// 矩阵平移
Transform.translate(transform, -originalExtent[0], -originalExtent[1]);

// 计算目标坐标值
let destCoord = Transform.apply(transform, originalCoord);
return destCoord;
}

  当需要进行坐标转换时,使用方法和等比变换中的用法一样,源代码如下:

1
2
3
4
5
6
7
8
let graphExtent = [4000, 2000, 14000, 8000];
let canvasExtent = [0, 0, 1000, 600];

// 将图形坐标转换为Canvas坐标
let pixel = convert([5000, 3000], graphExtent, canvasExtent);

// 将Canvas坐标转换为图形坐标
let coord = convert([120, 200], canvasExtent, graphExtent);

  HTML中鼠标在移动时将触发mouseMove事件,该事件中包含了鼠标所在位置的Canvas像素坐标,下面这个示例演示了将Canvas像素坐标转换为图形坐标的功能。运行效果如下图所示:

运行效果

5.4 功能实现

  我们已经学习了图形缩放和平移的原理,并且掌握了边界范围类和坐标转换的方法,这一节中我们采用两种方式分别实现对图形的平移和缩放功能。

  下面这组演示数据是由七巧板拼成的一个正方形。

1
2
3
4
5
6
7
8
9
let tangram = [
{ "type": "Polygon", "coords": [[0, 0], [80, 0], [40, 40]], "style": { "fillStyle": 1, "fillColor": "#caff67" } },
{ "type": "Polygon", "coords": [[0, 0], [40, 40], [0, 80]], "style": { "fillStyle": 1, "fillColor": "#67becf" } },
{ "type": "Polygon", "coords": [[80, 0], [80, 40], [60, 60], [60, 20]], "style": { "fillStyle": 1, "fillColor": "#ef3d61" } },
{ "type": "Polygon", "coords": [[60, 20], [60, 60], [40, 40]], "style": { "fillStyle": 1, "fillColor": "#f9f51a" } },
{ "type": "Polygon", "coords": [[40, 40], [60, 60], [40, 80], [20, 60]], "style": { "fillStyle": 1, "fillColor": "#a54c09" } },
{ "type": "Polygon", "coords": [[20, 60], [40, 80], [0, 80]], "style": { "fillStyle": 1, "fillColor": "#fa8ccc" } },
{ "type": "Polygon", "coords": [[80, 40], [80, 80], [40, 80]], "style": { "fillStyle": 1, "fillColor": "#f6ca29" } }
]

5.4.1 平移(也称之为漫游)

  在本章第一节实现原理中我们已经论述了,当分辨率 resolution 不变时,图形显示范围发生了变换,图形就会出现平移;按照这个原理,我们增加了一个局部函数,用于计算平移操作后的“图形显示范围”,源代码如下:

1
2
3
4
5
6
7
8
 // 修改图形显示范围
function _extentMove(extent, dx, dy) {
extent[0] += dx;
extent[1] += dy;
extent[2] += dx;
extent[3] += dy;
return extent;
}

这个函数的参数为:

  • extent: 原有的图形显示范围
  • dx: x方向平移的像素
  • dy: y方向平移的像素

  图形平移操作要么是由鼠标或手势触发,要么是由界面中的按钮触发。下面的代码已鼠标操作为例,实现鼠标平移功能。源代码如下:

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

let [lastClientX, lastClientY] = [0, 0];
let isMouseDown = false;

// 监听鼠标按下事件
$("#canvas").on('mousedown', function (e) {
lastClientX = e.offsetX;
lastClientY = e.offsetY;
isMouseDown = true;
});

// 监听鼠标移动事件
$("#canvas").on('mousemove', function (e) {
if(isMouseDown === true) {
let resolution = Extent.getWidth(graphExtent) / Extent.getWidth(canvasExtent);
let distX = lastClientX - e.offsetX;
let distY = lastClientY - e.offsetY;
graphExtent = _extentMove(graphExtent, distX * resolution, distY * resolution);
redraw();
lastClientX = e.offsetX;
lastClientY = e.offsetY;
}
});

// 监听鼠标按键抬起事件
$(document).on('mouseup', function (e) {
isMouseDown = false;
});

  这段代码中先定义了三个变量: isMouseDown, lastClientXlastClientYisMouseDown用于记录鼠标是否按下的状态,只有在这个状态下,鼠标移动的时候,图形才跟着一起移动。

  lastClientXlastClientY用于记录上一次鼠标按下时的位置,为了保持连续移动的效果,在鼠标移动中也会更新该值。通过该坐标和当前移动时的坐标相减即可得到鼠标移动的距离。访问 _extentMove() 函数即可得到新的图形显示范围,在重绘图形时进行坐标转换,从而绘制出平移后的图形。

将各个图形对象的图形坐标转换为Canvas画布像素坐标,实现图形平移

  下面我们在看一下根据图形显示范围,绘制图形的代码,如下所示:

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
// 绘图
function redraw() {
// 清除已有内容
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制七巧板
tangram.forEach(shape => {
// 坐标转换
let pixel = [];
shape.coords.forEach(coord => {
pixel.push(convert(coord, graphExtent, canvasExtent));
})
// 绘制图形
_drawShape(ctx, pixel, shape.style);
})
}

// 绘制多边形
function _drawShape(ctx, pixel, style) {
ctx.save();
ctx.beginPath();
for (let i = 0, ii = pixel.length; i < ii; i += 1) {
if (i === 0) {
ctx.moveTo(pixel[i][0], pixel[i][1]);
} else {
ctx.lineTo(pixel[i][0], pixel[i][1]);
}
}
ctx.closePath();
ctx.fillStyle = style.fillColor;
ctx.fill();
ctx.stroke();
ctx.restore();
}

  每次重绘图形的时候,首先是清屏,然后根据图形数据逐个对象的进行坐标转换, 最后将这些对象绘制至Canvas中。这里访问的convert() 函数,既可以是 5.3.1 等比变换计算坐标转换的那个 convert() 函数,也可以是 5.3.2矩阵变换 进行坐标转换中介绍的那个 convert() 函数。

  除了使用鼠标事件实现图形平移操作外,还可以在界面中增加上下左右移动的按钮实现平移操作。程序实现逻辑与鼠标操作相似,每次移动的是一个固定的距离。整个功能代码就这些,运行效果如下图所示:

运行效果

计算Canvas渲染上下文对象的变换矩阵,实现图形平移

  下面我们在来讲解一下由Canvas渲染上下文对象提供的矩阵变换来实现平移功能,图形数据和鼠标事件处理的代码完全一样,只需要改动redraw()方法,将坐标转换的代码改为Canvas矩阵变换代码。源代码如下:

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
// 绘图
function redraw() {
// 清除已有内容
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 计算变换矩阵
let transform = getTransform(graphExtent, canvasExtent)

// 对Canvas渲染上下文对象进行矩阵变换,实现图形的缩放和平移功能
ctx.save();
ctx.setTransform(transform[0], transform[1], transform[2], transform[3], transform[4], transform[5]);

// 绘制七巧板
tangram.forEach(shape => {
// 绘制图形
_drawShape(ctx, shape.coords, shape.style);
})
ctx.restore();
}

/**
* 计算坐标变换矩阵
* @param {*} originalExtent 原坐标区域,其格式为[minX, minY, maxX, maxY]
* @param {*} destExtent 目标坐标区域,其格式为[minX, minY, maxX, maxY]
* @returns 目标坐标值,其格式为[x,y]
*/
function getTransform(originalExtent, destExtent) {
let originalWidth = Extent.getWidth(originalExtent);
let destWidth = Extent.getWidth(destExtent);

// 分辨率
let res = destWidth / originalWidth;

// 矩阵初始化
let transform = Transform.create();
Transform.translate(transform, destExtent[0], destExtent[1]);

// 矩阵缩放
Transform.scale(transform, res, res);

// 矩阵旋转, 如果坐标系进行旋转操作,只需添加下面这一行代码即可。
// Transform.rotate(transform, angle);

// 矩阵平移
Transform.translate(transform, -originalExtent[0], -originalExtent[1]);

return transform;
}

  这段代码中 redraw() 函数在重绘图形时不在将图形坐标转换为Canvas坐标,而是计算了图形坐标转换为Canvas像素坐标的变换矩阵,然后通过Canvas渲染上下文对象的 setTransform() 方法应用该矩阵。最后直接使用图形坐标绘图。细心的读者一定发现了,getTransform() 计算变换矩阵的方法和 使用矩阵进行坐标转换convert()函数中计算变换矩阵的方法是一样的,这是因为两者使用的均是相同结构的三阶矩阵。其运行效果如下图所示:

运行效果

这两种方式实现的效果有一点点区别,亲爱的读者,您发现了吗?

5.4.2 缩放

  在本章第一节实现原理中我们已经论述了,当分辨率 resolution 增大时,在相同的大小的Canvas中显示示更大的图形范围,图形就会缩小。当分辨率 resolution 减少时,在相同的大小的Canvas中显示更小的图形范围,图形就会放大;也就是说只需调整分辨率 resolution,就能够实现图形的放大和缩小功能。

5.4.2.1 中心点缩放

  在本章第二节边界范围内中提供了一些基本方法,而图形显示范围的中心点缩放也是一个常用的方法,应添加至 Extent 类中,其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 计算以中心点缩放后的空间范围
* @param {Extent} extent
* @param {number} scale 倍率
* @returns Extent 边界范围值
*/
static scaleFromCenter(extent, scale) {
let size = this.getSize(extent);
const deltaX = size.width * (scale - 1);
const deltaY = size.height * (scale - 1);
let x1 = extent[0] - deltaX * 0.5;
let x2 = extent[2] + deltaX * 0.5;
let y1 = extent[1] - deltaY * 0.5;
let y2 = extent[3] + deltaY * 0.5;
return [MathUtil.toFixed(x1, 2), MathUtil.toFixed(y1, 2), MathUtil.toFixed(x2, 2), MathUtil.toFixed(y2, 2)];
}

  这个方法的 scale 参数为缩放倍率,其值小于1时,得到更小的图形范围(分辨率缩小),从而实现图形的放大;其值大于1时,将得到更大的图形范围(分辨率增大),从而实现图形的缩小。

5.4.2.2 锚点缩放

  上面这个方法的缩放是对图形中心的缩放,常用用于 “缩放按钮” 或 “缩放工具栏” 中。当我们使用鼠标滚轮进行图形缩放时,通常都是根据鼠标的位置进行图形缩放,也就是根据指定的锚点进行缩放。因此 Extent 类还应提供根据描点进行缩放的函数 scaleFromPoint(extent, scale, point),这部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 计算以指定点缩放的空间范围
* @param {Extent} extent
* @param {number} scale 倍率
* @param {Coord} point
* @returns Extent 边界范围值
*/
static scaleFromPoint(extent, scale, point) {
let size = this.getSize(extent);
const deltaX = size.width * (scale - 1);
const deltaY = size.height * (scale - 1);
let x1 = extent[0] - deltaX * (point[0] - extent[0]) / size.width;
let x2 = extent[2] + deltaX * (extent[2] - point[0]) / size.width;
let y1 = extent[1] - deltaY * (point[1] - extent[1]) / size.height;
let y2 = extent[3] + deltaY * (extent[3] - point[1]) / size.height;
return [MathUtil.toFixed(x1, 2), MathUtil.toFixed(y1, 2), MathUtil.toFixed(x2, 2), MathUtil.toFixed(y2, 2)];
}

5.4.2.3 鼠标滚轮事件

  下面的代码实现了通过鼠标滚轮进行图形缩放的功能。源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 监听鼠标滚轮事件,实现鼠标滚轮缩放的功能
$("#canvas").on('wheel', function (e) {
let delta = (e.originalEvent.wheelDelta != null) ? e.originalEvent.wheelDelta : e.originalEvent.detail;
let offsetX = e.originalEvent.offsetX;
let offsetY = e.originalEvent.offsetY;
let coord = convert([offsetX, offsetY], canvasExtent, graphExtent)
if (delta > 0) {
graphExtent = Extent.scaleFromPoint(graphExtent, 0.8, coord);
} else {
graphExtent = Extent.scaleFromPoint(graphExtent, 1.25, coord);
}
redraw();
});

  这段代码中使用 jquery 绑定了Canvas的滚轮 wheel 事件,通过事件对象得到鼠标滚动的方向,当滚轮向下滚动时wheelDelta 大于0,向下滚动时 wheelDelta 小于0,进而计算新的图形显示范围。最后通过 redraw() 重绘图形, 这里的redraw() 的就是上一节平移中的 redraw() 函数。

  下面我们看一看运行效果图:

运行效果


  与平移功能类似,当将 redraw() 函数改为通过Canvas渲染上下文对象的 setTransform() 方法应用变换矩阵实现图形重绘的方式后,亦可实现图形的缩放。其运行效果图如下:

运行效果


  这一次,我们更明显的发现了这两种方式实现图形的缩放功能后其图形绘制效果的差异,即边框粗细。使用坐标转换后,边框的粗细可以更容易控制,而使用矩阵变换绘图的结果不仅仅是图形变大了,其边框粗细也跟着同步变化。这两种结果的差异没有优缺之分,主要还是根据应用需求而异。除了边框的粗细,虚线效果和阴影效果、文字大小等也会出现类似情况,在处理时需根据应用场景进行相应处理。

5.5 AnyGraph 的实现

  上面的这几节内容讲述了 “图形交互操作:平移和缩放” 的实现原理和实现方法,如果您觉得这些实现这些功能很无聊,那么您可以直接使用 AnyGraph

  AnyGraph 内置了缩放和平移的功能,在初始化 Graph 类的时候指定是否启动该功能,并能够通过参数控制采用是基于坐标转换实现该功能,还是基于Canvas渲染上下文对象实现该功能。

5.5.1 采用坐标转换实现图形的缩放和平移

  Graph 类的初始化选项中提供了一个 mouse 选项,可控制图形加载之后能否提供鼠标滚轮的缩放功能和鼠标中键的平移功能,该选项默认值为 true,也就意味着缺省情况,可对任何图形提供缩放和平移功能。该选项的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// graph对象
let graph = new Graph({
"target": "graphWrapper",
"layers": [
new Layer({
"source": new VectorSource({
"data": [
{ "type": "Polygon", "coords": [[0, 0], [80, 0], [40, 40]], "style": { "fillStyle": 1, "fillColor": "#caff67" } },
{ "type": "Polygon", "coords": [[0, 0], [40, 40], [0, 80]], "style": { "fillStyle": 1, "fillColor": "#67becf" } },
{ "type": "Polygon", "coords": [[80, 0], [80, 40], [60, 60], [60, 20]], "style": { "fillStyle": 1, "fillColor": "#ef3d61" } },
{ "type": "Polygon", "coords": [[60, 20], [60, 60], [40, 40]], "style": { "fillStyle": 1, "fillColor": "#f9f51a" } },
{ "type": "Polygon", "coords": [[40, 40], [60, 60], [40, 80], [20, 60]], "style": { "fillStyle": 1, "fillColor": "#a54c09" } },
{ "type": "Polygon", "coords": [[20, 60], [40, 80], [0, 80]], "style": { "fillStyle": 1, "fillColor": "#fa8ccc" } },
{ "type": "Polygon", "coords": [[80, 40], [80, 80], [40, 80]], "style": { "fillStyle": 1, "fillColor": "#f6ca29" } }
]
})
})
]
});

5.5.2 采用矩阵变换实现图形的缩放和平移

  Layer 类的初始化选项中提供了一个 useTransform 选项,可控制该图层中图形在进行缩放和平移功能时,是采用将 “图形坐标” 转换为 “Canvas像素坐标” 实现缩放和平移功能,还是基于 Canvas 渲染上下文对象的矩阵变换实现该功能。 该参数缺省值为false,意味着缺省是使用坐标转换实现图形的缩放和平移功能。该选项用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// graph对象
let graph = new Graph({
"target": "graphWrapper",
"layers": [
new Layer({
"useTransform":true,
"source": new VectorSource({
"data": [
{ "type": "Polygon", "coords": [[0, 0], [80, 0], [40, 40]], "style": { "fillStyle": 1, "fillColor": "#caff67" } },
{ "type": "Polygon", "coords": [[0, 0], [40, 40], [0, 80]], "style": { "fillStyle": 1, "fillColor": "#67becf" } },
{ "type": "Polygon", "coords": [[80, 0], [80, 40], [60, 60], [60, 20]], "style": { "fillStyle": 1, "fillColor": "#ef3d61" } },
{ "type": "Polygon", "coords": [[60, 20], [60, 60], [40, 40]], "style": { "fillStyle": 1, "fillColor": "#f9f51a" } },
{ "type": "Polygon", "coords": [[40, 40], [60, 60], [40, 80], [20, 60]], "style": { "fillStyle": 1, "fillColor": "#a54c09" } },
{ "type": "Polygon", "coords": [[20, 60], [40, 80], [0, 80]], "style": { "fillStyle": 1, "fillColor": "#fa8ccc" } },
{ "type": "Polygon", "coords": [[80, 40], [80, 80], [40, 80]], "style": { "fillStyle": 1, "fillColor": "#f6ca29" } }
]
})
})
]
});

5.5.3 缩放和平移 API

  Graph 类提供了以下图形缩放和平移的api,便于在业务系统使用平移和缩放的功能。

名称 说明
doMove(position) 图形平移
doZoom(scale, anchor) 图形放大/缩小

示例如下:

1
2
3
4
5
6
7
8
// 向右下方移动图形
graph.doMove([80, 40])

// 图形放大
graph.doZoom(2)

// 图形缩小
graph.doZoom(0.5)

  “图形系统实战开发-进阶篇 第五章 图形交互操作: 平移和缩放” 的内容讲解到这里就结束了,如果觉得对你有帮助有收获,可以关注我们的官方账号,持续关注更多精彩内容。


本文为“图形开发学院”(graphanywhere.com)网站原创文章,遵循CC BY-NC-ND 4.0版权协议,商业转载请联系作者获得授权,非商业转载请附上原文出处链接及本声明。