0%

OpenLayers6实例分析:Custom Animation(水波扩散)

分析 Custom Animation 这个 demo,官网介绍是:

This example shows how to use postrender and vectorContext to animate features. Here we choose to do a flash animation each time a feature is added to the layer.
此示例演示如何使用 postrender 和 vectorContext 对要素进行动画处理。 做法是当一个要素被添加到图层时给它添加闪烁效果。

custom

定义基本结构

先展示地图基本结构:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<title>Custom Animation(自定义动画)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
// 定义一个瓦片图层
let tileLayer = new ol.layer.Tile({
source: new ol.source.OSM({
wrapX: false // 是否水平循环
})
});
// 定义一个地图
let map = new ol.Map({
layers: [tileLayer],
target: "map",
view: new ol.View({
center: [0, 0],
zoom: 1,
multiWorld: true // 视图是否只能看到一个世界
})
});

let source = new ol.source.Vector({
wrapX: false
});
let vector = new ol.layer.Vector({
source: source
});
// 给map添加一个图层vector
map.addLayer(vector);
</script>
</body>
</html>

在地图定义这里跟前一篇的区别主要是map.addLayer(vector),这里的图层是通过addLayer方法加入到map中的,github 中的源码如下,看注释可以看出,新加的图层显示在最上层:

1
2
3
4
5
6
7
8
9
10
11
/**
* Adds the given layer to the top of this map. If you want to add a layer
* elsewhere in the stack, use `getLayers()` and the methods available on
* {@link module:ol/Collection~Collection}.
* @param {import("./layer/Base.js").default} layer Layer.
* @api
*/
addLayer(layer) {
const layers = this.getLayerGroup().getLayers();
layers.push(layer);
}

生成随机要素

做动画之前,首先添加需要展示动画的要素,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @description: 给source添加随机位置的要素
* @param {null}
* @return: null
*/
const addRandomFeature = () => {
let x = Math.random() * 360 - 180; // 随机区间[-180, 180]
let y = Math.random() * 180 - 90; // 随机区间[-90, 90]
// ol.proj.fromLonLat(coordinate,[projection:默认3857]),转化经纬度为投影下坐标
// new ol.geom.Point(coordinates, opt_layout) 生成一个点对象
//
let geom = new ol.geom.Point(ol.proj.fromLonLat([x, y]));
// new ol.Feature(opt_geometryOrProperties) 生成一个要素
let feature = new ol.Feature(geom);
source.addFeature(feature); // 把要素添加给vectorSource
};
// 省略
// 省略
// 每1000ms执行一次addRandomFeature,添加一个随机要素
window.setInterval(addRandomFeature, 1000);

addRandomFeature

该方法是用来创建随机位置的要素,并添加到source中。首先用随机数模拟了地球上的随机经度[-180, 180]和随机纬度[-90, 90]。然后使用ol.proj.fromLonLat方法将经纬度转化为 3857 坐标系下的坐标,fromLonLat方法的源码如下,输入一个经纬度,转化为指定坐标系下的坐标(默认 EPSG:3857)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Transforms a coordinate from longitude/latitude to a different projection.
* @param {import("./coordinate.js").Coordinate} coordinate Coordinate as longitude and latitude, i.e.
* an array with longitude as 1st and latitude as 2nd element.
* @param {ProjectionLike=} opt_projection Target projection. The
* default is Web Mercator, i.e. 'EPSG:3857'.
* @return {import("./coordinate.js").Coordinate} Coordinate projected to the target projection.
* @api
*/
export function fromLonLat(coordinate, opt_projection) {
return transform(
coordinate,
"EPSG:4326",
opt_projection !== undefined ? opt_projection : "EPSG:3857"
);
}

什么是EPSG:3857坐标系?

EPSG:4326 (WGS84)

世界大地测量系统 1984 (World Geodetic System of 1984) 是 GPS 用来描述地球上位置的地理学坐标系统(三维)。WGS84 通常使用 GeoJSON 作为坐标系统的单位,GeoJSON 中使用数字作为经度和纬度的单位。大部分时候,当你描述一个经纬度坐标的时候,它就是基于 EPSG:4326 坐标系统的。

EPSG: 3857 (Pseudo-Mercator)

Pseudo-Mercator 投影系统将 WGS84 坐标系统投影在平面上(这个投影规则也被称之为球面墨卡托或者 web 墨卡托)。但是这个投影系统并不是包含地球上所有的位置,北纬和南纬的 85.06 度以上的地区不会展示。这个投影首次是被使用在 Google 地图上,加上几乎所有的 Web 地图,但是有趣的一点是,这些投影(EPSG:3857)内部都是使用的 WGS84 坐标系统 – 即使用的 WGS84 椭球体构建,但是将它们(EPSG:3857)的坐标是投射在一个球面上。

得到了 3857 坐标系下的坐标以后,需要用ol.geom.Point来生成一个点集合体对象,保存着几何体的形状。再用ol.Feature来生成一个要素,最后把要素添加给source,完成了添加一个点的操作。同时,在代码的最后,设置了一个setInterval,每一秒执行一次addRandomFeature添加一个随机位置点。

addfeature 事件

下面的代码就是为在添加要素的时候能够触发动画,duration是提前设置好的每个动画的持续时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const duration = 3000; // 每个点动画持续时间 ms
/**
* @description: 当添加要素时,对该要素执行的回调函数
* @param {feature} 要素
* @return: null
*/
const flash = feature => {
// 省略
// 省略
};
// 每当触发添加要素的事件时,执行flash()
source.on("addfeature", e => {
flash(e.feature);
});

ol.source.Vector事件如下图所示,addfeature事件是当给source添加feature时触发,然后把event.feature传给用来处理动画的 flash 函数。



水纹动画实现

水波扩散的效果使用postrender事件实现。如代码所示,在flash函数中又定义了一个新的回调函数animate,代码最后的时候把animate绑定给postrender事件,并且保存了一个key,便于在动画结束后及时解除绑定。

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
const flash = feature => {
/**
* @description: 当postrender事件发生时执行的回调函数
* @param {event} : 事件对象
* @return: null
*/
const animate = event => {
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
let flashGeom = feature.getGeometry().clone(); //拿到几何体对象
let elapsed = frameState.time - start; // 计算出当前帧所经历的时间
let elapsedRatio = elapsed / duration;
// 半径从5开始。到30结束
let radius = ol.easing.easeOut(elapsedRatio) * 25 + 5;
// 透明度时从有到无
let opacity = ol.easing.easeOut(1 - elapsedRatio);

let style = new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
stroke: new ol.style.Stroke({
color: "rgba(255, 0, 0, " + opacity + ")", // 逐渐变得透明
width: 0.25 + opacity // 边框变细
})
})
});

vectorContext.setStyle(style);
vectorContext.drawGeometry(flashGeom);
// 当持续时间超过了duration后,取消绑定的postrender事件
if (elapsed > duration) {
ol.Observable.unByKey(listenerKey);
return;
}
map.render();
};
let start = new Date().getTime();
// 保存住postrender事件绑定的key,方便以后解除
let listenerKey = tileLayer.on("postrender", animate);
};

缓动函数

动画实现在逻辑上最核心的部分如下所示,首先计算出当前已经历的时间与设定的动画时间长度的比例elapsedRatio,这个比例一定是小于 1 的。

1
2
3
4
5
6
let elapsed = frameState.time - start; // 计算出当前帧所经历的时间
let elapsedRatio = elapsed / duration; // 计算已经历时间与设定时间长度的比例
// 半径从5开始。到30结束
let radius = ol.easing.easeOut(elapsedRatio) * 25 + 5;
// 透明度时从有到无
let opacity = ol.easing.easeOut(1 - elapsedRatio);

计算出elapsedRatio这个比例是有原因的,用过 jQuery 的同学应该更熟悉这里的easing,也就是缓动函数。缓动函数本质上就是传入一个 0-1 之间的值,传出另一个 0-1 之间的值。不同的缓动函数传出的值是不同的,如下图所示,x轴是传入的值,y轴是传出的值。

在 OpenLayers 中,没有这么多的预置缓动函数类型,主要是下面几种,当然也可以自定义缓动函数:

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
/**
* @module ol/easing
*/

/**
* Start slow and speed up.
* @param {number} t Input between 0 and 1.
* @return {number} Output between 0 and 1.
* @api
*/
export function easeIn(t) {
return Math.pow(t, 3);
}

/**
* Start fast and slow down.
* @param {number} t Input between 0 and 1.
* @return {number} Output between 0 and 1.
* @api
*/
export function easeOut(t) {
return 1 - easeIn(1 - t);
}

/**
* Start slow, speed up, and then slow down again.
* @param {number} t Input between 0 and 1.
* @return {number} Output between 0 and 1.
* @api
*/
export function inAndOut(t) {
return 3 * t * t - 2 * t * t * t;
}

/**
* Maintain a constant speed over time.
* @param {number} t Input between 0 and 1.
* @return {number} Output between 0 and 1.
* @api
*/
export function linear(t) {
return t;
}

/**
* Start slow, speed up, and at the very end slow down again. This has the
* same general behavior as {@link module:ol/easing~inAndOut}, but the final
* slowdown is delayed.
* @param {number} t Input between 0 and 1.
* @return {number} Output between 0 and 1.
* @api
*/
export function upAndDown(t) {
if (t < 0.5) {
return inAndOut(2 * t);
} else {
return 1 - inAndOut(2 * (t - 0.5));
}
}

相信看完源码,就很轻松的理解缓动函数的实现了,在这个 demo 中,主要是利用easeOut这个缓动函数,实现越扩散越慢的一种波纹视觉效果,本质实际上是随着时间的增长,让半径逐渐变大同时越来越透明。

解除事件绑定

之前 demo 中没有见到的 API 还有ol.Observable.unByKey,这个 API 很奇怪,不知道为什么在官网查询 API 的时候查不到,但是在源码中却能看到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Removes an event listener using the key returned by `on()` or `once()`.
* @param {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} key The key returned by `on()`
* or `once()` (or an array of keys).
* @api
*/
export function unByKey(key) {
if (Array.isArray(key)) {
for (let i = 0, ii = key.length; i < ii; ++i) {
unlistenByKey(key[i]);
}
} else {
unlistenByKey(/** @type {import("./events.js").EventsKey} */ (key));
}
}

其实就是利用key来解除事件绑定。当动画经历的时间已经大于设定的duration了以后,就把这个postrender事件取消掉,这样一次完整的动画就完成了。每添加一个feature时触发一次addfeature,回调函数里都会新加一个postrender的动画事件,最后实现了 demo 中所示的不断有波纹产生的效果。

源码

最后贴出完整代码:

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<title>Custom Animation(自定义动画)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
// 定义一个瓦片图层
let tileLayer = new ol.layer.Tile({
source: new ol.source.OSM({
wrapX: false // 是否水平循环
})
});
// 定义一个地图
let map = new ol.Map({
layers: [tileLayer],
target: "map",
view: new ol.View({
center: [0, 0],
zoom: 1,
multiWorld: true // 视图是否只能看到一个世界
})
});

let source = new ol.source.Vector({
wrapX: false
});
let vector = new ol.layer.Vector({
source: source
});
// 给map添加一个图层vector
map.addLayer(vector);
/**
* @description: 给source添加随机位置的要素
* @param {null}
* @return: null
*/
const addRandomFeature = () => {
let x = Math.random() * 360 - 180; // 随机区间[-180, 180]
let y = Math.random() * 180 - 90; // 随机区间[-90, 90]
// ol.proj.fromLonLat(coordinate,[projection:默认3857]),转化经纬度为投影下坐标
// new ol.geom.Point(coordinates, opt_layout) 生成一个点对象
let geom = new ol.geom.Point(ol.proj.fromLonLat([x, y]));
// new ol.Feature(opt_geometryOrProperties) 生成一个要素
let feature = new ol.Feature(geom);
source.addFeature(feature); // 把要素添加给vectorSource
};

const duration = 3000; // 每个点动画持续时间 ms
/**
* @description: 当添加要素时,对该要素执行的回调函数
* @param {feature} 要素
* @return: null
*/
const flash = feature => {
/**
* @description: 当postrender事件发生时执行的回调函数
* @param {event} : 事件对象
* @return: null
*/
const animate = event => {
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
let flashGeom = feature.getGeometry().clone();
let elapsed = frameState.time - start;
let elapsedRatio = elapsed / duration;
// 半径从5开始,到30结束
let radius = ol.easing.easeOut(elapsedRatio) * 25 + 5;
// 透明度时从有到无
let opacity = ol.easing.easeOut(1 - elapsedRatio);

let style = new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
stroke: new ol.style.Stroke({
color: "rgba(255, 0, 0, " + opacity + ")", // 逐渐变得透明
width: 0.25 + opacity // 边框变细
})
})
});

vectorContext.setStyle(style);
vectorContext.drawGeometry(flashGeom);
// 当持续时间超过了duration后,取消绑定的postrender事件
if (elapsed > duration) {
ol.Observable.unByKey(listenerKey);
return;
}
// tell OpenLayers to continue postrender animation
map.render();
};
let start = new Date().getTime();
// 保存住postrender事件绑定的key,方便以后解除
let listenerKey = tileLayer.on("postrender", animate);
};
// 每当触发添加要素的事件时,执行flash()
source.on("addfeature", e => {
flash(e.feature);
});
// 每1000ms执行一次addRandomFeature,添加一个随机要素
window.setInterval(addRandomFeature, 1000);
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇