现代 CSS 高阶技巧,不规则边框解决方案
本文是 CSS Houdini 之 CSS Painting API 系列第四篇。
在上三篇中,我们详细介绍了 CSS Painting API 是如何一步一步,实现自定义图案甚至实现动画效果的!
在这一篇中,我们将继续探索,尝试使用 CSS Painting API,去实现过往 CSS 中非常难以实现的一个点,那就是如何绘制不规则图形的边框。
CSS Painting API
再简单快速的过一下,什么是 CSS Painting API。
CSS Painting API 是 CSS Houdini 的一部分。而 Houdini 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。Houdini 是一组 API,它们使开发人员可以直接访问 CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器中本地实现。
CSS Paint API 目前的版本是 CSS Painting API Level 1。它也被称为 CSS Custom Paint 或者 Houdini's Paint Worklet。
我们可以把它理解为 JS In CSS,利用 JavaScript Canvas 画布的强大能力,实现过往 CSS 无法实现的功能。
过往 CSS 实现不规则图形的边框方式
CSS 实现不规则图形的边框,一直是 CSS 的一个难点之一。在过往,虽然我们有很多方式利用 Hack 出不规则图形的边框,我在之前的多篇文章中有反复提及过:
我们来看看这样一个图形:
利用 CSS 实现这样一个图形是相对简单的,可以利用 mask 或者 background 中的渐变实现,像是这样:
<div class="arrow-button"></div>
.arrow-button {
position: relative;
width: 180px;
height: 64px;
background: #f49714;
&::after {
content: "";
position: absolute;
width: 32px;
height: 64px;
top: 0;
right: -32px;
background:
linear-gradient(-45deg, transparent 0, transparent 22px, #f49714 22px, #f49714 100%),
linear-gradient(-135deg, transparent 0, transparent 22px, #f49714 22px, #f49714 100%);
background-size: 32px 32px;
background-repeat: no-repeat;
background-position: 0 bottom, 0 top;
}
}
但是,如果,要实现这个图形,但是只有一层边框,利用 CSS 就不那么好实现了,像是这样:
在过往,有两种相对还不错的方式,去实现这样一个不规则图形的边框:
- 借助 filter,利用多重
drop-shadow() - 借助 SVG 滤镜实现
我们快速回顾一下这两个方法。
借助 filter,利用多重 drop-shadow() 实现不规则边框
还是上面的图形,我们利用多重 drop-shadow(),可以大致的得到它的边框效果。代码如下:
div {
position: relative;
width: 180px;
height: 64px;
background: #fff;
&::after {
content: "";
position: absolute;
width: 32px;
height: 64px;
top: 0;
right: -32px;
background:
linear-gradient(-45deg, transparent 0, transparent 22px, #fff 22px, #fff 100%),
linear-gradient(-135deg, transparent 0, transparent 22px, #fff 22px, #fff 100%);
background-size: 32px 32px;
background-repeat: no-repeat;
background-position: 0 bottom, 0 top;
}
}
div {
filter:
drop-shadow(0px 0px .5px #000)
drop-shadow(0px 0px .5px #000)
drop-shadow(0px 0px .5px #000);
}
可以看到,这里我们通过叠加 3 层 drop-shadow(),来实现不规则图形的边框,虽然 drop-shadow() 是用于生成阴影的,但是多层值很小的阴影叠加下,竟然有了类似于边框的效果:
借助 SVG 滤镜实现实现不规则边框
另外一种方式,需要掌握比较深的 SVG 滤镜知识。通过实现一种特殊的 SVG 滤镜,再通过 CSS 的 filter 引入,实现不规则边框。
看看代码:
<div></div>
<svg width="0" height="0">
<filter id="outline">
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="1"></feMorphology>
<feMerge>
<feMergeNode in="DILATED" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>
div {
position: relative;
width: 180px;
height: 64px;
background: #fff;
&::after {
content: "";
position: absolute;
width: 32px;
height: 64px;
top: 0;
right: -32px;
background:
linear-gradient(-45deg, transparent 0, transparent 22px, #fff 22px, #fff 100%),
linear-gradient(-135deg, transparent 0, transparent 22px, #fff 22px, #fff 100%);
background-size: 32px 32px;
background-repeat: no-repeat;
background-position: 0 bottom, 0 top;
}
}
div {
filter: url(#outline);
}
简单浅析一下这段 SVG 滤镜代码:
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="1"></feMorphology>将原图的不透明部分作为输入,采用了 dilate 扩张模式且程度为 radius="1",生成了一个比原图大 1px 的黑色图块- 使用 feMerge 将黑色图块和原图叠加在一起
- 可以通过控制滤镜中的 radius="1" 来控制边框的大小
这样,也可以实现不规则图形的边框效果:
CodePen Demo -- 3 ways to achieve unregular border
利用 CSS Painting API 实现不规则边框
那么,到了今天,利用 CSS Painting API ,我们有了一种更为直接的方式,更好的解决这个问题。
还是上面的图形,我们利用 clip-path 来实现一下。
<div></div>
div {
position: relative;
width: 200px;
height: 64px;
background: #f49714;
clip-path: polygon(85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;);
}
我们可以得到这样一个图形:
当然,本文的主角是 CSS Painting API,既然我们有 clip-path 的参数,其实完全也可以利用 CSS Painting API 的 borderDraw 来绘制这个图形。
我们尝试一下,改造我们的代码:
<div></div>
<script>
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('/CSSHoudini.js');
}
</script>
div {
position: relative;
width: 200px;
height: 64px;
background: paint(borderDraw);
--clipPath: 85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;);
}
这里,我们将原本的 clip-path 的具体路径参数,定义为了一个 CSS 变量 --clipPath,传入我们要实现的 borderDraw 方法中。整个图形效果,就是要利用 background: paint(borderDraw) 绘制出来。
接下来,看看,我们需要实现 borderDraw。核心的点在于,我们通过拿到 --clipPath 参数,解析它,然后通过循环函数利用画布把这个图形绘制出来。
// CSSHoudini.js 文件
registerPaint(
"borderDraw",
class {
static get inputProperties() {
return ["--clipPath"];
}
paint(ctx, size, properties) {
const { width, height } = size;
const clipPath = properties.get("--clipPath");
const paths = clipPath.toString().split(",");
const parseClipPath = function (obj) {
const x = obj[0];
const y = obj[1];
let fx = 0,
fy = 0;
if (x.indexOf("%") > -1) {
fx = (parseFloat(x) / 100) * width;
} else if (x.indexOf("px") > -1) {
fx = parseFloat(x);
}
if (y.indexOf("%") > -1) {
fy = (parseFloat(y) / 100) * height;
} else if (y.indexOf("px") > -1) {
fy = parseFloat(y);
}
return [fx, fy];
};
var p = parseClipPath(paths[0].trim().split(" "));
ctx.beginPath();
ctx.moveTo(p[0], p[1]);
for (var i = 1; i < paths.length; i++) {
p = parseClipPath(paths[i].trim().split(" "));
ctx.lineTo(p[0], p[1]);
}
ctx.closePath();
ctx.fill();
}
}
);
简单解释一下上述的代码,注意其中最难理解的 parseClipPath() 方法的解释。
- 首先我们,通过
properties.get("--clipPath"),我们能够拿到传入的--clipPath参数 - 通过
spilt()方法,将--clipPath分成一段段,也就是我们的图形实际的绘制步骤 - 这里有一点非常重要,也就是
parseClipPath()方法,由于我们的-clipPath的每一段可能是100% 50%这样的构造,但是实际在绘图的过程中,我们需要的实际坐标的绝对值,譬如在一个 100 x 100 的画布上,我们需要将50% 50%的百分比坐标,转化为实际的50 50这样的绝对值 - 在理解了
parseClipPath()后,剩下的就都非常好理解了,我们通过ctx.beginPath()、ctx.move、ctx.lineTo以及ctx.closePath()将整个--clipPath的图形绘制出来 - 最后,利用
ctx.fill()给图形上色
这样,我们就得到了这样一个图形:
都拿到了完整的图形了,那么我们只给这个图形绘制边框,不上色,不就得到了它的边框效果了吗?
简单改造一些 JavaScript 代码的最后部分:
// CSSHoudini.js 文件
registerPaint(
"borderDraw",
class {
static get inputProperties() {
return ["--clipPath"];
}
paint(ctx, size, properties) {
// ...
ctx.closePath();
// ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
ctx.stroke();
}
}
);
这样,我们就得到了图形的边框效果:
仅仅利用 background 绘制的缺陷
但是,仅仅利用 [bacg](background: paint(borderDraw)) 来绘制边框效果,会有一些问题。
上述的图形,我们仅仅赋予了 1px 的边框,如果我们把边框改成 5px 呢?看看会发生什么?
// CSSHoudini.js 文件
registerPaint(
"borderDraw",
class {
static get inputProperties() {
return ["--clipPath"];
}
paint(ctx, size, properties) {
// ...
ctx.lineWidth = 5;
ctx.strokeStyle = "#000";
ctx.stroke();
}
}
);
此时,整个图形会变成:
可以看到,没有展示完整的 5px 的边框,这是由于整个画布只有元素的高宽大小,而上述的代码中,元素的边框有一部分绘制到了画布之外,因此,整个图形并非我们期待的效果。
因此,我们需要换一种思路解决这个问题,继续改造一下我们的代码,仅仅需要改造 CSS 代码即可:
div {
position: relative;
width: 200px;
height: 64px;
margin: auto;
clip-path: polygon(var(--clipPath));
--clipPath: 85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;
&::before {
content:"";
position:absolute;
inset: 0;
mask: paint(borderDraw);
background: #000;
}
}
这里,我们的元素本身,还是利用了 clip-path: polygon(var(--clipPath)) 剪切了自身,同时,我们借助了一个伪元素,利用这个伪元素去实现具体的边框效果。
这里其实用了一种内外切割的思想,去实现的边框效果:
- 利用父元素的
clip-path: polygon(var(--clipPath))剪切掉外围的图形 - 利用给伪元素的 mask 作用实际的
paint(borderDraw)方法,把图形的内部镂空,只保留边框部分
还是设置 ctx.lineWidth = 5,再看看效果:
看上去不错,但是实际上,虽然设置了 5px 的边框宽度,但是实际上,上图的边框宽度只有 2.5px 的,这是由于另外一点一半边框实际上被切割掉了。
因此,我们如果需要实现 5px 的效果,实际上需要 ctx.lineWidth =10。
当然,我们可以通过一个 CSS 变量来控制边框的大小:
div {
position: relative;
width: 200px;
height: 64px;
margin: auto;
clip-path: polygon(var(--clipPath));
--clipPath: 85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;
--borderWidth: 5;
&::before {
content:"";
position:absolute;
inset: 0;
mask: paint(borderDraw);
background: #000;
}
}
在实际的 borderDraw 函数中,我们将传入的 --borderWidth 参数,乘以 2 使用就好:
registerPaint(
"borderDraw",
class {
static get inputProperties() {
return ["--clipPath", "--borderWidth"];
}
paint(ctx, size, properties) {
const borderWidth = properties.get("--borderWidth");
// ...
ctx.lineWidth = borderWidth * 2;
ctx.strokeStyle = "#000";
ctx.stroke();
}
}
);
这样,我们每次都能得到我们想要的边框长度:
CodePen Demo -- CSS Hudini & Unregular Custom Border
到这里,整个实现就完成了,整个过程其实有多处非常关键的点,会有一点点难以理解,具体可能需要自己实际调试一遍找到实现的原理。
具体应用
在掌握了上述的方法后,我们就可以利用这个方式,实现各类不规则图形的边框效果,我们只需要传入对于的 clip-path 参数以及我们想要的边框长度即可。
好,这样,我们就能实现各类不同的不规则图形的边框效果了。
像是这样:
div {
position: relative;
width: 200px;
height: 200px;
clip-path: polygon(var(--clipPath));
--clipPath: 0% 15%, 15% 15%, 15% 0%, 85% 0%, 85% 15%, 100% 15%, 100% 85%, 85% 85%, 85% 100%, 15% 100%, 15% 85%, 0% 85%;
--borderWidrh: 1;
--color: #000;
&::before {
content:"";
position:absolute;
inset: 0;
mask: paint(borderDraw);
background: var(--color);
}
}
div:nth-child(2) {
--clipPath: 50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%;
--borderWidrh: 2;
--color: #ffcc00;
}
div:nth-child(3) {
--clipPath: 90% 58%90% 58%, 69% 51%, 69% 51%, 50% 21%, 50% 21%, 39% 39%, 39% 39%, 15% 26%, 15% 26%, 15% 55%, 15% 55%, 31% 87%, 31% 87%, 14% 84%, 14% 84%, 44% 96%, 44% 96%, 59% 96%, 59% 96%, 75% 90%, 75% 90%, 71% 83%, 71% 83%, 69% 73%, 69% 73%, 88% 73%, 88% 73%, 89% 87%, 89% 87%, 94% 73%, 94% 73%;
--borderWidrh: 1;
--color: deeppink;
}
div:nth-child(4) {
--clipPath: 0% 0%, 100% 0%, 100% 75%, 75% 75%, 75% 100%, 50% 75%, 0% 75%;
--borderWidrh: 1;
--color: yellowgreen;
}
div:nth-child(5) {
--clipPath: 20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%;
--borderWidrh: 3;
--color: #c7b311;
}
得到不同图形的边框效果:
CodePen Demo -- CSS Hudini & Unregular Custom Border
又或者是基于它们,去实现各类按钮效果,这种效果在以往使用 CSS 是非常非常难实现的:
它们的核心原理都是一样的,甚至加上 Hover 效果,也是非常的轻松:
完整的代码,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/ExRLqdO
至此,我们再一次利用 CSS Painting API 实现了我们过往 CSS 完全无法实现的效果。这个也就是 CSS Houdini 的魅力,是 JS In CSS 的魅力。
兼容性?
好吧,其实上一篇文章也谈到了兼容问题,因为可能有很多看到本篇文章并没有去翻看前两篇文章的同学。那么,CSS Painting API 的兼容性到底如何呢?
CanIUse - registerPaint 数据如下(截止至 2022-11-23):
Chrome 和 Edge 基于 Chromium 内核的浏览器很早就已经支持,而主流浏览器中,Firefox 和 Safari 目前还不支持。
CSS Houdini 虽然强大,目前看来要想大规模上生产环境,仍需一段时间的等待。让我们给时间一点时间!
最后
好了,本文到此结束,希望本文对你有所帮助
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
现代 CSS 高阶技巧,不规则边框解决方案的更多相关文章
- css高級技巧
1.鼠標顯示 a:小手cursor:pointer b:默認cursor:default c:勾選文本cursor:text d:拖動cursor:move 2.css三種佈局模型 a.流動模型(默認 ...
- 数独高阶技巧之八——SDC
在本系列的第四篇“简单异数链”中,向大家介绍了XY-Wing等一系列Wing类技巧,并提到可以用(拐弯的)数组的观念来理解这些结构,经过第六篇ALS的学习之后,大家回过头再去看Wing,应该可以发现相 ...
- 数独高阶技巧入门之六——ALS
在这个系列的第一篇(链及其简单应用)以及第四篇(简单异数链)中已经简单介绍过ALS结构的定义,即n格中存在n+1个不同的候选数 (双值格可视为特殊的ALS结构) .根据数独规则,在组成ALS的候选数 ...
- 数独高阶技巧入门之七——AIC & Nice Loop
AIC(交替推理链,Alternate Inference Chain) 在简单异数链一文中我们介绍过XY-Chain技法,AIC可以看作是XY-Chain的扩展.有别于XY-Chain仅局限于双值格 ...
- 数独高阶技巧入门之三——Fish
术语Fish代表了一组工作原理相同的关于特定候选数的解题技巧(Fish技巧直接产生自数独规则——每个单元内的数字都不能重复),Fish家族成员包括“体型”从小到大的X-Wing.Swordfish. ...
- 读《实战 GUI 产品的自动化测试》之:第四步,高阶技巧
转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting4/ 定义测试控件库 本系列前几篇文章对 IBM 框架做了介绍, ...
- 高阶 CSS 技巧在复杂动效中的应用
最近我在 CodePen 上看到了这样一个有意思的动画: 整个动画效果是在一个标签内,借助了 SVG PATH 实现.其核心在于对渐变(Gradient)的究极利用. 完整的代码你可以看看这里 -- ...
- css border 三角形阴影(不规则图形阴影) & 多重边框的制作
前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! border 的组合写法 border:border-width border-style border- ...
- 《前端之路》之 JavaScript 进阶技巧之高阶函数(下)
目录 第二章 - 03: 前端 进阶技巧之高阶函数 一.防篡改对象 1-1:Configurable 和 Writable 1-2:Enumerable 1-3:get .set 2-1:不可扩展对象 ...
- iOS开发小技巧 -- tableView-section圆角边框解决方案
[iOS开发]tableView-section圆角边框解决方案 tableView圆角边框解决方案 iOS 7之前,图下圆角边框很容易设置 iOS 7之后,tableviewcell的风格不再是圆角 ...
随机推荐
- MyCLI :一个支持自动补全和语法高亮的 MySQL/MariaDB 客户端
MyCLI 是一个易于使用的命令行客户端,可用于受欢迎的数据库管理系统 MySQL.MariaDB 和 Percona,支持自动补全和语法高亮.它是使用 prompt_toolkit 库写的,需要 P ...
- 《吐血整理》高级系列教程-吃透Fiddler抓包教程(25)-Fiddler如何优雅地在正式和测试环境之间来回切换-下篇
1.简介 在开发或者测试的过程中,由于项目环境比较多,往往需要来来回回地反复切换,那么如何优雅地切换呢?宏哥今天介绍几种方法供小伙伴或者童鞋们进行参考. 2.实际工作场景 2.1问题场景 (1)已发布 ...
- POJ3417 Network暗的连锁 (树上差分)
树上的边差分,x++,y++,lca(x,y)-=2. m条边可以看做将树上的一部分边覆盖,就用差分,x=1,表示x与fa(x)之间的边被覆盖一次,m次处理后跑一遍dfs统计子树和,每个节点子树和va ...
- POJ3237 Tree (树链剖分)
通过打懒标记实现区间取反,和线段树基本操作都差不多. 本题还是一道边权化为点权的问题. 200行巨长代码: 1 #include<cstdio> 2 #include<cstring ...
- 【pytest官方文档】解读- 插件开发之hooks 函数(钩子)
上一节讲到如何安装和使用第三方插件,用法很简单.接下来解读下如何自己开发pytest插件. 但是,由于一个插件包含一个或多个钩子函数开发而来,所以在具体开发插件之前还需要先学习hooks函数. 一.什 ...
- [Thread] Synchronized
1.Monitor对象 Monitor对象被称为管程或者监视器锁. 在Java中,每一个对象实例都会关联一个Monitor对象. 这个Monitor对象既可以与对象一起创建销毁,也可以在线程试图获取对 ...
- Vue学习之--------组件在Vue脚手架中的使用(代码实现)(2022/7/24)
文章目录 1.第一步编写组件 1.1 编写一个 展示学校的组件 1.2 定义一个展示学生的信息组件 2.第二步引入组件 3.制作一个容器 4.使用Vue接管 容器 5.实际效果 6.友情提示: 7.项 ...
- 深入浅出redis缓存应用
0.1.索引 https://blog.waterflow.link/articles/1663169309611 1.只读缓存 只读缓存的流程是这样的: 当查询请求过来时,先从redis中查询数据, ...
- F118校准(二)-- 操作步骤(使用PX01 PG点屏,并使用PX01 PG校准F118)
1. 准备工作 硬件连接: CA310通过USB线材连接PC PX01通过USB线材连接PC F118通过灰排线连接PX01左上角的GPIO扩展口(如下图所示) LCM连接PX01 启动LcdTool ...
- Trino Worker 规避 OOM 思路
背景 Trino 集群如果不做任何配置优化,按照默认配置上线,Master 和 Worker 节点都很容易发生 OOM.本文从 Trino 内存设计出发, 分析 Trino 内存管理机制,到限制与优化 ...