Лінія і Круг

Щоб перевірити, чи круг перетинається з лінією, ми використаємо код із попередніх прикладів. Подібну практику ми використовуватимемо і в решті книги. Результівна математика стає трохи заплутаною, але ми спростимо складні частини.

Спочатку перевіримо, чи один із кінців лінії знаходиться всередині круга. Це може статися зокрема, якщо відрізок набагато менший за круг. Для цього ми можемо використати код із розділу Точка і Круг з початку книги. Якщо будь-який кінець відрізка знаходиться всередині, тоді одразу повертаємо true.

const inside1 = isPointWithCircleCollides(x1,y1, cx,cy,r);
const inside2 = isPointWithCircleCollides(x2,y2, cx,cy,r);

if (inside1 || inside2) {
    return true;
}

Далі нам потрібно знайти найближчу точку на лінії. Для початку давайте обчислимо довжину лінії за допомогою теореми Піфагора:

const distX = x1 - x2;
const distY = y1 - y2;
const len = sqrt((distX * distX) + (distY * distY));

Потім ми отримуємо значення, яке англійською коротко називають dot. Якщо раніше ви стикалися з векторною математикою, то мова йде про скалярний добуток двох векторів. Якщо ж для вас це щось нове, то не хвилюйтеся і вважайте цю частину кроком з такою математикою, яку вам не хотілося та не доведеться рахувати вручну

const dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / pow(len, 2);

Ви завжди можете почитати більше про скалярний добуток, щоб зрозуміти його суть краще. Наприклад в книзі Nature Of Code Деніела Шиффмана є цілий розділ де доступною мовою розповідається про вектори, а у розділі автономних агентів про скалярний добуток та цілі його використання, зокрема і про визначення кута між двома векторами.

Нарешті, ми можемо використати це рівняння, щоб знайти найближчу точку на прямій:

const closestX = x1 + (dot * (x2 - x1));
const closestY = y1 + (dot * (y2 - y1));

Однак це повертає точку в будь-якому місці прямої, оскільки вона тягнеться до нескінченності в обох напрямках. Іншими словами, це може дати нам точку з кінця лінії! Отже, перевірмо, чи ця найближча точка насправді знаходиться на нашому відрізку, використовуючи щойно створений алгоритм з розділу Лінія і Точка. Це один з перших випадків, коли ми будемо вкладати функції з попередніх розділів під час роботи над складнішими зіткненнями.

Якщо точка знаходиться на відрізку значить ми можемо продовжувати. Якщо ні, тоді ми можемо негайно повернутися false, оскільки це означає, що найближча точка знаходиться на лінії поза одним із кінців відрізка:

const isOnSegment = isLineWithPointCollides(x1, y1, x2, y2, closestX, closestY);

if (!isOnSegment) {
    return false;
}

Нарешті, ще раз використовуючи теорему Піфагора, ми вираховуємо відстань від круга до найближчої точки на лінії:

const distX = closestX - cx;
const distY = closestY - cy;
const distance = sqrt((distX * distX) + (distY * distY));

Якщо ця відстань менша за радіус, тоді ми маємо колізію (так само як Точка і Круг).

if (distance <= r) {
    return true;
}

return false;

Ось повний приклад, у якому зібрано усі частини. Зверніть увагу, що у нас внизу є три функції: та, яку ми щойно створили, і дві з попередніх розділів.

// координати та радіус круга
let cx = 0;
let cy = 0;
let r = 30;

// координати відрізка
let x1, y1;
let x2, y2;

function setup() {
  createCanvas(window.innerWidth, window.innerHeight);
  strokeWeight(15); // збільшена жирність, щоб краще бачити лінію

  x1 = 100;
  y1 = height - 100;
  x2 = width - 100;
  y2 = 100;
}

function draw() {
  background(255);

  // оновлення координат круга координатами курсора
  cx = mouseX;
  cy = mouseY;

  // результат перевірки на зіткнення
  const isHit = isLineWithCircleCollides(x1, y1, x2, y2, cx, cy, r);

  // зміна кольору при зіткненні
  if (isHit) {
    stroke(255, 150, 0, 150);
  } else {
    stroke(0, 150, 255, 150);
  }

  // малювання лінії
  line(x1, y1, x2, y2);

  // малювання круга
  fill(0, 150);
  noStroke();
  ellipse(cx, cy, r * 2, r * 2);
}

// перевірка на перетин між лінією та кругом
function isLineWithCircleCollides(x1, y1, x2, y2, cx, cy, r) {
  // якщо один з кінців всередині круга,
  // тоді одразу повертаємо true
  const inside1 = isPointWithCircleCollides(x1, y1, cx, cy, r);
  const inside2 = isPointWithCircleCollides(x2, y2, cx, cy, r);
  if (inside1 || inside2) return true;

  // отримання довжини лінії
  let distX = x1 - x2;
  let distY = y1 - y2;
  const len = sqrt((distX * distX) + (distY * distY));

  // скалярний добуток прямої та круга
  const dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / pow(len, 2);

  // знаходження найближчої точки на прямій
  const closestX = x1 + (dot * (x2 - x1));
  const closestY = y1 + (dot * (y2 - y1));

  // ця точка дійсно знаходиться на відрізку?
  // якщо так, продовжуємо, але повертаємо false
  const onSegment = isLineWithPointCollides(x1, y1, x2, y2, closestX, closestY);
  if (!onSegment) return false;

  // малювання кружечка, щоб підсвітити найближчу точку на лінії (опціонально)
  fill(255, 0, 0);
  noStroke();
  ellipse(closestX, closestY, 20, 20);

  // обчислення відстані до найближчої точки
  distX = closestX - cx;
  distY = closestY - cy;
  const distance = sqrt((distX * distX) + (distY * distY));

  if (distance <= r) {
    return true;
  }

  return false;
}

// перевірка на перетин між лінією та точкою
function isLineWithPointCollides(x1, y1, x2, y2, px, py) {
  // обчислення відстані від точки до двох кінців відрізка
  const d1 = dist(px, py, x1, y1);
  const d2 = dist(px, py, x2, y2);

  // обчислення довжини відрізка
  const lineLen = dist(x1, y1, x2, y2);

  // невеликий додатковий буфер для збільшення області перетину
  const buffer = 0.1;

  // якщо сума двох відстаней дорівнює довжині відрізка, тоді точка знаходиться на відрізку!
  // зауважте, що тут ми додатково використовуємо буфер для збільшення радіуса зіткнення
  if (d1 + d2 >= lineLen - buffer && d1 + d2 <= lineLen + buffer) {
    return true;
  }

  return false;
}

// перевірка на перетин між точкою та кругом
function isPointWithCircleCollides(px, py, cx, cy, r) {
  // отримання дистанції між точкою та центром круга
  // за допомоги теореми Піфагора
  const distX = px - cx;
  const distY = py - cy;
  const distance = sqrt((distX * distX) + (distY * distY));

  // якщо відстань менша за радіус кола, значить точка всередині!
  if (distance <= r) {
    return true;
  }

  return false;
}

Реалізацію потрібної математики при роботі з лініями й не лише з ними можна отримати від деяких вбудованих методів класу p5.Vector. Якщо раніше ви не користувалися векторним обʼєктом, можливо, варто трохи з ними ознайомитись. У чудовій книзі Деніела Шиффмана The Nature Of Code є цілий розділ присвячений векторам, який написано дуже простою і доступною мовою. Ми також трохи розглянемо p5.Vector, коли почнемо працювати з багатокутниками.

Цей приклад базується на коді Philip Nicoletti. і там більше опису того, як працює цей алгоритм, і математики, що стоїть за ним.

Далі: Лінія і Лінія