OpenLayers 6 实现带有4个控制点的三阶贝塞尔曲线
问题
实现一个类似Photoshop钢笔工具画出来的贝赛尔曲线,带有4个控制点,可以通过控制点实现对曲线的修改。
分析
绘制贝塞尔曲线的原理比较简单,网上一搜一大把,对照着公式去计算就好了,这里有一篇可以参考的文章;
控制点和曲线分别使用两个矢量图层渲染,便于后期开发隐藏控制点;
实现
为了方便计算,首先需要实现一个阶乘函数:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
然后实现一个在t时刻计算贝塞尔曲线的辅助函数,t的含义见参考文章:
function getCoordinatesBezier(controlPoints, t) {
var x = 0,
y = 0,
n = controlPoints.length - 1;
controlPoints.forEach(function (item, index) {
let coord = item.getGeometry().getFirstCoordinate();
if (!index) {
x += coord[0] * Math.pow((1 - t), n - index) * Math.pow(t, index)
y += coord[1] * Math.pow((1 - t), n - index) * Math.pow(t, index)
} else {
x += factorial(n) / factorial(index) / factorial(n - index) * coord[0] * Math.pow((1 - t), n - index) * Math.pow(t, index)
y += factorial(n) / factorial(index) / factorial(n - index) * coord[1] * Math.pow((1 - t), n - index) * Math.pow(t, index)
}
})
return [x, y]
}
最后是生成贝塞尔曲线的整体过程,step是步进的变化量,也就是t每次变化的量:
function genBezierGeom(controlPoints, step) {
const nodeArr = controlPoints.sort(function (a, b) {
return a.get('cid') - b.get('cid')
});
if (nodeArr.length === 2) {
var lineFeature = turf.lineString([nodeArr[0].getGeometry().getFirstCoordinate(), nodeArr[1].getGeometry().getFirstCoordinate()]);
return lineFeature
} else {
var bezierPoints = [];
for (i = 0; i < 1; i += ((step !== null) ? step : 0.01)) {
bezierPoints.push(getCoordinatesBezier(nodeArr, i))
}
var bezierLine = turf.lineString(bezierPoints);
return bezierLine
}
}
控制点的移动是用translate实现的,每次移动的时候,都要根据控制点的位置重新计算曲线。
translate.on('translating', (evt) => {
if (evt.features.item(0).getGeometry().getType() === 'Point') {
bSource.clear();
bSource.addFeature((new ol.format.GeoJSON()).readFeature(genBezierGeom(cSource.getFeatures(), 0.001)));
} else {
const deltaX = evt.coordinate[0] - startCoord[0];
const deltaY = evt.coordinate[1] - startCoord[1];
startCoord=evt.coordinate.concat();
cSource.getFeatures().forEach(function (feature) {
const geom = feature.getGeometry();
geom.translate(deltaX, deltaY);
feature.setGeometry(geom);
});
}
})
translate.on('translatestart', (evt) => {
startCoord=evt.coordinate.concat();
})
完整代码
更高阶更复杂的贝塞尔曲线通过修改本例也可以实现
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet" href="./include/ol.css" type="text/css" />
<script src="./include/ol.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
</head>
<style>
</style>
<body>
<div id="map" class="map"></div>
<script>
let baseLayer = new ol.layer.Tile({
title: "base",
source: new ol.source.XYZ({
url: 'http://www.google.cn/maps/vt?lyrs=m@189&gl=cn&x={x}&y={y}&z={z}'
})
});
var bSource = new ol.source.Vector({
wrapX: false,
});
var cSource = new ol.source.Vector({
wrapX: false,
});
var bLayer = new ol.layer.Vector({
source: bSource
});
var cLayer = new ol.layer.Vector({
source: cSource
})
var pointArr = [[0, 0], [20, 30], [50, 30], [75, 40]];
var ctrlFeatures = [];
pointArr.forEach((item, index) => {
var ctrlPointFeature = new ol.Feature({
cid: index,
geometry: new ol.geom.Point(item)
});
ctrlPointFeature.setStyle(
new ol.style.Style({
image: new ol.style.Circle({
radius: 5,
fill: new ol.style.Fill({
color: [255, 255, 255, 0.5]
}),
stroke: new ol.style.Stroke({
color: [122, 122, 122, 1],
width: 2
})
})
})
)
ctrlFeatures.push(ctrlPointFeature);
cSource.addFeature(ctrlPointFeature);
})
bSource.addFeature((new ol.format.GeoJSON()).readFeature(genBezierGeom(ctrlFeatures, 0.1)));
let translate = new ol.interaction.Translate({
hitTolerance: 5,
});
let map = new ol.Map({
target: 'map',
interactions: ol.interaction.defaults().extend([
translate
]),
layers: [baseLayer, bLayer, cLayer],
view: new ol.View({
center: [0, 0],
projection: "EPSG:4326",
zoom: 4
})
});
var startCoord=[0,0];
translate.on('translating', (evt) => {
if (evt.features.item(0).getGeometry().getType() === 'Point') {
bSource.clear();
bSource.addFeature((new ol.format.GeoJSON()).readFeature(genBezierGeom(cSource.getFeatures(), 0.001)));
} else {
const deltaX = evt.coordinate[0] - startCoord[0];
const deltaY = evt.coordinate[1] - startCoord[1];
startCoord=evt.coordinate.concat();
cSource.getFeatures().forEach(function (feature) {
const geom = feature.getGeometry();
geom.translate(deltaX, deltaY);
feature.setGeometry(geom);
});
}
})
translate.on('translatestart', (evt) => {
startCoord=evt.coordinate.concat();
})
function getCoordinatesBezier(controlPoints, t) {
var x = 0,
y = 0,
n = controlPoints.length - 1;
controlPoints.forEach(function (item, index) {
let coord = item.getGeometry().getFirstCoordinate();
if (!index) {
x += coord[0] * Math.pow((1 - t), n - index) * Math.pow(t, index)
y += coord[1] * Math.pow((1 - t), n - index) * Math.pow(t, index)
} else {
x += factorial(n) / factorial(index) / factorial(n - index) * coord[0] * Math.pow((1 - t), n - index) * Math.pow(t, index)
y += factorial(n) / factorial(index) / factorial(n - index) * coord[1] * Math.pow((1 - t), n - index) * Math.pow(t, index)
}
})
return [x, y]
}
function genBezierGeom(controlPoints, step) {
const nodeArr = controlPoints.sort(function (a, b) {
return a.get('cid') - b.get('cid')
});
if (nodeArr.length === 2) {
var lineFeature = turf.lineString([nodeArr[0].getGeometry().getFirstCoordinate(), nodeArr[1].getGeometry().getFirstCoordinate()]);
return lineFeature
} else {
var bezierPoints = [];
for (i = 0; i < 1; i += ((step !== null) ? step : 0.01)) {
bezierPoints.push(getCoordinatesBezier(nodeArr, i))
}
var bezierLine = turf.lineString(bezierPoints);
return bezierLine
}
}
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
</script>
</body>
</html>