push() и pop(): стек трансформаций
Сегодня ты научишься главному фокусу всех трансформаций: ставить «закладку» в системе координат через push() и возвращаться к ней через pop() — чтобы повороты одного цыплёнка не утаскивали за собой всю остальную картинку.
Главная идея урока: трансформации (
translate,rotate,scale) не отменяются сами — они накапливаются и действуют на всё, что нарисовано после них.push()запоминает текущее состояние холста, аpop()возвращает к нему. Всё, что между ними, остаётся в своём «пузыре» и не портит соседей.
Зачем тебе push() и pop()
Представь, что ты собрал в скетче целую полянку: посередине сидит CodeChick, слева второй цыплёнок, справа третий. И ты хочешь повернуть только одного — пусть наклонит голову набок, будто прислушивается. В прошлом уроке про rotate и scale ты уже умеешь поворачивать систему координат. Но вот беда: стоит написать rotate() — и повернётся не один цыплёнок, а вообще всё, что нарисовано дальше в этом кадре. Один наклонился — и за ним съехала вся полянка.
Это классическая ловушка новичка. Ты ставишь rotate() для второго цыплёнка, а у тебя кренится третий, фон и зёрна. Картинка едет, и непонятно почему. Кажется, что трансформации «глючат», но на самом деле они работают ровно так, как задумано — просто ты не поставил границу, где поворот должен закончиться.
Решение — пара push() и pop(). Это как кнопки «сохранить» и «откатить» для всей системы координат. Перед тем как крутить одного цыплёнка, ты говоришь push() — «запомни, как сейчас стоит холст». Крутишь, рисуешь. А потом pop() — «верни всё как было». Следующий цыплёнок рисуется на чистой, не перекошенной сетке. К концу урока у тебя будет полянка из трёх независимо повёрнутых цыплят, каждый со своим наклоном, и ни один не утащит за собой остальных. Поехали разбираться.
Как работает стек состояний
Метафора: стопка кальки на чертеже
Вспомни, как чертят на бумаге. У тебя есть основной лист, а сверху ты кладёшь прозрачную кальку, чтобы что-то набросать, не пачкая оригинал. Покрутил кальку, понаклонял, нарисовал на ней деталь — а когда закончил, просто снял её, и под ней основной лист остался ровным и нетронутым.
push() — это «положить новый лист кальки поверх». Все твои translate, rotate, scale и даже цвет заливки теперь действуют на этой кальке. А pop() — это «снять кальку», вернуться к листу, который был под ней, со всеми его настройками. Калька со всеми перекосами выбрасывается, а основа цела.
Стек (по-английски stack — «стопка») — это способ хранить данные стопкой: последнее, что положил сверху, первым и снимаешь.
push()кладёт текущее состояние холста на верх стопки,pop()снимает верхний слой. Поэтому пары всегда вложены аккуратно, как матрёшки, а не пересекаются крест-накрест.
Что именно запоминает push()? Не картинку — её стирать поздно, она уже нарисована. Он запоминает настройки: текущую систему координат (все накопленные сдвиги, повороты, масштабы) и стиль (fill, stroke, толщину линии и так далее). pop() возвращает ровно эти настройки. Поэтому если внутри пары ты сменил цвет заливки на красный, после pop() заливка снова станет такой, какой была до push() — менять её обратно вручную не нужно.
Почему трансформации накапливаются
Главное, что надо уложить в голове: трансформации в p5.js не разовые. Когда ты пишешь translate(100, 0), ты не «рисуешь со сдвигом один раз» — ты сдвигаешь саму систему координат, и она остаётся сдвинутой. Следующий translate(100, 0) сдвинет ещё на 100, итого на 200. Два rotate подряд сложатся в один большой угол. Это и называется «накопление».
В draw() есть тонкость: эта функция вызывается заново 60 раз в секунду, и в начале каждого кадра p5.js сбрасывает систему координат к исходной. Поэтому если у тебя один rotate(0.01) в draw() без цикла — фигура не закрутится бесконечно, она просто повёрнута на 0.01 в каждом кадре. А вот внутри одного кадра накопление работает в полную силу: всё, что ты натрансформировал, действует на каждую следующую фигуру до конца кадра — пока не вмешается pop().
Разбираем на примерах
Пример 1: что будет БЕЗ push()/pop()
Сначала посмотрим на проблему вживую. Нарисуем двух цыплят и попробуем повернуть только второго — без всякой защиты.
function setup() {
createCanvas(400, 300);
noStroke();
}
function draw() {
background(135, 206, 235);
// первый цыплёнок слева — рисуем как обычно
fill(255, 209, 64);
circle(120, 150, 70);
// хотим повернуть только второго, справа
translate(280, 150); // сдвинули центр координат к нему
rotate(0.4); // наклонили
fill(255, 209, 64);
circle(0, 0, 70); // второй цыплёнок
// а теперь рисуем землю — и она тоже едет!
fill(110, 180, 90);
rect(-200, 80, 600, 80);
}Результат: первый цыплёнок слева стоит ровно. Второй справа повёрнут — пока всё хорошо. Но зелёная полоса земли, которую мы нарисовали последней, оказалась сдвинута и наклонена вместе со вторым цыплёнком: translate и rotate подействовали и на неё тоже. Картинка перекошена, земля висит криво — потому что мы перекрутили систему координат и не вернули её на место.
Вот она, ловушка из начала урока. Трансформации накопились и потащили за собой всё, что нарисовано ниже. Чтобы наклон жил только внутри «своего» цыплёнка, его нужно завернуть в push() и pop().
Пример 2: тот же скетч, но с push() и pop()
Чиним. Оборачиваем трансформации второго цыплёнка в пару — ставим «закладку» до и возвращаемся после.
function setup() {
createCanvas(400, 300);
noStroke();
}
function draw() {
background(135, 206, 235);
// первый цыплёнок слева
fill(255, 209, 64);
circle(120, 150, 70);
push(); // запомнили чистую систему координат
translate(280, 150); // эти трансформации живут только внутри пузыря
rotate(0.4);
fill(255, 209, 64);
circle(0, 0, 70); // второй цыплёнок — наклонён
pop(); // вернули всё как было
// земля рисуется на ровной сетке — больше не едет
fill(110, 180, 90);
rect(0, 230, 400, 70);
}Результат: оба цыплёнка на месте — первый прямой, второй аккуратно наклонён. А зелёная земля внизу теперь лежит ровно по всей ширине холста, потому что после pop() система координат вернулась в исходное положение. Наклон остался заперт внутри пары push()/pop() и не вылез наружу.
Сравни два примера построчно. Код почти одинаковый — добавились только две строчки, push() перед трансформациями и pop() после фигуры. Но именно они превращают «весь холст поехал» в «наклонился только тот, кого я хотел». Запомни этот паттерн как заклинание: push() → translate/rotate/scale → рисуем → pop().
Пример 3: полянка из независимо повёрнутых цыплят
Теперь применим приём по-настоящему. Сделаем трёх цыплят в ряд, и каждому зададим свой наклон — будто они качают головами под музыку в разном ритме. Каждый цыплёнок — в своём пузыре push()/pop(), поэтому наклоны не складываются и не мешают друг другу.
function setup() {
createCanvas(420, 240);
noStroke();
}
function drawChick() {
// рисуем цыплёнка в точке (0,0) — голова смотрит вверх-вправо
fill(255, 209, 64);
circle(0, 0, 60); // тело
fill(255, 140, 30);
triangle(24, -4, 42, 0, 24, 4); // клюв
fill(40);
circle(10, -10, 8); // глаз
}
function draw() {
background(135, 206, 235);
let angles = [-0.4, 0, 0.4]; // три разных наклона
let xs = [80, 210, 340]; // три позиции по горизонтали
for (let i = 0; i < 3; i = i + 1) {
push(); // свой пузырь для каждого
translate(xs[i], 130); // переносим центр к i-му цыплёнку
rotate(angles[i]); // наклоняем только его
drawChick();
pop(); // закрываем пузырь
}
}Результат: на голубом небе сидят три жёлтых CodeChick в ряд. Левый наклонил голову влево, средний смотрит прямо, правый завалился вправо — каждый со своим углом, как кадр из клипа, где птички качаются под бит. Несмотря на то что мы трижды вызвали translate и rotate, наклоны не сложились: каждый push() начинает с чистой сетки, а pop() её возвращает перед следующим витком цикла.
Обрати внимание на трюк: функцию drawChick() мы написали так, будто цыплёнок всегда в точке (0, 0). Это удобно — внутри пузыря translate уже перенёс начало координат куда надо, а rotate повернул. Цыплёнку «не важно», где он на холсте: он рисует себя вокруг нуля, а пузырь сам ставит и крутит его на месте. Так трансформации и функции работают в команде: одна рисует деталь «у себя дома», другие расставляют копии по сцене.
Пример 4: вложенные пузыри — рука внутри тела
Пузыри можно вкладывать друг в друга, как матрёшки. Это нужно, когда у объекта есть подвижная часть: например, цыплёнок наклонён целиком, а внутри него ещё и машет крылом. Внешний push() держит наклон всего тела, внутренний — добавляет поворот крыла поверх.
function setup() {
createCanvas(300, 300);
noStroke();
}
function draw() {
background(135, 206, 235);
push(); // внешний пузырь — всё тело
translate(150, 150);
rotate(0.2); // наклонили цыплёнка целиком
fill(255, 209, 64);
circle(0, 0, 90); // тело
fill(255, 140, 30);
triangle(38, -5, 60, 0, 38, 5); // клюв
push(); // внутренний пузырь — только крыло
translate(-10, 5);
rotate(-0.6); // крыло повёрнуто ОТНОСИТЕЛЬНО тела
fill(245, 190, 40);
ellipse(0, 0, 50, 22); // крыло
pop(); // закрыли крыло — наклон тела ещё держится
pop(); // закрыли тело — система координат чистая
}Результат: крупный CodeChick в центре наклонён вправо целиком, а сбоку у него приподнятое крыло, повёрнутое под своим углом относительно тела. Когда мы поворачиваем крыло внутренним rotate(-0.6), оно крутится поверх наклона тела (0.2) — потому что трансформации внутреннего пузыря складываются с внешним. Внутренний pop() убирает только поворот крыла, оставляя наклон тела. Внешний pop() убирает и его.
Это и есть сила стека: пузыри вкладываются, и каждый pop() снимает ровно один слой — последний открытый. Так строят персонажей с подвижными частями, стрелки часов, ветки деревьев — всё, у чего есть «деталь внутри детали».
Подумай, насколько это упрощает жизнь. Без вложенных пузырей тебе пришлось бы вручную считать, под каким итоговым углом окажется крыло, если тело наклонено на 0.2, а крыло — ещё на -0.6 относительно него. Это тригонометрия и куча формул. А с push()/pop() ты просто описываешь каждую деталь «у себя дома» и складываешь повороты слоями — математику за тебя делает сам стек. Именно так устроены движки настоящих игр и мультфильмов: персонаж — это дерево вложенных систем координат, где плечо держит локоть, локоть держит кисть, а кисть держит пальцы, и каждый сустав крутится в своём пузыре.
Частые ошибки и подводные камни
push() без pop() (или наоборот). Если открыл
push()и забылpop(), лишний слой кальки остаётся на стеке. С каждым кадромdraw()стек растёт, трансформации накапливаются — и картинка медленно уезжает или крутится сама по себе. Правило: на каждыйpush()ровно одинpop(). Считай их парами, как открывающие и закрывающие скобки.Перекрёстные пары вместо вложенных. Пузыри обязаны быть вложены, как матрёшки:
push() push() pop() pop(). Нельзя «перекрестить»: открыть два, а закрыть в обратном порядке так, чтобы пары пересеклись.pop()всегда снимает последний открытыйpush()— рассчитывай порядок исходя из этого.Думают, что push() запоминает картинку. Нет —
push()запоминает только настройки: систему координат и стиль (fill, stroke, толщину). Уже нарисованные фигурыpop()не стирает и не двигает. Хочешь «откатить» рисунок — это делается не через pop(), а перерисовкой кадра (background()в началеdraw()).Ставят push() после трансформаций, а не до. Толку от
push()нет, если ты уже накрутилtranslate/rotateдо него — он запомнит уже перекошенное состояние. Закладку надо ставить раньше трансформаций, которые хочешь изолировать: сначалаpush(), потомtranslate/rotate/scale.Забывают, что pop() возвращает и цвет тоже. Иногда это удивляет: поменял
fillвнутри пузыря, а послеpop()заливка «сама» вернулась к прежней. Это не баг —push()/pop()хранят и стиль. Если нужно, чтобы цвет остался после пузыря, задавай его снова уже послеpop().
Мини-практика: хоровод цыплят по кругу
Собери сцену, где цыплята стоят кольцом и каждый развёрнут «лицом» наружу, будто водят хоровод. Тут push()/pop() в цикле раскроется в полную силу. План такой:
- Возьми за основу Пример 3 с циклом, но вместо ряда расставь цыплят по кругу. Внутри цикла на каждом шаге считай угол
let a = (TWO_PI / count) * i;. - В пузыре сначала перенеси центр в середину холста, потом поверни на угол
a, потом сдвинься на радиус наружу — примерно так:push(); translate(width/2, height/2); rotate(a); translate(120, 0); drawChick(); pop();. - Проверь, что без
pop()всё ломается: временно закомментируйpop()и посмотри, как цыплята слипаются в кучу и улетают — это наглядно покажет, зачем нужна пара. - Усложни: добавь каждому цыплёнку внутренний пузырь с машущим крылом из Примера 4, и пусть угол крыла зависит от
i, чтобы все махали по-разному.
Когда заработает, поэкспериментируй: поменяй count, поиграй радиусом во втором translate, попробуй добавить лёгкий scale() внутри пузыря, чтобы цыплята были разного размера. Главное — следи, чтобы каждый push() закрывался своим pop().
Итоги
Сегодня ты разобрался с сердцем всех трансформаций — стеком состояний:
- Трансформации накапливаются:
translate,rotate,scaleменяют саму систему координат и действуют на всё, что нарисовано после них, до конца кадра. push()кладёт текущее состояние холста (систему координат + стиль) на стек — как лист кальки поверх чертежа.pop()снимает верхний слой и возвращает всё как было — наклоны и цвета внутри пузыря наружу не вылезают.- Пузыри вкладываются как матрёшки: каждый
pop()закрывает последний открытыйpush(). Это позволяет рисовать объекты с подвижными частями. - Запомни паттерн: push() → трансформации → рисуем → pop(), и на каждый push() ровно один pop().
Теперь ты можешь крутить и двигать любую фигуру, не боясь, что за ней поедет вся остальная картинка — а это пропуск к сложным сценам из многих независимых деталей. В следующем уроке мы используем эти пузыри в цикле, чтобы строить рекурсивные узоры и фракталы: деревья, у которых из каждой ветки растут ветки поменьше, и снежинки, повторяющие сами себя. push()/pop() там станут твоим главным инструментом — так что закрепи их сегодня, поводи цыплят в хороводе, и до встречи на следующей странице!