绘制二次贝塞尔曲线(二次贝兹曲线)等距线:让 IE 支持 canvas接口 isPointInPath
一、背景:
在使用 canvas
做知识图谱的时,实体关系使用线宽为 1px 的线绘制, 用户必须点在线上, 才能正常拾取到点击的边。 边关系,有些是直线边,有些是二次贝塞尔曲线。产品提议,线不能加粗, 否则图谱展示大量数据时, 有碍美观。直线边扩展已经完成, 曲线边相对麻烦一些。 IE 浏览器不支持 canvas
判断点击事件源是否在路径上的接口 sPointInstroke
; 而对 isPointInPath
的支持,仅限于中心线封闭出来的路径, 中心线外侧的部分能通过 isPointInPath
判断是无效的,也就是说, 加宽线宽在 IE 浏览器是无法正常使用 isPointInPath
接口的。
二、方案:通过数学运算, 实现 isPointInPath
接口底层逻辑
在二次贝兹曲线的外侧绘制两条等距曲线,然后封闭路径。判断点击事件源坐标是否在该路径内,如果是,则判定该曲线被点中; 否则该曲线未被点中。以下是图示:
如图, 请看 a<0
下面的图片。
途中浅蓝色的二次曲线一共有三条, 中间的一条是图谱上已经绘制的,记为 lineA
, 上方和下方的两条曲线分别记为 lineB
和 lineC
, 可以看到 lineB
和 lineC
将 lineA
包围, 如果将 lineB
和 lineC
两端连接, 则形成了一个封闭的图形, 正好包裹住中间的曲线。
三、实现:
1. 数学几何知识提要
- 直线可以用方程来表示 :
y = ax + b
, 其中 a 为斜率, b 为常数项; - 斜率的计算方法:
a = (deltaY1 - deltaY2) / (deltaX1 / detalX2)
; - 平行的直线斜率相同, 常数项不同, 即
a
相等,b
不相等; - 垂直的直线斜率的乘积是 -1;
- 直角三角形、勾股定理、相似三角形的角和边关系以及三角函数
sin
与cos
请自行百度。
2. 注意事项
- 计算机屏幕坐标系原点
(0,0)
为屏幕的左上角, 向右为x
轴正方向, 向下为y
轴正方向。 所以在不偏移画布的前提下, 是没有负数的坐标的。 - “起点-控制点”连线形成的直线, 上图中
AH
, “终点-控制点” 连线形成的直线HK
, 若这两条直线的斜率正负号相同, 是一种情形; 这两条直线的斜率正负号不同,又是另一种情形。详见后边的代码。
3. 实现代码:
- 为了能用最小的开销实现预览代码效果, 我把代码都摘出来放到一个
html
文档中了; - 为了能更好的表达点、线的所属关系, 命名有点冗长了。倒也情有可原,原因嘛,一是现代编译工具可以帮我打包压缩,因此也无所顾忌; 二是如果不好好命名,过一段时间我自己也睁眼瞎了。
<!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>
四、效果预览:
如下图, 一共有四条曲线, 每条曲线都被两条曲线包围, 并且两端封闭。我们只需要判断点击事件源坐标是否在封闭图形内部, 即可判定封闭路径内部的曲线是否被点中, 这样就可以实现在不加宽线宽的前提下,扩展点击范围的目的了。几点说明:
- 黑色的点是扩展曲线的起点;
- 红色点点是中间曲线的控制点;
- 蓝色的点和绿色的点是两条扩展曲线的控制点。
绘制二次贝塞尔曲线(二次贝兹曲线)等距线:让 IE 支持 canvas接口 isPointInPath相关推荐
- [zz]用三阶贝塞尔曲线(贝兹曲线)拟合劣圆弧的公式(附伪代码)
转自:用三阶贝塞尔曲线(贝兹曲线)拟合劣圆弧的公式(附伪代码) 三阶贝塞尔曲线有四个控制点A.B.C.D, 若要用三阶贝塞尔曲线拟合劣圆弧,自然的要求是: 1)A位于圆弧的起点,D位于圆弧的终点: 2 ...
- bezier.CSS_SVG_canvas画_贝兹曲线
ZC:(1).SVG可以绘制 贝兹曲线:(2).canvas能绘制 贝兹曲线:(3).现在(20180202)查资料发现,css 貌似不能绘制 贝兹曲线,css使用贝兹曲线 主要是用于控制动画的速度, ...
- python 绘制lift曲线_洛伦兹曲线(Lorenz curve)提升指数、提升表和提升图
python金融风控评分卡模型和数据分析微专业课(博主亲自录制视频):http://dwz.date/b9vv 医药统计项目可联系 QQ:231469242 洛伦兹曲线(Lorenz curve)也叫 ...
- 泊松回归、gamma回归、Tweedie回归等广义线性回归模型GLM的评估指标:校准曲线、 洛伦兹曲线、卡方检验、AIC、BIC、偏差(Deviance)指标
泊松回归.gamma回归.Tweedie回归等广义线性回归模型GLM的评估指标:校准曲线(Calibration curve). 洛伦兹曲线(Lorenz Curve).卡方检验.AIC.BIC.偏差 ...
- canvas 贝萨尔曲线
二次贝塞尔曲线 定义:quadraticCurveTo() 方法通过使用表示二次贝塞尔曲线的指定控制点,向当前路径添加一个点. 说明:二次贝塞尔曲线需要两个点.第一个点是用于二次贝塞尔计算中的控制点, ...
- css沿曲线进行动画,jQuery沿贝兹曲线运动动画特效
特效描述:jQuery 沿贝兹曲线运动 动画特效.jQuery沿贝兹曲线运动动画特效 代码结构 1. 引入CSS 2. 引入JS 3. HTML代码 10条曲线 开始动画 Plot 10条贝兹曲线 开 ...
- .net cf wince 贝兹 曲线图
项目需要在WINCE设备中显示曲线图,由于wince采集器默认是不带画图的动态库的,所以在网上找了一个动态库(XrossGDIPlus)来画图,关于XrossGDIPlus具体参考http://www ...
- 洛伦茨曲线半高全宽_洛伦兹曲线
洛伦兹曲线 百科名片 洛伦兹曲线(Lorenz curve),也译为"劳伦兹曲线".就是,在一个总体(国家.地区)内,以"最贫穷的人口计算起一直到最富有人口"的 ...
- 洛伦茨曲线_什么叫洛伦兹曲线,什么叫基尼系数,我国的基尼系数偏大说明什么问题...
展开全部 1.洛伦兹曲线 洛伦兹曲线(Lorenz curve),也译为"劳伦兹曲线".指在一个总体(国家.地区)内,以e69da5e6ba9062616964757a686964 ...
最新文章
- 混编ObjectiveC++
- CSS+jQuery/JavaScript图片切换播放
- JS面向对象——原型式继承函数、寄生式继承函数、寄生组合式继承
- mysql维表的代理键字段_mysql多维数据仓库指南--第三篇第12章(2)
- OpenShift 4 之Istio-Tutorial (5) 其它流量控制场景以及VirtualService和DestinationRule的关系
- CF10D LCIS
- 基于asp网上书店购物商城计算机毕业设计网站作品
- java长按底栏_java - 如何在导航抽屉物品中添加长按功能? - SO中文参考 - www.soinside.com...
- VS2013 Qt Unable to find a Qt Build 及 LINK1112错误
- LT8618SXB-HDMI发射器,运行功率小于100mA播放24bit 1080P内容,待机功率小于2mA
- 互联网晚报 | 9月18日 星期六 | 微信发布外部链接内容管理规范调整声明;知乎宣布月活破亿;京东宣布双11节奏提前...
- JavaScript正则表达式学习笔记(一)
- 写在2016的最后一周
- 如何用U盘进行装机?
- sap清账使用反记账_【转】SAP反记账功能祥解
- starrocker关联hive外表
- TPS Motion(CVPR2022)视频生成论文解读
- MySql-基础查询与排序
- abp 打包部署到ubuntu_如何通过宝塔运维面板进行部署?
- 裸函数naked解析