Цикл 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() вызывается снова и снова, а не один раз.

Разберём по строкам:

  1. createCanvas(400, 400) и настройки текста стоят в setup() — их достаточно задать один раз.
  2. background(230, 245, 255) в начале draw() стирает прошлый кадр, иначе числа накладывались бы друг на друга кашей.
  3. 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 не уезжал за край, а ходил туда-сюда — как маятник метронома или как персонаж в платформере, патрулирующий уступ.

План действий:

  1. Оставь background(...) первой строкой draw() — без него снова получишь шлейф.
  2. Вместо frameCount * 2 попробуй формулу, которая колеблется. Подойдёт встроенная функция: let x = 200 + sin(frameCount * 0.05) * 120;. Не пугайся sin — это просто число, которое плавно качается туда-сюда между -1 и 1; мы растягиваем его до ±120 пикселей вокруг центра холста (200).
  3. Нарисуй тело и клюв относительно этого x, как в примере 2.
  4. Поиграй числами: 0.05 отвечает за скорость покачивания (больше — быстрее), 120 — за размах (больше — шире амплитуда).

Когда заработает — цыплёнок будет плавно скользить от левого края к правому и обратно, бесконечно. Хочешь хардкора? Добавь frameRate(8) в setup и увидишь, как плавный маятник станет «пошаговым», как в пиксельной игре. Поменяй число и посмотри, что будет — именно так и нащупывается интуиция аниматора.

Итоги

Сегодня ты понял, как из неподвижных картинок рождается движение:

  • draw() — это бесконечный цикл: p5.js вызывает её снова и снова, рисуя кадр (frame) за кадром.
  • setup() выполняется один раз (подготовка), draw()постоянно (сам мультик).
  • background(...) в начале draw() стирает прошлый кадр — без него движение превращается в шлейф.
  • frameCount — растущий счётчик кадров; привязав к нему координату, ты заставляешь объект двигаться.
  • frameRate(число) задаёт частоту кадров — скорость флипбука; это пожелание, ограниченное возможностями экрана (около 60).

Главная мысль: анимация — это не «нарисовать движение», а «нарисовать много кадров, в каждом чуть-чуть по-другому». Ты уже умеешь сдвигать цыплёнка по холсту, и это фундамент всего, что будет дальше.

В следующем уроке мы перестанем привязывать движение прямо к frameCount и заведём собственную переменную состояния — отдельное число, которое хранит позицию цыплёнка между кадрами и которое мы будем менять по своим правилам. Это даст куда больше свободы: цыплёнок сможет ускоряться, останавливаться и отскакивать от стен. А пока — погоняй своего метронома и порадуйся: твой CodeChick наконец ожил.

Проверьте себя
1. Сколько раз p5.js вызывает функцию draw() за время работы скетча?
AРовно один раз, в самом начале
BПостоянно, много раз в секунду, по кругу
CОдин раз на каждое нажатие мыши
DСтолько раз, сколько фигур на холсте
2. Что произойдёт, если убрать background(...) из начала draw() в скетче, где цыплёнок едет вправо?
AЦыплёнок будет двигаться плавнее
BСкетч перестанет запускаться
CВместо движения останется размазанный жёлтый след-шлейф
DЦыплёнок начнёт двигаться в обратную сторону
3. Что такое frameCount?
AЧисло кадров, которые рисуются за одну секунду
BРастущий счётчик: сколько кадров уже прошло с начала скетча
CРазмер холста в пикселях
DЦвет фона текущего кадра
4. Ты вызвал frameRate(4) в setup(). Как изменится скетч со счётчиком frameCount?
AЧисло будет меняться примерно 4 раза в секунду — медленно и читаемо
BЧисло вырастет до 4 и остановится
CСкетч будет рисовать 4 холста подряд
DНичего не изменится: frameRate ни на что не влияет
5. Почему createCanvas() пишут в setup(), а не в draw()?
AТак короче печатать
BВ draw() функция createCanvas вообще не работает
CХолст нужно создать один раз; в draw() он пересоздавался бы 60 раз в секунду и скетч бы тормозил
DcreateCanvas() задаёт частоту кадров, а её меняют только в setup()
6. Цыплёнок не двигается, хотя в draw() есть строка let x = 200; и circle(x, 200, 80). В чём причина?
AНужно увеличить frameRate
Bx каждый кадр заново становится числом 200 и не меняется — для движения координата должна расти или колебаться
Ccircle() не умеет двигаться, нужен ellipse()
DНе хватает второго вызова background()