OpenLayers 6 實(shí)現(xiàn)帶有4個(gè)控制點(diǎn)的三階貝塞爾曲線
問題
實(shí)現(xiàn)一個(gè)類似Photoshop鋼筆工具畫出來的貝賽爾曲線,帶有4個(gè)控制點(diǎn),可以通過控制點(diǎn)實(shí)現(xiàn)對曲線的修改。
分析
繪制貝塞爾曲線的原理比較簡單,網(wǎng)上一搜一大把,對照著公式去計(jì)算就好了,這里有一篇可以參考的文章;
控制點(diǎn)和曲線分別使用兩個(gè)矢量圖層渲染,便于后期開發(fā)隱藏控制點(diǎn);
實(shí)現(xiàn)
為了方便計(jì)算,首先需要實(shí)現(xiàn)一個(gè)階乘函數(shù):
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
然后實(shí)現(xiàn)一個(gè)在t時(shí)刻計(jì)算貝塞爾曲線的輔助函數(shù),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是步進(jìn)的變化量,也就是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
}
}
控制點(diǎn)的移動(dòng)是用translate實(shí)現(xiàn)的,每次移動(dòng)的時(shí)候,都要根據(jù)控制點(diǎn)的位置重新計(jì)算曲線。
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();
})
完整代碼
更高階更復(fù)雜的貝塞爾曲線通過修改本例也可以實(shí)現(xiàn)
<!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>