\\n <path class=\\"st0\\" d=\\"M260 6c-1-.6-4.7-1.2-5.8.3-.2-.1.1-.3.2-.4-.9.2-2.2.1-3.6 0s-2.9-.2-4.2 0c-1 1.5-3.9-.6-4.8 1.4l.5-.4c.9.5-1.2 1.4-1.5 1.9-.8-1.2-.1-1-1-2l1.1.4-.3-1c-3.1 2.8-6.2-.9-8.2 1.1.1-.1.1-.3.2-.4-1.4-.5-2.3.8-3.3 1.2-.1-.5.6-.9 1.1-1.3-2.4-.3-6.4 1.2-9 .4-.9.7.4.9-.6 1.5-.8-.2-1.4-.7-.4-1.1-2.3-1.2-7.6 1-11.1-.2-1.8.8-.7 1.1-3.5 1.6.7-.5-.7-1.7 1-1.7l.2-.5c-2.8-.1-6.6-.3-8.1 1.2-.1-1.1-.5-.2-1.6-.8-.4.1 0 .2.2.2-1 .9-1.6-.1-2.3.1l.3-.2-2 .7c-.3-.2-.8-.4-.9-.7v.8c-1.1 0-.5-1-1.9-.8l.3.6c-.9-.4-2.2.4-2.4-.5 0-.2.1-.1.4-.1-1.3-1.2-3.5.3-5.1-.3l.4 1.3c-1.6.4-1-.3-.9-.7-1.1 0-1.3-.4-2.7-.6-.7.3-.4.5-.6.8l-1.5-.4 1-.7c-2.3 1.8-5.6-.4-7.2 1.2-.8-.4.8-.7.3-1-2.6-.9-6 1-8.2 0-3.6-1-7.8-.4-11.8-1.1l.1.3-2.9-.4c-.8.7-2.7.3-4 1.1.1-.3-.1-.7.2-.9-1.2.1-2.6.4-3.3-.1l.4-.3c-2.7-.3-6.4-.5-7.9.1-.9 0-.9-.6-1.1-1-1.6-.1-2.6.2-3.9.7-.3-.2-.7-.3-1-.6l-.6.8c-.6-.1-.1-.7-.6-.9-2.5.9-5.3-.1-7-.1l.2.4c-.7.3-2.1-.3-1.2-.7-3.4-.6-5.1 1.2-9.6.8-.6-1.5-4.1.3-4.8-1.4-1.9.4-3.2-.3-4.5.6 0-.2-.2-.2.1-.3-.8-.6-3.3-.2-5.3.2l-.1-.5c-.9 1.2-4.2.9-4.9 2-.2-.2.4-.5.7-.7-1-1.1-1.8.5-3.1.2.1-.3-.3-.6 0-.8-4.4-1.2-10.6.7-16.3-.1-1.6 0 .6 1.2-1.5 1.1-.6-.6 1-1.1-.3-1.4-.9.7-1.3.5-2.6.5.2-.4 0-.6.9-.9-.7-.5-3.1.9-4.5 0 .1.3-.2.5-.5.7-2.1 1-4.9-.9-5.1-.4 0 0-.7.2-.1.3-.8 0-1.9-.2-1.7-.7-.4.3-.8.8-1.4.8l.3-.6c-.4.1-.8.5-1.1.6l.6.4c-.9-.5-2.6.8-2.6-.4h.3l-1.7-.5c-.7.5-1.3 1-2.5.9-.5-1.3-2.9-.2-4.3-.3l.1-.4c-1.1.6-4 .4-3.5.6-1.1 0-2.6-.2-2-.6-.8.1-2.7.1-3.2.9l-1.8-1c-1 1.6-3.6-.5-3.6 1.2-1-.2-.8-.6-1.5-.9-1.4.9-2.8.8-4.2.7v-.2c-1.4-.1-3.1.8-5.1 1l-.5-1.2c-1 .2-1.3 1.2-2.3 1-.2-.2 0-.3.2-.3-1 .3-2.3.1-3.1-.2-1.5 1-2.7.7-3.9 1.8-1.3-1 1.7-.6.6-1.6-2.2-.4-4.4.4-6.7 1.1-.2-.2 0-.4.1-.7 0 0-1.2.9-2.2 1.8C.9 8.3 0 9.4.5 10c-.5.9-1.2 1.4.9 2 .6-.5 2.5-1.3 2.9-.4l.1-.9c2.6-.6.4 1.8 3.6 1.6l-.7-.3c.6-.1 1.1-.7 1.8-.5.2.2-.2.4-.5.6.9-.5 1.7-.9 2.6-1.4.1.5.1.8-.4 1.2 2.5-.2-.6-1.6 2.4-1.4.6.4-.2.6-.5.9 1.4.7 2.3-.1 3.8-.6.1.8-.9.7.3 1.2-.3-.4-.5-1.1.5-1.2-.4.8.7.4 1.6.5-.2-.3-.1-.6.2-.8.4-.1.8.1 1.4.1l-1.1-.7c1.5-.8 2.4.3 3.6.6-.1.1-.3.3-.5.3 1.2.3 2.5.9 4.1.1l-.3.1c2.9-.9-1-1.3 2.4-2.2 1.1.1-.4 2.6 2.1 1.6-1.3-.6 1.6-1.7 3.1-2L32.4 10c.6 0 1.6-.5 2-.3l-.1-.3c-.2-1.3 1.9.1 3-.7-1.3 1.8-1.4 1.5-1.6 3.2 1-1 2.2-1.9 4.1-1.8l-1.5 1.4c2.5.2 5.5-1.9 7.6-3-.5 1 .3 1.4-.6 2.2l2.4-.3-.7 1.1c1-1.2 2.1-.4 1.9-1.9-.3.2-.2.4-.7.3.1-.4.5-1.4 1.7-1.3.9.3-.5.6-.2 1 .8-.6.9.3 1.7-.1l-.8-.6c.6-.9 1.4-.1 2.2-.5-1 .4-.7.9-.3 1.4l-.1-.1c.8-.1 1.6-.7 2-.2l-.5 1.2.9-.9c.3.1.6.6 0 .8 2.8.7-.1-2.5 3.6-1.5 0 .5-.4.8-1.4.5-.2.7.1 1.1 1.1 1.4v.1c1.9 0 4.4 0 5.6-.8.4.3 0 .6-.4.9 2.1.4 2.8-.7 5 .1l-1-.4c1.4-.6 4-.8 5.3.1l-.4.3c1.3-.7 3.5.6 4 0-.6-.4 0-.6-.8-1l3.4-.7.2 1.2 1.8-.4c-.4-.5 2.4.4 2.5-.7 1 .4-.4.9-.8 1.4 1-.3 1.1.2 2.1-.5l1 1.1 2.6-.7c-.1.1 0 .2-.1.3 1.2-.9 3.1.6 4.6-.9-.1.1-.1.1-.1.2.9-.8 2.9-.2 3.7 0 1.4-.2.6-1 .6-1.4 3.9.4 2.7.3 6-.9 2 1.4-2.4 2.1.1 3 .4-.6 2.1-1.1 4.1-1.3 1.8.5 4.8.9 6.5 1.9l-.2-.9 2.6-.4-1.5 1.2c.4-.3 1.7-.8 2.6-1.2 2.7-.7 1.4 1.9 3.5.7.1.1.1.2.2.3.7-.6 2.4-.3 4.4-.5l-.7 1.1-1.3-.3c.7 1.1 2.1-.1 3.4 0 1.3-.3.7-1.3 1.4-1.6.5.1 1.2-.2 1.6.1 1.1.4.1 1.3-.3 1.8 1-1.1 1.4-.9 3.6-1.3.1.5-.1.8-.4.9.5-.1.9-.3 1.2-.8l.7.7c2.5 1 2.6-2 5.6-1.5-.8.6 2.6 0 3.5.7-1.1.1.4 1.6-.2 2.3 2.4.5 1-1.3 3-1.4l-.9 1.3c1.9-.5.5-.7 2.4-1.1-.5.4.8.4-.3.8 2.5.2 1.9.1 4.1.3l.2-1.3c.7-.1 1 .5 1.2.7-.3 0-.8-.1-.7.1.8 1.2 1.4-.6 2.4.5-.2-.4-.5-1 .4-1.1-.3.8 1.4.8 1.4 1.2-.6-.6 2-.2 2.1-1 1 .7-.4.6-.4 1.1.9-1 3.7 0 4.6-.6 0 .1.1.1.1.2 1.2-.6 3-.7 5.3-1.5l-.8.7c2.2.4 1.4-1.5 3.3-1.5-.4 1.1 3.1 0 2.2 1.2 1.1-.6 2.3-.8 3.1-1.7 1 .6-2.1 1.4-.6 1.8l1.6-.5.3.6c.1-.4 1.5-.4 1.4-.8.2.7.9 1.2.8 1.8 1-.2 2.4.5 3.3-.1l.1.3c1-1.3 3.1-.2 3.6-1.5l.6.7c1.5-.1 1.3-1.5 2-1.8.6 0 1.4-.2 2 0-2 .8 1 1.1 1.4 1.6.8 0 3.1 0 3.7-.7-1 .7-.4 1.2-2.1 1.3.9 1.3 2.6-.2 4.5-.1v.6c2.7-.4 2.8-1.5 4-2.5.3.8.1 1-.7 1.7 1.8.5 4.7-.1 6.7 0 .6.5.2.9-.5 1.1 2.1-.6 4.7.1 6-1.2-.5.5.9.3 1.6.6 0-.3.1-.6.2-.6 1.3-.6 4.1-1.1 5.6-.7l-.5.4c1.7.1 3-.5 4.3-.9 1.3-.4 2.6-.8 4.5-.4.7.3-.7 1.1.7 1 .7-.5.4-1.5 2.2-1.3l-.1.9 1.2-.9c-.7-.7-2.6-.4-1.3-1.2 1.6.8 1.3-.9 3.3 0-.4.1-1 .8-1.3 1.2 2 .4 3.4.1 4.8-.1 1.4-.3 2.8-.6 4.9-.2 2-.8 4.6-1.2 5.9-1.9 0 .9 0 1.7-.8 2.4 1.8 0 2.4-2.1 3.7-.9.7-1.3 4.7-1.2 5-3l2-.8z\\"/>\\n <path class=\\"st0\\" d=\\"M58.1 11.1c-1 0-1.9 0-2.3.2.2.2 2.3.6 2.3-.2zM208.2 13.3c-.1 0-.3.1-.4.1.1 0 .3 0 .4-.1zM216.3 12.9c-.1-.1-.2-.2-.4-.3 0 .3.1.5.4.3zM132.6 11.5zM178.5 13.7c.7-.4 1-.7 1-1-.4.1-.7.3-1 1zM163 12.6c-.1.1-.2.1-.3.2.3-.1.3-.2.3-.2zM130.2 12c.7-.4 1.6-.3 2.4-.5-.7.2-1.9-.3-2.4.5zM226.1 11.4l-.7.6.8-.4zM218.6 12c-.3-.1-1.7.3-1.3.6.4-.3.9-.5 1.3-.6zM189.6 11.4l-.3.6.7-.5z\\"/>\\n</svg>\\n\\n
使用SVG创建文本下划线效果,除了直接引用一个.svg
文件之外,还可以将SVG转换为Data Base64,比如下面这个示例:
\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
上面示例其实和引用一个背景图片并无差异,但在某些场景下还是有一定的局限性,比如说,在鼠标悬浮状态下给改变颜色,添加动效等。不过,我们可以将SVG直接内联到HTML中,并且配合CSS的一些样式,灵活性就会更强一些。我们一起来看一个小示例:
\\n<h1>\\n W3cplus.com!!\\n <svg fill=\\"currentColor\\" role=\\"img\\" aria-hidden=\\"true\\" focusable=\\"false\\" xmlns=\\"http://www.w3.org/2000/svg\\">\\n <use xlink:href=\\"#line\\" href=\\"#line\\"></use>\\n </svg>\\n</h1>\\n\\n<svg xmlns=\\"http://www.w3.org/2000/svg\\" id=\\"sr-only\\" viewBox=\\"0 0 260 15.6\\">\\n <symbol id=\\"line\\" xmlins=\\"http://www.w3.org/2000/svg\\">\\n <path fill=\\"currentColor\\" class=\\"st0\\" d=\\"M206.8 7.3l-.1.3c...\\" />\\n <path fill=\\"currentColor\\" class=\\"st0\\" d=\\"M260 6c-1-.6-4.7-1...\\" />\\n <path fill=\\"currentColor\\" class=\\"st0\\" d=\\"M58.1 11.1c-1 ...\\" />\\n <symbol>\\n</svg>\\n
\\nh1 {\\n display: inline-flex;\\n position: relative;\\n color: #fff;\\n transition: color .28s ease;\\n cursor: pointer;\\n svg {\\n position: absolute;\\n left: 0;\\n right: 0;\\n bottom: -4px;\\n z-index: -1;\\n pointer-events: none;\\n height: 16px;\\n width: 100%;\\n }\\n &:hover {\\n color: #f36;\\n }\\n}\\n\\n#sr-only {\\n position: absolute;\\n width: 0;\\n height: 0;\\n}\\n
\\n效果如下:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
在上例的基础上,我们还可以使用clip-path
实现动画效果:
h1 {\\n display: inline-flex;\\n position: relative;\\n color: #fff;\\n transition: color .28s ease;\\n cursor: pointer;\\n font-size: 30px;\\n svg {\\n position: absolute;\\n left: 0;\\n right: 0;\\n bottom: -4px;\\n z-index: -1;\\n pointer-events: none;\\n height: 16px;\\n width: 100%;\\n transition: clip-path 275ms ease;\\n clip-path: polygon(0 0, 0 0, 0% 100%, 0 100%);\\n }\\n &:hover {\\n color: #f36;\\n svg {\\n clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);\\n }\\n }\\n}\\n
\\n将鼠标悬浮动文本上,你可以看到一个动效:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
在SVG中,可以通过改变path
的stroke-dasharray
、stroke-dashoffset
等属性的值,实现动画效果,比如下面的示例,尝试着调整示例中的进度条滑块,可以看到SVG路径的动画效果:
\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
基于这个原理,我们就可以很好给文本下划线添加动画效果,前提是使用了SVG的path
。比如下面这个示例:
h1 {\\n display: inline-flex;\\n position: relative;\\n font-size: 64px;\\n font-weight: bold;\\n svg {\\n position: absolute;\\n left: 0;\\n right: 0;\\n bottom: -20px;\\n height: 30px;\\n z-index: -1;\\n pointer-events: none;\\n }\\n path {\\n animation: ani 6s ease alternate infinite;\\n }\\n}\\n\\n@keyframes ani {\\n 100% {\\n stroke-dashoffset:1040;\\n }\\n}\\n
\\n效果如下:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
这一节中提到的几个示例都是围绕着SVG的<path>
展开的,换句话说,如果你熟悉<path>
的话,那么可以在此基础上实现任意你想要的下划线效果。如果你还不太熟悉的话,建议你点击这里获得更多这方面的信息。
除了使用制图软件之外,还可以使用<path>
的时候可以使用很多指令来绘制不同的线条,前提是你熟悉他的指令:
\\n\\n有关于 SVG
\\n<path>
命令的详细介绍,请点击这里获取!
比如下面代码:
\\n<svg width=\\"400\\" height=\\"35\\" xmlns=\\"http://www.w3.org/2000/svg\\">\\n <path id=\\"pathItem\\"\\n d=\\"M5 5 Q 30 15 170 5\\"\\n stroke=\\"black\\"\\n fill=\\"transparent\\"\\n stroke-width=\\"7\\"\\n stroke-linecap=\\"round\\"/>\\n</svg>\\n
\\n其中d
的值是<path>
的数据,绘制了一条二次贝塞尔曲线(Quadratic Bézier curve)。其意思是”移动点1的坐标是(5, 5)
,然后在点(30,15)
处添加曲线斜率,移动点2的坐标是(170,5)
“。最终绘制出来的效果如下:
如果在制作图像中的话,上面的路径大致就是像下图那样:
\\n除了使用Q
指令之外,还可以使用C
指令:
<svg width=\\"400\\" height=\\"35\\" xmlns=\\"http://www.w3.org/2000/svg\\">\\n <path id=\\"pathItem\\"\\n d=\\"M0,0 C16,6 72,6 165,0\\"\\n stroke=\\"black\\"\\n fill=\\"transparent\\"\\n stroke-width=\\"7\\"\\n stroke-linecap=\\"round\\"/>\\n</svg>\\n
\\n效果如下:
\\n对于如何使用<path>
指令如何绘制线条,这里不做过多的探讨。你也可以使用 SVG Path Generator 生成。
有了这个基础之后,我在想:
\\n\\n\\n使用SVG的
\\n<path>
并配合一定的JavaScript脚本是否就可以创建随机的线条,从而实现给文本添加随机的下划线效果。
记得前面的示例中,改变<path>
的stroke-dashoffset
和stroke-dasharray
属性的值可以让线条动起来,并且结合clip-path
可以实现其他的动画效果,比如条线由短变长,由长变短:
其实在<path>
中,还可以直接改变坐标点来调整路径的长短,比如:
<svg width=\\"400\\" height=\\"35\\" xmlns=\\"http://www.w3.org/2000/svg\\">\\n <path id=\\"pathItem\\"\\n d=\\"M5 5 Q 30 15 170 5\\"\\n stroke=\\"black\\"\\n fill=\\"transparent\\"\\n stroke-width=\\"7\\"\\n stroke-linecap=\\"round\\"/>\\n</svg>\\n\\n<svg width=\\"400\\" height=\\"35\\" xmlns=\\"http://www.w3.org/2000/svg\\">\\n <path id=\\"pathItem\\"\\n d=\\"M5 5 Q 30 15 70 5\\"\\n stroke=\\"#f35\\"\\n fill=\\"transparent\\"\\n stroke-width=\\"7\\"\\n stroke-linecap=\\"round\\"/>\\n</svg>\\n
\\n你将看到效果如下:
\\n两条路径的d
数据非常的相似:
d=\\"M5 5 Q 30 15 170 5\\"\\nd=\\"M5 5 Q 30 15 70 5\\"\\n
\\n可以看出来,坐标2的x
轴值发生了变化,由170
变成了70
。因此线条也就变短了。基于这个原理,可以使用JavaScript来控制这个x
值,从而得给路径不同的d
数据:
const pathItem = document.getElementById(\\"pathItem\\");\\n\\ninput.addEventListener(\\"change\\", handleUpdate);\\ninput.addEventListener(\\"mousemove\\", handleUpdate);\\n\\nfunction handleUpdate(e) {\\n let pathEnd = this.value;\\n let newPath = M5 5 Q 30 15 ${pathEnd} 5;\\n pathItem.setAttribute(\\"d\\", newPath);\\n}\\n
\\n尝试着拖动下面进度条滑块,你可以看到SVG的路径长短将会发生变化:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
使用JavaScript还可以动态创建贝塞尔曲线,比如:
\\n\\n\\nDemo 地址:codepen.io/thebabydino…
\\n
要彻底搞懂如何使用JavaScript绘制贝塞尔曲线是比较复杂的。我们还是回到我们自己的主题中来。在我们的示例中,不需要具备很深的知识,为什么呢?
\\n刚才提到过,只需要几行JavaScript脚本就可以改变SVG的<path>
的d
值,即改变路径。其实,我们还可以使用JavaScript让路径变成一个随机的路径,即<path>
的d
的值是随机的,示例所需的脚本如下:
const moveYMin = 5;\\nconst moveYMax = 12;\\n\\nconst curveXMin = 20;\\nconst curveXMax = 100;\\n\\nconst curveYMin = 5;\\nconst curveYMax = 20;\\n\\nconst endXMin = 50\\nconst endXMax = 160\\n\\nconst endYMin = 5;\\nconst endYMax = 10;\\n\\nconst button = document.getElementById(\\"button\\");\\nconst randomPath = document.getElementById(\\"pathItem\\");\\n\\nbutton.addEventListener(\\"click\\", e => {\\n let moveY = Math.floor(Math.random() * (moveYMax - moveYMin)) + moveYMin;\\n let curveX = Math.floor(Math.random() * (curveXMax - curveXMin)) + curveXMin;\\n let curveY = Math.floor(Math.random() * (curveYMax - curveYMin)) + curveYMin;\\n let endX = Math.floor(Math.random() * (endXMax - endXMax)) + endXMax;\\n let endY = Math.floor(Math.random() * (endYMax - endYMin)) + endYMin;\\n let newPath = M5 ${moveY} Q ${curveX} ${curveY} ${endX} ${endY};\\n\\n randomPath.setAttribute(\\"d\\", newPath);\\n\\n});\\n
\\n效果如下:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
在上面的基础上再进行一下改造:
\\nconst createSVG = targetWidth => {\\n const svg = document.createElementNS(\\"http://www.w3.org/2000/svg\\", \\"svg\\");\\n svg.setAttribute(\\"width\\", targetWidth);\\n svg.setAttribute(\\"height\\", \\"20\\");\\n\\n const path = document.createElementNS(\\"http://www.w3.org/2000/svg\\", \\"path\\");\\n let pathD = randomizePath(targetWidth);\\n\\n path.setAttribute(\\"d\\", pathD);\\n path.setAttribute(\\"fill\\", \\"transparent\\");\\n path.setAttribute(\\"stroke\\", \\"lightgrey\\");\\n path.setAttribute(\\"stroke-width\\", \\"7\\");\\n path.setAttribute(\\"stroke-linecap\\", \\"round\\");\\n\\n svg.appendChild(path);\\n\\n return svg;\\n};\\n\\nconst randomizePath = targetWidth => {\\n const moveYMin = 5;\\n const moveYMax = 12;\\n const curveXMin = 20;\\n const curveXMax = targetWidth; /* Width of the targetElement */\\n const curveYMin = 5;\\n const curveYMax = 20;\\n const endYMin = 5;\\n const endYMax = 10;\\n\\n let moveY = Math.floor(Math.random() * (moveYMax - moveYMin)) + moveYMin;\\n let curveX = Math.floor(Math.random() * (curveXMax - curveXMin)) + curveXMin;\\n let curveY = Math.floor(Math.random() * (curveYMax - curveYMin)) + curveYMin;\\n let endY = Math.floor(Math.random() * (endYMax - endYMin)) + endYMin;\\n\\n let newPath = M5 ${moveY} Q ${curveX} ${curveY} ${targetWidth - 7} ${endY};\\n\\n return newPath;\\n};\\n\\nconst insertAfter = (el, referenceNode) => {\\n referenceNode.parentNode.insertBefore(el, referenceNode.nextSibling);\\n};\\n\\nconst targetEle = document.querySelector(\\"h1\\");\\nconst targetEleWidth = parseInt(targetEle.offsetWidth);\\nconst svg = createSVG(targetEleWidth);\\ninsertAfter(svg, targetEle);\\n
\\n效果如下:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
每次刷新,SVG的<path>
效果都会有所差异。如果你感兴趣的话,还可以在此基础上做更多的改造和优化。
这里假设你对CSS Masking有一定的接触或了解。即使没有也不用太紧张,因为接下来的内容不会过于复杂。
\\n首先,使用一个矢量制图软件,比如Sketch软件,快速画一条线(其实就是<path>
)。怎么画,前面有提到过,这里有一点需要注意的是,为了让线条能重复,需要确保线条在水平方向可以无缝衔接。要做到这一点,只需要确保第一个点和最后一个点的 y
坐标值相同。
导出SVG,并且对SVG进行优化,拿到最简洁的SVG代码:
\\n<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 250 75\\">\\n <path id=\\"squiggle\\" d=\\"M12 19.778C12 1.318 36.745-6.008 54.219 5.556c8.913 5.898 16.498 21.96 17.028 36.162.047 1.243.041 2.458-.016 3.642l-.024.41.485.132c6.082 1.634 11.768 2.9 16.315 3.61l.586.09c5.018.746 10.426 1.091 15.846 1.082l1.241-.009-.175-.216c-5.34-6.67-8.374-14.234-8.5-21.288L97 28.712c0-17.914 11.128-24.145 33.419-21.816 20.311 2.122 30.088 20.755 22.159 36.496-3.407 6.763-12.924 11.075-26.427 13.395l-.386.064.5.26c7.795 3.986 17.234 6.242 27.999 6.34l.736.003c7.47 0 12.604-2.367 16.657-7.101l.363-.431c2.922-3.54 4.959-7.632 8.263-15.958l1.487-3.786c3.53-8.983 5.894-13.9 9.46-18.657C196.01 11.14 202.088 6.918 210.058 5c17.363-4.179 28.412 6.79 32.24 25.786.224 1.117.421 2.252.59 3.397l.048.337.214.01c1.84.073 3.669.117 5.488.131l1.362.006v7.94c-1.817 0-3.644-.027-5.48-.084l-.888-.03.004.119c.366 11.224-1.754 21.867-5.821 25.652l-.181.163c-9.839 8.545-20.88 8.82-35.619.459-18.22-10.335-5.375-40.825 15.68-37.137 5.313.931 10.547 1.639 15.704 2.124l1.36.123-.145-.82-.161-.832c-3.048-15.125-10.496-22.518-22.51-19.627-6.078 1.463-10.587 4.596-14.295 9.543l-.405.55c-2.735 3.786-4.788 8.074-7.798 15.69l-1.083 2.762c-4.15 10.598-6.524 15.462-10.609 20.233-5.57 6.505-12.93 9.9-22.753 9.9-16.603 0-30.714-4.653-41.213-12.816l-.399-.314-.894.057c-8.445.501-17.051.247-24.233-.746l-.854-.122c-4.71-.702-10.583-1.978-16.88-3.642l-.695-.185-.068.22C66.356 64.55 57.219 71.217 42.596 71.393l-.596.003c-12.942 0-18.604-5.695-20.82-16.46l-.248-1.299c-.069-.381-.13-.744-.228-1.35l-.352-2.19c-.842-5.087-1.48-6.296-3.01-6.548l-.655-.103c-3.604-.529-8.965-.813-16.023-.835l-.667-.001.006-7.941 1.433.004c7.527.044 13.246.385 17.222 1.043 3.58.592 6.114 2.65 7.634 5.814l.205.44c.786 1.761 1.206 3.533 1.732 6.73l.545 3.353.126.69.129.657C30.526 60.722 33.352 63.454 42 63.454c11.048 0 17.56-4.226 20.075-11.764l.08-.248-.02-.005c-6.241-1.881-12.573-4.043-18.347-6.323l-.784-.312C23.514 37.002 12 28.946 12 19.778zm204.305 19.793c-12.522-2.194-20.467 16.666-10.32 22.421l.867.484c11.452 6.288 18.614 5.969 25.514-.025.33-.286.91-1.346 1.45-2.991.64-1.954 1.152-4.486 1.474-7.35.337-2.995.453-6.193.342-9.41l-.027-.667-.805-.063a204.58 204.58 0 01-17.052-2.152l-1.443-.247zM129.58 14.793l-1.075-.107C111.166 13.043 105 16.735 105 28.712c0 6.8 3.921 14.786 10.896 21.225l.204.185.254-.023c14.007-1.36 26.081-5.004 28.933-10.007l.135-.251c5.47-10.858-1.172-23.515-15.84-25.048zm-79.8-2.63C36.983 3.693 20 8.721 20 19.778c0 4.32 9.802 11.178 25.996 17.66 5.25 2.101 11.031 4.11 16.777 5.886l.505.155v-.267c0-.263-.005-.529-.012-.797l-.013-.404c-.442-11.828-6.897-25.497-13.472-29.848z\\"/>\\n</svg>\\n
\\n线条的效果如下:
\\n虽然我们可以通过fill
来修改SVG的 <path>
的颜色:
<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 250 75\\">\\n <path id=\\"squiggle\\" fill=\\"#f36\\" d=\\"M12 19.778C12 ...\\"/>\\n</svg>\\n
\\n除此之外,还可以通过CSS来设置fill
值改变路径颜色:
path {\\n fill: #f36;\\n}\\n
\\n还可以使用CSS Masking让它更为灵活。通过mask
来让事情变得更为灵活:
h1 {\\n display: inline-flex;\\n position: relative;\\n padding-bottom: 8vh;\\n font-size: 20vh;\\n cursor: pointer;\\n transition: all .2s ease;\\n &:hover {\\n color: #f36;\\n }\\n\\n &::after {\\n content: \\"\\";\\n position: absolute;\\n left: 0;\\n right: 0;\\n bottom: 4vh;\\n height: 6vh;\\n background-color: currentColor;\\n mask-image: url(\\"/repeater.svg#squiggle\\");\\n }\\n}\\n
\\n效果如下:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
在上面的基础上,我们只需要调整h1
的color
就可以得到不一样的效果(伪元素的background-color
设置了currentColor
):
\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
如果将背景色调整渐变色效果就是另一种风彩:
\\n\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
甚至我们可以调整mask
的其他属性值,比如mask-size
、mask-position
得到其他效果,还可以结合@keyframes
实现一些动效:
\\n\\nDemo 地址:codepen.io/airen/full/…
\\n
上面我们看到的效果是实现下划线效果。该技术方案还可以用于一些不规则的Banner设计上,如下图所示:
\\n是不是类似水波纹,实现水波动效也变得更容易。感兴趣的同学,不妨自己写个效果看看,验证一下也是不错的嘛。
\\n虽然CSS的text-decoration
特性新增了不少属性,可以帮助我们给文本添加更丰富的下划线效果,但要实现一些具有创意的效果还是有很大的局限性。不过随着Web新技术的到来以及浏览器的变革,我们也具备更多的技术方案,比如文章中提到的,使用clip-path
、mask
和SVG等实现不同的下划线效果。
虽然文章中聊的都是给文本添加下划线效果,但其技术也可以运用于其他的场景。感兴趣的同学不妨一试,如果你在这方面有更好的建议和经验,欢迎在下面的评论中与我们一起分享。
\\n如果你觉得该教程对你有所帮助,请给我点个赞。要是你喜欢 CSS ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程:
\\n看到该标题,我想你可能会感到非常的意外和好奇,“CSS怎么能加把锁呢”?其实这里所说的给CSS加把锁是指业内所说的CSS Locks(或者CSS calc Lock)。简单地说,使用CSS锁技术方案来锁定流式排版。让我们实现一个精准的流式排版不再是难题。在今天这篇文章,我们来一起探讨一下CSS锁的故事。
\\n这里所指的CSS锁并非说给CSS加把锁,让程序无法写入CSS代码,所指的是CSS Locks。那么具体什么是CSS锁呢?如果要搞清这个概念,我们要先回顾一点Web相关的知识。
\\n早在2010年@Ethan Marcotte就提出了**Web响应式设计** (Responsive Web Design) 的概念。“响应式设计”简单地说是可以让你的网站从宽屏显示器到手持移动终端(比如手机)都能良好的显式。
\\n这是一种Web设计和开发的方法,它可以让你基于同一套源码为不同的设备终端提供最佳的展示。
\\n熟悉响应式设计的同学都知道:
\\n\\n\\n“响应式设计是通过 CSS的媒体查询 来实现的”
\\n
开发者借助CSS的媒体查询特性有条件地使用CSS规则,比如媒体查询根据断点(屏幕分辨率节点):
\\n它们会告诉浏览器应该根据用户的设备忽略或应用某些规则:
\\n最终用户在不同的终端会看到不一样的效果:
\\n事实上,现在社区中实现响应式设计采用的大都是该方案,很多优秀的CSS框架都会有一些预定义的断点。不过基于断点来做媒体查询条件判断,特别是在一些CSS框架中预定一些断点,难免有点武断。这好比有点像猜测和妥协一样。我们越想让它工作得更好,需要设计(处理)的东西就越多。这也造就响应式设计基于媒体查询来实现总是令人感到不太优雅,效率也不高。
\\n另外响应式设计在断点之间的切换时,流畅度是有一定的折扣(会闪那么一下),而且在响应式排版中也有着令开发者感到头痛的事情,尤其是文本的排版以及元素间距间的控制。也正因如此,才会有CSS锁的出现。
\\n简单地说,CSS锁是一种响应式的Web设计技术,他主要致力于解决响应式设计的文本排版,它允许你根据当前的视窗大小大两个值之间平稳地转换,而不是直接从一个值跳到另一个值。
\\nCSS锁是@Mike Riethmuller在2015年的《Precise control over responsive typography》中首次展示的,但早在2012年的时候@Tim Brown在《Flexible typography with CSS locks》中就提出了该概念,并称之为CSS Locks。
\\n@Tim Brown用了实际生活中的一个示例来描述CSS锁。在运河和河流中都会备有船闸用来控制河中水位,也可以在不同水位的水域之间升降船只:
\\n用到CSS锁上来的话,就是CSS锁允许你设置最小和最大的字体大小。最小字体大小将应用于最小视窗宽度以下,最大字体大小将应用于最大视窗宽度之上,在最小宽度和最大宽度之间,字体大小将按比例从最小字体到最大字体间按一定的比例缩放:
\\n如果用CSS来描述的话会像下面这样:
\\n/**\\nminf: 最小font-size\\nmaxf: 最大font-size\\nminw: 视窗最小宽度\\nmaxw: 视窗最大宽度\\n*/\\n\\nfont-size: calc([minf]px + ([maxf] - [minf]) * ( (100vw - [minw]px) / ([maxw] - [minw]) ));\\n\\n@media only screen and (max-width: [minw]px) { \\n font-size: [minf]px; \\n};\\n@media only screen and (min-width: [maxw]px) { \\n font-size: [maxf]px; \\n};\\n
\\n现在我们大致知道CSS锁是什么?那么接下来,我们再花点时间来了解一下,其设计原理。
\\n接下来通过一些数学计算来向大家阐述CSS锁的设计过程。
\\n先由视窗单位来说起。可能很多同学会和我一样,在做响应式布局时对于文本的排版,比如font-size
会采用视窗单位:
h1 {\\n font-size: 4vw;\\n}\\n
\\n期望的是标题在小屏幕有小的字体,大屏幕有大的字体。如果直接使用视窗单位可能会存在两个缺陷:
\\n文本在小屏幕上非常小,比如在320px
屏幕下font-size
为12.8px
;在大屏幕时非常大,比如1500px
屏幕下font-size
为60px
它没办法根据用户的喜好来设置文本字号大小
\\n而CSS锁技术旨在解决第一个问题,当然该技术也在尝试着解决第二个问题:遵循用户的喜好来设置字号大小。
\\n前面的内容告诉我们,“CSS锁是一种特定类型的CSS值计算”。比如font-size
和line-height
。比如计算font-size
的代码:
font-size: calc([minf]px + ([maxf] - [minf]) * ( (100vw - [minw]px) / ([maxw] - [minw]) ));\\n
\\n上面的calc()
计算涵盖了:
minf
:最小字号
maxf
:最大字号
minw
:视窗最小宽度(断点1
)
maxw
:视窗最大宽度(断点2
)
在这两个断点之间的实际值是从最小值到最大值之间变化的,该变化是一个线性的变化
\\n比如,我们在低于320px
(minw
)使用的font-size
是20px
(minf
),而高于960px
(maxw
)使用的font-size
是40px
(maxf
);在断点320px ~ 960px
之间采用的font-size
是20px ~ 40px
(minf~maxf
),用水位图来描述的话像下面这样:
如果用图表来描述的话,大致像下面这样:
\\n用CSS可以像下面这样描述:
\\nh1 {\\n font-size: 20px;\\n}\\n\\n@media (min-width: 320px) {\\n h1 {\\n font-size: /* 从20px到40px之间 */;\\n }\\n}\\n\\n@media (min-width: 960px) {\\n h1 { font-size: 40px; }\\n}\\n
\\n我们要接受的第一个挑战是真正的实现这个魔幻的值(自动变化的值)。在这里首先引入一个新的值,该值被称为视窗相对值,并且通过calc()
来做值的计算:
h1 {\\n font-size: calc(20px + \'视窗相对值\')\\n}\\n
\\n\\n\\n视窗相对值可以是一个单一的值,比如
\\n4vw
,也可以是一个更复杂的计算值(也是基于vw
或另一个视窗单位计算得来的值) 。有关于视窗单位的更多介绍可以阅读《CSS 的值和单位》或《现代 CSS 中的相对单位》一文。
由于计算是基于视窗单位的,所以CSS锁有一定的限制。它们只对数字值有效,可以使用calc()
,并且可以接像像素值。为什么是像素值呢?那是因为,浏览器客户端最终计算出来的值都是px
值。也就是说,视窗单位vw
、vh
、vmin
和vmax
总是会解析成px
。例如,如果视窗宽度是750px
,那么1vw
就相当于7.5px
。
从理论上来说,CSS锁对于所有应用数值的属性都应该是有效的,但事实上,它的有用性还是有限的,其中我们不能有效的查询元素,并且将每个元素的动态绑定到媒体查询上是件复杂的事情。为了让事情变得简单化,接下来我们主要聊的相关技术和特性可以运用于font-size
或line-height
(或许有一天可以运用于其他的属性,比如width
、height
等)以及它们是如何基于px
或基于em
的断点来构建CSS锁。
先来看基于像素为单位的断点的CSS锁计算。
\\n对于CSS锁而言,我们期望的font-size
(或line-height
)值在两个断点之间是按比例增长的。而且这种增长是一种线性增长。用图表来表示的话,像下面这样:
上图中红色线其实就是一个简单的线性函数。如果用公式来表达的话,可以是y = mx + b
,其中:
y
表示是font-size
(纵轴)
x
表示的是视窗宽度,单位为像素(横轴)
m
是函数的斜率(“视窗宽度每增加1px
,对应的font-size
应该增加多少像素?”)
b
是在添加任何基于视窗值之前的font-size
大小
其中m
和b
在y = mx + b
中是一个不可变的部分。接下来我们要做的是算出m
和b
各自的值。先来看m
的值如何计算。
在计算m
我们需要两个点的坐标值,从图表上可以看到x
和y
在红线上都会有一个交点,其中每个点的坐标就是(x, y)
。比如视窗宽度为320px
时,x
的值为320
,对应y
的值为20
(即font-size
是20px
);同样的,视窗宽度960px
和font-size
为40px
也有一个对应的坐标,即(960, 40)
。加上m
其实表示的就是图表中红线的斜率,即可“字体大小增长值与视窗宽度增长值与的比例”。这样一来,我们就可以得到:
m = 字体大小增长值 / 视窗宽度增长值\\nm = (y2 - y1) / (x2 - x1)\\nm = (40px - 20px) / (960px - 320px) = 20 / 640 = 0.03125\\n
\\n还可以像下面这样简单的来表述:
\\n\\n\\n\\n
font-size
增加了20px
,即在y
轴上的增长值是20
(40-20
);视窗宽度增加了640px
,即在x
轴上增长值是640
(960 - 320
)。如果视窗宽度只增加1px
,那么对应的font-size
增加就是20 / 640 = 0.03125px
。也就是m
的值为0.03125px
计算出了m
的值,那么再要计算出b
的值就不是难事了。我们可以根据y = mx + b
公式即可转换出b = y - mx
,即**b = y - 0.03125x
**。
这样我们就得到了m
和b
的值,其实我们也可以使用断点的坐标值来验证,比如断点1
的坐标(x1, y1
),即(320, 20)
,套用到公式中:
b = y1 - 0.03125 × x1 = 20 - 0.03125 × 320 = 10\\n
\\n或者用断点2
坐标(x2, y2
)也可以验证:
b = y2 - 0.03125 x x2 = 40 - 0.03125 x 960 = 10\\n
\\n这样一来,将m
和b
套到公式y = mx + b
中的话,线性函数就变成了 y = 0.03125x + 10
。公式中的y
对应的是font-size
,那么在CSS中的话,我们就可以通过calc()
像下面这样表达出来:
font-size: calc( 0.03125 * x + 10px)\\n
\\n如果就像上面那样使用的话,将是一行无效的CSS代码,因为x
不是有效的CSS语法。但在公式中x
对应的视窗的宽度,在CSS中可以用100vw
来表示,线性函数在CSS中可以像下面这样有效的表达出来:
font-size: calc( 0.03125 × 100vw + 10px)\\n
\\n根据calc()
的语法规则,上面的公式可以变得更短一些:
font-size: calc(3.125vw + 10px)\\n
\\n而这个值对于我们来说是期望在两个断点之间的font-size
的值,那么使用CSS媒体查询就可以很好的表述出来:
h1 { font-size: 20px; }\\n\\n@media (min-width: 320px) {\\n h1 {\\n font-size: calc( 3.125vw + 10px );\\n }\\n}\\n\\n@media (min-width: 960px) {\\n h1 {\\n font-size: 40px;\\n }\\n}\\n
\\n如果用图表来描述的话就会像下图这样:
\\n不过很多同学可能不太喜欢给font-size
使用固定单位(比如px
),而是更喜欢采用一些相对单位(比如rem
、em
和%
等)。接下来,我们将上面公式中的px
单位换成rem
单位。
要在公式中使用rem
这样的单位值,首先要确保的是根字体大小没有被固定值覆盖,即root
(也就是html
)的font-size
不是固定值。我们可以像下面这样设置html
的font-size
值:
html {\\n font-size: 62.5%;\\n}\\n
\\n在用户未显式设置客户端字体大小的话,那么浏览器默认的font-size
是16px
,这样一来:
html {\\n font-size: 62.5%; /* 该值相当于.625 × 16px = 10px */\\n}\\n
\\n如果换成rem
的话,62.5%
相当于 1rem
,即62.5% ▶ 1rem = 10px,.1rem = 1px
。如果将html
的font-size
设置为125%
的话(即16 × 1.25 = 20px
),即125% ▶ 1rem = 20px,.05rem = 1px
。
也就是说,将html
的font-size
大小保持不变,即始终为 16px
。这样的话:
10px ▶ .625rem
,
20px ▶ 1.25rem
,
40px ▶ 2.5rem
将上面的值换到CSS锁中:
\\nh1 { \\n font-size: 1.25rem; \\n}\\n\\n@media (min-width: 320px) {\\n h1 {\\n font-size: calc( 3.125vw + .625rem );\\n }\\n}\\n\\n@media (min-width: 960px) {\\n h1 {\\n font-size: 2.5rem;\\n }\\n}\\n
\\n如果我们使用浏览器默认的字体大小,即16px
,上面这段代码和前面基于px
代码起到的效果是相同的。而我们的目标是能让用户根据自己的喜欢去设置,也就是说,如果用户将浏览器的默认的font-size
改成自己所需要的字号,比如说24px
(在16px
的基础上增加了50%
),这个时候上面的CSS锁运行之后,效果可能会如下图这样所示:
上图中蓝色点色表示浏览器默认字号是 16px
,未连接在一起红色表示浏览器默认字号是 24px
。
从图中可以看出来,在断点1
(320px
)处,font-size
大小实际上变小了(从30px
跳到了25px
),而在断点2
(960px
)处,font-size
大小反而变大了(从45px
跳到了60px
)。整个红线是断层的,不像蓝色点线那样能连接在一起。这样和CSS锁的初衷是不同的。
要解决这个问题,我们可以对所有三种大小使用相同的用户可配置基线。例如,我们选择1.25rem
作为用户配置的基线值,这样一来,上面的CSS锁就变成:
h1 { font-size: 1.25rem; }\\n\\n@media (min-width: 320px) {\\n h1 {\\n font-size: calc( 1.25rem + 3.125vw - 10px );\\n }\\n}\\n\\n@media (min-width: 960px) {\\n h1 { font-size: calc( 1.25rem + 20px ); }\\n}\\n
\\n看到代码中的3.125vw - 10px
?其实就是前面所说的线性函数(mx + b
)相似,只不过这里的b
的值有所不同,我们暂且将它称为**b′
**,因为我们的基线值等于20px
,我们可以像下面这样来计算b′
的值:
b′ = b - 用户可配置基线值\\nb′ = 10px - 20px\\nb′ = -10px\\n
\\n另一种策略就是一开始选择好基线值,然后再寻找描述font-size
增加的线性函数,这里为了和前面的线性函数区分出来,暂且命名为y\'
(也可以用来区别完整的字体大小y
)。
/* x轴,表示视窗宽度,x1和x2分别是断点1和断点2视窗的宽度 */\\nx1 = 320\\nx2 = 960\\n\\n/* y轴,表示font-size大小 */ \\ny\'1 = 0\\ny\'2 = 20\\n\\n/* 斜率 */ \\nm = (y\'2 - y\'1) / (x2 - x1)\\nm = (20 - 0) / (960 - 320) = 20 / 640 = 0.03125\\n\\nb\' = y\' - mx\\nb\' = y\'1 - 0.03125 × x1 = 0 - 0.03125 × 320 = -10 \\n
\\n这样一来,我们得到了y\' = 0.03125x - 10
(前面的线性函数是y = 0.03125x + 10
),如果用图表来描述的话:
这样我们基本值是rem
或者是vw
还是px
都可以得到完全工作CSS锁(在font-size
上的CSS锁)。当用户改变他们的基本字体大小时,整个东西就会上升或下降,而不再会中断。如下图所示:
上面我们看到的是CSS锁在 font-size
上的运用,接下来,我们来看看CSS锁在line-height
上的运用。
和font-size
类似,假设我们在断点1
(视窗宽度为320px
)处设置的line-height
是1.4
,而在断点2
(视窗宽度为960px
)处设置的line-height
是1.8
。
CSS中的line-height
在排版中的运用相对来说是非常复杂的,比如说,不同的单位计算,结果也有相应的差异,如下图所示:
从上图中可以看出,设置line-height
的最佳实践是 不带任何单位设置行高的值。
接下来,我们回到我们要聊的CSS锁中。通过前面的学习,我们知道,在CSS锁中会使用一个基础值加上一个以像素表示的动态值,那么在line-height
设置中,我们首先要知道是 1.4
和 1.8
的比率涉及多少像素。也意味着我们需要知道段落(文本)的font-size
。假设我们的段落文本使用默认的font-size
,即16px
,那么line-height
的值相应为:
/* 断点1,视窗宽度为320px处的line-height */ \\n16 × 1.4 = 22.4px\\n\\n/* 断点2,视窗宽度为960px处的line-height */\\n16 × 1.8 = 28.8px\\n
\\n假设我们使用1.4em = 22.4px
作为基线,那么6.4px
(28.8px - 22.4px
)就是增量。这样一来,对应的线性函数:
x1 = 320\\nx2 = 960\\n\\ny′1 = 0\\ny′2 = 6.4\\n\\nm = (y′2 - y′1) / (x2 - x1)\\nm = (6.4 - 0) / (960 - 320)\\nm = 6.4 / 640\\nm = 0.01\\n\\nb′ = y′ - mx\\nb′ = y′1 - 0.01 × x1\\nb′ = 0 - 0.01 × 320\\nb′ = 3.2\\n\\ny′ = 0.01x - 3.2\\n
\\n用CSS来描述的话是:
\\nline-height: calc( 1.4em + 1vw - 3.2px)\\n
\\n\\n\\n特别声明,这里基值使用
\\n1.4em
主要是为了处理各浏览器下calc()
的兼容性,因为使用不带单位的1.4
,在所有浏览器中的calc()
都不支持,如果使用140%
,虽然在Chrome和Firefox中得到支持,但在Safari中不起作用。
那么相应的line-height
对应的CSS锁就如下所示:
p { line-height: 1.4em; }\\n\\n@media (min-width: 320px) {\\n p {\\n line-height: calc( 1.4em + 1vw - 3.2px );\\n }\\n}\\n\\n@media (min-width: 960px) {\\n p {\\n line-height: calc( 1.4em + 6.4px );\\n }\\n}\\n
\\n另外,对于大的值,我们不能只使用1.8em
,因为我们需要添加到基础上的部分是用px
表示的。也就是说,要是我们直接使用1.8em
的话,对于16px
的基本字号来说,结果还是不错的,但用户更改了字号的基本值时,结果就有可有不一样了。
我们可以用函数图像来检查它是否适用于不同的基本字号大小:
\\n也就是说,由于line-height
计算公式取决于元素本身font-size
的大小,如果我们更改font-size
大小,就必须更改相应的公式。例如下面这个示例font-size
是1.66em
,不再是16px
,那么对应的line-height
计算就是:
/* 元素的line-height */ \\n.big {\\n font-size: 1.66em\\n}\\n\\n/* 断点1,视窗宽度为320px处的line-height */ \\n16 × 1.66 × 1.4 = 37.184px\\n\\n/* 断点2,视窗宽度为960px处的line-height */ \\n16 × 1.66 × 1.8 = 47.808px\\n
\\n根据前面的运算过程,我们可以得到最终的线性函数公式为 y\' = 0.0166x - 5.312
。这样就可以得到最终的CSS锁:
p {\\n line-height: 1.4em; \\n}\\n.big {\\n font-size: 1.66em; \\n}\\n\\n@media (min-width: 320px) {\\n p {\\n line-height: calc( 1.4em + 1vw - 3.2px );\\n }\\n .big {\\n line-height: calc( 1.4em + 1.66vw - 5.312px );\\n }\\n}\\n\\n@media (min-width: 960px) {\\n p {\\n line-height: calc( 1.4em + 6.4px );\\n }\\n .big {\\n line-height: calc( 1.4em + 10.624px );\\n }\\n}\\n
\\n另一个选项是让CSS进行计算。因为在标准段落使用相同断点和相对line-height
,所以我们只需要添加一个1.66
因子:
p {\\n line-height: 1.4em; \\n}\\n.big {\\n font-size: 1.66em; \\n}\\n\\n@media (min-width: 320px) {\\n p {\\n line-height: calc( 1.4em + 1vw - 3.2px );\\n }\\n .big {\\n line-height: calc( 1.4em + (1vw - 3.2px) * 1.66 );\\n }\\n}\\n\\n@media (min-width: 960px) {\\n p {\\n line-height: calc( 1.4em + 6.4px );\\n }\\n .big {\\n line-height: calc( 1.4em + 6.4px * 1.66 );\\n }\\n}\\n
\\n上面看到的分别是font-size
和line-heihgt
在以px
为单位断点下的CSS锁。其实我们还可以将他们结合在一起。假设我们的排版中有标题h1
和段落p
,他们对应的font-size
和line-height
分别如下表所示:
按排版本原则来说:“文本变大时( font-size
更大),行高( line-height
)要更紧凑一点;当宽度变宽时,行高( line-height
)要更松散一点”。这样一来,在我们这个场景中,两个原则都使用的话就会相互矛盾。所以我们只会考虑其中一方面:
对于标题h1
来说,字体大小(font-size
)的增加将比宽度增加更显著
对于段落p
来说,宽度的增加比字体大小(font-size
)只增加一点,效果会更显著
现在,我们继续基于320px
和960px
两个断点,先来完善font-size
的CSS锁:
h1 {\\n font-size: 1.5rem; /* 24px / 16px 浏览器默认font-size为16px */\\n}\\n\\n/* 0.9375rem = 15px */\\np {\\n font-size: .9375rem; /* 15px / 16px*/\\n}\\n\\n@media (min-width: 320px) {\\n h1 {\\n font-size: calc( 1.5rem + 2.5vw - 8px)\\n }\\n\\n /* .46875vw - 1.5px的结果是0 ~ 3px*/\\n p {\\n font-size: calc(.9375rem + .46875vw - 1.5px)\\n }\\n}\\n\\n@media (min-width: 960px) {\\n h1 {\\n font-size: calc(1.5rem + 16px)\\n }\\n p {\\n font-size: calc(.9375rem + 3px)\\n }\\n}\\n
\\n接下来再来完成line-height
的CSS锁,相对来说要比font-size
的CSS锁更复杂一点。
先从h1
开始。我们想给line-height
设置一个相对基线的值,所以最低的值1.2em
(上面表格所列的最小值),加上元素的字体大小是可变的,所以1.2em
将描述一个动态的线性值,该线性值具备下面两个特征:
24 × 1.2 = 28.8px
在断点1
(320px
)处的值
40 × 1.2 = 48px
在断点2
(960px
)处的值
从表格中我们也知道,在断点1
处,期望的line-height
的值是1.33em
,因此我们可以将该值四舍五入为32px
。另外我们希望找到一个线性函数可以来描述“我们添加到1.2em
基线的内容”。如果我们从数据点中删除这个1.2em
基线值,就会重新得到两数据:
24 × (1.3333 - 1.2) = 3.2px
在断点1
(320px
)处的值
40 × (1.2 - 1.2) = 0px
在断点2
(960px
)处的值
放到我们前面提到的公式中来计算:
\\nm = (y′2 - y′1) / (x2 - x1)\\nm = (0 - 3.2) / (960 - 320)\\nm = -3.2 / 640\\nm = -0.005\\n\\nb′ = y′ - mx\\nb′ = y′1 - (-0.005 × x1)\\nb′ = 3.2 + 0.005 × 320\\nb′ = 4.8\\n\\ny′ = -0.005x + 4.8\\n
\\n最终结果用CSS来描述的话,像下面这样:
\\nh1 {\\n line-height: calc( 1.2em - .5vw + 4.8px)\\n}\\n
\\n同样可以用图表来描述行高和字体大小函数之间的关系:
\\n对于段落p
,将使用1.5em
作为基线。我们要增加的行高是:(1.75 - 1.5) × 18 = 4.5px
。
虽然上面的计算都是人肉的在计算,但效果确实是存在的。为了减少人肉计算过程造成的错误,我们可以选择整个过程通过CSS的计算来完成。比如下面这个示例:
\\n@media (min-width: 320px) and (max-width: 959px) {\\n h1 {\\n font-size: calc(\\n /* y1 * /\\n 1.5rem\\n /* + m × x * /\\n+ (( 40 - 24 ) / ( 960 - 320 )) * 100vw\\n /* - m × x1 */\\n - ((40 - 24) / (960 - 320)) * 320px\\n );\\n }\\n}\\n
\\n可以将上面的代码进一步简化:
\\n@media (min-width: 320px) and (max-width: 959px) {\\n h1 {\\n font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );\\n }\\n}\\n
\\n如果把行高的锁一起结合进来像下面这样:
\\n@media (min-width: 320px) and (max-width: 959px) {\\n h1 {\\n font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );\\n /* 对于负的斜率,需要对断点求倒数 */\\n line-height: calc( 1.2em + 3.2 * (100vw - 960px) / (320 - 960) );\\n }\\n}\\n
\\nem
断点的CSS锁首先要说的是在CSS媒体查询中使用em
做为断点时,CSS锁的计算会存在很多陷阱。比如说,我们前面的示例中使用m × 100vw
(在CSS中类似calc( base + 2.5vw)
)就会有问题存在。主要因为在媒体查询的上下文中,em
或rem
单位都是一种相对单位,其最终大小都会取决于别人。比如rem
就是基于用户代理的基本字体大小,虽然默认情况下是16px
,但它可有可能会更小或更大,这主要取决于两个因素:
浏览器或操作系统选项
\\n用户的偏好
\\n这也意味着,如果我们的断点是20em
和60em
,它可能匹配的实际宽度是:
基于font-size
为16px
的场景,20em
和60em
所匹配的是320px
和960px
基于font-size
为24px
的场景,20em
和60em
所匹配的是480px
和1440px
如果浏览器默认字体大小更换,那么对应的值也会再次更改
\\n比如我们前面示例中的:
\\nfont-size: calc( 3.125vw + .625rem );\\n
\\n如果断点换成em
来计算,假设媒体查询中的1em
为16px
,那么CSS锁对应媒体查询中的px
换成了em
,代码像下面这样:
h1 {\\n font-size: 1.25rem; \\n}\\n\\n@media (min-width: 20em) {\\n h1 {\\n font-size: calc( 1.25rem + 3.125vw - 10px );\\n }\\n}\\n\\n@media (min-width: 60em) {\\n h1 {\\n font-size: calc( 1.25rem + 20px );\\n }\\n}\\n
\\n就上面的代码而言,如果客户端浏览器的默认字体大小为16px
的话,上面的CSS锁运行不会有任何缺陷,但是用户将客户端浏览器默认字号更换了,那么就会造成相应的混乱。如果用图表来表示的话,正常的是连续性的,不正常的就会断连:
造成这样的现象主要是我们更改了基本字体大小。因为基于em
的断点将移动到更大的像素值。在公式中的3.125vw - 10px
值仅适用于特定的像素点(基于像素的媒体查询)。如果更换了场景,那么结果就会差强人意:
在320px
时,3.125vw - 10px
的结果是0px
,这是我们计划中也是我们想要的结果
在480px
时,3.125vw - 10px
的结果是5px
在960px
时,3.125vw - 10px
的结果是20px
在1440px
时,3.125vw - 10px
的结果是35px
如果拿320px
和480px
两个断点来比较的话,3.125vw - 10px
在两个断点的差值是5px
,还能让人接受。但在960px
和1440px
两个断点间,3.125vw - 10px
的差值为15px
,该差值过于偏大。
基于上述原因,我们如果想在媒体查询中使用em
作为断点的话,就需要对CSS锁进行改造,换句话说需要一种不同的技术来实现CSS锁。
我们回到以em
为单位的媒体查询的CSS锁计算中来。在以em
为断点的媒体查询计算CSS锁,我们需要依赖CSS做大量的计算,在计算中会用到两个变量:
视窗宽度为100vw
在低的断点下,采用rem
为单位
将采用的计算公式为:
\\ny = m × (x - x1) / (x2 - x1)\\n
\\n该公式是如何得来的呢?我们简单地来看看。在前面的内容中,我们知道字体大小或行高可以用一个线性函数来描述,比如y = mx + b
。在CSS中,虽然可以使用100vw
来替代公式中的x
,但无法将m
和b
用精确的px
或vw
值来描述,其中原因是我们采用的是以px
为单位的固定值。如果更改基本字体大小,它们将与基于em
的断点会不相匹配。如此一来,就需要考虑用别的方式来替代m
和b
,也就是两个数据点(x1, y1)
和(x2,y2)
。在前面的内容中,我们知道如何从线性函数中得到b
:
b = y - mx\\nb = y1 - m × x1\\n
\\n将它们放在一起的话,可以得到:
\\ny = mx + b\\ny = mx + y1 - m × x1\\n
\\n最后的结果,我们可以看到,在整个函数中已经没有b
的身影了。同样,将font-size
和line-height
的基线值添加进来,从前面的知识中我们可以知道,该值是一个动态值,我们把这个动态部分用``表示,那么整个公式可以是:
y = y1 + y′\\ny′ = y - y1\\n
\\n上面的方式式可以继续演进成下面这样的:
\\ny′ = mx + y1 - m × x1 - y1\\ny′ = mx + y1 - m × x1 - y1\\n
\\n可以进一步优化:
\\ny′ = m × x - m × x1\\ny′ = m × (x - x1)\\n
\\n根据上面的公式,我们就可以轻意得到m
的值:
m = (y2 - y1) / (x2 - x1)\\n
\\n也就是:
\\ny′ = (y2 - y1) / (x2 - x1) × (x - x1)\\n
\\n上面的公式还可以换成:
\\ny′ = 最大值的增长 × (x - x1) / (x2 - x1)\\n
\\n接下来将上面的公式用CSS代码来描述,我们接着回到20px ~ 40px
的示例中:
@media (min-width: 20em) and (max-width: 60em) {\\n h1 {\\n /* 警告: 这还不行! * /\\n font-size : calc (\\n 1.25rem /* 基线值 * /\\n + 20px /* 最大值与基线值的差值 * /\\n * (100vw - 20rem) /* x - x1 * /\\n / (60rem - 20rem) /* x2 - x1 */\\n );\\n }\\n}\\n
\\n\\n\\n注意,上在代码中的
\\ncalc()
计算中使用相同或不同的值进行行乘法和除法时有许多限制,因此上面的代码在运行时可能会有问题
先回到100vw - 20rem
的计算,该计算可以正常,并将返回一个像素值。为什么呢?我们来看一个小示例。如果基本字体的大小是16px
,那么视窗的宽度就是600px
,结果将是280px
(600 - 20 × 16 = 280
);如果基础字体大小是24px
,视窗宽度为600px
,则结果为120px
(600 - 20 × 24 = 120
)。
可能你会问,既然em
存在问题,我们为何不考虑采用rem
呢?因为rem
毕竟在计算的过程是基于根元素html
或:root
来做计算的。这个原理是对的,但我们需要一个CSS单位来引用浏览器的基本字体大小,但这个单位并不存在。在CSS中最接近的只能是在html
元素或:root
中来做,如下面的代码:
/* 不好的 */\\nhtml { \\n font-size: 10px; \\n}\\n\\n:root { \\n font-size: 16px; \\n}\\n\\n/* 但在媒体查询的断点我们需要这样来进行管理,比如20rem / 1.25 或 40em / 1.25这样 */\\n:root { \\n font-size: 125%; \\n}\\n
\\n这样一来,把事情变得更为复杂化。为了让计算变得更简单,接下来在计算中引入无单位的因子计算。先来看理想情况之下的现象。
\\n在理想的情况之下,我们期望60rem ~ 20rem
部分解析为像素宽度。这意味着整个(x - x1) / (x2 - x1)
结果会分解为0 ~ 1
之间的值,在这里,将该值暂且称为 n
。
那么在基本字体大小为16px
,视窗宽度为600px
的情况下,我们会得到:
n = (x - x1) / (x2 - x1)\\nn = (600 - 320) / (960 - 320)\\nn = 280 / 640\\nn = 0.475\\n
\\n遗憾的是,上面的公式如果要用CSS的calc()
来转换的话,有可能是不能工作的。主要是因为在calc()
的除法计算中除数不能带有任何单位。不过我们可以尝试着将除数中单位去除,比如calc( (100vw - 20rem) / (60 - 20) )
的结果:
上面表格的计算是基于基本字体大小为 16px
(客户端浏览器默认字体大小为 16px
) 。
上面表格的计算是基于基本字体大小为 24px
(客户端浏览器默认字体大小为 24px
) 。
你可能也注意到了,断点20em ~ 60em
之间对应得到的线性变化的值是0 ~ 1em
之间。该特性我们可以用起来。接下来在CSS的calc()
中计算时采用20
作为因子数:
font-size: calc( 1.25rem + 20px * n );\\n
\\n上面公式中的n
是一个0 ~ 1
之间的任意值,但在CSS的calc()
中,并无法得到0 ~ 1
之间对应的结果。为此,将该值暂时用r
来描述。
这里需要注意,r
是一个带单位的数值,但在calc()
的乘法计算中,其中一个数是不能带任何单位的。也就是说,和r
相乘的另一个数是需要不带单位的。基于这个原因,在我们的示例中,我们希望在较大的断点处增加20px
(20px
对应的是1.25rem
),所以示例中采用的系数是1.25
:
font-size: calc( 1.25rem + 1.25 * r );\\n
\\n上面的代码在CSS中是可以工作了,但要注意r
是根据基本字体大小而变化:
基本字体大小是16px
,那么1.25 * r
的值在0 ~ 20px
之间
基本字体大小是24px
,那么1.25 * r
的值在0 ~ 30px
之间
这样一来,我们的CSS锁则变成像下面这样:
\\nh1 {\\n font-size: 1.25rem;\\n}\\n\\n@media (min-width: 20em) {\\n /* (100vw - 20rem) / (60 - 20) 计算出断点 (20em ~ 60em)之间 0 ~ 1rem 对应的值 */\\n h1 {\\n font-size: calc( 1.25rem + 1.25 * (100vw - 20rem) / (60 - 20) );\\n }\\n}\\n\\n@media (min-width: 60em) {\\n h1 {\\n font-size: calc( 1.25rem + 1.25 * 1rem );\\n }\\n}\\n
\\n这与基于px
的font-size
锁不同,这一次,当用户将基本字体大小增加50%
时,所有内容都会增加50%
(基线值,可变部分和断点)。我们得到了一个30px ~ 60px
的范围,而不是默认的20px ~ 40px
的范围。
按同样的原理,我们可以得到line-height
的CSS锁:
h1 {\\n line-height: calc( 1.2em + 0.2 * 1rem );\\n}\\n\\n@media (min-width: 20em) {\\n h1 {\\n line-height: calc( 1.2em + 0.2 * (100vw - 60rem) / (20 - 60) );\\n }\\n}\\n\\n@media (min-width: 60em) {\\n h1 {\\n line-height: 1.2em;\\n }\\n}\\n
\\n将font-size
和line-height
结合在一起,CSS锁对应的代码如下:
@media (min-width: 320px) and (max-width: 959px) {\\n h1 {\\n font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );\\n /* For a negative slope, we have to invert the breakpoints */\\n line-height: calc( 120% + 3.2 * (100vw - 960px) / (320 - 960) );\\n }\\n}\\n
\\n@trysmudford 对传统的CSS锁做了一些重构,在重构的CSS锁中引入了CSS自定义属性,让整个CSS锁变得更灵活。有关于整个重构的过程可以阅读《Refactoring CSS Locks》一文。最终重构出来的代码如下:
\\n:root {\\n --fluid-min-screen: 20;\\n --fluid-max-screen: 80;\\n --fluid-viewport: 100vw;\\n --fluid-bp: (\\n (var(--fluid-viewport) - calc(var(--fluid-min-screen) * 1em)) / (var(\\n --fluid-max-screen\\n ) - var(--fluid-min-screen))\\n );\\n}\\n\\nh2 {\\n font-size: calc(2em + (4 - 2) * var(--fluid-bp));\\n}\\n\\nh3 {\\n font-size: calc(1.5em + (3 - 1.5) * var(--fluid-bp));\\n}\\n\\nh4 {\\n margin-top: calc(0.5em + (2 - 0.5) * var(--fluid-bp));\\n}\\n\\nhr {\\n border: calc(0.1em + (4 - 0.1) * var(--fluid-bp)) solid teal;\\n}\\n\\n@media screen and (min-width: 80em) {\\n :root {\\n --fluid-viewport: calc(var(--fluid-max-screen) * 1em);\\n }\\n}\\n
\\n如果你不想人肉的去处理CSS锁的话,我们可以在工程中使用**postcss-scale**,可以让事情变得更简单:
\\n@media screen and (min-width: 21em) {\\n p {\\n line-height: scale(21em, 35em, 1.3em, 1.5em, 100vw);\\n }\\n}\\n
\\n编译出来:
\\n@media screen and (min-width: 21em) {\\n p {\\n line-height: calc(((1.5 - 1.3) * ((100vw) - 21em) / (35 - 21)) + 1.3em);\\n }\\n}\\n
\\nRFS是Responsive Font Size的缩写,该技术也是基于CSS锁上面做的。比如说:
\\nhtml {\\n font-size: 16px;\\n}\\n\\n@media screen and (min-width: 320px) {\\n html {\\n font-size: calc(16px + 6 * ((100vw - 320px) / 680));\\n }\\n}\\n\\n@media screen and (min-width: 1200px) {\\n html {\\n font-size: 22px;\\n }\\n}\\n
\\n在Sass中我们可以像下面这样使用RFS:
\\n.title {\\n @include font-size(4rem);\\n}\\n
\\n编译出来的结果:
\\n.title {\\n font-size: 4rem;\\n}\\n\\n@media (max-width: 1200px) {\\n .title {\\n font-size: calc(1.525rem + 3.3vw);\\n }\\n}\\n
\\n下面这张图表可以帮助我们更好的理解RFS是如何调整字体大小的:
\\n随着 CSS 的比较函数 min() ,max() 和 clamp() 的出现,给 CSS 上锁变得越来越容易。
\\n对于现代 Web 设计师和开发者都不是一件容易的事情,因为随着众多不同终端的出现,要考虑的场景越来越复杂。随着 min()
、max()
和 clamp()
函数的到来,设计师估计在做设计的时候也要有一些思想上的变化。比如说,在这之前,设计师可能会根据不同的场景为元素设计不同的大小:
那现在,基于 CSS 比较函数相关特性,Web 设计师可以像下图这样提供设计。比如,提供最小、最大和推荐三种状态下的设计风格:
\\n比如说,在响应式设计中,希望在不同的终端上文本的字号有所不同,就可以像下面这样使用:
\\n你可能也已经发现了,这和我们前面介绍的 clamp()
函数非常匹配:
body { \\n font-size: clamp(16px, 5vw, 50px);\\n} \\n
\\n这样的设计对于一些需要动态改变值的场景非常有用。
\\n如果你对这方面感兴趣,可以移步阅读:
\\n自响应式Web设计的出现,对于响应式中的文本排版(后期也被称为流式文本排版或精准排版)都面临着很大的困难。也正因为如此,社区中有很多大神,比如@Mike Riethmuller、@Tim Brown和@Geoff Graham都在致力于这方面的研究,而且找到了使用CSS锁技术方案来锁定流式排版。在该文中我们将CSS锁相关的知识结合在一起,介绍了什么是CSS锁,以及其设计原理。最后希望这篇文章对于你在响应式或流式排版本中有所帮助。如果你在这方面有更好的经验或建议,欢迎在下面的评论中与我们一起共享。
\\n如果你觉得该教程对你有所帮助,请给我点个赞。要是你喜欢 CSS ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程:
\\n