Клики и состояния

Один клик — и цыплёнок засыпает; ещё клик — просыпается. Сегодня учим CodeChick реагировать на нажатие мыши и помнить, в каком он состоянии.
Состояние — это то, что скетч помнит между кадрами: открыт глаз или закрыт, день сейчас или ночь, спит цыплёнок или скачет. Клик переключает состояние, а draw() каждый кадр смотрит на него и решает, что рисовать.

Зачем это вообще нужно

Представь любую игру в телефоне. Ты тапаешь по экрану — и что-то происходит: персонаж прыгает, лампочка загорается, лайк под постом меняет цвет с серого на красный. Программа не просто рисует картинку — она слушает, что ты делаешь, и помнит, что было раньше. Лайк красный? Значит, ты уже лайкнул. Тапнешь ещё раз — снова станет серым. Это и есть переключение состояния по клику.

В прошлом уроке про мышь и координаты mouseX/mouseY наш цыплёнок следил за курсором — двигался туда, куда ты ведёшь мышь. Это здорово, но он реагировал всё время, непрерывно. А сегодня мы научим его реагировать на отдельное событие — на сам момент нажатия. И не просто дёргаться, а запоминать результат: кликнул — заснул, кликнул ещё раз — проснулся.

К концу урока ты соберёшь скетч, где CodeChick спит с закрытыми глазами на тёмном фоне, а по клику просыпается: фон светлеет, глаз открывается. Каждый новый клик переключает его туда-обратно, как выключатель света в комнате. Поехали разбираться, как это устроено.

Подумай, сколько вокруг тебя вещей, которые работают по этому принципу. Кнопка паузы в плеере: жмёшь — музыка останавливается, жмёшь снова — играет дальше. Тёмная тема в приложении: один тап — и весь экран перекрашивается. Сторис в соцсети: тап вправо — следующий кадр, тап влево — предыдущий. Везде одно и то же: программа ловит твоё нажатие и меняет что-то, что она помнит. Сегодня ты впервые соберёшь такой механизм своими руками, и дальше будешь узнавать его в каждой второй программе.

Метафора: выключатель света

Самая близкая вещь из жизни — обычный выключатель на стене. У него ровно два положения: свет горит или свет погашен. Ты не держишь его пальцем всё время — ты просто щёлкаешь один раз, и он остаётся в новом положении сам. Щёлкнул снова — вернулся обратно.

В p5.js этот выключатель собирается из двух кусочков:

  • Память о положении — переменная, которая хранит, горит свет сейчас или нет. У нас это будет Переменная состояния — обычно булева (true/false).
  • Реакция на щелчок — функция, которая срабатывает в момент нажатия мыши. В p5.js это mousePressed().
mousePressed() — это специальная функция p5.js: ты её просто объявляешь, а библиотека сама вызывает её один раз в тот момент, когда ты нажал кнопку мыши. Не каждый кадр, а именно на событие нажатия.

Важно почувствовать разницу. draw() — это художник, который без устали перерисовывает кадр за кадром, много раз в секунду. А mousePressed() — это звоночек на двери: он молчит, молчит, а потом дзынь — кто-то нажал. Ты пишешь, что делать на этот «дзынь», а всё остальное время функция спит.

Почему важно ловить именно событие, а не проверять мышь каждый кадр? Допустим, ты решил по-другому: в draw() смотреть, нажата ли кнопка мыши (для этого есть встроенная переменная mouseIsPressed). Но draw() крутится десятки раз в секунду, а палец на кнопке ты держишь хотя бы пару десятых секунды. Значит, за один твой щелчок состояние переключится не один раз, а много — и выключатель будет бешено мигать туда-сюда, пока ты не отпустишь. С mousePressed() такой беды нет: она срабатывает ровно в момент нажатия и ровно один раз, сколько бы ты ни держал кнопку. Поэтому для переключателей всегда бери событие, а не проверку состояния кнопки.

Пример 1. Самый первый клик

Начнём с минимума: будем считать, сколько раз ты кликнул, и показывать это словом в консоли. Никакой графики — только чтобы увидеть, что событие вообще ловится.

let cliks = 0;

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(255, 240, 180);
}

function mousePressed() {
  cliks = cliks + 1;
  console.log('Цыплёнок клюнул зерно ' + cliks + ' раз');
}

Результат: на экране ровный тёплый жёлтый холст, как солнечное гнездо. Сам холст не меняется, но каждый раз, когда ты кликаешь по нему мышью, в консоли появляется новая строка: «Цыплёнок клюнул зерно 1 раз», потом «...2 раз», «...3 раз». Счётчик растёт ровно на единицу за клик — значит, mousePressed() срабатывает строго по одному разу на нажатие.

Что здесь происходит по шагам

  1. Снаружи всех функций мы завели переменную cliks = 0. Она живёт всё время работы скетча и помнит число между кадрами и кликами — это и есть переменная состояния.
  2. setup() один раз создаёт холст 400 на 400.
  3. draw() просто заливает фон — он крутится постоянно, но ничего интересного тут пока не делает.
  4. mousePressed() мы объявили рядом с setup() и draw(), на том же уровне. p5.js сам её найдёт по имени и вызовет на каждое нажатие. Внутри мы увеличиваем счётчик и печатаем строку.

Запомни главное: mousePressed() не нужно нигде «включать» или вызывать вручную. Достаточно объявить функцию ровно с таким именем — p5.js делает остальное.

Пример 2. Цыплёнок-выключатель

Теперь самое вкусное — переключение состояния. Заведём булеву переменную spit (спит): если true — цыплёнок дремлет, фон тёмный, глаз закрыт; если false — бодрствует, фон светлый, глаз открыт. Клик будет переворачивать значение.

let spit = true;

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  if (spit) {
    background(40, 40, 70);
  } else {
    background(180, 220, 255);
  }

  // тело цыплёнка
  fill(255, 215, 0);
  ellipse(200, 220, 160, 150);

  // голова
  ellipse(200, 130, 110, 100);

  // клюв
  fill(255, 140, 0);
  triangle(245, 125, 245, 145, 285, 135);

  // глаз: открыт или закрыт
  if (spit) {
    stroke(0);
    strokeWeight(3);
    line(190, 120, 215, 120);
    noStroke();
  } else {
    fill(0);
    ellipse(205, 118, 18, 18);
  }
}

function mousePressed() {
  spit = !spit;
}

Результат: на старте фон тёмно-синий, как ночь, и у жёлтого цыплёнка вместо глаза просто короткая чёрточка — он спит. Кликаешь мышью — и в тот же миг фон становится светло-голубым, как дневное небо, а на месте чёрточки появляется круглый чёрный глаз: цыплёнок проснулся и смотрит на тебя. Ещё клик — снова ночь и закрытый глаз. Так туда-сюда, сколько хочешь.

Сердце примера — строка spit = !spit

Знак ! читается как «не» и переворачивает булево значение наоборот: !true даёт false, а !false даёт true. Поэтому строка spit = !spit означает «возьми текущее значение и сделай противоположным». Был true — стал false, и наоборот. Это классический трюк для выключателя: одна строка, и переключение готово.

Дальше всё держится на draw(). Он каждый кадр спрашивает переменную: if (spit) — рисуй ночь и закрытый глаз, else — рисуй день и открытый. Сам mousePressed() ничего не рисует! Он только меняет переменную. А художник draw() через долю секунды увидит новое значение и перерисует картинку. Получается красивое разделение труда: клик меняет память, кадр читает память и показывает.

Пример 3. Три состояния по кругу

Выключатель с двумя положениями — это хорошо, но иногда нужно больше. Представь настроение цыплёнка: спокойное, радостное, сонное — и клик переключает их по кругу. Для этого булевой переменной мало, возьмём число-режим.

let nastroenie = 0; // 0 спокоен, 1 радостен, 2 сонный

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(255, 245, 200);

  fill(255, 215, 0);
  ellipse(200, 200, 180, 170);

  fill(0);
  if (nastroenie === 0) {
    // спокоен: два кружка-глаза
    ellipse(175, 185, 16, 16);
    ellipse(225, 185, 16, 16);
  } else if (nastroenie === 1) {
    // радостен: глаза-дуги вверх
    noFill();
    stroke(0);
    strokeWeight(4);
    arc(175, 190, 24, 24, PI, TWO_PI);
    arc(225, 190, 24, 24, PI, TWO_PI);
    noStroke();
  } else {
    // сонный: чёрточки
    stroke(0);
    strokeWeight(4);
    line(165, 185, 185, 185);
    line(215, 185, 235, 185);
    noStroke();
  }
}

function mousePressed() {
  nastroenie = (nastroenie + 1) % 3;
}

Результат: жёлтый цыплёнок на кремовом фоне. Сначала у него два спокойных круглых глаза. Кликнул — глаза превратились в две весёлые дуги, будто он улыбается ими. Ещё клик — дуги стали ровными чёрточками, цыплёнок задремал. Четвёртый клик возвращает к спокойным круглым глазам, и круг повторяется.

Магия строки (nastroenie + 1) % 3

Здесь работает оператор остатка от деления %. Мы прибавляем единицу к режиму, а потом берём остаток от деления на 3. Смотри, как бежит число: 0 → 1 → 2 → снова 0. Когда дошли до 3, остаток от деления на 3 равен 0 — и счётчик красиво заворачивается обратно в начало. Этот приём называют «зацикливанием по модулю», и он пригодится тебе всюду, где нужно перебирать варианты по кругу: смена темы оформления, переключение кадров спрайта, листание слайдов.

Обрати внимание на разницу подходов. В примере 2 у нас было ровно два состояния, и булева переменная подошла идеально — она и так умеет хранить только true или false. А когда вариантов три и больше, булевой уже мало: тут на помощь приходит число-режим. Правило простое: два состояния — бери булеву и переворачивай через !; три и больше — бери число и крути его по модулю. Оба способа делают одно и то же — хранят состояние и переключают его по клику.

Пример 4. Клик по самому цыплёнку

Пока мы реагировали на клик в любом месте холста. Но в настоящих приложениях кнопки занимают не весь экран — важно понять, попал ли клик именно по объекту. Сделаем так: цыплёнок пищит (выводит сообщение и чуть подпрыгивает), только если ты кликнул прямо по нему, а не мимо.

let pisknul = false;

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(200, 235, 255);

  // если пискнул — поднимем чуть выше
  let y = 220;
  if (pisknul) {
    y = 200;
  }

  fill(255, 215, 0);
  ellipse(200, y, 140, 130);

  fill(255, 140, 0);
  triangle(255, y - 5, 255, y + 15, 295, y + 5);
}

function mousePressed() {
  // расстояние от клика до центра цыплёнка
  let d = dist(mouseX, mouseY, 200, 220);
  if (d < 70) {
    pisknul = !pisknul;
    console.log('Пи-пи!');
  }
}

Результат: на голубом фоне сидит жёлтый цыплёнок. Если ты кликаешь мимо него, в пустое небо — ничего не происходит. Но стоит кликнуть прямо по телу цыплёнка, и он подскакивает чуть вверх, а в консоли появляется «Пи-пи!». Кликнул по нему ещё раз — опускается обратно. Клики мимо по-прежнему игнорируются.

Как мы поняли, что попали по цыплёнку

Здесь работает функция dist() — она считает расстояние между двумя точками. Мы измеряем, насколько далеко место клика (mouseX, mouseY из прошлого урока) от центра цыплёнка (200, 220). Если расстояние меньше 70 — значит, клик попал внутрь его круглого тела, и тогда мы переключаем состояние. Если дальше — клик мимо, и if просто не срабатывает. Это и есть основа любой кнопки: проверить, попал ли курсор в нужную область, и только тогда что-то делать.

Частые ошибки и подводные камни

1. Кладут mousePressed() внутрь draw()

Самая популярная ошибка новичка — написать function mousePressed() внутри фигурных скобок draw() или setup(). Так нельзя: p5.js ищет эту функцию на самом верхнем уровне, рядом с setup и draw. Если спрятать её внутри другой функции, библиотека её просто не найдёт, и клики перестанут работать. Объявляй mousePressed() отдельным блоком, не вложенным.

2. Меняют картинку прямо в mousePressed() вместо переменной

Хочется внутри mousePressed() сразу нарисовать новый глаз или сменить фон. Но draw() через миг перерисует весь кадр заново и затрёт то, что ты нарисовал на клик. Правильный путь: в mousePressed() меняй переменную состояния, а рисуй всегда в draw() по этой переменной. Клик отвечает за «что запомнить», кадр — за «что показать».

3. Объявляют переменную состояния внутри функции

Если написать let spit = true; внутри draw(), она будет рождаться заново на каждом кадре и тут же сбрасываться в true — переключение не запомнится. Переменная состояния должна жить снаружи всех функций, в самом верху файла. Только тогда она переживает кадры и клики.

4. Путают = и === при проверке режима

В условии if (nastroenie === 0) нужны три знака равенства — это сравнение. Один знак = — это присваивание, и если написать if (nastroenie = 0), ты случайно обнулишь переменную прямо в проверке. Картинка застрянет на одном состоянии. Для сравнения всегда ===.

5. Забывают, что !spit ничего не меняет без присваивания

Строка !spit; сама по себе бесполезна: она вычисляет противоположное значение и тут же выбрасывает его. Чтобы переключение сохранилось, результат надо записать обратно: spit = !spit;. Без левой части spit = переменная не изменится, и цыплёнок не проснётся.

Мини-проект: режим день и ночь

Собери свой переключатель сцены. Базу бери из примера 2 и доделай сам:

  1. Заведи булеву переменную den (день) со старта true.
  2. В draw() по if (den) рисуй светлый фон и жёлтое солнце-кружок в углу; в else — тёмный фон и белый месяц.
  3. Цыплёнка рисуй всегда, но в режиме ночи добавь ему закрытый глаз (чёрточку), а днём — открытый круглый глаз.
  4. В mousePressed() переключай den = !den;.
  5. Усложни: добавь третье состояние «закат» через число-режим и оператор % 3, как в примере 3, и подбери для заката оранжево-розовый фон.

Когда заработает — поэкспериментируй: пусть на закате у цыплёнка будет ещё и румянец (маленький розовый кружок на щеке). Поменяй цвета фонов, посмотри, какое сочетание тебе нравится. Это твоя сцена — играй с ней.

Итоги

Сегодня ты научил CodeChick реагировать на отдельное событие, а не дёргаться без остановки. Главное, что стоит унести с собой:

  • mousePressed() — функция-звоночек: p5.js сам вызывает её один раз на каждое нажатие мыши. Объявляй её на верхнем уровне.
  • Переменная состояния живёт снаружи функций и помнит, что было между кадрами и кликами.
  • Клик меняет переменную, а рисует всегда draw(), читая эту переменную. Не рисуй прямо в обработчике клика.
  • Булеву переключают через spit = !spit, а перебор по кругу — через (режим + 1) % число.

Это фундамент любой интерактивности: лайки, кнопки, режимы, паузы в играх — всё держится на «событие меняет состояние, кадр его показывает». В следующем уроке мы дадим цыплёнку слушать клавиатуру: стрелки будут двигать его по холсту, а пробел — заставлять подпрыгивать. Тот же приём с состоянием, только источник события другой. До встречи в коде!

Проверьте себя
1. Сколько раз p5.js вызывает функцию mousePressed() при одном нажатии кнопки мыши?
AНи разу — её надо вызывать вручную
BРовно один раз — в момент нажатия
CКаждый кадр, пока кнопка нажата
D60 раз в секунду
2. Что делает строка spit = !spit;?
AПереворачивает булево значение на противоположное
BУдаляет переменную spit
CСравнивает spit с самой собой
DВсегда делает spit равным true
3. Где нужно объявлять переменную состояния, чтобы она помнила значение между кадрами и кликами?
AВнутри draw()
BВнутри mousePressed()
CСнаружи всех функций, в верху файла
DВнутри setup() после createCanvas
4. Почему не стоит рисовать новый кадр прямо внутри mousePressed()?
Ap5.js запрещает рисовать вне draw()
Bdraw() через миг перерисует кадр и затрёт нарисованное на клик
CmousePressed() не умеет вызывать fill()
DЭто замедлит скетч в два раза
5. Что выведет выражение (nastroenie + 1) % 3, если nastroenie сейчас равен 2?
A3
B2
C0
D1
6. Какой оператор сравнения правильно использовать в условии if при проверке режима?
A= (один знак)
B=== (три знака)
C!=
D:=