本篇介绍一下使用openlayersmarker光晕扩散(光环扩散)(postrender事件和render方法)
1 需求
- marker光晕扩散(光环扩散)
2 分析
marker光晕扩散(光环扩散)使用postrender事件和render方法
3 知识点
即时渲染
- 解释:
OpenLayers 的即时渲染功能允许用户在不先将几何图形添加到图层的情况下,直接在渲染事件中绘制样式化的几何图形。可以根据应用的需求,在渲染事件中动态地添加、修改或删除几何图形。 - 使用:
- openLayers 提供了即时渲染 API,允许在
渲染事件
中直接绘制几何图形,如在postrender事件中,使用即时渲染 API 来绘制和更新几何图形。 - 使用 getVectorContext 函数从
渲染事件
中获取渲染上下文。 - 在这个渲染上下文中,使用 context.drawGeometry() 和 context.setStyle() 方法来绘制和设置样式的几何体。
- 如果想在渲染过程中获取或处理即时渲染的几何图形,通常并不是直接“获取”,因为它们是作为渲染过程的一部分被绘制到 Canvas 上的
- 如果需要更精细地控制几何图形的渲染,可以通过自定义图层的渲染函数来实现。这通常涉及到覆盖图层的 render 方法,并在其中使用 getVectorContext 来获取渲染上下文,并直接调用渲染上下文的方法来绘制几何图形。
- 如果想要获取渲染后的几何图形在 Canvas 上的像素数据(例如,用于进一步分析或导出为图像),可以使用 Canvas 的 toDataURL 方法来获取包含渲染结果的图像数据。
4 实现
4.1 实现一(使用vectorContext)
<template>
<div id="map" class="map"></div>
<div class="toolbar">
<el-button type="primary" @click="handleClick">{{ animationFlag ? '停止' : '开始' }}</el-button>
<el-input v-model.number="duration"></el-input><span>持续时间</span>
</div>
</template>
<script setup lang="ts">
import { Feature, Map, View } from 'ol';
import { LineString, Point } from 'ol/geom';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { get } from 'ol/proj';
import { Vector as VectorSource, XYZ } from 'ol/source';
import { Circle, Fill, Icon, Stroke, Style } from 'ol/style';
import { easeOut } from 'ol/easing.js';
import { getVectorContext } from 'ol/render.js';
import iconSrc from '@/assets/image/truck.png';
const projection = get('EPSG:4326');
const layerTypeMap = {
vector: ['vec', 'cva'], // [矢量底图, 矢量注记]
image: ['img', 'cia'], // [影像底图, 影像注记]
terrain: ['ter', 'cta'] // [地形晕渲, 地形注记]
};
const map = shallowRef();
const vectorLayer = shallowRef();
const source = shallowRef<VectorSource>(new VectorSource());
const animationFlag = ref(false);
const duration = ref(3000);
const start = ref(Date.now());
onMounted(() => {
initMap('image');
initFeature();
});
const initMap = (layerType = 'image') => {
const key = '替换为天地图key';
vectorLayer.value = new VectorLayer({
source: source.value,
style: new Style({
image: new Icon({
crossOrigin: 'anonymous',
src: iconSrc,
width: 30,
height: 30
})
})
});
// c: 经纬度投影 w: 墨卡托投影
const matrixSet = 'c';
map.value = new Map({
target: 'map',
layers: [
// 底图
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][0]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
// 注记
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][1]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
vectorLayer.value
],
view: new View({
center: [116.406393, 39.909006],
projection: projection,
zoom: 5,
maxZoom: 17,
minZoom: 1
})
});
};
const initFeature = () => {
const coordinates = [
[110, 30],
[111, 31],
[112, 32],
[113, 33]
];
coordinates.forEach(c => {
const f = new Feature({
geometry: new Point(c)
});
source.value.addFeature(f);
});
};
const handleClick = () => {
if (!animationFlag.value) {
startAnimation();
} else {
stopAnimation();
}
animationFlag.value = !animationFlag.value;
};
const startAnimation = () => {
start.value = Date.now();
vectorLayer.value.on('postrender', flash);
// 触发地图渲染
vectorLayer.value.changed();
};
const flash = e => {
const time = e.frameState.time;
// 时间戳差(毫秒)
const elapsedTime = time - start.value;
if (elapsedTime >= duration.value) {
stopAnimation();
startAnimation();
return;
}
// 获取用于绘制到事件画布的矢量上下文
const vectorContext = getVectorContext(e);
// elapsedRatio值0到1之间
const elapsedRatio = elapsedTime / duration.value;
const radius = easeOut(elapsedRatio) * 25 + 5;
const opacity = easeOut(1 - elapsedRatio);
const style = new Style({
image: new Circle({
radius: radius,
stroke: new Stroke({
color: 'rgba(255, 0, 0, ' + opacity + ')',
width: 1 + opacity
})
})
});
// 首先调用setStyle()来设置呈现样式
vectorContext.setStyle(style);
const fea = source.value.getFeatures();
fea.forEach(f => {
const geo = f.getGeometry().clone();
// 将几何体渲染到画布中
vectorContext.drawGeometry(geo);
});
// 调用地图渲染
map.value.render();
};
const stopAnimation = () => {
vectorLayer.value.un('postrender', flash);
};
</script>
<style scoped lang="scss">
.map {
width: 100%;
height: 100%;
}
.toolbar {
position: absolute;
top: 20px;
left: 100px;
display: flex;
justify-content: center;
align-items: center;
color:#FFF;
.el-input {
width: 100px;
margin: 0 20px;
}
}
</style>
4.2 实现二(不使用vectorContext,直接动态修改样式)
<template>
<div id="map" class="map"></div>
<div class="toolbar">
<el-button type="primary" @click="handleClick">{{ animationFlag ? '停止' : '开始' }}</el-button>
<el-input v-model.number="duration"></el-input><span>持续时间</span>
</div>
</template>
<script setup lang="ts">
import { Feature, Map, View } from 'ol';
import { LineString, Point } from 'ol/geom';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { get } from 'ol/proj';
import { Vector as VectorSource, XYZ } from 'ol/source';
import { Circle, Fill, Icon, Stroke, Style } from 'ol/style';
import { easeOut } from 'ol/easing.js';
import iconSrc from '@/assets/image/truck.png';
const projection = get('EPSG:4326');
const layerTypeMap = {
vector: ['vec', 'cva'], // [矢量底图, 矢量注记]
image: ['img', 'cia'], // [影像底图, 影像注记]
terrain: ['ter', 'cta'] // [地形晕渲, 地形注记]
};
const map = shallowRef();
const vectorLayer = shallowRef();
const source = shallowRef<VectorSource>(new VectorSource());
const animationFlag = ref(false);
const duration = ref(3000);
const start = ref(Date.now());
const iconStyle = new Style({
image: new Icon({
crossOrigin: 'anonymous',
src: iconSrc,
width: 30,
height: 30
})
});
onMounted(() => {
initMap('image');
initFeature();
});
const initMap = (layerType = 'image') => {
const key = '替换为天地图key';
vectorLayer.value = new VectorLayer({
source: source.value,
style: iconStyle
});
// c: 经纬度投影 w: 墨卡托投影
const matrixSet = 'c';
map.value = new Map({
target: 'map',
layers: [
// 底图
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][0]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
// 注记
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][1]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
vectorLayer.value
],
view: new View({
center: [116.406393, 39.909006],
projection: projection,
zoom: 5,
maxZoom: 17,
minZoom: 1
})
});
};
const initFeature = () => {
const coordinates = [
[110, 30],
[111, 31],
[112, 32],
[113, 33]
];
coordinates.forEach(c => {
const f = new Feature({
geometry: new Point(c)
});
source.value.addFeature(f);
});
};
const handleClick = () => {
if (!animationFlag.value) {
startAnimation();
} else {
stopAnimation();
}
animationFlag.value = !animationFlag.value;
};
const startAnimation = () => {
start.value = Date.now();
vectorLayer.value.on('postrender', flash);
// 触发地图渲染
vectorLayer.value.changed();
};
const flash = e => {
const time = e.frameState.time;
// 时间戳差(毫秒)
const elapsedTime = time - start.value;
if (elapsedTime >= duration.value) {
stopAnimation();
startAnimation();
return;
}
// elapsedRatio值0到1之间
const elapsedRatio = elapsedTime / duration.value;
const radius = easeOut(elapsedRatio) * 25 + 5;
const opacity = easeOut(1 - elapsedRatio);
const style = new Style({
image: new Circle({
radius: radius,
stroke: new Stroke({
color: 'rgba(255, 0, 0, ' + opacity + ')',
width: 1 + opacity
})
})
});
const fea = source.value.getFeatures();
fea.forEach(f => {
f.setStyle([iconStyle, style]);
});
// 调用地图渲染
map.value.render();
};
const stopAnimation = () => {
const fea = source.value.getFeatures();
fea.forEach(f => {
f.setStyle(iconStyle);
});
vectorLayer.value.un('postrender', flash);
};
</script>
<style scoped lang="scss">
.map {
width: 100%;
height: 100%;
}
.toolbar {
position: absolute;
top: 20px;
left: 100px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
.el-input {
width: 100px;
margin: 0 20px;
}
}
</style>
问题:
所有扩散频率都是一致的,过于呆板
分析:
频率一致主要是因为duration和start一样,只要为不同图标修改为不同值的即可
4.3 实现三(多个icon)
<template>
<div id="map" class="map"></div>
<div class="toolbar">
<el-button type="primary" @click="handleClick">{{ animationFlag ? '停止' : '开始' }}</el-button>
</div>
</template>
<script setup lang="ts">
import { Feature, Map, View } from 'ol';
import { LineString, Point } from 'ol/geom';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { get } from 'ol/proj';
import { Vector as VectorSource, XYZ } from 'ol/source';
import { Circle, Fill, Icon, Stroke, Style } from 'ol/style';
import { easeOut } from 'ol/easing.js';
import { getVectorContext } from 'ol/render.js';
import iconSrc from '@/assets/image/truck.png';
const projection = get('EPSG:4326');
const layerTypeMap = {
vector: ['vec', 'cva'], // [矢量底图, 矢量注记]
image: ['img', 'cia'], // [影像底图, 影像注记]
terrain: ['ter', 'cta'] // [地形晕渲, 地形注记]
};
const map = shallowRef();
const vectorLayer = shallowRef();
const source = shallowRef<VectorSource>(new VectorSource());
const animationFlag = ref(false);
const duration = ref([3000, 5000, 8000, 1000]);//可以加入随机值
const start = ref([0, 0, 0, 0]);
onMounted(() => {
initMap('image');
initFeature();
});
const initMap = (layerType = 'image') => {
const key = '替换为天地图key';
vectorLayer.value = new VectorLayer({
source: source.value,
style: new Style({
image: new Icon({
crossOrigin: 'anonymous',
src: iconSrc,
width: 30,
height: 30
})
})
});
// c: 经纬度投影 w: 墨卡托投影
const matrixSet = 'c';
map.value = new Map({
target: 'map',
layers: [
// 底图
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][0]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
// 注记
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][1]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
vectorLayer.value
],
view: new View({
center: [116.406393, 39.909006],
projection: projection,
zoom: 5,
maxZoom: 17,
minZoom: 1
})
});
};
const initFeature = () => {
const coordinates = [
[110, 30],
[111, 31],
[112, 32],
[113, 33]
];
coordinates.forEach(c => {
const f = new Feature({
geometry: new Point(c)
});
source.value.addFeature(f);
});
};
const handleClick = () => {
if (!animationFlag.value) {
startAnimation();
} else {
stopAnimation();
}
animationFlag.value = !animationFlag.value;
};
const startAnimation = () => {
start.value = start.value.map(i => Date.now());
vectorLayer.value.on('postrender', flash);
// 触发地图渲染
vectorLayer.value.changed();
};
const flash = e => {
const time = e.frameState.time;
const fea = source.value.getFeatures();
fea.forEach((f, idx) => {
// 时间戳差(毫秒)
let elapsedTime = time - start.value[idx];
if (elapsedTime >= duration.value[idx]) {
start.value[idx] = Date.now();
elapsedTime = duration.value[idx];
}
const vectorContext = getVectorContext(e);
// elapsedRatio值0到1之间
const elapsedRatio = elapsedTime / duration.value[idx];
const radius = easeOut(elapsedRatio) * 25 + 5;
const opacity = easeOut(1 - elapsedRatio);
const style = new Style({
image: new Circle({
radius: radius,
stroke: new Stroke({
color: 'rgba(255, 0, 0, ' + opacity + ')',
width: 1 + opacity
})
})
});
// 将feature渲染到画布中。
vectorContext.drawFeature(f.clone(), style);
});
// 调用地图渲染
map.value.render();
};
const stopAnimation = () => {
vectorLayer.value.un('postrender', flash);
};
</script>
<style scoped lang="scss">
.map {
width: 100%;
height: 100%;
}
.toolbar {
position: absolute;
top: 20px;
left: 100px;
display: flex;
justify-content: center;
align-items: center;
.el-input {
width: 100px;
margin: 0 20px;
}
}
</style>
4.4 实现四(增加光晕)
<template>
<div id="map" class="map"></div>
<div class="toolbar">
<el-button type="primary" @click="handleClick">{{ animationFlag ? '停止' : '开始' }}</el-button>
</div>
</template>
<script setup lang="ts">
import { Feature, Map, View } from 'ol';
import { LineString, Point } from 'ol/geom';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { get } from 'ol/proj';
import { Vector as VectorSource, XYZ } from 'ol/source';
import { Circle, Fill, Icon, Stroke, Style } from 'ol/style';
import { easeOut } from 'ol/easing.js';
import { getVectorContext } from 'ol/render.js';
import iconSrc from '@/assets/image/truck.png';
const projection = get('EPSG:4326');
const layerTypeMap = {
vector: ['vec', 'cva'], // [矢量底图, 矢量注记]
image: ['img', 'cia'], // [影像底图, 影像注记]
terrain: ['ter', 'cta'] // [地形晕渲, 地形注记]
};
const map = shallowRef();
const vectorLayer = shallowRef();
const source = shallowRef<VectorSource>(new VectorSource());
const animationFlag = ref(false);
const duration = ref([3000, 5000, 8000, 1000]);
const start = ref([0, 0, 0, 0]);
onMounted(() => {
initMap('image');
initFeature();
});
const initMap = (layerType = 'image') => {
const key = '替换为天地图key';
vectorLayer.value = new VectorLayer({
source: source.value,
style: new Style({
image: new Circle({
radius: 0,
fill: new Fill({
color: 'rgba(228, 147, 87, 1)'
}),
stroke: new Stroke({
color: 'rgba(255, 0, 0,1)',
width: 1
})
})
})
});
// c: 经纬度投影 w: 墨卡托投影
const matrixSet = 'c';
map.value = new Map({
target: 'map',
layers: [
// 底图
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][0]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
// 注记
new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=${layerTypeMap[layerType][1]}_${matrixSet}&tk=${key}&x={x}&y={y}&l={z}`,
projection
})
}),
vectorLayer.value,
new VectorLayer({
source: source.value,
style: new Style({
image: new Icon({
crossOrigin: 'anonymous',
src: iconSrc,
width: 30,
height: 30
})
})
})
],
view: new View({
center: [116.406393, 39.909006],
projection: projection,
zoom: 5,
maxZoom: 17,
minZoom: 1
})
});
};
const initFeature = () => {
const coordinates = [
[110, 30],
[111, 31],
[112, 32],
[113, 33]
];
coordinates.forEach(c => {
const f = new Feature({
geometry: new Point(c)
});
source.value.addFeature(f);
});
};
const handleClick = () => {
if (!animationFlag.value) {
startAnimation();
} else {
stopAnimation();
}
animationFlag.value = !animationFlag.value;
};
const startAnimation = () => {
start.value = start.value.map(i => Date.now());
vectorLayer.value.on('postrender', flash);
// 触发地图渲染
vectorLayer.value.changed();
};
const flash = e => {
const time = e.frameState.time;
const fea = source.value.getFeatures();
fea.forEach((f, idx) => {
// 时间戳差(毫秒)
let elapsedTime = time - start.value[idx];
if (elapsedTime >= duration.value[idx]) {
start.value[idx] = Date.now();
elapsedTime = duration.value[idx];
}
const vectorContext = getVectorContext(e);
// elapsedRatio值0到1之间
const elapsedRatio = elapsedTime / duration.value[idx];
const radius = easeOut(elapsedRatio) * 25 + 5;
const opacity = easeOut(1 - elapsedRatio);
const style = new Style({
image: new Circle({
radius: radius,
fill: new Fill({
color: 'rgba(228, 147, 87, ' + opacity + ')'
}),
stroke: new Stroke({
color: 'rgba(255, 0, 0, ' + opacity + ')',
width: 1 + opacity
})
})
});
// 将feature渲染到画布中。
vectorContext.drawFeature(f.clone(), style);
});
// 调用地图渲染
map.value.render();
};
const stopAnimation = () => {
vectorLayer.value.un('postrender', flash);
};
</script>
<style scoped lang="scss">
.map {
width: 100%;
height: 100%;
}
.toolbar {
position: absolute;
top: 20px;
left: 100px;
display: flex;
justify-content: center;
align-items: center;
.el-input {
width: 100px;
margin: 0 20px;
}
}
</style>