前言
最近看了几篇文章,是关于 CSS Houdini 的。作为一个前端搬砖的还真不知道这玩意,虽然不知道的东西挺多的,但是这玩意有点高大上啊。
Houdini 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。Houdini 是一组 API,它们使开发人员可以直接访问CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器中本地实现。
具体解释见:CSS Houdini
推荐文章
用CSS Houdini画一片星空
CSS Houdini 实现磁吸效果
CSS Houdini:用浏览器引擎实现高级CSS效果
官方案例
Houdini Samples,可以看看有些东西还是很有意思的。
给textarea加一个方格背景
文章中的效果图:
文章中提到了渐变,自己尝试了一下没啥思路。但是有一种取巧的方式——背景图,把下面的图片当成背景图不就好了
textarea{
width: 300px;
height: 100px;
background-image: url('./abc.png');
background-size: 80px 40px;
color: white;
font-size: 30px;
}
我的效果,这不是差不多吗😃
突然发现自己有点傻叉,自己这个每一行的颜色都是一样的,跟官方的差远了。
Houdini 基本用法
attributeStyleMap 和 computedStyleMap
- attributeStyleMap 用于处理内联样式,可以进行读写操作
- computedStyleMap 用于处理非内联样式,只允许读
// Before Houdini
const size = 30
target.style.fontSize = size + 'px' // "20px"
const imgUrl = 'https://www.exampe.com/sample.png'
target.style.background = 'url(' + imgUrl + ')' // "url(https://www.exampe.com/sample.png)"
target.style.cssText = 'font-size:' + size + 'px; background: url('+ imgUrl +')'
// "font-size:30px; background: url(https://www.exampe.com/sample.png)"
上面的代码应该很多人都写过,拿到dom元素直接修改内联样式。简单的样式这样写还是可以的,但是比较复杂的时候就比较容易出错
这时可以使用 attributeStyleMap 和 computedStyleMap
<template>
<div id="test">
111
</div>
</template>
<script>
export default {
mounted() {
let el = document.getElementById('test');
// attributeStyleMap 用于处理内联样式,可以进行读写操作
el.attributeStyleMap.set('color', 'blue');
el.attributeStyleMap.set('font-size', '40px');
console.log(el.attributeStyleMap.get('font-size'), el.attributeStyleMap.get('color'), el.attributeStyleMap.get('width'));
// computedStyleMap 用于处理非内联样式,只允许读
console.log(el.computedStyleMap().get('color').toString(), el.computedStyleMap().get('font-size'));
}
};
</script>
<style lang="scss" scoped>
#test {
width: 100px;
height: 100px;
background: pink;
}
</style>
自己尝试了一下,有几个需要注意的点:
- attributeStyleMap ,可以进行读写,但是只能读取到内联样式
- computedStyleMap() ,只读,可以读取到dom元素上最终的样式
- 对于有单位的样式,比如
width
,会返回一个对象;对于五单位样式,比如color
,需要调用toString()
方法来进行转换 - 以设置样式来说,不支持链式操作
这里补充一个与computedStyleMap 类似的东西,之前在 css中样式类型及属性值的获取 简单说过,感兴趣的可以看一下
CSS Properties & Values API
可以允许开发者自定义属性和属性值,其实就是css变量。
这个不说了,赶紧用处不大,而且有点习惯使用scss
了
Paint API
提供了一组与绘制(Paint)过程相关的API,我们可以通过它自定义的渲染规则,例如调整颜色(color)、边框(border)、背景(background)、形状等绘制规则。
文章最开始介绍的如何实现文本域的背景就是通过这种方式来实现的,通常需要创建一个js来编写绘制逻辑,然后使用registerPaint
进行注册。比如:
/* checkboardWorklet.js */
class CheckerboardPainter {
paint(ctx, geom, properties) {
const colors = ['red', 'green', 'blue'];
const size = 32;
for(let y = 0; y < geom.height/size; y++) {
for(let x = 0; x < geom.width/size; x++) {
const color = colors[(x + y) % colors.length];
ctx.beginPath();
ctx.fillStyle = color;
ctx.rect(x * size, y * size, size, size);
ctx.fill();
}
}
}
}
// 注册checkerboard
registerPaint('checkerboard', CheckerboardPainter);
/* index.html */
<script>
CSS.paintWorklet.addModule('path/to/checkboardWorklet.js') // 添加checkboardWorklet到paintWorklet
</script>
/* index.html */
<!doctype html>
<textarea></textarea>
<style>
textarea {
background-image: paint(checkerboard); // 使用paint()方法调用checkboard绘制背景
}
</style>
但是在vue中, CSS.paintWorklet.addModule('path/to/checkboardWorklet.js')
这一步失败了,无法请求的js文件,应该是需要进行设置。
因为不知道如何处理,所以采用第二种方式
示例:绘制一个矩形
// js
let blobURL = URL.createObjectURL(new Blob(['(',
function() {
class CheckerboardPainter {
static get inputProperties() {
return ['--rect-color'];
}
paint(ctx, geom, properties, args) {
// ctx 一个 Canvas 的 Context 对象,因此 paint 中的绘制方式跟 canvas 绘制是一样的。
// geom 包含节点的尺寸信息,同时也是 canvas 可绘制范围(画板)的尺寸信息。
// properties 包含节点的 CSS 属性,需要调用静态方法 inputProperties 声明注入。
// CSS 中调用 Paint 类时传入的参数,需要调用静态方法 inputArguments 声明注入
const color = properties.get('--rect-color')[0];
ctx.fillStyle = color;
ctx.fillRect(0, 0, geom.width, geom.height);
}
}
registerPaint('checkerboard', CheckerboardPainter);
}.toString(),
')()'], { type: 'application/javascript' })
);
export default blobURL;
// 使用
<template>
<div>
<div class="rect"></div>
</div>
</template>
<script>
import blobURL from './checkerboard.js';
export default {
mounted() {
CSS.paintWorklet.addModule(blobURL);
}
};
</script>
<style lang="scss" scoped>
.rect {
width: 100px;
height: 100px;
--rect-color: red;
background-image: paint(checkerboard);
}
</style>
官方示例文本域
let blobURL = URL.createObjectURL(new Blob(['(',
function() {
class CheckerboardPainter {
paint(ctx, geom, properties, args) {
const colors = ['red', 'green', 'blue'];
// 方块的尺寸
const size = 32;
for (let y = 0; y < geom.height / size; y++) {
for (let x = 0; x < geom.width / size; x++) {
const color = colors[(x + y) % colors.length];
ctx.beginPath();
ctx.fillStyle = color;
ctx.rect(x * size, y * size, size, size);
ctx.fill();
}
}
}
}
registerPaint('checkerboard', CheckerboardPainter);
}.toString(),
')()'], { type: 'application/javascript' })
);
export default blobURL;
<template>
<div>
<textarea></textarea>
</div>
</template>
<script>
import blobURL from './checkerboard.js';
export default {
mounted() {
CSS.paintWorklet.addModule(blobURL);
}
};
</script>
<style lang="scss" scoped>
textarea {
background-image: paint(checkerboard);
}
</style>
要玩好Paint API,还是需要对canvas非常熟悉才行。
Animation API
提供了一组与合成(composite)渲染相关的API,我们可以通过它调整绘制层级和自定义动画。
略,现在有一些很不错的js动画库,没必要用这个写动画。
Layout API
提供了一组与布局(Layout)过程相关的API,我们可以通过它自定义的布局规则,类似于实现诸如flex、grid等布局,自定义元素或子元素的对齐(alignment)、位置(position)等布局规则。
略,这个一般人玩不了,也没必要看了。
磁吸效果
效果:https://codepen.io/HelKyle/pen/zYWozde
效果图:
let blobURL = URL.createObjectURL(new Blob(['(',
function() {
class MagnetMatrixPaintWorklet {
// 鼠标位置、点的颜色、大小、间隔、影响半径
static get inputProperties() { return ['--mouse-x', '--mouse-y', '--magnet-color', '--magnet-size', '--magnet-gap', '--magnet-radius']; }
paint(ctx, size, props) {
const mouseX = parseInt(props.get('--mouse-x'));
const mouseY = parseInt(props.get('--mouse-y'));
const color = props.get('--magnet-color');
const lineWidth = parseInt(props.get('--magnet-size'));
const gap = parseInt(props.get('--magnet-gap'));
const radius = parseInt(props.get('--magnet-radius'));
ctx.lineCap = 'round';
for (let i = 0; i * gap < size.width; i++) {
for (let j = 0; j * gap < size.height; j++) {
const posX = i * gap;
const posY = j * gap;
const distance = Math.sqrt(Math.pow(posX - mouseX, 2) + Math.pow(posY - mouseY, 2));
const width = distance < radius ? (1 - distance / radius * distance / radius) * gap * 0.4 : 0;
const startPosX = posX - width * 0.5;
const endPosX = posX + width * 0.5;
const rotate = Math.atan2(mouseY - posY, mouseX - posX);
ctx.save();
ctx.beginPath();
ctx.translate(posX, posY);
ctx.rotate(rotate);
ctx.translate(-posX, -posY);
ctx.moveTo(startPosX, posY);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineTo(endPosX, posY);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
}
}
}
registerPaint('magnet-matrix', MagnetMatrixPaintWorklet);
}.toString(),
')()'], { type: 'application/javascript' })
);
export default blobURL;
<template>
<div class="demo"></div>
</template>
<script>
import blobURL from './checkerboard.js';
export default {
mounted() {
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule(blobURL);
}
let div = document.getElementsByTagName('div')[0];
div.addEventListener('mouseenter', e => {
div.style.setProperty('--mouse-x', e.clientX);
div.style.setProperty('--mouse-y', e.clientY);
});
div.addEventListener('mousemove', e => {
div.style.setProperty('--mouse-x', e.clientX);
div.style.setProperty('--mouse-y', e.clientY);
});
div.addEventListener('mouseleave', () => {
div.style.setProperty('--mouse-x', -999);
div.style.setProperty('--mouse-y', -999);
});
},
};
</script>
<style lang="scss" scoped>
div {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.demo {
width: 100vw;
height: 100vh;
padding: 80px;
--magnet-color: rgb(97, 123, 255);
--magnet-size: 2;
--magnet-gap: 20;
--magnet-radius: 150;
background: paint(magnet-matrix);
}
</style>