xinglie / xinglie.github.io

blog
https://xinglie.github.io
153 stars 22 forks source link

把三次贝塞尔曲线放进外接矩形中 #44

Open xinglie opened 5 years ago

xinglie commented 5 years ago

给出任意的4个点绘制一个三次贝塞尔曲线,把这个曲线放进外接的一个矩形内

三次贝塞尔曲线网上有很多种实现,我们找到这样的一个方法

let threeBezier = (t, p1, cp1, cp2, p2) => {
        let { x: x1, y: y1 } = p1;
        let { x: x2, y: y2 } = p2;
        let { x: cx1, y: cy1 } = cp1;
        let { x: cx2, y: cy2 } = cp2;
        let x =
            x1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cx1 * t * (1 - t) * (1 - t) +
            3 * cx2 * t * t * (1 - t) +
            x2 * t * t * t;
        let y =
            y1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cy1 * t * (1 - t) * (1 - t) +
            3 * cy2 * t * t * (1 - t) +
            y2 * t * t * t;
        return { x, y };
    }

给出任意的一个进度t(0<=t<=1)和4个点的坐标,我们可以计算出当前贝塞尔曲线上的一个点。

根据上述代码,我们可以推导出一个t的一元三次方程。

image

根据求根公式,我们只需要计算出在0-1区间内,对应的x和y最大值和最小值即可。

推导出计算区间的函数如下

let getRanges = (p1, cp1, cp2, p2) => {
        let part = -2 * (3 * p1 - 6 * cp1 + 3 * cp2);
        let power = Math.pow(3 * p1 - 6 * cp1 + 3 * cp2, 2);
        let delta = 4 * power - 36 * (p2 - p1 + 3 * cp1 - 3 * cp2) * (cp1 - p1);
        let down = 6 * (p2 - p1 + 3 * cp1 - 3 * cp2);
        if (delta > 0) {
            let sqrt = Math.sqrt(delta);
            let t1 = (part + sqrt) / down;
            let t2 = (part - sqrt) / down;
            if (t1 >= 0 && t2 >= 0 && t1 <= 1 && t2 <= 1) {
                return [t1, t2, 0, 1];
            } else if (t1 >= 0 && t1 <= 1) {
                return [t1, 0, 1];
            } else if (t2 >= 0 && t2 <= 1) {
                return [t2, 0, 1];
            } else {
                return [0, 1];
            }
        } else if (delta <= 0) {
            return [0, 1];
        }
    };

这样给出4个点,我们就能算出它x或y的最大值和最小值

然后根据最大x,y和最小x,y画出矩形即可,完成的代码如下

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Page Title</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
</head>

<body>
    <div style="position:relative;width:500px;height:300px;background:#ccc;margin:50px;">
        <svg id="bezier" style="width:100%;height:100%;overflow:visible">
        </svg>
        <div style="position:absolute;border:solid 1px red;pointer-events: none;" id="rect"></div>
    </div>
</body>
<script>
    //p1 cp1 cp2 p2
    let bezierPoints = [
        { x: 100, y: 50 },
        { x: 150, y: 100 },
        { x: 260, y: 100 },
        { x: 200, y: 200 }
    ];
    let threeBezier = (t, p1, cp1, cp2, p2) => {
        let { x: x1, y: y1 } = p1;
        let { x: x2, y: y2 } = p2;
        let { x: cx1, y: cy1 } = cp1;
        let { x: cx2, y: cy2 } = cp2;
        let x =
            x1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cx1 * t * (1 - t) * (1 - t) +
            3 * cx2 * t * t * (1 - t) +
            x2 * t * t * t;
        let y =
            y1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cy1 * t * (1 - t) * (1 - t) +
            3 * cy2 * t * t * (1 - t) +
            y2 * t * t * t;
        return { x, y };
    }
    let getRanges = (p1, cp1, cp2, p2) => {
        let part = -2 * (3 * p1 - 6 * cp1 + 3 * cp2);
        let power = Math.pow(3 * p1 - 6 * cp1 + 3 * cp2, 2);
        let delta = 4 * power - 36 * (p2 - p1 + 3 * cp1 - 3 * cp2) * (cp1 - p1);
        let down = 6 * (p2 - p1 + 3 * cp1 - 3 * cp2);
        if (delta > 0) {
            let sqrt = Math.sqrt(delta);
            let t1 = (part + sqrt) / down;
            let t2 = (part - sqrt) / down;
            if (t1 >= 0 && t2 >= 0 && t1 <= 1 && t2 <= 1) {
                return [t1, t2, 0, 1];
            } else if (t1 >= 0 && t1 <= 1) {
                return [t1, 0, 1];
            } else if (t2 >= 0 && t2 <= 1) {
                return [t2, 0, 1];
            } else {
                return [0, 1];
            }
        } else if (delta <= 0) {
            return [0, 1];
        }
    };
    let updateOutlineRect = () => {
        let xRanges = getRanges(bezierPoints[0].x,
            bezierPoints[1].x,
            bezierPoints[2].x,
            bezierPoints[3].x);
        let yRanges = getRanges(bezierPoints[0].y,
            bezierPoints[1].y,
            bezierPoints[2].y,
            bezierPoints[3].y);
        let xValues = [];
        for (let xt of xRanges) {
            xValues.push(threeBezier(xt, bezierPoints[0], bezierPoints[1], bezierPoints[2], bezierPoints[3]).x);
        }

        let yValues = [];
        for (let yt of yRanges) {
            yValues.push(threeBezier(yt, bezierPoints[0], bezierPoints[1], bezierPoints[2], bezierPoints[3]).y);
        }

        let minX = Math.min(...xValues);
        let maxX = Math.max(...xValues);
        let minY = Math.min(...yValues);
        let maxY = Math.max(...yValues);

        rect.style.left = minX + 'px';
        rect.style.top = minY + 'px';
        rect.style.width = (maxX - minX) + 'px';
        rect.style.height = (maxY - minY) + 'px';
    };
    let updateBezier = () => {
        bezier.innerHTML = `<path style="fill:none;stroke:#000;stroke-width:1;" d="M${bezierPoints[0].x},${bezierPoints[0].y} C${bezierPoints[1].x} ${bezierPoints[1].y} ${bezierPoints[2].x} ${bezierPoints[2].y} ${bezierPoints[3].x},${bezierPoints[3].y}"/>
        <path d="M${bezierPoints[0].x},${bezierPoints[0].y} L${bezierPoints[1].x} ${bezierPoints[1].y} M${bezierPoints[2].x} ${bezierPoints[2].y} L${bezierPoints[3].x},${bezierPoints[3].y}" style="fill:none;stroke:#FA742B;stroke-width:1" />
        <circle onmousedown="resize(event,'start')" r="4" cx="${bezierPoints[0].x}" cy="${bezierPoints[0].y}" style="fill: #fff;stroke: #FA742B;" />
        <circle onmousedown="ctrl(event,'start')" r="4" cx="${bezierPoints[1].x}" cy="${bezierPoints[1].y}" style="fill: #fff;stroke: #FA742B;" />
        <circle onmousedown="ctrl(event)" r="4" cx="${bezierPoints[2].x}" cy="${bezierPoints[2].y}" style="fill: #fff;stroke: #FA742B;" />
        <circle onmousedown="resize(event)" r="4" cx="${bezierPoints[3].x}" cy="${bezierPoints[3].y}" style="fill: #fff;stroke: #FA742B;" />`;
    };
    let ctrl = (event, key) => {
        let index = key == 'start' ? 1 : 2;
        let startX = bezierPoints[index].x;
        let startY = bezierPoints[index].y;
        document.onmousemove = e => {
            let offsetX = e.pageX - event.pageX;
            let offsetY = e.pageY - event.pageY;
            bezierPoints[index].x = startX + offsetX;
            bezierPoints[index].y = startY + offsetY;
            updateBezier();
            updateOutlineRect();
        };
        document.onmouseup = e => {
            document.onmouseup = document.onmousemove = null;
        };
    };
    let resize = (event, key) => {
        let index = key == 'start' ? 0 : 3;
        let startX = bezierPoints[index].x;
        let startY = bezierPoints[index].y;
        document.onmousemove = e => {
            let offsetX = e.pageX - event.pageX;
            let offsetY = e.pageY - event.pageY;
            bezierPoints[index].x = startX + offsetX;
            bezierPoints[index].y = startY + offsetY;
            updateBezier();
            updateOutlineRect();
        };
        document.onmouseup = e => {
            document.onmouseup = document.onmousemove = null;
        };
    };
    updateBezier();
    updateOutlineRect();
</script>

</html>