探索CSS绘画API: 多边形边框 2021-10-30 默认分类 暂无评论 2887 次阅读 ![2021-10-30 11.15.11.jpg][1] 现在,使用clip-path创建复杂的形状是一件很容易的事,但给形状添加边框总是很麻烦。没有一个强大的CSS解决方案,我们总是需要为每个特定的情况制作特定的 "黑客 "代码。在这篇文章中,我将告诉你如何使用CSS Paint API来解决这个问题。 在我们进入第三个实验之前,这里有一个关于我们正在建造的东西的小概述。而且,请注意,我们在这里所做的一切只支持基于Chromium的浏览器,所以你会想在Chrome、Edge或Opera中查看演示。[请参阅caniuse了解最新的支持情况][2]。[代码演示][3] ![overview-polygon-border-1.gif][4] 你会发现那里没有复杂的CSS代码,而是一个通用代码,我们只调整几个变量来控制形状。 **主要思路** -------- 为了实现多边形的边框,我将依靠CSS的clip-path属性和用Paint API创建的自定义遮罩的组合。[代码演示][5] ![s_8C7B7FD228C95E70E08E0344785EF03E99A61895BF9BC4080F4DC424E6AFB8F0_1630408175106_overview2.png][6] 1.我们从一个基本的矩形形状开始。 2.我们应用剪辑路径来获得我们的多边形形状。 3.我们应用自定义蒙版来获得我们的多边形边界 CSS设置 这里是我们将要讨论的剪辑路径步骤的CSS。 ``` .box { --path: 50% 0,100% 100%,0 100%; width: 200px; height: 200px; background: red; display: inline-block; clip-path: polygon(var(--path)); } ``` 到目前为止并不复杂,但请注意CSS变量--path的使用。整个技巧都依赖于这个单一的变量。由于我将使用一个剪辑路径和一个遮罩,两者都需要使用相同的参数,因此使用了--path变量。而且,是的,Paint API将使用相同的变量来创建自定义遮罩。 整个过程的CSS代码变成了。 ``` .box { --path: 50% 0,100% 100%,0 100%; --border: 5px; width: 200px; height: 200px; background: red; display: inline-block; clip-path: polygon(var(--path)); -webkit-mask: paint(polygon-border) } ``` 除了剪辑路径外,我们还应用了自定义遮罩,另外我们还添加了一个额外的变量--边框,以控制边框的厚度。正如你所看到的,到目前为止,一切都还是非常基本和通用的CSS。毕竟,这也是使CSS绘画API如此好用的原因之一。 **JavaScript的设置** ----------------- 现在,让我们看看paint()函数内部发生了什么,因为我们跳到了JavaScript。 ``` const points = properties.get('--path').toString().split(','); const b = parseFloat(properties.get('--border').value); const w = size.width; const h = size.height; const cc = function(x,y) { // ... } var p = points[0].trim().split(" "); p = cc(p[0],p[1]); ctx.beginPath(); ctx.moveTo(p[0],p[1]); for (var i = 1; i < points.length; i++) { p = points[i].trim().split(" "); p = cc(p[0],p[1]); ctx.lineTo(p[0],p[1]); } ctx.closePath(); ctx.lineWidth = 2*b; ctx.strokeStyle = '#000'; ctx.stroke(); ``` 获取和设置CSS自定义属性的能力是它们如此伟大的原因之一。我们可以达到JavaScript首先读取--path变量的值,然后将其转换为一个点的数组(在上面第一行看到)。因此,这意味着50% 0,100% 100%,0 100%成为遮罩的点,即point = ["50% 0", "100% 100%", "0 100%"]。 然后我们用moveTo和lineTo循环浏览这些点,绘制一个多边形。这个多边形与CSS中使用clip-path属性绘制的多边形完全相同。 最后,在画完这个形状后,我给它添加了一个笔画。我使用lineWidth定义了笔画的厚度,并使用strokeStyle设置了一个纯色。换句话说,只有形状的笔触是可见的,因为我没有给形状填充任何颜色(也就是说,它是透明的)。 现在我们要做的就是更新路径和厚度来创建任何多边形边框。值得注意的是,由于我们使用的是CSS的背景属性,所以在这里我们并不局限于纯色。我们可以考虑梯度或图像。 [代码演示][7] ![CSS-polygon-border-result.png][8] 如果我们需要添加内容,我们必须考虑一个伪元素。否则,内容会在这个过程中被剪掉。支持内容并不是非常困难的。我们把掩码属性移到伪元素上。我们可以保留主元素上的clip-path声明。[代码参考][9] **到目前为止有问题吗?** -------------- 我知道在看完最后一个剧本后,你可能有一些迫切的问题想问。请允许我先回答几个问题,我想你一定有想法。 **那个cc()函数是什么?** 我正在使用该函数将每个点的值转换成像素值。对于每个点,我都会得到x和y坐标--使用point[i].trim().split(" ")--然后我转换这些坐标,使它们可以在画布元素中使用,使我们可以用这些点作画。 ``` const cc = function(x,y) { var fx=0,fy=0; if (x.indexOf('%') > -1) { fx = (parseFloat(x)/100)*w; } else if(x.indexOf('px') > -1) { fx = parseFloat(x); } if (y.indexOf('%') > -1) { fy = (parseFloat(y)/100)*h; } else if(y.indexOf('px') > -1) { fy = parseFloat(y); } return [fx,fy]; } ``` 逻辑很简单:如果它是一个百分比值,我就用宽度(或高度)来找到最终值。如果它是一个像素值,我就简单地得到这个值,而不考虑单位。例如,如果我们有[50% 20%],宽度等于200px,高度等于100px,那么我们得到[100 20]。如果是[20px 50px],那么我们就得到[20 50]。以此类推。 **如果遮罩已经将元素剪切到形状的笔画上,你为什么还要使用CSS clip-path?** 只使用遮罩是我心中的第一个想法,但我偶然发现了这种方法的两个主要问题。第一个问题与stroke()的工作方式有关。来自MDN。 > 描边与路径的中心对齐;换句话说,描边的一半画在内侧,一半画在外侧。 这种 "一半内侧,一半外侧 "的做法让我很头疼,在把所有东西放在一起时,我总是会出现奇怪的溢出。这就是CSS clip-path的帮助所在;它把外侧的部分剪掉,只保留内侧的部分--不再有溢出的现象了 你会注意到ctx.lineWidth = 2*b的使用。我加入了双倍的边框厚度,因为我将剪掉一半的边框,最后在整个形状周围获得所需的正确厚度。 第二个问题是与形状的可悬停区域有关。众所周知,遮蔽不会影响该区域,我们仍然可以悬停/与整个矩形互动。同样的,达到clip-path可以解决这个问题,另外我们把交互限制在形状本身。 下面的演示说明了这两个问题。第一个元素有一个遮罩和夹层路径,而第二个元素只有遮罩。我们可以清楚地看到溢出的问题。试着将第二个元素悬停,看看我们可以改变颜色,即使光标在三角区之外。[代码参考][10] 你为什么要用@property与边界值? 这是一个有趣的--也是相当棘手的--部分。默认情况下,自定义属性(如-边框)被视为 "CSSUnparsedValue",这意味着它们被当作字符串处理。来自CSS规范。 > CSSUnparsedValue "对象代表引用自定义属性的属性值。它们是由字符串片段和变量引用的列表组成的。 通过@property,我们可以注册自定义属性,并给它一个类型,这样它就可以被浏览器识别,并作为一个有效的类型而不是字符串来处理。在我们的例子中,我们将边框注册为类型,这样以后它就变成了CSSUnitValue。这也允许我们使用任何长度单位(px、em、ch、vh等)来表示边框值。 这听起来可能有点复杂,但让我试着用DevTools的截图来说明其区别。 ![s_8C7B7FD228C95E70E08E0344785EF03E99A61895BF9BC4080F4DC424E6AFB8F0_1630579831868_unit.png][11] 我在一个变量上使用console.log(),我定义了5em。第一个变量被注册了,但第二个变量却没有。 在第一种情况下,浏览器会识别类型并将其转换为像素值,这很有用,因为我们在paint()函数中只需要像素值。在第二种情况下,我们得到的变量是一个字符串,这不是很有用,因为我们不能在paint()函数中把em单位转换成px单位。 尝试所有的单位。它的结果总是与paint()函数中计算出的像素值一致。 **那--path变量呢?** 我想对--path变量使用同样的方法,但不幸的是,我认为我把CSS推到了它能做的极限。使用@property,我们可以注册复杂的类型,甚至是多值变量。但这对于我们需要的路径来说还是不够的。 我们可以使用+和#符号来定义空格分隔或逗号分隔的值列表,但我们的路径是一个逗号分隔的百分比(或长度)值列表。我想使用类似[+]#的东西,但它并不存在。 对于路径,我不得不把它作为一个字符串值来处理。这就限制了我们现在只用百分比和像素值。出于这个原因,我定义了cc()函数来将字符串值转换成像素值。 我们可以在CSS规范中阅读: > 语法字符串的内部语法是CSS值定义语法的一个子集。该规范的未来级别预计将扩大允许的语法的复杂性,允许自定义属性更接近于CSS属性所允许的全部范围。 即使语法扩展到能够注册路径,我们仍然会面临问题,如果我们需要在我们的路径中包含calc()。 ``` --path: 0 0,calc(100% - 40px) 0,100% 40px,100% 100%,0 100%; ``` 在上面的例子中,calc(100%-40px)是一个被浏览器认为是<长度-百分比>的值,但浏览器在知道百分比的引用之前,无法计算这个值。换句话说,我们无法在paint()函数中获得等效的像素值,因为只有在var()中使用该值时才能知道参考值。 为了克服这个问题,我们可以扩展cc()函数来进行转换。我们做了百分比值和像素值的转换,所以让我们把这些合并成一个转换。我们将考虑两种情况:calc(P% - Xpx)和calc(P% + Xpx)。我们的脚本就变成了。 ``` const cc = function(x,y) { var fx=0,fy=0; if (x.indexOf('calc') > -1) { var tmp = x.replace('calc(','').replace(')',''); if (tmp.indexOf('+') > -1) { tmp = tmp.split('+'); fx = (parseFloat(tmp[0])/100)*w + parseFloat(tmp[1]); } else { tmp = tmp.split('-'); fx = (parseFloat(tmp[0])/100)*w - parseFloat(tmp[1]); } } else if (x.indexOf('%') > -1) { fx = (parseFloat(x)/100)*w; } else if(x.indexOf('px') > -1) { fx = parseFloat(x); } if (y.indexOf('calc') > -1) { var tmp = y.replace('calc(','').replace(')',''); if (tmp.indexOf('+') > -1) { tmp = tmp.split('+'); fy = (parseFloat(tmp[0])/100)*h + parseFloat(tmp[1]); } else { tmp = tmp.split('-'); fy = (parseFloat(tmp[0])/100)*h - parseFloat(tmp[1]); } } else if (y.indexOf('%') > -1) { fy = (parseFloat(y)/100)*h; } else if(y.indexOf('px') > -1) { fy = parseFloat(y); } return [fx,fy]; } ``` 我们使用indexOf()来测试calc的存在,然后,通过一些字符串的处理,我们提取这两个值,并找到最终的像素值。 因此,我们也需要更新这一行。 ``` p = points[i].trim().split(" "); ``` ``` p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g); ``` 由于我们需要考虑calc(),所以使用空格字符就不能进行分割了。这是因为calc()也包含空格。因此,我们需要一个重构函数。不要问我--这是在Stack Overflow上尝试了很多之后的结果。 下面是基本的演示,说明我们到目前为止为支持calc()所做的更新 [代码参考][12] 请注意,我们将calc()表达式存储在我们注册为的变量--v内。这也是技巧的一部分,因为如果我们这样做,浏览器就会使用正确的格式。无论calc()表达式有多复杂,浏览器总是将其转换为calc(P% +/- Xpx)格式。由于这个原因,我们只需要在paint()函数中处理这种格式。 下面是不同的例子,我们在每个例子中使用不同的calc()表达式。[代码参考][13] 如果你检查每个盒子的代码,看看--v的计算值,你总是会发现相同的格式,这是超级有用的,因为我们可以有任何一种我们想要的计算方式。 应该注意的是,使用变量--v并不是强制性的。我们可以直接在路径中加入calc()。我们只需要确保我们插入了正确的格式,因为浏览器不会为我们处理它(记住,我们不能注册路径变量,所以它对浏览器来说是一个字符串)。当我们需要在路径内有许多calc(),而为每一个创建一个变量会使代码过于冗长时,这可能很有用。我们将在最后看到几个例子。 **我们可以有虚线边界吗?** --------------- 我们可以! 而且只需要一条指令。元素已经有一个内置的函数来绘制虚线笔划setLineDash()。 > Canvas 2D > API的CanvasRenderingContext2D接口的setLineDash()方法可以设置描画线条时使用的虚线模式。它使用一个数值数组来指定描述该模式的线条和空隙的交替长度。 我们所要做的就是引入另一个变量来定义我们的dash模式。[代码演示地址][14] ![css-dashed-polygon-border.png][15] 在CSS中,我们简单地添加了一个CSS变量,--dash,在掩码内是以下内容。 ``` // ... const d = properties.get('--dash').toString().split(','); // ... ctx.setLineDash(d); ``` 我们还可以用lineDashOffset来控制偏移量。我们将在后面看到,控制偏移量可以帮助我们达到一些很酷的动画效果。 **为什么不使用@property来注册dash变量?** ---------------------------- 从技术上讲,我们可以将dash变量注册为#,因为它是一个以逗号分隔的长度值的列表。它确实有效,但我无法在paint()函数中检索到这些值。我不知道这是一个错误,还是缺乏支持,或者我只是错过了拼图中的一个部分。 这里有一个演示来说明这个问题。 [代码演示][16] 我正在用这个注册--dash变量。 ``` @property --dash{ syntax: '#'; inherits: true; initial-value: 0; } ``` ......后来又把这个变量声明成这样。 ``` --dash: 10em,3em; ``` 如果我们检查这个元素,我们可以看到,浏览器正在正确处理这个变量,因为计算出来的值是像素级的 ![s_8C7B7FD228C95E70E08E0344785EF03E99A61895BF9BC4080F4DC424E6AFB8F0_1630668590686_computed.png][17] 但我们只在paint()函数中得到第一个值 ![s_8C7B7FD228C95E70E08E0344785EF03E99A61895BF9BC4080F4DC424E6AFB8F0_1630668702093_onevalue.png][18] 在我找到解决这个问题的方法之前,我只能把--dahs变量作为一个字符串使用,就像--path一样。在这种情况下不是什么大问题,因为我认为我们不需要超过像素的值。 **使用案例!** --------- 在探索了这一技术的背后,现在让我们把重点放在CSS部分,看看我们的多边形边框的几个使用案例。 **一个按钮的集合** 我们可以很容易地生成具有很酷的悬停效果的自定义形状的按钮。[代码演示地址][19] 注意到Calc()是如何在最后一个按钮的路径中使用的,我们前面描述过。由于我遵循了正确的格式,所以它工作得很好。 **面包屑** 在创建面包屑系统时,不再有令人头疼的问题了 下面,你会发现没有 "黑客 "或复杂的CSS代码,而是一些非常通用和容易理解的东西,我们所要做的就是调整一些变量。 [代码演示][20] 卡片显示动画 如果我们对厚度应用一些动画,我们可以得到一些花哨的悬停效果 [代码演示][21] 我们可以用同样的思路来创建一个揭示卡片的动画。[代码演示][22] **呼叫和语音泡** "我们到底怎样才能给那个小箭头加上边框?"。我想每个人在处理呼号或语音泡的设计时都会遇到这个问题。Paint API使这个问题变得微不足道。[代码演示][23] 在那个演示中,你会发现一些你可以扩展的例子。你只需要为你的语音泡泡找到路径,然后调整一些变量来控制边界的厚度和箭头的大小/位置。 动画化破折号 在我们结束之前的最后一个。这一次我们将专注于虚线的边框来创建更多的动画。我们已经在按钮集合中做了一个,我们把虚线边框变成了实线。让我们来处理另外两个。 悬停在下面,看看我们得到的漂亮效果。[代码演示][24] 那些已经使用SVG工作了一段时间的人可能对我们通过动画化stroke-dasharray实现的那种效果很熟悉。Chris甚至在不久前还讨论过这个概念。多亏了Paint API,我们可以直接在CSS中这样做。这个想法和我们在SVG中使用的几乎一样。我们定义破折号这个变量。 ``` --dash: var(--a),1000; ``` 变量--a从0开始,所以我们的图案是一条实线(长度等于0),有一个缺口(长度为1000);因此没有边界。我们将 --a 动画化为一个大的数值来绘制我们的边框。 我们还谈到了使用lineDashOffset,我们可以用它来做另一种动画。将鼠标悬停在下面,看看结果。[代码演示][25] 终于有了一个CSS解决方案,可以让破折号的位置动画化,并且适用于任何类型的形状! 我所做的很简单。我添加了一个额外的变量--offset,对其应用了一个从0到N的过渡。然后,在paint()函数中,我做了以下工作。 ``` const o = properties.get('--offset'); ctx.lineDashOffset=o; ``` 就这么简单! 让我们不要忘记使用关键帧的无限动画。[代码演示][26] 我们可以通过偏移0到N来使动画连续运行,其中N是破折号变量中使用的数值之和(在我们的例子中,是10+15=25)。我们使用一个负值来拥有相反的方向。 [1]: http://guobacai.com/usr/uploads/2021/10/3966507273.jpg [2]: https://caniuse.com/css-paint-api [3]: https://codepen.io/t_afif/pen/wvewXjZ [4]: http://guobacai.com/usr/uploads/2021/10/1832165321.gif [5]: https://codepen.io/t_afif/pen/RwgrvEr [6]: http://guobacai.com/usr/uploads/2021/10/604985241.png [7]: https://codepen.io/t_afif/pen/zYzqzjO [8]: http://guobacai.com/usr/uploads/2021/10/3454414764.png [9]: https://codepen.io/t_afif/pen/dyRMQXP [10]: https://codepen.io/t_afif/pen/dyRMRby [11]: http://guobacai.com/usr/uploads/2021/10/1057132595.png [12]: https://codepen.io/t_afif/pen/zYzogdX [13]: https://codepen.io/t_afif/pen/mdwRVBr [14]: https://codepen.io/t_afif/pen/RwgaqEV [15]: http://guobacai.com/usr/uploads/2021/10/778425598.png [16]: https://codepen.io/t_afif/pen/QWgEJwv [17]: http://guobacai.com/usr/uploads/2021/10/2918555557.png [18]: http://guobacai.com/usr/uploads/2021/10/158819707.png [19]: https://codepen.io/t_afif/pen/abwBPPz [20]: https://codepen.io/t_afif/pen/powNXwR [21]: https://codepen.io/t_afif/pen/oNwBxmp [22]: https://codepen.io/t_afif/pen/oNwBxmp [23]: https://codepen.io/t_afif/pen/qBjRPeV [24]: https://codepen.io/t_afif/pen/MWoJROX [25]: https://codepen.io/t_afif/pen/JjJEVwP [26]: https://codepen.io/t_afif/pen/OJgWGKx 标签: javascript, css
评论已关闭