Тач и адаптив для телефона

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

Главное правило урока: чтобы скетч был играбельным на телефоне, нужно две вещи — ловить касания экрана (а не только мышь) и подстраивать размер холста под окно через windowWidth и windowHeight.

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

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

А ведь почти все твои друзья откроют ссылку именно с телефона. TikTok, мемы, игры, музыка — всё это давно живёт в кармане, а не за монитором. Если твоя работа не работает на телефоне, считай, её половина людей вообще не увидит по-человечески. И дело не только в друзьях: даже если ты делаешь скетч для себя, приятно, когда его можно открыть с любого устройства под рукой, а не объяснять «запускай только с компьютера, иначе всё кривое».

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

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

Звучит как много работы? На самом деле это буквально несколько новых функций. Давай разберёмся.

Холст под размер экрана: windowWidth и windowHeight

Проблема фиксированного размера

Когда ты пишешь createCanvas(400, 400), ты прибиваешь холст гвоздями к размеру 400 на 400 пикселей. На широком мониторе это нормально. Но экран телефона узкий и высокий — например, 390 пикселей в ширину и 800 в высоту. Холст 400 в него уже не влезает по ширине, и браузер показывает горизонтальную полосу прокрутки. Выглядит криво.

Решение простое: не задавать размер числом, а спросить у браузера, какого размера сейчас окно, и сделать холст ровно таким. Для этого у p5.js есть две встроенные переменные — как mouseX и mouseY, только про окно:

  • windowWidth — ширина окна браузера в пикселях прямо сейчас;
  • windowHeight — высота окна браузера в пикселях прямо сейчас.

Метафора: представь, что ты раскладываешь плед на полу. Можно отрезать кусок ткани заранее «на глаз» — и он окажется то мал, то велик. А можно сначала посмотреть на комнату и раскроить плед ровно по полу. windowWidth и windowHeight — это как взгляд на комнату: ты узнаёшь реальный размер места и кроишь холст точно под него.

Холст на весь экран

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
}

function draw() {
  background(135, 206, 235); // небо на весь экран

  fill(255, 209, 64);
  circle(width / 2, height / 2, 120); // цыплёнок ровно по центру
}

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

Разберём по шагам. createCanvas(windowWidth, windowHeight) создаёт холст ровно такого размера, какое сейчас окно. Дальше мы рисуем цыплёнка не в точке (200, 200), как раньше, а в width / 2, height / 2 — то есть в середине холста. Тут важная деталь: width и height — это встроенные переменные p5.js с реальными размерами холста. Раз холст занял весь экран, width / 2 — это его центр на любом устройстве. Если бы мы написали circle(200, 200, 120), цыплёнок на большом экране уехал бы в левый верхний угол.

Запомни: когда холст подстраивается под экран, забудь про числа-координаты вроде 200 или 400 и считай всё от width и height. Центр — это width / 2, правый край — width, низ — height.

Когда экран повернули: windowResized()

На телефоне есть ещё одна засада. Человек повернул телефон из вертикального положения в горизонтальное — окно стало другим: было узкое и высокое, стало широкое и низкое. А наш холст создался один раз в setup() и о повороте не знает. Получится холст не того размера.

Чтобы холст подстраивался при изменении окна, у p5.js есть специальная функция windowResized(). Она работает как setup() или draw(): ты её просто объявляешь, а p5.js сам вызывает её каждый раз, когда окно меняет размер (поворот телефона, изменение размера окна на компьютере).

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // перекроить холст под новое окно
}

function draw() {
  background(135, 206, 235);
  fill(255, 209, 64);
  circle(width / 2, height / 2, 120);
}

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

Здесь главное — функция resizeCanvas(windowWidth, windowHeight). Она не создаёт новый холст, а меняет размер уже существующего. Внутри windowResized() мы говорим: «окно изменилось — подгони холст под свежие windowWidth и windowHeight». Поскольку мы рисуем всё от width и height, картинка сама встаёт на место.

Касания: палец вместо мыши

Чем мышь отличается от тача

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

Вот ключевая разница, которую важно прочувствовать:

МышьТач (палец)
Курсор есть всегда, можно «навести» не нажимаяКоординаты есть только пока палец на экране
Один указательМожет быть несколько пальцев сразу
mouseX, mouseYмассив touches со всеми касаниями
событие mousePressed()событие touchStarted()

Хорошая новость: p5.js многое делает за тебя. Когда ты касаешься экрана пальцем, p5.js сам кладёт координаты касания в те же mouseX и mouseY. То есть твой старый код с цыплёнком, следящим за мышью, на телефоне уже частично работает — палец двигает «мышь». Но чтобы поймать именно момент касания и сделать всё надёжно, есть отдельные события.

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

touchStarted: ловим момент касания

Функция touchStarted() — это «тач-версия» события нажатия. p5.js вызывает её ровно один раз в тот момент, когда палец впервые коснулся экрана. Внутри неё mouseX и mouseY уже содержат точку касания.

let chickX, chickY;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  chickX = width / 2;
  chickY = height / 2;
}

function draw() {
  background(135, 206, 235);

  // тело
  fill(255, 209, 64);
  circle(chickX, chickY, 120);
  // клюв
  fill(255, 140, 30);
  triangle(chickX, chickY - 10, chickX + 55, chickY, chickX, chickY + 14);
  // глаз
  fill(40);
  circle(chickX - 26, chickY - 22, 16);
}

function touchStarted() {
  // палец коснулся экрана — телепортируем цыплёнка туда
  chickX = mouseX;
  chickY = mouseY;
  return false; // не даём странице прокручиваться от касания
}

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

Разберём по шагам. У цыплёнка есть переменные состояния chickX и chickY — его позиция, которую мы рисуем в draw() каждый кадр. В setup() мы ставим его в центр. А функция touchStarted() срабатывает в момент касания и записывает в chickX, chickY координаты пальца — то есть mouseX и mouseY, которые p5.js заполнил за нас. Цыплёнок рисуется из нескольких фигур, и все они считаются от chickX, chickY, поэтому перепрыгивает он целиком, не разваливаясь.

Отдельно про загадочную строку return false; в конце. Без неё браузер на телефоне может решить, что ты хочешь прокрутить страницу, и начнёт её дёргать при каждом касании. return false говорит браузеру: «это касание для скетча, страницу не трогай». Запомни как привычку: в конце touchStarted() почти всегда ставь return false;.

touchMoved: цыплёнок едет за пальцем

А если хочется не телепорта, а чтобы цыплёнок ехал за пальцем, пока ты ведёшь им по экрану? Для этого есть touchMoved() — она вызывается, пока палец движется по стеклу.

let chickX, chickY;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  chickX = width / 2;
  chickY = height / 2;
}

function draw() {
  background(135, 206, 235);
  fill(255, 209, 64);
  circle(chickX, chickY, 120);
}

function touchMoved() {
  // цыплёнок мягко догоняет палец (easing)
  chickX = chickX + (mouseX - chickX) * 0.2;
  chickY = chickY + (mouseY - chickY) * 0.2;
  return false;
}

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

Обрати внимание: touchMoved() срабатывает только пока палец на экране и движется. Как только палец оторвался, движение прекращается — в этом и есть разница с мышью, у которой координаты есть всегда. На телефоне «навести, не касаясь» невозможно: нет пальца — нет координат.

Несколько пальцев сразу: массив touches

Ещё одно, чего мышь не умеет в принципе: на телефоне можно касаться экрана несколькими пальцами одновременно. Вспомни, как ты двумя пальцами увеличиваешь фотографию. p5.js складывает все активные касания в массив touches: у каждого касания есть свои .x и .y. mouseX и mouseY при этом держат координаты только первого пальца — поэтому для мультитача нужен именно массив.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
}

function draw() {
  background(135, 206, 235);

  // под каждым пальцем — свой цыплёнок
  fill(255, 209, 64);
  for (let i = 0; i < touches.length; i++) {
    circle(touches[i].x, touches[i].y, 100);
  }
}

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

Здесь мы перебираем массив touches обычным циклом, как делали со стаей цыплят раньше: для каждого касания touches[i] берём его .x и .y и рисуем там круг. Сколько пальцев на экране — столько и цыплят. Это уже совсем не то, что можно сделать мышью, и хороший пример того, чем мобильный интерактив богаче настольного.

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

  • Оставил createCanvas с числами. Написал createCanvas(400, 400) и удивляешься, почему на телефоне холст торчит за край и появилась полоса прокрутки. Для мобильного скетча используй createCanvas(windowWidth, windowHeight) — холст подстроится под экран.

  • Рисуешь по числам вместо width и height. Сделал холст на весь экран, но цыплёнка ставишь в circle(200, 200, 120). На большом экране он уедет в угол, а центр окажется пустым. Считай всё от width и height: центр — это width / 2, height / 2.

  • Забыл return false в touchStarted/touchMoved. При каждом касании страница дёргается и прокручивается, играть невозможно. Поставь return false; в конце тач-функции — браузер перестанет считать касание прокруткой.

  • Пишешь touchStarted внутри draw() или setup(). touchStarted(), touchMoved(), windowResized() — это отдельные функции верхнего уровня, как setup() и draw(). Их нельзя вкладывать одну в другую: объявляй каждую отдельно, а p5.js сам их вызовет в нужный момент.

  • Ждёшь mouseX в touchStarted, но проверял только на компьютере. На мышином компьютере касаний нет, и touchStarted() просто не сработает — будет казаться, что код не работает. Проверяй мобильное поведение на телефоне (или в режиме эмуляции устройства в браузере), а не только на ноутбуке.

Мини-практика: играбельный цыплёнок в кармане

Возьми за основу пример с touchStarted() и доведи его до настоящей мини-игры для телефона. План:

  1. Сделай так, чтобы при каждом касании цыплёнок не телепортировался резко, а плавно ехал к точке касания. Подсказка: запомни цель касания в targetX, targetY в touchStarted(), а в draw() двигай chickX к ней через easing.
  2. Нарисуй на небе неподвижное зёрнышко в случайной точке (от width и height). Когда цыплёнок доезжает близко к зерну, перенеси зерно в новую случайную точку — будто он его склевал.
  3. Добавь windowResized() с resizeCanvas(windowWidth, windowHeight) и проверь, что после поворота телефона всё остаётся на месте (помни: координаты считаем от width и height).
  4. Для смелых: выведи в углу счётчик склёванных зёрен через text(), размером width / 12, чтобы цифры читались и на маленьком экране.

Открой готовое на своём телефоне и дай потыкать другу. Меняй числа в easing и размере — и сразу смотри, как меняется ощущение игры под пальцем.

Итоги

Теперь твой цыплёнок живёт не только на компьютере, но и в кармане. Вот что ты освоил:

  • windowWidth и windowHeight — размеры окна прямо сейчас; createCanvas(windowWidth, windowHeight) растягивает холст на весь экран.
  • Когда холст под размер экрана, координаты считают от width и height, а не от чисел вроде 200 — иначе на разных устройствах всё съезжает.
  • windowResized() с resizeCanvas(...) перекраивает холст при повороте телефона и изменении окна.
  • Мышь и тач — разные: у мыши курсор есть всегда, у тача координаты появляются только пока палец на экране.
  • touchStarted() ловит момент касания, touchMoved() — движение пальца; внутри них работают mouseX/mouseY, а в конце ставь return false;, чтобы страница не прокручивалась.

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

Проверьте себя
1. Какой вызов createCanvas сделает холст во весь экран телефона?
AcreateCanvas(400, 400)
BcreateCanvas(windowWidth, windowHeight)
CcreateCanvas(fullScreen)
DcreateCanvas(screen)
2. Холст растянут на весь экран. Где правильно нарисовать цыплёнка по центру?
Acircle(200, 200, 120)
Bcircle(width / 2, height / 2, 120)
Ccircle(windowWidth, windowHeight, 120)
Dcircle(0, 0, 120)
3. Чем касание (тач) принципиально отличается от мыши?
AТач быстрее мыши
BУ мыши курсор есть всегда, а у тача координаты появляются только пока палец на экране
CТач не даёт координат вообще
DРазницы нет, p5.js всё делает одинаково
4. Зачем в конце touchStarted() ставят return false;?
AЧтобы цыплёнок исчез
BЧтобы браузер не прокручивал страницу в ответ на касание
CЧтобы функция выполнялась быстрее
DЭто обязательно для любой функции в p5.js
5. Какая функция сама вызывается при изменении размера окна (например, повороте телефона)?
Adraw()
BwindowResized()
Csetup()
DtouchMoved()
6. Ты написал touchStarted() и проверяешь скетч на ноутбуке мышкой — но он не срабатывает. Почему?
AВ коде обязательно ошибка
BtouchStarted() реагирует на касания экрана; на мышином компьютере касаний нет, проверять надо на телефоне или в эмуляции устройства
CtouchStarted() работает только в setup()
DНужно дописать createCanvas с числами