Мини-проект: генеративный аватар
Пришло время собрать всё, чему ты научился, в одну работу: генератор уникальных аватаров CodeChick, который при каждом запуске рисует нового, ни на кого не похожего цыплёнка.
Главная мысль урока: генеративный аватар — это не один рисунок, а машина, которая рисует тысячи разных рисунков по одному набору правил. Ты задаёшь, что именно может случайно меняться (цвет, размер, перья, фон), а random() и сид превращают эти правила в бесконечную галерею.Зачем вообще нужен генеративный аватар
Ты наверняка видел это в играх и приложениях: заходишь первый раз, а тебе уже выдали смешного человечка или зверька с случайной причёской, очками и цветом футболки. Никто не рисовал эту картинку вручную — её собрал код по набору правил. Такие штуки называют генеративными аватарами, и сегодня ты сделаешь свой: фабрику цыплят CodeChick, где каждый запуск выдаёт нового героя.
Это идеальный финал курса, потому что аватар — как выпускной альбом всех твоих навыков. Цвет из модуля про краски станет случайным оттенком пёрышек. Случайность раскидает перья и нарисует разный клюв. Трансформации повернут хохолок под своим углом. А сид сделает так, что понравившегося цыплёнка можно будет сохранить и показать друзьям — он соберётся точно таким же. Мы не учим ничего принципиально нового: мы соединяем уже знакомые кубики в одну большую работу.
Подумай, где ты встречаешь такие штуки каждый день. Случайные аватарки, которые мессенджер выдаёт новому контакту. Генераторы скинов и причёсок в играх, где двух одинаковых персонажей почти не встретишь. Профильные картинки коллекций, которыми хвастаются в сети, — там тоже всё собирается кодом по слоям, и у каждой картинки есть свой номер-сид. Когда ты сделаешь свой генератор, ты поймёшь главный секрет: за этой «бесконечной уникальностью» стоит не магия, а аккуратно подобранные диапазоны случайных чисел плюс один постоянный силуэт. И это абсолютно по силам тебе прямо сейчас.
Вот к чему мы придём — генератор, который при каждом обновлении страницы рисует уникального цыплёнка:
let chickSeed;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100);
angleMode(DEGREES);
chickSeed = floor(random(100000));
}
function draw() {
randomSeed(chickSeed);
drawAvatar();
noLoop();
}
function mousePressed() {
chickSeed = floor(random(100000));
redraw();
}
function drawAvatar() {
let bodyHue = random(40, 60);
background(random(180, 220), 30, 95);
translate(width / 2, height / 2);
drawFeathers(bodyHue);
noStroke();
fill(bodyHue, 80, 100);
ellipse(0, 0, 160, 160);
fill(30, 90, 100);
let beakTilt = random(-15, 15);
push();
rotate(beakTilt);
triangle(70, -10, 70, 10, 105, 0);
pop();
fill(0);
ellipse(30, -30, 16, 16);
}
function drawFeathers(bodyHue) {
let count = floor(random(5, 12));
for (let i = 0; i < count; i++) {
push();
rotate(random(360));
fill(bodyHue, 60, 100);
ellipse(90, 0, 30, 12);
pop();
}
}Результат: на пастельном небе появляется крупный цыплёнок: жёлто-золотистое тело со случайным оттенком, вокруг него торчат несколько перьев под разными углами, клюв слегка наклонён, чёрный глаз на месте. Кликнешь по холсту — и нарисуется совсем другой цыплёнок: другой оттенок, другое число перьев, другой наклон клюва. Не пугайся незнакомых строк — к концу урока ты разберёшь тут каждую.
Концепция: аватар — это машина, а не картинка
Главное переключение в голове такое. Когда ты рисуешь в скетчбуке, ты создаёшь один рисунок. Когда ты пишешь генеративный аватар, ты создаёшь правила, по которым рисуются тысячи разных рисунков. Это как разница между «нарисовать одного цыплёнка» и «построить автомат, который штампует цыплят, и ни один не повторится».
Представь конструктор LEGO с инструкцией, где часть деталей выбирается броском кубика. Каркас всегда один: тело-круг, глаз, клюв, перья. Но цвет тела, число перьев, их углы и оттенок фона на каждой сборке свои. Ты, как автор, решаешь две вещи: что остаётся постоянным (силуэт цыплёнка, чтобы он всегда узнавался) и что отдаётся на волю случая (детали, которые делают каждого уникальным). В этом и есть искусство генеративного дизайна — найти баланс, чтобы герой и узнавался, и каждый раз удивлял.
Если отдать случайности слишком мало, все цыплята получатся почти близнецами и смотреть будет скучно. Если отдать слишком много — например, разрешить телу быть любого цвета и любого размера, — герой расплывётся в бесформенное цветное пятно, и никто не узнает в нём CodeChick. Поэтому опытные авторы держат «скелет» жёстким, а играют деталями: оттенками внутри одного семейства, небольшими наклонами, количеством мелких элементов. Так каждая работа выглядит и свежо, и узнаваемо одновременно. Это тот же приём, что у художников комиксов: поза и одежда героя меняются от кадра к кадру, а лицо и силуэт остаются теми же, чтобы ты ни на секунду не потерял персонажа из виду.
План случайных параметров
Прежде чем писать код, полезно составить список — какие параметры мы отдаём случайности и в каких пределах. Это как продумать персонажа до того, как взять в руки карандаш.
| Параметр | Диапазон | Что меняет |
| Оттенок тела | random(40, 60) | От лимонного до золотистого жёлтого |
| Цвет фона | random(180, 220) | Разные оттенки голубого неба |
| Число перьев | random(5, 12) | От аккуратного до пушистого цыплёнка |
| Угол каждого пера | random(360) | Перья торчат в разные стороны |
| Наклон клюва | random(-15, 15) | Лёгкое «настроение» мордочки |
Видишь, как удобно? Сначала таблица на бумаге, потом код. Каждая строка таблицы превратится в одну переменную со своим random(). А постоянным останется силуэт: круглое тело, один глаз, один клюв — чтобы цыплёнок всегда был цыплёнком.
Разбор по шагам
Шаг 1. Каркас: один цыплёнок без случайности
Начинаем с самого простого — нарисуем базового цыплёнка вообще без случайных чисел. Это наш постоянный силуэт, на который мы потом навесим вариативность.
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100);
}
function draw() {
background(200, 30, 95);
translate(width / 2, height / 2);
noStroke();
fill(50, 80, 100);
ellipse(0, 0, 160, 160);
fill(30, 90, 100);
triangle(70, -10, 70, 10, 105, 0);
fill(0);
ellipse(30, -30, 16, 16);
}Результат: в центре голубоватого холста жёлтый круглый цыплёнок с оранжевым клювом справа и чёрным глазом. Пока он один и всегда одинаковый. Заметь: мы включили colorMode(HSB, 360, 100, 100) — это режим цвета через тон, насыщенность и яркость, который удобен, когда мы хотим перебирать оттенки одним числом. И сразу сделали translate(width / 2, height / 2), чтобы рисовать вокруг центра — точки (0, 0).
Шаг 2. Добавляем случайный цвет
Теперь оживим первый параметр из таблицы — оттенок тела. Вместо жёсткого числа 50 подставим случайное значение из диапазона.
function draw() {
background(random(180, 220), 30, 95);
translate(width / 2, height / 2);
noStroke();
let bodyHue = random(40, 60);
fill(bodyHue, 80, 100);
ellipse(0, 0, 160, 160);
fill(30, 90, 100);
triangle(70, -10, 70, 10, 105, 0);
fill(0);
ellipse(30, -30, 16, 16);
}Результат: цыплёнок всё тот же по форме, но при каждом запуске его тело то лимонно-жёлтое, то почти оранжево-золотистое, а небо за ним — разный оттенок голубого. Мы храним оттенок в переменной bodyHue, потому что он скоро пригодится и для перьев — чтобы они были одного семейства цветов с телом.
Шаг 3. Раскидываем перья циклом и push/pop
Самая интересная часть — перья. Мы рисуем одно перо как вытянутый эллипс, отодвинутый от центра, а потом в цикле поворачиваем мир на случайный угол и рисуем перо снова. Чтобы повороты не накапливались друг на друге, каждый оборачиваем в push() и pop() — пару функций-закладок, которые сохраняют и восстанавливают систему координат. Если подзабыл, как они работают, загляни в урок про стек push() и pop().
function drawFeathers(bodyHue) {
let count = floor(random(5, 12));
for (let i = 0; i < count; i++) {
push();
rotate(random(360));
fill(bodyHue, 60, 100);
ellipse(90, 0, 30, 12);
pop();
}
}Результат: вокруг тела цыплёнка торчит от 5 до 12 светлых перьев, и каждое смотрит в свою сторону — получается пушистый «солнечный» силуэт. Разберём логику. count — случайное число перьев; floor отрезает дробную часть, чтобы получилось целое. В цикле для каждого пера мы делаем push() (запоминаем чистую сетку), поворачиваем её на случайный угол random(360), рисуем перо в точке (90, 0) — то есть на расстоянии 90 от центра, — и pop() возвращает сетку обратно. Без push/pop углы складывались бы, и перья поехали бы по спирали.
Тут важно прочувствовать роль расстояния. Само перо мы всегда рисуем в одной и той же точке (90, 0), как будто оно лежит на правом боку. Но перед рисованием мы поворачиваем всю систему координат на случайный угол — и эта точка (90, 0) уезжает вместе с повёрнутой сеткой в новое место по кругу. Получается, что мы рисуем перо «всегда справа», но справа уже от повёрнутого мира. Это тот самый приём «нарисуй у нуля, а позицию задай трансформацией», который мы отрабатывали в уроках про стек push() и pop(). Поменяй число 90 на 70 — и перья прижмутся ближе к телу, поменяй 30 и 12 в эллипсе — и они станут длиннее или толще. Каждое такое число — ручка, которую можно крутить.
Шаг 4. Наклон клюва через rotate()
Добавим характер мордочке — пусть клюв слегка наклоняется. Здесь тоже нужен push/pop, иначе наклон клюва утащит за собой и глаз.
fill(30, 90, 100);
let beakTilt = random(-15, 15);
push();
rotate(beakTilt);
triangle(70, -10, 70, 10, 105, 0);
pop();
fill(0);
ellipse(30, -30, 16, 16);Результат: клюв цыплёнка чуть наклонён вверх или вниз — кто-то выглядит задорным, кто-то задумчивым. beakTilt берёт случайный угол от −15 до 15 градусов. Мы оборачиваем только поворот клюва в push/pop, поэтому глаз рисуется уже на ровной сетке и всегда сидит на своём месте. Не забудь в setup() добавить angleMode(DEGREES), иначе rotate(15) повернёт мир на 15 радиан и клюв улетит.
Сид: как закрепить понравившегося цыплёнка
Вот мы и подошли к самому важному для проекта. Сейчас каждый запуск даёт нового цыплёнка — здорово, но что, если попался идеальный и хочется его сохранить или показать другу, чтобы у него собрался точно такой же? Для этого есть сид — стартовое число генератора случайности. Если задать один и тот же сид, random() выдаст ровно ту же последовательность чисел, а значит, и того же цыплёнка. Подробно мы разбирали это в уроке про сиды и воспроизводимость.
Метафора такая: random() — это не настоящая магия, а очень длинная заранее записанная книга случайных чисел. Сид — это номер страницы, с которой ты начинаешь читать. Откроешь книгу на странице 42 — получишь те же числа, что и вчера на странице 42. Поэтому «случайный, но воспроизводимый» цыплёнок — это просто номер страницы, который мы запомнили.
let chickSeed;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100);
angleMode(DEGREES);
chickSeed = floor(random(100000));
}
function draw() {
randomSeed(chickSeed);
drawAvatar();
noLoop();
}
function mousePressed() {
chickSeed = floor(random(100000));
redraw();
}Результат: при запуске рисуется один цыплёнок и анимация останавливается (noLoop() просит p5.js не перерисовывать кадры впустую). По клику мыши берётся новый случайный сид и redraw() рисует следующего цыплёнка. Ключевая строка — randomSeed(chickSeed) в начале draw(): она «открывает книгу» на нужной странице, поэтому при одном и том же chickSeed ты всегда получишь идентичного цыплёнка.
Чтобы закрепить любимца, добавь вывод сида на холст или в консоль:
function draw() {
randomSeed(chickSeed);
drawAvatar();
fill(0);
textSize(16);
text('seed: ' + chickSeed, -width / 2 + 10, height / 2 - 15);
noLoop();
}Результат: в нижнем левом углу холста подписан номер сида, например seed: 73421. Запиши это число — и в любой момент, поставив chickSeed = 73421 прямо в setup(), ты соберёшь того же самого цыплёнка снова. Координаты текста идут от -width / 2, потому что мы рисуем внутри сдвинутой translate-сетки, где центр — это (0, 0).
Частые ошибки и подводные камни
- Забыл randomSeed() в начале draw(). Тогда сид ни на что не влияет, и цыплёнок каждый кадр разный — закрепить любимца не получится.
randomSeed(chickSeed)должен стоять до первогоrandom()в кадре. - random() в setup() вместо draw(). Если посчитать случайные параметры один раз в setup() и сохранить в переменные, сид уже не поможет их пересобрать по клику. Держи всю случайную «начинку» внутри функции рисования, которая запускается после
randomSeed(). - Перья без push()/pop(). Без закладок повороты в цикле складываются, и перья уезжают по спирали всё дальше от тела вместо аккуратного веера. Каждый
rotate()в цикле оборачивай в свою пару push/pop. - Забыл angleMode(DEGREES). Тогда
rotate(15)иrandom(360)воспринимаются как радианы, и углы выходят дикими. СтавьangleMode(DEGREES)в setup() один раз. - random(5, 12) как число перьев без floor(). Число перьев может получиться дробным (например 7.3), а цикл ждёт целое. Оборачивай в
floor(), иначе поведение будет странным. - Слишком широкий разброс цвета. Если отдать оттенок тела на
random(0, 360), цыплёнок может оказаться синим или зелёным и перестанет узнаваться. Держи диапазоны узкими (random(40, 60)) — герой должен оставаться собой.
Мини-проект: твой генератор аватаров
Теперь главное задание курса — доведи генератор до своего. Возьми код из начала урока за основу и добавь к нему хотя бы две новые случайные детали. Вот каркас и идеи:
function drawAvatar() {
let bodyHue = random(40, 60);
background(random(180, 220), 30, 95);
translate(width / 2, height / 2);
drawFeathers(bodyHue);
noStroke();
fill(bodyHue, 80, 100);
ellipse(0, 0, 160, 160);
// TODO 1: сделай размер тела случайным,
// например let bodySize = random(140, 180);
// и подставь его в ellipse вместо 160.
// TODO 2: добавь случайный хохолок на макушке —
// маленький эллипс или треугольник в точке (0, -90),
// повёрнутый на random(-20, 20) внутри push()/pop().
fill(30, 90, 100);
let beakTilt = random(-15, 15);
push();
rotate(beakTilt);
triangle(70, -10, 70, 10, 105, 0);
pop();
fill(0);
ellipse(30, -30, 16, 16);
}Идеи, что ещё можно отдать случайности: размер и положение глаза, цвет клюва из узкого диапазона оранжевого, второй ряд перьев другого оттенка, лёгкий случайный наклон всего цыплёнка через rotate() на пару градусов. Главное правило — добавляй каждую новую деталь в свою таблицу параметров с понятным диапазоном, чтобы цыплёнок не превратился в кашу.
А когда найдёшь сид цыплёнка, который тебе особенно нравится, запиши его и пропиши chickSeed = твоёЧисло прямо в setup() — так у тебя будет «официальный» аватар, который собирается одинаково каждый раз. Поэкспериментируй: расширь диапазон перьев до random(8, 20) и получишь пушистого одуванчика, сузь оттенок фона — и небо станет спокойнее. Это твой проект, ломай и пересобирай его сколько хочешь.
Итоги и что дальше
Ты собрал настоящий генеративный проект и закрепил всё, чему учился весь курс:
- Генеративный аватар — это набор правил, а не один рисунок; ты решаешь, что постоянно, а что случайно.
- Сначала список параметров и диапазонов, потом код — так проще держать героя узнаваемым.
random()в узких диапазонах даёт разнообразие без хаоса;floor()нужен там, где требуется целое число.push()/pop()иrotate()позволяют наклонять детали и раскидывать перья, не ломая остальную картинку.randomSeed()в началеdraw()делает понравившегося цыплёнка воспроизводимым: один сид — один и тот же аватар.
У тебя на руках готовая работа, которой не стыдно поделиться. В следующем уроке раздела мы разберёмся, как навести на скетч последний лоск и опубликовать его в интернете — чтобы дать ссылку другу, и он увидел твоего CodeChick прямо в браузере. До встречи на холсте!