Paint Worklet 是一种现代Web技术,它允许开发者通过JavaScript直接在浏览器中自定义CSS属性的绘制逻辑。这意味着开发者可以创建复杂的图形效果,而无需依赖外部图形库或预处理器。Paint Worklet 作为CSS的一个扩展,提供了一种新的编程范式,使得Web页面的视觉效果更加丰富和动态。
Paint Worklet 的基本使用步骤
- 创建 Paint Worklet 文件:编写一个包含自定义绘制逻辑的JavaScript文件,例如
index.js。 - 注册 Paint Worklet:在HTML文档的
<script>标签中,使用CSS.paintWorklet.addModule("index.js");来注册你的 Paint Worklet 文件。 - 定义 CSS 类:在CSS中定义使用 Paint Worklet 的类,并通过
background-image: paint(your-paint-worklet-name);来应用这些自定义绘制效果。 - 使用自定义属性:通过CSS变量(如
--variable-name),你可以动态地调整绘制效果,使得效果更加灵活。
1. 首先创建一个 index.js 文件,然后在其中写入下面的内容:
class MyGradient {
paint(ctx, geom) {
// 创建一个线性渐变
const gradient = ctx.createLinearGradient(0, 0, geom.width, geom.height);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
// 使用渐变填充整个区域
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, geom.width, geom.height);
}
}
// 注册自定义绘制器
registerPaint('my-gradient', MyGradient);
这个类定义了一个名为 MyGradient 的 Paint Worklet,它使用 paint 方法来绘制一个从红色到蓝色的线性渐变。ctx 是一个 CanvasRenderingContext2D 对象,它提供了绘制渐变所需的方法。geom 是一个包含绘制区域尺寸的对象。createLinearGradient 方法创建了一个渐变对象,通过 addColorStop 方法添加颜色停止点,最后使用 fillRect 方法用渐变填充整个区域。
2. 然后创建一个 index.html 文件,其内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./index.js"></script>
<script>
CSS.paintWorklet.addModule("index.js");
</script>
<style>
body {
background-color: #ccc;
}
.my-element {
width: 100px;
height: 100px;
background-image: paint(my-gradient);
}
</style>
</head>
<body>
<div class="my-element"></div>
</body>
</html>
最后使用 live server 等工具查看效果
Paint Worklet 的作用
Paint Worklet 允许开发者创建自定义的图形和动画效果,这些效果可以响应页面的尺寸变化,并且可以与CSS的其他属性和媒体查询相结合,以实现更加丰富和交互式的用户界面。
效果案例
1. 动态波浪效果
class WaveEffect {
static get inputProperties() {
return ['--wave-color'];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
const amplitude = 25;
const frequency = 0.3;
const color = styleMap.get('--wave-color').toString();
ctx.strokeStyle = color;
ctx.strokeWidth = 1;
ctx.beginPath();
for (let x = 0; x < width; x += 4) {
const y = height * Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y);
}
ctx.lineTo(width, height);
ctx.closePath();
ctx.fill();
}
}
WaveEffect 类定义了一个波浪效果的 Paint Worklet。它首先通过 get inputProperties 静态方法指定了它依赖的 CSS 自定义属性 --wave-color。在 paint 方法中,它使用正弦函数 Math.sin 来计算波浪的 Y 坐标,然后通过 lineTo 方法绘制波浪线。波浪的颜色由 --wave-color 属性决定。
在 CSS 中使用波浪效果:
.my-element2 {
width: 100px;
height: 100px;
--wave-color: red;
background-image: paint(wave-effect);
}
2. 星系螺旋臂
class SpiralGalaxy {
static get inputProperties() {
return ['--spiral-inner-color', '--spiral-outter-color', '--spiral-count'];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
const armLength = Math.min(width, height) * 0.45;
ctx.fillStyle = styleMap.get('--spiral-inner-color');
ctx.strokeStyle = styleMap.get('--spiral-outter-color');
ctx.strokewidth = 2;
for (let i = 0; i < styleMap.get('--spiral-count'); i++) {
const angle = (i / 4) * Math.PI;
const radius = armLength * (i + 1) / styleMap.get('--spiral-count');
ctx.beginPath();
ctx.arc(width / 2, height / 2, radius, angle, angle + Math.PI / 4, false);
ctx.stroke();
ctx.beginPath();
ctx.arc(width / 2, height / 2, Math.max(radius - 0, 0), angle + Math.PI / 4, angle + Math.PI / 2, false);
ctx.fill();
}
}
}
SpiralGalaxy 类绘制了一个螺旋星系效果。它通过 get inputProperties 方法指定了三个依赖的 CSS 自定义属性:--spiral-inner-color、--spiral-outter-color 和 --spiral-count。在 paint 方法中,它使用 arc 方法来绘制螺旋的弧线,并通过循环来创建多个螺旋臂。螺旋的内部使用 fillStyle 填充颜色,而外部使用 strokeStyle 绘制轮廓。
在 CSS 中使用星系螺旋臂效果:
.my-element3 {
width: 100px;
height: 100px;
--spiral-inner-color: rgba(30, 30, 200, 0.8);
--spiral-outter-color: white;
--spiral-count: 15;
background-image: paint(spiral-galaxy);
}
3. 动态颜色过渡
class GradientTransition {
static get inputProperties() {
return ['--colors', '--stops'];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
const colors = styleMap.get('--colors').toString().split(' ').map(color => color.trim());
const stops = styleMap.get('--stops').toString().split(' ').map(stop => parseFloat(stop));
const gradient = ctx.createLinearGradient(0, 0, width, height);
const numStops = Math.min(colors.length, stops.length);
for (let i = 0; i < numStops; i++) {
gradient.addColorStop(stops[i] / 100, colors[i]);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
}
GradientTransition 类允许用户通过 CSS 自定义属性定义渐变的颜色和停止点。get inputProperties 方法指定了两个依赖的属性:--colors 和 --stops。在 paint 方法中,它首先解析这些属性的值,然后创建一个线性渐变对象,并通过循环添加颜色停止点。最后,使用 fillRect 方法用渐变填充整个区域。
在 CSS 中使用动态颜色过渡效果:
.my-element4 {
width: 100px;
height: 100px;
--colors: red pink yellow blue; /* 颜色列表,空格分隔 */
--stops: 0% 33% 67% 100%; /* 颜色停止点,空格分隔 */
background-image: paint(gradient-transition);
}
4. 动态阴影效果
class DynamicShadow {
static get inputProperties() {
return [
'--blur-radius',
'--x-offset',
'--y-offset',
'--shadow-bg-color',
'--shadow-color'
];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
const blurRadius = styleMap.get('--blur-radius')[0];
const x = styleMap.get('--x-offset')[0];
const y = styleMap.get('--y-offset')[0];
const shadowColor = styleMap.get('--shadow-color')[0];
ctx.fillStyle = styleMap.get('--shadow-bg-color')[0];
ctx.shadowBlur = blurRadius;
ctx.shadowColor = shadowColor;
ctx.shadowOffsetX = x;
ctx.shadowOffsetY = y;
ctx.beginPath();
ctx.arc(width / 2, height / 2, Math.min(width, height) / 2 - Math.max(Math.abs(x), Math.abs(y)) - blurRadius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
}
DynamicShadow 类创建了一个动态阴影效果。它通过 get inputProperties 方法指定了五个依赖的 CSS 自定义属性,用于控制阴影的大小、位置和颜色。在 paint 方法中,它首先获取这些属性的值,然后设置阴影的样式,包括模糊半径、颜色、偏移量和背景颜色。最后,使用 arc 方法和 fill 方法来绘制带有阴影的圆形。
在 CSS 中使用动态阴影效果:
.my-element5 {
width: 100px;
height: 100px;
--blur-radius: 5; /* 模糊半径 */
--x-offset: 2; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
--shadow-bg-color: white; /* 阴影颜色 */
--shadow-color: rgba(0, 0, 0, 0.5); /* 阴影颜色 */
animation: sun linear infinite;
animation-duration: 3000ms;
background-image: paint(dynamic-shadow);
}
@keyframes sun {
0%,
100% {
--x-offset: -5; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
25% {
--x-offset: -1; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
50% {
--x-offset: 2; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
75% {
--x-offset: 5; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
}
5. 自定义标题
registerPaint(
"headerHighlight",
class {
static get inputProperties() {
return ["--highColor"];
}
static get contextOptions() {
return { alpha: true };
}
paint(ctx, size, props) {
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();
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
},
);
headerHighlight 类为 h1 元素创建了一个高亮效果。它通过 get inputProperties 方法指定了一个依赖的 CSS 自定义属性 --highColor,用于定义高亮颜色。get contextOptions 方法设置了 alpha 通道,允许使用透明度。在 paint 方法中,它首先定义了高亮区域的起始点、宽度和高度,然后使用 fill 方法填充高亮颜色。接着,它通过循环创建了一系列的虚线效果,增加了视觉效果的复杂性。
通过这些自定义绘制器,开发者可以创建出独特的视觉效果,增强网页的吸引力和用户体验。
在 CSS 中使用动态阴影效果:
.fancy {
background-image: paint(headerHighlight);
}
h1 {
--highColor: hsl(155 90% 60% / 70%);
}
总之,Paint Worklet 为Web开发者提供了一个强大的工具,使得他们能够以一种声明式和响应式的方式,将创意转化为网页上的动态图形。
最终效果
如果你想要亲自实现上面的效果,请按照下面的步骤:
- 创建项目结构
mkdir test-worklet && cd test-worklet
touch index.html index.js
- 将 index.js 的内容复制到空文件中
// 在 paint-worklet.js 文件中
class MyGradient {
paint(ctx, geom) {
// 创建一个线性渐变
const gradient = ctx.createLinearGradient(0, 0, geom.width, geom.height);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
// 使用渐变填充整个区域
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, geom.width, geom.height);
}
}
// 注册自定义绘制器
registerPaint('my-gradient', MyGradient);
// wave-worklet.js
class WaveEffect {
static get inputProperties() {
return ['--wave-color'];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
const amplitude = 25;
const frequency = 0.3;
const color = styleMap.get('--wave-color').toString();
ctx.strokeStyle = color;
ctx.strokeWidth = 1;
ctx.beginPath();
for (let x = 0; x < width; x += 4) {
const y = height * Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y);
}
ctx.lineTo(width, height);
ctx.closePath();
ctx.fill();
}
}
registerPaint('wave-effect', WaveEffect);
// spiral-worklet.js
class SpiralGalaxy {
static get inputProperties() {
return ['--spiral-inner-color', '--spiral-outter-color', '--spiral-count'];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
const armLength = Math.min(width, height) * 0.45;
ctx.fillStyle = styleMap.get('--spiral-inner-color');
ctx.strokeStyle = styleMap.get('--spiral-outter-color');
ctx.strokewidth = 2;
for (let i = 0; i < styleMap.get('--spiral-count'); i++) {
const angle = (i / 4) * Math.PI;
const radius = armLength * (i + 1) / styleMap.get('--spiral-count');
ctx.beginPath();
ctx.arc(width / 2, height / 2, radius, angle, angle + Math.PI / 4, false);
ctx.stroke();
ctx.beginPath();
ctx.arc(width / 2, height / 2, Math.max(radius - 0, 0), angle + Math.PI / 4, angle + Math.PI / 2, false);
ctx.fill();
}
}
}
registerPaint('spiral-galaxy', SpiralGalaxy);
// gradient-transition-worklet.js
class GradientTransition {
static get inputProperties() {
// 指定 Paint Worklet 所依赖的 CSS 自定义属性
return ['--colors', '--stops'];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
// 获取 CSS 自定义属性 'colors' 和 'stops'
const colors = styleMap.get('--colors').toString().split(' ').map(color => color.trim());
const stops = styleMap.get('--stops').toString().split(' ').map(stop => parseFloat(stop));
// 创建线性渐变
const gradient = ctx.createLinearGradient(0, 0, width, height);
// 确保 colors 和 stops 数组长度相同
const numStops = Math.min(colors.length, stops.length);
// 添加颜色停止点到渐变
for (let i = 0; i < numStops; i++) {
gradient.addColorStop(stops[i] / 100, colors[i]);
}
// 使用渐变填充整个区域
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
}
// 注册自定义绘制器
registerPaint('gradient-transition', GradientTransition);
class DynamicShadow {
static get inputProperties() {
// 指定依赖的 CSS 自定义属性
return [
'--blur-radius',
'--x-offset',
'--y-offset',
'--shadow-bg-color',
'--shadow-color'
];
}
paint(ctx, geom, styleMap) {
const width = geom.width;
const height = geom.height;
// 使用 styleMap.get() 获取 CSS 自定义属性的值
const blurRadius = styleMap.get('--blur-radius')[0];
const x = styleMap.get('--x-offset')[0];
const y = styleMap.get('--y-offset')[0];
const shadowColor = styleMap.get('--shadow-color')[0]; // 颜色值是字符串
ctx.fillStyle = styleMap.get('--shadow-bg-color')[0];
ctx.shadowBlur = blurRadius;
ctx.shadowColor = shadowColor; // 直接使用颜色字符串
ctx.shadowOffsetX = x;
ctx.shadowOffsetY = y;
ctx.beginPath();
ctx.arc(width / 2, height / 2, Math.min(width, height) / 2 - Math.max(Math.abs(x), Math.abs(y)) - blurRadius, 0, 2 * Math.PI);
ctx.closePath(); // 闭合路径
ctx.fill();
}
}
// 注册自定义绘制器
registerPaint('dynamic-shadow', DynamicShadow);
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
},
);
- 将 index.html 的内容复制到空文件中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./index.js"></script>
<script>
CSS.paintWorklet.addModule("index.js");
</script>
<style>
body {
background-color: #ccc;
}
div {
margin-bottom: 10px;
}
.my-element {
width: 100px;
height: 100px;
background-image: paint(my-gradient);
}
.my-element2 {
width: 100px;
height: 100px;
--wave-color: red;
background-image: paint(wave-effect);
}
.my-element3 {
width: 100px;
height: 100px;
--spiral-inner-color: rgba(30, 30, 200, 0.8);
--spiral-outter-color: white;
--spiral-count: 15;
background-image: paint(spiral-galaxy);
}
.my-element4 {
width: 100px;
height: 100px;
--colors: red pink yellow blue; /* 颜色列表,空格分隔 */
--stops: 0% 33% 67% 100%; /* 颜色停止点,空格分隔 */
background-image: paint(gradient-transition);
}
.my-element5 {
width: 100px;
height: 100px;
--blur-radius: 5; /* 模糊半径 */
--x-offset: 2; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
--shadow-bg-color: white; /* 阴影颜色 */
--shadow-color: rgba(0, 0, 0, 0.5); /* 阴影颜色 */
animation: sun linear infinite;
animation-duration: 3000ms;
background-image: paint(dynamic-shadow);
}
@keyframes sun {
0%,
100% {
--x-offset: -5; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
25% {
--x-offset: -1; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
50% {
--x-offset: 2; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
75% {
--x-offset: 5; /* X 轴偏移 */
--y-offset: 2; /* Y 轴偏移 */
}
}
.fancy {
background-image: paint(headerHighlight);
}
h1 {
--highColor: hsl(155 90% 60% / 70%);
}
</style>
</head>
<body>
<div class="my-element"></div>
<div class="my-element2"></div>
<div class="my-element3"></div>
<div class="my-element4"></div>
<div class="my-element5"></div>
<h1 class="fancy">Largest Header</h1>
</body>
</html>
- 浏览 index.html