Эффект фрагментации изображений при помощи CSS Paint API

Image by Dmitry Grushin from Pixabay

Недавно я создавал эффект фрагментации, используя CSS-маску и пользовательские свойства. Эффект получился красивый, но был у него один недостаток: слишком много CSS-кода (сгенерированного при помощи Sass).

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

Вот результат, который нам нужен. Как и в прошлый раз, все это будет работать только в Chrome и Edge.

See the Pen houdini mask by Temani Afif (@t_afif) on CodePen.

Видите? Не больше пяти объявлений CSS, и у нас есть довольно интересный hover-эффект.

Что из себя представляет Paint API?

Paint API — это часть проекта Houdini. Да, «Houdini» — тот самый странный термин, о котором все говорят. Уже есть достаточно много статей о его теоретическом аспекте, так что я не буду нагружать вас этим. Скажу в двух словах: это будущее CSS.

Paint API (и прочие API под зонтиком Houdini) позволяет нам расширять CSS за счет нашего собственного функционала. Больше не нужно ждать выхода новых функций: мы можем написать их сами!

Из спецификации:

«Paint API позволяет веб-разработчикам определять (при помощи JavaScript) пользовательский <image> в CSS, реагирующий на изменения стиля и размера. См. EXPLAINER».

Explainer:

«CSS Paint API создан для улучшения расширяемости CSS. В частности, он позволяет разработчикам написать функцию paint, с помощью которой можно будет рисовать прямо в фоне, рамках и содержимом элементов».

Думаю, идея ясна. Мы можем рисовать все, что захотим. Давайте начнем с очень базового демо — расцвечивания фона:

See the Pen Paint API overview by Temani Afif (@t_afif) on CodePen.

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

На первый взгляд это не кажется интуитивным. Но обратите внимание на то, что основная структура всегда остается одинаковой: три шага, указанные выше, это «копипаст», который вы повторяете в каждом проекте. Настоящую работу выполняет код внутри функции paint().

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

See the Pen Paint API overview #2 by Temani Afif (@t_afif) on CodePen.

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

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

Создаем маску

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

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

Сейчас, вместо использования большого числа градиентов, мы определим только один пользовательский image для нашей маски, а управляться он будет нашим Paint API.

Пример, пожалуйста!

See the Pen Mask houdini #1 by Temani Afif (@t_afif) on CodePen.

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

Теперь нам нужно разбить нашу картинку на большее количество частей. Давайте определим две переменные и обновим наш код:

See the Pen Mask houdini #2 by Temani Afif (@t_afif) on CodePen.

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

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-переменные:

See the Pen Mask houdini #3 by Temani Afif (@t_afif) on CodePen.

Теперь нам нужен контроль над альфа-каналом — чтобы создать эффект затухания для каждого прямоугольника и построить эффект фрагментации.

Давайте представим третью переменную, которую мы используем для альфа-канала. Ее тоже будем менять при наведении.

See the Pen Mask houdini #4 by Temani Afif (@t_afif) on CodePen.

Мы определили пользовательское свойство CSS как <number> с переходом от 1 к 0. То же свойство используется для определения альфа-канала наших прямоугольников. При наведении не происходит ничего необычного, потому что все прямоугольники затухают одинаково.

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

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

Погодите, но значение альфа не должно же быть в диапазоне [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 — наша переменная для управления задержкой.

Давайте посмотрим на все это в действии:

See the Pen Houdini mask #5 by Temani Afif (@t_afif) on CodePen.

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

Проблема связана с функцией random(). Мы сказали, что каждый альфа-канал должен анимироваться между X и Y. Логично, что эти значения должны оставаться постоянными. Но в течение перехода функция paint() вызывается неоднократно. Поэтому функция random() каждый раз дает нам другие значения X и Y для каждого альфа-канала. Отсюда и «рандомный» эффект, который мы получаем.

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

К сожалению, это нельзя сделать при помощи встроенной в JavaScript функции 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;
}

И результат становится таким:

See the Pen Mask houdini #6 by Temani Afif (@t_afif) on CodePen.

Мы получили эффект фрагментации без сложного кода. Наш код — это:

  • простой базовый цикл для создания прямоугольников NxM
  • умная формула для альфа-канала для создания задержки перехода
  • готовая функция random(), взятая из Сети.

Вот и все! Все, что вам нужно сделать, это применить свойство маски к любому элементу и подогнать CSS-переменные.

Боремся с промежутками!

Поиграв с демками, вы заметите, что в некоторых случаях между прямоугольниками появляются странные промежутки.

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

Мы изменим это:

ctx.fillRect(i*w, j*h, w, h);

на это:

ctx.fillRect(i*w-.5, j*h-.5, w+.5, h+.5);

Это создаст небольшое перекрытие между прямоугольниками, которое компенсирует пробелы между ними. За значением 0.5, которое я использовал, никакая логика не стоит. Вы можете сделать его как больше, так и меньше.

See the Pen Mask houdini #7 by Temani Afif (@t_afif) on CodePen.

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

Можно ли все это применить к каким-то фигурам, кроме прямоугольников? Конечно!

Давайте не забывать, что при помощи 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[i]][0]    , dots[triangles[i]][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, а эффект получился очень интересный.

See the Pen houdini mask — triangular by Temani Afif (@t_afif) on CodePen.

Мы можем сделать и больше фигур! Все, что для этого нужно, — найти подходящий алгоритм.

Я не мог отказать себе в удовольствии и не создать шестиугольники!

See the Pen houdini mask — hexagon by Temani Afif (@t_afif) on CodePen.

Код я взял из вот этой статьи (автор — Izan Pérez Cosano). Наша переменная теперь — R, она определяет размеры одного шестиугольника.

Что дальше?

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

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

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

img:hover {
  opacity:0;
}

Эффект фрагментации

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

img:hover {
  --f-o:0;
}

Это значит, что мы можем легко интегрировать такой эффект для создания более сложных анимаций. Далее представлены несколько идей.

Адаптивный слайдер изображений

See the Pen Image slider I by Temani Afif (@t_afif) on CodePen.

Еще одна версия того же слайдера:

See the Pen Image slider II by Temani Afif (@t_afif) on CodePen.

Эффект шума

See the Pen noise effect by Temani Afif (@t_afif) on CodePen.

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

See the Pen loading mask by Temani Afif (@t_afif) on CodePen.

Эффект наведения на карточку

See the Pen Card hover effect by Temani Afif (@t_afif) on CodePen.

Итоги

Это все на сегодня. Но это лишь малая толика того, чего можно достичь при помощи Paint API.

Напоследок скажу пару вещей:

  • Paint API — это на 90% <canvas>. Так что чем лучше вы знаете <canvas>, тем более крутые вещи сможете делать. Canvas используется очень широко, а это значит, что вы без проблем найдете хорошую документацию и массу руководств.
  • Paint API убирает всю сложность из CSS. Для рисования интересных вещей больше не нужно писать сложный код с хаками. Благодаря этому CSS-код намного легче поддерживать, не говоря уже о том, что он становится менее подверженным ошибкам.

Перевод статьи «Exploring the CSS Paint API: Image Fragmentation Effect».

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх