translate(): сдвигаем мир
Сегодня ты узнаешь главный секрет ленивого художника: вместо того чтобы пересчитывать координаты каждой фигуры, ты будешь двигать саму систему координат — а цыплёнок останется нарисованным «в нуле».
Трансформация — это изменение системы координат перед отрисовкой. А translate(dx, dy) — самая простая из трансформаций: она сдвигает начало координат (точку 0, 0) на dx вправо и dy вниз, и все фигуры после неё рисуются от нового нуля.Зачем двигать мир, а не фигуру
Представь, что ты рисуешь цыплёнка из нескольких частей: тело, голова, клюв, два глаза, лапки. Чтобы он выглядел цельным, все эти кружки и треугольники должны стоять в строго определённых местах относительно друг друга. Ты подобрал их идеально — и тут понимаешь, что цыплёнок нужен не в центре, а в правом верхнем углу. Или вообще в пяти разных местах холста, как стая.
Без сегодняшнего приёма пришлось бы лезть в каждую строчку и пересчитывать: к иксу тела прибавить 150, к иксу головы прибавить 150, к иксу клюва прибавить 150… Ошибся в одном месте — и клюв уехал от головы. Знакомая боль: один объект из множества частей, и ты боишься его сдвинуть, потому что всё развалится.
В уроке про первые формы ты рисовал фигуры, вписывая их координаты прямо в circle() и rect(). Это работало, пока фигура одна. Но как только их становится много и они связаны в единого героя, такой подход начинает мешать. Сегодня мы перевернём мышление: фигуру будем рисовать всегда у нуля, в одних и тех же координатах, а перемещать будем не её, а весь холст под ней.
К концу урока ты сможешь нарисовать цыплёнка один раз — «в нуле» — и расставить его копии по холсту одной строчкой translate() на каждую, не трогая внутренние координаты частей. Это и есть та свобода, ради которой существуют трансформации.
Подумай о любимой игре с открытым миром. Когда твой персонаж бежит вперёд, движется ведь не только он — едет вся декорация: деревья проплывают мимо, дома уходят за спину. На самом деле во многих движках персонаж стоит почти на месте по центру экрана, а двигается мир вокруг него. Звучит как обман, но это куда удобнее: не нужно пересчитывать положение каждого дерева отдельно, достаточно сдвинуть всю сцену. translate() — твой первый шаг к такому мышлению: ты учишься двигать сцену, а не каждый объект по отдельности.
Как работает translate(): сдвигаем лист, а не рисунок
Вспомни клетки в тетради. Когда ты рисуешь схему, ты ставишь точку отсчёта — обычно левый верхний угол страницы — и от неё отсчитываешь клетки вправо и вниз. В p5.js всё так же: система координат начинается в точке (0, 0) в левом верхнем углу холста, икс растёт вправо, игрек — вниз.
А теперь представь, что ты рисуешь не на странице, а на прозрачной кальке, которая лежит поверх тетради. Рисунок на кальке прибит к её собственному углу. Если ты сдвинешь кальку вправо и вниз — рисунок поедет вместе с ней, хотя на самой кальке ты ничего не менял. Вот что делает translate(): он сдвигает «кальку» — систему координат — а не отдельные фигуры.
После вызова translate(100, 50) новый ноль оказывается там, где раньше была точка (100, 50). И теперь, если ты напишешь circle(0, 0, 40), кружок появится не в углу холста, а в точке (100, 50) — потому что его «ноль» переехал. Фигура думает, что она в нуле, но мир под ней сместился.
| Вызов | Куда переедет начало координат |
translate(0, 0) | никуда, ноль остаётся в левом верхнем углу |
translate(200, 200) | в центр холста 400×400 |
translate(width / 2, height / 2) | в центр холста любого размера |
translate(-50, 0) | влево за край — ноль уезжает наружу слева |
Важно почувствовать одну вещь: translate() ничего не рисует. Это не фигура и не цвет — это команда «передвинь систему координат для всего, что я нарисую дальше». Сама по себе она на холсте незаметна, её эффект виден только по тому, куда встанут следующие фигуры.
Ещё одно наблюдение, которое снимает половину будущей путаницы: фигура, нарисованная после сдвига, понятия не имеет, что мир под ней сместился. Для неё ноль — это всегда ноль. Это как актёр на сцене: он играет свою сцену одинаково, а уж куда повернули прожектор и где стоят декорации — решает режиссёр снаружи. Твой код рисования цыплёнка — актёр, а translate() — режиссёр, который ставит этого актёра в нужную точку сцены.
Первый сдвиг
Давай сравним два способа поставить кружок в центр холста.
function setup() {
createCanvas(400, 400);
background(30);
noStroke();
fill(255, 221, 51);
// способ 1: задаём координаты прямо в circle
circle(200, 200, 80);
}Результат: на тёмном холсте — жёлтый кружок ровно в центре. Мы посчитали центр сами (200, 200) и вписали эти числа в circle().
А теперь то же самое, но через сдвиг мира:
function setup() {
createCanvas(400, 400);
background(30);
noStroke();
fill(255, 221, 51);
translate(200, 200); // ноль переехал в центр
circle(0, 0, 80); // рисуем «в нуле»
}Результат: точно такой же жёлтый кружок в центре. Но смотри, что изменилось в голове: фигуру мы нарисовали в координатах (0, 0), а в центр её привёл translate(). Кружок «не знает», где он на холсте — за его положение отвечает сдвинутый мир.
Пока разницы как будто нет: одна фигура, одна строчка. Магия начинается, когда фигура составная.
Цыплёнок, собранный «в нуле»
Соберём нашего героя из нескольких частей вокруг точки (0, 0): тело, голова, клюв, глаз. Все координаты — маленькие числа относительно нуля.
function setup() {
createCanvas(400, 400);
background(30);
noStroke();
translate(200, 200); // ставим цыплёнка в центр
// тело — большой жёлтый круг в нуле
fill(255, 221, 51);
circle(0, 0, 100);
// голова чуть выше и правее нуля
circle(35, -40, 55);
// оранжевый клюв сбоку головы
fill(255, 140, 0);
triangle(60, -45, 60, -30, 85, -38);
// глаз
fill(30);
circle(45, -50, 8);
}Результат: в центре холста сидит жёлтый цыплёнок: круглое тело, голова справа сверху, оранжевый клюв-треугольник торчит вбок, чёрный глаз на месте. Все части собраны вокруг нуля, а в центр их привела одна строчка translate(200, 200).
Вот теперь почувствуй силу. Хочешь передвинуть цыплёнка целиком в правый верхний угол? Не трогай ни одной координаты частей — поменяй только translate на translate(300, 90). Тело, голова, клюв и глаз поедут вместе, сохранив все пропорции и расстояния между собой, потому что они привязаны к общему нулю. Ты двигаешь мир — герой едет следом как единое целое.
Сравни это с тем, как было бы без сдвига. Чтобы переставить цыплёнка, тебе пришлось бы открыть каждую строчку и прибавить смещение к иксу и игреку: к телу, к голове, к трём вершинам клюва, к глазу — десяток правок, и в каждой можно ошибиться. А с translate() правка ровно одна. Чем сложнее герой, тем больнее без сдвига и тем слаще с ним. Именно поэтому в реальных скетчах любую составную фигуру почти всегда рисуют «в нуле».
Стая цыплят: translate() в цикле
Раз один translate() ставит цыплёнка куда нужно, то несколько сдвигов расставят целую стаю. Но тут есть тонкость, на которой спотыкаются все новички, поэтому разберём её медленно.
translate() работает накопительно. Каждый новый вызов сдвигает мир не от исходного угла, а от того места, где ноль оказался после прошлого сдвига. Два вызова translate(100, 0) подряд сдвинут ноль на 200 вправо, а не на 100. Это как делать шаги: второй шаг ты делаешь от того места, куда пришёл первым, а не от двери.
function setup() {
createCanvas(400, 200);
background(30);
noStroke();
translate(60, 100); // встаём к первому цыплёнку
for (let i = 0; i < 4; i++) {
// рисуем цыплёнка в текущем нуле
fill(255, 221, 51);
circle(0, 0, 50);
circle(18, -20, 28);
fill(255, 140, 0);
triangle(30, -22, 30, -14, 44, -18);
// и шагаем вправо к следующему
translate(90, 0);
}
}Результат: вдоль холста выстроились четыре одинаковых цыплёнка, каждый правее предыдущего на 90 пикселей — аккуратная цепочка. Тело каждого рисуется в нуле, а вправо их разводит накопительный translate(90, 0) на каждом витке цикла.
Разберём по шагам. Перед циклом translate(60, 100) ставит ноль к первому цыплёнку. На каждом витке мы рисуем цыплёнка в текущем нуле (одни и те же маленькие координаты!), а потом translate(90, 0) переносит ноль на 90 пикселей вправо — к месту следующего. Поскольку сдвиги копятся, второй цыплёнок встанет на 90, третий на 180, четвёртый на 270 правее старта. Один и тот же код рисования — четыре разных места.
Поиграй с числами
Лучший способ прочувствовать накопление — покрутить ручки:
- Шаг стаи. Замени
translate(90, 0)наtranslate(90, -15)— цыплята пойдут по диагонали, лесенкой вверх, потому что каждый шаг сдвигает ещё и вверх. - Количество. Поменяй
4в цикле на6, но не забудь расширить холст, иначе крайние цыплята уедут за правый край. - Стартовая точка. Подвигай первый
translate(60, 100)— вся стая переедет целиком, сохранив строй, ведь от неё пляшут все остальные сдвиги.
Частые ошибки и подводные камни
Трансформации простые, но именно из-за их накопительной природы новички ловят самые обидные баги. Вот что ломается чаще всего.
1. Ждут, что translate() двигает фигуру, а не мир
Главное недопонимание. Человек думает: «translate(100, 0) сдвинет мой кружок на 100 вправо». Нет — он сдвигает систему координат, и поэтому фигура, нарисованная после, окажется правее. Разница тонкая, но из-за неё ломается интуиция. Запомни мантру: translate() двигает мир, фигуры остаются «в нуле».
2. Забывают, что сдвиги копятся
Очень частая засада в циклах. Человек хочет нарисовать второго цыплёнка на 100 правее первого и пишет translate(100, 0), потом третьего — снова translate(100, 0), ожидая, что тот встанет на те же 100. А он встанет на 200, потому что сдвиги складываются. Это не баг p5.js, это его логика: каждый translate() отсчитывает от текущего нуля, а не от угла холста.
3. Сдвинули мир и забыли вернуть назад
Если ты сделал translate() и не вернул систему координат обратно, всё, что нарисуешь дальше, тоже поедет. Хотел поставить надпись в углу холста после сдвинутого цыплёнка — а она уехала вслед за нулём. Сегодня мы решаем это вручную (обратным сдвигом или повторным расчётом), но в p5.js для этого есть удобная пара push() и pop(), которая запоминает и восстанавливает систему координат. С ней мы подробно познакомимся в следующем уроке — пока просто держи в голове, что сдвиг «залипает».
4. Зовут translate() в setup(), а рисуют в draw()
Сдвиг координат действует только в той функции и в том кадре, где его вызвали. Если ты сделал translate() один раз в setup(), а фигуры рисуешь в draw() — сдвиг до них не дойдёт, координаты в начале каждого кадра draw() сбрасываются к углу. Вызывай translate() в той же функции, где рисуешь.
5. Путают знак у вертикали
Помни из урока про координаты: игрек растёт вниз. Значит, translate(0, 50) сдвигает мир вниз, а чтобы поднять цыплёнка вверх, нужен отрицательный игрек: translate(0, -50). Минус вверх, плюс вниз — это сбивает с толку первое время, но быстро входит в привычку.
Мини-проект: рассадить стаю по холсту
Теперь твоя очередь. Возьми составного цыплёнка «в нуле» из примера и преврати его в живую стаю. Чек-лист задания:
- Собери цыплёнка из частей вокруг точки (0, 0): тело, голова, клюв, глаз. Все координаты частей — маленькие числа относительно нуля, как в примере выше.
- Заведи функцию
drawChick(), чтобы не дублировать код, и вызывай её после каждого сдвига. (Если функции ещё непривычны — просто копируй блок рисования, это тоже сработает.) - Поставь трёх цыплят в ряд через накопительный
translate(), как в примере со стаей. Подбери шаг так, чтобы они не наезжали друг на друга. - Сделай так, чтобы каждый следующий цыплёнок стоял чуть выше предыдущего — добавь отрицательный игрек в шаг сдвига, чтобы стая шла лесенкой вверх.
- Бонус. Поставь самого первого цыплёнка не сдвигом, а в координаты (0, 0) холста (то есть без стартового
translate) и посмотри, как половина его тела уедет за угол. Это наглядно покажет, что фигура реально нарисована «в нуле».
Когда стая выстроится лесенкой, поэкспериментируй: меняй только стартовый translate() и шаг — и вся композиция будет двигаться как единое целое, а внутренний код цыплёнка останется нетронутым. Это и есть награда за то, что ты научился двигать мир, а не фигуры.
Итоги
Сегодня ты освоил первую трансформацию и перевернул своё мышление о координатах. Коротко главное:
translate(dx, dy)сдвигает начало координат, а не отдельную фигуру: новый ноль оказывается там, где была точка (dx, dy).- Удобный приём — рисовать фигуру всегда «в нуле» (в координатах около 0, 0), а её положение на холсте задавать одним
translate(). Тогда составной герой двигается целиком, не разваливаясь на части. - Сдвиги копятся: каждый
translate()отсчитывает от текущего нуля, а не от угла холста. На этом строится стая в цикле — и на этом же спотыкаются новички. - Сдвиг «залипает» до конца кадра: что нарисуешь после
translate(), тоже поедет. Помни про знак игрека — минус поднимает вверх.
Но мы оставили висеть проблему: после сдвига всё, что рисуешь дальше, тоже уезжает, и возвращать координаты вручную неудобно. В следующем уроке мы познакомимся с парой push() и pop() — они работают как кнопки «сохранить» и «вернуть» для системы координат. Ты сможешь сдвинуть мир, нарисовать одного цыплёнка, а потом одним движением вернуть всё на место — и следующий цыплёнок встанет точно там, где ты задумал. До встречи!