Розділ 1. Вектори

Я вчиняю злочини спрямовано і велично.

— Вектор, Нікчемний Я

Паличкова мапа Маршаллових островів на виставці в Художньому музеї Берклі (фото Джима Хіфі)
Паличкова мапа Маршаллових островів на виставці в Художньому музеї Берклі (фото Джима Хіфі)

Паличкова мапа — це навігаційний інструмент, створений корінним населенням Маршаллових островів, розташованих у центральній частині Тихого океану. Цей стародавній інструмент був виготовлений з ретельно зв’язаних середніх жилок кокосових листків. Мушлі на мапі вказують на розташування островів у регіоні. Розташування листків і мушель слугує географічним орієнтиром, пропонуючи абстрактне представлення векторів, які фіксують патерни хвилювання океану і їх напрямок.


Ця книга присвячена вивченню навколишнього нас світу і розробці способів його симуляції за допомогою коду. Першу частину книги я почну з розгляду основ фізики: як яблуко падає з дерева, як маятник коливається на повітрі, як земля обертається навколо сонця тощо. Абсолютно все, що міститься в перших п’яти розділах книги, вимагає використання найпростішого будівельного блоку для програмування руху — вектора. Саме з цього я і розпочну нашу подорож.

Слово вектор може означати багато речей. Це і назва рок-гурту нової хвилі, сформованого на початку 1980-х років в Сакраменто, штат Каліфорнія, і назва пластівців для сніданку, вироблених компанією Kellogg’s Canada. В області епідеміології вектор — це організм, який передає інфекцію від одного господаря до іншого. У мові програмування C++ вектор (std::vector) є імплементацією структури даних масиву, що динамічно змінює свій розмір.

Хоча всі ці визначення певною мірою цікаві, вони тут поза увагою. Натомість цей розділ розглядає евклідів вектор (названий на честь грецького математика Евкліда), також відомий як геометричний вектор. Коли ви бачите у цій книзі термін вектор, можете вважати, що він стосується евклідового вектора, визначеного як сутність, яка має магнітуду (величину) і напрямок.

Малюнок 1.1: Вектор, представлений у вигляді стрілки, проведеної з точки A до точки B
Малюнок 1.1: Вектор, представлений у вигляді стрілки, проведеної з точки АА до точки ВВ

Вектор зазвичай малюється у вигляді стрілки, як на малюнку 1.1. Напрямок вектора вказується вістрям стрілки, а його величина — довжиною цієї стрілки.

Вектор на малюнку 1.1 зображений у вигляді стрілки від точки АА до точки ВВ. Він служить інструкцією, як рухатися від АА до ВВ.

Суть векторів

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

let x = 100;
let y = 100;
let xspeed = 2.5;
let yspeed = 2;

Змінні для положення і швидкості кульки.


function setup() {

  createCanvas(640, 240);

}


function draw() {

  background(255);


  x = x + xspeed;
  y = y + yspeed;

Переміщення кульки відповідно до її швидкості.

  if (x > width || x < 0) {
    xspeed = xspeed * -1;
  }
  if (y > height || y < 0) {
    yspeed = yspeed * -1;
  }

Перевірка кульки на дотик до стінок.

  stroke(0);
  fill(127);
  circle(x, y, 48);

Малювання кульки у вигляді круга за координатами (x, y).

}

У цьому прикладі є плаский 2D світ — порожнє полотно з кулькою, яка по ньому рухається. Ця кулька має такі властивості, як положення та швидкість, які представлені в коді окремими змінними:

ВластивістьНазви змінних
Положенняx і y
Швидкістьxspeed і yspeed

У складнішому прикладі ви можете мати набагато більше змінних, що представляють інші властивості кульки та її середовища:

ВластивістьНазви змінних
Прискоренняxacceleration і yacceleration
Цільове положенняxtarget і ytarget
Вітерxwind і ywind
Тертяxfriction і yfriction

Ви можете помітити, що для кожного концепту в цьому світі (вітру, положення, прискорення тощо) є дві змінні. І це лише 2D світ. У 3D-світі для кожної властивості вам знадобляться вже три змінні: x, y і z для представлення положення, xspeed, yspeed та zspeed для швидкості й так далі. Хіба не було б добре спростити код, використовуючи менше змінних? Замість того, щоб починати програму з чогось такого:

let x;

let y;

let xspeed;

let yspeed;

я волів би почати її з чогось подібного:

let position;

let speed;

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

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

У вступі до векторів я збираюся дотримуватися двовимірного їх представлення (принаймні у перших кількох розділах). Усі ці приклади можна досить легко розширити до трьох вимірів і клас p5.Vector, який я буду використовувати, має таку можливість. Однак, при вивченні основ, додаткове ускладнення з третім виміром відвертатиме увагу від основної мети.

Вектори у p5.js

Подумайте про вектор як про різницю між двома точками або як інструкцію для переходу від однієї точки до іншої. Наприклад, на малюнку 1.2 показано деякі вектори та їх можливі інтерпретації.

Малюнок 1.2: Три приклади векторів, намальованих у вигляді стрілок, із супровідними інструкціями для руху в напрямках на північ, південь, схід або захід
Малюнок 1.2: Три приклади векторів, намальованих у вигляді стрілок, із супровідними інструкціями для руху в напрямках на північ, південь, схід або захід

Ці вектори можна розглядати наступним чином:

ВекторІнструкції
(15,3)(-15, 3)Пройдіть 15 кроків на захід, а потім 3 кроки на північ.
(3,4)(3, 4)Пройдіть 3 кроки на схід, а потім 4 кроки на північ.
(2,1)(2, -1)Пройдіть 2 кроки на схід, а потім 1 крок на південь.

Можливо, ви вже думали про це таким чином, коли програмували рух. Для кожного кадру анімації (одна ітерація функції draw() у p5.js) ви вказуєте кожному об’єкту перемістити себе в нове місце на певну кількість пікселів по горизонталі та певну кількість пікселів по вертикалі. Ця інструкція по суті є вектором, як на малюнку 1.3. Він має магнітуду (як далеко ви рухалися) і напрямок (яким шляхом ви йшли).

Малюнок 1.3: Вектор вказує кількість горизонтальних і вертикальних кроків для переходу у нове положення
Малюнок 1.3: Вектор вказує кількість горизонтальних і вертикальних кроків для переходу у нове положення

Вектор встановлює швидкість об’єкта, визначаючи темп зміни положення об’єкта відносно часу. Іншими словами, вектор швидкості визначає нове положення об’єкта для кожного кадру анімації відповідно до базового алгоритму руху: нове положення дорівнює результату застосування швидкості до поточного положення.

Якщо швидкість є вектором (різницею між двома точками), то що стосовно положення? Воно теж вектор? Технічно можна стверджувати, що положення не є вектором, оскільки воно не описує, як рухатися від однієї точки до іншої, а описує одну точку в просторі. Однак, ось інший спосіб описати положення — це шлях, пройдений від початкової точки відліку (0,0)(0, 0) до поточної точки. Коли ви думаєте про положення таким чином воно стає вектором так само як і швидкість, як це зображено на малюнку 1.4.

Малюнок 1.4: Вікно комп’ютерної графіки із позначенням точки відліку (0, 0) у верхньому лівому куті й зображеним вектором положення та вектором швидкості
Малюнок 1.4: Вікно комп’ютерної графіки із позначенням точки відліку (0,0)(0, 0) у верхньому лівому куті й зображеним вектором положення та вектором швидкості

На малюнку 1.4 вектори зображені на полотні комп’ютерної графіки. На відміну від малюнку 1.2, початкова точка (0,0)(0, 0) знаходиться не в центрі, а у верхньому лівому куті. І замість півночі, півдня, сходу й заходу тут є позитивні та негативні напрямки вздовж xx- та yy- осей (при цьому yy вказує вниз у позитивному напрямку).

Давайте розглянемо основні дані для положення і швидкості. У прикладі з кулькою я спочатку мав наступні змінні:

ВластивістьНазви змінних
Положенняx, y
Прискоренняxspeed, yspeed

Тепер я розглядатиму положення та швидкість як вектори, кожен з яких представлений об’єктом із атрибутами x і y. Якби мені самому потрібно було написати клас Vector, то я почав би із чогось подібного:

class Vector {

  constructor(x, y) {

    this.x = x;

    this.y = y;

  }

}

Зверніть увагу, що цей клас спроєктовано для зберігання тих самих даних, що й раніше — двох чисел на кожен вектор, одне значення для x і друге для y. За своєю суттю об’єкт Vector — це просто зручний спосіб зберігати два значення (або три, як ви побачите у 3D-прикладах) під одною назвою.

Оскільки p5.js уже має вбудований клас p5.Vector, мені не потрібно писати його самому. Тому ось цей код:

let x = 100;

let y = 100;

let xspeed = 1;

let yspeed = 3.3;

перетворюється на цей:

let position = createVector(100, 100);

let velocity = createVector(1, 3.3);

Зауважте, що об’єкти векторів для position і velocity не створюються, як ви могли очікувати, викликом функції-конструктора. Замість того, щоб написати new p5.Vector(x, y), я викликав функцію createVector(x, y). Функція createVector() включена у p5.js як допоміжна функція, щоб подбати про деталі створення вектора за сценою. За винятком особливих обставин, ви завжди повинні створювати об’єкти p5.Vector за допомогою createVector(). Відзначу, що функції p5.js, такі як createVector(), не можуть викликатися поза контексту функцій setup() чи draw(), оскільки бібліотека p5.js до того моменту може ще не завантажитись. Я покажу, як це зробити у прикладі 1.2.

Тепер, коли у мене є два векторних об’єкти (position і velocity), я готовий реалізувати алгоритм руху на основі векторів: position = position + velocity. У прикладі 1.1, без векторів, код виглядає так:

x = x + xspeed;
y = y + yspeed;

Додавання кожної швидкості до кожної позиції.

В ідеальному світі я міг би переписати код як показано нижче:

position = position + velocity;

Додавання вектора швидкості до вектора положення.

Проте у JavaScript оператор додавання + зарезервований для примітивних даних (чисел, рядків тощо). JavaScript не знає як додати два об’єкти типу p5.Vector, так само як він не знає, як додати два об’єкти типу p5.Font чи p5.Image. На щастя, клас p5.Vector містить методи для типових математичних операцій.

Додавання векторів

Перш ніж я продовжу працювати з класом p5.Vector і його методом add(), розглянемо додавання векторів за допомогою нотації, яка зустрічається у підручниках з математики та фізики. Вектори зазвичай записуються або жирним шрифтом або зі стрілкою зверху. У цій книзі, щоб відрізнити вектор (з величиною та напрямком) від скаляра (окремого значення, такого як ціле число чи число з рухомою крапкою), я використовуватиму нотацію зі стрілкою:

  • Вектор: v\vec{v}
  • Скаляр: x{x}

Припустимо, у мене є два вектори, показані на малюнку 1.5.

Малюнок 1.5: Два вектори
\vec{u} і \vec{v} зображені у вигляді трикутників
Малюнок 1.5: Два вектори u\vec{u} і v\vec{v} зображені у вигляді трикутників

Кожен вектор має два компоненти: xx і yy. Щоб додати два вектори разом, додайте між собою обидва xx-компонента й обидва yy-компонента, щоб створити новий вектор, як показано на малюнку 1.6.

Малюнок 1.6: Додавання векторів шляхом додавання їх x і y компонентів
Малюнок 1.6: Додавання векторів шляхом додавання їх xx і yy компонентів

Іншими словами w=u+v\vec{w} = \vec{u} + \vec{v} можна записати наступним чином:

wx=ux+vxw_x = u_x + v_x
wy=uy+vyw_y = u_y + v_y

Замінивши u\vec{u} і v\vec{v} на їхні значення з малюнку 1.6, ви отримаєте:

wx=5+3=8w_x = 5 + 3 = 8
wy=2+4=6w_y = 2 + 4 = 6

Нарешті запишемо результат у вигляді вектора:

w=(8,6)\vec{w} = (8,6)

Властивості додавання векторів

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

Правило комутативності: u+v=v+u\vec{u} + \vec{v} = \vec{v} + \vec{u}

Правило асоціативності: u+(v+w)=(u+v)+w\vec{u} + (\vec{v} + \vec{w}) = (\vec{u} + \vec{v}) + \vec{w}

Відкинувши вигадливу термінологію з символами, ці правила зводяться до досить простої концепції: результат буде однаковий незалежно від порядку додавання векторів. Замініть вектори звичайними числами (скалярами) і ці правила буде легко побачити:

Комутативність: 3+2=2+33 + 2 = 2 + 3

Асоціативність: (3+2)+1=3+(2+1)(3 + 2) + 1 = 3 + (2 + 1)

Тепер, коли я розглянув теорію додавання двох векторів, я можу повернутися до додавання векторних об’єктів у p5.js. Уявіть ще раз, що я створюю власний клас Vector. Я можу додати йому метод під назвою add(), який прийматиме аргументом інший об’єкт типу Vector:

class Vector {


  constructor(x, y) {

    this.x = x;

    this.y = y;

  }


  add(v) {
    this.x = this.x + v.x;
    this.y = this.y + v.y;
  }

Новинка! Метод для додавання переданого вектора до поточного. Додає між собою компоненти x і y.

}

Метод знаходить компоненти x і y двох векторів та додає їх між собою. Саме так написано метод add() у вбудованому класі p5.Vector. Розібравшись, як працює метод, я тепер можу повернутися до прикладу кульки, що відплигує від стінок, з її алгоритмом position + velocity та застосувати додавання векторів:

position = position + velocity;

Це не спрацює!

position.add(velocity);

Додавання швидкості до положення.

Тепер ви маєте все необхідне, щоб переписати приклад з кулькою за допомогою векторів.

let position;
let velocity;

Замість купи змінних у нас тепер лише дві.


function setup() {

  createCanvas(640, 240);

  position = createVector(100, 100);
  velocity = createVector(2.5, 2);

Зауважте, що createVector() викликано всередині setup().

}


function draw() {

  background(255);

  position.add(velocity);


  if (position.x > width || position.x < 0) {
    velocity.x = velocity.x * -1;
  }
  if (position.y > height || position.y < 0) {
    velocity.y = velocity.y * -1;
  }

Вам все ще іноді потрібно посилатися на окремі компоненти p5.Vector, що можна зробити через синтаксис з крапкою: position.x, velocity.y тощо.


  stroke(0);

  fill(127);

  circle(position.x, position.y, 48);

}

На цьому етапі ви можете відчути певне розчарування. Адже, може здатися, що всі ці зміни зробили код складнішим за оригінальну версію. Хоча це цілком обґрунтована та слушна критика, важливо розуміти, що потужність програмування за допомогою векторів ще не була повністю реалізована. Розгляд відбивання кульки та імплементація лише додавання векторів — це тільки перший крок. У міру просування вперед до складнішого світу з багатьма об’єктами й багатьма силами (які я представлю у Розділі 2), що діють на ці об’єкти, переваги векторів стануть більш очевидними.

Однак, мені варто відзначити важливий аспект переходу до програмування з векторами. Навіть якщо я використовую об'єкти p5.Vector для інкапсуляції двох значень — x і y положення кульки або x та y її швидкості — під спільною назвою змінної, мені все ще часто потрібно буде звертатися до x і y компонентів кожного вектора окремо.

Функція circle() не дозволяє використовувати об’єкт p5.Vector у якості аргументу напряму. Круг можна намалювати лише з двома скалярними значеннями: xx-координатою та yy-координатою. Тому потрібно занурюватися в об’єкт p5.Vector і витягувати з нього x та y компоненти за допомогою синтаксису з крапкою:

circle(position, 48);

circle(position.x, position.y, 48);

Така ж проблема виникає і при перевірці чи досягла кулька краю свого полотна. У цьому випадку мені потрібно отримати доступ до окремих компонентів обох векторів — position і velocity:

if ((position.x > width) || (position.x < 0)) {

  velocity.x = velocity.x * -1;

}

Може бути не завжди очевидно, коли звертатися до властивостей об’єкта безпосередньо, а коли посилатися на об’єкт як одне ціле чи використовувати один із його методів. Мета цього розділу і більшої частини цієї книги — допомогти розрізняти ці сценарії, надавши вам різноманітні приклади та варіанти використання.

Вправа 1.1

Візьміть один із прикладів блукача з Розділу 0 і оновіть його з використанням векторів.

Вправа 1.2

Знайдіть іншу програму, яку ви раніше робили за допомоги p5.js, використовуючи окремі змінні x і y, та використайте замість них вектори.

Вправа 1.3

Розширте приклад 1.2 у 3D. Чи зможете ви налаштувати сферу, щоб вона відбивалася від стінок коробки?

Більше векторної математики

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

МетодЗавдання
add()
Додає вектор до поточного вектора
sub()
Віднімає вектор від поточного вектора
mult()
Масштабує поточний вектор за допомогою множення
div()
Масштабує поточний вектор за допомогою ділення
mag()
Повертає магнітуду поточного вектора
setMag()
Встановлює величину поточного вектора
normalize()
Нормалізує поточний вектор до одиничної довжини у тому ж напрямку
limit()
Обмежує магнітуду поточного вектора
heading()
Обчислює напрямок поточного вектора — кут його повороту
rotate()
Повертає поточний вектор на заданий кут
lerp()
Виконує лінійну інтерполяцію до іншого вектора
dist()
Обчислює евклідову відстань між двома векторами (розглядаються як точки)
angleBetween()
Знаходить кут між двома векторами
dot()
Повертає скалярний добуток двох векторів
cross()
Повертає перехресний добуток двох векторів (доречно лише в трьох вимірах)
random2D()
Повертає випадковий 2D вектор
random3D()
Повертає випадковий 3D вектор

Зараз я розповім про кілька основних методів. А в наступних розділах, де приклади ставатимуть складнішими, я продовжу розкривати більше деталей.

Віднімання векторів

Малюнок 1.7: Відношення між \vec{v} і -\vec{v}
Малюнок 1.7: Відношення між v\vec{v} і v-\vec{v}

Завершивши з додаванням я перейду до віднімання. Це не так вже й складно, просто візьміть знак плюс і замініть його на мінус! Однак перед тим, як братися за віднімання, розглянемо, що це означає, коли вектор v\vec{v} стає від’ємним, тобто v-\vec{v}. Від’ємна версія скаляра 3 це -3. Негативний вектор має схожу логіку де полярність кожного його компонента інвертується. Отже, якщо v\vec{v} має компоненти (x,y)(x, y), тоді v-\vec{v} це (x,y)(-x, -y). Візуально це призводить до стрілки такої ж довжини, що й у початкового вектора, але з протилежним напрямком у протилежну сторону, як показано на малюнку 1.7.

Отже, віднімання — це те ж саме, що і додавання, але другий вектор у рівнянні розглядається як його від’ємна версія:

uv=u+v\vec{u} - \vec{v} = \vec{u} + -\vec{v}

Вектори віднімаються подібно до того, як вони додаються, розміщуючи їх “кінчики до хвоста”, тобто вирівнюючи вістря (або кінцеву точку) одного вектора з хвостом (або початковою точкою) наступного. Це змінює початковий напрямок з урахуванням другого вектора, розміщуючи його в кінці першого, як на малюнку 1.8.

Малюнок 1.8: Векторне віднімання розміщує один вектор на кінці іншого, але спрямованого в протилежному напрямку.
Малюнок 1.8: Векторне віднімання розміщує один вектор на кінці іншого, але спрямованого в протилежному напрямку.

Щоб фактично виконати віднімання, обрахуйте різницю між компонентами векторів. Тобто, рівняння w=uv\vec{w} = \vec{u} - \vec{v} можна записати так:

wx=uxvxw_x = u_x - v_x
wy=uyvyw_y = u_y - v_y

Всередині p5.Vector код буде таким:

sub(v) {

  this.x = this.x - v.x;

  this.y = this.y - v.y;

}

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

function draw() {

  background(255);

  let mouse = createVector(mouseX, mouseY);
  let center = createVector(width / 2, height / 2);

Два вектори: один для положення курсору, а другий для положення центру вікна.

  stroke(200);
  strokeWeight(4);
  line(0, 0, mouse.x, mouse.y);
  line(0, 0, center.x, center.y);

Малювання двох векторів сірими лініями.

  mouse.sub(center);

Віднімання вектора!

  stroke(0);
  translate(width / 2, height / 2);
  line(0, 0, mouse.x, mouse.y);

Малювання лінії, що представляє результат віднімання. Зверніть увагу, що для розміщення вектора, я перемістив тут початок координат за допомогою функції translate().

}

Зверніть увагу на використання функції translate() для візуалізації результівного вектора як лінії від центру (width / 2, height / 2) до курсора. Віднімання вектора — це свого роду трансляція, яка переміщує “початкове” положення вектора. Тут, віднімаючи вектор центру від вектора курсора, я фактично переміщую початкову точку результівного вектора у центр полотна. Тому мені також потрібно перемістити початкові координати полотна за допомогою translate(). Без цього лінія буде проведена від верхнього лівого кута, і візуальний зв’язок буде не таким очевидним.

Множення і ділення векторів

Малюнок 1.9: Масштабування вектора шляхом множення
Малюнок 1.9: Масштабування вектора шляхом множення

Стосовно множення вектора ви повинні думати трохи інакше. Множення вектора зазвичай належить до процесу масштабування вектора. Якщо я хочу масштабувати вектор до удвічі більшого розміру або до однієї третини його розміру, залишивши незмінним напрямок, я б сказав: “Помножте вектор на 2” або “Помножте вектор на 1/3”. На відміну від додавання і віднімання, я множу вектор на скаляр (одне число), а не на інший вектор. На малюнку 1.9 показано, як масштабувати вектор у 3 рази.

Щоб масштабувати вектор, помножте кожен компонент (xx і yy) на переданий скаляр. Тобто цей вираз w=u×n\vec{w} = \vec{u} \times n може бути записано так:

wx=ux×nw_x = u_x \times n
wy=uy×nw_y = u_y \times n

Як приклад, скажімо що u=(3,7)\vec{u} = (-3, 7), а n=3n = 3. Ви можете розрахувати це рівняння w=u×n\vec{w} = \vec{u} \times n наступним чином:

wx=3×3w_x = -3 \times 3
wy=7×3w_y = 7 \times 3
w=(9,21)\vec{w} = (-9, 21)

Саме так у класі p5.Vector працює метод mult():

mult(n) {

  this.x = this.x * n;
  this.y = this.y * n;

Компоненти вектора множаться на передане число.

}

Реалізація множення у коді така ж проста:

let u = createVector(-3, 7);

u.mult(3);

Цей p5.Vector тепер утричі більший і дорівнює (-9, 21), див. малюнок 1.9.

Приклад 1.4 ілюструє векторне множення шляхом малювання лінії між центром полотна та курсором, як у попередньому прикладі, а потім масштабує цю лінію на 0.5.

function draw() {

  background(255);


  let mouse = createVector(mouseX, mouseY);

  let center = createVector(width / 2, height / 2);

  mouse.sub(center);


  translate(width / 2, height / 2);

  strokeWeight(2);

  stroke(200);

  line(0, 0, mouse.x, mouse.y);

  mouse.mult(0.5);

Множення вектора! Тепер вектор зменшився вдвічі від початкового розміру (помножений на 0.5).

  stroke(0);

  strokeWeight(4);

  line(0, 0, mouse.x, mouse.y);

}

Отриманий вектор становить половину початкового розміру. Замість того, щоб множити вектор на 0.5, я міг би досягти того самого ефекту, поділивши його на 2, як на малюнку 1.10.

Малюнок 1.10: Масштабування вектора через ділення
Малюнок 1.10: Масштабування вектора через ділення

Отже, ділення вектора працює прямо як множення — просто замініть знак множення (*) на знак ділення (/). Ось як клас p5.Vector реалізує метод div():

div(n) {

  this.x = this.x / n;

  this.y = this.y / n;

}

А ось як використовувати метод div() у коді:

let u = createVector(8, -4);

u.div(2);

Ділення вектора! Тепер вектор має половину початкового розміру (поділений на 2).

Це бере вектор u та ділить його на 2.

Додаткові особливості операцій із векторами

Як і у випадку додавання, основні алгебраїчні правила множення застосовуються і до векторів.

Правило асоціативності: (n×m)×v=n×(m×v)(n \times m) \times \vec{v} = n \times (m \times \vec{v})

Розподільний закон з двома скалярами і одним вектором: (n+m)×v=(n×v)+(m×v)(n + m) \times \vec{v} = (n \times \vec{v}) + (m \times \vec{v})

Розподільний закон з двома векторами і одним скаляром: (u+v)×n=(u×n)+(v×n)(\vec{u} + \vec{v}) \times n = (\vec{u} \times n) + (\vec{v} \times n)

Магнітуда вектора

Малюнок 1.11: Довжина або магнітуда вектора \vec{v} часто записується як: \lVert\vec{v}\rVert
Малюнок 1.11: Довжина або магнітуда (модуль, величина) вектора v\vec{v} часто записується як: v\lVert\vec{v}\rVert

Множення і ділення, як щойно описано, змінюють довжину вектора, не впливаючи на його напрямок. Можливо, ви задаєте собі питання: “Добре, а як мені дізнатися, яка довжина вектора? Мені відомі компоненти вектора (xx і yy), але якої довжини (у пікселях) ця стрілка?” Розуміння того, як обчислити довжину вектора, також відомої як магнітуда, є надзвичайно корисним і важливим.

Малюнок 1.12: Теорема Піфагора обчислює довжину вектора, використовуючи його компоненти
Малюнок 1.12: Теорема Піфагора обчислює довжину вектора, використовуючи його компоненти

Зверніть увагу на малюнок 1.11, як вектор, зображений у вигляді стрілки та двох компонентів (xx і yy), створює прямокутний трикутник. Сторони — це компоненти, а гіпотенуза — сама стрілка. Нам пощастило отримати цей прямокутний трикутник, оскільки колись дуже давно грецький математик, на ім’я Піфагор, відкрив чудову формулу, яка описує відношення між сторонами й гіпотенузою прямокутного трикутника. Ця формула — теорема Піфагора — записується як a2+b2=c2a^2 + b^2 = c^2 (див. малюнок 1.12).

Озброївшись цією формулою, ми тепер можемо обчислити магнітуду v\vec{v} наступним чином:

v=vxvx+vyvy||\vec{v}||=\sqrt{v_x * v_x + v_y * v_y}

У класі p5.Vector метод mag() визначається за тією ж формулою:

mag() {

  return sqrt(this.x * this.x + this.y * this.y);

}

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

function setup() {

  createCanvas(640, 240);

}


function draw() {

  background(255);


  let mouse = createVector(mouseX, mouseY);

  let center = createVector(width / 2, height / 2);

  mouse.sub(center);

  let m = mouse.mag();
  fill(0);
  rect(0, 0, m, 10);

Магнітуду (тобто довжину) вектора можна отримати за допомогою методу mag(). Тут вона використовується для ширини прямокутника, намальованого у верхній частині полотна.

  translate(width / 2, height / 2);

  line(0, 0, mouse.x, mouse.y);

}

Зверніть увагу, що магнітуда вектора завжди додатна, навіть якщо компоненти вектора від’ємні.

Нормалізація векторів

Малюнок 1.13: Коли вектор нормалізований, він вказує в тому самому напрямку, але його розмір змінюється до одиничної довжини — 1
Малюнок 1.13: Коли вектор нормалізований, він вказує в тому самому напрямку, але його розмір змінюється до одиничної довжини — 1

Обчислення магнітуди вектора — це лише початок. Це відкриває двері до багатьох можливостей, першою з яких є нормалізація (малюнок 1.13). Це процес створення чогось стандартного або нормального. У випадку з векторами заведено вважати, що стандартний вектор має довжину 1. Таким чином, щоб нормалізувати вектор, потрібно взяти вектор будь-якої довжини та змінити його довжину на 1, не змінюючи його напрямку. Такий нормалізований вектор називається одиничним вектором.

Одиничний вектор описує напрямок вектора без урахування його довжини. Ви побачите, що це стане особливо корисним, коли я почну працювати з силами у Розділі 2.

Для будь-якого заданого вектора u\vec{u}, його одиничний вектор (записаний як u^\hat{u}) обчислюється наступним чином:

u^=uu\hat{u} = \frac{\vec{u}}{||\vec{u}||}
Малюнок 1.14: Для нормалізації вектора його компоненти діляться на магнітуду вектора
Малюнок 1.14: Для нормалізації вектора його компоненти діляться на магнітуду вектора

Іншими словами, щоб нормалізувати вектор, розділіть кожен його компонент на магнітуду вектора. Щоб зрозуміти, чому це працює, розглянемо вектор (4,3)(4, 3), який має довжину 5 (див. малюнок 1.14). Після нормалізації вектор матиме величину 1. Розглядаючи вектор як прямокутний трикутник, нормалізація скорочує гіпотенузу шляхом ділення на 5 (оскільки 5/5 = 1). У цьому процесі кожна сторона також зменшується у 5 разів. Довжина сторін змінюється від 4 і 3 до 4/5 та 3/5.

У класі p5.Vector метод нормалізації зроблено наступним чином:

normalize() {

  let m = this.mag();

  this.div(m);

}

Звісно, є одна маленька проблема. Що, якщо довжина вектора дорівнює 0? Ділити на 0 не можна! Проста перевірка допоможе швидко виправити це:

normalize() {

  let m = this.mag();

  if (m > 0) {

    this.div(m);

  }

}

Цей приклад використовує нормалізацію, щоб надати вектору між курсором і центром полотна фіксовану довжину, незалежно від фактичної магнітуди початкового вектора.

function draw() {

  background(255);


  let mouse = createVector(mouseX, mouseY);

  let center = createVector(width / 2, height / 2);

  mouse.sub(center);


  translate(width / 2, height / 2);

  stroke(200);

  line(0, 0, mouse.x, mouse.y);

  mouse.normalize();
  mouse.mult(50);

У цьому прикладі після нормалізації вектора він множиться на 50. Зверніть увагу, що незалежно від того, де знаходиться курсор, вектор завжди має однакову довжину (50) саме завдяки нормалізації.

  stroke(0);

  strokeWeight(8);

  line(0, 0, mouse.x, mouse.y);

}

Зауважте, що я помножив вектор mouse на 50 після його нормалізації до 1. Нормалізація часто є першим кроком у створенні вектора певної довжини, навіть якщо бажана довжина відрізняється від 1. Далі в розділі ви побачите більше подібного використання.

Вся ця векторна математика звучить як щось, про що ви повинні знати, але навіщо? Як це реально допоможе вам писати код? Терпіння. Потрібен деякий час, перш ніж вам повністю відкриється краса використання p5.Vector. Це досить поширене явище під час вивчення нової структури даних. Наприклад, коли ви вперше дізналися про масив, могло здатися, що його використання потребує більше роботи, ніж заведення кількох змінних, щоб позначити декілька речей. Однак ця стратегія швидко руйнується, коли вам потрібно 100, 1 000 або 10 000 речей.

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

Рух з векторами

Що означає програмувати рух, використовуючи вектори? Ви вже спробували це у прикладі 1.2 з кулькою, що відплигує від стінок. Кулька на екрані має своє положення (місце де вона знаходиться у будь-який момент) і швидкість (інструкція про те, як вона має рухатися від одного моменту до наступного). Швидкість додається до положення:

position.add(velocity);

Потім об’єкт малюється у новому положенні:

circle(position.x, position.y, 48);

Разом ці кроки складають базовий алгоритм руху:

  1. Додати швидкість до положення.
  2. Намалювати об’єкт в оновленому положенні.

У прикладі з кулькою, весь цей код відбувався у функціях setup() і draw(). Тепер я хочу інкапсулювати всю логіку руху об’єкта і перенести її всередину класу. Таким чином я можу створити основу для програмування рухомих об’єктів, які я зможу легко використовувати знову і знову. (Для короткого огляду основ ООП перегляньте “Клас випадкового блукача”.)

Спочатку я створю загальний клас Mover, який описуватиме фігуру, що рухається по полотну. Для цього я маю розглянути наступні два запитання:

  1. Які дані потрібні для цього класу?
  2. Який потрібен функціонал?

Базовий алгоритм руху відповідає на обидва ці питання. По-перше, об’єкт Mover має дві частини даних, position і velocity, які є об’єктами типу p5.Vector. Вони ініціалізуються у конструкторі об’єкта. У цей раз я вирішив ініціалізувати об’єкт Mover, надавши йому випадкові значення для положення і швидкості. Зверніть увагу на використання ключового слова this з усіма змінними, які є частиною об’єкта Mover:

class Mover {

  constructor() {

    this.position = createVector(random(width), random(height));

    this.velocity = createVector(random(-2,2), random(-2, 2));

  }

Функціональність залежить від потреб. Об’єкту Mover потрібно рухатися, застосовуючи його швидкість до його положення, і він має бути видимим. Я реалізую ці потреби як методи з назвами update() і show(). Весь код логіки руху я розміщу у методі update(), а малюванням об’єкту буде займатися метод show():

  update() {

    this.position.add(this.velocity);

Переміщення об’єкта.

  }


  show() {

    stroke(0);

    fill(175);

    circle(this.position.x, this.position.y, 48);

Об’єкт малюється у вигляді кульки.

  }

Класу Mover також потрібен метод, який визначатиме, що робити об’єкту, коли він досягає краю полотна. Наразі я зроблю щось просте і дозволю йому переходити між краями полотна:

  checkEdges() {

    if (this.position.x > width) {
      this.position.x = 0;
    } else if (this.position.x < 0) {
      this.position.x = width;
    }

    if (this.position.y > height) {
      this.position.y = 0;
    } else if (this.position.y < 0) {
      this.position.y = height;
    }

Коли положення досягне одного з країв, установіть його на протилежний бік.

  }

}

Тепер клас Mover готовий, але сам по собі він не об’єкт, а лише шаблон для створення екземпляра об’єкта. Щоб насправді створити об’єкт Mover, мені спершу потрібно оголосити змінну для його зберігання:

let mover;

Потім у функції setup() я створюю об’єкт, викликаючи назву класу разом із ключовим словом new. Це спричинить запуск конструктора класу для створення екземпляра об’єкта:

mover = new Mover();

Тепер залишається тільки викликати відповідні методи у функції draw():

mover.update();

mover.checkEdges();

mover.show();

Нижче показано весь приклад.

let mover;

Оголошення змінної для об'єкту Mover.


function setup() {

  createCanvas(640, 240);

  mover = new Mover();

Створення об’єкту Mover.

}


function draw() {

  background(255);

  mover.update();
  mover.checkEdges();
  mover.show();

Виклик методів об’єкту Mover.

}


class Mover {

  constructor() {

    this.position = createVector(random(width), random(height));
    this.velocity = createVector(random(-2, 2), random(-2, 2));

Об’єкт має два вектори: положення і швидкість.

  }


  update() {

    this.position.add(this.velocity);

Базовий рух: положення змінюється за рахунок швидкості.

  }


  show() {

    stroke(0);

    strokeWeight(2);

    fill(127);

    circle(this.position.x, this.position.y, 48);

  }


  checkEdges() {

    if (this.position.x > width) {

      this.position.x = 0;

    } else if (this.position.x < 0) {

      this.position.x = width;

    }


    if (this.position.y > height) {

      this.position.y = 0;

    } else if (this.position.y < 0) {

      this.position.y = height;

    }

  }

}

Якщо ООП для вас ще нове, один аспект тут може здатися трохи дивним. На початку цього розділу я обговорював клас p5.Vector, що він є шаблоном для створення об’єктів position і velocity. Так чому ці об’єкти знаходяться всередині ще одного об’єкта, об’єкта Mover?

Насправді це зовсім нормальна справа. Об’єкт — це контейнер, що містить дані та функціональні можливості. Ці дані можуть бути числами або іншими об’єктами (тими ж масивами)! Ви будете постійно бачити це у книзі. У Розділі 4, наприклад, я напишу клас для опису системи частинок. Цей об’єкт ParticleSystem міститиме список об’єктів Particle... і кожен об’єкт Particle матиме свої власні дані з кількома об’єктами типу p5.Vector!

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

Сподіваюсь, що на цьому етапі, ви вже відчуваєте себе комфортно відносно двох понять: (1) розумієте, що таке вектор і (2) як використовувати вектори всередині об’єкта, щоб відстежувати його положення та рух. Це чудовий перший крок, який заслуговує тихих оплесків. Але перш ніж зірвати овації, потрібно зробити ще один, дещо більший крок вперед. Зрештою спостерігати базовий приклад руху досить нудно. Кулька ніколи не прискорюється, не сповільнюється і не змінює напрямку. Для більш витонченого та складного руху, який проявляється в навколишньому світі, до класу потрібно додати ще один вектор: acceleration.

Прискорення

Прискорення — це темп зміни швидкості. Подумайте про це визначення деяку мить. Це нова концепція? Не зовсім. Раніше я визначив швидкість як темп зміни положення, тому, по суті, я розвиваю ефект “просочування”. Прискорення впливає на швидкість, яка своєю чергою впливає на положення. У вигляді короткого передвістя скажу, що цей момент стане більш важливим у наступному розділі, коли я розгляну, як сили, такі як тертя, впливають на прискорення, яке впливає на швидкість, яка впливає на положення. У коді цей ефект просочування виглядає так:

velocity.add(acceleration);

position.add(velocity);

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

Наступний крок — придумати спосіб обчислення прискорення. Ось кілька можливих алгоритмів:

  • Постійне прискорення
  • Випадкове прискорення
  • Прискорення у напрямку курсора

У решті даного розділу я покажу вам, як реалізувати ці алгоритми.

Алгоритм 1: Постійне прискорення

Алгоритм постійного прискорення не особливо цікавий, але він найпростіший, а отже, стане чудовою відправною точкою для впровадження прискорення у код. Першим кроком потрібно додати до класу Mover ще одну змінну:

class Mover {

  constructor() {

    this.position = createVector(width / 2, height / 2);
    this.velocity = createVector(0, 0);

Ініціалізація нерухомого положення у центрі полотна.

    this.acceleration = createVector(0, 0);

Новий вектор для прискорення.

  }

Далі додамо прискорення до методу update():

  update() {

    this.velocity.add(this.acceleration);
    this.position.add(this.velocity);

Алгоритм руху тепер складається з двох рядків коду!

  }

Я майже закінчив. Єдине, що залишається, так це змусити об’єкт рухатися! У конструкторі початкова швидкість встановлена на 0, а не на випадковий вектор, як це робилося раніше. Тому, коли починається програма, об’єкт перебуває у спокої. Щоб змусити його рухатися, замість безпосередньої зміни швидкості, я оновлю швидкість через прискорення об’єкта. Відповідно до першого алгоритму, прискорення має бути постійним, тому зараз я оберу для нього певне значення:

    this.acceleration = createVector(-0.001, 0.01);

Це означає, що з кожним кадром анімації швидкість об'єкта має збільшуватися на -0.001 пікселя у горизонтальному напрямку і на 0.01 пікселя — у вертикальному. Можливо, ви думаєте: “Боже, ці значення здаються жахливо малими!” Дійсно, вони досить маленькі, але так і заплановано. Значення прискорення з часом накопичуються у швидкості, приблизно 60 разів на секунду, залежно від частоти кадрів програми. Щоб не допустити занадто швидкого зростання магнітуди вектора швидкості і її виходу з-під контролю, значення прискорення повинні залишатися досить малими.

Я також можу допомогти зберегти швидкість у прийнятному діапазоні, використовуючи метод класу p5.Vector під назвою limit(), який обмежує магнітуду вектора:

    this.velocity.limit(10);

Метод limit() обмежує магнітуду вектора.

Це перекладається наступним чином:

Яка магнітуда у velocity? Якщо вона менша за 10, то немає проблем, просто залишу її як є. Але якщо вона більша за 10, то зменшу її до 10!

Вправа 1.4

Напишіть реалізацію методу limit() для класу p5.Vector:

  limit(max) {

    if (this.mag() > max) {

      this.normalize();

      this.mult(max);

    }

  }

Подивимося на зміни в класі Mover з використанням acceleration і limit().

class Mover {

  constructor() {

    this.position = createVector(width / 2, height / 2);

    this.velocity = createVector(0, 0);

    this.acceleration = createVector(-0.001, 0.01);

Прискорення є основою!

    this.topSpeed = 10;

Змінна topSpeed обмежуватиме магнітуду швидкості.

  }


  update() {

    this.velocity.add(this.acceleration);
    this.velocity.limit(this.topSpeed);

Швидкість змінюється через прискорення та обмежується значенням topSpeed.

    this.position.add(this.velocity);

  }


  show() {}

Метод show() такий як і раніше.


  checkEdges() {}

Метод checkEdges() такий як і раніше.

}

У кінцевому результаті об’єкт падає вниз і вліво, поступово прискорюючись все більше, поки не досягне максимальної швидкості.

Вправа 1.5

Створіть симуляцію об’єкта (наприклад, автомобілю), який прискорюється, коли ви натискаєте на клавіатурі стрілочку вгору і гальмує, коли ви натискаєте стрілочку вниз.

Алгоритм 2: Випадкове прискорення

Тепер перейдемо до алгоритму випадкового прискорення. У цьому випадку замість ініціалізації властивості acceleration у конструкторі об’єкта, я хочу встановити йому випадкове значення в методі update(). Таким чином об’єкт отримуватиме різний вектор прискорення для кожного кадру анімації:

update() {

  this.acceleration = p5.Vector.random2D();
  this.velocity.add(this.acceleration);
  this.velocity.limit(this.topSpeed);
  this.position.add(this.velocity);

Метод random2D() повертає одиничний вектор, що вказує у випадковому напрямку.

}

Метод random2D() створює нормалізований вектор, який має випадковий напрямок, а його магнітуда дорівнює 1. Щоб зробити речі цікавішими, я можу спробувати масштабувати випадковий вектор константним значенням:

  this.acceleration = p5.Vector.random2D();

  this.acceleration.mult(0.5);

Константне масштабування.

Для ще більшої різноманітності, я можу збільшити прискорення випадковим значенням. У прикладі 1.9 вектор acceleration має як випадковий напрямок, так і випадкову величину від 0 до 2.

  this.acceleration = p5.Vector.random2D();

  this.acceleration.mult(random(2));

Випадкове масштабування.

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

Ви також можете помітити, що цей приклад є ще одним видом випадкового блукання. Ключова відмінність між тим, що я роблю тут і прикладами з попереднього розділу полягає в тому, що саме рандомізовано. У традиційному випадковому блуканні я безпосередньо маніпулював швидкістю, тобто кожен крок був повністю незалежним від попереднього. У прикладі 1.9 рандомізується вже прискорення (швидкість зміни швидкості), а не сама швидкість. Це робить рух об’єкта залежним від його попереднього стану: швидкість змінюється поступово, відповідно до випадкового прискорення. Отриманий рух об’єкта має певну послідовність і плавність, яких бракувало оригінальному випадковому блукачу. Зміна може здаватися незначною, але вона фундаментально змінює спосіб, яким об'єкт рухається по полотну.

Вправа 1.6

Звернувшись до прикладу 0.6, реалізуйте прискорення, обчислене за допомогою шуму Перліна.

Статичні та нестатичні методи

Можливо, в попередньому прикладі ви помітили щось трохи дивне і незвичайне. Метод random2D(), використаний для створення випадкового одиничного вектора, викликався з під імені класу, як p5.Vector.random2D(), а не на поточному екземплярі класу, як this.random2D(). Це тому, що random2D() це статичний метод, що означає, що він пов'язаний з класом в цілому, а не з окремими об’єктами, тобто екземплярами цього класу.

Статичні методи рідко потрібні, коли ви пишете власні класи (наприклад Walker чи Mover), тому ви, можливо, раніше не стикалися з ними. Однак іноді вони складають важливу частину попередньо написаних класів, таких як p5.Vector. Алгоритм з прискоренням до курсора вимагає подальшого використання цього концепту, тому зробімо крок назад і розглянемо різницю між статичними й нестатичними методами.

Відкладіть на секунду вектори та погляньте на наступний код:

let x = 0;

let y = 5;

x = x + y;

Це, напевно, звично для вас, чи не так? Я присвоюю для x значення 0, додаю до нього y, і тепер x дорівнює 5. Я міг би написати подібний код для додавання двох векторів:

let v = createVector(0, 0);

let u = createVector(4, 5);

v.add(u);

Вектор v має значення (0,0)(0, 0), я додаю до нього вектор u, і тепер v дорівнює (4,5)(4, 5). Має сенс, правда?

Тепер розгляньте цей приклад:

let x = 0;

let y = 5;

let z = x + y;

Я надаю змінній x значення 0, потім додаю до нього y і зберігаю результат у новій змінній z. Значення x тут не змінюється (y також)! Це може здатися тривіальним, інтуїтивно зрозумілим моментом, коли йдеться про математичні операції з простими числами. Однак це не так очевидно для математичних операцій з використанням об’єктів p5.Vector. Спробуємо переписати приклад із векторами, базуючись на тому, що я вже розповів про клас p5.Vector:

let v = createVector(0, 0);

let u = createVector(4, 5);

let w = v.add(u);

Не обманіться — це неправильно!

Це може здатися хорошим припущенням, але клас p5.Vector так не працює. Якщо ви подивитеся на визначення методу add(), то зрозумієте чому:

add(v) {

  this.x = this.x + v.x;

  this.y = this.y + v.y;

}

Цей код має дві проблеми. По-перше, метод add() не повертає новий об’єкт типу p5.Vector, а по-друге, змінює значення того вектора, на якому викликається метод add(). Щоб додати два векторні об’єкти разом і повернути результат у новому векторі, мені потрібно використати статичну версію методу add(), викликаючи її через саме ім’я класу, а не через нестатичну версію на конкретному екземплярі об’єкта.

Ось як я міг би написати статичну версію методу add():

static add(v1, v2) {
  let v3 = createVector(v1.x + v2.x, v1.y + v2.y);
  return v3;
}

Статична версія методу додає два вектори разом і призначає результат новому вектору, залишаючи початкові вектори (v і u в попередніх блоках коду) незмінними.

Ключова відмінність полягає в тому, що метод повертає новий вектор (v3), створений за допомогою суми компонентів v1 і v2. У результаті метод не вносить зміни в жодний з оригінальних векторів.

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

let v = createVector(0, 0);

let u = createVector(4, 5);

let w = v.add(u);

Цей рядок замінюємо на наступний.

let w = p5.Vector.add(v, u);

Клас p5.Vector має статичні версії методів add(), sub(), mult() і div(). Ці статичні методи дозволяють виконувати загальні математичні операції з векторами, не змінюючи у процесі значення жоден із вхідних векторів.

Вправа 1.7

Перекладіть наступний псевдокод у робочий код, використовуючи відповідні статичні або нестатичні методи:

  • Вектор v дорівнює (1,5)(1, 5).
  • Вектор u дорівнює v помноженому на 2.
  • Вектор w дорівнює v мінус u.
  • Вектор w зменшено у 3 рази.
let v = createVector(1, 5);

let u = p5.Vector.mult(v, 2);

let w = p5.Vector.sub(v, u);

w.div(3);

Алгоритм 3: Інтерактивний рух

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

Малюнок 1.15: Вектор від об’єкта до положення курсору
Малюнок 1.15: Вектор від об’єкта до положення курсору

Щоразу, коли ви хочете обчислити вектор на основі правила або формули, вам потрібно обчислити два атрибути: магнітуду та напрямок. Почнемо з напрямку. Я знаю, що вектор прискорення має вказувати від положення об’єкта до положення курсора (малюнок 1.15). Скажімо, об’єкт розташований у векторі з положенням (x,y)(x, y), а курсор у координатах (mouseX,mouseY)(mouseX, mouseY).

Малюнок 1.16: Обчислення початкового вектора прискорення за допомогою різниці векторів курсора та положення об’єкта
Малюнок 1.16: Обчислення початкового вектора прискорення за допомогою різниці векторів курсора та положення об’єкта

На малюнку 1.16 ви бачите, що вектор прискорення (dx,dy)(dx, dy) можна обчислити шляхом віднімання положення об’єкта від положення курсора:

  • dx=mouseXxdx = mouseX - x
  • dy=mouseYydy = mouseY - y

Реалізуємо це за допомогою p5.Vector. Припускаючи, що код буде розміщено всередині класу Mover і матиме доступ до об’єкта position, я можу написати:

let mouse = createVector(mouseX, mouseY);

let direction = p5.Vector.sub(mouse, this.position);

Подивіться! Я використовую статичний метод sub(), оскільки хочу отримати новий p5.Vector!

Я використав статичну версію методу sub(), щоб створити новий вектор direction, який вказує від положення рухомого об’єкта до курсора. Однак, якщо об’єкт дійсно прискорюватиметься за допомогою цього вектора, він миттєво з’явиться у положенні курсора, оскільки магнітуда direction дорівнює відстані між об’єктом і курсором. Звісно, це не призведе до плавної анімації. Тому наступним кроком є визначення того, наскільки швидко об’єкт має прискорюватися до курсора, змінюючи величину вектора.

Щоб встановити магнітуду (якою б вона не була) для вектора прискорення, я повинен спочатку ______ вектор. Правильно — нормалізувати! Якщо я можу зменшити вектор до одиничного вектора (з довжиною 1), то зможу легко масштабувати його до будь-якого іншого значення, оскільки одиниця помножена на будь-що, дорівнює цьому будь-що:

let anything = __________________;

Будь-яке число!

direction.normalize();

direction.mult(anything);

Підсумовуючи, виконайте наступні кроки, щоб змусити об’єкт прискоритися до курсора:

  1. Обчисліть вектор, який вказує від об’єкта до цільового положення курсора.
  2. Нормалізуйте цей вектор, зменшивши його довжину до 1.
  3. Масштабуйте цей вектор до відповідного значення, помноживши його на це значення.
  4. Присвойте отриманий вектор прискоренню.

Мені потрібно зізнатися. Нормалізація, а потім масштабування — це настільки поширені операції з векторами, що клас p5.Vector включає метод, який виконує одразу обидві ці дії, встановлюючи магнітуду вектора на вказане значення. Це метод — setMag():

let anything = ?????

dir.setMag(anything);

У наступному прикладі, щоб підкреслити математику, я збираюся написати код з використанням методів normalize() і mult(), але це, ймовірно, востаннє, коли я це робитиму. У подальших прикладах ви будете бачити вже використання методу setMag().

  update() {

    let mouse = createVector(mouseX, mouseY);

    let dir = p5.Vector.sub(mouse, this.position);

Крок 1: Обчислення напрямку.

    dir.normalize();

Крок 2: Нормалізація.

    dir.mult(0.2);

Крок 3: Масштабування.

    this.acceleration = dir;

Крок 4: Прискорення.

    this.velocity.add(this.acceleration);

    this.velocity.limit(this.topSpeed);

    this.position.add(this.velocity);

  }

Можливо, вам цікаво, чому круг не зупиняється, коли досягає цілі. Важливо відзначити, що об’єкт, який рухається, не знає про потребу зупинитися в пункті призначення, а знає лише про його положення. Об’єкт намагається прискоритися туди з фіксованою швидкістю, незалежно від того, наскільки далеко він знаходиться. Це означає, що він неминуче перескочить ціль і йому доведеться розвернутися, знову прискорюючись до неї, знову перестрибуючи її й так далі. Слідкуйте за оновленнями: у наступних розділах я покажу вам, як запрограмувати об’єкт на досягнення цілі з уповільненням при наближенні.

Вправа 1.8

Приклад 1.10 з притягуванням об’єкта до положення курсора надзвичайно близький до концепції гравітаційного притягання. Однак у цьому прикладі величина притягання є постійною, тоді як у реальної сили тяжіння величина обернено пропорційна відстані: чим ближче об’єкт до точки притягання, тим швидше він прискорюється. Я розповім про гравітаційне тяжіння більш детально в наступному розділі, а поки спробуйте реалізувати власну версію прикладу 1.10 зі змінною величиною прискорення, яка буде тим сильнішою, чим ближче об'єкт наближатиметься до цілі.

Проєкт “Екосистема”

Застосуйте використання векторів для подальшого розвитку та вдосконалення руху елементів у вашій екосистемі. Дізнайтеся, як можна керувати рухом, маніпулюючи лише вектором прискорення об’єкта.

Як ви можете розрахувати прискорення для імітації певних поведінок — безладного польоту бентежної мухи, м’яких стрибків зайчика чи плазування змії? Яку роль відіграє прискорення в природі? Розгляньте, як птах прискорюється при зльоті або як риба під час плавання раптово змінює свій напрямок. Знову ж таки, наскільки характер створіння може бути сформований лише його поведінкою? Що додається (або прибирається) при більшому впровадженні візуальних елементів дизайну замість простих форм?