9
头图

前言

在这之前我写过这样一篇文章教你用canvas打造一个炫酷的碎片切图效果,这个效果是用Canvas来实现的,现在想着尝试使用CSS Paint API来实现一个类似的碎片效果。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~

简单介绍下CSS Paint API

CSS Paint API是Houdini项目的一部分。它允许我们使用自己的功能扩展CSS,所以我们可以不再需要等待新功能的发布,完全可以自己实现想要的新功能,可以说CSS Paint API就是CSS的未来。Houdini是一组底层API,它暴露一部分CSS引擎给开发者介入浏览器渲染与布局的能力,从而能够扩展CSS。

W3C上关于CSS Paint API的介绍是这样的:

CSS Paint API 允许开发者使用 JavaScript 创建对样式与大小变化自适应的 CSS 图像。这种方式创建的图像,可以通过调用 paint() 函数,应用在诸如 background-imageborder-imagemask-image 等属性上。

有了这个API,我们在CSS中可以绘制任意自己想要的图案了,是不是泰裤辣!

使用

使用流程具体可以按照这张图来即可:

paint1.png

  1. 创建一个 js 文件,使用 registerPaint() 方法注册一个 PaintWorklet
  2. 调用 addModule() 方法添加此模块
  3. 在 CSS 中使用 paint() 函数调用

更具体的介绍可以在MDN上查看,了解完大概的使用流程我们就可以开始尝试实现我们的图像碎片效果了

开始制作

初步绘制

首先我们使用Paint API将我们的图片绘制成为一半不透明一半透明的效果:

<template>
  <div :class="$style.paint_container">
    <img src="/public/3.jpg" :class="$style.paint_img1" />
  </div>
</template>

<script lang="ts" setup>
import { onMounted } from "vue";
// import paintSuipian from "../../utils/paintSuipian";

const paint = () => {
  if (CSS.paintWorklet) {
    CSS.paintWorklet.addModule("/src/utils/paintSuipian1.js");
  }
};

onMounted(() => {
  paint();
});
</script>
<style lang="scss" module>
.paint_img1 {
  width: 400px;
  height: auto;
  --m: 12;
  --n: 12;
  -webkit-mask: paint(suipian1);
}
</style>
// paintSuipian1.js
registerPaint(
  "suipian1",
  class {
    paint(ctx, size) {
      console.log("ctx:", ctx, "size:", size);
      /* left */
      ctx.fillStyle = "rgba(0,0,0,1)";
      ctx.fillRect(0, 0, size.width / 2, size.height);
      /* right */
      ctx.fillStyle = "rgba(0,0,0,0.5)";
      ctx.fillRect(size.width / 2, 0, size.width / 2, size.height);
    }
  }
);

从这里来看,paint内部的一些操作API其实与我们熟悉的Canvas有点类似,ctx就是当前的绘制上下文,而size则是应用这个paint元素的宽高大小。

所以从这里看上去理解并不难,与Canvas差不多,通过fillRect绘制矩形,然后通过fillStyle填充染色,我们就能看到下面这个效果:

paint2.png

看到这个效果我们是不是就能够想象到之前的那种碎片效果应该怎样去实现呢?

这上面其实就是绘制了两个矩形,分别填充了不同的透明度,那么我们如果绘制多一点的矩形也让它填充不同的透明度会是什么样子呢,大家可以想象一下🤔...

稍微加工

为了更加实用,我们可以通过定义CSS变量来达到控制碎片密度的效果。

// paintSuipian1.js
registerPaint(
  "suipian1",
  class {
    static get inputProperties() {
      return ["--m", "--n"];
    }
    paint(ctx, size, properties) {
      console.log("ctx:", ctx, "size:", size);
      const m = properties.get("--m");
      const n = properties.get("--n");

      const w = size.width / m;
      const h = size.height / n;

      for (var i = 0; i < n; i++) {
        for (var j = 0; j < m; j++) {
          ctx.fillStyle = "rgba(0,0,0," + Math.random() + ")";
          ctx.fillRect(i * w, j * h, w, h);
        }
      }
    }
  }
);

此时我们的js改的稍微复杂了一点,其实也还好,只是定义了一下矩形矩阵的维度,然后计算了没个碎片的宽高,再经过for循环进行绘制。

这里我们可以通过get inputProperties来获取我们CSS中定义的变量,需要注意的是这里的获取的变量也是有作用域的,它只能获取到自己身定义的变量或者是它继承而来的CSS变量

.paint_img1 {
  width: 400px;
  height: auto;
  --m: 12;
  --n: 12;
  -webkit-mask: paint(suipian1);
}

现在这张图片会变成什么样呢,我们来看一下:

paint3.png

这样一看是不是有点感觉了

我们可以通过控制CSS变量来实现不同的效果,比如:

.paint_img1 {
  width: 400px;
  height: auto;
  --m: 22;
  --n: 22;
  -webkit-mask: paint(suipian1);
}

paint4.png

添加动画

到这了,整个碎片的轮廓大概是有了,现在需要做的是怎么为这些碎片加上动画?

这里有个技巧是我们可以通过使用@property来自定义一个属性,这个属性其实是一个Number值,而这个值又恰好是我们每个碎片计算随机透明度需要用的值,然后我们再使用transition再对这个自定义属性进行过渡。

OK,我们来尝试一下:

// paintSuipian1.js
registerPaint(
  "suipian1",
  class {
    static get inputProperties() {
      return ["--m", "--n", "--f"];
    }
    paint(ctx, size, properties) {
      console.log("ctx:", ctx, "size:", size);
      const m = properties.get("--m");
      const n = properties.get("--n");
      const f = properties.get("--f");

      const w = size.width / m;
      const h = size.height / n;

      for (var i = 0; i < n; i++) {
        for (var j = 0; j < m; j++) {
          ctx.fillStyle = "rgba(0,0,0," + f + ")";
          ctx.fillRect(i * w - 0.5, j * h - 0.5, w + 0.5, h + 0.5);
        }
      }
    }
  }
);
.paint_img1 {
  width: 400px;
  height: auto;
  --m: 10;
  --n: 10;
  --f: 1;
  -webkit-mask: paint(suipian1);
  transition: --f 1s;
}
.paint_img1:hover {
  --f: 0;
}

看看效果:

paint5.gif

这里看着有点怪怪的,因为所有的碎片都会同时隐藏出现,并没有达到我们想要的效果,所以这里我们还得想办法防止所有的碎片同时显示隐藏🤔

最后优化

这里其实需要通过一种算法来让每个碎片的不透明度达到一个随机的效果。

// paintSuipian.js
registerPaint(
  "suipian",
  class {
    static get inputProperties() {
      return ["--color", "--m", "--n", "--f"];
    }
    paint(ctx, size, properties) {
      console.log("ctx:", ctx, "size:", size, properties.get("--color"));
      const m = properties.get("--m");
      const n = properties.get("--n");
      const f = properties.get("--f");
      const w = size.width / m;
      const h = size.height / n;
      const l = 10;

      const mask = 0xffffffff;
      const seed = 30;
      let m_w = (123456789 + seed) & mask;
      let m_z = (987654321 - seed) & mask;
            // 随机算法
      const random = function () {
        m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
        m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
        let result = ((m_z << 16) + (m_w & 65535)) >>> 0;
        result /= 4294967296;
        return result;
      };

      for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
          ctx.fillStyle =
            "rgba(0,0,0," + (random() * (l - 1) + 1 - (1 - f) * l) + ")";
          ctx.fillRect(i * w - 1, j * h - 1, w + 1, h + 1);
        }
      }
    }
  }
);
.paint_img {
  width: 400px;
  height: auto;
  --color: rgba(255, 255, 255, 1);
  --m: 10;
  --n: 10;
  --f: 1;
  -webkit-mask: paint(suipian);
  transition: --f 1s;
}
.paint_img:hover {
  --f: 0;
}

最后的效果:

paint6.gif

欢迎大家关注公众号 「前端南玖」

我是南玖,我们下期见!!!


南玖
1.1k 声望109 粉丝