一、背景:

在使用 canvas 做知识图谱的时,实体关系使用线宽为 1px 的线绘制, 用户必须点在线上, 才能正常拾取到点击的边。 边关系,有些是直线边,有些是二次贝塞尔曲线。产品提议,线不能加粗, 否则图谱展示大量数据时, 有碍美观。直线边扩展已经完成, 曲线边相对麻烦一些。 IE 浏览器不支持 canvas 判断点击事件源是否在路径上的接口 sPointInstroke ; 而对 isPointInPath 的支持,仅限于中心线封闭出来的路径, 中心线外侧的部分能通过 isPointInPath 判断是无效的,也就是说, 加宽线宽在 IE 浏览器是无法正常使用 isPointInPath 接口的。

二、方案:通过数学运算, 实现 isPointInPath 接口底层逻辑

在二次贝兹曲线的外侧绘制两条等距曲线,然后封闭路径。判断点击事件源坐标是否在该路径内,如果是,则判定该曲线被点中; 否则该曲线未被点中。以下是图示:

如图, 请看 a<0 下面的图片。

途中浅蓝色的二次曲线一共有三条, 中间的一条是图谱上已经绘制的,记为 lineA , 上方和下方的两条曲线分别记为 lineBlineC , 可以看到 lineBlineClineA 包围, 如果将 lineBlineC 两端连接, 则形成了一个封闭的图形, 正好包裹住中间的曲线。

三、实现:

1. 数学几何知识提要

  1. 直线可以用方程来表示 : y = ax + b, 其中 a 为斜率, b 为常数项;
  2. 斜率的计算方法:a = (deltaY1 - deltaY2) / (deltaX1 / detalX2);
  3. 平行的直线斜率相同, 常数项不同, 即 a 相等, b 不相等;
  4. 垂直的直线斜率的乘积是 -1;
  5. 直角三角形、勾股定理、相似三角形的角和边关系以及三角函数 sincos 请自行百度。

2. 注意事项

  1. 计算机屏幕坐标系原点 (0,0) 为屏幕的左上角, 向右为 x 轴正方向, 向下为 y 轴正方向。 所以在不偏移画布的前提下, 是没有负数的坐标的。
  2. “起点-控制点”连线形成的直线, 上图中 AH , “终点-控制点” 连线形成的直线 HK , 若这两条直线的斜率正负号相同, 是一种情形; 这两条直线的斜率正负号不同,又是另一种情形。详见后边的代码。

3. 实现代码:

  1. 为了能用最小的开销实现预览代码效果, 我把代码都摘出来放到一个 html 文档中了;
  2. 为了能更好的表达点、线的所属关系, 命名有点冗长了。倒也情有可原,原因嘛,一是现代编译工具可以帮我打包压缩,因此也无所顾忌; 二是如果不好好命名,过一段时间我自己也睁眼瞎了。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><canvas id="canvas" width="1980" height="1000"></canvas><script>const run = (start, controlPoint, end) => {const canvas = document.querySelector('#canvas')const context = canvas.getContext('2d');const RANGE = 10; // 拾取扩展范围/*求出贝塞尔曲线“起点-控制点”和“终点-控制点”的直线方程@params controlPoint 控制点@params controlPoint 起点@params controlPoint 终点*/function getBaseEquation(controlPoint, start, end) {const slopeOfLineControlPointToStart = (controlPoint.y - start.y) / (controlPoint.x - start.x) //   控制点-起点斜率const slopeOfLineControlPointToEnd = (controlPoint.y - end.y) / (controlPoint.x - end.x) //  控制点-终点斜率const absoluteTermOfLineToStart = start.y - slopeOfLineControlPointToStart * start.x //  控制点-起点直线方程常数项const absoluteTermOfLineToEnd = end.y - slopeOfLineControlPointToEnd * end.x // 控制点-终点直线方程常数项const lineControlPointToStart = x => slopeOfLineControlPointToStart * x + absoluteTermOfLineToStart // 控制点-起点直线方程const lineControlPointToEnd = x => slopeOfLineControlPointToEnd * x + absoluteTermOfLineToEnd // 控制点-终点直线方程return {lineControlPointToStart,lineControlPointToEnd,slopeOfLineControlPointToStart,slopeOfLineControlPointToEnd,absoluteTermOfLineToStart,absoluteTermOfLineToEnd}}const { lineControlPointToStart,lineControlPointToEnd,slopeOfLineControlPointToStart,slopeOfLineControlPointToEnd,absoluteTermOfLineToStart,absoluteTermOfLineToEnd } = getBaseEquation(controlPoint, start, end)/*根据贝塞尔曲线“起点-控制点”和“终点-控制点”的直线方程,求出扩展后的上下四条平行线的方程* */function getShiftedEquation(controlPoint, start, end, range) {const hypotenuseOfStart = Math.sqrt(Math.pow(controlPoint.x - start.x, 2) + Math.pow(controlPoint.y - start.y, 2)) // 控制点-起点弦长const RANGE_OF_START = RANGE * hypotenuseOfStart / Math.abs(controlPoint.x - start.x) // 与 “控制点-起点”直线平行的两条直线的 y 轴偏移量const hypotenuseOfEnd = Math.sqrt(Math.pow(controlPoint.x - end.x, 2) + Math.pow(controlPoint.y - end.y, 2)) // 控制点-终点弦长const RANGE_OF_END = RANGE * hypotenuseOfEnd / Math.abs(controlPoint.x - end.x)// 与 “控制点-终点”直线平行的两条直线的 y 轴偏移量const lineLeftAbove = x => (lineControlPointToStart(x) + RANGE_OF_START) // 起点-控制点向上偏移方程const lineLeftBellow = x => (lineControlPointToStart(x) - RANGE_OF_START) // 起点-控制点向下偏移方程const lineRightAbove = x => (lineControlPointToEnd(x) + RANGE_OF_END) // 终点-控制点向上偏移方程const lineRightBellow = x => (lineControlPointToEnd(x) - RANGE_OF_END) // 终点-控制点向下偏移方程return {RANGE_OF_START,RANGE_OF_END,lineLeftAbove,lineLeftBellow,lineRightAbove,lineRightBellow}}const {RANGE_OF_START,RANGE_OF_END,lineLeftAbove,lineLeftBellow,lineRightAbove,lineRightBellow} = getShiftedEquation(controlPoint, start, end, RANGE)/**** 求出偏移后的控制点, 偏移直线的交点即为偏移后的控制点*/function getShiftedControlPoint() {let controlPonitAboveX = (absoluteTermOfLineToEnd + RANGE_OF_END - absoluteTermOfLineToStart - RANGE_OF_START) / (slopeOfLineControlPointToStart - slopeOfLineControlPointToEnd)if (slopeOfLineControlPointToStart * slopeOfLineControlPointToEnd < 0) {controlPonitAboveX = (absoluteTermOfLineToEnd - RANGE_OF_END - absoluteTermOfLineToStart - RANGE_OF_START) / (slopeOfLineControlPointToStart - slopeOfLineControlPointToEnd)}const controlPointAbove = {x: controlPonitAboveX,y: lineLeftAbove(controlPonitAboveX)}let controlPonitBelloweX = (absoluteTermOfLineToEnd - RANGE_OF_END - absoluteTermOfLineToStart + RANGE_OF_START) / (slopeOfLineControlPointToStart - slopeOfLineControlPointToEnd)if (slopeOfLineControlPointToStart * slopeOfLineControlPointToEnd < 0) {controlPonitBelloweX = (absoluteTermOfLineToEnd + RANGE_OF_END - absoluteTermOfLineToStart + RANGE_OF_START) / (slopeOfLineControlPointToStart - slopeOfLineControlPointToEnd)}const controlPointBellow = {x: controlPonitBelloweX,// slopeOfLineControlPointToStart * slopeOfLineControlPointToEnd < 0 时, 线要变y: lineLeftBellow(controlPonitBelloweX)}return {controlPointAbove,controlPointBellow}}const { controlPointAbove, controlPointBellow } = getShiftedControlPoint()/*** 求出经过左、右侧端点的垂线方程* 求出偏移后的两条贝塞尔曲线的起点和终点坐标*/function getShiftedStartAndEndPoints() {const absoluteTermOfPerpendicularLineControlPointToStart = start.y + 1 / slopeOfLineControlPointToStart * start.x  // “控制点-起点”偏移方程的常数项const absoluteTermOfPerpendicularLineControlPointToEnd = end.y + 1 / slopeOfLineControlPointToEnd * end.x // “控制点-终点”偏移方程的常数项const perpendicularLineControlPointToStart = x => -(1 / slopeOfLineControlPointToStart) * x + absoluteTermOfPerpendicularLineControlPointToStart  // 垂线方程const perpendicularLineControlPointToEnd = x => -(1 / slopeOfLineControlPointToEnd) * x + absoluteTermOfPerpendicularLineControlPointToEnd // 垂线方程// 5. 求出4个垂足的坐标// 5.1 上边曲线起点// y = slopeOfLineControlPointToStart * x + absoluteTermOfLineToStart + RANGE_OF_START// y = -(1/slopeOfLineControlPointToStart) * x + absoluteTermOfPerpendicularLineControlPointToStart// 0 = (slopeOfLineControlPointToStart + 1/slopeOfLineControlPointToStart) * x + absoluteTermOfLineToStart + RANGE_OF_START -absoluteTermOfPerpendicularLineControlPointToStartconst leftAboveStartX = (absoluteTermOfPerpendicularLineControlPointToStart - absoluteTermOfLineToStart - RANGE_OF_START) / (slopeOfLineControlPointToStart + 1 / slopeOfLineControlPointToStart)const leftAboveStart = {x: leftAboveStartX,y: lineLeftAbove(leftAboveStartX)}// 5.2 上边曲线终点const rightAboveEndX = (absoluteTermOfPerpendicularLineControlPointToEnd - absoluteTermOfLineToEnd - RANGE_OF_END) / (slopeOfLineControlPointToEnd + 1 / slopeOfLineControlPointToEnd)const rightAboveEnd = {x: rightAboveEndX,y: lineRightAbove(rightAboveEndX)}// 5.3 下边曲线起点// y = slopeOfLineControlPointToStart * x + absoluteTermOfLineToStart - RANGE_OF_START// y = -(1/slopeOfLineControlPointToStart) * x + absoluteTermOfPerpendicularLineControlPointToStart// 0 = (slopeOfLineControlPointToStart + 1/slopeOfLineControlPointToStart) * x + absoluteTermOfLineToStart - RANGE_OF_START - absoluteTermOfPerpendicularLineControlPointToStartconst leftBellowStartX = (absoluteTermOfPerpendicularLineControlPointToStart + RANGE_OF_START - absoluteTermOfLineToStart) / (slopeOfLineControlPointToStart + 1 / slopeOfLineControlPointToStart)const leftBellowStart = {x: leftBellowStartX,y: lineLeftBellow(leftBellowStartX)}// 5.4 下边曲线终点const rightBellowX = (absoluteTermOfPerpendicularLineControlPointToEnd + RANGE_OF_END - absoluteTermOfLineToEnd) / (slopeOfLineControlPointToEnd + 1 / slopeOfLineControlPointToEnd)const rightBellowEnd = {x: rightBellowX,y: lineRightBellow(rightBellowX)}return {leftAboveStart,rightAboveEnd,leftBellowStart,rightBellowEnd,}}const { leftAboveStart,rightAboveEnd,leftBellowStart,rightBellowEnd} = getShiftedStartAndEndPoints()// 6. 画圆环if (slopeOfLineControlPointToStart * slopeOfLineControlPointToEnd < 0) {context.save();context.beginPath();context.moveTo(leftAboveStart.x, leftAboveStart.y);context.quadraticCurveTo(controlPointAbove.x, controlPointAbove.y, rightBellowEnd.x, rightBellowEnd.y);context.lineTo(rightAboveEnd.x, rightAboveEnd.y);context.quadraticCurveTo(controlPointBellow.x, controlPointBellow.y, leftBellowStart.x, leftBellowStart.y);context.lineTo(leftAboveStart.x, leftAboveStart.y);context.stroke();context.restore();} else {// 6. 画圆环context.save();context.moveTo(leftAboveStart.x, leftAboveStart.y);context.quadraticCurveTo(controlPointAbove.x, controlPointAbove.y, rightAboveEnd.x, rightAboveEnd.y);context.lineTo(rightBellowEnd.x, rightBellowEnd.y);context.quadraticCurveTo(controlPointBellow.x, controlPointBellow.y, leftBellowStart.x, leftBellowStart.y);context.lineTo(leftAboveStart.x, leftAboveStart.y);context.stroke();context.restore();}// 1. 以下是辅助作图, 对实际作图无影响, 只是便于理解// 2. 以下是辅助作图, 对实际作图无影响, 只是便于理解// 3. 以下是辅助作图, 对实际作图无影响, 只是便于理解// 画中心贝塞尔曲线context.save();context.beginPath();context.beginPath();context.moveTo(start.x, start.y);context.quadraticCurveTo(controlPoint.x, controlPoint.y, end.x, end.y);context.stroke();context.restore();const drawCircle = (x, y, color) => {context.save()context.beginPath();context.moveTo(x, y);context.strokeStyle = color;context.fillStyle = color;context.arc(x, y, 5, 0, Math.PI * 2);context.fill();context.restore();}drawCircle(controlPoint.x, controlPoint.y, 'red')drawCircle(controlPointAbove.x, controlPointAbove.y, 'green')drawCircle(controlPointBellow.x, controlPointBellow.y, 'blue')drawCircle(leftAboveStart.x, leftAboveStart.y, 'black')drawCircle(leftBellowStart.x, leftBellowStart.y, 'black')const drawLines = (x1, y1, x2, y2, x3, y3, color) => {context.save();context.beginPath();context.strokeStyle = color || "#000";context.moveTo(x1, y1);context.lineTo(x2, y2);context.lineTo(x3, y3);context.stroke();context.restore();}// if (slopeOfLineControlPointToStart * slopeOfLineControlPointToEnd < 0) {//   // 高线起点---高线控制点的线---高线终点//   drawLines(leftAboveStart.x, leftAboveStart.y, controlPointAbove.x, controlPointAbove.y, rightBellowEnd.x, rightBellowEnd.y)//   drawLines(leftBellowStart.x, leftBellowStart.y, controlPointBellow.x, controlPointBellow.y, rightAboveEnd.x, rightAboveEnd.y)// } else {//   // 高线起点---高线控制点的线---高线终点//   drawLines(leftAboveStart.x, leftAboveStart.y, controlPointAbove.x, controlPointAbove.y, rightAboveEnd.x, rightAboveEnd.y)//   drawLines(leftBellowStart.x, leftBellowStart.y, controlPointBellow.x, controlPointBellow.y, rightBellowEnd.x, rightBellowEnd.y)// }}const line1 = run({ x: 321, y: 743 },{ x: 345, y: 632 },{ x: 324, y: 126 })const line2 = run({ x: 621, y: 743 },{ x: 587, y: 632 },{ x: 612, y: 126 })const line3 = run({ x: 1432, y: 200 },{ x: 1342, y: 600 },{ x: 800, y: 800 })const line4 = run({ x: 900, y: 200 },{ x: 800, y: 600 },{ x: 700, y: 800 })</script></body></html>

四、效果预览:

如下图, 一共有四条曲线, 每条曲线都被两条曲线包围, 并且两端封闭。我们只需要判断点击事件源坐标是否在封闭图形内部, 即可判定封闭路径内部的曲线是否被点中, 这样就可以实现在不加宽线宽的前提下,扩展点击范围的目的了。几点说明:

  1. 黑色的点是扩展曲线的起点;
  2. 红色点点是中间曲线的控制点;
  3. 蓝色的点和绿色的点是两条扩展曲线的控制点。

绘制二次贝塞尔曲线(二次贝兹曲线)等距线:让 IE 支持 canvas接口 isPointInPath相关推荐

  1. [zz]用三阶贝塞尔曲线(贝兹曲线)拟合劣圆弧的公式(附伪代码)

    转自:用三阶贝塞尔曲线(贝兹曲线)拟合劣圆弧的公式(附伪代码) 三阶贝塞尔曲线有四个控制点A.B.C.D, 若要用三阶贝塞尔曲线拟合劣圆弧,自然的要求是: 1)A位于圆弧的起点,D位于圆弧的终点: 2 ...

  2. bezier.CSS_SVG_canvas画_贝兹曲线

    ZC:(1).SVG可以绘制 贝兹曲线:(2).canvas能绘制 贝兹曲线:(3).现在(20180202)查资料发现,css 貌似不能绘制 贝兹曲线,css使用贝兹曲线 主要是用于控制动画的速度, ...

  3. python 绘制lift曲线_洛伦兹曲线(Lorenz curve)提升指数、提升表和提升图

    python金融风控评分卡模型和数据分析微专业课(博主亲自录制视频):http://dwz.date/b9vv 医药统计项目可联系 QQ:231469242 洛伦兹曲线(Lorenz curve)也叫 ...

  4. 泊松回归、gamma回归、Tweedie回归等广义线性回归模型GLM的评估指标:校准曲线、 洛伦兹曲线、卡方检验、AIC、BIC、偏差(Deviance)指标

    泊松回归.gamma回归.Tweedie回归等广义线性回归模型GLM的评估指标:校准曲线(Calibration curve). 洛伦兹曲线(Lorenz Curve).卡方检验.AIC.BIC.偏差 ...

  5. canvas 贝萨尔曲线

    二次贝塞尔曲线 定义:quadraticCurveTo() 方法通过使用表示二次贝塞尔曲线的指定控制点,向当前路径添加一个点. 说明:二次贝塞尔曲线需要两个点.第一个点是用于二次贝塞尔计算中的控制点, ...

  6. css沿曲线进行动画,jQuery沿贝兹曲线运动动画特效

    特效描述:jQuery 沿贝兹曲线运动 动画特效.jQuery沿贝兹曲线运动动画特效 代码结构 1. 引入CSS 2. 引入JS 3. HTML代码 10条曲线 开始动画 Plot 10条贝兹曲线 开 ...

  7. .net cf wince 贝兹 曲线图

    项目需要在WINCE设备中显示曲线图,由于wince采集器默认是不带画图的动态库的,所以在网上找了一个动态库(XrossGDIPlus)来画图,关于XrossGDIPlus具体参考http://www ...

  8. 洛伦茨曲线半高全宽_洛伦兹曲线

    洛伦兹曲线 百科名片 洛伦兹曲线(Lorenz curve),也译为"劳伦兹曲线".就是,在一个总体(国家.地区)内,以"最贫穷的人口计算起一直到最富有人口"的 ...

  9. 洛伦茨曲线_什么叫洛伦兹曲线,什么叫基尼系数,我国的基尼系数偏大说明什么问题...

    展开全部 1.洛伦兹曲线 洛伦兹曲线(Lorenz curve),也译为"劳伦兹曲线".指在一个总体(国家.地区)内,以e69da5e6ba9062616964757a686964 ...

最新文章

  1. 混编ObjectiveC++
  2. CSS+jQuery/JavaScript图片切换播放
  3. JS面向对象——原型式继承函数、寄生式继承函数、寄生组合式继承
  4. mysql维表的代理键字段_mysql多维数据仓库指南--第三篇第12章(2)
  5. OpenShift 4 之Istio-Tutorial (5) 其它流量控制场景以及VirtualService和DestinationRule的关系
  6. CF10D LCIS
  7. 基于asp网上书店购物商城计算机毕业设计网站作品
  8. java长按底栏_java - 如何在导航抽屉物品中添加长按功能? - SO中文参考 - www.soinside.com...
  9. VS2013 Qt Unable to find a Qt Build 及 LINK1112错误
  10. LT8618SXB-HDMI发射器,运行功率小于100mA播放24bit 1080P内容,待机功率小于2mA
  11. 互联网晚报 | 9月18日 星期六 | 微信发布外部链接内容管理规范调整声明;知乎宣布月活破亿;京东宣布双11节奏提前...
  12. JavaScript正则表达式学习笔记(一)
  13. 写在2016的最后一周
  14. 如何用U盘进行装机?
  15. sap清账使用反记账_【转】SAP反记账功能祥解
  16. starrocker关联hive外表
  17. TPS Motion(CVPR2022)视频生成论文解读
  18. MySql-基础查询与排序
  19. abp 打包部署到ubuntu_如何通过宝塔运维面板进行部署?
  20. 裸函数naked解析

热门文章

  1. js--动态生成表格
  2. 《海上钢琴师》斗琴部分的曲子
  3. 华为设备配置链路聚合(手工负载分担模式)
  4. 什么是ORM框架?常用的orm框架有哪些?能否不用ORM框架直接使用SQL语句创建WebAPI?
  5. 3D游戏模型教程系列:3D max安装
  6. web服务器性能测试---服务器性能测试实例
  7. iceberg-flink 十:flink 窗口,事件时间,处理时间。
  8. pytorch - GAN
  9. Android 11 自动亮度调试流程
  10. Hyper-V 2016 系列教程30 机房温度远程监控方案