使用 CSS 绘制 API
该 CSS 绘制 API 旨在使开发人员能够以编程方式定义图像,然后可以在任何可以使用 CSS 图像的地方调用这些图像,例如 CSS
background-image
、border-image
、mask-image
等。
要以编程方式创建 CSS 样式表使用的图像,我们需要完成以下几个步骤
- 使用
registerPaint()
函数定义绘制工作线程 - 注册工作线程
- 包含
CSS 函数paint()
为了详细说明这些步骤,我们将从创建半高亮背景开始,就像此标题上的背景一样
注意:本文中所有示例的完整源代码可在 https://github.com/mdn/dom-examples/tree/main/css-painting 找到,示例可在 https://mdn.github.io/dom-examples/css-painting/ 中实时运行。
CSS 绘制工作线程
在外部脚本文件中,我们使用 registerPaint()
函数为我们的 CSS 绘制工作线程 命名。它接受两个参数。第一个是我们为工作线程提供的名称 - 这是我们在 CSS 中用作 paint()
函数参数的名称,当我们想要将此样式应用于元素时。第二个参数是执行所有操作的类,定义上下文选项以及要绘制到二维画布上的内容,该画布将成为我们的图像。
registerPaint(
"headerHighlight",
class {
/*
define if alphatransparency 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
表示。二维渲染上下文是 HTML Canvas API 的一个子集;Houdini 可用的版本(称为 PaintRenderingContext2D
)是包含完整 Canvas API 中大多数功能的另一个子集,例外 是 CanvasImageData
、CanvasUserInterface
、CanvasText
和 CanvasTextDrawingStyles
API。
我们将 fillStyle
定义为 hsl(55 90% 60% / 100%)
,这是一种黄色阴影,然后调用 fillRect()
创建该颜色的矩形。fillRect()
参数依次为 x 轴原点、y 轴原点、宽度和高度。fillRect(0, 15, 200, 20)
会创建一个宽度为 200 个单位、高度为 20 个单位的矩形,该矩形位于内容框左侧 0 个单位和顶部 15 个单位的位置。
我们可以使用 CSS background-size
和 background-position
属性来调整此背景图像的大小或重新定位它,但这是我们在绘制工作线程中创建的黄色框的默认大小和位置。
我们试图使示例保持简单。有关更多选项,请查看 画布文档。我们稍后在本教程中还会增加一些复杂性。
注册工作线程
要使用绘制工作线程,我们需要使用 addModule()
注册它,并将其包含在我们的 CSS 中,确保 CSS 选择器与 HTML 中的 DOM 节点匹配
绘制工作线程的设置和设计发生在上面显示的外部脚本中。我们需要从我们的主脚本中注册该 工作线程。
CSS.paintWorklet.addModule("nameOfPaintWorkletFile.js");
这可以通过在主 HTML 中的 <script>
或从文档链接的外部 JavaScript 文件中使用绘制工作线程的 addModule()
方法来完成。
使用绘制工作线程
在我们的示例中,绘制工作线程存储在主脚本文件旁边。要使用它,我们首先注册它
CSS.paintWorklet.addModule("header-highlight.js");
在 CSS 中引用绘制工作线程
注册绘制工作线程后,我们可以在 CSS 中使用它。像使用任何其他 <image>
类型一样使用 CSS paint()
函数,使用与绘制工作线程的 registerPaint()
函数中使用的相同字符串标识符。
.fancy {
background-image: paint(headerHighlight);
}
整合在一起
然后,我们可以将花哨的类添加到页面上的任何元素,以添加黄色框作为背景
<h1 class="fancy">My Cool Header</h1>
以下示例在 支持 CSS 绘制 API 的浏览器 中将看起来像上图。
虽然您无法使用工作线程的脚本,但您可以更改 background-size
和 background-position
以更改背景图像的大小和位置。
PaintSize
在上面的示例中,我们创建了一个 20x200 个单位的框,绘制在距其所在元素顶部 15 个单位的位置,无论元素的大小如何,它都保持不变。如果文本很小,黄色框看起来像一个巨大的下划线。如果文本很大,该框可能看起来像前三个字母上方的横条。如果背景图像相对于元素的大小,效果会更好 - 我们可以使用元素的 paintSize
属性来确保背景图像与元素的盒子模型大小成比例。
在上图中,背景与元素的大小成比例。第三个示例在块级元素上设置了 width: 50%
;使元素变窄,从而使背景图像变窄。
绘制工作线程
执行此操作的代码如下所示
registerPaint(
"headerHighlight",
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);
}
},
);
此代码示例与我们的第一个示例有两个区别
- 我们包含了第二个参数,即绘制大小。
- 我们将矩形的大小和位置更改为相对于元素框的大小,而不是绝对值。
我们可以将第二个参数传递到 paint()
函数中,以便通过 .width
和 .height
属性访问元素的宽度和高度。
我们的标题现在有一个根据其大小变化的高亮显示。
使用绘制工作线程
HTML
<h1 class="fancy">Largest Header</h1>
<h6 class="fancy">Smallest Header</h6>
<h3 class="fancy half">50% width header</h3>
CSS
虽然您无法使用工作线程的脚本,但您可以更改元素的 font-size
和 width
以更改背景图像的大小。
.fancy {
background-image: paint(headerHighlight);
}
.half {
width: 50%;
}
JavaScript
CSS.paintWorklet.addModule("header-highlight.js");
结果
在 支持 CSS 绘制 API 的浏览器 中,以下示例中的元素应获得与其字体大小成比例的黄色背景。
自定义属性
除了访问元素的大小之外,工作线程还可以访问 CSS 自定义属性和常规 CSS 属性。
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()
函数的三个参数包括绘图上下文、绘制大小和属性。为了能够访问属性,我们包含了静态 inputProperties()
方法,该方法提供了对 CSS 属性(包括常规属性和 自定义属性)的实时访问,并返回一个 数组
属性名称。我们将在最后一节中查看 inputArguments
。
让我们创建一个项目列表,其背景图像在三种不同的颜色和三种宽度之间旋转。
为此,我们将定义两个自定义 CSS 属性,--boxColor
和 --widthSubtractor
。
绘制工作线程
在我们的工作线程中,我们可以引用这些自定义属性。
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 ["--boxColor", "--widthSubtractor"];
}
paint(ctx, size, props) {
/*
ctx -> drawing context
size -> paintSize: width and height
props -> properties: get() method
*/
ctx.fillStyle = props.get("--boxColor");
ctx.fillRect(
0,
size.height / 3,
size.width * 0.4 - props.get("--widthSubtractor"),
size.height * 0.6,
);
}
},
);
我们在 registerPaint()
类中使用了 inputProperties()
方法来获取应用了 boxbg
的元素上设置的两个自定义属性的值,然后在我们的 paint()
函数中使用它们。inputProperties()
方法可以返回影响元素的所有属性,而不仅仅是自定义属性。
使用绘制工作线程
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 中,我们定义了 --boxColor
和 --widthSubtractor
自定义属性。
li {
background-image: paint(boxbg);
--boxColor: hsl(55 90% 60% / 100%);
}
li:nth-of-type(3n) {
--boxColor: hsl(155 90% 60% / 100%);
--widthSubtractor: 20;
}
li:nth-of-type(3n + 1) {
--boxColor: hsl(255 90% 60% / 100%);
--widthSubtractor: 40;
}
JavaScript
在我们的 <script>
中,我们注册了工作线程
CSS.paintWorklet.addModule("boxbg.js");
结果
虽然您无法使用工作线程的脚本,但您可以在 DevTools 中更改自定义属性值以更改背景图像的颜色和宽度。
增加复杂度
以上示例可能看起来并不令人兴奋,因为您可以使用现有的 CSS 属性以多种不同的方式重新创建它们,例如,通过使用 ::before,
定位一些装饰性的 生成内容,或包含 background: linear-gradient(yellow, yellow) 0 15px / 200px 20px no-repeat;
使 CSS 绘制 API 如此有趣和强大的原因在于,您可以创建复杂的图像,传递变量,这些变量会自动调整大小。
让我们来看一个更复杂的绘制示例。
绘制工作线程
registerPaint(
"headerHighlight",
class {
static get inputProperties() {
return ["--highColor"];
}
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("--highColor");
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
},
);
使用绘制工作线程
然后,我们可以创建一个可以接受此图像作为背景的小 HTML
<h1 class="fancy">Largest Header</h1>
<h3 class="fancy">Medium size header</h3>
<h6 class="fancy">Smallest Header</h6>
我们为每个标题提供不同的 --highColor
自定义属性 值
.fancy {
background-image: paint(headerHighlight);
}
h1 {
--highColor: hsl(155 90% 60% / 70%);
}
h3 {
--highColor: hsl(255 90% 60% / 50%);
}
h6 {
--highColor: hsl(355 90% 60% / 30%);
}
并且我们注册了我们的工作线程
CSS.paintWorklet.addModule("header-highlight.js");
结果如下所示
虽然您无法编辑工作线程本身,但您可以使用 CSS 和 HTML。也许可以尝试在标题上使用 float
和 clear
?
您可以尝试在没有 CSS 绘制 API 的情况下创建上述背景图像。这是可行的,但您必须为要创建的每种不同的颜色声明不同的、相当复杂的线性渐变。使用 CSS 绘制 API,可以重用一个工作线程,在这种情况下可以传递不同的颜色。
传递参数
注意:以下示例需要在 Chrome 或 Edge 中启用实验性 Web 平台功能标志,方法是访问 about://flags
。
使用 CSS Paint API,我们不仅可以访问自定义属性和常规属性,还可以将自定义参数传递给paint()
函数。
我们在 CSS 中调用该函数时可以添加这些额外的参数。假设我们有时希望描边背景而不是填充它——让我们为此场合传递一个额外的参数。
li {
background-image: paint(hollowHighlights, stroke);
}
现在,我们可以在registerPaint()
类中使用inputArguments()
方法来访问我们已添加到paint()
函数中的自定义参数。
static get inputArguments() { return ['*']; }
然后我们可以访问该参数。
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;
}
// …
}
我们还可以指定我们想要特定类型的参数。
假设我们添加第二个参数,表示我们希望描边的宽度(以像素为单位)。
li {
background-image: paint(hollowHighlights, stroke, 10px);
}
当我们获取参数值列表时,我们可以专门请求<length>
单位。
static get inputArguments() { return ['*', '<length>']; }
在这种情况下,我们专门请求了<length>
属性。返回数组中的第一个元素将是CSSUnparsedValue
。第二个将是CSSStyleValue
。
如果自定义参数是 CSS 值(例如单位),则可以在registerPaint()
函数中检索它时,使用值类型关键字调用 Typed OM CSSStyleValue 类(及其子类)。
现在我们可以访问 type 和 value 属性,这意味着我们可以直接获取像素数和数字类型。(诚然,ctx.lineWidth
使用浮点数作为值,而不是带长度单位的值,但为了举例……)
paint(ctx, size, props, args) {
const strokeWidth = args[1];
if (strokeWidth.unit === 'px') {
ctx.lineWidth = strokeWidth.value;
} else {
ctx.lineWidth = 1.0;
}
// …
}
值得注意的是,使用自定义属性控制此工作线程的不同部分与此处设置的参数之间的区别。自定义属性(实际上,样式映射上的任何属性)都是全局的——它们可以在我们的 CSS(和 JS)中的其他地方使用。
例如,您可能有一个--mainColor
,它将用于在paint()
函数中设置颜色,但也可以用于在 CSS 中的其他地方设置颜色。如果您想专门为 paint 更改它,可能会很困难。这就是自定义参数功能派上用场的地方。另一种思考方式是,参数设置为控制您实际绘制的内容,而属性设置为控制样式。
现在我们可以真正开始看到此 API 的好处,如果我们可以通过自定义属性和额外的paint()
函数参数从我们的 CSS 中控制大量绘图参数,那么我们就可以真正开始构建可重用且高度可控的样式函数。
绘制工作线程
registerPaint(
"hollowHighlights",
class {
static get inputProperties() {
return ["--boxColor"];
}
// 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("--boxColor");
const strokeType = args[0].toString();
const strokeWidth = parseInt(args[1]);
// 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
},
);
使用绘制工作线程
我们可以设置不同的颜色、描边宽度,并选择背景图像应该是填充的还是空心的。
li {
--boxColor: hsl(155 90% 60% / 50%);
background-image: paint(hollowHighlights, stroke, 5px);
}
li:nth-of-type(3n) {
--boxColor: hsl(255 90% 60% / 50%);
background-image: paint(hollowHighlights, filled, 3px);
}
li:nth-of-type(3n + 1) {
--boxColor: hsl(355 90% 60% / 50%);
background-image: paint(hollowHighlights, stroke, 1px);
}
在我们的 <script>
中,我们注册了工作线程
CSS.paintWorklet.addModule("hollow.js");