Алгоритм растрового построения градиента

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

Алгоритм растрового построения градиента состоит в вычислении цвета для каждого отдельно взятого пикселя. Для простоты, рассмотрим построение градиента с двумя ключевыми цветами. Представим эти цвета в формате RGBA и рассмотрим их как четырёхмерные векторы $\mathbf{c_0}$ и $\mathbf{c_1}$.

Любой градиент можно представить как функцию двух переменных, которая принимает на вход точку $(x, y)$ на экране и возвращает числовое значение $z$. Это значение затем используется для вычисления цвета для конкретного пикселя. Для простоты вычислений, ограничим это значение диапазоном $z \in [0; 100]$. Значения меньше $0$ будем считать равными нулю, а значения больше $100$ будем считать равными $100$. Тогда, цвет $\mathbf{c}$ пикселя с координатами $(x_0, y_0)$ будет вычислен по формуле:
$$
\mathbf{c} = \mathbf{c_0} + \frac{z}{100}(\mathbf{c_1} - \mathbf{c_0})
$$
где $z = f(x_0, y_0)$, $f$ - заданная функция градиента, зависящая от выбранного типа градиента (линейный, кольцевой и т.д., подробнее о выборе функций далее), $\mathbf{c_0}, \mathbf{c_1}$ - векторы начального и конечного цвета градиента соответственно.

Алгоритм вычисления цвета пикселя для градиента с $n$ ключевыми цветами

Пусть $\mathbf{c_0}, \mathbf{c_1}, ... \mathbf{c_{n-1}}$ - ключевые цвета. В случае с двумя ключевыми цветами, первый ключевой цвет соответствовал цвету пикселей с $z = f(x, y) = 0$, а второй - с $z = f(x, y) = 100$. Для случая $n$ ключевых цветов нам нужно больше информации для построения градиента. Для каждого ключевого цвета $\mathbf{c_i}$ нам нужно знать соответствующее ему соответсвующее значение $z_i \in [0; 100]$. Для правильной работы алгоритма, $z_i$ и соответсвующие им цвета должны быть отсортированы в порядке возрастания.

Алгоритм вычисления цвета пикселя с координатами $(x, y)$:

  1. Вычисляем $z = f(x, y)$ используя функцию градиента;
  2. Если $z \leq z_0$, цвет пикселя равен $\mathbf{c_0}$, если $z \geq z_{n-1}$, цвет пикселя равен $\mathbf{c_{n-1}}$
  3. Иначе, находим такое $i$, для которого $z_{i-1} \leq z \leq z_i$
  4. Вычисляем цвет $\mathbf{c}$ пикселя $(x, y)$ по формуле:
    $$
    \mathbf{c} = \mathbf{c_{i-1}} + \frac{z - z_{i-1}}{z_i - z_{i-1}}(\mathbf{c_i} - \mathbf{c_{i-1}})
    $$

Программная реализация

Функция создания градиента (возвращает лямбда-выражение)
Так как количество ключевых цветов может быть довольно большим, использование классического алгоритма двоичного (бинарного) поиска для нахождения двух соседних ключей, между которыми попадает значение $z = f(x, y)$, может значительно ускорить работу алгоритма.

function createGradient(f, keys, colors) {
  return (x, y) => {
    let z = f(x, y);
    if (z > 100) z = 100;
    if (z < 0) z = 0;
 
    // Двоичный поиск
    let l = 0;
    let r = keys.length - 1;
    while (r - l > 1) {
      let m = Math.floor((r + l) / 2);
      if (keys[m] <= z) l = m;
      else r = m;
    }
 
    // Вычисляем вектор цвета по формуле
    let color = [];
    for (let i = 0; i < 4; i++) {
      color[i] = colors[l][i] + (z-keys[l]) / (keys[r]-keys[l]) * (colors[r][i]-colors[l][i]);
    }
    return color;
  };
}

Пример использования градиента при закраске прямоугольника при помощи функции установки точки:

function fillRect(x0, y0, width, height, gradient) {
  for (let i = 0; i < height; i++) {
    for (let j = 0; j < width; j++) {
      let x = x0 + i;
      let y = y0 + j;
      pixel(x, y, gradient(x, y));
    }
  }
}

Пример простейшего градиента:

let exampleGradient = createGradient(
  (x, y) => 100 * (x - 200) / 400,
  [0, 50, 100],
  [
    [255, 0, 0, 255], // красный
    [0, 255, 0, 255], // зеленый
    [0, 0, 255, 255] // синий
  ]
);
fillRect(200, 150, 400, 300, exampleGradient);

Результат:

Основные типы градиентов

Линейный градиент


Пусть $(x_0, y_0), (x_1, y_1)$ - две точки экрана, задающие направление. Тогда функция, задающая линейный градиент имеет вид:
\begin{cases}
f(x,y) = ax + by - ax_0 - by_0 \\
a = 100\frac{\Delta x}{\Delta x^2 + \Delta y^2} \\
b = 100\frac{\Delta y}{\Delta x^2 + \Delta y^2}
\end{cases}
где $\Delta x = x_1 - x_0, \Delta y = y_1 - y_0$

Реализация

function linearGradient(start, end, keys, colors) {
  let dx = end.x - start.x;
  let dy = end.y - start.y;
  let a = 100*dx / (dx*dx + dy*dy);
  let b = 100*dy / (dx*dx + dy*dy);
  let c = - a*start.x - b*start.y;
  let f = (x, y) => {
    return a*x + b*y + c;
  };
  return createGradient(f, keys, colors);
}

Круговой градиент

Пусть $r_1, r_2$ - радиусы окружностей, $(x_0, y_0)$ - их центр. Тогда функция, задающая круговой градиент имеет вид:
$$
f(x,y) = \frac{100}{r_2-r_1} \left(\sqrt{(x - x_0)^2 + (y - y_0)^2} - r_1 \right)
$$

Обобщение 1: Введём коэффициент сжатия $s$ для окружностей. Заменим окружность с радиусом $r_1$ на эллипс с фокусами $r_1$ и $sr_1$, окружность с радиусом $r_2$ на эллипс с фокусами $r_2$ и $sr_2$. Для получения окружностей, достаточно положить $s = 1$. Обобщенная функция кругового градиента имеет вид:
$$
f(x,y) = \frac{100}{s(r_2-r_1)} \left(\sqrt{s^2(x - x_0)^2 + (y - y_0)^2} - sr_1 \right)
$$

Обобщение 2: Добавим возможность поворота эллипса на произвольный угол $\theta$. Тогда обобщенная функция кругового градиента будет иметь вид:
$$
\begin{cases}
f(x,y) = \frac{100}{s(r_2-r_1)} \left(\sqrt{s^2x_r^2 + y_r^2} - sr_1 \right) \\
x_r = (x - x_0)\cos\theta + (y - y_0)\sin\theta \\
y_r = -(x - x_0)\sin\theta + (y - y_0)\cos\theta \\
\end{cases}
$$

Реализация

function radialGradient(x0, y0, r1, r2, yScaling, angle, keys, colors) {
  let f = (x, y) => {
    let xs = x - x0;
    let ys = y - y0;
    let xr = xs*Math.cos(angle) + ys*Math.sin(angle);
    let yr = -xs*Math.sin(angle) + ys*Math.cos(angle);
    return 100 / (yScaling * (r2-r1)) * (Math.sqrt(yScaling*yScaling*xr*xr+yr*yr)-yScaling*r1);
  }
  return createGradient(f, keys, colors);
}

Конический градиент


Пусть $(x_0, y_0)$ - центр градиента. Функция конического градиента имеет вид:
$$
f(x,y) = \begin{cases}
25(1 - \frac{x - x_0}{\sqrt{(x-x_0)^2 + (y - y_0)^2}}), \text{если } y \geq y_0 \\
25(3 + \frac{x - x_0}{\sqrt{(x-x_0)^2 + (y - y_0)^2}}), \text{если } y < y_0
\end{cases}
$$
Замечание: во избежание деления на $0$, при реализации необходимо отдельно обработать случай $(x, y) = (x_0, y_0)$.

Реализация

function conicGradient(x0, y0, keys, colors) {
  let f = (x, y) => {
    if (x == x0 && y == y0) return 0;
    let d = Math.sqrt((x - x0)*(x - x0) + (y - y0)*(y - y0));
    if (y >= y0) return 25 * (1 - (x - x0) / d);
    else return 25 * (3 + (x - x0) / d);
  };
  return createGradient(f, keys, colors);
}