Canvas绘制道路交叉点技术要点个人总结

1. 简介

大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题

绘制道路交叉点的初衷是为了在cesium的地图上清晰地展示道路交汇处的位置和结构,提升地图的可读性和美观性,但由于里面涉及较多的业务,在此用canvas抽出其中技术要点。

这是未绘制之前的效果:

这是绘制后的效果:

2. 技术实现

js 复制代码
// 这个是道路的初始数据
const path1 = [
    { x: 500, y: 0 },
    { x: 500, y: 1000 }
];
const path2 = [
    { x: 0, y: 800, },
    { x: 1000, y: 800 }
];
const path3 = [
    { x: 0, y: 100, },
    { x: 1000, y: 300 }
];

思路:通过计算出path1,path2,path3的所有路段的交接点,再根据路宽,通过线段平移和点平移,贝塞尔曲线计算生成交界面。

2.1 准备工作

  • 创建Canvas画布,并准备好绘制道路的基本环境。
html 复制代码
<canvas id="myCanvas" width="1000" height="1000" style="border:1px solid #000;"></canvas>
<script>
class Road {
    /**
     * Road 类的构造函数
     * @param {Canvas} canvas - 画布对象
     */
    constructor(canvas, options) {
        this.canvas = canvas;
        this.ctx = canvas.getContext("2d");
        this.paths = []; // 存储所有道路,新增道路时需要遍历老道路,看是否存在交点
        this.lineWidth = 100;
    }
}
</script>
  • 初始化Road类,包括构造函数和属性设置。
js 复制代码
 const canvas = document.getElementById("myCanvas");
 const road = new Road(canvas);

2.2 绘制路径

  • 绘制路径的方法drawPath,展示如何绘制道路线段。
js 复制代码
/**
 * 绘制路径
 * @param {Array} positions - 路径顶点数组
 * @param {Number} lineWidth - 线条宽度
 * @param {String} color - 线条颜色
 */
drawPath(positions, lineWidth = this.lineWidth, color = "#666467") {
    // 绘制路径
    this.ctx.beginPath();
    this.ctx.moveTo(positions[0].x, positions[0].y); // 移动到第一个顶点
    for (let point of positions) {
        this.ctx.lineTo(point.x, point.y);
    }
    // this.ctx.closePath(); // 封闭路径
    this.ctx.strokeStyle = color; // 设置填充颜色
    this.ctx.lineWidth = lineWidth; // 设置线条宽度
    this.ctx.stroke(); // 描边第二条线
}

这里就是简单的通过canvas绘制了一条宽为100像素的直线,颜色为#666467,canvas的具体使用方法可以参考MDN文档

2.3 获取交叉点

  • 计算两条线段的交点坐标的方法getCross,用于确定交叉点坐标。

    在获取交叉点坐标之前,我们需要掌握一定的数学知识。

    1. 两向量是否有交点,需判断两向量是否平行,若平行则无交点。(这里不讨论两线段重合的问题)
    2. 当两向量不平行时,计算出对应的相交点。
    3. 计算出的交点通过判断是否在线段上,即可判断两线段是否相交。

如果不清楚怎么获取交叉点,可以细看这篇文章

js 复制代码
/**
 * 计算两条线段的交点坐标
 * @param {Array} path1 - 第一条线段顶点数组
 * @param {Array} path2 - 第二条线段顶点数组
 * @returns {Object} - 交点坐标对象
 */
getCross(path1, path2) {
    // 计算交点的坐标
    const x1 = path1[0].x;
    const y1 = path1[0].y;
    const x2 = path1[1].x;
    const y2 = path1[1].y;

    const x3 = path2[0].x;
    const y3 = path2[0].y;
    const x4 = path2[1].x;
    const y4 = path2[1].y;

    const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
    if (denom === 0) {
        // 两条线段平行,无交点
        return null;
    }

    const t = (x3 * (y4 - y3) + y1 * (x4 - x3) - y3 * (x4 - x3) - x1 * (y4 - y3)) / denom;

    const intersectionX = x1 + t * (x2 - x1);
    const intersectionY = y1 + t * (y2 - y1);

    // 判断交点是否在两条线段之间
    if (t < 1 && t > 0) {
        return {
            x: intersectionX,
            y: intersectionY
        }
    } else {
        return null;
    }
}

2.4 绘制交叉点形状

绘制交叉多边形,我的想法是这样:

  1. 通过前面的计算我们已经知道交叉点坐标。
  1. 通过交叉点以及线段的起始或终点位置,获取一个向量
  1. 获取该向量的法向量,将向量沿着法向量的方向平移道路宽度lineWidth的长度
  1. 取平移后向量的lineWidth/2的长度即可获得路段平移后的点
  1. 按上述方式即可获取马路的8个点,依次取2个点和交点取一个合适的值,计算出一个中点。
js 复制代码
getCurve(p1, p2, cross) {
    let midPoint = {
        x: (p1.x + p2.x) / 2,
        y: (p1.y + p2.y) / 2
    };
    const x = midPoint.x - cross.x;
    const y = midPoint.y - cross.y;
    const len = - 5 * Math.sqrt(2) * (Math.sqrt(x * x + y * y)) / 8;
    const p = this.extendLine([midPoint, cross], len);
    const points = this.createBezire(p1, p, p2);
    return points;
}
  1. 根据3个点即可获取贝塞尔曲线,然后绘制曲线路径,根据路径绘制多边形即可。

3. 完整代码展示

js 复制代码
/**
 * Road 类用于操作道路相关的绘图功能
 */
class Road {
    /**
     * Road 类的构造函数
     * @param {Canvas} canvas - 画布对象
     */
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext("2d");
        this.paths = [];
        this.lineWidth = 100;
    }

    /**
     * 创建新路径
     * @param {Array} positions - 路径顶点数组
     * @param {String} color - 路径颜色
     */
    createPath(positions, color) {
        this.drawPath(positions, color);
        this.drawCrossRoad(positions);
        this.paths.push(positions);
    }

    /**
     * 绘制路径
     * @param {Array} positions - 路径顶点数组
     * @param {Number} lineWidth - 线条宽度
     * @param {String} color - 线条颜色
     */
    drawPath(positions, lineWidth = this.lineWidth, color = "#666467") {
        // 绘制路径
        this.ctx.beginPath();
        this.ctx.moveTo(positions[0].x, positions[0].y); // 移动到第一个顶点
        for (let point of positions) {
            this.ctx.lineTo(point.x, point.y);
        }
        // this.ctx.closePath(); // 封闭路径
        this.ctx.strokeStyle = color; // 设置填充颜色
        this.ctx.lineWidth = lineWidth; // 设置线条宽度
        this.ctx.stroke(); // 描边第二条线
    }

    /**
     * 绘制多边形
     * @param {Array} positions - 多边形顶点数组
     */
    drawPolygon(positions) {
        // 绘制多边形
        this.ctx.beginPath();
        this.ctx.moveTo(positions[0].x, positions[0].y);
        for (let point of positions) {
            this.ctx.lineTo(point.x, point.y);
        }
        this.ctx.closePath();
        this.ctx.fillStyle = "#383a46";
        this.ctx.fill();
    }

    /**
     * 计算两条线段的交点坐标
     * @param {Array} path1 - 第一条线段顶点数组
     * @param {Array} path2 - 第二条线段顶点数组
     * @returns {Object} - 交点坐标对象
     */
    getCross(path1, path2) {
        // 计算交点的坐标
        const x1 = path1[0].x;
        const y1 = path1[0].y;
        const x2 = path1[1].x;
        const y2 = path1[1].y;

        const x3 = path2[0].x;
        const y3 = path2[0].y;
        const x4 = path2[1].x;
        const y4 = path2[1].y;

        const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
        if (denom === 0) {
            // 两条线段平行,无交点
            return null;
        }

        const t = (x3 * (y4 - y3) + y1 * (x4 - x3) - y3 * (x4 - x3) - x1 * (y4 - y3)) / denom;

        const intersectionX = x1 + t * (x2 - x1);
        const intersectionY = y1 + t * (y2 - y1);

        // 判断交点是否在两条线段之间
        if (t < 1 && t > 0) {
            return {
                x: intersectionX,
                y: intersectionY
            }
        } else {
            return null;
        }

        // const k1 = (y1 - y2) / (x1 - x2);
        // const b1 = y1 - k1 * x1;

        // const k2 = (y3 - y4) / (x3 - x4);
        // const b2 = y3 - k2 * x3;

        // if(k1 === k2) {
        //     return null;
        // }
        // const x = (b2 - b1) / (k1 - k2);
        // let y;
        // if (k1) {
        //     y = k1 * x + b1;
        // } else {
        //     y = k2 * x + b2;
        // }
        // if (
        //     y >= Math.min(y1, y2) &&
        //     y <= Math.max(y1, y2) &&
        //     y >= Math.min(y3, y4) &&
        //     y <= Math.max(y3, y4) &&
        //     x >= Math.min(x1, x2) &&
        //     x <= Math.max(x1, x2) &&
        //     x >= Math.min(x3, x4) &&
        //     x <= Math.max(x3, x4)
        // ) {
        //     return {
        //         x: x,
        //         y: y
        //     }
        // }
        // return null;
    }

    /**
     * 计算二次贝塞尔曲线上的点
     * @param {Object} p0 - 起点
     * @param {Object} p1 - 控制点
     * @param {Object} p2 - 终点
     * @param {Number} t - 参数值
     * @returns {Object} - 曲线上的点
     */
    static quadraticBezier(p0, p1, p2, t) {
        let x = Math.pow(1 - t, 2) * p0.x + 2 * (1 - t) * t * p1.x + Math.pow(t, 2) * p2.x;
        let y = Math.pow(1 - t, 2) * p0.y + 2 * (1 - t) * t * p1.y + Math.pow(t, 2) * p2.y;
        return { x: x, y: y };
    }

    /**
     * 获取曲线路径
     * @param {Object} p1 - 起点
     * @param {Object} p2 - 终点
     * @param {Object} cross - 交点
     * @returns {Array} - 曲线路径数组
     */
    getCurve(p1, p2, cross) {
        let midPoint = {
            x: (p1.x + p2.x) / 2,
            y: (p1.y + p2.y) / 2
        };
        const x = midPoint.x - cross.x;
        const y = midPoint.y - cross.y;
        const len = - 5 * Math.sqrt(2) * (Math.sqrt(x * x + y * y)) / 8;
        const p = this.extendLine([midPoint, cross], len);
        const points = this.createBezire(p1, p, p2);
        return points;
    }

    /**
     * 生成贝塞尔曲线
     * @param {Object} p0 - 起点
     * @param {Object} p1 - 控制点
     * @param {Object} p2 - 终点
     * @returns {Array} - 贝塞尔曲线路径数组
     */
    createBezire(p0, p1, p2) {
        const path = [];
        for (let t = 0; t <= 1; t += 0.1) {
            let point = Road.quadraticBezier(p0, p1, p2, t);
            path.push(point);
        }
        return path;
    }

    /**
     * 按法线平移线段
     * @param {Array} path - 线段顶点数组
     * @param {Number} movementLength - 移动距离
     * @returns {Array} - 新的起点和终点坐标数组
     */
    moveLineAlongNormal(path, movementLength) {
        const x1 = path[0].x;
        const y1 = path[0].y;
        const x2 = path[1].x;
        const y2 = path[1].y;

        // 计算线段的方向向量
        let dx = x2 - x1;
        let dy = y2 - y1;

        // 计算线段的法线向量
        let nx = -dy;
        let ny = dx;

        // 计算法线向量的长度
        let length_n = Math.sqrt(nx * nx + ny * ny);

        // 计算单位法线向量
        let unit_nx = nx / length_n;
        let unit_ny = ny / length_n;

        // 返回新的起点和终点坐标
        return [
            {
                x: x1 + movementLength * unit_nx,
                y: y1 + movementLength * unit_ny
            },
            {
                x: x2 + movementLength * unit_nx,
                y: y2 + movementLength * unit_ny
            }
        ];
    }

    /**
     * 延长线段计算坐标
     * @param {Array} path - 线段顶点数组
     * @param {Number} distance - 射线长度
     * @returns {Object} - 计算得到的坐标对象
     */
    extendLine(path, distance) {
        let x1 = path[0].x;
        let y1 = path[0].y;
        let x2 = path[1].x;
        let y2 = path[1].y;

        // 计算线段的方向向量
        let dx = x2 - x1;
        let dy = y2 - y1;

        // 计算方向向量的长度
        let length = Math.sqrt(dx * dx + dy * dy);

        // 计算单位方向向量
        let unit_dx = dx / length;
        let unit_dy = dy / length;

        // 计算新的终点坐标
        let new_x2 = x2 + distance * unit_dx;
        let new_y2 = y2 + distance * unit_dy;
        // 输出新的终点坐标
        return {
            x: new_x2,
            y: new_y2
        }
    }


    /**
     * 绘制道路
     * @param {Array} positions - 路径顶点数组
     */
    drawCrossRoad(positions) {
        const res = [];
        for (let path of this.paths) {
            for (let i = 0, len = path.length; i < len - 1; i++) {
                const oldPath = [path[i], path[i + 1]];
                for (let j = 0, len = positions.length; j < len - 1; j++) {
                    const newPath = [positions[j], positions[j + 1]];
                    const cross = this.getCross(oldPath, newPath);
                    if (cross) {
                        const path1 = this.moveLineAlongNormal([newPath[0], cross], this.lineWidth / 2);
                        const point1Start = this.extendLine(path1, -this.lineWidth);
                        const point1End = this.extendLine(path1, this.lineWidth);

                        const path2 = this.moveLineAlongNormal([newPath[0], cross], -this.lineWidth / 2);
                        const point2Start = this.extendLine(path2, -this.lineWidth);
                        const point2End = this.extendLine(path2, this.lineWidth);

                        const path3 = this.moveLineAlongNormal([oldPath[0], cross], this.lineWidth / 2);
                        const point3Start = this.extendLine(path3, -this.lineWidth);
                        const point3End = this.extendLine(path3, this.lineWidth);

                        const path4 = this.moveLineAlongNormal([oldPath[0], cross], -this.lineWidth / 2);
                        const point4Start = this.extendLine(path4, -this.lineWidth);
                        const point4End = this.extendLine(path4, this.lineWidth);

                        const bezire1 = this.getCurve(point1Start, point3End, cross);
                        const bezire2 = this.getCurve(point4End, point1End, cross);
                        const bezire3 = this.getCurve(point2End, point4Start, cross);
                        const bezire4 = this.getCurve(point3Start, point2Start, cross);

                        const points = [...bezire1, ...bezire2, ...bezire3, ...bezire4, bezire1[0]];
                        this.drawPolygon(points, 1, '#0099ff');
                    }
                }
            }
        }
    }
}
html 复制代码
<canvas id="myCanvas" width="1000" height="1000" style="border:1px solid #000;"></canvas>
<script src="./road.js"></script>
<script>
    var canvas = document.getElementById("myCanvas");
    const road = new Road(canvas);
    const path1 = [
        { x: 500, y: 0 },
        { x: 500, y: 1000 }
    ]
    const path2 = [
        { x: 0, y: 600, },
        { x: 1000, y: 600 }
    ]
    const path3 = [
        { x: 0, y: 100, },
        { x: 1000, y: 300 }
    ];
    road.createPath(path1);
    road.createPath(path2);
    road.createPath(path3);
</script>
相关推荐
天宇&嘘月2 小时前
web第三次作业
前端·javascript·css
小王不会写code3 小时前
axios
前端·javascript·axios
尼尔森系4 小时前
排序与算法:希尔排序
c语言·算法·排序算法
发呆的薇薇°4 小时前
vue3 配置@根路径
前端·vue.js
luckyext4 小时前
HBuilderX中,VUE生成随机数字,vue调用随机数函数
前端·javascript·vue.js·微信小程序·小程序
小小码农(找工作版)4 小时前
JavaScript 前端面试 4(作用域链、this)
前端·javascript·面试
AC使者4 小时前
A. C05.L08.贪心算法入门
算法·贪心算法
冠位观测者4 小时前
【Leetcode 每日一题】624. 数组列表中的最大距离
数据结构·算法·leetcode
前端没钱4 小时前
前端需要学习 Docker 吗?
前端·学习·docker
前端郭德纲4 小时前
前端自动化部署的极简方案
运维·前端·自动化