От круга к многоугольнику
Столкновение двух кругов определяется одним условием. Для треугольника такого простого условия нет. На этом занятии сравниваются четыре способа обнаружения столкновения: от быстрого приближённого до точного.
Основные результаты
- построить охватывающую окружность треугольника;
- проверить столкновение по контрольным точкам на сторонах;
- определить, по какую сторону от прямой лежит точка;
- обнаружить пересечение траектории шарика со стороной треугольника;
- сравнить точность и трудоёмкость методов;
- перенести один из методов в собственный проект.
Вершины треугольника можно перетаскивать мышью. Выберите метод и увеличьте скорость: при большой скорости приближённые методы начинают ошибаться.
Конструкции, которые понадобятся на занятии
Справочник не изучается отдельно. Возвращайтесь к нему, когда в тексте или в заготовке встречается незнакомая запись.
| Запись | Назначение |
|---|---|
{ a: { x: 10, y: 20 } } | Объект, свойство которого само является объектом. |
tri.a.x | Координата x вершины a объекта tri. Точки читаются слева направо: сначала объект, затем его свойство. |
return выражение; | Завершает функцию и возвращает значение выражения в место вызова. |
return { x: cx, y: cy }; | Функция возвращает новый объект. |
[[a, b], [b, c]] | Массив, элементы которого тоже являются массивами. |
sides[1][0] | Первый элемент второго элемента массива sides. Нумерация начинается с нуля. |
| Запись | Назначение |
|---|---|
Math.max(a, b, c) | Возвращает наибольшее из чисел. |
Math.sqrt(v) | Возвращает квадратный корень числа. |
Math.abs(v) | Возвращает модуль числа: Math.abs(-3) равно 3. |
a && b | Логическое «и»: истинно, только если истинны оба условия. |
a || b | Логическое «или»: истинно, если истинно хотя бы одно условие. |
ctx.setLineDash([6, 4]) | Задаёт пунктир: штрих 6 пикселей, промежуток 4 пикселя. Пустой массив возвращает сплошную линию. |
new Path2D() | Создаёт объект-контур, который можно рисовать и проверять. |
ctx.isPointInPath(path, x, y) | Возвращает true, если точка (x, y) лежит внутри контура. |
Изученные ранее функции, которые используются в заготовках
function distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function isColliding(a, b) {
return distance(a, b) <= a.r + b.r;
}Повторение занятия 4
выбор · сборка · порядокФрагменты перетаскиваются мышью в пропуски. Можно также выбрать фрагмент щелчком, а затем щёлкнуть по пропуску. Повторный щелчок по заполненному пропуску возвращает фрагмент на место.
particles три частицы с индексами 0, 1 и 2. Какие пары (i, j) будут проверены при выполнении этого кода?for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
// проверка пары (i, j)
}
}
function distance(a, b) {
const dx = ;
const dy = ;
return Math.sqrt();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);— очистить холстbounceWalls();— отразить частицы, вышедшие за границы холстаdrawParticles();— нарисовать частицы в новых положенияхmoveParticles();— изменить координаты частиц на величину скоростиfindCollisions();— отметить частицы, которые столкнулись друг с другом
Почему треугольник сложнее круга
Круг полностью задаётся центром и радиусом. Поэтому для двух кругов достаточно сравнить расстояние между центрами с суммой радиусов. Треугольник задаётся тремя вершинами, а его граница состоит из трёх отрезков. Расстояние от шарика до такой границы зависит от того, к какой стороне или вершине шарик расположен ближе.
Треугольник в программе
Каждая вершина хранится как объект с координатами, а треугольник — как объект из трёх вершин.
const triangle = {
a: { x: 330, y: 45 },
b: { x: 450, y: 270 },
c: { x: 210, y: 235 }
};
triangle.b.y // 270
Два вида ошибок
Ложное срабатывание: программа сообщает о столкновении, хотя шарик препятствия не касается.
Пропуск столкновения: шарик задел препятствие или прошёл сквозь него, а программа этого не обнаружила.
Приближённые методы допускают ошибки одного или обоих видов. Их достоинство — простота и скорость вычислений.
| Метод | Идея | Характер результата |
|---|---|---|
| 1. Охватывающая окружность | Заменить треугольник окружностью. | Приближённый |
| 2. Контрольные точки | Проверить набор точек на сторонах. | Приближённый; точность зависит от числа точек |
| 3. Траектория | Проверить, пересекает ли путь центра шарика сторону треугольника. | Точный для траектории центра |
4. isPointInPath | Поручить проверку точек браузеру. | Приближённый; зависит от выбора точек |
Охватывающая окружность
Приближённый метод заменяет сложную фигуру простой. Треугольник помещают внутрь окружности и проверяют столкновение шарика с этой окружностью. Условие столкновения двух кругов уже известно.
Алгоритм
- Найдите центр окружности. Его координаты равны средним арифметическим координат вершин. Эта точка называется центроидом треугольника.
- Найдите радиус: расстояние от центра до самой удалённой вершины. Тогда все три вершины, а значит, и весь треугольник лежат внутри окружности.
- Столкновение обнаружено, если расстояние между центром шарика и центром окружности не больше суммы их радиусов.
function boundingCircle(tri) {
const cx = (tri.a.x + tri.b.x + tri.c.x) / 3;
const cy = (tri.a.y + tri.b.y + tri.c.y) / 3;
const center = { x: cx, y: cy };
const r = Math.max(
distance(center, tri.a),
distance(center, tri.b),
distance(center, tri.c)
);
return { x: cx, y: cy, r: r };
}
function hitCircle(p, tri) {
const c = boundingCircle(tri);
return distance(p, c) <= p.r + c.r;
}
Тренировка: охватывающая окружность
const cx = (tri.a.x + tri.b.x + tri.c.x) / ;
const r = Math.(
distance(center, tri.a),
distance(center, tri.b),
distance(center, tri.c)
);
return distance(p, c) p.r + c.r;
Практикум: охватывающая окружность
Дополните заготовку. После каждого запуска эмулятор проверяет поведение программы и отмечает выполненные задания. Вершины треугольника можно перетаскивать мышью прямо на рисунке.
Контрольные точки на сторонах
Второй приближённый метод проверяет не всю границу треугольника, а набор точек на ней. Если хотя бы одна точка оказалась внутри шарика, столкновение считается обнаруженным. Для каждой точки применяется известное условие: расстояние от точки до центра шарика не больше радиуса.
Точка на отрезке
Точка отрезка AB задаётся числом t от 0 до 1. Число показывает, какую долю пути от A до B нужно пройти.
y = a.y + (b.y − a.y) · t
При t = 0 получается точка A, при t = 1 — точка B, при t = 0.5 — середина отрезка.
function pointOnSegment(a, b, t) {
return {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t
};
}
function triangleSides(tri) {
return [[tri.a, tri.b], [tri.b, tri.c], [tri.c, tri.a]];
}
function sidePoints(a, b, n) {
const points = [];
for (let k = 0; k <= n; k++) {
points.push(pointOnSegment(a, b, k / n));
}
return points;
}
function hitPoints(p, tri, n) {
const sides = triangleSides(tri);
for (let s = 0; s < sides.length; s++) {
const points = sidePoints(sides[s][0], sides[s][1], n);
for (let k = 0; k < points.length; k++) {
if (distance(p, points[k]) <= p.r) {
return true;
}
}
}
return false;
}
Как работает функция
Внешний цикл перебирает три стороны, внутренний — точки текущей стороны.
Команда return true внутри цикла сразу завершает функцию: одной найденной точки достаточно, остальные проверять не нужно.
Команда return false выполняется только тогда, когда циклы перебрали все точки и ни одна не подошла.
Исследование
Перетащите шарик к середине стороны между двумя точками. Затем увеличивайте n, пока касание не будет обнаружено.
n уменьшает шаг, но увеличивает объём вычислений: на каждую частицу приходится 3 · (n + 1) проверок расстояния.Тренировка: контрольные точки на сторонах
pointOnSegment({x: 0, y: 0}, {x: 100, y: 40}, 0.25)?sidePoints, чтобы на стороне получилось n + 1 точек от A до B.for (let k = 0; k n; k++) {
points.push(pointOnSegment(a, b, ));
}
- Для каждой точки вычислить расстояние до центра шарика.
- Если ни одна точка не подошла, вернуть
false. - Составить массив из трёх сторон треугольника.
- Если расстояние не больше радиуса, вернуть
true. - Для каждой стороны построить контрольные точки.
n = 10, радиус шарика равен 8. Может ли шарик пройти через сторону между точками?Практикум: контрольные точки на сторонах
Дополните заготовку. Когда все задания будут выполнены, измените значение POINTS_N на 2 и запустите программу снова. Объясните, почему некоторые частицы проходят сквозь стороны.
Добавьте треугольное препятствие в свой проект
Эмулятор нужен для изучения метода. Основной проект по-прежнему хранится в файлах на компьютере. Перенесите в него метод охватывающей окружности.
project с тремя файлами: index.html (холст и панель управления), style.css (оформление) и script.js (частицы отражаются от стенок, столкновения отмечаются красным, работают пауза и ползунок скорости). Распакуйте архив и работайте с файлами из папки project.
Порядок действий
- Откройте папку проекта. Откройте
script.jsв Notepad++, аindex.html— в браузере. - Скопируйте функции метода кнопкой ниже и вставьте их в
script.jsперед функциейfindTriangleHits. Функцияdistanceв проекте уже есть, поэтому её повтор из скопированного фрагмента удалите. - Измените координаты вершин в объекте
triangleтак, чтобы треугольник поместился на холсте 600 × 300. - В функции
drawпосле очистки холста вызовитеdrawTriangle(triangle). - В функции
findTriangleHits(tri)раскомментируйте строкуp.hitTri = hitCircle(p, tri);. Результат записывается в отдельное свойствоhitTri, поэтому отметкиhitо столкновениях частиц друг с другом сохраняются. - В функции
tickпослеfindCollisions();вызовитеfindTriangleHits(triangle);. ФункцияdrawParticleуже окрашивает частицы сhitTriв оранжевый цвет. - Сохраните файл сочетанием Ctrl + S и обновите страницу клавишей F5.
Проверка
x, y и r. Если в вашем проекте радиус называется иначе, например size, переименуйте свойство или передайте в функцию объект с нужными именами.Итоги первой части
Изучено
Два приближённых метода. Охватывающая окружность требует одной проверки расстояния, но даёт ложные срабатывания. Контрольные точки точнее, но требуют больше вычислений и пропускают быстрые шарики.
После перерыва
Точный метод: проверка траектории. Программа будет определять не только факт столкновения, но и сторону треугольника, о которую ударился шарик.
По какую сторону от прямой лежит точка
Прямая, проходящая через точки A и B, делит плоскость на две части. Чтобы определить, в какой из них лежит точка P, вычисляют значение выражения:
function side(a, b, p) {
return (b.x - a.x) * (p.y - a.y) -
(b.y - a.y) * (p.x - a.x);
}
Значение side(a, b, p) | Положение точки P |
|---|---|
| больше 0 | по одну сторону от прямой AB |
| меньше 0 | по другую сторону от прямой AB |
| равно 0 | на прямой AB |
Исследование
Перетаскивайте точки A, B и P. Области по разные стороны от прямой окрашены в разные цвета.
Найдите положение P, при котором значение равно нулю. Что происходит со знаком, если поменять местами A и B?
side(a, b, p) равен удвоенной площади треугольника ABP, а знак показывает направление поворота от вектора AB к вектору AP. В математике такое выражение называют векторным произведением (точнее, его единственной ненулевой координатой для векторов на плоскости). На Canvas ось y направлена вниз, поэтому положительное значение соответствует точкам, которые лежат справа, если смотреть из A в сторону B.side не содержит деления и работает при любом расположении точек.Пересекает ли траектория сторону треугольника
За один шаг центр шарика перемещается из точки P1 = (x, y) в точку P2 = (x + vx, y + vy). Отрезок P1P2 называется траекторией шага. Если траектория пересекла сторону AB, шарик столкнулся с этой стороной, даже если ни в одном проверенном положении он стороны не касался.
Условие пересечения двух отрезков
Отрезки P1P2 и AB пересекаются, если одновременно выполнены два условия:
- точки P1 и P2 лежат по разные стороны от прямой AB;
- точки A и B лежат по разные стороны от прямой P1P2.
Две точки лежат по разные стороны от прямой, если значения side для них имеют разные знаки, то есть их произведение отрицательно.
Исследование
Перетаскивайте точки. Найдите положение, при котором первое условие выполнено, а второе нет. Как при этом расположена траектория относительно отрезка AB?
function segmentsCross(p1, p2, a, b) {
const d1 = side(a, b, p1);
const d2 = side(a, b, p2);
const d3 = side(p1, p2, a);
const d4 = side(p1, p2, b);
return d1 * d2 < 0 && d3 * d4 < 0;
}
function crossedSide(p, tri) {
const next = { x: p.x + p.vx, y: p.y + p.vy };
const sides = triangleSides(tri);
for (let s = 0; s < sides.length; s++) {
if (segmentsCross(p, next, sides[s][0], sides[s][1])) {
return s;
}
}
return -1;
}
Пояснения
Одного первого условия недостаточно. Точки P1 и P2 могут лежать по разные стороны от прямой AB, но траектория при этом проходит мимо отрезка AB, за его концом.
Функция crossedSide возвращает номер пересечённой стороны: 0 — AB, 1 — BC, 2 — CA. Значение -1 означает, что пересечения нет. Такое соглашение принято, потому что номер стороны не может быть отрицательным.
Строгое неравенство < 0 не учитывает точного касания, например прохождения траектории через вершину. При дробных координатах такие случаи редки, и в учебной модели ими можно пренебречь.
Реакция на столкновение: отражение от наклонной стороны
Номер стороны позволяет выбрать реакцию: подсветить сторону, изменить счёт или отразить шарик. Отражение выполняет готовая функция reflect. Скорость шарика раскладывается на две составляющие: вдоль стороны и перпендикулярно ей. Составляющая вдоль стороны сохраняется, а перпендикулярная меняет знак. Для горизонтальной стороны результат совпадает с известной командой p.vy = -p.vy.
function reflect(p, a, b) {
const len = distance(a, b);
const nx = -(b.y - a.y) / len; // единичный вектор,
const ny = (b.x - a.x) / len; // перпендикулярный стороне
const dot = p.vx * nx + p.vy * ny;
p.vx = p.vx - 2 * dot * nx;
p.vy = p.vy - 2 * dot * ny;
}
r. Это дополнительная задача практикума.Тренировка: пересечение траектории со сторонами треугольника
side(A, B, P) для A(0, 0), B(10, 0) и P(3, 4).side(A, B, P) равно 0. Что это означает?return d1 * d2 d3 * d4 ;
crossedSide в правильном порядке.for (let s = 0; s < sides.length; s++) {return -1;const next = { x: p.x + p.vx, y: p.y + p.vy };if (segmentsCross(p, next, sides[s][0], sides[s][1])) return s;const sides = triangleSides(tri);}
crossedSide, и его смысл.012-1Практикум: пересечение траектории со сторонами треугольника
Первая частица заготовки движется очень быстро. Пока функции не дописаны, она пролетает сквозь треугольник. После выполнения заданий она должна отражаться от сторон, а пересечённая сторона — подсвечиваться.
Дополнительные задачи
- Счётчик ударов. Создайте массив
hits = [0, 0, 0]и увеличивайте элемент с номером пересечённой стороны. Выводите значения в консоль командойconsole.log(hits). - Учёт радиуса. Перед проверкой сдвиньте концы стороны наружу на
p.rвдоль перпендикуляра(nx, ny)из функцииreflect. Направление «наружу» определяется по знакуsideдля центроида треугольника.
Проверка средствами Canvas
Контекст рисования Canvas умеет сам определять, лежит ли точка внутри фигуры. Фигуру описывают объектом Path2D — контуром, а проверку выполняет метод isPointInPath. Математику выполняет браузер, поэтому метод подходит для фигур любой формы: звезды, многоугольника, фигуры с дугами.
function trianglePath(tri) {
const path = new Path2D();
path.moveTo(tri.a.x, tri.a.y);
path.lineTo(tri.b.x, tri.b.y);
path.lineTo(tri.c.x, tri.c.y);
path.closePath();
return path;
}
function hitPath(p, tri) {
const path = trianglePath(tri);
const points = [
{ x: p.x, y: p.y },
{ x: p.x + p.r, y: p.y },
{ x: p.x - p.r, y: p.y },
{ x: p.x, y: p.y + p.r },
{ x: p.x, y: p.y - p.r }
];
for (let k = 0; k < points.length; k++) {
if (ctx.isPointInPath(path, points[k].x, points[k].y)) {
return true;
}
}
return false;
}
Достоинства и недостатки
Достоинства: не требуется геометрических формул; форма препятствия может быть любой; тот же контур используется для рисования.
Недостатки: проверяются только пять точек шарика, поэтому шарик может задеть угол треугольника между ними; быстрый шарик по-прежнему может пролететь насквозь; метод не сообщает, о какую сторону произошёл удар.
Метод isPointInPath принадлежит контексту ctx, поэтому функция работает только там, где этот контекст создан.
Практикум: проверка средствами Canvas
Сравнение методов
| Метод | Проверок на одну частицу | Точность | Типичная ошибка |
|---|---|---|---|
| 1. Окружность | 1 сравнение расстояния | Низкая | Ложное срабатывание |
| 2. Точки | 3 · (n + 1) сравнений | Зависит от n | Пропуск между точками; шарик внутри треугольника |
| 3. Траектория | 3 проверки пересечения отрезков | Высокая для центра шарика | Погружение в сторону на радиус, если стороны не сдвинуты |
4. isPointInPath | до 5 проверок точек | Средняя | Пропуск при большой скорости или касании углом |
Перенесите в проект более точный метод
Выберите метод контрольных точек или метод траектории и замените им проверку окружностью в своём проекте. Метод траектории сложнее, но позволяет отражать объекты от сторон.
Порядок действий
- Сохраните текущую работающую версию проекта. Создайте копию
script.jsс понятным именем, напримерscript-circle.js. - Скопируйте функции выбранного метода и вставьте их в
script.js. Функциюdistanceи объектtriangleповторно не вставляйте, если они уже есть. - Метод контрольных точек: в функции
findTriangleHitsзаменитеhitCircle(p, tri)наhitPoints(p, tri, POINTS_N). - Метод траектории: в функции
findTriangleHitsвызовитеcrossedSide(p, tri). Если результат не равен −1, присвойтеp.hitTri = trueи добавьте реакцию: отражение функциейreflectили подсветку стороны. ВызовfindTriangleHits(triangle)перенесите в началоtick: сразу после проверкиrunningи до изменения координат. В проекте частица за шаг сдвигается наp.vx * speed, поэтому вcrossedSideвычисляйтеnextс учётомspeed. - Проверьте работу при обычной и при большой скорости объектов.
- Сохраните актуальную копию папки проекта в Google Drive.
Проверка
Проверьте результат занятия
Проект
Объяснение
Завершение работы
Нажмите кнопку после проверки всех чек-листов. Результаты тренажёров и практикумов будут отправлены учителю.