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>