Мини-проект: генеративный аватар

Пришло время собрать всё, чему ты научился, в одну работу: генератор уникальных аватаров 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 прямо в браузере. До встречи на холсте!

Проверьте себя
1. Чем генеративный аватар отличается от обычного рисунка?
AЭто набор правил, по которым код рисует множество разных вариантов
BЭто один рисунок, нарисованный вручную пиксель за пикселем
CЭто фотография, обработанная фильтром
DЭто анимация из заранее заготовленных кадров
2. Зачем в начале draw() стоит randomSeed(chickSeed)?
AЧтобы ускорить отрисовку кадра
BЧтобы при одном и том же сиде получался один и тот же цыплёнок
CЧтобы сделать цвета ярче
DЧтобы отключить случайность совсем
3. Почему каждое перо в цикле оборачивают в push() и pop()?
AЧтобы перья были ярче
BЧтобы перья рисовались быстрее
CЧтобы повороты не накапливались и перья не уезжали по спирали
DЧтобы сменить режим цвета на HSB
4. Зачем число перьев оборачивают в floor(random(5, 12))?
AЧтобы перьев было ровно 12
BЧтобы получить целое число, ведь цикл не умеет крутиться дробное количество раз
CЧтобы перья стали крупнее
DЧтобы задать им случайный цвет
5. Почему оттенок тела берут из узкого диапазона random(40, 60), а не random(0, 360)?
AТак код работает быстрее
BЧтобы цыплёнок оставался жёлтым и узнаваемым, а не синим или зелёным
CПотому что random() не принимает большие числа
DЧтобы фон тоже стал жёлтым
6. Как закрепить понравившегося цыплёнка, чтобы он собирался одинаково каждый раз?
AСделать скриншот холста
BЗаписать его сид и задать chickSeed этим числом в setup()
CУбрать все random() из кода
DПоставить noLoop() в начало setup()