~/sagadav
все статьи

// 19 октября 2026 · 5 мин

ASCII-молния на canvas без библиотек

Справа на главной бьёт молния, собранная из точек ·•●. Никаких библиотек: один React-компонент, <canvas> и примерно триста строк кода. Расскажу, из чего он состоит.

Сетка и дизеринг

Холст делится на клетки по 9 пикселей. Для каждой клетки на каждом кадре считаются два числа: яркость и заряд. Яркость определяет размер точки, заряд определяет цвет: от серого грозового до электрического синего и белого в ядре разряда.

Размеров у точки всего три, а яркость непрерывная. Чтобы плавные переходы не превращались в ступеньки, используется упорядоченный дизеринг по матрице Байера 4×4: к яркости добавляется порог, который зависит от позиции клетки.

const level = clamp01(bright) * DOTS.length + BAYER[y & 3][x & 3] - 1
const g = Math.floor(level)
if (g < 0) continue // пустая клетка

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

Атлас вместо fillText

Сначала точки рисовались символами через fillText, но ● в разных системных шрифтах разного размера, и на Windows диск солнца в прошлой версии сайта превращался в мелкую сыпь. Теперь все комбинации «размер × яркость × заряд» один раз рисуются кругами на скрытом холсте-атласе, а кадр собирается из drawImage. Это и быстрее, и выглядит одинаково везде.

Как рождается разряд

Молния — это случайное блуждание сверху вниз. Каждый шаг немного опускается и случайно смещается вбок; иногда от канала отходит ветка, у которой может быть своя ветка.

const nx = x + drift * 1.5 + (Math.random() - 0.5) * 4.5
const ny = y + 1 + Math.random() * 2.2
if (depth < 2 && Math.random() < (depth ? 0.06 : 0.11)) {
  walk(nx, ny, side, 6 + Math.random() * 14, power * 0.45, depth + 1)
}

Получившиеся отрезки один раз растеризуются в поле света: для каждой клетки берётся расстояние до ближайшего отрезка, из него считается яркое узкое ядро плюс мягкое гало. Заодно для клетки запоминается, на какой высоте её «достаёт» канал. Это понадобится для лидера.

Таймлайн вспышки

Настоящая молния не загорается целиком. Сначала вниз ползёт тусклый ступенчатый лидер, потом проходит главный разряд, который мерцает несколько раз и гаснет. Это ровно три фазы:

  1. Лидер (0,22 с): видны только клетки выше текущей отметки, с лёгким случайным дрожанием.
  2. Мерцание: яркость идёт по последовательности [1, 0.25, 0.95, 0.15, 0.8, 0.5], по 60 мс на шаг.
  3. Послесвечение: экспоненциальное затухание за 0,7 с.

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

Производительность и доступность

  • Анимация идёт с частотой 24 кадра в секунду, а не 60: для точечной графики разницы не видно, а работы втрое меньше.
  • В фоновой вкладке цикл останавливается.
  • Если в системе включено «уменьшить движение», рисуется один статичный кадр в момент удара.
  • Холст подстраивается под размер блока через ResizeObserver.

Исходники лежат в репозитории сайта, компонент — components/ui/ascii-lightning.tsx.