Цикл draw() и кадры в секунду
Анимация — это просто много неподвижных картинок, которые сменяют друг друга так быстро, что глаз видит движение; в p5.js за эти картинки отвечает функция draw().
draw() — функция p5.js, которая повторяется много раз в секунду и отрисовывает каждый новый кадр (frame) — одно изображение анимации. Кадры идут подряд, и из них складывается иллюзия движения.
В первом скетче с setup() и draw() ты уже видел эти две функции рядом и, может быть, удивился: зачем нужна вторая, если картинка и так стоит на месте? Сейчас разберёмся по-настоящему — и именно отсюда начинается вся анимация в курсе. Цыплёнок CodeChick, который до этого просто сидел на холсте, наконец-то задвигается.
Зачем это нужно: флипбук в твоём коде
Вспомни мультики на полях тетради. Рисуешь человечка в углу страницы, на следующей странице — чуть-чуть сдвигаешь руку, на следующей — ещё чуть-чуть. А потом быстро отпускаешь стопку листов из-под большого пальца — и человечек машет рукой. Каждый лист сам по себе неподвижен. Движение появляется только потому, что листы сменяются быстро.
Это и есть флипбук — и ровно так работает любая анимация на экране: в играх, в TikTok, в твоей любимой заставке на телефоне. Никто не рисует «движение». Рисуют много отдельных кадров, а движение — это иллюзия, которую достраивает твой мозг, когда кадры мелькают достаточно быстро.
Тот же фокус стоит за всем, что движется на твоих экранах. Когда герой в игре бежит — это не «бегущая картинка», а десятки поз, нарисованных по очереди. Когда в ленте крутится сторис — это кадры, сменяющие друг друга. Даже плавный скролл и анимация лайка — те же самые сменяющиеся изображения. Освоив draw(), ты получаешь ровно тот инструмент, которым сделаны все эти штуки: бесконечный конвейер кадров, в каждый из которых ты можешь вмешаться.
В p5.js страницы флипбука перелистывает за тебя функция draw(). Ты пишешь в ней, как нарисовать один кадр, а p5.js сам вызывает её снова и снова — десятки раз в секунду. Если в каждом следующем вызове цыплёнок будет нарисован чуть правее предыдущего — он поедет по холсту. Вот к чему мы придём к концу урока: CodeChick, который сам собой плывёт слева направо, без единой кнопки и без всякой магии — только за счёт того, что draw() крутится по кругу.
draw() — это бесконечный цикл, который рисует кадры
Главная мысль урока, ради которой всё затевалось: p5.js вызывает draw() не один раз, а постоянно, по кругу, пока открыт скетч. Закончилась функция — p5.js тут же запускает её заново. И так десятки раз в секунду.
Сравни с setup(): та выполняется один раз в самом начале — как подготовка сцены перед спектаклем. А draw() — это сам спектакль, который идёт кадр за кадром, пока ты не закроешь вкладку.
frameRate — количество кадров, которые p5.js рисует за одну секунду. По умолчанию это около 60: то есть
draw()успевает выполниться примерно 60 раз, пока на часах тикнет одна секунда.
Шестьдесят раз в секунду — это очень много. Если в каждом кадре стирать старую картинку и рисовать новую, чуть сдвинутую, глаз не успевает заметить отдельные кадры и видит плавное движение. Ровно как стопка листов из-под большого пальца.
Тут возникает естественный вопрос: а как тогда вообще что-то меняется от кадра к кадру, если код в draw() каждый раз один и тот же? Секрет в том, что между кадрами p5.js успевает обновить пару своих внутренних чисел — например, счётчик кадров. Сам твой код не меняется, а вот числа, на которые он опирается, — да. Поэтому достаточно нарисовать цыплёнка не по жёстко заданной координате, а по координате, которая вычисляется из такого меняющегося числа. И каждый новый кадр цыплёнок окажется чуть в другом месте — хотя строка кода всё та же. Именно на этом и держится весь фокус, и сейчас мы увидим его вживую.
Почему фон рисуют заново в каждом кадре
Тут прячется первая важная деталь. Холст не очищается сам. Всё, что ты нарисовал в прошлом кадре, остаётся лежать на месте, пока ты не закрасишь его сверху. Поэтому первой строкой в draw() почти всегда стоит background(...) — она заливает весь холст цветом и стирает предыдущий кадр, давая чистый лист для нового рисунка. Если её убрать — увидишь не движение, а след-шлейф (и это иногда полезно, но об этом ниже).
Разбираем на примерах
Пример 1. Считаем кадры вслух
Сначала убедимся своими глазами, что draw() правда крутится без остановки. У p5.js есть встроенный счётчик frameCount — это число, которое начинается с 1 и увеличивается на единицу с каждым новым кадром. Просто выведем его на холст.
function setup() {
createCanvas(400, 400);
textAlign(CENTER, CENTER);
textSize(48);
}
function draw() {
background(230, 245, 255); // светло-голубое небо
fill(40);
text(frameCount, 200, 200); // показываем номер кадра
}Результат: на светло-голубом холсте по центру крупно стоит число, и оно стремительно растёт: 1, 2, 3 … за пару секунд улетает за сотню, за десяток секунд — за тысячу. Цифры мелькают так быстро, что отдельные значения почти не прочитать. Это и есть доказательство, что draw() вызывается снова и снова, а не один раз.
Разберём по строкам:
createCanvas(400, 400)и настройки текста стоят вsetup()— их достаточно задать один раз.background(230, 245, 255)в началеdraw()стирает прошлый кадр, иначе числа накладывались бы друг на друга кашей.text(frameCount, 200, 200)рисует текущее значение счётчика по центру. В каждом следующем кадреframeCountуже на единицу больше — поэтому число растёт.
Запомни frameCount: это твой главный помощник в анимации. Он как номер страницы во флипбуке — всегда знает, какой сейчас кадр по счёту.
Пример 2. CodeChick едет по холсту
Теперь главное — заставим цыплёнка двигаться. Идея проста: координата X тела будет зависеть от номера кадра. Чем больше кадров прошло, тем правее цыплёнок.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(230, 245, 255); // стираем прошлый кадр
let x = frameCount * 2; // с каждым кадром на 2 пикселя правее
// тело цыплёнка
noStroke();
fill(255, 220, 60);
circle(x, 200, 80);
// клюв
fill(245, 130, 20);
triangle(x + 30, 195, x + 55, 200, x + 30, 210);
}Результат: жёлтый цыплёнок с оранжевым клювом плавно выезжает из левого края холста и катится вправо. Через несколько секунд он уходит за правый край и пропадает — потому что x становится больше 400, и круг рисуется уже за пределами видимой области.
Что здесь происходит по шагам:
frameCount * 2— в кадре №10 это дастx = 20, в кадре №100 —x = 200, в кадре №200 —x = 400. Число кадров растёт равномерно, значит и X растёт равномерно — движение получается ровным.background(...)в начале обязателен: он стирает цыплёнка с прошлого места, прежде чем нарисовать на новом. Без него ты увидишь жёлтую полосу-размазню (попробуй убрать строку и посмотри сам — это нагляднее любых слов).- Клюв нарисован относительно того же
x(x + 30и так далее), поэтому он едет вместе с телом, а не отрывается.
Меняй число в frameCount * 2: поставь * 1 — цыплёнок поползёт медленнее, поставь * 5 — умчится пулей. Это первое, что стоит пощупать руками.
Пример 3. Управляем частотой кадров через frameRate()
А что если замедлить сам флипбук — листать страницы реже? За это отвечает функция frameRate(число): она говорит p5.js, сколько кадров рисовать в секунду. Поставим всего 4 кадра в секунду и снова покажем счётчик.
function setup() {
createCanvas(400, 400);
frameRate(4); // всего 4 кадра в секунду
textAlign(CENTER, CENTER);
textSize(48);
}
function draw() {
background(230, 245, 255);
fill(40);
text(frameCount, 200, 200);
}Результат: тот же скетч со счётчиком, что и в первом примере, но теперь число меняется заметно медленнее — примерно четыре раза в секунду, как тиканье. Ты успеваешь прочитать каждую цифру: 1 … 2 … 3 … 4. Видно, что draw() вызывается реже, и каждый кадр «живёт» дольше.
Несколько важных мыслей про frameRate():
frameRate()ставят обычно вsetup()— задал желаемую частоту один раз, и она держится весь скетч.- Это пожелание, а не приказ: если кадр получается тяжёлым (много фигур, сложные вычисления), p5.js может не успеть, и реальная частота окажется ниже заданной. Маленькие числа вроде 4 или 10 он выдержит легко, а вот 200 — вряд ли.
- Низкий
frameRateделает движение рваным, «по слогам». Высокий (около 60) — плавным. Для анимации обычно оставляют значение по умолчанию, аframeRate(4)удобно ставить, чтобы разглядеть, как именно меняются кадры.
Соедини это с примером 2: добавь frameRate(10) в setup к едущему цыплёнку — и увидишь, как плавный полёт превращается в прыжки-скачки, будто старый GIF. Кадров стало меньше, между соседними положениями цыплёнка теперь большие промежутки.
Частые ошибки и подводные камни
На этих граблях спотыкаются почти все, кто впервые делает анимацию. Узнаешь их заранее — сэкономишь кучу нервов.
1. Забыл background() — вместо движения получается шлейф
Самая частая история. Ты двигаешь цыплёнка, но background(...) в draw() не поставил. Результат — цыплёнок не едет, а размазывается жёлтой колбасой через весь холст, потому что каждый новый круг рисуется поверх старых, а старые никто не стёр. Лечится одной строкой: background(...) первым делом в draw(). Кстати, иногда такой шлейф рисуют специально — но это уже осознанный приём, а не случайность.
2. Поставил createCanvas() внутрь draw()
Если написать createCanvas(400, 400) в draw(), холст будет пересоздаваться 60 раз в секунду — скетч начнёт тормозить и мигать, а иногда появится несколько холстов подряд. Холст создаётся один раз и навсегда, поэтому createCanvas() живёт только в setup(). То же касается textSize(), загрузки картинок и других разовых настроек.
3. Ждёшь, что frameRate(1000) ускорит всё в разы
Заказать можно любое число, но видеокарта и экран имеют предел — обычно те самые 60 кадров в секунду. Поставишь frameRate(1000) — реально получишь около 60, не больше. frameRate() хорошо работает на замедление (4, 10, 30), а вот разогнать скетч выше возможностей экрана он не может.
4. Двигаешь объект числом, которое не растёт
Новички иногда пишут в draw() что-то вроде let x = 200; и удивляются, почему цыплёнок стоит. А он и не должен ехать: x каждый кадр заново становится одним и тем же числом 200. Чтобы было движение, координата должна меняться от кадра к кадру — например, зависеть от frameCount или от переменной, которую ты сам увеличиваешь. Неизменное число — неподвижная картинка.
5. Путаешь frameCount и frameRate
Похожие имена, разный смысл. frameCount — это сколько кадров уже прошло (растущий счётчик: 1, 2, 3 …). frameRate — это сколько кадров в секунду (скорость флипбука). Первое ты обычно читаешь, чтобы понять «который сейчас кадр», второе — задаёшь функцией frameRate(), чтобы управлять темпом. Запомни: Count — считает, Rate — про скорость.
Мини-проект: цыплёнок-метроном
Теперь твоя очередь. Возьми пример 2 (едущий цыплёнок) за основу и сделай так, чтобы CodeChick не уезжал за край, а ходил туда-сюда — как маятник метронома или как персонаж в платформере, патрулирующий уступ.
План действий:
- Оставь
background(...)первой строкойdraw()— без него снова получишь шлейф. - Вместо
frameCount * 2попробуй формулу, которая колеблется. Подойдёт встроенная функция:let x = 200 + sin(frameCount * 0.05) * 120;. Не пугайсяsin— это просто число, которое плавно качается туда-сюда между -1 и 1; мы растягиваем его до ±120 пикселей вокруг центра холста (200). - Нарисуй тело и клюв относительно этого
x, как в примере 2. - Поиграй числами:
0.05отвечает за скорость покачивания (больше — быстрее),120— за размах (больше — шире амплитуда).
Когда заработает — цыплёнок будет плавно скользить от левого края к правому и обратно, бесконечно. Хочешь хардкора? Добавь frameRate(8) в setup и увидишь, как плавный маятник станет «пошаговым», как в пиксельной игре. Поменяй число и посмотри, что будет — именно так и нащупывается интуиция аниматора.
Итоги
Сегодня ты понял, как из неподвижных картинок рождается движение:
draw()— это бесконечный цикл: p5.js вызывает её снова и снова, рисуя кадр (frame) за кадром.setup()выполняется один раз (подготовка),draw()— постоянно (сам мультик).background(...)в началеdraw()стирает прошлый кадр — без него движение превращается в шлейф.frameCount— растущий счётчик кадров; привязав к нему координату, ты заставляешь объект двигаться.frameRate(число)задаёт частоту кадров — скорость флипбука; это пожелание, ограниченное возможностями экрана (около 60).
Главная мысль: анимация — это не «нарисовать движение», а «нарисовать много кадров, в каждом чуть-чуть по-другому». Ты уже умеешь сдвигать цыплёнка по холсту, и это фундамент всего, что будет дальше.
В следующем уроке мы перестанем привязывать движение прямо к frameCount и заведём собственную переменную состояния — отдельное число, которое хранит позицию цыплёнка между кадрами и которое мы будем менять по своим правилам. Это даст куда больше свободы: цыплёнок сможет ускоряться, останавливаться и отскакивать от стен. А пока — погоняй своего метронома и порадуйся: твой CodeChick наконец ожил.