用SVG实现一个优雅的提示框


NO.1
前言

Tooltips常被称为 提示框(或信息提示框)
,提示框能够以较强的交互性、自由度为用户提供相应的提示信息。今天我们要聊的不是如何实现强大的交互行为,而是来看看如何以最好的方式来还原他们的视觉效果,并且能适用于不同的场景。

NO.2
背景


上图是从平时工作场景碰到的UI效果截图过来的。上图中展示的Tooltips框基本上覆盖了常见的UI风格。简单的来归纳一下:

  • 带边框的提示框
  • 纯色(或带透明度纯色)的提示框
  • 带内阴影(或外阴影)的提示框
  • 带边框+渐变的提示框
  • 带边框+透明度背景的提示框
  • 提示框三角带圆角和阴影的提示框

可能还有我未碰到的提示框UI风格。面对这么多的UI风格,对于前端实现上来说是具有一定的挑战性,特别是多种效果组合在一起的。比如说,带有边框+内外阴影+渐变(或透明度)+圆角三角等。基本上组合了上图所提到的各种UI风格。

NO.3
clip-path方案
通常上图的实现是使用CSS画个尖角来拼接上去,比较优秀的方案如下图:


我们简单介绍下 clip-path
方案:
把提示框分成两个部分,一个是四方形,一个是三角形,然后两个拼接在一起组合成一个提示框。这样整个坐标示意图如下:


假设提示框的尺寸是 w x h
,边框厚度是 h1
,那么绘制带有缺口的时需要以下几个坐标点:

  • d1
    坐标 (0, 0)

  • d2
    坐标 ((50% - b), 0)
    ((w / 2 - b), 0)
    其中 b
    是三角形对角边长度的一半,后面会介绍到

  • d3
    坐标 ((50% - b - h1), h1)
    ((w / 2 - b - h1), h1)

  • d4
    坐标 ((50% + b + h1), h1)
    ((w / 2 + b + h1), h1)

  • d5
    坐标 ((50% + b), 0)
    ((w / 2 + b), 0)

  • d6
    坐标 (100%, 0)
    (w, 0)

  • d7
    坐标 (100%, 100%)
    (w, h)

  • d8
    坐标 (0, 100%)
    (0, h)

坐标点放置到 clip-path
polygon()
函数中,最终剪切之后的图形看上去像下图


clip-path: polygon(
    0 0,
    calc(50% - 4px) 0,
    calc(50% - 7px) 2px,
    calc(50% + 7px) 2px, 
    calc(50% + 4px) 0, 
    100% 0, 
    100% 100%, 
     0 100%,
    0 0);

另外就是三角形的部分,如果我们的三角形是一个 10px x 10px
旋转 45deg
得到。根据一些三角函数的公式和已知的正方形边长就可以计算出正方形斜对角的长度:


NO.4
clip-path方案碰到问题
这个效果整体看起来还是不错的,但是细看就会发现在接缝处或许会存在有空隙、有重叠的问题,如下图:






采用 vw
方案后这类像素对不齐的问题也算是司空见惯了,同时第一个Tooltips由于是背景需要从左到右渐变的,此时尖角的渐变过度要和下方的渐变匹配上就更需要费力气了。
由于先前就遇到过此类ToolTip样式问题,告知视觉同学后,体贴的视觉同学修改了一版不带透明度的纯色提示框,然而视觉效果大打折扣。

其实我们对于原先采用CSS clip-path
的方案其实也存在很多的缺陷,它在面对带有阴影、背景透明或者渐变、带边框同时出现时就显出了实现成本高和效果一般的缺点。

NO.5
SVG 方案

在讨论中我们想到 SVG的 path
和这个提示框的样式天然的匹配(建议先了解下 path
的相关文档),查阅了相关的文档和资料后我们大致得到了使用SVG来实现的如下几个优点:

  • 能轻松满足阴影、背景透明或者渐变、带边框的效果,甚至更为复杂多变的场景
  • SVG的 path
    实现简单,并且代码量极小

  • 可扩展性,可维护性

参考相关文章后,我们完善Demo工具如下:



使用Demo工具,我们会得到 path
的数据大致如下:

M 0,0 L -15,-15 H -79 Q -84,-15 -84,-20 V -85 Q -84,-90-79,-90 
H 61 Q 66,-90 66,-85 V -20 Q 66,-15 61,-15 H 15 z

通常使用SVG画 path
时用到命令如下表:

命令 名称 参数
M moveto(移动到) (x y)+
Z closepath(关闭路径) (none)
L lineto(画线到) (x y)+
H horizontal lineto(水平线到) x+
V vertical lineto (垂直线到) y+
C curveto(三次贝塞尔曲线到) (x1 y1 x2 y2 x y)+
S smooth curveto(光滑三次贝塞尔曲线到) (x2 y2 x y)+
Q quadratic Bézier curveto(二次贝塞尔曲线到) (x1 y1 x y)+
T smooth quadratic Bézier curveto(光滑二次贝塞尔曲线到) (x y)+
A elliptical arc (椭圆弧) (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
R Catmull-Rom curveto* (Catmull-Rom曲线) x1 y1 (x y)+

贝塞尔曲线

在SVG path
命令中我个人认为最精髓的部分是贝塞尔曲线,贝塞尔能画出各种令人愉悦的曲线。

贝塞尔曲线完全由其控制点决定其形状,n个控制点对应着n-1阶的贝塞尔曲线,并且可以 通过递归的方式来绘制。
我们先看下一次和二次贝塞尔曲线如何来绘制的:

一次曲线:


一条直线上,随着时间t的变化,红色线段的那个点的坐标公式应该如下:

二次贝塞尔曲线:


p0、p1、p2
是3个不共线的点,依次用线段连接,此时随意取线段 p0p1
上的一个点 p0'
, 如上图: 我们的 p0'
点在 p0p1
线段的0.26处(t=0.26),此刻 p1p2
线段相同比列取 p1'
点,此时 p0'
p1'
连接后形成线段 p0'p1'
, 在按照如上比列进行取值 p0''
, 这时候就确定了二次贝塞尔曲线的一个点。
通过一番巴拉巴拉牛逼的推导后,二次贝塞尔曲线公式为:
N次贝塞尔可以认为是如上取值方式的迭代过程,可以通过下图直观的感受到1~4次曲线随着时间t的变化过程,具体N次贝塞尔曲线的公式可以参考下方关于曲线的文章


SVG中的Q命令

回到我们的 ToolTips
话题, 其中的圆角是可以通过二次贝塞尔曲线来实现,SVG中 Q
命令就是来实现二次贝塞尔曲线的,SVG中 Q
命令的示例图如下:


对应的指令,其中x1,y1就是我们上面提到的 p1
点:

Q x1 y1, x y

二次贝塞尔曲线 Q
示例如下:

   

通过设置起始点和调整控制点 p1
我们能得到我们想要的圆角,如下图所示,小圆点为我们的 p1
控制点


NO.6
样式设置
实现了上方的SVG后接下来的透明、背景渐变、阴影、边框的设置就都不成问题了。
背景透明

path {
  fill: rgba(0,0,0, .3);
  storke: #ffffff;
  storke-width: 1px
}


阴影

svg {
  filter:drop-shadow(2px 4px 6px black)
}


关于为何使用 drop-shadow
来实现阴影,可以看下图使用了 box-shadow
drop-shadow
效果区别,

使用 box-shadow
的时候我们的尖角部分没有阴影,气泡框部分是有阴影的,就会出现下图所示的情况,而使用 drop-shadow
就能符合我们尖角和气泡框都有阴影的要求。


背景渐变
SVG不仅支持简单的填充,还支持线性渐变和径向渐变以及图形纹理等。为了让渐变能被重复使用,渐变内容需要定义在标签内部。
如下图是径向渐变的演示:

  
      
        
        
        
      
  
    


将这个渐变作用到我们提示框后可以看到如下图的效果,终于不用辛辛苦苦的处理尖角的渐变衔接问题了。


更多

SVG同时也支持纹理叠加效果,具体感兴趣的可以自行去研究下。

NO.7
需求还没完
上面方案落地到项目中后,可能是我们不经意感动了设计师,最近的需求视觉稿中我们发现其中涉及到的Tooltips样式已经越发令人惊艳。简单列举如下两个样式:


第一版方案我们基于Demo工具演示我们已经产出了ToolTips的SDK, 我们使用的单个参数 arrowHeight
传入来生成尖角。在应付上方两个样式是不可能的,尖角样式多变,如何来扩展性和易用性成为了一个问题,不可能多变的尖角样式都开发一个SDK。

NO.8
方案改进
要应付多变的气泡尖角一定要想办法把尖角抽离出原先的气泡外层路径,生成尖角路径后在整合到气泡上形成一个完整的闭合路径。

为了简单处理数值,我将原先的尖角 (0,0)
坐标定义更换到下方图示点:


所以接下来尖角可以自由设计了,只要保证从 (0,0)
出发最后回到
(-arrowWidth,0)
就行了,如下是一个尖角的路径:( M 0 0 C -10 0 -8 5 -12 5 S -14 0 -24 0


通过设计不同的尖角路径我们就能组合成不同的气泡样式:




上方右侧的尖角气泡最终给出的路径字符串如下,其中 Q -2 7 -9 10 Q -6 5 -7 0
这一段即为我们的尖角路径:

M 0 0 
Q -2 7 -9 10 Q -6 5 -7 0
H -110
Q -116,0 -116,-6
V -56
Q -116,-62 -110,-62
H 101
Q 107,-62 107,-56
V -6
Q 107,0 101,0
H 0 z
从上方简短的路径能看出,我们的尖角路径是完整的整合在整个SVG气泡路径中的,所以就不会担心会出现CSS的 clip-path 方案的问题。

NO.9
可视化工具
方案看起来好像已经搞定了需求中的尖角样式,然而你可能会说这尖角路径是如何产生,难道需要通过强大的数学能力推导出来?如下三次贝塞尔曲线就已经不敢直视了,更何况四次、五次…


所以想配合的我们一定要产出可视化工具来实现这路径生成过程,得益于D3.js工具库操作SVG方面的强大功能,我们开发完的 生成工具地址 (https://market.m.taobao.com/app/fdilab/svg-tool-demo/pages/index/index.html) 如下:

对于熟悉SVG的 path
命令的同学来说这个操作不难,如果不熟悉的推荐看下下方的参考文章,了解了曲线命令后就能画出圆滑的曲线。

10
总结

至此在 ToolTips
这块基本已经满足了设计的需求,同时也沉淀了SVG路径生成工具。使用SVG来实现 ToolTips
能覆盖 CSS clip-path
不能完美解决的几个场景。在此特别感谢大漠老师的指导。

11
参考文章

  • D3官网(https://d3js.org.cn/)
  • 曲线篇: 贝塞尔曲线(https://zhuanlan.zhihu.com/p/136647181)
  • Tooltips using SVG Path(https://medium.com/welldone-software/tooltips-using-svg-path-1bd69cc7becd)
  • SVG渐变(https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Gradients)
  • 深度掌握SVG路径path的贝塞尔曲线指令(https://www.zhangxinxu.com/wordpress/2014/06/deep-understand-svg-path-bezier-curves-command/)
  • css drop-shadow(https://www.zhangxinxu.com/wordpress/2016/05/css3-filter-drop-shadow-vs-box-shadow/)