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>
相关推荐
浮华似水6 分钟前
Javascirpt时区——脱坑指南
前端
王二端茶倒水8 分钟前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i13 分钟前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠15 分钟前
前端(1)——快速入门HTML
前端·html
凹凸曼打不赢小怪兽41 分钟前
react 受控组件和非受控组件
前端·javascript·react.js
醉颜凉1 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
狂奔solar1 小时前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky1 小时前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
lapiii3581 小时前
图论-代码随想录刷题记录[JAVA]
java·数据结构·算法·图论
清云随笔1 小时前
axios 实现 无感刷新方案
前端