奇技指南

本文作者奇舞团前端开发工程师李喆明

本文转载自奇舞周刊

1

前言

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

2

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

将这个问题分解出来就是我们要画一个一个的弧块,所以第一步我们需要了解"如何使用SVG画弧线"。关于 SVG 的Path参数了解大家可以去参考一下 张鑫旭老师的博文:《深度掌握SVG路径path的贝塞尔曲线指令》                                        (http://www.zhangxinxu.com/wordpress/2014/06/deep-understand-svg-path-bezier-curves-command/)。不过我们需要的 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° 角的圆弧为:

在线预览:

https://code.h5jun.com/cocis/edit?html,output

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

在线预览:

https://code.h5jun.com/hezac/edit?js,output

3

如何处理弧线标注位置

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

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

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

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

在线预览:

https://code.h5jun.com/zehim/edit?js,output

4

如何处理渐变色

当初回答这个问题的时候,那时候我还只知道 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()})`;    });
}

5

最终效果

处理完以上三个关键的问题之后其实这道题的代码已经出来了。由于要增加点击事件,我使用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(' '));    }
})

在线预览:https://code.h5jun.com/nubo/edit?js,output

作者推荐:刚好看到羡辙同学发了类似的视频,对 Arc 的讲解更加深入,配合视频看文章会更香 https://www.bilibili.com/video/av70653194

界世的你当不

只做你的肩膀

360官方技术公众号

技术干货|一手资讯|精彩活动

空·

使用 SVG 实现圆环日期选择器相关推荐

  1. svg 点击 事件_使用 SVG 实现圆环日期选择器

    前言 这篇文章是多年前在 SegmentFault 上的一个回答,原问题是问如何使用 Canvas 实现一个下图类似的圆环选择器,点击后会出现对应的日期.虽然已经有 Canvas 的答案了,不过当时正 ...

  2. jQuery实现日期选择器

    改组件在原生js日期选择器的代码上进行改造,新增了禁用及默认选中的功能及样式改造趋近现代化.如有侵权,联系删除. index.css .date-js {position: absolute;back ...

  3. 【iOS】自定义日期选择器

    自定义了一个日期选择器,与大家分享一下,期待宝贵建议. github下载地址:https://github.com/huahua0809/XHDatePicker 下面只是说明一下怎么用,具体实现请下 ...

  4. flutter 类似日期选择器控件_一切皆组件的Flutter,安能辨我是雄雌

    从一开始接触Flutter,相信读者都会铭记一句话,那就是--一切皆组件.今天我们就来体会一下这句话的神奇魔力,我们先从实际的产品需求说起. 我们先来看一个简化的运行图: 我们要实现如上图所示的日期选 ...

  5. java下拉列表选日期_iPhone应用程序:日期选择器查看下拉列表

    当我在文本字段中"触及"时,我用它来弹出日期选择器视图 . 也就是说,我需要文本字段来显示我在日期选择器视图中选择的内容作为其内容 . 在选择所需日期/时间后单击"完成& ...

  6. ElementUI 中日期选择器总结

    elememtUI日期选择器 <el-date-pickerv-model="value1"type="datetime"placeholder=&quo ...

  7. Android中DatePicker日期选择器的使用和获取选择的年月日

    场景 实现效果如下 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程序猿 获取编程相关电子书.教程推送与免费下载. 实现 将布局改 ...

  8. 疯狂ios讲义疯狂连载之日期选择器(UIDatePicker)

    UIDatePicker是一个可以用来选择日期和时间的控件.除此之外,它也可作为倒计时控件. 日期选择器(UIDatePicker)继承了UIControl,因此UIDatePicker可以作为活动控 ...

  9. html中下拉列表监听事件,ExtJS 下拉框监听事件、日期选择器监听事件、实现动态给items添加删除数据...

    本文将为您描述ExtJS 下拉框监听事件.日期选择器监听事件.实现动态给items添加删除数据,具体实现方法: 1.下拉框 下拉框选择时,触发事件的方法: 在 Ext.form.ComboBox 组件 ...

最新文章

  1. PaddleClas
  2. PHP特级课视频教程_第二十八集 PHP搜索代码测试_李强强
  3. python和c++哪个好找工作-Scratch和Python与C++选哪个合适
  4. LeetCode(69):x 的平方根
  5. @font-face 使用过程
  6. android获取手机唯一识别号
  7. checkSelfPermission总是返回PERMISSION_GRANTED
  8. JAVA匹配所有英文_java匹配汉字、英文、数字
  9. VS编程,几个好用的Visual Studio
  10. Unity3D-设置地形
  11. LINUX新手入门及安装配置FAQ(http://bbs.blueidea.com/viewthread.php?tid=635906amp;page=)
  12. 安装包制作工具NSIS (NullSoft Scriptable Install System)
  13. 机械制造与自动化与计算机相关吗,浅析机械设计制造及自动化与计算机技术的关系(原稿)...
  14. sql server 统计表信息
  15. 【FTP】一、什么是FTP?
  16. HTML font 标签的 size 属性
  17. 回看皮尔斯—皮尔斯的逻辑开篇
  18. Downie 4 4.6.13 MAC上最好的一款视频下载工具
  19. Win10中文语言包安装方法
  20. 在google应用商店下载的Vue.js Devtools在控制台没有vue选项

热门文章

  1. Ceph学习笔记2-在Kolla-Ansible中使用Ceph后端存储
  2. 江湖云RFID电子标签在珠宝行业的应用
  3. mysql时间格式秒微秒_mysql 时间类型精确到毫秒、微秒及其处理
  4. 【Tableau 设计提示8.0】在 Tableau 中使用形状的 10 个技巧
  5. Eclipse安装内存分析工具(Memory Analyzer)
  6. 远程桌面连接管理 工具使用说明
  7. 老板说java后台管理系统3天内必须上线,我丢了这套源码给他
  8. beanstalkd队列简述
  9. 怎么关闭breeno语音
  10. 常见的生化检测指标及其意义