草庐IT

现代 CSS 高阶技巧,不规则边框解决方案

ChokCoco 2023-03-28 原文

本文是 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.movectx.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. ruby - capybara field.has_css?匹配器 - 2

    我在MiniTest::Spec和Capybara中使用以下规范:find_field('Email').must_have_css('[autofocus]')检查名为“电子邮件”的字段是否具有autofocus属性。doc说如下:has_css?(path,options={})ChecksifagivenCSSselectorisonthepageorcurrentnode.据我了解,字段“Email”是一个节点,因此调用must_have_css绝对有效!我做错了什么? 最佳答案 通过JonasNicklas得到了答案:No

  2. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  3. css - 用 watir 检查标签类? - 2

    我有一个div,它根据表单是否正确提交而改变。我想知道是否可以检查类的特定元素?开始元素看起来像这样。如果输入不正确,添加错误类。 最佳答案 试试这个:browser.div(:id=>"myerrortest").class_name更多信息:http://watir.github.com/watir-webdriver/doc/Watir/HTMLElement.html#class_name-instance_method另一种选择是只查看具有您期望的类的div是否存在browser.div((:id=>"myerrortes

  4. 动漫制作技巧如何制作动漫视频 - 2

    动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、

  5. Ruby 守护进程和 JRuby - 备选方案 - 2

    我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts

  6. ruby-on-rails - Assets 管道损坏 : Not compiling on the fly css and js files - 2

    我开始了一个新的Rails3.2.5项目,Assets管道不再工作了。CSS和Javascript文件不再编译。这是尝试生成Assets时日志的输出:StartedGET"/assets/application.css?body=1"for127.0.0.1at2012-06-1623:59:11-0700Servedasset/application.css-200OK(0ms)[2012-06-1623:59:11]ERRORNoMethodError:undefinedmethod`each'fornil:NilClass/Users/greg/.rbenv/versions/1

  7. python - 在 Ruby 中使用高阶函数 - 2

    在学习Python之后,我现在正在尝试学习Ruby,但我在将这段代码转换为Ruby时遇到了问题:defcompose1(f,g):"""Returnafunctionh,suchthath(x)=f(g(x))."""defh(x):returnf(g(x))returnh我必须使用block来翻译吗?或者Ruby中是否有类似的语法? 最佳答案 您可以使用Ruby中的lambda执行此操作(我在这里使用的是1.9stabby-lambda):compose=->(f,g){->(x){f.(g.(x))}}所以compose是一个返

  8. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  9. css - Rails 4.1 和 Bootstrap 3 字形图标不工作 - 2

    我正在尝试消除使用Bootstrap3的Rails4元素中的glyphicon错误。我没有使用任何Bootstrapgem将其添加到Assets管道中。我手动将bootstrap.css和bootstrap.js添加到各自的app/assets目录下,分别添加到application.css和application.js什么的我现在在网络浏览器的控制台中看到以下内容:GEThttp://localhost:3000/fonts/glyphicons-halflings-regular.woff404(NotFound)localhost/:1GEThttp://localhost:30

  10. css - rails 萨斯 : variables are not passed with @import - 2

    我有一个使用twitterbootstrap和sass的Rails元素。scss文件结构化到文件夹中,所以我有更好的概述。现在我想为包含我的颜色等的全局变量定义一个文件,并将这些值传递给其他文件,这样我就有更少的冗余代码。虽然所有代码都已正确导入和应用,变量不起作用。这是当前的设置:样式表/application.css.scss/**=require_self*=require_tree*//*stylesheets/||–base/||–_reset.scss#Reset/normalize||–_typography.scss#Typographyrules||–componen

随机推荐