Ascii Sharks — живой океан из ASCII-акул и рыб на canvas
📂 Исходный код на GitHubASCII-акулы на TypeScript: симуляция искусственной жизни на canvas — косяки рыб (boids), голодные акулы-охотники, светящийся кильватерный след и мерцающие точки. Ноль зависимостей, лицензия MIT.
Если 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, портфолио и любые места, где хочется анимации без видеофайла. Код забирается в проект целиком, не тянет зависимостей и легко ужимается под свою задачу — вплоть до одной стаи в углу страницы. А тем, кто хочет разобраться с бойдами и стайным поведением, файлы читаются сверху вниз как готовый конспект: сначала модель мира, потом виды, потом рендер и движок.