使用 CSS Painting API

CSS Paint API 旨在让开发者以编程方式定义图像,然后这些图像可以在任何可以调用 CSS 图像的地方使用,例如 CSS 的 background-imageborder-imagemask-image 等。

要以编程方式创建 CSS 样式表使用的图像,我们需要经历几个步骤:

  1. 使用 registerPaint() 函数定义一个 paint worklet。
  2. 注册 worklet。
  3. 包含 paint() CSS 函数。

为了详细说明这些步骤,我们将从创建一个半高亮背景开始,就像这个标题一样。

Text reading 'My Cool Header' with a solid yellow background image block on the bottom left two thirds of the header

注意: 有关完整的演示以及 源代码,请参阅 CSS Painting API 示例

CSS Paint Worklet

在外部脚本文件中,我们使用 registerPaint() 函数来命名我们的 CSS Paint Worklet。它接受两个参数。第一个是我们给 worklet 起的名字——这个名字将在我们的 CSS 中用作 paint() 函数的参数,当我们想将此样式应用于某个元素时。第二个参数是实现所有魔力的类,它定义了上下文选项以及在将渲染到将成为我们图像的二维画布上。

js
registerPaint(
  "header-highlight",
  class {
    /*
     * define if alpha transparency is allowed alpha
     * is set to true by default. If set to false, all
     * colors used on the canvas will be fully opaque
     */
    static get contextOptions() {
      return { alpha: true };
    }

    /*
     * ctx is the 2D drawing context
     * a subset of the HTML Canvas API.
     */
    paint(ctx) {
      ctx.fillStyle = "hsl(55 90% 60% / 100%)";
      ctx.fillRect(0, 15, 200, 20); /* order: x, y, w, h */
    }
  },
);

在此类示例中,我们使用 contextOptions() 函数定义了一个上下文选项:我们返回了一个对象,说明允许 alpha 透明度。

然后,我们使用 paint() 函数在画布上进行绘制。

paint() 函数可以接受三个参数。在这里,我们提供了一个参数:渲染上下文(我们稍后会更详细地介绍),通常用变量名 ctx 来引用。2D Rendering ContextHTML Canvas API 的一个子集;Houdini 可用的版本(称为 PaintRenderingContext2D)是 Canvas API 的一个更小的子集,它包含了 Canvas API 的大部分功能,但 例外CanvasImageDataCanvasUserInterfaceCanvasTextCanvasTextDrawingStyles API。

我们将 fillStyle 定义为 hsl(55 90% 60% / 100%),这是一种黄色,然后调用 fillRect() 来创建一个该颜色的矩形。fillRect() 的参数按顺序是 x 轴原点、y 轴原点、宽度和高度。fillRect(0, 15, 200, 20) 创建了一个宽度为 200 个单位、高度为 20 个单位的矩形,该矩形距离内容框左侧 0 个单位,距离顶部 15 个单位。

我们可以使用 CSS 的 background-sizebackground-position 属性来重置此背景图像的大小或位置,但这只是我们在 paint worklet 中创建的黄色框的默认大小和位置。

我们尝试使示例保持简单。有关更多选项,请查看 <canvas> 文档。我们稍后也会在本教程中增加一些复杂性。

注册 Worklet

要使用 paint worklet,我们需要使用 addModule() 注册它,并在 CSS 中包含它,确保 CSS 选择器匹配我们 HTML 中的 DOM 节点。

我们的 paint worklet 的设置和设计是在上面显示的外部脚本中进行的。我们需要从主脚本中注册这个 worklet

js
CSS.paintWorklet.addModule("nameOfPaintWorkletFile.js");

这可以通过在主 HTML 的 <script> 或从文档链接的外部 JavaScript 文件中使用 paint worklet 的 addModule() 方法来完成。

使用 Paint Worklet

在我们的示例中,paint worklet 与主脚本文件一起存储。要使用它,我们首先注册它。

js
CSS.paintWorklet.addModule("header-highlight.js");

在 CSS 中引用 Paint Worklet

一旦我们注册了一个 paint worklet,我们就可以在 CSS 中使用它。像使用任何其他 <image> 类型一样使用 CSS paint() 函数,使用我们在 paint worklet 的 registerPaint() 函数中使用的相同字符串标识符。

css
.fancy {
  background-image: paint(header-highlight);
}

整合起来

然后,我们可以将 fancy 类添加到页面上的任何元素,以添加一个黄色框作为背景。

html
<h1 class="fancy">My Cool Header</h1>

支持 CSS Paint API 的浏览器中,下面的示例看起来会和上面的图片一样。

虽然你无法操作 worklet 的脚本,但你可以修改 background-sizebackground-position 来改变背景图像的大小和位置。

PaintSize

在上面的示例中,我们创建了一个 20x200 单位的框,距离元素顶部 15 个单位绘制,无论元素大小如何,它都是相同的。如果文本很小,黄色框看起来就像一个巨大的下划线。如果文本很大,这个框看起来可能像第一个三个字母上方的条形。如果背景图像相对于元素的大小是成比例的,那就更好了——我们可以使用元素的 paintSize 属性来确保背景图像与元素盒模型大小成比例。

The background is 50% of the height and 60% of the width of the element

在上图中,背景与元素的大小成比例。第三个示例在块级元素上设置了 width: 50%,这使得元素变窄,因此背景图像也变窄。

Paint Worklet

实现此目的的代码如下所示:

js
registerPaint(
  "header-highlight",
  class {
    static get contextOptions() {
      return { alpha: true };
    }

    /*
     * ctx is the 2D drawing context
     * size is the paintSize, the dimensions (height and width) of the box being painted
     */
    paint(ctx, size) {
      ctx.fillStyle = "hsl(55 90% 60% / 100%)";
      ctx.fillRect(0, size.height / 3, size.width * 0.4, size.height * 0.6);
    }
  },
);

此代码示例与我们的第一个示例有两处不同:

  1. 我们包含了一个第二个参数,即 paint size。
  2. 我们将矩形的尺寸和位置改成了相对于元素盒大小的值,而不是绝对值。

我们可以将第二个参数传递给 paint() 函数,以便通过 .width.height 属性访问元素的宽度和高度。

我们的标题现在有了一个高亮,该高亮会根据其大小而变化。

使用 Paint Worklet

HTML

html
<h1 class="fancy">Largest Header</h1>
<h6 class="fancy">Smallest Header</h6>
<h3 class="fancy half">50% width header</h3>

CSS

虽然你无法操作 worklet 的脚本,但你可以修改元素的 font-sizewidth 来改变背景图像的大小。

css
.fancy {
  background-image: paint(header-highlight);
}
.half {
  width: 50%;
}

JavaScript

js
CSS.paintWorklet.addModule("header-highlight.js");

结果

支持 CSS Paint API 的浏览器中,下面示例中的元素应该获得与其字体大小成比例的黄色背景。

自定义属性

除了访问元素的大小之外,worklet 还可以访问 CSS 自定义属性和常规 CSS 属性。

js
registerPaint(
  "cssPaintFunctionName",
  class {
    static get inputProperties() {
      return ["PropertyName1", "--customPropertyName2"];
    }
    static get inputArguments() {
      return ["<color>"];
    }
    static get contextOptions() {
      return { alpha: true };
    }

    paint(drawingContext, elementSize, styleMap) {
      // Paint code goes here.
    }
  },
);

paint() 函数的三个参数包括绘制上下文、paint size 和属性。为了能够访问属性,我们包含了静态 inputProperties() 方法,它提供了对 CSS 属性的实时访问,包括常规属性和 自定义属性,并返回一个属性名称的 数组。我们将在最后一个部分探讨 inputArguments

让我们创建一个带有背景图像的列表项,该图像在三种不同的颜色和三种宽度之间旋转。

The width and color of the background image changes based on the custom properties

为了实现这一点,我们将定义两个自定义 CSS 属性:--box-color--width-subtractor

Paint Worklet

在我们的 worklet 中,我们可以引用这些自定义属性。

js
registerPaint(
  "boxbg",
  class {
    static get contextOptions() {
      return { alpha: true };
    }

    /*
     * use this function to retrieve any custom properties (or regular properties, such as 'height')
     * defined for the element, return them in the specified array
     */
    static get inputProperties() {
      return ["--box-color", "--width-subtractor"];
    }

    paint(ctx, size, props) {
      /*
       * ctx -> drawing context
       * size -> paintSize: width and height
       * props -> properties: get() method
       */
      ctx.fillStyle = props.get("--box-color");
      ctx.fillRect(
        0,
        size.height / 3,
        size.width * 0.4 - props.get("--width-subtractor"),
        size.height * 0.6,
      );
    }
  },
);

我们在 registerPaint() 类中使用了 inputProperties() 方法来获取应用于具有 boxbg 类的元素的两个自定义属性的值,然后将这些值用于我们的 paint() 函数。inputProperties() 方法可以返回影响元素的全部属性,而不仅仅是自定义属性。

使用 Paint Worklet

HTML

html
<ul>
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
  <li>item 6</li>
  <li>item 7</li>
  <li>item 8</li>
  <li>item 9</li>
  <li>item 10</li>
  <li>item 11</li>
  <li>item 12</li>
  <li>item 13</li>
  <li>item 14</li>
  <li>item 15</li>
  <li>item 16</li>
  <li>item 17</li>
  <li>item</li>
</ul>

CSS

在我们的 CSS 中,我们定义了 --box-color--width-subtractor 自定义属性。

css
li {
  background-image: paint(boxbg);
  --box-color: hsl(55 90% 60% / 100%);
}

li:nth-of-type(3n) {
  --box-color: hsl(155 90% 60% / 100%);
  --width-subtractor: 20;
}

li:nth-of-type(3n + 1) {
  --box-color: hsl(255 90% 60% / 100%);
  --width-subtractor: 40;
}

JavaScript

在我们的 <script> 中,我们注册了 worklet。

js
CSS.paintWorklet.addModule("boxbg.js");

结果

虽然你无法操作 worklet 的脚本,但你可以在 DevTools 中修改自定义属性值来改变背景图像的颜色和宽度。

增加复杂性

上面的示例可能看起来不太令人兴奋,因为你可以用现有的 CSS 属性以几种不同的方式重现它们,例如,通过定位一些装饰性的 生成内容::before,或者包含 background: linear-gradient(yellow, yellow) 0 15px / 200px 20px no-repeat; CSS Paint API 之所以如此有趣和强大,是因为你可以创建复杂的图像,传递变量,并自动调整大小。

让我们来看一个更复杂的 paint 示例。

Paint Worklet

js
registerPaint(
  "header-highlight",
  class {
    static get inputProperties() {
      return ["--high-color"];
    }
    static get contextOptions() {
      return { alpha: true };
    }

    paint(ctx, size, props) {
      /* set where to start the highlight & dimensions */
      const x = 0;
      const y = size.height * 0.3;
      const blockWidth = size.width * 0.33;
      const highlightHeight = size.height * 0.85;
      const color = props.get("--high-color");

      ctx.fillStyle = color;

      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(blockWidth, y);
      ctx.lineTo(blockWidth + highlightHeight, highlightHeight);
      ctx.lineTo(x, highlightHeight);
      ctx.lineTo(x, y);
      ctx.closePath();
      ctx.fill();

      /* create the dashes */
      for (let start = 0; start < 8; start += 2) {
        ctx.beginPath();
        ctx.moveTo(blockWidth + start * 10 + 10, y);
        ctx.lineTo(blockWidth + start * 10 + 20, y);
        ctx.lineTo(
          blockWidth + start * 10 + 20 + highlightHeight,
          highlightHeight,
        );
        ctx.lineTo(
          blockWidth + start * 10 + 10 + highlightHeight,
          highlightHeight,
        );
        ctx.lineTo(blockWidth + start * 10 + 10, y);
        ctx.closePath();
        ctx.fill();
      }
    } // paint
  },
);

使用 Paint Worklet

然后,我们可以创建一个简单的 HTML 来接受此图像作为背景。

html
<h1 class="fancy">Largest Header</h1>
<h3 class="fancy">Medium size header</h3>
<h6 class="fancy">Smallest Header</h6>

我们为每个标题提供了不同的 --high-color 自定义属性值。

css
.fancy {
  background-image: paint(header-highlight);
}
h1 {
  --high-color: hsl(155 90% 60% / 70%);
}
h3 {
  --high-color: hsl(255 90% 60% / 50%);
}
h6 {
  --high-color: hsl(355 90% 60% / 30%);
}

然后我们注册我们的 worklet。

js
CSS.paintWorklet.addModule("header-highlight.js");

结果如下所示

虽然你无法编辑 worklet 本身,但你可以尝试修改 CSS 和 HTML。也许可以尝试在标题上使用 scalerotate

你可以尝试不使用 CSS Paint API 来创建上面的背景图像。这是可行的,但你必须为每个想要的颜色声明一个不同且相当复杂的线性渐变。使用 CSS Paint API,一个 worklet 可以被重用,例如在这里传入不同的颜色。

传递参数

注意: 以下示例需要启用 Chrome 或 Edge 中的实验性 Web Platform 功能标志,方法是访问 about://flags

使用 CSS Paint API,我们不仅可以访问自定义属性和常规属性,还可以将自定义参数传递给 paint() 函数。

我们可以在 CSS 中调用函数时添加这些额外的参数。假设我们有时想描边背景而不是填充它——让我们为此场合传递一个额外的参数。

css
li {
  background-image: paint(hollow-highlights, stroke);
}

现在,我们可以在 registerPaint() 类中使用 inputArguments() 方法来访问我们添加到 paint() 函数中的自定义参数。

js
class Worklet {
  static get inputArguments() {
    return ["*"];
  }
  // …
}

然后我们就可以访问该参数了。

js
class Worklet {
  // …
  paint(ctx, size, props, args) {
    // use our custom arguments
    const hasStroke = args[0].toString();

    // if stroke arg is 'stroke', don't fill
    if (hasStroke === "stroke") {
      ctx.fillStyle = "transparent";
      ctx.strokeStyle = color;
    }
    // …
  }
  // …
}

我们还可以指定我们想要的参数类型。

假设我们添加第二个参数,指定我们想要的描边像素宽度。

css
li {
  background-image: paint(hollow-highlights, stroke, 10px);
}

当我们 get 我们的参数列表时,我们可以专门要求一个 <length> 单位。

js
class Worklet {
  // …
  static get inputArguments() {
    return ["*", "<length>"];
  }
  // …
}

在这种情况下,我们专门请求了 <length> 属性。返回数组的第一个元素将是 CSSUnparsedValue。第二个将是 CSSStyleValue

如果自定义参数是 CSS 值,例如单位,我们可以通过在 registerPaint() 函数中检索它时使用值类型关键字来调用 Typed OM CSSStyleValue 类(及其子类)。

现在我们可以访问 type 和 value 属性,这意味着我们可以直接获得像素数和数字类型。(诚然,ctx.lineWidth 接受一个浮点数作为值而不是带有长度单位的值,但仅作示例……)

js
class Worklet {
  // …
  paint(ctx, size, props, args) {
    const strokeWidth = args[1];

    if (strokeWidth.unit === "px") {
      ctx.lineWidth = strokeWidth.value;
    } else {
      ctx.lineWidth = 1.0;
    }

    // …
  }
  // …
}

值得注意的是,使用自定义属性来控制 worklet 的不同部分与此处设置的参数之间的区别。自定义属性(实际上是样式图上的任何属性)是全局的——它们也可以在我们的 CSS(和 JS)的其他地方使用。

例如,你可能有一个 --main-color,它对于在 paint() 函数中设置颜色很有用,但也可以用于在 CSS 的其他地方设置颜色。如果你想专门更改它以用于 paint,可能会很困难。这就是自定义参数功能派上用场的地方。另一种思考方式是,参数用于控制你实际绘制的内容,而属性用于控制样式。

The list items have a background image that is either pink, purple or green, with different stroke widths, and the green one being filled.

现在我们可以真正看到这个 API 的优势了,如果我们能够通过自定义属性和额外的 paint() 函数参数从 CSS 控制大量绘制参数,那么我们就可以真正开始构建可重用且高度可控的样式函数了。

Paint Worklet

js
registerPaint(
  "hollow-highlights",
  class {
    static get inputProperties() {
      return ["--box-color"];
    }
    // Input arguments that can be passed to the `paint` function
    static get inputArguments() {
      return ["*", "<length>"];
    }

    static get contextOptions() {
      return { alpha: true };
    }

    paint(ctx, size, props, args) {
      // ctx   -> drawing context
      // size  -> size of the box being painted
      // props -> list of custom properties available to the element
      // args  -> list of arguments set when calling the paint() function in the CSS

      // where to start the highlight & dimensions
      const x = 0;
      const y = size.height * 0.3;
      const blockWidth = size.width * 0.33;
      const blockHeight = size.height * 0.85;

      // the values passed in the paint() function in the CSS
      const color = props.get("--box-color");
      const strokeType = args[0].toString();
      const strokeWidth = parseInt(args[1], 10);

      // set the stroke width
      ctx.lineWidth = strokeWidth ?? 1.0;
      // set the fill type
      if (strokeType === "stroke") {
        ctx.fillStyle = "transparent";
        ctx.strokeStyle = color;
      } else if (strokeType === "filled") {
        ctx.fillStyle = color;
        ctx.strokeStyle = color;
      } else {
        ctx.fillStyle = "none";
        ctx.strokeStyle = "none";
      }

      // block
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(blockWidth, y);
      ctx.lineTo(blockWidth + blockHeight, blockHeight);
      ctx.lineTo(x, blockHeight);
      ctx.lineTo(x, y);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
      // dashes
      for (let i = 0; i < 4; i++) {
        let start = i * 2;
        ctx.beginPath();
        ctx.moveTo(blockWidth + start * 10 + 10, y);
        ctx.lineTo(blockWidth + start * 10 + 20, y);
        ctx.lineTo(blockWidth + start * 10 + 20 + blockHeight, blockHeight);
        ctx.lineTo(blockWidth + start * 10 + 10 + blockHeight, blockHeight);
        ctx.lineTo(blockWidth + start * 10 + 10, y);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
      }
    } // paint
  },
);

使用 Paint Worklet

我们可以设置不同的颜色、描边宽度,并选择背景图像是填充还是镂空。

css
li {
  --box-color: hsl(155 90% 60% / 50%);
  background-image: paint(hollow-highlights, stroke, 5px);
}

li:nth-of-type(3n) {
  --box-color: hsl(255 90% 60% / 50%);
  background-image: paint(hollow-highlights, filled, 3px);
}

li:nth-of-type(3n + 1) {
  --box-color: hsl(355 90% 60% / 50%);
  background-image: paint(hollow-highlights, stroke, 1px);
}

在我们的 <script> 中,我们注册了 worklet。

js
CSS.paintWorklet.addModule("hollow.js");

另见