Компас D-Lab · занятие 3

Положение и движение частиц на Canvas

Повторение материала занятий 1–2 и новая задача: набор частиц на Canvas, который хранится в массиве, имеет координаты и скорость и реагирует на границы поля.

≈120 минут
Компас D-Lab · занятие 3
1 / 12
Введение · 5 минут

Порядок занятия

Занятие состоит из двух частей. Сначала — четыре блока повторения материала занятий 1 и 2, оформленные как контрольные вопросы. Задание, решённое без затруднений, не требует задержки: переходите к следующему блоку сразу. Далее следует новая тема — набор частиц на Canvas: от одной точки с известными координатами до множества частиц, которые перемещаются и отражаются от границ поля.

Структура занятия

1. Четыре блока повторения: HTML, подключение JavaScript, CSS, Canvas.
2. Новая тема: массив частиц, движение, отражение от границ.
3. Перенос результата в собственный проект.

Итоговая точка на сегодня у разных учеников будет разной — это заложено в структуре занятия, а не является отклонением от неё.

Блок 1 · HTML

Структура документа и атрибуты

Пять заданий на повторение материала занятия 1. Задания построены не на определениях, а на разборе конкретного кода — это ближе к тому, с чем вы уже работали в тренажёре.

1. В чём ошибка фрагмента <p>Первый абзац<p>Второй абзац</p>?
2. Почему тег <img src="cat.png"> используется без парного закрывающего тега?
3. На странице есть два элемента с одинаковым id="box". Чем это опасно?
4. Дан фрагмент: <img src="cat.png" ___="кот на подоконнике">. Какой атрибут пропущен, если нужно описать изображение текстом?
5. Разметка ошибочно помещена так: заголовок вкладки браузера записан внутри <body>. Что при этом произойдёт?
Подсказка

Задание 1: каждый парный тег закрывается до начала следующего элемента того же уровня, иначе структура документа искажается. Задание 2: исключение из этого правила — теги без содержимого, такие как <img>: у них нет пары. Задание 3: идентификатор id служит для однозначного обращения к элементу и не должен повторяться на странице — иначе команда, ищущая элемент по этому id, работает непредсказуемо. Задание 4: значение атрибута, описывающего изображение словами, записывается в alt. Задание 5: содержимое <head> не отображается на самой странице — это служебная часть документа, а не текст для читателя.

Теория блока: структура HTML-документа

HTML (HyperText Markup Language — язык гипертекстовой разметки) описывает, из каких частей состоит страница и как они вложены друг в друга. Полный минимальный документ выглядит так:

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Моя страница</title>
  </head>
  <body>
    <h1>Заголовок</h1>
    <p>Текст абзаца.</p>
    <img src="cat.png" alt="кот на подоконнике">
  </body>
</html>

Построчный разбор:

  • <!DOCTYPE html> — первая строка любого HTML-файла: сообщает браузеру, что документ написан по современным правилам HTML.
  • <html>…</html> — корневой элемент, внутри которого находится весь документ целиком.
  • <head>…</head> — служебная часть. Ничего из неё не отображается на самой странице. Здесь находится заголовок вкладки браузера (<title>) и подключения внешних файлов — CSS и JavaScript.
  • <body>…</body> — видимая часть: всё, что находится здесь, показывается пользователю.
  • <h1> — крупный заголовок раздела; более мелкие заголовки — <h2>, <h3> и так далее по убыванию значимости.
  • <p> (paragraph — абзац) — обычный текстовый блок.

Парные и одиночные теги. Большинство тегов работают в паре: у открывающего <p> есть закрывающий </p> с косой чертой, а между ними — содержимое элемента:

<p>Это содержимое абзаца.</p>

Небольшая группа тегов не имеет содержимого и потому используется без закрывающей пары — это одиночные (void) теги: <img> (изображение), <br> (перенос строки), <input> (поле ввода). Если пометить пару такому тегу — например, написать <img></img>, — лишний закрывающий тег не даст ошибки, но не будет иметь смысла и обычно не пишется.

Вложенность. Элементы можно вкладывать друг в друга, но открытые теги нужно закрывать в обратном порядке — как скобки в математике:

<!-- верно: p открыт, затем strong, затем strong закрыт, затем p -->
<p>Обычный текст и <strong>жирный текст</strong>.</p>

<!-- неверно: теги закрыты в неправильном порядке -->
<p>Обычный текст и <strong>жирный текст</p></strong>

Похожая по духу ошибка — незакрытый тег перед началом следующего элемента того же уровня: <p>Первый<p>Второй</p>. Браузер способен исправить некоторые такие ошибки самостоятельно, но полагаться на это не стоит: разметка должна оставаться понятной и без автоматических исправлений.

Атрибуты — дополнительные характеристики тега, которые записываются внутри открывающего тега в виде имя="значение", через пробел, если атрибутов несколько:

<img src="cat.png" alt="кот на подоконнике" id="mainPhoto">
  • src (source — источник) — путь к файлу изображения.
  • alt (alternative — альтернативный) — текстовое описание изображения на случай, если файл не загрузится, а также для программ чтения с экрана.
  • id — уникальное имя элемента. По этому имени к элементу впоследствии обращаются CSS и JavaScript. В отличие от класса (с которым мы познакомимся в следующем блоке), id не должен повторяться на странице: если два элемента получат одинаковый id, команда поиска по этому id найдёт только один из них, а какой именно — заранее не очевидно.

Наконец, в HTML можно оставлять комментарии — заметки для человека, читающего код, которые браузер не отображает: <!-- это комментарий -->.

Блок 2 · CSS

Селектор, свойство, значение

Четыре задания на разбор CSS-правил. Материал соответствует занятию 2.

1. В правиле #title { color white; } пропущен один символ, из-за которого правило не сработает. Запишите объявление верно.
2. Дано правило .card { padding: 10px; } и элемент <div id="card"> (без класса card). Применится ли к нему это правило?
3. Какой тег правильно подключает внешний файл style.css?
4. На странице три элемента с классом highlight. К скольким из них применится правило .highlight { color: red; }?
Подсказка

Задание 1: CSS-объявление состоит из свойства и значения, разделённых двоеточием, с точкой с запятой в конце: color: white;. Задание 2: точка перед именем в селекторе означает обращение по классу, а решётка — обращение по идентификатору; это разные атрибуты, и они не заменяют друг друга. Задание 3: внешний файл стилей подключается тегом <link rel="stylesheet" href="…">, а не тегом <script>. Задание 4: в отличие от id, класс может использоваться на любом числе элементов, и правило применится ко всем из них одновременно.

Теория блока: CSS — язык оформления

HTML описывает содержимое страницы: где заголовок, где абзац, где картинка. CSS (Cascading Style Sheets — каскадные таблицы стилей) описывает внешний вид этого содержимого: цвет, размер, отступы, шрифт. Одно и то же HTML-содержимое может выглядеть совершенно по-разному в зависимости от того, какой CSS к нему подключён.

Правило CSS состоит из двух частей: селектора и блока объявлений в фигурных скобках.

селектор {
  свойство: значение;
  свойство: значение;
}

Рассмотрим на конкретном примере:

#title {
  color: white;
  background-color: #123d32;
  padding: 12px;
  text-align: center;
}
  • #title — селектор: к какому элементу применяется оформление. Решётка означает «элемент с id="title"».
  • color: white; — свойство color отвечает за цвет текста; значение white — белый.
  • background-color: #123d32; — свойство цвета фона; значение записано в шестнадцатеричном формате (можно и словом, например green).
  • padding: 12px; — внутренний отступ между границей элемента и его содержимым, 12 пикселей со всех сторон.
  • text-align: center; — выравнивание текста по центру.

Три основных вида селектора различаются тем, скольких элементов они касаются:

  • p { … } — по имени тега: правило применится ко всем элементам <p> на странице.
  • .card { … } — по классу (точка перед именем): правило применится ко всем элементам с атрибутом class="card", сколько бы их ни было.
  • #header { … } — по идентификатору (решётка перед именем): правило применится к одному элементу с атрибутом id="header", поскольку id уникален.

Класс и id — разные атрибуты HTML, и селекторы для них пишутся по-разному: <div class="card"> подхватывается селектором .card, а <div id="card"> — селектором #card. Совпадение названия не делает их взаимозаменяемыми.

Подключение CSS к странице возможно тремя способами:

<!-- 1. Прямо в атрибуте, только для одного элемента -->
<p style="color: red;">Текст</p>

<!-- 2. Внутри самого документа, для всей страницы -->
<head>
  <style>
    p { color: red; }
  </style>
</head>

<!-- 3. Во внешнем файле — самый удобный способ для проекта -->
<head>
  <link rel="stylesheet" href="style.css">
</head>

Третий способ используется в собственном проекте: правила оформления собираются в отдельном файле style.css и подключаются тегом <link> в <head>. Это позволяет менять оформление сразу всей страницы, не трогая разметку в HTML-файле. Комментарий в CSS-файле записывается как /* текст комментария */ и браузером не учитывается.

Блок 3 · JavaScript

Обработчик события и первые команды

Пять заданий на разбор кода. Материал соответствует второй половине занятия 1.

1. Кнопка не реагирует на нажатие: <button onclik="start()">Старт</button>. В чём причина?
2. Что изменится на странице после выполнения команды document.getElementById("title").textContent = "Готово";?
3. Допишите свойство: document.body.style.___ = "orange"; — чтобы изменить цвет фона страницы.
4. Зачем в записи onclick="start()" после имени функции стоят круглые скобки?
5. Функция содержит две строки: сначала меняет фон, затем — заголовок. Какая из команд выполнится первой?
Подсказка

Задание 1: атрибут, связывающий клик с функцией, пишется как onclick, без опечаток. Задание 2: команда элемент.textContent = "…" заменяет видимый текст внутри найденного элемента, а не его id и не его оформление. Задание 3: свойство, отвечающее за цвет фона в JavaScript, называется backgroundColor — без пробела, с заглавной буквой C. Задание 4: круглые скобки после имени функции означают вызов — команду немедленно выполнить её содержимое. Задание 5: команды внутри функции выполняются последовательно, одна за другой, в порядке записи сверху вниз.

Теория блока: JavaScript — язык поведения страницы

Если HTML описывает содержимое, а CSS — оформление, то JavaScript отвечает за поведение: что происходит на странице в ответ на действия пользователя или течение времени. Код JavaScript размещается либо в отдельном файле, подключённом тегом <script src="script.js"></script>, либо прямо внутри тега <script>…</script>.

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

<button onclick="start()">Старт</button>

<script>
  function start() {
    document.body.style.backgroundColor = "orange";
  }
</script>

Построчный разбор:

  • onclick="start()" — атрибут кнопки: при щелчке вызвать функцию с именем start.
  • function start() { … } — объявление функции: создаёт именованную группу команд. Само объявление ничего не выполняет — команды внутри сработают только при вызове.
  • start() — вызов функции. Круглые скобки после имени означают «выполнить эту функцию сейчас».
  • document.body.style.backgroundColor = "orange"; — команда внутри функции: найти видимую часть страницы (document.body), обратиться к её оформлению (.style) и установить свойство цвета фона (.backgroundColor) в значение "orange".

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

document.getElementById("title").textContent = "Готово";
  • document — вся текущая страница, точка входа для любых команд JavaScript, работающих с содержимым.
  • getElementById("title") — метод, который ищет на странице единственный элемент с указанным id и возвращает его.
  • .textContent — свойство найденного элемента: его видимый текст.
  • = "Готово"; — оператор присваивания записывает новое значение в это свойство; точка с запятой отмечает конец команды.

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

function start() {
  document.body.style.backgroundColor = "orange"; // выполнится первой
  document.getElementById("title").textContent = "Готово"; // выполнится второй
}

Комментарий — текст после // до конца строки — предназначен для человека, читающего код, и браузером не выполняется.

Блок 4 · Canvas

Отрисовка и состояние

x = 60

Щелчок по полю изменяет переменную x и вызывает повторную отрисовку. Само по себе значение не меняется — его меняет код, реагирующий на действие.

1. Из цикла отрисовки убрали строку ctx.clearRect(0, 0, canvas.width, canvas.height);. Что произойдёт при повторной отрисовке движущегося объекта?
2. В команде ctx.arc(150, 80, 20, 0, Math.PI * 2) третий параметр задаёт радиус круга. Чему он равен?
3. Перед рисованием каждой новой фигуры вызывается ctx.beginPath(). Для чего это нужно?
Подсказка

Задание 1: Canvas не хранит прежние кадры — без очистки поля старое изображение остаётся под новым, и движение выглядит как след. Задание 2: у метода ctx.arc(x, y, радиус, начальный угол, конечный угол) третий параметр — радиус; в примере это число 20. Задание 3: команда beginPath() отделяет одну фигуру от другой, чтобы их контуры не соединялись между собой при заливке.

Теория блока: рисование на Canvas

Элемент <canvas> — область страницы для программного рисования: не изображение, а чистое полотно, на котором JavaScript рисует линии и фигуры командами.

<canvas id="scene" width="560" height="200"></canvas>

<script>
  const canvas = document.getElementById("scene");
  const ctx = canvas.getContext("2d");
</script>
  • width и height у самого тега <canvas> задают размер полотна в пикселях рисования — это не то же самое, что размер на экране через CSS.
  • canvas.getContext("2d") возвращает объект контекста — своего рода «инструмент рисования» для этого полотна. Все дальнейшие команды рисования вызываются у контекста (ctx), а не у самого canvas.

Система координат. Точка (0, 0) находится в левом верхнем углу поля. Координата x растёт вправо, а координата y растёт вниз — это отличается от привычной оси y на уроках математики, где вверх означает увеличение.

Рисование круга. Полный пример:

ctx.beginPath();
ctx.fillStyle = "green";
ctx.arc(150, 80, 20, 0, Math.PI * 2);
ctx.fill();
  • ctx.beginPath() — начать новый, независимый контур. Без этой команды новая фигура может «слипнуться» с предыдущей.
  • ctx.fillStyle = "green"; — устанавливает цвет, которым будет заполнена следующая фигура.
  • ctx.arc(x, y, радиус, начальный угол, конечный угол) — описывает контур окружности с центром в точке (x, y). Углы задаются в радианах; 0 и Math.PI * 2 означают полный оборот, то есть целую окружность.
  • ctx.fill() — заливает описанный контур цветом из fillStyle. Вместо заливки можно вызвать ctx.stroke() — тогда будет нарисован только контур, без заливки, цветом из strokeStyle.

Прямоугольники рисуются проще, без beginPath/fill — одной командой:

ctx.fillStyle = "orange";
ctx.fillRect(x, y, ширина, высота);

Очистка поля. Canvas не хранит историю кадров — всё, что нарисовано, остаётся на полотне навсегда, пока не будет явно стёрто:

ctx.clearRect(0, 0, canvas.width, canvas.height);

clearRect(x, y, ширина, высота) делает прозрачным прямоугольную область, начиная с точки (x, y). Указывая координаты (0, 0) и полные canvas.width, canvas.height, мы очищаем всё полотно целиком. Именно поэтому в любой Canvas-анимации перед отрисовкой нового кадра поле сначала полностью очищается — иначе прежние положения движущегося объекта останутся видны, образуя след.

Новая тема · 5 минут

Система частиц: координаты, массив, движение

Сегодня мы собираем на Canvas систему из множества однотипных объектов — частиц. Каждая частица описывается набором свойств (координаты, размер, скорость), а сам набор частиц хранится в массиве.

Положение

Координаты x и y задают точку на поле — тема, уже отработанная в блоке повторения по Canvas.

Хранение

Массив хранит сразу много частиц; цикл проходит по ним по очереди.

Движение

Скорость и условие на границе определяют, как частица перемещается со временем.

Работа строится по шагам: одна частица с известными координатами → массив частиц → движение → отражение от границ поля. Каждый шаг опирается на конструкции, уже разобранные в блоках повторения, и вводит одну новую идею языка — массив, цикл, повтор во времени или условие.
Использование ИИ. Формулировка задачи целиком («сделай модель частиц») ассистенту не передаётся. Порядок работы: сформулировать словами, что должна делать конкретная функция; запросить у ассистента только эту функцию; подписать к результату краткий комментарий, поясняющий его работу. Если пояснить полученный код не удаётся — переход к следующему шагу откладывается.
Ступень 1 · Одна частица

Одна частица: переменные и координаты

Коротко: объявление переменных — let и const

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

  • let x = 280; — объявляет переменную x и сразу записывает в неё значение 280. Слово let используется, когда значение переменной в дальнейшем будет меняться (например, координата движущейся частицы).
  • const ctx = canvas.getContext("2d"); — объявляет переменную, значение которой в дальнейшем меняться не будет. Слово const (от constant — постоянный) защищает от случайной повторной записи в эту переменную: попытка присвоить ей новое значение вызовет ошибку.

Выбор между let и const — это сообщение читающему код человеку (и самому себе в будущем) о намерении: «это значение постоянно» или «это значение будет меняться». В коде ниже canvas и ctx объявлены через const, потому что они находятся один раз и далее не переназначаются, а x, y, r — через let, потому что координаты частицы в следующих ступенях начнут меняться.

Первый шаг — неподвижная частица на Canvas, построенная теми же средствами, что и в блоке 4.

const canvas = document.getElementById("solidCanvas");
const ctx = canvas.getContext("2d");

let x = 280;
let y = 100;
let r = 10;

ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
1. Значение x изменили с 280 на 60. На сколько пикселей сместился центр частицы?
2. Запишите команду, рисующую круг радиусом r с центром в точке (x, y).
Ступень 2 · Массив частиц

Массив как хранилище нескольких значений

Коротко: массив, объект, цикл for и Math.random()

Массив — упорядоченный набор значений, записанный в одной переменной. Значения перечисляются через запятую в квадратных скобках: let numbers = [10, 20, 30];. Обращение к отдельному значению происходит по номеру — индексу, который начинается с нуля: numbers[0] равно 10, numbers[1] равно 20. Добавить новое значение в конец массива позволяет метод .push(): numbers.push(40);. Свойство .length хранит число элементов в массиве: для numbers после добавления четвёртого значения numbers.length равно 4.

Объект — набор значений, у каждого из которых есть собственное имя (ключ), а не порядковый номер, как в массиве. Запись {x: 10, y: 20, r: 6} создаёт объект с тремя свойствами: x, y и r. Чтобы получить значение конкретного свойства, к переменной, хранящей объект, дописывают точку и имя свойства — точно так же, как мы уже делали в document.body.style:

let p = { x: 10, y: 20, r: 6 };
p.x;       // 10 — читаем свойство
p.x = 15;  // записываем в свойство новое значение

Массив и объект часто используются вместе: массив хранит много однотипных «карточек»-объектов, а каждый объект описывает свойства одной из них. Именно так будут устроены частицы в этом занятии — массив particles, где каждый элемент представляет собой объект с полями x, y, r.

Чтобы не обращаться к каждому элементу массива по отдельности, используется цикл — конструкция, повторяющая одно и то же действие заданное число раз. Классическая запись:

for (let i = 0; i < numbers.length; i++) {
  // команды, использующие numbers[i]
}

Заголовок цикла состоит из трёх частей, разделённых точкой с запятой:

  • let i = 0 — выполняется один раз перед первым повтором: объявляет переменную-счётчик и присваивает ей начальное значение.
  • i < numbers.length — условие, проверяемое перед каждым повтором. Пока оно истинно, цикл продолжается; как только становится ложным — цикл останавливается.
  • i++ — выполняется после каждого повтора: увеличивает i на 1. Запись i++ — сокращение для i = i + 1.

В результате i по очереди принимает значения 0, 1, 2 и так далее до numbers.length - 1, то есть перебирает номер каждого элемента массива. Метод массива .forEach() решает ту же задачу более короткой записью: numbers.forEach(n => { ... }) — команда внутри выполнится для каждого значения n массива по очереди, без явного счётчика. Здесь n => { ... } — функция без имени, записанная короче обычной: значок => («стрелка») заменяет слово function, а n перед стрелкой — параметр, в который на каждом шаге подставляется очередной элемент массива.

Команда Math.random() возвращает случайное дробное число от 0 (включительно) до 1 (не включительно). Чтобы получить случайное число в пределах ширины поля, значение умножают на нужную величину: Math.random() * canvas.width.

Отдельная переменная на каждую частицу неудобна при их значительном числе. Массив хранит список частиц, а цикл for последовательно обращается к каждому элементу этого списка.

let particles = [];
for (let i = 0; i < 30; i++) {
  particles.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    r: 6
  });
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
    ctx.fill();
  }
}
draw();
1. Число повторений цикла определяет количество созданных частиц. Сколько частиц создаст приведённый код?
2. Что произойдёт, если в функции draw убрать цикл for и оставить только одно обращение particles[0]?
3. Запишите выражение, дающее случайную координату x в пределах ширины Canvas.

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

Ступень 3 · Движение частиц

Движение: обновление состояния через равные промежутки времени

Коротко: setInterval

Команда setInterval(функция, интервал) запускает переданную функцию не один раз, а многократно, повторяя вызов через указанный интервал времени в миллисекундах (1000 мс = 1 секунда). Запись setInterval(tick, 30) означает: выполнять функцию tick каждые 30 миллисекунд, то есть примерно 33 раза в секунду. Если на каждом таком повторе координата объекта немного меняется и сцена перерисовывается заново, глаз воспринимает последовательность отдельных положений как непрерывное движение — на этом принципе основана любая компьютерная анимация.

Изменение координаты записывается через составное присваивание: p.x += p.vx; — сокращение для p.x = p.x + p.vx;, то есть «прибавить к текущему значению p.x величину p.vx и сохранить результат обратно в p.x». Такая запись короче и привычна для любых повторяющихся накоплений значения, не только для координат.

Каждой частице присваивается скорость — компоненты vx и vy. На каждом шаге координата увеличивается на величину скорости, после чего сцена перерисовывается. Повтор действия через равные промежутки времени обеспечивает команда setInterval.

particles.forEach(p => {
  p.vx = (Math.random() - 0.5) * 1.5;
  p.vy = (Math.random() - 0.5) * 1.5;
});

function tick() {
  particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;
  });
  draw();
}
setInterval(tick, 30);
1. С каким интервалом повторяется вызов функции tick?
2. Значение во втором аргументе setInterval(tick, 30) изменили на 300. Как изменится видимая скорость частиц?
3. Запишите строку, изменяющую координату x частицы на величину её скорости.
Ступень 4 · Отражение от границ

Отражение от границы: конструкция if

Коротко: условие if и операторы сравнения

Конструкция if (условие) { команды } выполняет команды в фигурных скобках только тогда, когда условие истинно. Условие — это выражение, принимающее одно из двух логических значений: true (истина) или false (ложь). Значения сравниваются операторами <, >, <=, >= (меньше, больше, меньше или равно, больше или равно) и === (равно). Несколько условий можно объединить оператором || («или»): выражение a || b истинно, если истинно хотя бы одно из условий a и b.

Важно не путать одиночный знак = и тройной ===: одиночный = — это присваивание, он записывает значение в переменную (мы уже использовали его в let x = 280;). Тройной === — это сравнение, он проверяет, равны ли два значения, и сам по себе ничего никуда не записывает. Существует ещё двойной знак ==, тоже сравнение, но менее строгое: перед проверкой он пытается привести значения к одному типу, из-за чего 0 == false и 5 == "5" дают true, хотя слева и справа разные типы данных. Такое поведение сложно удержать в голове, поэтому в этом курсе для сравнения всегда используется только ===.

Чтобы частица не покидала пределы Canvas, перед перемещением проверяется её положение относительно границы. При достижении края направление движения по соответствующей оси меняется на противоположное.

function tick() {
  particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;

    if (p.x < p.r || p.x > canvas.width - p.r) {
      p.vx = -p.vx;
    }
    if (p.y < p.r || p.y > canvas.height - p.r) {
      p.vy = -p.vy;
    }
  });
  draw();
}
1. Какое действие выполняет строка p.vx = -p.vx;?
2. Условие p.x < p.r убрали, оставив только проверку правой границы. Что произойдёт с частицей у левого края?
3. По образцу условия для оси x запишите условие проверки границы по оси y.
При наличии времени можно добавить элемент управления, изменяющий скорость всех частиц по нажатию кнопки — например, умножением vx и vy на коэффициент. Это небольшой самостоятельный шаг в сторону темы событий, которая будет разобрана подробно на одном из следующих занятий.
Продолжение · самостоятельно или на следующем занятии

Столкновение частиц друг с другом

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

// Общий принцип — не обязательно реализовывать сегодня
function distance(a, b) {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  return Math.sqrt(dx * dx + dy * dy);
}
// если distance(a, b) меньше суммы a.r и b.r — частицы столкнулись
Результат сегодняшней работы переносится в папку собственного проекта (index.html, script.js и, при использовании, отдельный файл стилей). Файлам следует присвоить понятные названия; актуальная копия сохраняется в Google Drive.
Итоги занятия

Результат занятия

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

Проверка перед завершением

Файлы сохранены и понятно названы; страница открывается в браузере без ошибок; каждая написанная сегодня строка кода может быть объяснена.

Следующее занятие

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