前言:本来懒得写这个博客,实在深感无聊,没啥事情做,出篇博客让大家看看。文章会尽可能简短。
简单效果
掉帧属录屏效果,尚未测试过性能,因为这个可以看自己调节。以下为一条贝塞尔曲线分了180段的效果描述。
颜色属于瞎编乱造,只为示例,不为效果负责。
来自新增2022/7/26
发了个包,应该说如果你想学习实现的思路,尽管往下看,如果你想要使用这个效果
ol-dynamic-curves地址
直接根据npm的内容使用即可。
里面装了一些比较常见的可能会处理到的API,新增删除,等等,以及配置的选项。
如果你想学习代码实现的核心内容
b站视频
准备步骤
1、先生成起点与终点的表示点。这个很重要,原因在于:openlayers 会智能的检测图层中的数据源(source)是否有需要更新的features,如果你没有设置features,或者不在视图内,是不会触发渲染、因此,也就不会触发我们需要的prerender事件。
2、监听图层的prerender 事件,顾名思义,prerender 意味着这是 openlayers 暴露出来的一个图层的渲染事件,prerender意味着在渲染前执行的一个函数,他会传入一个renderEvent对象。
3、获取到renderEvent之后,我们可以通过此API,获取一个有关于openlayers底层对当前图层绘制的canvas内容,里头主要封装了两个操作:绘制geometry, 设置样式。
核心实现
贝塞尔曲线的实现
上图中可以看出,线是动态画出来的,其实是由一个个密集的线表示而绘制出来的一条曲线。为此,我们分为三点一个个去说。
1、动态增加的线
需要一个数组去记录当前应该渲染的线的集合。以及一个用于表示上一个结束点坐标的位置 去 在下一个阶段 作为 开始位置。
let lineCoords = []
let startPos = ...
layer.on('prerender',function(evt){
let endPos = []
lineCoords.push([startPos,endPos])
let geometry = new MultiLineString(lineCoords);
})
2、曲线的绘制
本例使用二阶贝塞尔曲线绘制。不懂请去别处找资料。
主要在于控制点的寻找。以及如何获取当前贝塞尔曲线上的点坐标。
getCurrentEnd 函数 里面的计算 取自 百度百科上的贝塞尔曲线 二次方公式。
function ConstantMultiVector2(c, pos) {
return [c * pos[0], c * pos[1]];
}
function vector2Add(a, b) {
return [a[0] + b[0], a[1] + b[1]];
}
/**
* a = > [ lng,lat]
* b = > [ lng,lat]
* n => ratio
* 二维向量线性插值
*/
function linerInperpote(a, b, n) {
let curA = ConstantMultiVector2(1.0 - n, a);
let curB = ConstantMultiVector2(n, b);
return vector2Add(curA, curB);
}
// 获取 当前贝塞尔曲线上的 点坐标
function getCurrenetEnd(originPos1, center, originPos2, times) {
let curTimes = times / 180;
let a = ConstantMultiVector2(Math.pow(1.0 - curTimes, 2), originPos1);
let b = ConstantMultiVector2(2 * curTimes * (1 - curTimes), center);
let c = ConstantMultiVector2(Math.pow(curTimes, 2), originPos2);
return vector2Add(vector2Add(a, b), c);
}
以下表示控制点为: 开始点 与结束点 的中点 的经纬度位置 ,经度减10作为控制点。
let controlPos = vector2Add(
linerInperpote(originPos1, originPos2, 0.5),
[-10.0, 0]
)
3、段数的处理(时间的增加)
涉及到动画必然跟时间会有联系。这里我们选择以线段的处理到达终点作为一个周期。
将整个线位置的输入过程分为180段。并非180段最好,这个看个人的设置。理论上来说段数越高,表现越明显,当然,运动会越慢,所以合适就好。
let times = 0;
layer.on("prerender", (evt) => {
if (times % 180 === 0) {
times = 0;
lineCoords = [];
lastEndPos = startPos;
}
times++;
layer.changed()
}
在结束一个周期时,初始化相关的变量。
调用layer.changed() 重复执行这个函数,即告诉openlayers框架:当窗口中存在该图层的features时,始终更新此图层。
4、渲染
layer.on("prerender", (evt) => {
let geometry = new MultiLineString(multiCoords);
let ctx = getVectorContext(evt);
ctx.setStyle(
new Style({
stroke: new Stroke({
// 模板字符串
color: 'red',
lineCap: "butt",
width: 3,
}),
})
);
}
ctx.drawGeometry(geometry);
样式处理
渐变色处理
理论上来说,你可以操作每一条线的颜色,但通常我们不会这么做,因为太损耗性能了。(理由跟canvas的底层设计有关,有兴趣可以去搜索下。总之fillStyle strokeStyle的设置耗时可能比绘制还长)
而可以看到上图,实际上就是个渐变色的应用。只不过是比较不常见的一个圆形渐变。不使用大家更常见的linear-gradient渐变 也就是线性渐变的原因如下图。
从表现形式上来说,我更喜欢一小段呈现出更加多变的颜色。而且只要颜色设置的较为相近,应该说线条的颜色还是会挺好看的。
圆形渐变许多人了解较少。这里特地说明一下。
主要分为开始圆跟结束圆的渐变色叠加,也就是说,我们大可以设置两个同样的色板,对开始圆的坐标进行偏移达到一种绚丽的效果。但更普遍的,我们一般只用一个圆就够了。
在使用之前,我们还需要计算当前两个线之间,开始点与结束点的距离以让整个圆在开始点的坐标将颜色扩散出去。同时将开始点与结束点都迁移到开始点的屏幕像素位置。
// 通过getPixelFromCoordinate 获取当前位置对应的屏幕像素位置
let getPixelFromCoordinate = this.map.getPixelFromCoordinate.bind(
this.map
);
let startGrdPixelPos = getPixelFromCoordinate(pos1);
let endGrdPixelPos = getPixelFromCoordinate(pos2);
let xdiff = endGrdPixelPos[0] - startGrdPixelPos[0]
let ydiff = endGrdPixelPos[1] - startGrdPixelPos[1]
let radius = Math.pow(Math.pow(xdiff,2) + Math.pow(ydiff,2),0.5);
var grd = ctx.context_.createRadialGradient(
startGrdPixelPos[0],
startGrdPixelPos[1],
0,
startGrdPixelPos[0],
startGrdPixelPos[1],
radius
);
grd.addColorStop(0, "yellow");
grd.addColorStop(0.2, "red");
grd.addColorStop(0.4, "pink");
grd.addColorStop(0.6, "green");
grd.addColorStop(0.8, "orange");
grd.addColorStop(1, "blue");
ctx.setStyle(
new Style({
stroke: new Stroke({
// 模板字符串
color: grd,
lineCap: "butt",
width: 3,
}),
})
);
ctx.drawGeometry...
箭头处理
本实例中箭头主要是通过添加Icon 的方式 对图片进行旋转达到的。所以说,对比使用逻辑去计算的箭头应该说方便许多。但是有一点在这里需要注意: 不要使用src 去 为Icon 添加图片。此处也困扰了我很久,后面我基本上确定这就是一个BUG。使用src属性在prerender函数这里调用setStyle你是创建不了图片的。至于是为什么,这里就不再赘述了。
因此,我们使用图片对象去做处理。
let arrowImage = new Image();
// 再说一次: 在vue 里面, 静态文件资源放于public目录下
// 意味着此时的请求路径,如果你的端口是8080,从本质上来说等于: http://localhost:8080/image/arrow1.png
// 你的目录结构应为 public/image/arrow1.png
// 再问我就自杀
arrowImage.src = "image/arrow1.png";
let arrowFlag = false;
arrowImage.onload = function () {
arrowFlag = true;
};
layer.on("prerender", (evt) => {
let arrowGeometry = new Point(endPos);
const dx = endPos[0] - lastEndPos[0];
const dy = endPos[1] - lastEndPos[1];
const rotation = Math.atan2(dy, dx);
if (arrowFlag) {
ctx.setImageStyle(
new Icon({
img: arrowImage,
rotateWithView: true,
rotation: -rotation,
imgSize: [16, 16],
})
);
}
ctx.drawGeometry(arrowGeometry);
}
绘制多条动态曲线线段
经过上面的核心实现后,我们离应用还差一点。首先在多个不同的开始点与结束点坐标之中。我们如何计算一个大致的控制点 以及 如何描述一个曲线线段的运动。
仔细观察上述所需的属性。 我们将他 放置在一个类里统一管理。命名为source 表示 这个动态曲线的 数据源(有点像openlayers的设计,图层=》数据源=》?)。 但由于我们的实现是完全基于geometry变化进行处理的, 为了描述这个geometry的变化,我们往推进去的这一个feature 对他的属性挂载上这个数据源。
// 一个用于描述 flyLine 属性 的数据源
export default class FlyLineSource {
constructor(options) {
/**
* 开始点位置经纬度表示
*/
this.startPos = options.startPos
/**
* 结束点位置经纬度表示
*/
this.endPos = options.endPos
/**
* 线段中点位置
*/
this.centerPos = this.linearInterpolation(this.startPos, this.endPos, 0.5)
/**
* 控制点位置
*/
this.controlPos = this.getControlPoint(this.startPos,this.endPos,this.centerPos,1.0)
/**
* 保存上一个结束点位置 初始则为 开始点
*/
this.lastEndPos = this.startPos
/**
* 计数器 用于分段
*/
this.times = 0;
/**
* 曲线 数组
*/
this.lineCoords = []
/**
* 箭头相关
*/
this.arrowImage = new Image()
this.arrowImage.src = 'arrow.png'
this.arrowLoad = false
this.arrowImage.onload = () => {
this.arrowLoad = true
}
}
getControlPoint(startPos,endPos,centerPos,ratio){
let xDiff = endPos[0] - startPos[0]
let addX = startPos[0] + xDiff
let addY = startPos[1]
let controlX,controlY
controlX = addX
controlY = addY
let controlPos = [controlX , controlY]
// this real control pos
return this.linearInterpolation(centerPos,controlPos, ratio)
}
getRenderState() {
return {
times: this.times,
lineCoords: this.lineCoords,
startPos: this.startPos,
centerPos: this.centerPos,
endPos: this.endPos,
lastEndPos: this.lastEndPos,
arrowImage: this.arrowImage,
arrowLoad: this.arrowLoad,
controlPos: this.controlPos
}
}
setRenderState(options){
for(let i in options){
this[i] = options[i]
}
}
// 线性插值 函数 ... 此处的计算 只处理 二维 带x ,y 的 向量
linearInterpolation(startPos, endPos, t) {
let a = this.constantMultiVector2(1 - t, startPos)
let b = this.constantMultiVector2(t, endPos)
return this.vector2Add(a, b)
}
// 常数乘以二维向量数组 的函数
constantMultiVector2(constant, vector2) {
return [constant * vector2[0], constant * vector2[1]];
}
vector2Add(a, b) {
return [a[0] + b[0], a[1] + b[1]]
}
}
在外部定义 生成一个特别的点 feature
// 根据 pointPositions 生成 point Features
generatePointsFeatures() {
for (let i = 0, ii = this.pointPositions.length; i < ii; i++) {
let registeFeature = new Feature({
geometry: new Point(fromLonLat(this.pointPositions[i][0])),
flyLineSource: new FlyLineSource({
startPos: this.pointPositions[i][0],
endPos: this.pointPositions[i][1]
})
})
this.pointsFeatures.push(registeFeature, new Feature({
geometry: new Point(fromLonLat(this.pointPositions[i][1])) }))
}
}