14 февраля в корпоративной социальной сети Контура лайки (реакции вида «палец вверх») превращаются в вылетающие сердечки.

Это наша традиция с 2019 года. Но каждый год новые сотрудники впервые узнают про встроенный дудл и начинают задавать вопросы по поводу того, откуда этот эффект и как он устроен. Но а сегодня пришло время, рассказать про него и Хабру.
Истоки
Каждый раз, когда вы нажимаете на лайк, сердечки вылетают по-разному, при этом их поведение не упрощено — это не CSS эффект, это действительно честная процедурная 3D-графика, с использованием WebGL и GLSL.
В качестве основы был взят шейдер с ShaderToy, и тут стоит рассказать про то, что это такое.
Шейдеры
Шейдеры — это «программируемые части» графического конвейера 3D-графики. Шейдеры позволяют гибко управлять, тем как происходит отрисовка сцены.
Фрагментный шейдер

Из всех возможных видов шейдеров нам в первую очередь интересен фрагментный (или пиксельный, если говорить в терминах DirectX). Такой шейдер задает, как закрашивать треугольник.
На вход фрагментного шейдера поступают координаты (UV, или по-другому «текстурные координаты»). На выходе от него обычно ожидают цвет, которым закрашивать конкретное место на треугольнике.
ShaderToy
ShaderToy предлагает упрощённую модель и под капотом задает два треугольника для того, чтобы закрасить шейдером всю сцену. В такой модели от разработчика эффекта требуется написать только один метод с описанием эффекта.
На вход шейдера приходят координаты экрана по (x, y) в виде вектора из двух чисел, а на выходе от вас ожидается вектор из 4-х чисел — это RGB цвет и прозрачность, в диапазоне от 0..1. Дополнительно передается номер кадра, координаты нажатия кнопок мыши. и др. опциональные параметры.

Такой простой шейдер заливает всё в один цвет:
void mainImage(out vec4 fragColor, in vec2 fragCoord )
{
fragColor = vec4(1.,.5,.5,1.);
}
Тут можно открыть ShaderToy и попробовать поменять в нем что-то. Например, выводимый цвет. Ну, и нажать на кнопку запуска компиляции шейдера.
ShaderToy скрывает и абстрагирует подготовительный код, кроме главного метода mainImage, в котором от вас требуется написать функцию расчета цвета для каждой точки холста. Как раз за счёт такой простой модели на ShaderToy можно найти очень много прикольных эффектов, 2D и 3D-фракталов. Для каждого доступен код, и можно сделать свою версию.
Если захочется использовать шейдер, сделанный для ShaderToy в WebGL или 3d.js, нужно задать два треугольника и настроить шейдеры, которые будут применяться при его отрисовке. Что мы и сделали, используя WebGL.
Более сложный пример
Ещё нам доступны координаты мышки при нажатии (iMouse) и значение таймера (iTime) — текущее время.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
float speed = .1;
float scale = 0.002;
vec2 p = fragCoord * scale;
for(int i=1; i<10; i++){
p.x+=0.3/float(i)*sin(float(i)*3.*p.y+iTime*speed)+iMouse.x/1000.;
p.y+=0.3/float(i)*cos(float(i)*3.*p.x+iTime*speed)+iMouse.y/1000.;
}
float r=cos(p.x+p.y+1.)*.5+.5;
float g=sin(p.x+p.y+1.)*.5+.5;
float b=(sin(p.x+p.y)+cos(p.x+p.y))*.3+.5;
vec3 color = vec3(r,g,b);
fragColor = vec4(color,1);
}
Сердечки
Код эффекта, которые мы решили использовать, базируется на этой демке:

После некоторых манипуляций он стал таким:

И в почти финальной форме – таким:

А когда интегрировали в кнопку лайка, получилось так:

Устройство эффекта
Сердечки — это сферы
Эффект на самом деле трехмерный. Для того чтобы отрисовать сердечки, код задает ортографическую камеру. Другими словами, мы для всех пикселей запускаем параллельные лучи как бы «в глубину» монитора.

Направления полета и углы поворота сфер случайны для каждого жамка на кнопку лайк.
Дальше нам нужно проверить, что луч пересекся со сферой. Для этого нужно решить квадратное уравнение.

А на сфере — сердце
Код проецирует 2D-плоскость на поверхность сферы этой простой формулой.

И перейдя в 2D, мы можем понять, где закрашивать, а где – нет, используя одну из «формул любви».

Для того чтобы закрасить не только обводку, но и сердце целиком, равенство нужно заменить на неравенство. Еще варианты формулы можно посмотреть тут.
Теперь мы знаем, где мы хотим закрашивать, а где нет на сфере. Нужно задать цвета, цвета два — снаружи светлее, внутри темнее. Используется просто два цвета. Понять, внутри мы или нет, можно из того, закрасили ли мы первое пересечение луча со сферой или нет.
Ещё, конечно, нужно не забыть все правильно повернуть, сгладить края сердечка, но подробный разбор убьет статью. Тем, кто хочет хардкора, можно внимательно вчитаться в код, даже поправить. А если кто-то захочет собрать что-то подобное на шейдерах, еще стоит прочитать тут, как генерировать в них случайные числа.
Интеграция в соцсетку
Рисовать шейдером по всему сайту — плохая идея, поэтому, в нужном месте рисуется прямоугольник.
Там, где сердечек нет, шейдер возвращает прозрачный цвет. Так же прозрачность, градиентом добавлена справа на границе холста и сверху. Если туда залетали сферы, они становятся плавно невидимыми.
Для того чтобы анимация работала, код сайта передает в шейдер номер кадра и случайное число. Это нужно для того, чтобы анимация была каждый раз разная.
Конец
Примеров на ShaderToy — масса, можно утонуть. Но многие из них требуют хорошей видеокарты, некоторые зависают из-за несовместимостей между производителями gpu. Учитывайте, что у пользователей не всегда мощное железо и обязательно тестируйте на Intel, Amd, Nvidia, Apple, так как шейдер может не завестись или даже не скомпилироваться на другом железе, чем у вас.

А чтобы разобраться с математикой в шейдерах, можно почитать известный блог Inigo Quilez — там разобраны многие компоненты процедурной графики.