Время вместо кадров: millis()
Считать кадры — всё равно что измерять путь шагами друга: у каждого шаг свой. Время измеряет одинаково для всех — и millis() даёт тебе эти самые секунды.
Главное правило урока: если анимация привязана к номеру кадра, на быстром устройстве она летит, а на медленном — тормозит. Привяжи её ко времени через
millis()— и она пойдёт одинаково везде.
Зачем тебе считать время, а не кадры
Представь: ты записал классную анимацию, где CodeChick прыгает на месте раз в секунду. На твоём ноутбуке всё идеально. Ты скидываешь ссылку другу — а у него цыплёнок дёргается как сумасшедший, прыгает по три раза в секунду. У другого друга на стареньком телефоне, наоборот, еле шевелится. Ты ничего не менял в коде — почему же у всех по-разному?
Разгадка вот в чём. В прошлом уроке про цикл draw() и кадры в секунду мы выяснили, что draw() повторяется снова и снова, рисуя кадры. Но сколько именно кадров в секунду нарисует p5.js — заранее неизвестно. На мощном компьютере это легко 60 кадров, на слабом телефоне — может быть 25, а если вкладка свёрнута — браузер вообще притормаживает отрисовку. И вот тут кроется ловушка: если ты считаешь движение по кадрам, скорость анимации зависит от железа.
Это как договориться с друзьями встретиться «через 200 шагов от метро». У длинноногого баскетболиста 200 шагов — это полкилометра, а у малыша — двор. Шаги у всех разные. А вот если сказать «через 3 минуты ходьбы» — это уже честно: время-то у всех одно. Сегодня мы научим CodeChick двигаться по времени, а не по шагам, и тогда он будет вести себя одинаково на любом устройстве.
К концу урока ты сделаешь цыплёнка, который ровно раз в секунду подпрыгивает и моргает по таймеру — и эта анимация пойдёт с одинаковой скоростью хоть на игровом ПК, хоть на бабушкином планшете. Поехали разбираться.
Кстати, это не какая-то экзотическая проблема для гиков. Та же самая беда подстерегала разработчиков старых игр: персонаж, привязанный к кадрам, на новом компьютере начинал носиться вдвое быстрее, и игра становилась непроходимой. Видео в любимом сериале, музыкальный визуализатор в плеере, плавная прокрутка ленты в соцсети — всё это под капотом считает время, а не кадры, именно чтобы выглядеть одинаково у каждого зрителя. Освоив millis() на цыплёнке, ты возьмёшь в руки тот же приём, на котором держится вся «гладкость» цифрового мира вокруг тебя.
Кадры против времени: в чём разница
frameCount — счётчик шагов
У p5.js есть встроенная переменная frameCount — это номер текущего кадра. На первом кадре она равна 1, на втором — 2, и так растёт всё время, пока работает скетч. Кажется удобным: хочешь движение — двигай по frameCount.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(135, 206, 235);
// двигаем цыплёнка по номеру кадра
let x = frameCount * 2;
fill(255, 209, 64);
circle(x, 200, 80);
}Результат: жёлтый круг-цыплёнок едет слева направо по голубому небу. Но скорость движения целиком зависит от того, как быстро устройство рисует кадры: на быстром он пролетит холст за пару секунд, на медленном — будет ползти. Одинаковости нет.
Проблема в множителе * 2. Он означает «сдвигайся на 2 пикселя каждый кадр». Но кадры приходят с разной частотой! 2 пикселя за кадр при 60 кадрах в секунду — это 120 пикселей в секунду. А при 30 кадрах — только 60. Один и тот же код, вдвое разная скорость.
millis() — честные часы скетча
А теперь знакомься с героем урока. millis() — это функция p5.js, которая возвращает, сколько миллисекунд прошло с запуска скетча. Миллисекунда — это одна тысячная секунды, так что 1000 миллисекунд = 1 секунда. millis() — это как секундомер, который запустился в момент старта программы и тикает сам по себе, не глядя на кадры.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(135, 206, 235);
// двигаем цыплёнка по времени
let seconds = millis() / 1000; // прошло секунд
let x = seconds * 60; // 60 пикселей в секунду
fill(255, 209, 64);
circle(x, 200, 80);
}Результат: тот же жёлтый цыплёнок едет вправо, но теперь со скоростью ровно 60 пикселей в секунду — и эта скорость одинакова на любом устройстве. Через секунду после старта он будет на x=60, через две — на x=120, и неважно, сколько кадров успел нарисовать твой телефон между этими моментами.
Почувствуй разницу в формулировке. Раньше было «сдвигайся на 2 пикселя за кадр» — а кадры у всех разные. Теперь «будь на позиции, равной 60 × число прошедших секунд» — а секунды у всех одинаковые. Мы перестали считать шаги и начали смотреть на часы.
Заметь ещё одну важную мелочь: в варианте с frameCount мы как бы накапливали движение — каждый кадр добавлял к прошлому ещё чуть-чуть. А в варианте с millis() мы каждый кадр вычисляем позицию заново прямо из времени. Это два разных образа мышления. Накопление чувствительно к тому, сколько раз сработал draw(); вычисление от времени — нет. Если один кадр случайно «пропадёт» (браузер задумался), при накоплении ты потеряешь кусочек движения, а при расчёте от времени цыплёнок просто окажется там, где и должен быть по часам. Время прощает пропущенные кадры — счёт кадров нет.
Как это работает по шагам
Превращаем миллисекунды в удобные числа
Сырые миллисекунды — это большие числа: уже через 5 секунд millis() вернёт 5000. Работать напрямую с тысячами неудобно, поэтому почти всегда первым делом их делят на 1000, чтобы получить секунды:
let t = millis() / 1000; // t — это секунды с момента старта (дробное число)Теперь t — это, например, 3.5 (три с половиной секунды). С таким числом удобно строить любое движение: умножай его на скорость, подставляй в sin() для качания, сравнивай с порогом для событий. Всё, что раньше ты делал через frameCount, теперь делается через t — но честно по времени.
Запомни такую простую таблицу-перевод — она поможет, когда захочешь переделать старую анимацию «по кадрам» на анимацию «по времени»:
| Что хочешь | По кадрам (хрупко) | По времени (надёжно) |
| Ехать вправо | x = frameCount * 2 | x = t * 60 |
| Плавно качаться | sin(frameCount * 0.1) | sin(t * TWO_PI) |
| Событие по кругу | frameCount % 60 == 0 | t % 1 < 0.05 |
Правый столбец читается как обычные слова: «60 пикселей в секунду», «один полный цикл качания в секунду», «срабатывать в начале каждой секунды». Левый же столбец молчит про секунды и держит в уме невидимое предположение «у меня ровно 60 кадров в секунду» — а это предположение и ломается на чужих устройствах.
Прыжок цыплёнка раз в секунду
Сделаем так, чтобы CodeChick подпрыгивал ровно раз в секунду, плавно вверх и вниз. Для плавной волны нам пригодится синус — функция, которая мягко колеблется от -1 до 1. Не пугайся слова «синус»: думай о нём как о готовом «качельном» движении, которое p5.js рисует за тебя.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(135, 206, 235);
let t = millis() / 1000; // секунды с старта
// sin делает полный цикл за 2*PI секунд; умножим t, чтобы цикл был ~1 сек
let jump = sin(t * TWO_PI) * 40; // колебание от -40 до +40
let y = 250 - abs(jump); // вычитаем модуль: цыплёнок только подпрыгивает вверх
// тело CodeChick
fill(255, 209, 64);
noStroke();
circle(200, y, 100);
// клюв
fill(255, 140, 30);
triangle(200, y - 6, 240, y + 4, 200, y + 18);
}Результат: жёлтый цыплёнок с оранжевым клювом ритмично подпрыгивает по центру неба — вверх и обратно вниз примерно раз в секунду. Прыжок плавный, без рывков, и его темп не зависит от мощности устройства: время-то идёт одинаково.
Разберём ключевые строки. t * TWO_PI — TWO_PI это полный круг (≈6.28), и синус проходит свой полный цикл как раз за такой аргумент; значит, за одну секунду t вырастает на 1 и волна делает ровно один цикл — один прыжок в секунду. * 40 задаёт высоту прыжка в пикселях. А abs() берёт модуль (убирает минус), чтобы цыплёнок прыгал только вверх от линии 250, а не нырял под неё.
Здесь стоит остановиться на главном выигрыше. Самое классное в этом коде — то, чего в нём нет: тут нигде не написано «двигайся на столько-то за кадр». Позиция цыплёнка целиком вычисляется из t, то есть из времени. Поставь скетч на паузу, открой его завтра, перетащи окно на другой монитор с другой частотой обновления — ритм прыжка не собьётся ни на йоту, потому что он привязан к секундам, а не к кадрам. Хочешь прыжки повыше — меняй 40. Хочешь медленнее — уменьшай множитель перед TWO_PI (например, t * TWO_PI * 0.5 даст прыжок раз в две секунды). Каждое число в этой формуле имеет понятный смысл во «времени и пикселях», а не в загадочных «кадрах».
Событие по таймеру: моргаем каждые 2 секунды
Время удобно не только для плавного движения, но и для событий по расписанию. Допустим, мы хотим, чтобы CodeChick моргал: закрывал глаз на короткий миг каждые 2 секунды. Тут поможет оператор остатка от деления %.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(135, 206, 235);
let t = millis() / 1000;
// остаток от деления на 2 даёт число от 0 до 2, повторяющееся по кругу
let cycle = t % 2;
// глаз закрыт первые 0.15 секунды каждого 2-секундного цикла
let blinking = cycle < 0.15;
// тело
fill(255, 209, 64);
noStroke();
circle(200, 220, 120);
// глаз: открыт — кружок, закрыт — чёрточка
fill(40);
if (blinking) {
rect(178, 200, 24, 3); // закрытый глаз — линия
} else {
circle(190, 200, 18); // открытый глаз — кружок
}
}Результат: жёлтый цыплёнок смотрит на тебя круглым тёмным глазом, и примерно раз в две секунды глаз на мгновение превращается в тонкую чёрточку — цыплёнок моргает. Ритм морганий ровный и одинаковый на любом устройстве.
Магия в строке t % 2. Оператор % возвращает остаток от деления: для времени он создаёт «пилу», которая растёт от 0 до 2, потом сбрасывается в 0 и снова растёт. Так мы получаем повторяющийся 2-секундный цикл из непрерывно бегущего времени. А условие cycle < 0.15 ловит самое начало каждого цикла — те самые 0.15 секунды, когда глаз закрыт.
Этот приём — остаток от деления времени — стоит выучить отдельно, потому что им делается уйма всего. Хочешь, чтобы что-то мигало раз в секунду? t % 1. Раз в три секунды? t % 3. Светофор, который держит зелёный 4 секунды, потом жёлтый 1 секунду, потом красный 4? Всё это — игра с одним числом внутри % и парой условий. Меняя порог (0.15), ты управляешь, как долго длится «закрытое» состояние: больше число — дольше цыплёнок держит глаз закрытым, будто щурится на солнце. Попробуй поставить 0.5 — и увидишь сонного, медленно моргающего цыплёнка вместо бодрого.
Частые ошибки и подводные камни
Двигаешь по frameCount и удивляешься разной скорости. Это корень всех бед.
frameCountрастёт со скоростью кадров, а она у всех разная. Если хочешь стабильную скорость — считай поmillis(), а не по кадрам.Забыл поделить на 1000.
millis()отдаёт миллисекунды, а не секунды. Если написатьlet x = millis() * 60, цыплёнок мгновенно улетит за край холста — ведь уже через секунду это 60000 пикселей. Почти всегда первым делом пишиmillis() / 1000.Сравниваешь millis() напрямую с маленьким числом. Условие вроде
if (millis() == 1000)почти наверняка не сработает: между кадрами время прыгает скачками (например, с 990 сразу на 1007), и точное значение 1000 может проскочить мимо. Используй интервалы (millis() % 1000 < 50) или сравнение «больше/меньше», а не строгое равенство.Путаешь millis() и frameRate().
frameRate()— это про то, сколько кадров в секунду рисовать.millis()— это про то, сколько времени уже прошло. Они о разном: первое управляет частотой отрисовки, второе просто измеряет время.Ждёшь, что millis() сбросится сам.
millis()только растёт от старта скетча и никогда не обнуляется сам по себе. Чтобы получить повторяющийся цикл, используй остаток%— он и делает из бесконечно растущего времени аккуратную «пилу».
Мини-практика: оживляем CodeChick по таймеру
Возьми пример с прыжком за основу и собери из приёмов урока маленькую сценку. Вот план:
- Заставь цыплёнка прыгать раз в 1.5 секунды, а не раз в секунду. Подумай: на что умножить
tвнутриsin(), чтобы цикл стал длиннее? (Подсказка: меньший множитель — медленнее цикл.) - Добавь моргание каждые 3 секунды поверх прыжка — совмести оба приёма в одном
draw(). Глаз должен моргать, пока цыплёнок прыгает. - Сделай так, чтобы через 5 секунд после старта небо плавно темнело (используй
millis()и сравнение со временем, чтобы понять, что «пора»). Цыплёнок встречает закат по таймеру.
Меняй числа и смотри, как перестраивается ритм. Открой свой скетч на телефоне и на компьютере одновременно — и убедись, что теперь цыплёнок ведёт себя одинаково. Это и есть та самая независимость от частоты кадров, ради которой мы всё затеяли.
Итоги
Сегодня ты сделал важный шаг от «движения на глаз» к честной, предсказуемой анимации:
- Движение по
frameCountзависит от железа: на быстром устройстве летит, на медленном тормозит. millis()возвращает миллисекунды с момента старта — это честные часы скетча, одинаковые для всех.- Почти всегда первым делом делай
let t = millis() / 1000, чтобы получить удобные секунды. - Умножай
tна скорость для движения, подставляй вsin()для плавных колебаний, бери остаток%для повторяющихся событий по таймеру.
Теперь твой CodeChick прыгает и моргает одинаково везде — он стал по-настоящему живым, а не «живым только на твоём ноутбуке». Дальше будет ещё интереснее: в следующем уроке мы научим цыплёнка реагировать на тебя — следить за курсором мыши и двигаться туда, куда ты ведёшь. Анимация по времени превратится в настоящий интерактив. Увидимся на следующей странице!