Ascii Sharks — живой океан из ASCII-акул и рыб на canvas

· 6 мин чтения
typescript frontend animation creative-coding open-source
📂 Исходный код на GitHub

ASCII-акулы на TypeScript: симуляция искусственной жизни на canvas — косяки рыб (boids), голодные акулы-охотники, светящийся кильватерный след и мерцающие точки. Ноль зависимостей, лицензия MIT.

Ascii Sharks — живой океан из ASCII-акул и рыб на canvas

Если hero-секции сайта не хватает жизни, обычно ставят видео или WebGL-сцену. Ascii-sharks предлагает путь дешевле: целый океан, нарисованный обычными символами на canvas. Косяки рыб мечутся по экрану, акулы охотятся на них, и всё это собрано из глифов моноширинного шрифта. Работает на чистом TypeScript без единой runtime-зависимости.

Репозиторий — stackedlol/ascii-sharks. У проекта уже почти тысяча звёзд на GitHub, лицензия MIT. В npm пакет не публикуется: в package.json стоит private: true, код подключается прямо из репозитория. Из dev-зависимостей только TypeScript и Vite.

Несмотря на игрушечную тему, внутри аккуратная симуляция искусственной жизни. Файл src/world.ts открывается комментарием, который честно описывает всю механику, — и дальше код ему соответствует. Получается редкое сочетание: проект можно поставить на сайт и заодно разобрать как небольшой учебник по стаям, хищникам и ASCII-рендерингу.

Что внутри

Весь код умещается в папке src из семи файлов. Наружу торчат четыре сущности:

  • World — мир рыб и акул, вся логика симуляции;
  • OceanEngine — движок: цикл с фиксированным шагом, ресайз, ввод, пауза вне экрана;
  • AsciiRenderer и функция paintGrid — отрисовка сетки символов;
  • AsciiOcean — готовый React-компонент.

Быстрый старт

npm install && npm run dev

Минимальный пример из README:

import { OceanEngine } from "./src";

const engine = new OceanEngine(canvas, [{ kind: "hammerhead", size: 1 }]);

Конструктор принимает canvas и список акул. У каждой акулы два параметра: kind — вид, size — относительный размер, где 1 у самой крупной. Если список не передать, в воде плавают четыре классические акулы на разной «глубине».

Как ведут себя рыбы

Рыбы — классические бойды (boids). На каждую действуют три силы: разделение, выравнивание и сплочённость. Поверх базовой модели добавлены детали, которые и делают картинку живой:

  • Слепая зона. Рыба не видит соседей прямо позади себя.
  • Волна тревоги. Испуг передаётся от рыбы к рыбе и чуть слабеет с каждым звеном цепочки. Школа срывается бегством, даже когда акулу видит только край косяка.
  • Эффект «фонтана». Убегающие рыбы расходятся в стороны от траектории хищника. Именно так разлетается настоящий кормовой шар.
  • Скорость растёт вместе с испугом: спокойная стая движется лениво, потревоженная несётся.
  • Фоновое течение и немного индивидуальной суеты не дают картинке застыть.

«Личное пространство» задано эллипсом в клетках сетки: каждая рыба занимает свою ячейку и не слипается с соседями в один символ. Соседей ищет пространственный хеш, поэтому симуляция держит от 85 до 340 рыб — число зависит от площади канваса.

Как ведут себя акулы

Тело акулы — «позвоночник» из 20 точек. Голова задаёт направление, остальные точки догоняют её, но сустав гнётся ограниченно: тело может сложиться в дугу, а в узел — нет. Дальше начинается охотничье поведение:

  • Голод растёт со временем и падает после каждой добычи.
  • Когда голод переваливает за порог, акула начинает преследование и целится не в рыбу, а в точку, где та скоро окажется.
  • Добыча ищется в переднем конусе зрения, и акула выбирает одиночек: плотная стая сбивает хищника с толку.
  • Разворот на месте невозможен — радиус поворота растёт со скоростью.
  • Съеденные рыбы через несколько секунд возвращаются в виде новых стай, которые заплывают с краёв экрана. Численность держится постоянной.

Курсором тоже можно вмешаться: движение мыши пугает рыб, а клик создаёт «всплеск» — расходящуюся волну испуга.

Как из символов собирается картинка

Рендерер в src/renderer.ts рисует сетку ASCII-ячеек поверх чёрного фона. Это самая вкусная часть проекта:

  • Фон — чёрная сетка с едва заметными линиями ячеек.
  • Кильватерный след. Там, где плывут рыбы и машут хвостом акулы, в поле «энергии» попадают точки. Поле размывается и гаснет, а клетки с сильным следом подсвечиваются голубыми точками.
  • Рыбы — короткое тире -, по одному на клетку. Дальние бледнее, ближние ярче, а рыбу на резком повороте или в панике выдаёт блик.
  • Акулы собираются старательно. У каждого вида есть своя математическая форма: профиль тела, грудные и брюшные плавники, спинной плавник, жаберные щели (их пять), глаза. Каждая клетка проверяется пятью точками выборки, а частично покрытые клетки получают «краевые» глифы — силуэт выходит сглаженным.
  • Плотность символов повторяет светотень: на светлой коже знаки реже, в тени плотнее. Рампа кожи — :;t%SX8#@.
  • Тигровая акула получает волнистые полосы, зебровая — пятна леопарда, а у гигантской акулы-молота «молот» шире и с выемкой на переднем крае.
  • Цвета шкуры чуть перебрасываются по собственному таймеру, поэтому кожа мерцает, и по ней изредка пробегают акцентные цвета.

Виды рыб и акул

В src/species.ts пять видов рыб: sardine, anchovy, silverside, mackerel, goby. У каждого своя крейсерская и максимальная скорость, размер стаи и свой набор весов поведения. От смеси весов зависит форма стаи: сильное выравнивание собирает плотные «ленты», слабое превращает стаю в рой, а параметр mill закручивает косяк во вращающийся кормовой шар. В комментарии прямо назван источник — статья Couzin et al. 2002.

Вид Размер стаи Характер
sardine 18–28 закручивается в кормовой шар
anchovy 20–32 плотные быстрые стаи
silverside 12–20 средний профиль
mackerel 5–9 небольшие спокойные группы
goby 8–12 дёрганая и суетливая

Акулы восьми видов: classic, hammerhead, zebra, tiger, greathammer, mako, thresher, nurse. Для каждого вида в рендерере задана отдельная форма — вплоть до положения глаз: у молотоголовых они сидят на краях «молота».

React-компонент

В src/react.tsx лежит компонент AsciiOcean:

<AsciiOcean sharks={[{ kind: "hammerhead", size: 1 }]} />

Компонент создаёт движок в useEffect и вызывает destroy() при размонтировании. Состав акул фиксируется на всю жизнь канваса — это осознанное ограничение из кода. Канвас помечен aria-hidden="true": для скринридеров весь океан — чистая декорация. Ещё одна деталь из комментария в коде: глифы берутся из font-family канваса, поэтому самому канвасу или родителю нужен моноширинный шрифт.

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

В src/engine.ts собраны вещи, о которых в демо-проектах обычно забывают:

  • IntersectionObserver ставит симуляцию на паузу, пока канвас вне экрана.
  • При prefers-reduced-motion анимация не запускается: мир тихо прокручивается на 240 шагов и рисуется один статичный кадр.
  • devicePixelRatio ограничен двойкой.
  • Цикл идёт с фиксированным шагом 1/60 секунды и максимум четырьмя шагами за кадр: на слабом устройстве симуляция замедлится, но не рассыплется.
  • Когда моноширинный шрифт догружается, статичный кадр перерисовывается заново.

Демо и настройка

Файл demo.ts — страница с канвасом на весь экран. Состав акул задаётся прямо в URL: ?sharks=thresher,hammerhead,mako,nurse. Первая акула в списке самая большая, остальные уменьшаются с шагом 0.1. Без параметра плывут четыре классические акулы. Второй аргумент конструктора World — fishCount: точное число рыб, если нужно, например, посадить в сайдбар одну маленькую стаю.

Где пригодится

Сценарии понятны: живые hero-секции, страницы 404, портфолио и любые места, где хочется анимации без видеофайла. Код забирается в проект целиком, не тянет зависимостей и легко ужимается под свою задачу — вплоть до одной стаи в углу страницы. А тем, кто хочет разобраться с бойдами и стайным поведением, файлы читаются сверху вниз как готовый конспект: сначала модель мира, потом виды, потом рендер и движок.

Источник: https://github.com/stackedlol/ascii-sharks