rotate() и scale()
В прошлый раз мы сдвигали мир через translate(); теперь научимся его крутить и растягивать — и наконец заставим CodeChick кружиться и расти прямо на холсте.
Главная мысль урока: rotate() поворачивает не фигуру, а всю систему координат вокруг точки (0, 0), а scale() растягивает эту же сетку. Поэтому почти всегда сначала идёт translate(), потом rotate()/scale(), и только затем сама фигура.
Зачем вообще крутить мир
Представь, что ты листаешь ленту с короткими видео. Кто-то снял, как фигурка персонажа медленно вращается на месте, будто на витрине, а потом резко увеличивается, заполняя весь экран. Выглядит как магия, но за этим стоит ровно две функции: rotate() и scale(). И ты сегодня научишься делать то же самое — только с нашим цыплёнком CodeChick.
В прошлом уроке про translate() мы поняли важную вещь: когда ты вызываешь translate(x, y), ты не двигаешь фигуру — ты двигаешь всю сетку, всю систему координат. Начало координат (точка (0, 0)) уезжает в новое место, и дальше всё рисуется относительно него. С rotate() и scale() та же логика: ты не крутишь и не растягиваешь фигуру, ты крутишь и растягиваешь сам лист, на котором рисуешь.
Почему это вообще так устроено и зачем нам крутить именно лист, а не фигуру? Потому что один раз повернув или растянув сетку, ты можешь нарисовать поверх неё сколько угодно фигур — и все они сразу окажутся повёрнутыми. Это куда удобнее, чем считать новые координаты каждого уголка треугольника вручную через формулы с синусами и косинусами. Ты говоришь холсту «теперь рисуй наклонно», а дальше просто рисуешь как обычно. Этим приёмом пользуются буквально все: от анимации логотипов в заставках до вращающихся монеток в играх, которые ты собираешь по уровню. За красивым кадром почти всегда прячется пара строк трансформаций — и сегодня ты научишься их писать.
Вот к чему мы придём к концу урока — крутящийся и пульсирующий CodeChick:
let angle = 0;
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}
function draw() {
background(135, 206, 235);
translate(width / 2, height / 2);
rotate(angle);
let s = 1 + 0.3 * sin(angle * 4);
scale(s);
drawChick();
angle = angle + 1;
}
function drawChick() {
noStroke();
fill(255, 214, 10);
ellipse(0, 0, 80, 80);
fill(255, 153, 0);
triangle(35, -5, 35, 5, 55, 0);
fill(0);
ellipse(15, -15, 8, 8);
}Результат: в центре голубого неба жёлтый кружок-цыплёнок с оранжевым клювом и чёрным глазом плавно вращается по кругу и одновременно слегка пульсирует — то немного увеличивается, то уменьшается, как будто дышит. Не пугайся незнакомых строк: к концу урока ты разберёшь тут каждую.
rotate(): крутим не фигуру, а лист
Метафора простая. Возьми лист из скетчбука и нарисуй на нём цыплёнка в левом верхнем углу. А теперь поверни сам лист на сорок пять градусов — цыплёнок повернётся вместе с ним, потому что он нарисован на листе. Функция rotate() делает ровно это: поворачивает систему координат вокруг её начала — точки (0, 0).
Градусы и радианы
По умолчанию p5.js считает углы не в градусах, а в радианах. Это привычная математикам единица, но для нас, кто хочет «повернуть на 90», она неудобна. Поэтому в самом начале, в setup(), мы один раз говорим:
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}Результат: создаётся холст 400 на 400 пикселей, и теперь все углы в rotate() мы задаём в привычных градусах: 90 — четверть оборота, 180 — половина, 360 — полный круг.
Без этой строчки rotate(90) повернул бы мир на 90 радиан — это больше четырнадцати полных оборотов, и результат был бы непредсказуемым. Так что договорились: пишем angleMode(DEGREES) и думаем в градусах.
Маленькая шпаргалка, чтобы углы не путались в голове. Полный круг — это 360 градусов. Половина оборота, когда цыплёнок смотрит ровно в противоположную сторону, — 180. Четверть, поворот «вбок», — 90. А совсем лёгкий наклон, который заметен, но не ломает картинку, — 10–15 градусов. Если хочешь, чтобы что-то медленно вращалось без остановки, ты просто прибавляешь к углу по чуть-чуть каждый кадр — например по 1 градусу, — и за 360 кадров (примерно шесть секунд при стандартной частоте) получится один полный оборот. Чем больше прибавляешь, тем быстрее крутится.
Первый поворот
Посмотрим, что произойдёт, если просто повернуть и нарисовать квадрат:
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}
function draw() {
background(240);
rotate(30);
fill(255, 214, 10);
rect(50, 50, 100, 100);
}Результат: жёлтый квадрат нарисован не ровно, а наклонён на 30 градусов. Но наклонился он не вокруг своего центра, а уехал куда-то вбок и вверх — потому что поворот произошёл вокруг угла холста, точки (0, 0), а квадрат стоит далеко от неё. Это и есть та самая ловушка, ради которой нам нужен translate().
Почему translate() идёт перед rotate()
Вернёмся к листу из скетчбука. Когда ты крутишь лист, он вращается вокруг той точки, где ты его держишь пальцем. В p5.js этот «палец» по умолчанию стоит в левом верхнем углу — в точке (0, 0). Поэтому любой rotate() крутит мир вокруг угла холста.
А мы почти всегда хотим крутить вокруг центра объекта. Решение: сначала translate() переносит начало координат туда, где должна быть «булавка» поворота, и только потом rotate() крутит вокруг неё.
Подумай о вертушке-флюгере или о стрелке часов. Стрелка крутится не вокруг своего острого кончика, а вокруг гвоздика в центре циферблата. Этот гвоздик и есть наша точка (0, 0) после translate. Если бы гвоздик был вбит в угол часов, стрелка при вращении описывала бы огромную дугу и улетала за край циферблата — ровно то, что мы видели с наклонённым квадратом выше. Поэтому первым делом мы «вбиваем гвоздик» в нужное место через translate, а уже потом крутим.
Запомни этот порядок как рецепт из трёх шагов:
- translate(x, y) — переставь булавку (начало координат) в нужную точку.
- rotate(угол) — поверни мир вокруг этой булавки.
- Рисуй фигуру вокруг (0, 0) — то есть рисуй её относительно новой булавки, а не относительно угла холста.
Сравни два кода. Сначала так, как делать не надо:
function draw() {
background(240);
rotate(30);
fill(255, 214, 10);
ellipse(200, 200, 100, 100);
}Результат: жёлтый круг хотели поставить в центр, но из-за поворота вокруг угла холста он уехал в сторону и наполовину вылез за край. Вышло криво.
А теперь правильно:
function draw() {
background(240);
translate(200, 200);
rotate(30);
fill(255, 214, 10);
ellipse(0, 0, 100, 100);
}Результат: жёлтый круг ровно в центре холста. Поскольку круг симметричный, поворот на нём не заметен, зато он точно там, где мы хотели. Обрати внимание: после translate(200, 200) мы рисуем круг в точке (0, 0) — потому что булавка теперь в центре, и (0, 0) означает «центр холста».
Делаем поворот видимым
Чтобы увидеть сам поворот, нарисуем не симметричный круг, а клюв цыплёнка — острый треугольник, который торчит вбок:
let angle = 0;
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}
function draw() {
background(135, 206, 235);
translate(width / 2, height / 2);
rotate(angle);
fill(255, 214, 10);
noStroke();
ellipse(0, 0, 90, 90);
fill(255, 153, 0);
triangle(40, -8, 40, 8, 65, 0);
angle = angle + 1;
}Результат: цыплёнок-кружок стоит в центре, а его оранжевый клюв медленно обходит по кругу — будто голова вращается, осматриваясь по сторонам. С каждым кадром angle увеличивается на 1 градус, поэтому за 360 кадров клюв делает полный оборот. Мы используем width / 2 и height / 2 вместо чисел 200 — так центр посчитается сам, даже если ты поменяешь размер холста.
scale(): растягиваем сетку
Теперь второй герой урока — scale(). Если rotate() крутит лист, то scale() его растягивает или сжимает, как будто ты тянешь картинку за уголок на телефоне, увеличивая её двумя пальцами.
Аргумент scale() — это множитель:
scale(1)— обычный размер, ничего не меняется.scale(2)— всё вдвое крупнее.scale(0.5)— всё вдвое мельче.scale(1, 2)— по ширине как обычно, по высоте вдвое выше (можно растягивать по осям отдельно).
И снова важная тонкость: scale() растягивает сетку от точки (0, 0). Если объект далеко от начала координат, при увеличении он не только вырастет, но и уедет в сторону. Представь, что ты растягиваешь резиновую плёнку, прибитую гвоздиком в углу: точка прямо у гвоздика остаётся на месте, а всё, что дальше, разъезжается тем сильнее, чем дальше оно было. Поэтому объект, нарисованный в углу, при увеличении буквально «убегает» от начала координат. Лекарство то же самое, что и с поворотом — сначала translate() в центр, потом scale(), и тогда объект растёт ровно на месте.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(135, 206, 235);
translate(width / 2, height / 2);
scale(1.8);
fill(255, 214, 10);
noStroke();
ellipse(0, 0, 80, 80);
fill(0);
ellipse(15, -15, 8, 8);
}Результат: цыплёнок в центре, но почти вдвое крупнее обычного — большой жёлтый круг с чёрной точкой-глазом. Важно: мы не меняли число 80 в ellipse, мы растянули всю сетку через scale(1.8), и круг увеличился сам.
Пульсация через scale() и sin()
Чтобы цыплёнок «дышал», нам нужно, чтобы множитель не стоял на месте, а плавно ходил туда-сюда. Для этого идеально подходит синус — функция, которая мягко колеблется между −1 и 1. Не пугайся математики: тебе достаточно знать, что sin(чего-то растущего) даёт волну.
let t = 0;
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}
function draw() {
background(135, 206, 235);
translate(width / 2, height / 2);
let s = 1 + 0.3 * sin(t);
scale(s);
fill(255, 214, 10);
noStroke();
ellipse(0, 0, 80, 80);
t = t + 4;
}Результат: цыплёнок в центре плавно пульсирует — то чуть больше, то чуть меньше, размер гуляет примерно от 0.7 до 1.3 от обычного. sin(t) даёт волну от −1 до 1, мы умножаем её на 0.3 и прибавляем 1, поэтому множитель колеблется вокруг единицы. Поменяй 0.3 на 0.6 — и цыплёнок начнёт пульсировать сильнее.
Совмещаем поворот и масштаб
Самое вкусное — когда оба приёма работают вместе. Порядок такой же, как в нашем первом примере: translate(), потом rotate(), потом scale(), и только в конце рисуем фигуру вокруг (0, 0). Именно так устроен код из самого начала урока. Разберём его построчно:
translate(width / 2, height / 2)— переносим булавку в центр холста.rotate(angle)— крутим мир вокруг центра;angleрастёт каждый кадр.let s = 1 + 0.3 * sin(angle * 4)— считаем пульсирующий множитель.scale(s)— растягиваем сетку этим множителем.drawChick()— рисуем цыплёнка вокруг (0, 0), и он наследует и поворот, и масштаб.
Заметь: трансформации складываются по очереди. Сначала сдвинули, потом на уже сдвинутом мире повернули, потом на уже повёрнутом — растянули. Поменяешь порядок — получишь другой результат. Это как одеваться: сначала носки, потом ботинки, наоборот не выйдет.
Чтобы прочувствовать это, попробуй мысленно поменять местами строки. Если сделать rotate() до translate(), то сначала повернётся пустая сетка вокруг угла холста, а потом по уже наклонённым осям произойдёт сдвиг — и твой центр окажется не там, где ты думал, а где-то по диагонали. А если поставить scale(2) перед translate(100, 0), то сам сдвиг тоже удвоится: мир уедет не на 100 пикселей, а на 200, потому что растянулись в том числе и расстояния. Это не баг, а честная математика: каждая следующая трансформация работает поверх уже изменённого мира. Запомни рабочий порядок «сдвиг — поворот — масштаб», и в девяти случаях из десяти всё встанет на свои места.
Частые ошибки и подводные камни
- Забыл angleMode(DEGREES). Тогда
rotate(90)поворачивает на 90 радиан, а не градусов, и фигура улетает в неожиданное место. Либо ставьangleMode(DEGREES)в setup(), либо честно пиши радианы черезradians(90). - rotate() без translate(). Поворот идёт вокруг угла холста (0, 0), и объект уезжает за край. Почти всегда тебе нужно сначала перенести начало координат в центр объекта.
- Рисуешь по старым координатам после translate(). После
translate(200, 200)точка (0, 0) уже в центре. Если по привычке написатьellipse(200, 200, ...), фигура уедет в нижний правый угол. Рисуй вокруг (0, 0). - Не сбрасываешь трансформации между кадрами. Кажется, что translate и rotate накапливаются и всё разъезжается. На самом деле p5.js сам сбрасывает систему координат в начале каждого
draw()— но если ты крутишь внутри цикла for много объектов, обязательно оборачивай каждый вpush()иpop(), иначе повороты сложатся друг с другом. Подробно про push/pop будет в следующих уроках, но знай о ловушке заранее. - scale(0). Множитель ноль схлопывает всё в точку — холст будто пустой. А отрицательный
scale(-1, 1)отзеркаливает картинку: иногда это полезно, но если получилось случайно, ты долго будешь думать, почему клюв смотрит не туда.
Мини-практика: вращающаяся стая
Собери свой скетч. Задача — сделать «карусель» из нескольких цыплят, которые кружатся вокруг центра, и при этом каждый ещё и пульсирует. Вот каркас, который тебе нужно доделать:
let angle = 0;
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}
function draw() {
background(135, 206, 235);
translate(width / 2, height / 2);
rotate(angle);
// TODO 1: нарисуй цыплёнка не в (0,0), а на расстоянии,
// например в (120, 0) — тогда при rotate он поедет по кругу.
// TODO 2: добавь scale() с пульсацией через sin(),
// чтобы цыплёнок дышал во время вращения.
fill(255, 214, 10);
noStroke();
ellipse(0, 0, 60, 60);
angle = angle + 1.5;
}Подсказки: чтобы цыплёнок ехал по орбите, рисуй его в точке вроде (120, 0) — поворот мира потащит эту точку по кругу. Хочешь несколько цыплят? Оберни рисование в цикл for и перед каждым делай дополнительный rotate(360 / количество), не забывая про push()/pop(). А пульсацию добавь строкой scale(1 + 0.2 * sin(angle * 5)) сразу после translate.
Поэкспериментируй: поменяй 1.5 на 5 — карусель закрутится быстрее. Поставь 120 побольше — орбита станет шире. Это твой скетч, ломай и собирай заново сколько хочешь.
Итоги и что дальше
Сегодня ты освоил две мощные трансформации:
rotate(угол)поворачивает всю систему координат вокруг точки (0, 0), а не саму фигуру.scale(множитель)растягивает или сжимает сетку, тоже от точки (0, 0).- Чтобы крутить и масштабировать вокруг центра объекта, сначала идёт
translate(), и только потомrotate()иscale(). - Трансформации складываются по очереди, поэтому порядок важен.
- Не забывай про
angleMode(DEGREES)и рисуй фигуры вокруг нового начала координат.
Ты заставил CodeChick вращаться и пульсировать — но пока каждый объект крутится одинаково. А что, если нарисовать целую стаю, где у каждого цыплёнка свой поворот и размер, и при этом ничего не разъезжается? Для этого нужна пара функций-«закладок» — push() и pop(), которые сохраняют и восстанавливают состояние сетки. Именно ими мы займёмся в следующем уроке. До встречи на холсте!