本文是 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 就不那么好实现了,像是这样:

在过往,有两种相对还不错的方式,去实现这样一个不规则图形的边框:

  1. 借助 filter,利用多重 drop-shadow()
  2. 借助 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 滤镜代码:

  1. <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="1"></feMorphology> 将原图的不透明部分作为输入,采用了 dilate 扩张模式且程度为 radius="1",生成了一个比原图大 1px 的黑色图块
  2. 使用 feMerge 将黑色图块和原图叠加在一起
  3. 可以通过控制滤镜中的 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() 方法的解释。

  1. 首先我们,通过 properties.get("--clipPath"),我们能够拿到传入的 --clipPath 参数
  2. 通过 spilt() 方法,将 --clipPath 分成一段段,也就是我们的图形实际的绘制步骤
  3. 这里有一点非常重要,也就是 parseClipPath() 方法,由于我们的 -clipPath 的每一段可能是 100% 50% 这样的构造,但是实际在绘图的过程中,我们需要的实际坐标的绝对值,譬如在一个 100 x 100 的画布上,我们需要将 50% 50% 的百分比坐标,转化为实际的 50 50 这样的绝对值
  4. 在理解了 parseClipPath() 后,剩下的就都非常好理解了,我们通过 ctx.beginPath()、ctx.move、ctx.lineTo 以及 ctx.closePath() 将整个 --clipPath 的图形绘制出来
  5. 最后,利用 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)) 剪切了自身,同时,我们借助了一个伪元素,利用这个伪元素去实现具体的边框效果。

这里其实用了一种内外切割的思想,去实现的边框效果:

  1. 利用父元素的 clip-path: polygon(var(--clipPath)) 剪切掉外围的图形
  2. 利用给伪元素的 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 高阶技巧,不规则边框解决方案的更多相关文章

  1. css高級技巧

    1.鼠標顯示 a:小手cursor:pointer b:默認cursor:default c:勾選文本cursor:text d:拖動cursor:move 2.css三種佈局模型 a.流動模型(默認 ...

  2. 数独高阶技巧之八——SDC

    在本系列的第四篇“简单异数链”中,向大家介绍了XY-Wing等一系列Wing类技巧,并提到可以用(拐弯的)数组的观念来理解这些结构,经过第六篇ALS的学习之后,大家回过头再去看Wing,应该可以发现相 ...

  3. 数独高阶技巧入门之六——ALS

    ​在这个系列的第一篇(链及其简单应用)以及第四篇(简单异数链)中已经简单介绍过ALS结构的定义,即n格中存在n+1个不同的候选数 (双值格可视为特殊的ALS结构) .根据数独规则,在组成ALS的候选数 ...

  4. 数独高阶技巧入门之七——AIC & Nice Loop

    AIC(交替推理链,Alternate Inference Chain) 在简单异数链一文中我们介绍过XY-Chain技法,AIC可以看作是XY-Chain的扩展.有别于XY-Chain仅局限于双值格 ...

  5. 数独高阶技巧入门之三——Fish

    术语Fish代表了一组工作原理相同的关于特定候选数的解题技巧(Fish技巧直接产生自数独规则——每个单元内的数字都不能重复),Fish家族成员包括“体型”从小到大的X-Wing.Swordfish. ...

  6. 读《实战 GUI 产品的自动化测试》之:第四步,高阶技巧

    转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting4/ 定义测试控件库 本系列前几篇文章对 IBM 框架做了介绍, ...

  7. 高阶 CSS 技巧在复杂动效中的应用

    最近我在 CodePen 上看到了这样一个有意思的动画: 整个动画效果是在一个标签内,借助了 SVG PATH 实现.其核心在于对渐变(Gradient)的究极利用. 完整的代码你可以看看这里 -- ...

  8. css border 三角形阴影(不规则图形阴影) & 多重边框的制作

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! border 的组合写法 border:border-width border-style border- ...

  9. 《前端之路》之 JavaScript 进阶技巧之高阶函数(下)

    目录 第二章 - 03: 前端 进阶技巧之高阶函数 一.防篡改对象 1-1:Configurable 和 Writable 1-2:Enumerable 1-3:get .set 2-1:不可扩展对象 ...

  10. iOS开发小技巧 -- tableView-section圆角边框解决方案

    [iOS开发]tableView-section圆角边框解决方案 tableView圆角边框解决方案 iOS 7之前,图下圆角边框很容易设置 iOS 7之后,tableviewcell的风格不再是圆角 ...

随机推荐

  1. 2>&1到底是什么意思?

    java -jar snapshot.jar > snapshot.log 2>&1 & 写Java的朋友一定对上面的命令很熟悉,相信大部分人都知道>表示的是重定向, ...

  2. 用户输入学号,如果是以aabcddef开头,并且后边是4位数字,前两位大于06小于等于当前年份。判断用户输入是否合法

    package seven_topic; import java.util.*; public class p_19_1 { public static void main(String[] args ...

  3. 关于aws cli命令的exit/return code分析

    最近总是收到一个备份脚本的失败邮件,脚本是之前同事写的,没有加入任何有调试信息,及有用的日志 于是去分析 ,脚本中有一条 aws s3 sync $srclocal  $dsts3 命令,然后根据这条 ...

  4. CEOI2020 道路(Roads) Solution

    直接来构造. 考虑扫描线.从左到右扫,考虑当前扫到了一个左端点,我们把这个左端点连到其他点上. 我们可以找到这个点下方离他最近的线段,并且记下每条线段上方在扫描线左侧且最靠右,与这条线段中间没有其他线 ...

  5. 2021东华杯misc project

    ​ project 题目附件发现是工程文件,按日期排序只有一个新的exe文件,那考点肯定就在这了 ​编辑 运行exe生成了一个zip ​编辑 打开解压缩的文件发现有三部分编码 base64 quote ...

  6. C语言------选择结构

    仅供借鉴.仅供借鉴.仅供借鉴(整理了一下大一C语言每个章节的练习题.没得题目.只有程序了) 文章目录 1 .实训名称 2 .实训目的及要求 3 .源代码及运行截图 4.小结 1 .实训名称 实训4:选 ...

  7. 【JavaWeb】学习笔记——JSP

    概念 全称:Java Server Pages, Java服务端页面 描述:一种动态的网页技术,可以在其中定义HTML.JS.CSS等静态内容,以及Java代码的动态内容 说明:JSP = HTML ...

  8. Java获取/resources目录下的资源文件方法

    Web项目开发中,经常会有一些静态资源,被放置在resources目录下,随项目打包在一起,代码中要使用的时候,通过文件读取的方式,加载并使用: 今天总结整理了九种方式获取resources目录下文件 ...

  9. Sublime Text - Linux Package Manager Repositories

    Linux Package Manager Repositories http://www.sublimetext.com/docs/linux_repositories.html Sublime T ...

  10. Nginx配置-1

    1.绑定nginx到指定cpu [root@nginx conf.d]# vim /apps/nginx/conf/nginx.conf worker_processes 2; worker_cpu_ ...