От нашего спонсора: Усовершенствуйте свой маркетинг с помощью дизайна, автоматизации, аналитики и многого другого, используя наши маркетинговые навыки.
CSS Paint — это API, который позволяет разработчикам программно создавать и рисовать графику там, где CSS ожидает изображение.
Это часть CSS Houdini, обобщающего термина для семи новых низкоуровневых API-интерфейсов, которые раскрывают различные части механизма CSS и позволяют разработчикам расширять CSS, подключаясь к процессу стилизации и компоновки механизма рендеринга браузера.
Он позволяет разработчикам писать код, который браузер может анализировать как CSS, тем самым создавая новые функции CSS, не дожидаясь их встроенной реализации в браузерах.
Сегодня мы рассмотрим два конкретных API, которые являются частью CSS Houdini:
- CSS Paint, который на момент написания этой статьи, был полностью реализован в Chrome, Opera и Edge и доступен в Firefox и Safari через полифил.
- CSS Properties and Values API , что позволит нам явно определять наши переменные CSS, их начальные значения, какой тип значений они поддерживают и могут ли эти переменные быть унаследованными.
CSS Paint предоставляет нам возможность визуализировать графику с помощью PaintWorklet
урезанная версия CanvasRenderingContext2D
. Основные отличия:
- Нет поддержки рендеринга текста
- Нет прямого доступа к пикселям / манипуляций
С учетом этих двух упущений все, что вы можете нарисовать, используя canvas2d
вы можете рисовать на обычном элементе DOM с помощью CSS Paint API. Те из вас, кто рисовал любую графику с помощью canvas2d
должны быть как дома.
Кроме того, мы, как разработчики, имеем возможность передавать переменные CSS в качестве входных данных в наш PaintWorklet
и управлять его представлением с помощью пользовательских предопределенных атрибутов.
Это обеспечивает высокую степень настройки даже для дизайнеров, которые не обязательно знакомы с Javascript.
Вы можете увидеть больше примеров здесь и здесь. А теперь давайте перейдем к кодированию!
Содержание статьи
Простейший пример: две диагональные линии
Давайте создадим рисовальщик CSS, который после загрузки будет рисовать две диагональные линии по поверхности элемента DOM, к которому мы его применяем. Размер поверхности рисования краской будет адаптироваться к ширине и высоте элемента DOM, и мы сможем управлять толщиной диагональной линии, передав переменную CSS.
Создание нашего PaintWorklet
Чтобы загрузить PaintWorklet
нам нужно будет создать его как отдельный файл Javascript ( diagonal-lines.js
).
const PAINTLET_NAME = 'диагональные линии'
class CSSPaintlet {
// 👉 Определим имена входных переменных CSS, которые мы будем поддерживать
static get inputProperties () {
возврат [
`--${PAINTLET_NAME}-line-width`,
]
}
// 👉 Определить имена для входных аргументов CSS, поддерживаемых в paint ()
// ⚠️ Эта часть API все еще экспериментальная и скрытая
// за флагом.
static get inputArguments () {
возврат []
}
// 👉 paint () будет выполняться каждый раз:
// - любое изменение свойства input
// - элемент DOM, к которому мы применяем нашу раскраску, изменяет свои размеры
paint (ctx, paintSize, props) {
// 👉 Получаем числовое значение ширины нашей линии, которое передается
// как переменная CSS
const lineWidth = Number (props.get (`- $ {PAINTLET_NAME} -line-width`))
ctx.lineWidth = lineWidth
// 🎨 Нарисуйте диагональную линию №1
ctx.beginPath ()
ctx.moveTo (0, 0)
ctx.lineTo (paintSize.width, paintSize.height)
ctx.stroke ()
// 🎨 Нарисуйте диагональную линию # 2
ctx.beginPath ()
ctx.moveTo (0, paintSize.height)
ctx.lineTo (paintSize.width, 0)
ctx.stroke ()
}
}
// 👉 Зарегистрируем наш CSS Paintlet с правильным именем
// чтобы мы могли ссылаться на него из нашего CSS
registerPaint (PAINTLET_NAME, CSSPaintlet)
Мы определяем нашу раскраску CSS как отдельный класс. Этому классу для работы нужен только один метод — paint ()
который будет рисовать графику поверх поверхности, которой мы назначаем CSS-рисование. Он будет выполнен при изменении любой из переменных CSS, на которые опирается наш рисовальщик, или когда наш элемент DOM изменяет свои размеры.
Другой статический метод inputProperties ()
является необязательным. Он сообщает мастеру CSS, какие именно входные переменные CSS он поддерживает. В нашем случае это будет - диагональ-линии-ширина линии
. Мы объявляем его как входное свойство и используем его в нашем методе paint ()
. Важно, чтобы мы преобразовали его в число, поместив его в Number
чтобы обеспечить кроссбраузерную поддержку.
Поддерживается еще один дополнительный статический метод: inputArguments
. Он предоставляет аргументы нашему методу paint ()
следующим образом:
#myImage {
фоновое изображение: краска (myWorklet, 30 пикселей, красный, 10 градусов);
}
Однако эта часть API рисования CSS все еще скрыта за флажком и считается экспериментальной. Для простоты использования и совместимости мы не будем рассматривать это в этой статье, но я рекомендую вам прочитать об этом самостоятельно. Вместо этого мы будем использовать переменные CSS, используя метод inputProperties ()
чтобы контролировать все входные данные для нашего рисования.
Регистрация нашего CSS PaintWorklet
После этого мы должны сослаться на нашу раскраску CSS и зарегистрировать ее на нашей главной странице. Важно, чтобы мы условно загрузили замечательный пакет css-paint-polyfill
который обеспечит работу наших раскрасок в Firefox и Safari.
Следует отметить, что вместе с рисованием CSS мы можем использовать новый API свойств и значений CSS, который также является частью «зонтика» Houdini, чтобы явно определять наши входные данные переменных CSS через CSS.registerProperty ()
. Мы можем управлять нашими переменными CSS следующим образом:
- Их типы и синтаксис
- Наследуется ли эта переменная CSS от каких-либо родительских элементов
- Какое будет ее начальное значение, если пользователь не указал его
Этот API также не поддерживается в Firefox и Safari, но мы все еще можем использовать его в браузерах Chromium. Таким образом мы защитим наши демоверсии от будущего, а браузеры, которые его не поддерживают, просто проигнорируют это.
; (асинхронная функция () {
// ⚠️ Обработка Firefox и Safari путем импорта полифилла для CSS Pain
if (CSS ['paintWorklet'] === undefined) {
ожидание импорта ('https://unpkg.com/css-paint-polyfill')
}
// 👉 Явно определяем нашу пользовательскую переменную CSS
// Это не поддерживается в Safari и Firefox, поэтому они будут
// игнорировать его, но при желании мы можем использовать его в браузерах, которые
// поддерживаем это.
// Таким образом мы защитим наши приложения от будущего, чтобы Safari
// и Firefox поддерживают это, они извлекут выгоду из этих
// определения тоже.
//
// Убедитесь, что браузер воспринимает это как число
// Это значение не наследуется
// Его начальное значение по умолчанию 1
if ('registerProperty' в CSS) {
CSS.registerProperty ({
name: '--diagonal-lines-line-width',
синтаксис: '',
наследует: ложь,
initialValue: 1
})
}
// 👉 Включаем наш отдельный файл раскраски
CSS.paintWorklet.addModule ('путь / к / наш / внешний / рабочий / диагональ-файлы.js')
}) ()
Ссылка на нашу раскраску как на фон CSS
После того, как мы включили нашу раскраску в виде файла JS, пользоваться ею стало очень просто. Мы выбираем наш целевой элемент DOM, который хотим стилизовать в CSS, и применяем нашу раскраску с помощью CSS-команды paint ()
:
#myElement {
// 👉 Ссылка на нашу раскраску CSS
background-image: paint ('- диагональные линии');
// 👉 Передаем пользовательскую переменную CSS, которая будет использоваться в нашей раскраске CSS
--diagonal-lines-line-width: 10;
// 👉 Помните - браузер рассматривает это как обычное изображение
// ссылка на CSS. Мы можем контролировать его повторение, размер, положение
// и любое другое доступное свойство, связанное с фоном
фон-повтор: без повторения;
размер фона: обложка;
фоновая позиция: 50% 50%;
// Еще несколько стилей, чтобы мы могли видеть наш элемент на странице
граница: сплошной красный 1px;
ширина: 200 пикселей;
высота: 200 пикселей;
маржа: 0 авто;
}
Убрав этот код, мы получим следующее:
Помните, что мы можем применить эту раскраску CSS в качестве фона к любому элементу DOM любых размеров. Давайте развернем наш элемент DOM в полноэкранный режим, уменьшим его значения background-size
x и y и установим для него background-repeat
повторение. Вот наш обновленный пример:
Мы используем ту же раскраску CSS из нашего предыдущего примера, но теперь мы расширили ее, чтобы покрыть всю демонстрационную страницу.
Итак, теперь, когда мы рассмотрели наш базовый пример и увидели, как организовать наш код, давайте напишем несколько более красивых демонстраций!
Связи между частицами
См. Перо
CSS Worklet Particles от Георгия Николоффа (@gbnikolov)
на CodePen.
Этот рисунок был вдохновлен потрясающей демонстрацией от @nucliweb.
Опять же, для тех из вас, кто использовал API canvas2d
для рисования графики в прошлом, это будет довольно просто.
Мы контролируем количество отображаемых точек с помощью CSS-переменной «–dots-connections-count». Получив его числовое значение в нашей раскраске, мы создаем массив подходящего размера и заполняем его объектами со случайными свойствами x
y
и radius
.
Затем мы зацикливаем каждый из наших элементов в массиве, рисуем сферу с ее координатами, находим ближайшего к ней соседа (минимальное расстояние контролируется с помощью CSS-переменной `–dots-connections-connection-min-dist`) и соедините их линией.
Мы также будем управлять цветом заливки сфер и цветом обводки линий с помощью CSS-переменных «–dots-connections-fill-color» и - dots-connections-stroke-color
соответственно.
]
Вот полный рабочий код:
const PAINTLET_NAME = 'точки-соединения'
class CSSPaintlet {
// 👉 Определим имена для входных переменных CSS, которые мы будем поддерживать
static get inputProperties () {
возврат [
`--${PAINTLET_NAME}-line-width`,
`--${PAINTLET_NAME}-stroke-color`,
`--${PAINTLET_NAME}-fill-color`,
`--${PAINTLET_NAME}-connection-min-dist`,
`--${PAINTLET_NAME}-count`,
]
}
// 👉 Наш метод рисования будет выполняться при изменении переменных CSS
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`))
// 👉 Генерация частиц в случайных положениях
// по поверхности нашего элемента DOM
const частицы = новый массив (numParticles) .fill (null) .map (_ => ({
x: Math.random () * paintSize.width,
y: Math.random () * paintSize.height,
радиус: 2 + Math.random () * 2,
}))
// 👉 Назначьте ширину линии из переменных CSS и убедитесь, что
// lineCap и lineWidth круглые
ctx.lineWidth = lineWidth
ctx.lineJoin = 'круглый'
ctx.lineCap = 'круглый'
// 👉 Перебираем частицы вложенными циклами - O (n ^ 2)
for (let i = 0; i <numParticles; i ++) {
const Particle = частицы [i]
// 👉 Повторение во второй раз
for (let n = 0; n <numParticles; n ++) {
if (i === n) {
Продолжать
}
const nextParticle = частицы [n]
// 👉 Вычислить расстояние между текущей частицей
// и частица из предыдущей итерации цикла
const dx = nextParticle.x - частица.x
const dy = nextParticle.y - частица.y
const dist = Math.sqrt (dx * dx + dy * dy)
// 👉 Если расстояние меньше, то minDist, указанный через
// Переменная CSS, потом соединим их линией
if (dist <minDist) {
ctx.strokeStyle = strokeColor
ctx.beginPath ()
ctx.moveTo (nextParticle.x, nextParticle.y)
ctx.lineTo (частица.x, частица.y)
// 👉 Рисуем соединительную линию
ctx.stroke ()
}
}
// Наконец, рисуем частицу в нужном месте
ctx.fillStyle = fillColor
ctx.beginPath ()
ctx.arc (частица.x, частица.y, частица.радиус, 0, Math.PI * 2)
ctx.closePath ()
ctx.fill ()
}
}
}
// 👉 Зарегистрируем нашу раскраску CSS с уникальным именем
// чтобы мы могли ссылаться на него из нашего CSS
registerPaint (PAINTLET_NAME, CSSPaintlet)
Line Loop
Вот наш следующий пример. Он ожидает следующие переменные CSS в качестве входных данных для нашего рисования:
- ширина линии петли
- цвет обводки петли
- стороны петли
- петля-шкала
- петля-вращение
Мы обходим полный круг (PI * 2) и располагаем их по периметру на основе - loop-side
count переменных CSS. Для каждой позиции мы снова обходим полный круг и соединяем его со всеми другими позициями с помощью команды ctx.lineTo ()
:
const PAINTLET_NAME = 'цикл'
class CSSPaintlet {
// 👉 Определим имена для входных переменных CSS, которые мы будем поддерживать
static get inputProperties () {
возврат [
`--${PAINTLET_NAME}-line-width`,
`--${PAINTLET_NAME}-stroke-color`,
`--${PAINTLET_NAME}-sides`,
`--${PAINTLET_NAME}-scale`,
`--${PAINTLET_NAME}-rotation`,
]
}
// 👉 Наш метод рисования будет выполняться при изменении переменных CSS
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 = 'круглый'
ctx.lineCap = 'круглый'
ctx.strokeStyle = strokeColor
ctx.translate (paintSize.width / 2, paintSize.height / 2)
ctx.rotate (вращение * (Math.PI / 180))
ctx.scale (масштаб / 100, масштаб / 100)
ctx.moveTo (0, радиус)
// 👉 Дважды перебираем числа во вложенном цикле - O (n ^ 2)
// Соединяем каждый угол со всеми остальными углами
for (let i = 0; i <numSides; i ++) {
const x = Math.sin (i * угол) * радиус
const y = Math.cos (i * угол) * радиус
for (let n = i; n <numSides; n ++) {
const x2 = Math.sin (n * угол) * радиус
const y2 = Math.cos (n * угол) * радиус
ctx.lineTo (x, y)
ctx.lineTo (x2, y2);
}
}
ctx.closePath ()
ctx.stroke ()
ctx.restore ()
}
}
// 👉 Зарегистрируем нашу раскраску CSS с уникальным именем
// чтобы мы могли ссылаться на него из нашего CSS
registerPaint (PAINTLET_NAME, CSSPaintlet)
Кнопка шумоподавления
Вот наш следующий пример. Он вдохновлен другим замечательным CSS Paintlet от Джея Томпкинса. Он ожидает следующие переменные CSS в качестве входных данных для нашего рисования:
- размер сетки
- цвет сетки
- шкала шума сетки
Сам рисовальщик использует шум Перлина (код любезно предоставлен joeiddon) для управления непрозрачностью каждой отдельной ячейки.
const PAINTLET_NAME = 'сетка'
class CSSPaintlet {
// 👉 Определим имена для входных переменных CSS, которые мы будем поддерживать
static get inputProperties () {
возврат [
`--${PAINTLET_NAME}-size`,
`--${PAINTLET_NAME}-color`,
`--${PAINTLET_NAME}-noise-scale`
]
}
// 👉 Наш метод рисования будет выполняться при изменении переменных CSS
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 = цвет
for (let x = 0; x <paintSize.width; x + = gridSize) {
for (let y = 0; y <paintSize.height; y + = gridSize) {
// 👉 Используйте шум Перлина для определения непрозрачности ячейки
ctx.globalAlpha = mapRange (perlin.get (x * noiseScale, y * noiseScale), -1, 1, 0.5, 1)
ctx.fillRect (x, y, размер сетки, размер сетки)
}
}
}
}
// 👉 Зарегистрируем нашу раскраску CSS с уникальным именем
// чтобы мы могли ссылаться на него из нашего CSS
registerPaint (PAINTLET_NAME, CSSPaintlet)
Фигурные разделители
В качестве последнего примера давайте сделаем что-нибудь более полезное. Мы программно нарисуем разделители для разделения текстового содержимого нашей страницы:
И, как обычно, вот код рисования CSS:
const PAINTLET_NAME = 'curvy-dividor'
class CSSPaintlet {
// 👉 Определим имена для входных переменных CSS, которые мы будем поддерживать
static get inputProperties () {
возврат [
`--${PAINTLET_NAME}-points-count`,
`--${PAINTLET_NAME}-line-width`,
`--${PAINTLET_NAME}-stroke-color`
]
}
// 👉 Наш метод рисования будет выполняться при изменении переменных CSS
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 = 'круглый'
ctx.lineCap = 'круглый'
ctx.strokeStyle = strokeColor
const offsetUpBound = -paintSize.height / 2
const offsetDownBound = paintSize.height / 2
// 👉 Рисуем квадратичные кривые Безье по горизонтальным осям
// наших разделителей:
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 ()
}
}
// 👉 Зарегистрируем нашу раскраску CSS с уникальным именем
// чтобы мы могли ссылаться на него из нашего CSS
registerPaint (PAINTLET_NAME, CSSPaintlet)
Заключение
В этой статье мы рассмотрели все ключевые компоненты и методы CSS Paint API. Его довольно легко настроить и очень полезно, если мы хотим рисовать более сложную графику, которую CSS не поддерживает "из коробки".
Мы можем легко создать библиотеку из этих CSS-раскрасок и продолжать использовать их в наших проектах с минимальными настройками.
В качестве хорошей практики я рекомендую вам найти классные демонстрации canvas2d
и перенести их в новый CSS Paint API.