前言

这篇文章是多年前在 SegmentFault 上的一个回答,原问题是问如何使用 Canvas 实现一个下图类似的圆环选择器,点击后会出现对应的日期。虽然已经有 Canvas 的答案了,不过当时正好在学习 SVG 就顺手自己实现了一下。我感觉对大家去理解 SVG 的贝塞尔曲线会有一定的帮助,所以重新整理了下发出来。另外感兴趣的同学还可以去原问题上看一下,除了标准答案 Canvas 的实现以及我写的 SVG 实现之外,还有使用 DIV+CSS 的实现方案。

SVG 如何画任意角度的圆弧线

将这个问题分解出来就是我们要画一个一个的弧块,所以第一步我们需要了解"如何使用SVG画弧线"。关于 SVG 的Path参数了解大家可以去参考一下 张鑫旭老师的博文:《深度掌握SVG路径path的贝塞尔曲线指令》。不过我们需要的 arc 命令并没有给出,这里我就稍作说明一下:

  • rx:弧线所在椭圆的长轴半径
  • ry:弧线所在椭圆的短轴半径
  • x-axis-rotation:弧线与 x 轴的旋转角度
  • large-arc-flag:两个值:0为小角度弧线,1为大角度弧线
  • sweep-flag:两个值:0为逆时针,1为顺时针
  • x:弧线终点的 x 坐标
  • y:弧线终点的 y 坐标

也就是说画一段弧线你必须给定:

  1. 弧线的起始和终点坐标
  2. 弧线所在椭圆的长短轴半径
  3. 弧线与 x 轴的夹角(即弧线所在椭圆与 x 轴的夹角)
  4. 是大角度弧线还是小角度弧线
  5. 圆弧是顺时针还是逆时针的

总共 7 个参数,怪复杂的。其它参数都比较好理解,就是large-arc-flag这个参数似乎不太明白的样子,这里我引用一张 MDN 文档 中的图片给大家做一下参考:

实在是不太明白也没有关系,反正就 4 种情况,大家试试也就试出来了。针对这个问题的情况下,因为我们画的是圆弧,所以椭圆直接变成了圆,2 - 5 这 4 条参数都能解决了,剩下的是我们只需要知道弧的起点和终点就好了。这个根据圆弧的角度我们也是可以利用公式计算出来的。这里我画了一个示意图给大家参考一下:

也就是说假设已知圆心坐标和圆心半径,逆时针方向角度为正值。则圆弧α的起点 A 和终点 B 的坐标我们都能知道了。所以控制一段圆弧通用的指令应该是:

M Xo, Yo-r A r r 0 [1|0]** 0 Xo-r*sinα, Yo-r*cosα
注: ** 当弧角度 小于180° 时使用小角度弧线,当弧角度 大于180° 时使用大角度弧线

举个例子,假设圆心坐标为 (100,100),半径为 50,则我们画一个 30° 角的圆弧为:

在线预览:

JS Bin​code.h5jun.com

我们可以将其化作一个 JS 函数以便动态创建:

在线预览:

JS Bin​code.h5jun.com

如何处理弧线标注位置

SVG本身是有marker用来指定其他元素用来做标注的,不过用起来稍微麻烦最终还是需要用到text标签,所以我就直接用text标签来做了。

text标签需要指定标签左下角的 (x,y) 坐标来确定标签的位置,为了达到好看的效果,通过计算弧中点的坐标将其旋转到其切线方向会达到很好看的效果。弧重点的坐标利用上一步中求终点的方法可以非常简单拿到。而旋转到切线方向其实就是将文字旋转弧线的角度。

text也是支持 transform属性 的,和平常在 CSS 中一样也是支持 rotate 等一些常用的变换的。但这里需要注意的是,默认的rotate并不是以文字的左下角做旋转,所以我们要在旋转角度后面定义旋转中心坐标,也就是 transform = "rotate(α x y)"

这样做完之后有个未完成的地方在于由于不是按照文字中心做的运算,所以你必须左下移动文字宽高的一半才能到达中心。我将这一步的过程封装成了SVG.prototype.appendCircleArcText方法做了一个DEMO:

在线预览:

JS Bin​code.h5jun.com

如何处理渐变色

当初回答这个问题的时候,那时候我还只知道 RGB 颜色,所以当我想要生成一系列的渐变色就比较困难。当时就想到了使用 Alpha 透明通道生成由深到浅的颜色,然后再将 RGBA 转换成 RGB。其中预留了 20% 的透明通道作为基值。

function gradientColor(len, color) {color = color || [147, 112, 219];const delta = 0.8 / len;const colorTransfer = (c, o) => '#' + c.map(t => parseInt((1 - o) * 255 + o * t).toString(16)).join('');return new Array(len).fill(0).map((o, i) => colorTransfer(color, 1 - i * delta));
}

多年后我终于发现,原来颜色的表达方式不只是一种,而这种时候使用 HSL 就能完美的实现我的需求。HSL 是一种使用色相、饱和度、明度三种因素来表达颜色的色彩模式。它与 RGB 不同的地方在于 RGB 采用的是直角坐标系,而 HSL 采用的是圆柱坐标系,在颜色的归类表达上会更直观。

刚才说了,它由 H(ue)、S(aturation)、L(ightness) 三个条件控制。从下图我们可以清楚的看到,如果我们想要生成一个颜色由深到浅的不同的颜色的话,仅需要把 L 的值从小到大增加一下即可,明度值越大,颜色则越亮越浅。修改后的代码就明显要比之前的更清晰易懂许多。

function gradientColor(len, color) {color = color || [260, 59.8, 64.9];const delta = 28 / len;return new Array(len).fill(0).map((_, i) => {const c = [...color];c[2] += i * delta;c[1] += '%';c[2] += '%';return `hsl(${c.join()})`;});
}

最终效果

处理完以上三个关键的问题之后其实这道题的代码已经出来了。由于要增加点击事件,我使用g标签将同一个圆弧和其对应的text标签包起来做成一个group,而后对每一个组增加了点击事件。由于 SVG 实际上可以看成一个一个的 DOM 标签,所以点击事件处理起来也是非常的得心应手的。最后附上我的最终代码和效果:

class SVG {constructor(width, height) {this.width = width;this.height = height;this.s = SVG.createSVG(width, height);}appendCircleArc(circle = { cx: 100, cy: 100, r: 100 },angel = { start: 0, end: 90 },attrs = { fill: "none" }) {const largeArcFlag = Number(angel > 180);this.appendItem(SVG.arc({largeArcFlag: largeArcFlag,rx: circle.r,ry: circle.r,startX: circle.cx - circle.r * Math.sin(angel.start / 180 * Math.PI),startY: circle.cy - circle.r * Math.cos(angel.start / 180 * Math.PI),endX: circle.cx - circle.r * Math.sin(angel.end / 180 * Math.PI),endY: circle.cy - circle.r * Math.cos(angel.end / 180 * Math.PI)}, attrs));return this;}appendCircleArcText(text,circle = { cx: 100, cy: 100, r: 100 },angel = { start: 0, end: 90 },width = 16,attrs = {}) {angel = angel.start + (angel.end - angel.start) / 2;const posX = circle.cx - circle.r * Math.sin(angel / 180 * Math.PI);const posY = circle.cx - circle.r * Math.cos(angel / 180 * Math.PI);const circleArcText = SVG.text(text, {...attrs,x: posX,y: posY,fontSize: width,transform: "rotate( -" + angel + " " + posX + " " + posY + ")"});this.appendItem(circleArcText);return this;}render() {return this.s;}renderTo(DOM = document.body) {DOM.innerHTML = this.s.outerHTML;const texts = Array.from(DOM.querySelectorAll('text'));texts.forEach(text => {const transform = text.getAttribute('transform');transform && text.removeAttribute('transform');const { width, height } = text.getBoundingClientRect();[['x', text.getAttribute('x') / 1 - width / 2],['y', text.getAttribute('y') / 1 + height / 2],['transform', transform || '']].forEach(([name, value]) => text.setAttribute(name, value));})return this;}appendItem(item) {this.s.appendChild(item);return this;}static arc({rx = 50,ry = 50,xAxisRotation = 0,largeArcFlag = 0,sweepFlag = 0,startX = 0,startY = 0,endX = 0,endY = 0}, attrs = {}) {attrs.d = `M ${startX},${startY} A ${rx} ${ry} ${xAxisRotation} ${largeArcFlag} ${sweepFlag} ${endX},${endY}`;const path = document.createElement('path');for (var i in attrs) {path.setAttribute(i.replace(/[A-Z]/g, o => `-${o}`), attrs[i]);}return path;}static text(text = '', attrs = {}) {const t = document.createElement('text');t.innerHTML = text;for (var i in attrs) {t.setAttribute(i.replace(/[A-Z]/g, o => `-${o}`), attrs[i]);}return t;}static g = class extends SVG {constructor(attrs = {}) {super();this.s = document.createElement("g");for (var i in attrs) {this.s.setAttribute(i.replace(/[A-Z]/g, o => `-${o}`), attrs[i]);}}};static createSVG(width, height) {const s = document.createElement('svg');s.setAttribute("xmlns", "http://www.w3.org/2000/svg");s.setAttribute("width", width);s.setAttribute("height", height);s.setAttribute("viewBox", "0 0 " + width + " " + height);return s;}
}function gradientColor(len, color) {color = color || [147, 112, 219];const delta = 0.8 / len;const colorTransfer = (c, o) => '#' + c.map(t => parseInt((1 - o) * 255 + o * t).toString(16)).join('');return new Array(len).fill(0).map((o, i) => colorTransfer(color, 1 - i * delta));
}function createCircle(svg, items, circle, width, attrs) {var colors = gradientColor(items.length);colors.forEach(function (color, i) {attrs.value = items[i];var g = new SVG.g(attrs);var angel = {start: 360 / colors.length * i,end: 360 / colors.length * (i + 1)};g.appendCircleArc(circle, angel, {fill: "none",stroke: color,strokeWidth: width});g.appendCircleArcText(items[i], circle, angel);svg.appendItem(g.render())})
}const s = new SVG(600, 600)
const width = 40;
const d = {year: [2009, 2010, 2011, 2012],month: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],week: ['Mon', 'Tue', 'Wes', 'Thu', 'Fri', 'Sat', 'Sun'],day: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
}
Object.keys(d).forEach(function (k, i) {createCircle(s, d[k], { cx: 300, cy: 300, r: 300 - width * (i + 1) }, width, { category: k });
});
s.renderTo();[].forEach.call(document.querySelectorAll("g"), function (g) {g.onclick = function () {var s = document.querySelector('svg')s.setAttribute(this.getAttribute("category"), this.getAttribute("value"))var u = {}, c = ['year', 'month', 'week', 'day'];for (var i = 0, l = c.length; i < l; i++) {var o = c[i];u[o] = s.getAttribute(o) || "";if (u[o] === "") return false;}alert(Object.keys(u).map(function (k) { return u[k] }).join(' '));}
})

在线预览:

JS Bin​code.h5jun.com

svg 点击 事件_使用 SVG 实现圆环日期选择器相关推荐

  1. 使用 SVG 实现圆环日期选择器

    奇技指南 本文作者奇舞团前端开发工程师李喆明 本文转载自奇舞周刊 1 前言 这篇文章是多年前在 SegmentFault上的一个回答(https://segmentfault.com/q/101000 ...

  2. canvas 元素绑定事件_绘制SVG内容到Canvas的HTML5应用

    SVG与Canvas是HTML5上绘制图形应用的两种完全不同模式的技术,两种绘制图形方式各有优缺点,但两者并非水火不容,尤其是SVG内容可直接绘制在Canvas上的功能,使得两者可以完美的融合在一起, ...

  3. java的按钮点击事件_[转载]java处理按钮点击事件

    不同的事件源可以产生不同类别的事件.例如,按钮可以发送一个ActionEvent对象,而窗口可以发送WindowEvent对象. AWT时间处理机制的概要: 1.监听器对象是一个实现了特定监听器接口( ...

  4. react中绑定点击事件_在React中绑定事件处理程序的最佳方法

    react中绑定点击事件 by Charlee Li 通过李李 在React中绑定事件处理程序的最佳方法 (The best way to bind event handlers in React) ...

  5. java添加按钮点击事件_如何为odoo 10中的按钮点击事件添加一个java脚本处理程序?...

    我想使用java脚本为header中的按钮创建一个处理程序.下面我视图模型给出:如何为odoo 10中的按钮点击事件添加一个java脚本处理程序? inherit_id="web.asset ...

  6. 如何让页面初始化的时候实现点击事件_辅助程序实现黑盒自动化测试的常见问题...

    背景辅助程序(Accessibility)在大多数机型上具有重启设备后被激活的特性,可以完成Android测试框架(Uiautomator1.0.Uiautomator2.0)无法实现的功能.本文介绍 ...

  7. jquery 监听td点击事件_安卓开发监听点击事件的一种方法

    本人是菜鸟一只,学习安卓纯属兴趣.没有真正上过编程课程,所有知识都是在网上获取的.今天分享的是监听点击事件的一个方法,这个方法的好处是代码较简洁. 如图,点击保存时,把上面的数据入库. 实现如下: 在 ...

  8. button layui 点击事件_解决layui中的form表单与button的点击事件冲突问题

    解决layui中的form表单与button的点击事件冲突问题 layui的form表单位置和button标签的位置重合,会使得button的click事件得不到响应,如图: 蓝色底为form的位置, ...

  9. android svg点击,尝试使用 Android SVG

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 普遍的 Android 开发可以理解为移动端界面开发,那么界面自然是重中之重.当设计师给到你设计稿时,你便需要将设计稿中 ...

最新文章

  1. iOS 生成带 logo 的二维码,区域截屏保存至相册(小功能二连发 (一))
  2. java 判断当前运行的操作系统
  3. 基于分类任务的信号(EEG)处理
  4. 解读SAP Hybris为何获国内B2B用户青睐?
  5. 怎么做 空间杜宾模型_面板数据空间杜宾模型
  6. 使用说明 思迅收银系统_便利店收银使用的收银系统应该取决于什么?
  7. mysql中查询出现的错误_在MySQL查询中查询语法错误
  8. ese如何实现支付 nfc_海运费如何实现快捷支付?答案有了
  9. C++:vector中的resize()函数 VS reserve()函数
  10. Delphi TStream 详细介绍
  11. vue地址栏输入路由跳转到首页_Vue路由跳转到新页面时 默认在页面最底部 而不是最顶部 的解决...
  12. sphereface 训练出现的问题
  13. 人脸生成识别 Towards Pose Invariant Face Recognition in the Wild
  14. 每日算法系列【LeetCode 927】三等分
  15. dom4j读取配置文件
  16. CCF推荐国际学术会议与学术期刊
  17. 浅谈URI和URL的区别
  18. 论文笔记-《深度卷积神经网络的发展及其在计算机视觉领域的应用》
  19. 爱站网关键词挖掘查询工具-批量网站关键词挖掘导出软件免费下载
  20. matlab中plot画图的颜色线型

热门文章

  1. pycharm debug code -1073741819
  2. Assertion desc failed at src/libswscale/swscale_internal.h:668
  3. 介绍Python中的__future__模块
  4. PL/SQL保存用户名密码 自定义界面
  5. vlc-android配置实录
  6. 深度学习与PyTorch实战
  7. mysql indentify by_测试工作中常用到的sql命令!!!
  8. android 摇一摇监听,Android摇一摇功能实现(摇一摇监听)
  9. python小白逆袭大神课程心得_Python小白逆袭大神学习心得
  10. python 数据比对 函数_1行代码实现Python数据分析:图表美观清晰,自带对比功能丨开源...