Как написать собственное свойство CSS

Kate

Administrator
Команда форума
b1e6bf2999b8210863997ba53b84d860.gif

Благодаря Paint API из состава Houdini можно не ждать выхода новых возможностей CSS. Шаг за шагом автор этого материала показывает, как написать новое свойство CSS для анимации с фрагментацией. Делимся материалом, пока у нас начинается курс по Frontend-разработке.


В предыдущей статье я создал эффект фрагментации с помощью маски CSS и пользовательских свойств. Эффект изящный, но не лишён недостатка: в нём используется много кода CSS. Я собираюсь повторить тот же эффект, но при помощи нового API Paint, что значительно сократит CSS, а Sass устранит полностью. Сейчас такое поддерживают только Chrome и Edge:

Видите? Не более пяти строк CSS — и мы получаем довольно классную анимацию наведения.


Что такое Paint API?​

Paint API — часть проекта Houdini. Да, «Houdini» — странный термин, о котором все говорят. Многие статьи уже освещают теоретический аспект, поэтому я не буду утомлять вас. Из спецификации:

[Этот] API, позволяющий веб-разработчикам определять пользовательский CSS с помощью JavaScript [sic], который будет реагировать на изменения стиля и размера.
И от автора статьи:

CSS API Paint разрабатывается для улучшения расширяемости CSS. В частности, это позволяет разработчикам написать функцию paint, которая позволяет нам рисовать непосредственно на фоне, границе или содержимом элемента.
Думаю, идея достаточно ясна. Мы можем рисовать то, что захотим. Давайте начнём с очень простой демонстрации окраски фона:

  1. Добавим ворклет строкой CSS.paintWorklet.addModule('your_js_file').
  2. Зарегистрируем новый метод рисования — draw.
  3. Внутри него создадим функцию paint(), в которой и выполняем всю работу. И знаете что? Всё похоже на работу с <canvas>. Этот ctx — 2D-контекст, и я просто использовал некоторые известные функции, чтобы нарисовать красный прямоугольник, охватывающий всю область.
Это может показаться неинтуитивным, но обратите внимание, что основная структура всегда одна и та же: три шага выше — это часть «копировать/вставить», которую вы повторяете для каждого проекта. Настоящая работа — это код, который мы пишем внутри функции paint().

Добавим переменную:

Как видите, логика довольно проста. Мы определяем геттер inputProperties с массивом переменных, добавляем свойства третьим параметром в paint(), а позже получим нашу переменную с помощью properties.get(). Вот и всё! Теперь у нас есть всё необходимое для сложного эффекта фрагментации.

Строим маску​

Вы можете удивиться, почему Paint API создаёт эффект фрагментации. Это инструмент для рисования изображений, так как же он позволит фрагментировать изображение?

В предыдущей статье я создал эффект, используя различные маски слоя, где каждая представляет собой квадрат, заданный градиентом (помните, что градиент — это изображение), так что мы получили своего рода матрицу, и фокус заключался в том, чтобы настроить альфа-канал каждой из них по отдельности.

На этот раз вместо множества градиентов мы определим только одно пользовательское изображение для маски, и оно будет обрабатываться API paint.

Вот пример:

Выше я создал изображение, где непрозрачный цвет покрывает левую часть, а полупрозрачный — правую. Применяя это изображение в качестве маски, мы получаем логичный результат — полупрозрачное изображение. Теперь всё, что нам нужно сделать, — это разделить наше изображение на несколько частей. Давайте определим две переменные и обновим код:

Вот соответствующая часть кода:

const n = properties.get('--f-n');
const m = properties.get('--f-m');

const w = size.width/n;
const h = size.height/m;

for(var i=0;i<n;i++) {
for(var j=0;j<m;j++) {
ctx.fillStyle = 'rgba(0,0,0,'+(Math.random())+')';
ctx.fillRect(i*w, j*h, w, h);
}
}
N и M определяют размерность матрицы прямоугольников. W и H — размеры каждого прямоугольника. Кроме того, у нас есть простой цикл for для заполнения каждого прямоугольника случайным прозрачным цветом.

Немного JavaScript — и мы получаем легко управляемую пользовательскую маску, изменяя переменные CSS:

Теперь нужно управлять альфа-каналом, чтобы создать эффект выцветания каждого прямоугольника и эффект фрагментации. Введём третью переменную, используемую для альфа-канала, который мы также будем менять при наведении.

Пользовательское свойство CSS мы определили как число, переходящее от 1 к 0, и это же свойство используется для определения альфа-канала наших прямоугольников. При наведении не произойдёт ничего необычного, потому что все прямоугольники будут исчезать одинаково.

Нужен трюк, чтобы предотвратить затухание всех прямоугольников одновременно, создавая задержку эффекта. Вот поясняющая иллюстрация:

2635047dca449c5c6d05d0d5a2e65edb.png

Выше показана альфа-анимация для двух прямоугольников. Сначала определяем переменную L, которая должна быть больше или равна 1, затем для каждого прямоугольника нашей матрицы (т. е. для каждого альфа-канала) выполняем переход между X и Y, где X – Y = L, поэтому имеем одинаковую общую продолжительность для всех альфа-каналов.

X должен быть больше или равен 1, а Y — меньше или равен 0. Подождите, значение альфа не должно быть в диапазоне [1 0], верно? Да, так и должно быть! И все приемы, над которыми мы работаем, основаны на этом.

Выше альфа анимируется от 8 до -2, а это означает, что мы имеем непрозрачный цвет в диапазоне [8 1], прозрачный в диапазоне [0 -2] и анимацию в диапазоне [1 0]. Другими словами, любое значение больше 1 будет иметь тот же эффект, что и 1, а любое значение меньше 0 — тот же эффект, что и 0.

Анимация в пределах [1 0] не будет идти одновременно для обоих наших прямоугольников. Прямоугольник 2 достигнет [1 0] раньше прямоугольника 1. Подход применяется ко всем альфа-каналам, чтобы получить отложенную анимацию.

Обновим этот код:

rgba(0,0,0,'+(o)+')
вот так:

rgba(0,0,0,'+((Math.random()*(l-1) + 1) - (1-o)*l)+')
l — показанная ранее переменная, а o — значение нашей переменной CSS, которая переходит от 1 к 0.

Когда o равно 1, имеем (Math.random()(l – 1) + 1). Учитывая, что функция random() выдаёт нам значение в диапазоне [0 1], конечное значение попадёт в диапазон [l 1]. Когда o=0, (Math.random()(l – 1) + 1 - l) и значение с диапазоном [0 1 – L]. L — это наша переменная для управления задержкой. Посмотрим на это в действии:

Приближаемся к цели. У нас уже есть классный эффект фрагментации, но не тот, что мы видели в начале статьи, не столь гладкий. Проблема связана с функцией random().

Как мы уже писали, каждый альфа-канал должен анимироваться между значениями X и Y, поэтому логично, что эти значения должны оставаться неизменными, но paint() во время перехода вызывается много раз, поэтому каждый раз random() даёт разные значения X и Y для каждого альфа-канала; отсюда и «случайный» эффект.

Чтобы исправить это, нужно найти такой способ хранения сгенерированных значений, чтобы они всегда были одинаковыми при каждом вызове paint().

Рассмотрим псевдослучайную функцию, то есть такую, которая всегда генерирует одну и ту же последовательность значений.

Другими словами, мы хотим управлять начальными значениями генератора псевдослучайных чисел. К сожалению, мы не можем сделать это с помощью встроенной в JavaScript функции random(), поэтому давайте возьмём random со Stack Overflow:

const mask = 0xffffffff;
const seed = 30; /* update this to change the generated sequence */
let m_w = (123456789 + seed) & mask;
let m_z = (987654321 - seed) & mask;

let random = function() {
m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
result /= 4294967296;
return result;
}
И вот результат:

Эффект фрагментации без сложного кода из:

  • простого вложенного цикла, создающего матрицу NxM;
  • формулы для альфа-канала, чтобы создать задержку перехода;
  • реализации random() из сети.
Вот и всё! Всё, что нужно сделать, — это применить свойство маски к любому элементу и настроить переменные CSS.

Боремся с отступами​

Немного поиграв с приведёнными выше демонстрациями, в некоторых случаях вы заметите странные промежутки между прямоугольниками:

a3f0f1d50e279145a4bd49ce4c4a468d.png

Чтобы их не было, можно расширить область каждого прямоугольника небольшим смещением. Заменим этот код:

ctx.fillRect(i*w, j*h, w, h);
вот этим:

ctx.fillRect(i*w-.5, j*h-.5, w+.5, h+.5);
Код создаст небольшое перекрытие между прямоугольниками, оно компенсирует промежутки между ними. В значении 0,5 нет никакой особой логики, в зависимости от ситуации можно выбрать размер больше или меньше.

Хотите больше фигур?​

Применим ли эффект не только к прямоугольникам? Конечно! Не будем забывать, что использовать canvas мы можем в рисовании любых форм — в отличие от фигур с чистым CSS, где иногда нужен хакерский код.

Создадим тот же эффект для треугольников: я нашёл то, что называется триангуляцией Делоне — это алгоритм построения соединенных треугольников с определёнными свойствами для набора точек. Есть множество готовых к использованию реализаций этого алгоритма, но мы остановимся на Delaunator, потому что он считается быстрейшим.

Сначала определяем набор точек (здесь воспользуемся random()), затем запустим Delauntor. В нашем случае нам нужна только одна переменная, которая определит количество точек:

const n = properties.get('--f-n');
const o = properties.get('--f-o');
const w = size.width;
const h = size.height;
const l = 7;

var dots = [[0,0],[0,w],[h,0],[w,h]]; /* we always include the corners */
/* we generate N random points within the area of the element */
for (var i = 0; i < n; i++) {
dots.push([random() * w, random() * h]);
}
/**/
/* We call Delaunator to generate the triangles*/
var delaunay = Delaunator.from(dots);
var triangles = delaunay.triangles;
/**/
for (var i = 0; i < triangles.length; i += 3) { /* we loop the triangles points */
/* we draw the path of the triangles */
ctx.beginPath();
ctx.moveTo(dots[triangles][0] , dots[triangles][1]);
ctx.lineTo(dots[triangles[i + 1]][0], dots[triangles[i + 1]][1]);
ctx.lineTo(dots[triangles[i + 2]][0], dots[triangles[i + 2]][1]);
ctx.closePath();
/**/
var alpha = (random()*(l-1) + 1) - (1-o)*l; /* the alpha value */
/* we fill the area of triangle with the semi-transparent color */
ctx.fillStyle = 'rgba(0,0,0,'+alpha+')';
/* we consider stroke to fight the gaps */
ctx.strokeStyle = 'rgba(0,0,0,'+alpha+')';
ctx.stroke();
ctx.fill();
}
Мне больше нечего добавить к комментариям в приведённом выше коде. Я написал простые JavaScript и canvas — и получился крутой эффект.

Можно сделать и другие фигуры! Нужно только найти алгоритм их построения. Шестиугольник не сделать нельзя!

Код я взял здесь. Переменная R определяет размер одного шестиугольника.

Что дальше?​

Теперь, когда мы создали эффект фрагментации, сосредоточимся на CSS. Обратите внимание, что эффект — это изменение значения свойства непрозрачности (или любого другого свойства) элемента в состоянии hover.

Анимация прозрачности

img {
opacity:1;
transition:eek:pacity 1s;
}

img:hover {
opacity:0;
}
Эффект фрагментации

img {
-webkit-mask: paint(fragmentation);
--f-o:1;
transition:--f-o 1s;
}

img:hover {
--f-o:0;
}
Это означает, что мы можем легко интегрировать такой эффект, чтобы создать анимации сложнее. Вот несколько идей!

Отзывчивый слайдер:

Другая версия того же слайдера:

Эффект шума:

Экран загрузки:

Наведение на карту:

Подведём итоги​

Всё это — лишь вершина айсберга API Paint. В заключение я хотел бы отметить два важных момента:

  • API Paint на 90% состоит из <canvas>, поэтому, чем больше вы знаете о <canvas>, тем больше причудливых вещей сможете сделать. Canvas используется широко, хорошо документирован и есть много статей, которые помогут вам войти в курс дела. Вот одна из них прямо на CSS-Tricks!
  • API Paint устраняет все сложности на стороне CSS. Чтобы нарисовать что-то крутое, не нужно работать со сложным, хакерским кодом. Обслуживание CSS сильно упрощается, не говоря уже о том, что такой код не столь подвержен ошибкам.

 
Сверху