CSS Paint是一个API,它允许开发者在CSS期望有图像的地方以编程方式生成和绘制图形。
它是CSS Houdini的一部分,是七个新的低级API的总称,这些API暴露了CSS引擎的不同部分,并允许开发者通过挂钩浏览器渲染引擎的样式和布局过程来扩展CSS。
它使开发者能够编写浏览器可以解析为CSS的代码,从而创造新的CSS功能,而不必等待它们在浏览器中的原生实现。
今天我们将探讨两个特别的API,它们是CSS Houdini伞的一部分。
- CSS Paint,在写这篇文章时,它已经在Chrome、Opera和Edge中完全实现,并通过一个polyfill在Firefox和Safari中可用。
- CSS属性和值API,它将允许我们明确地定义我们的CSS变量、它们的初始值、它们支持什么类型的值以及这些变量是否可以被继承。
CSS画图为我们提供了使用CSS API渲染图形的能力。 [PaintWorklet](https://developer.mozilla.org/en-US/docs/Web/API/PaintWorklet)的一个精简版本。 [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D).主要的区别在于。
- 不支持文本渲染
- 没有直接的像素访问/操作
考虑到这两个遗漏,凡是你能用canvas2d ,你就能用CSS Paint API在普通的DOM元素上绘制。对于那些使用canvas2d 做过任何图形的人来说,你应该很自在。
此外,作为开发者,我们有能力将CSS变量作为输入传递给我们的PaintWorklet ,并使用自定义的预定义属性来控制其表现。
这允许高度的定制化,即使是那些不一定熟悉Javascript的设计人员也能做到。
你可以在这里和这里看到更多的例子。说完这些,让我们开始编码吧!
最简单的例子:两条对角线
让我们创建一个CSS paintlet,一旦加载,它将在我们应用它的DOM元素的表面上画两条对角线。Painttlet绘制的表面大小将适应DOM元素的宽度和高度,我们将能够通过传递一个CSS变量来控制对角线的厚度。
创建我们的PaintWorklet
为了加载一个PaintWorklet ,我们将需要把它创建为一个单独的Javascript文件(diagonal-lines.js )。
const PAINTLET_NAME = 'diagonal-lines'
class CSSPaintlet {
// 👉 Define the names of the input CSS variables we will support
static get inputProperties() {
return [ `--${PAINTLET_NAME}-line-width`, ]
}
// 👉 Define names for input CSS arguments supported in paint()
// ⚠ This part of the API is still experimental and hidden
// behind a flag.
static get inputArguments () {
return []
}
// 👉 paint() will be executed every time:
// - any input property changes
// - the DOM element we apply our paintlet to changes its dimensions
paint(ctx, paintSize, props) {
// 👉 Obtain the numeric value of our line width that is passed
// as a CSS variable
const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
ctx.lineWidth = lineWidth
// 🎨 Draw diagonal line #1
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(paintSize.width, paintSize.height)
ctx.stroke()
// 🎨 Draw diagonal line #2
ctx.beginPath()
ctx.moveTo(0, paintSize.height)
ctx.lineTo(paintSize.width, 0)
ctx.stroke()
}
}
// 👉 Register our CSS Paintlet with the correct name
// so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)
我们将我们的CSS paintlet定义为一个独立的类。这个类只需要一个方法来工作 -paint() ,它将在我们指定的CSS paintlet的表面上绘制图形。它将在我们的painttlet所依赖的任何CSS变量发生变化或我们的DOM元素的尺寸发生变化时被执行。
另一个静态方法inputProperties() 是可选的。它告诉CSS paintlet它到底支持哪些输入CSS变量。在我们的例子中,这将是--diagonal-lines-line-width 。我们把它声明为一个输入属性,并在我们的paint() 方法中使用它。重要的是,我们把它放在Number ,以确保跨浏览器支持。
还有一个可选的静态方法支持:inputArguments 。它像这样向我们的paint() 方法暴露参数。
#myImage {
background-image: paint(myWorklet, 30px, red, 10deg);
}
然而,CSS paintlet API的这一部分仍然隐藏在一个标志后面,被认为是实验性的。为了便于使用和兼容,我们不会在本文中涉及它,但我鼓励你自己去阅读它。相反,我们将使用CSS变量,使用inputProperties() 方法来控制我们的Painttlet的所有输入。
注册我们的CSS PaintWorklet
之后,我们必须引用我们的CSS paintlet并将其注册到我们的主页面。重要的是,我们要有条件地加载awesome [css-paint-polyfill](https://github.com/GoogleChromeLabs/css-paint-polyfill)包,这将确保我们的painttlets能在Firefox和Safari中工作。
应该注意的是,沿着我们的CSS paintlet,我们可以使用新的CSS属性和值API(也是Houdini伞的一部分),通过CSS.registerProperty() ,明确地定义我们的CSS变量输入。我们可以像这样控制我们的CSS变量。
- 它们的类型和语法
- 这个CSS变量是否继承自任何父元素
- 如果用户没有指定,它的初始值是什么?
这个API在Firefox和Safari中也不支持,但我们仍然可以在Chromium浏览器中使用它。这样我们的演示就可以面向未来,不支持它的浏览器将直接忽略它。
;(async function() {
// ⚠ Handle Firefox and Safari by importing a polyfill for CSS Pain
if (CSS['paintWorklet'] === undefined) {
await import('https://unpkg.com/css-paint-polyfill')
}
// 👉 Explicitly define our custom CSS variable
// This is not supported in Safari and Firefox, so they will
// ignore it, but we can optionally use it in browsers that
// support it.
// This way we will future-proof our applications so once Safari
// and Firefox support it, they will benefit from these
// definitions too.
//
// Make sure that the browser treats it as a number
// It does not inherit it's value
// It's initial value defaults to 1
if ('registerProperty' in CSS) {
CSS.registerProperty({
name: '--diagonal-lines-line-width',
syntax: '<number>',
inherits: false,
initialValue: 1
})
}
// 👉 Include our separate paintlet file
CSS.paintWorklet.addModule('path/to/our/external/worklet/diagonal-files.js')
})()
将我们的painttlet作为CSS背景来引用
一旦我们把我们的painttlet作为一个JS文件,使用它就非常简单了。我们选择我们要在CSS中样式的目标DOM元素,通过paint() CSS命令应用我们的painttlet。
#myElement {
// 👉 Reference our CSS paintlet
background-image: paint('--diagonal-lines');
// 👉 Pass in custom CSS variable to be used in our CSS paintlet
--diagonal-lines-line-width: 10;
// 👉 Remember - the browser treats this as a regular image
// referenced in CSS. We can control it's repeat, size, position
// and any other background related property available
background-repeat: no-repeat;
background-size: cover;
background-position: 50% 50%;
// Some more styles to make sure we can see our element on the page
border: 1px solid red;
width: 200px;
height: 200px;
margin: 0 auto;
}
随着这段代码的完成,我们将得到以下结果。
请看Georgi Nikoloff (@gbnikolov) 在CodePen上的PenCSS Worklet介绍实例。
记住,我们可以将这个CSS小工具作为背景应用于任何尺寸的DOM元素。让我们把我们的DOM元素放大到全屏,降低它的background-size x和y值,并设置它的background-repeat ,以重复。这就是我们更新的例子。
请看Georgi Nikoloff (@gbnikolov)在CodePen上写的PenCSS Worklet介绍实例。
我们使用的是之前例子中的那个CSS paintlet,但现在我们将它扩展到了整个演示页面。
所以,现在我们已经涵盖了我们的基本例子,并且看到了如何组织我们的代码,让我们来写一些更漂亮的演示吧
粒子连接
请看Georgi Nikoloff(@gbnikolov)在CodePen上的PenCSS Worklet Particles。
这个painttlet的灵感来自于@nucliweb的精彩演示。
同样,对于那些过去使用过canvas2d API来绘制图形的人来说,这将是非常简单的。
我们通过`-dots-connections-count`这个CSS变量来控制我们要渲染多少个点。一旦我们在我们的painttlet中获得了它的数值,我们就创建一个具有适当大小的数组,并用随机的x,y 和radius 属性的对象填充它。
然后我们循环数组中的每一个项目,在它的坐标上画一个球体,找到离它最近的邻居(最小距离通过`-dots-connections-connection-min-dist`CSS变量控制),用线把它们连接起来。
我们还将分别通过`-dots-connections-fill-color`和--dots-connections-stroke-color CSS变量来控制球体的填充颜色和线条的笔划颜色。
下面是完整的工作代码。
const PAINTLET_NAME = 'dots-connections'
class CSSPaintlet {
// 👉 Define names for input CSS variables we will support
static get inputProperties() {
return [
`--${PAINTLET_NAME}-line-width`,
`--${PAINTLET_NAME}-stroke-color`,
`--${PAINTLET_NAME}-fill-color`,
`--${PAINTLET_NAME}-connection-min-dist`,
`--${PAINTLET_NAME}-count`,
]
}
// 👉 Our paint method to be executed when CSS vars change
paint(ctx, paintSize, props, args) {
const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
const minDist = Number(props.get(`--${PAINTLET_NAME}-connection-min-dist`))
const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
const fillColor = props.get(`--${PAINTLET_NAME}-fill-color`)
const numParticles = Number(props.get(`--${PAINTLET_NAME}-count`))
// 👉 Generate particles at random positions
// across our DOM element surface
const particles = new Array(numParticles).fill(null).map(_ => ({
x: Math.random() * paintSize.width,
y: Math.random() * paintSize.height,
radius: 2 + Math.random() * 2,
}))
// 👉 Assign lineWidth coming from CSS variables and make sure
// lineCap and lineWidth are round
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
// 👉 Loop over the particles with nested loops - O(n^2)
for (let i = 0; i < numParticles; i++) {
const particle = particles[i]
// 👉 Loop second time
for (let n = 0; n < numParticles; n++) {
if (i === n) {
continue
}
const nextParticle = particles[n]
// 👉 Calculate distance between the current particle
// and the particle from the previous loop iteration
const dx = nextParticle.x - particle.x
const dy = nextParticle.y - particle.y
const dist = Math.sqrt(dx * dx + dy * dy)
// 👉 If the dist is smaller then the minDist specified via
// CSS variable, then we will connect them with a line
if (dist < minDist) {
ctx.strokeStyle = strokeColor
ctx.beginPath()
ctx.moveTo(nextParticle.x, nextParticle.y)
ctx.lineTo(particle.x, particle.y)
// 👉 Draw the connecting line
ctx.stroke()
}
}
// Finally draw the particle at the right position
ctx.fillStyle = fillColor
ctx.beginPath()
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
}
}
}
// 👉 Register our CSS paintlet with a unique name
// so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)
线条循环
下面是我们的下一个例子。它期望以下CSS变量作为输入到我们的painttlet。
--loop-line-width
--loop-stroke-color
--loop-sides
--loop-scale
--loop-rotation
我们围绕一个完整的圆(PI * 2)进行循环,并根据--loop-sides 计的CSS变量沿圆周定位。对于每一个位置,我们再次围绕我们的全圆进行循环,并通过ctx.lineTo() 命令将其连接到所有其他位置。
请看Georgi Nikoloff (@gbnikolov)在CodePen上写的PenCSS Worklet Line Loop。
const PAINTLET_NAME = 'loop'
class CSSPaintlet {
// 👉 Define names for input CSS variables we will support
static get inputProperties() {
return [
`--${PAINTLET_NAME}-line-width`,
`--${PAINTLET_NAME}-stroke-color`,
`--${PAINTLET_NAME}-sides`,
`--${PAINTLET_NAME}-scale`,
`--${PAINTLET_NAME}-rotation`,
]
}
// 👉 Our paint method to be executed when CSS vars change
paint(ctx, paintSize, props, args) {
const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
const numSides = Number(props.get(`--${PAINTLET_NAME}-sides`))
const scale = Number(props.get(`--${PAINTLET_NAME}-scale`))
const rotation = Number(props.get(`--${PAINTLET_NAME}-rotation`))
const angle = Math.PI * 2 / numSides
const radius = paintSize.width / 2
ctx.save()
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
ctx.strokeStyle = strokeColor
ctx.translate(paintSize.width / 2, paintSize.height / 2)
ctx.rotate(rotation * (Math.PI / 180))
ctx.scale(scale / 100, scale / 100)
ctx.moveTo(0, radius)
// 👉 Loop over the numsides twice in nested loop - O(n^2)
// Connect each corner with all other corners
for (let i = 0; i < numSides; i++) {
const x = Math.sin(i * angle) * radius
const y = Math.cos(i * angle) * radius
for (let n = i; n < numSides; n++) {
const x2 = Math.sin(n * angle) * radius
const y2 = Math.cos(n * angle) * radius
ctx.lineTo(x, y)
ctx.lineTo(x2, y2);
}
}
ctx.closePath()
ctx.stroke()
ctx.restore()
}
}
// 👉 Register our CSS paintlet with a unique name
// so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)
噪声按钮
请看Georgi Nikoloff (@gbnikolov)在CodePen上发表的PenCSS Worklet Noise Button。
这里是我们的下一个例子。它的灵感来自于Jhey Tompkins的另一个很棒的CSS Paintlet。它期望以下的CSS变量作为我们painttlet的输入。
--grid-size
--grid-color
--grid-noise-scale
Paintlet本身使用perlin噪声(代码由joeiddon提供)来控制每个单元的不透明度。
const PAINTLET_NAME = 'grid'
class CSSPaintlet {
// 👉 Define names for input CSS variables we will support
static get inputProperties() {
return [
`--${PAINTLET_NAME}-size`,
`--${PAINTLET_NAME}-color`,
`--${PAINTLET_NAME}-noise-scale`
]
}
// 👉 Our paint method to be executed when CSS vars change
paint(ctx, paintSize, props, args) {
const gridSize = Number(props.get(`--${PAINTLET_NAME}-size`))
const color = props.get(`--${PAINTLET_NAME}-color`)
const noiseScale = Number(props.get(`--${PAINTLET_NAME}-noise-scale`))
ctx.fillStyle = color
for (let x = 0; x < paintSize.width; x += gridSize) {
for (let y = 0; y < paintSize.height; y += gridSize) {
// 👉 Use perlin noise to determine the cell opacity
ctx.globalAlpha = mapRange(perlin.get(x * noiseScale, y * noiseScale), -1, 1, 0.5, 1)
ctx.fillRect(x, y, gridSize, gridSize)
}
}
}
}
// 👉 Register our CSS paintlet with a unique name
// so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)
弯曲的分隔线
作为最后一个例子,让我们做一些也许更有用的事情。我们将以编程的方式画出分隔线,以分隔我们页面的文本内容。
请看Georgi Nikoloff (@gbnikolov)在CodePen上写的PenCSS Worklet Curvy Dividers。
和往常一样,这里是CSS paintlet的代码。
const PAINTLET_NAME = 'curvy-dividor'
class CSSPaintlet {
// 👉 Define names for input CSS variables we will support
static get inputProperties() {
return [
`--${PAINTLET_NAME}-points-count`,
`--${PAINTLET_NAME}-line-width`,
`--${PAINTLET_NAME}-stroke-color`
]
}
// 👉 Our paint method to be executed when CSS vars change
paint(ctx, paintSize, props, args) {
const pointsCount = Number(props.get(`--${PAINTLET_NAME}-points-count`))
const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
const stepX = paintSize.width / pointsCount
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
ctx.strokeStyle = strokeColor
const offsetUpBound = -paintSize.height / 2
const offsetDownBound = paintSize.height / 2
// 👉 Draw quadratic bezier curves across the horizontal axies
// of our dividers:
ctx.moveTo(-stepX / 2, paintSize.height / 2)
for (let i = 0; i < pointsCount; i++) {
const x = (i + 1) * stepX - stepX / 2
const y = paintSize.height / 2 + (i % 2 === 0 ? offsetDownBound : offsetUpBound)
const nextx = (i + 2) * stepX - stepX / 2
const nexty = paintSize.height / 2 + (i % 2 === 0 ? offsetUpBound : offsetDownBound)
const ctrlx = (x + nextx) / 2
const ctrly = (y + nexty) / 2
ctx.quadraticCurveTo(x, y, ctrlx, ctrly)
}
ctx.stroke()
}
}
// 👉 Register our CSS paintlet with a unique name
// so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)
结论
在这篇文章中,我们介绍了CSS Paint API的所有关键组件和方法。它很容易设置,如果我们想画出CSS不支持的更高级的图形,它是非常有用的。
我们可以很容易地用这些CSS绘画工具创建一个库,并在我们的项目中不断重复使用它们,而只需进行最少的设置。
作为一种良好的做法,我鼓励你找到很酷的canvas2d 演示,并将它们移植到新的CSS Paint API上。
The post Drawing Graphicswith the CSS Paint APIappeared first onCodrops.