Розділ 3. Коливання

Тригонометрія — це синус часу.

— Анонім

Гала, автор Бріджит Райлі, 1974. Акрил на полотні, 159.7 на 159.7 см
Гала, автор Бріджит Райлі, 1974. Акрил на полотні, 159.7 на 159.7 см

Бріджит Райлі, відома британська художниця, була рушійною силою руху оп-арту 1960-х років. Її робота містить геометричні візерунки, які кидають виклик сприйняттю глядача та викликають відчуття руху чи вібрації. Її твір “Гала” 1974 року демонструє серію криволінійних форм, які хвилюються по полотну, викликаючи природний ритм синусоїди.


У Розділі 1 і Розділі 2 я ретельно відпрацював об’єктно-орієнтовану структуру для анімації фігури на полотні p5.js, використовуючи концепцію вектора для представлення положення, швидкості та прискорення, викликаних силами середовища. Я міг би відразу перейти до таких тем, як системи частинок, керувальні сили, групова поведінка тощо. Однак це означало б пропустити фундаментальний аспект руху в природному світі: коливання (осциляція) або рух об’єкта вперед і назад навколо центральної точки або положення.

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

Я розпочну розділ з основ роботи із кутами в p5.js, а потім розгляну кілька аспектів тригонометрії. Наприкінець я зв’яжу тригонометрію з тим, що ви дізналися про сили у Розділі 2. Зміст цього розділу прокладе шлях для складніших прикладів цієї книги, які потребують тригонометрії.

Кути

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

Малюнок 3.1: Кути, виміряні у градусах
Малюнок 3.1: Кути, виміряні у градусах
Малюнок 3.2: Квадрат повернутий на 45 градусів
Малюнок 3.2: Квадрат повернутий на 45 градусів

Повний оберт відбувається від 0 до 360 градусів, а 90 градусів (прямий кут) становить одну четверту від 360, що зображено на малюнку 3.1 двома перпендикулярними лініями.

У комп’ютерній графіці кути загалом використовуються для вказівки повороту фігури. Наприклад, квадрат на малюнку 3.2 повернуто на 45 градусів навколо свого центру.

Малюнок 3.3: Довжина дуги для кута в 1 радіан дорівнює радіусу.
Малюнок 3.3: Довжина дуги для кута в 1 радіан дорівнює радіусу.

Заковика в тому, що p5.js за замовчуванням вимірює кути не в градусах, а у радіанах. Ця альтернативна одиниця вимірювання визначається відношенням довжини дуги кола (сегмента окружності кола) до радіуса цього кола. Один радіан — це кут, при якому це відношення дорівнює одиниці (див. малюнок 3.3). Повне коло (360 градусів) еквівалентно 2π2\pi радіанам, 180 градусів еквівалентно π\pi радіанам, а 90 градусів еквівалентно π/2\pi/2 радіанам.

Формула для переведення градусів у радіани:

радіани=2π×градуси360\text{радіани} = {2\pi \times \text{градуси} \over 360}

На щастя, якщо ви віддаєте перевагу кутам у градусах, то можете ввімкнути відповідний режим, викликавши функцію angleMode(DEGREES), або використовувати зручну функцію radians() для перетворення значень з градусів у радіани. Також доступні константи PI, TWO_PI і HALF_PI (еквіваленти 180, 360 і 90 градусам відповідно). Наприклад, ось два способи повернути фігуру на 60 градусів у p5.js:

let angle = 60;

rotate(radians(angle));


angleMode(DEGREES);

rotate(angle);

Що таке Пі?

Математична константа пі, яку позначають грецькою літерою π\pi, — це дійсне число, яке визначається як відношення довжини кола (або периметру кола) до його діаметра (прямої лінії, що проходить через центр кола і сполучає дві його точки). Воно дорівнює приблизно 3.14159 і доступне у p5.js через вбудовану константу PI.

Хоча градуси можуть бути корисними, у цій книзі я буду працювати з радіанами, оскільки вони є стандартною одиницею вимірювання в багатьох мовах програмування і графічних середовищах. Якщо радіани для вас новинка, це гарна нагода для практики! Крім того, якщо ви не знайомі з тим, як обертання реалізовано в p5.js, я рекомендую переглянути мою відеосерію на Coding Train про трансформації у p5.js.

Вправа 3.1

Налаштуйте обертання об’єкта, схожого на кийок, навколо його центру за допомогою функцій translate() і rotate().

Кутовий рух

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

На щастя, ви вже знаєте всі математичні аспекти, які потрібні для розуміння кутового руху. Пам’ятаєте матеріал, пояснення якому я майже повністю присвятив Розділ 1 і Розділ 2?

швидкість=(швидкість+прискорення)\overrightarrow{\text{швидкість}} = \overrightarrow{\text{(швидкість}} + \overrightarrow{\text{прискорення)}}
позиція=(позиція+швидкість)\overrightarrow{\text{позиція}} = \overrightarrow{\text{(позиція}} + \overrightarrow{\text{швидкість)}}

Ви можете застосувати ту саму логіку до об’єкта, що обертається:

кутова швидкість=(кутова швидкість+кутове прискорення)\text{кутова швидкість} = \text{(кутова швидкість} + \text{кутове~прискорення)}
кут=(кут+кутова швидкість)\text{кут} = \text{(кут} + \text{кутова швидкість)}

Насправді ці формули кутового руху простіші, ніж їхні еквіваленти лінійного руху, оскільки кут тут є скалярною величиною (одним числом), а не вектором! Це тому, що у 2D просторі лише одна вісь обертання, а в 3D просторі кут став би вектором. (Зауважте, що в більшості випадків ці формули включатимуть множення на зміну часу, відому як дельта часу. Я припускаю, що дельта часу дорівнює 1, що відповідає одному кадру анімації в p5.js.)

Використовуючи відповідь із вправи 3.1, скажімо, що ви хочете повернути кийок у p5.js на певний кут. Спочатку код міг виглядати наступним чином:

translate(width / 2, height / 2);

rotate(angle);

line(-60, 0, 60, 0);

circle(60, 0, 16);

circle(-60, 0, 16, 16);


angle = angle + 0.1;

Додавши принципи кутового руху, я можу замість цього написати наступний приклад (розв’язок вправи 3.1).

let angle = 0;

Кут.

let angleVelocity = 0;

Швидкість.

let angleAcceleration = 0.0001;

Прискорення.


function setup() {

  createCanvas(640, 240);

}


function draw() {

  background(255);

  translate(width / 2, height / 2);

  rotate(angle);

Поворот відповідно до кута.

  stroke(0);

  fill(127);

  line(-60, 0, 60, 0);

  circle(60, 0, 16);

  circle(-60, 0, 16);Z

  angleVelocity += angleAcceleration;

Кутовий еквівалент для velocity.add(acceleration)

  angle += angleVelocity;

Кутовий еквівалент для position.add(velocity)

}

Замість збільшення значення angle на фіксовану величину для рівномірного обертання, я кожного кадру додаю angleAcceleration до angleVelocity, а потім додаю angleVelocity до angle. У результаті нерухомий кийок починає обертатися та розкручуватися дедалі швидше і швидше в міру прискорення кутової швидкості.

Вправа 3.2

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

Наступним логічним кроком є включення ідеї кутового руху до класу Mover. Спершу мені потрібно додати кілька змінних до конструктора класу:

class Mover {

  constructor() {

    this.position = createVector();

    this.velocity = createVector();

    this.acceleration = createVector();

    this.mass = 1.0;

    this.angle = 0;
    this.angleVelocity = 0;
    this.angleAcceleration = 0;

Змінні для кутового руху.

  }

Потім у функції update(), позиція та кут об’єкта оновлюються відповідно до алгоритму, який я щойно продемонстрував:

  update() {

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

Звичайний старомодний рух.

    this.angleVelocity += this.angleAcceleration;
    this.angle += this.angleVelocity;

Новомодний кутовий рух.

    this.acceleration.mult(0);

  }

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

  show() {

    stroke(0);

    fill(175, 200);

    push();

Функція push() використовується для збереження поточного стану полотна, щоб після його відновлення, за допомогою функції pop(), обертання поточного об’єкта не впливало на решту об’єктів.

    translate(this.position.x, this.position.y);

Встановлення початку координат у позиції фігури.

    rotate(this.angle);

Застосування оберту на даний кут.

    circle(0, 0, this.radius * 2);

    line(0, 0, this.radius, 0);

    pop();

Виклик функції pop() відновлює попередньо збережений стан полотна. У цьому випадку це поверне початкові координати у їх основне положення.

  }

На цьому етапі, якщо ви дійсно створите об’єкт Mover, то не побачите, що він поводиться якось інакше. Це тому, що кутове прискорення ініціалізовано нулем (this.angleAcceleration = 0;). Щоб об'єкт обертався, йому потрібне ненульове прискорення! Звичайно, одним із варіантів є хардкод і пряме присвоєння бажаного числа у конструкторі:

    this.angleAcceleration = 0.01;

Однак ви можете отримати цікавіший результат, динамічно призначаючи кутове прискорення у методі update(), відповідно до сил у середовищі. Це могло бути натяком на початок дослідження фізики кутового прискорення на основі понять обертального моменту і моменту інерції, але на цьому етапі такий рівень моделювання буде трохи схожим на кролячу нору. Пізніше у цьому розділі я розповім більше про моделювання кутового прискорення на прикладі “Маятника”, а також розгляну, як сторонні фізичні бібліотеки реалістично моделюють обертальний рух у Розділі 6.

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

    this.angleAcceleration = this.acceleration.x;

Використання x-компоненти лінійного прискорення об’єкта для обчислення кутового прискорення.

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

Ділення x-компоненти на певне значення, або обмеження кутової швидкості до розумного діапазону, може допомогти. Нижче можна буде ознайомитись з усім методом update() із щойно згаданими змінами.

  update() {

    this.velocity.add(this.acceleration);

    this.position.add(this.velocity);

    this.angleAcceleration = this.acceleration.x / 10.0;

Розрахунок кутового прискорення відповідно до x-компоненти прискорення.

    this.angleVelocity += this.angleAcceleration;

    this.angleVelocity = constrain(this.angleVelocity, -0.1, 0.1);

Використання функції constrain() для забезпечення того, щоб кутова швидкість не виходила з-під контролю.

    this.angle += this.angleVelocity;

    this.acceleration.mult(0);

  }

Зверніть увагу, що я використовував кілька стратегій, щоб утримати обертання об’єкта під контролем. Спочатку я поділив acceleration.x на 10 перед тим як призначити його до angleAcceleration. Потім, я також використав функцію constrain() для обмеження angleVelocity у діапазоні від -0.1 до 0.1.

Вправа 3.3

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

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

Тригонометричні функції

Гадаю, я готовий розкрити секрет тригонометрії. Я говорив про кути й крутив кийки. Тепер настав час для … зачекайте-зачекайте … sohcahtoa. Так, для sohcahtoa! Це, здавалося б, безглузде слово насправді є основою для багатьох робіт з комп’ютерної графіки. Базове розуміння тригонометрії є досить важливим, якщо ви хочете обчислювати кути, визначати відстані між точками та працювати з кругами, дугами чи лініями. А sohcahtoa — це мнемонічна витівка (хоча й дещо абсурдна) для запам’ятовування того, що означають тригонометричні функції синус, косинус і тангенс. Вона посилається на різні сторони прямокутного трикутника, як показано на малюнку 3.4.

Малюнок 3.4: Прямокутний трикутник зі сторонами відносно кута: прилеглої, протилежної і гіпотенузи
Малюнок 3.4: Прямокутний трикутник зі сторонами відносно кута: прилеглої, протилежної і гіпотенузи

Візьміть один із непрямих кутів прямокутного трикутника. Прилегла (adjacent) сторона — та, яка дотикається до цього кута, протилежна (opposite) — та, яка не дотикається цього кута, і гіпотенуза (hypotenuse) — сторона, протилежна прямому куту. Sohcahtoa розказує, як обчислити тригонометричні функції кута на основі довжин цих сторін. Це мнемонічне скорочення побудоване з перших літер англійських слів як показано нижче:

  • soh: sine(angle)=opposite/hypotenuse\text{\textbf{s}ine(angle)} = \text{\textbf{o}pposite}/\text{\textbf{h}ypotenuse}
  • cah: cosine(angle)=adjacent/hypotenuse\text{\textbf{c}osine(angle)} = \text{\textbf{a}djacent} / \text{\textbf{h}ypotenuse}
  • toa: tangent(angle)=opposite/adjacent\text{\textbf{t}angent(angle)} = \text{\textbf{o}pposite} / \text{\textbf{a}djacent}

Ще раз погляньте на малюнок 3.4. Вам не потрібно це запам’ятовувати, але погляньте чи вам це зрозуміло. Спробуйте намалювати його самостійно. Далі розглянемо це трохи по-іншому (див. малюнок 3.5).

Малюнок 3.5: Вектор \vec{v} із xy-компонентами й кут
Малюнок 3.5: Вектор v\vec{v} із xyxy-компонентами й кут.

Бачите, як із вектора v\vec{v} створюється прямокутний трикутник? Сама стрілка вектора є гіпотенузою, а компоненти вектора (xx і yy) — сторонами трикутника. Кут є додатковим способом визначення напрямку (або курсу). Тригонометричні функції, розглянуті у такому вигляді, проявляють зв’язок між компонентами вектора і його напрямком з магнітудою. Таким чином, тригонометрія буде дуже корисною в цій книзі. Щоб проілюструвати це, розглянемо приклад, для якого потрібна функція тангенса.

Спрямованість у напрямку руху

Згадайте весь шлях до прикладу 1.10, у якому показано, як об’єкт Mover прискорюється в напрямку курсора (малюнок 3.6).

Малюнок 3.6: Об’єкт, що прискорюється у напрямку курсора (з прикладу 1.10)

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

Коли я говорю “спрямування у напрямку руху”, то насправді маю на увазі “поворот відповідно до вектора швидкості”. Швидкість — це вектор з xx і yy компонентами, але для обертання у p5.js потрібне одне число — кут. Давайте ще раз подивимося на тригонометричну діаграму, цього разу зосередившись на векторі швидкості об’єкта (малюнок 3.7).

Малюнок 3.7: Тангенс кута вектора швидкості дорівнює y ділене на x
Малюнок 3.7: Тангенс кута вектора швидкості дорівнює yy ділене на xx

Векторні компоненти xx і yy пов’язані з його кутом через функцію тангенса. Використовуючи toa з sohcahtoa, я можу записати це відношення наступним чином:

тангенс кута=швидкістьyшвидкістьx\text{тангенс кута} = \frac{\text{швидкість}_y}{\text{швидкість}_x}

Проблема в тому, що хоча я знаю xx і yy компоненти вектора швидкості, мені насправді не відомий кут його напрямку. Мені потрібно визначити цей кут. Ось тут з’являється інша функція, відома як обернений тангенс або арктангенс (скорочений запис arctan або atan). Існують також функції оберненого синуса та оберненого косинуса, які називаються відповідно арксинусом (arcsin) та арккосинусом (arccos).

Якщо тангенс деякого значення aa дорівнює деякому значенню bb, тоді обернений тангенс bb дорівнює aa. Наприклад:

Якщоtan(a)=b\tan(a) = b
Тодіa=arctan(b)a = \arctan(b)

Бачите, як одне є оберненим до іншого? Це дозволяє мені дізнатися кут вектора:

Якщоtan(angle)=velocityyvelocityx\tan(\text{angle}) = \frac{\text{velocity}_y}{\text{velocity}_x}
Тодіangle=arctan(velocityyvelocityx)\text{angle} = \arctan(\frac{\text{velocity}_y}{\text{velocity}_x})

Тепер, коли у мене є формула, подивимось де вона має бути у методі show() класу Mover, щоб повернути об’єкт (тепер намальований у вигляді прямокутника) у напрямку свого руху. Зверніть увагу, що у p5.js функція оберненого тангенса — це atan():

  show() {

    let angle = atan(this.velocity.y / this.velocity.x);

Визначення кута за допомогою функції atan().

    stroke(0);

    fill(175);

    push();

    rectMode(CENTER);

    translate(this.position.x, this.position.y);

    rotate(angle);

Поворот на потрібний кут.

    rect(0, 0, 30, 10);

    pop();

  }

Малюнок 3.8: Вектори \vec{v}_1 і \vec{v}_2 з компонентами (4, -3) та (-4, 3) вказують у протилежні напрямки.
Малюнок 3.8: Вектори v1\vec{v}_1 і v2\vec{v}_2 з компонентами (4,3)(4, -3) та (4,3)(-4, 3) вказують у протилежні напрямки

Цей код майже готовий і практично робочий. Однак є велика проблема. Розглянемо два вектори швидкості, зображені на малюнку 3.8.

Хоча зовні вони схожі, обидва вектори вказують у зовсім різних напрямках — протилежних напрямках! Не зважаючи на це, подивіться, що станеться, якщо я застосую формулу оберненого тангенса для визначення кута кожного із цих векторів:

v1angle=arctan(3/4)=arctan(0.75)=0.643501 radians=37 degrees\vec{v}_1 ⇒ \text{angle} = \arctan(3/{-4}) = \arctan(-0.75) = -0.643501 \text{ radians} = -37 \text{ degrees}
v2angle=arctan(3/4)=arctan(0.75)=0.643501 radians=37 degrees\vec{v}_2 ⇒ \text{angle} = \arctan(-3/4) = \arctan(-0.75) = -0.643501 \text{ radians} = -37 \text{ degrees}

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

    let angle = atan2(this.velocity.y, this.velocity.x);

Використання atan2() для врахування всіх можливих напрямків.

    push();

    rectMode(CENTER);

    translate(this.position.x, this.position.y);

    rotate(angle);

Поворот на потрібний кут.

    rect(0, 0, 30, 10);

    pop();

  }

Щоб спростити це ще більше, клас p5.Vector надає метод під назвою heading(), який самостійно піклується про виклик atan2() і повертає двовимірний кут напрямку у радіанах для будь-якого об’єкта типу p5.Vector:

    let angle = this.velocity.heading();

Найпростіший спосіб зробити це!

З heading() вам практично не потрібно виконувати тригонометричні функції для знаходження кута напрямку, але все одно корисно знати, як все це працює.

Вправа 3.4

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

Полярні та прямокутні координати

Кожного разу, коли ви малюєте фігуру в p5.js, вам потрібно вказати її піксельне положення, xx і yy координати. Ці координати відомі як декартові координати, названі на честь Рене Декарта, французького математика, який розвинув ідеї, що лежать в основі прямокутного простору.

Інша корисна система координат — полярні координати — описує точку в просторі як відстань від початку координат (наприклад, радіус кола) і відповідний кут (зазвичай позначається грецькою буквою тета θ\theta). Мислячи в термінах векторів, декартові координати описують векторні компоненти xx та yy, тоді як полярні координати описують векторну магнітуду (величину, довжину) і її напрямок (кут).

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

Малюнок 3.9: Грецька літера \theta (тета) часто використовується для позначення кута. Оскільки полярні координати традиційно позначаються як (r, \theta), я буду використовувати theta як назву для змінної, що відповідає за кут
Малюнок 3.9: Грецька літера θ\theta (тета) часто використовується для позначення кута. Оскільки полярні координати традиційно позначаються як (r,θ)(r, \theta), я буду використовувати theta як назву для змінної, що відповідає за кут

Наприклад, якщо задані полярні координати із радіусом у 75 пікселів і кутом (θ\theta) у 45 градусів (або π/4\pi/4 радіан), то декартові xx та yy можна обчислити наступним чином:

cos(θ)=x/rx=r×cos(θ)\cos(\theta) = x/r \Rightarrow x = r \times \cos(\theta)
sin(θ)=y/ry=r×sin(θ)\sin(\theta) = y / r \Rightarrow y = r \times \sin(\theta)

Для обчислення синуса і косинуса у p5.js є відповідні функції sin() та cos(). Кожна з них приймає один аргумент, число, що представляє кут у радіанах. Ці формули можна закодувати наступним чином:

let r = 75;

let theta = PI / 4;

let x = r * cos(theta);
let y = r * sin(theta);

Перетворення полярних координат (r, theta) у декартові (x, y).

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

let r;

let theta;


function setup() {

  createCanvas(640, 240);

  r = height * 0.45;
  theta = 0;

Ініціалізація значень.

}


function draw() {

  background(255);

  translate(width / 2, height / 2);

Переміщення початкової точки у центр екрана.

  let x = r * cos(theta);
  let y = r * sin(theta);

Полярні координати (r, theta) перетворюються на декартові (x, y) для використання у функції circle().

  fill(127);

  stroke(0);

  line(0, 0, x, y);

  circle(x, y, 48);

  theta += 0.02;

Поступове збільшення кута.

}

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

  let position = p5.Vector.fromAngle(theta);

Створення одиничного вектора, що вказує у напрямку кута.

  position.mult(r);

Помноження вектора position на r для завершення перетворення полярних координат у декартові.

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

Малювання кола з використанням xy-компонентів вектора.

Ви вже вражені? Я продемонстрував кілька чудових способів використання тангенса (для знаходження кута вектора) і синуса та косинуса (для перетворення полярних координат у декартові). Я міг би зупинитися на цьому і бути задоволеним. Але я не збираюся. Це лише початок. Як я вам покажу далі, те, що синус і косинус можуть зробити для вас, виходить за межі математичних формул та прямих трикутників.

Вправа 3.5

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

Вправа 3.6

Зробіть симуляцію космічного корабля з гри Астероїди. Якщо ви не знайомі з цією грою, то ось короткий опис: космічний корабель (тут зображено трикутником) плаває у 2D просторі. Клавіша зі стрілкою вліво повертає космічний корабель проти годинникової стрілки, клавіша зі стрілкою вправо — за годинниковою стрілкою. Клавіша Z застосовує силу тяги у напрямку в якому повернуто космічний корабель.

Властивості коливань

Подивіться на графік синус-функції на малюнку 3.10, де y=sin(x)y = \sin(x).

Малюнок 3.10: Графік y = sin(x)
Малюнок 3.10: Графік y=sin(x)y = sin(x)

Результат синус-функції — це гладенька крива, верхівка якої почергово переходить від 1−1 до 11, також відома під назвою синусоїдальна хвиля або синусоїда. Така поведінка, з періодичним рухом між двома точками, є коливанням (осциляцією), про яке я згадував на початку розділу. Вібрація гітарної струни, гойдання маятника, стрибання на пого-палиці — усе це приклади коливального руху, які можна змоделювати за допомогою синус-функції.

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

Малюнок 3.11: Кулька, що коливається

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

Коли рухомий об’єкт демонструє простий гармонічний рух, його положення (у цьому випадку xx-позицію) можна виразити як функцію часу з наступними двома елементами:

  • Амплітуда — відстань від центру руху до будь-якого крайнього положення
  • Період — тривалість (час) одного повного циклу руху

Щоб зрозуміти ці терміни, ще раз подивіться на графік синус-функції на малюнку 3.10. Крива ніколи не підіймається вище 1 або нижче -1 вздовж осі yy, тому функція синуса має амплітуду 1. Водночас хвилеподібний патерн кривої повторюється кожні 2π2\pi одиниці вздовж осі xx, тому період функції синуса дорівнює 2π2\pi. (За домовленістю одиницями вимірювання тут є радіани, оскільки вхідним значенням для функції синуса зазвичай є кут у радіанах.)

Ми сказали чимало про амплітуду та період абстрактної функції синуса, але що таке амплітуда та період у світі p5.js на прикладі кульки, що коливається? Що ж, амплітуду можна досить легко виміряти в пікселях. Наприклад, якщо полотно має ширину 200 пікселів, я можу вибрати коливання навколо центру полотна, переміщуючись від 100 пікселів праворуч від центру до 100 пікселів ліворуч від центру. Іншими словами, амплітуда становитиме 100 пікселів:

let amplitude = 100;

Амплітуда виміряна в пікселях.

Період — це кількість часу для одного повного циклу осциляції. Однак що означає час у програмі p5.js? Теоретично я міг би сказати, що хочу, щоб кулька коливалася кожні три секунди, а потім придумати складний алгоритм для руху об’єкта відповідно до реального часу, використовуючи функцію millis() для відстеження пройдених мілісекунд. Однак для того, що я намагаюсь тут досягти, реальний час не обов’язковий. Більш корисною мірою часу у p5.js є кількість кадрів яка минула, що доступна через вбудовану змінну frameCount. Чи хочу я, щоб коливальний рух повторювався кожні 30 кадрів? Кожні 50 кадрів? Як щодо періоду у 120 кадрів:

let period = 120;

Період вимірюваний кадрами (одиницями часу для анімації).

Коли я маю амплітуду і період, настав час написати формулу для обчислення xx-позиції кола як функції часу (тут це поточна кількість кадрів):

let x = amplitude * sin(TWO_PI * frameCount / period);

amplitude та period — це мої власні змінні, frameCount — вбудована у p5.js.

Подумайте про те, що тут відбувається. По-перше, будь-яке значення, яке повертає функція sin() множиться на amplitude. Як ви бачили на малюнку 3.10, результат функції синуса коливається між -1 і 1. Помноження цього значення на обрану мною амплітуду — назвемо її aa — дає мені бажаний результат: значення, яке коливається між -aa та aa. (Це також місце, де ви можете використовувати функцію map() для відображення результатів функції sin() у власний діапазон.)

Тепер подумайте про те, що знаходиться всередині функції sin():

TWO_PI * frameCount / period

Що тут відбувається? Почнемо з того, що ви знаєте. Я пояснив, що синус має період 2π2\pi, тобто починається з 00 і повторюється при 2π2\pi, 4π4\pi, 6π6\pi й так далі. Якщо мій бажаний період коливань складає 120 кадрів, тоді я хочу, щоб кулька була у тому самому положенні, коли frameCount становитиме 120 кадрів, 240 кадрів, 360 кадрів і так далі. Тут frameCount — це єдина змінна, яка змінюється з часом. Подивіться, які результати дає формула при збільшенні frameCount:

frameCountframeCount / periodTWO_PI * frameCount / period
000
600.5π\pi
12012π2\pi
24024π4\pi
. . .. . .. . .

Ділення frameCount на period говорить мені, скільки циклів вже було завершено. Чи хвиля на півдорозі першого циклу? Чи завершилося два цикли? Помноживши це число на TWO_PI, я отримую бажаний результат, відповідне вхідне значення для функції sin(), оскільки TWO_PI — це значення, необхідне для того, щоб синус (або косинус) завершив один повний цикл.

Зібравши все разом, отримаємо приклад коливання кола за його x-позицією з амплітудою у 100 пікселів і періодом у 120 кадрів.

function setup() {

  createCanvas(640, 240);

}


function draw() {

  background(255);

  let period = 120;

  let amplitude = 200;

  let x = amplitude * sin(TWO_PI * frameCount / period);

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

  stroke(0);

  fill(127);

  translate(width / 2, height / 2);

  line(0, 0, x, 0);

  circle(x, 0, 48);

}

Перш ніж рушити далі, я згадаю про частоту — кількість циклів коливань за одиницю часу. Частота є оберненою величиною до періоду, тобто 1 поділена на період. Наприклад, якщо період становить 120 кадрів, тоді у першому кадрі завершується лише 1/120 періоду, отже частота дорівнює 1/120. У прикладі 3.5 я вирішив визначити швидкість коливань на основі періоду, тому мені не потрібна була змінна для частоти. Іноді, однак, більш корисним є мислення з точки зору частоти, а не періоду.

Вправа 3.7

Використовуючи функцію синуса, створіть імітацію тягарця (англійською його іноді називають bob — підвіс), який висітиме на пружині у верхній частині полотна. Використовуйте функцію map(), щоб обчислити вертикальне положення тягарця. Пізніше у частині цього розділу під назвою “Сила пружності” я покажу, як створити цю саму симуляцію, моделюючи силу пружини згідно із законом Гука.

Коливання з кутовою швидкістю

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

let x = amplitude * sin(TWO_PI * frameCount / period);

Зараз я перепишу її трохи по-іншому:

let x = amplitude * sin( якесь значення, що повільно збільшується );

Якщо вам потрібно точне визначення періоду коливань у кадрах анімації, вам може знадобитися формула, яку я написав спочатку. Однак якщо вас не хвилює точний період — наприклад, якщо ви будете вибирати його випадковим чином — все, що вам справді потрібно всередині функції sin() це якесь значення, яке збільшується досить повільно, щоб рух об’єкта виглядав плавним від одного кадру до наступного. Щоразу, коли це значення ставатиме кратним 2π2\pi, об’єкт завершуватиме один цикл осциляції.

Ця техніка віддзеркалює те, що я зробив із шумом Перліна у Розділі 0. У тому випадку я збільшував змінну зміщення (яку я називав t або xoff), щоб вибирати різні результати функції noise(), створюючи плавний перехід значень. Тепер я збільшуватиму значення (я назву його angle), яке передається у функцію sin(). Відмінність полягає в тому, що результат від sin() це плавно повторювана синусоїда без будь-якої випадковості.

Вам може бути цікаво, чому я називаю змінну для збільшення значення angle, хоча видимого обертання самого об’єкта немає. Термін кут використовується, оскільки його значення передається у функцію sin(), а кути є традиційними вхідними даними для тригонометричних функцій. Маючи це на увазі, я можу повторно ввести поняття кутової швидкості (і прискорення), щоб переписати приклад для обчислення x-позиції в термінах зміни кута. Я заведу наступні глобальні змінні:

let angle = 0;

let angleVelocity = 0.05;

Потім я можу написати наступне:

function draw() {

  angle += angleVelocity;

  let x = amplitude * sin(angle);

}

Тут angle — це моє “значення, що повільно збільшується”, а кількість, на яку воно повільно збільшується — це angleVelocity.

let angle = 0;

let angleVelocity = 0.05;


function setup() {

  createCanvas(640, 240);

}


function draw() {

  background(255);

  let amplitude = 200;

  let x = amplitude * sin(angle);

  angle += angleVelocity;

Використання концепції кутової швидкості для збільшення змінної кута.

  translate(width / 2, height / 2);

  stroke(0);

  fill(127);

  line(0, 0, x, 0);

  circle(x, 0, 48);

}

Тільки тому, що я не посилаюся на нього безпосередньо, ще не означає, що я виключив концепцію періоду. Зрештою чим більша кутова швидкість, тим швидше буде коливатися кулька (а отже, буде менший період). Насправді період — це кількість кадрів, необхідних для збільшення angle на 2π2\pi. Оскільки величина збільшення angle контролюється кутовою швидкістю, я можу обчислити період таким чином:

період=2π/кутова швидкість\text{період} = 2\pi / \text{кутова ш�видкість}

Щоб проілюструвати потужність мислення про коливання у термінах кутової швидкості, я трохи розширю приклад, створивши клас Oscillator, об’єкти якого можуть коливатися незалежно як вздовж вісі xx (як і раніше), так і вздовж вісі yy. У класі потрібні два кути, дві кутові швидкості та дві амплітуди (по одній для кожної вісі).

Це чудова нагода використати createVector() для упакування кожної пари значень разом! На відміну від попередніх векторів, значення в цих векторах не будуть наборами декартових координат. Проте, клас p5.Vector надає зручний спосіб керування парами значень — у цьому випадку парами кутів (і пов’язаними з ними швидкостями, прискореннями тощо).

class Oscillator  {

  constructor()  {

    this.angle = createVector();

Використання p5.Vector для відстеження двох кутів!

    this.angleVelocity = createVector(random(-0.05, 0.05), random(-0.05, 0.05));

    this.amplitude = createVector(random(20, width / 2), random(20, height / 2));

Випадкові швидкості і амплітуди.

  }


  update()  {

    this.angle.add(this.angleVelocity);

  }


  show()  {

    let x = sin(this.angle.x) * this.amplitude.x;

Коливання по вісі x.

    let y = sin(this.angle.y) * this.amplitude.y;

Коливання по вісі y.

    push();

    translate(width / 2, height / 2);

    stroke(0);

    fill(127);

    line(0, 0, x, y);
    circle(x, y, 32);
    pop();

Малювання осцилятора у вигляді лінії з кругом на його кінці.

  }

}

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

Головне зрозуміти, що x і y властивості об’єктів типу p5.Vector, що наразі містяться у this.angle, this.angleVelocity і this.amplitude більше не пов’язані з просторовими векторами. Натомість вони використовуються для збереження відповідних властивостей для двох окремих коливань (одного вздовж вісі xx, іншого вздовж вісі yy). Зрештою, ці коливання проявляються просторово, коли x і y обчислюються в методі show(), відображаючи коливання на положення об’єкта.

Вправа 3.8

Спробуйте ініціалізувати кожен об’єкт Oscillator швидкістю й амплітудою, які не будуть випадковими, щоб створити якийсь регулярний патерн. Чи можете ви зробити так, щоб осцилятори виглядали як ноги комахоподібної істоти?

Вправа 3.9

Додайте у об’єкт Oscillator кутове прискорення.

Хвилі

Уявіть один круг, який коливається вгору і вниз відповідно до функції синуса. Це схоже на попередню симуляцію точки вздовж xx-вісі синусоїди. З невеликою вигадкою та циклом for, ви можете анімувати всю хвилю, розмістивши цілу серію цих кругів один біля одного (малюнок 3.12).

Малюнок 3.12: Анімація синусоїди за допомоги коливальних кіл

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

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

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

let angle = 0;

let deltaAngle = 0.2;

let amplitude = 100;

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

  1. Обчислення yy-позиції відповідно до амплітуди й синуса кута.
  2. Малювання круга у точці (x,y)(x, y).
  3. Збільшення кута відповідно до дельти кута.

У наступному прикладі ці кроки переведено у код.

let angle = 0;

let deltaAngle = 0.2;

let amplitude = 100;


function setup() {

  createCanvas(640, 240);

  background(255);

  stroke(0);

  fill(127, 127);

  for (let x = 0; x <= width; x += 24) {

    let y = amplitude * sin(angle);

Крок 1: Обчислення y-позиції відповідно до амплітуди й синуса кута.

    circle(x, y + height / 2, 48);

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

    angle += deltaAngle;

Крок 3: Збільшення кута відповідно до дельти кута.

  }

}

Що станеться, якщо спробувати різні значення для deltaAngle? На малюнку 3.13 показано деякі варіанти.

Малюнок 3.13: Три синусоїди з різними значеннями змінної deltaAngle (зліва направо: 0.05, 0.2, 0.6)

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

Зверніть увагу, що все в прикладі 3.8 відбувається всередині функції setup(), тому результат є статичним. Хвиля ніколи не змінюється і не коливається. Додавання руху трохи складніше. Ваша перша думка може бути такою: “Гей, не проблема, я просто розміщу цикл for всередині функції draw() і дозволю змінній angle продовжувати збільшуватись від одного циклу до наступного”.

Це гарна думка, але вона не спрацює. Якщо ви спробуєте це, результат виглядатиме надзвичайно нестабільно і глючно. Щоб зрозуміти чому, подивіться на приклад 3.8. Правий край хвилі не збігається з висотою лівого краю, тому те місце, де хвиля закінчується в одному циклі функції draw() не буде там, де вона починається у наступному. Замість цього вам потрібна змінна, повністю призначена для відстеження початкового значення angle у кожному кадрі анімації. Ця змінна (яку я назву startAngle) збільшується у власному темпі, контролюючи, наскільки хвиля просувається від одного кадру до наступного.

let startAngle = 0;

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

let deltaAngle = 0.2;


function setup() {

  createCanvas(640, 240);

}


function draw() {

  background(255);

  let angle = startAngle;

Щоразу під час малювання, значення для кута який збільшується, встановлюється зі змінної startAngle.

  for (let x = 0; x <= width; x += 24) {

    let y = map(sin(angle), -1, 1, 0, height);

    stroke(0);

    fill(127, 127);

    circle(x, y, 48);

    angle += deltaAngle;

  }

  startAngle += 0.02;

Збільшення початкового кута.

}

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

Вправа 3.10

Спробуйте використати функцію шуму Перліна замість синуса або косинуса, щоб встановити y-значення у прикладі 3.9.

Вправа 3.11

Інкапсулюйте код генерації хвилі у клас Wave і створіть програму, яка відображає дві хвилі (з різними амплітудами/періодами), як показано на наступному зображені. Спробуйте вийти за межі простих кругів і ліній, щоб візуалізувати хвилю більш творчо. Як щодо з’єднання точок за допомогою beginShape(), endShape() і vertex()?

Вправа 3.12

Для створення складнішої хвилі, ви можете додати кілька хвиль разом. Обчисліть значення висоти (або y) для кількох різних хвиль і додайте ці значення разом, щоб отримати єдине значення y. Результатом стане нова хвиля, яка міститиме у собі характеристики кожної окремої хвилі.

Сила пружності

Малюнок 3.14: Пружина з опорою (якорем) і тягарцем
Малюнок 3.14: Пружина з опорою (якорем) і тягарцем

Досліджувати математику трикутників і хвиль було чудово, але можливо, ви почали сумувати за законами руху Ньютона та векторами. Зрештою, суть цієї книги — моделювання фізики рухомих тіл. У секції про “Властивості коливань”, я змоделював простий гармонічний рух, зобразивши синусоїду розтягнуту на діапазон пікселів полотна. Вправа 3.7 пропонувала вам використати підхід із функцією sin(), щоб створити симуляцію з тягарцем, що висить на пружині. Однак таке швидке й брудне рішення з одним рядком коду не підійде, якщо ви дійсно хочете, щоб тягарець, що висить на пружині, реагував на інші сили свого середовища (вітер, гравітацію тощо). Щоб досягти такої симуляції, вам потрібно змоделювати силу пружності за допомогою векторів.

Я розглядатиму пружину як з’єднання між рухомим тягарцем і нерухомою опорною точкою, інакше кажучи, опорою чи якорем (див. малюнок 3.14).

Малюнок 3.15: Розтяг пружини (x) є різницею між її поточною довжиною та її довжиною у спокої
Малюнок 3.15: Розтяг пружини (xx) є різницею між її поточною довжиною та її довжиною у спокої

Сила пружності — це вектор, обчислений згідно із законом Гука, названим на честь Роберта Гука, британського фізика, який розробив формулу в 1660 році. Гук спочатку виклав закон латинською мовою: “Ut tensio, sic vis” або “Який розтяг, така й сила”. Подумайте про це наступним чином:

Сила пружності прямо пропорційна розтягу пружини.

Розтяг є мірою того, наскільки пружина була розтягнута або стиснута: як показано на малюнку 3.15, це різниця між поточною довжиною пружини та її довжиною в стані спокою (її станом рівноваги). Отже, закон Гука говорить, що якщо ви дуже тягнете за тягарець, то сила пружності буде великою, а якщо тягнете трохи, то сила буде слабкою.

Математично закон виражається так:

Fspring=kxF_{spring} = -kx

Тут kk — це коефіцієнт жорсткості. Його значення масштабує силу, встановлюючи, наскільки еластичною або жорсткою є пружина. Тим часом xx — це видовження, поточна довжина мінус довжина спокою.

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

let anchor = createVector(0, 0);
let bob = createVector(0, 120);
let restLength = 100;

Вибір довільних значень для позицій і довжини у спокої.

Далі я використаю закон Гука для обчислення величини сили. Для цього мені потрібні k і x. Розрахувати k легко — це просто константа, що залежить від матеріалу і форми пружини, тому наразі я вигадаю її значення:

let k = 0.1;

Знайти x, можливо, трохи складніше. Мені потрібно знати різницю між поточною довжиною пружини і її довжиною у стані спокою. Довжина у спокої визначається у змінній restLength. Яка поточна довжина? Відстань між опорою і тягарцем. І як я можу розрахувати цю відстань? Як щодо магнітуди вектора, який вказує від опорної точки до тягарця? (Зауважте, що це точно такий самий процес, який я використовував для обчислення відстані між об’єктами для обчислення гравітаційного тяжіння у Розділі 2.)

let dir = p5.Vector.sub(bob, anchor);

Вектор, що вказує від опори до тягарця, дає вам поточну довжину пружини.

let currentLength = dir.mag();

let x = currentLength - restLength;

Тепер, коли я розібрався зі складовими, необхідними для визначення величини сили (kx-kx), мені потрібно визначити напрямок сили у вигляді одиничного вектору. Хороша новина в тому, що я вже маю цей вектор. Правильно? Мить тому я поставив запитання “Як я можу розрахувати цю відстань?” і відповів “Як щодо магнітуди вектора, який вказує від опорної точки до тягарця”? Цей вектор описує і напрямок сили!

На малюнку 3.16 показано, що якщо ви розтягнете пружину понад її довжину в стані спокою, має виникнути сила, яка буде тягнути її назад до якоря. А якщо пружину стиснути менше за її довжину спокою, то утворена сила повинна відштовхнути її від якоря. Формула закону Гука враховує цю зміну напрямку за допомоги -1.

Малюнок 3.16: Сила пружності сп�рямована у протилежний напрямок від деформації
Малюнок 3.16: Сила пружності спрямована у протилежний напрямок від деформації

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

let k = 0.1;
let force = p5.Vector.sub(bob, anchor);
let currentLength = force.mag();
let x = currentLength - restLength;

Величина сили пружності відповідно до закону Гука.

force.setMag(-1 * k * x);

Збираємо все разом: напрямок і величину!

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

Оскільки я весь час працював з класом Mover, я буду дотримуватися тієї самої структури. Я буду думати про клас Mover, як про “тягарець” на пружині. Тягарцю для переміщення по полотну потрібні вектори position, velocity і acceleration. Ідеально — у мене все це вже є! І можливо, тягарець також відчуває силу тяжіння за допомогою методу applyForce(). Залишається лише один крок до застосування сили пружності:

let bob;


function setup() {

  bob = new Bob();

}


function draw()  {

  let gravity = createVector(0, 1);
  bob.applyForce(gravity);

Вигадана сила тяжіння з Розділу 2.

  let springForce = _______________????
  bob.applyForce(springForce);

Також мені потрібно розрахувати і застосувати силу пружності!

  bob.update();
  bob.show();

Стандартні методи update() і show().

}

Як варіант можна було б записати весь код сили пружності в основній функції draw(). Але подумавши про те, що у вас може бути кілька тягарців і пружинних з’єднань, буде розумно створити додатковий клас Spring. Як показано на малюнку 3.17, клас Bob відстежує рухи тягарця, а клас Spring відстежує положення якоря пружини, її довжину в стані спокою та розраховує силу пружності для тягарця.

Малюнок 3.17: Клас Spring має якір і довжину спокою. Клас Bob має положення, швидкість і прискорення
Малюнок 3.17: Клас Spring має якір і довжину спокою. Клас Bob має положення, швидкість і прискорення

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

let bob;

let spring;

Додавання змінної для посилання на об’єкт Spring.


function setup() {

  bob = new Bob();

  spring = new Spring();

}


function draw()  {

  let gravity = createVector(0, 1);

  bob.applyForce(gravity);

  spring.connect(bob);

Ця нова функція у класі Spring подбає про обчислення сили пружності тягарця.

  bob.update();

  bob.show();

  spring.show();

}

Подумайте про те, як це можна порівняти з моїм першим підходом з гравітаційним тяжінням у прикладі 2.6, коли я мав окремі класи Mover і Attractor. Там я написав щось на зразок:

  let force = attractor.attract(mover);

  mover.applyForce(force);

Аналогічна ситуація з пружиною могла бути такою:

  let force = spring.connect(bob);

  bob.applyForce(force);

Натомість у цьому прикладі я маю:

  spring.connect(bob);

Що це дає? Чому мені не потрібно викликати applyForce() на об’єкті тягарця? Відповідь, звісно ж, полягає в тому, що мені потрібно викликати applyForce() на об’єкті тягарця. Просто замість того, щоб робити це у функції draw(), я демонструю, що цілком розумною (і іноді кращою) альтернативою є попросити метод connect() викликати applyForce() всередині:

  connect(bob) {

    let force = якісь хитрі обчислення

    bob.applyForce(force);

Метод connect() піклується про виклик applyForce() і тому йому не потрібно повертати вектор у область виклику.

  }

Навіщо робити це по-одному з класом Attractor, а по-іншому з класом Spring? Коли я вперше обговорював сили, було трохи зрозуміліше показати все їх застосування у функції draw() і, сподіваюся, це допомогло вам зрозуміти ідею акумуляції сили. Тепер, коли все стало зрозуміліше, можливо, простіше вбудувати деякі деталі у самі об’єкти.

Розглянемо решту елементів у класі Spring.

class Spring {

  constructor(x, y, length) {

Конструктор ініціалізує якірну точку та довжину у стані спокою.

    this.anchor = createVector(x, y);

Положення для закріплення пружини (якір).

    this.restLength = length;
    this.k = 0.2;

Змінна для довжини у спокої та константа жорсткості.

  }


  connect(bob) {

Обчислення сили пружини на основі закону Гука.

    let force = p5.Vector.sub(bob.position, this.anchor);

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

    let currentLength = force.mag();
    let stretch = currentLength - this.restLength;

Обчислення різниці між поточною відстанню і довжиною спокою. Щоб бути більш описовим, я буду використовувати назву змінної stretch замість x.

    force.setMag(-1 * this.k * stretch);

Напрямок і величина разом!

    bob.applyForce(force);

Виклик методу applyForce() усередині методу connect!

  }


  show() {
    fill(127);
    circle(this.anchor.x, this.anchor.y, 10);
  }

Малювання якірної точки.


  showLine(bob) {
    stroke(0);

Малювання пружинного з’єднання між позицією тягарця і якоря.

    line(bob.position.x, bob.position.y, this.anchor.x, this.anchor.y);
  }

}

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

Вправа 3.13

Перш ніж переглядати приклад онлайн, подивіться на цей метод constrainLength і спробуйте заповнити пропуски:

constrainLength(bob, minlen, maxlen) {

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

Вектор, що вказує від тягарця до якоря.

  let length = direction.mag();

  if (length < minlen) {

Чи не занадто коротка довжина?

    direction.setMag(minlen);

    bob.position = p5.Vector.add(this.anchor, direction);

Утримання положення в межах обмеження.

    bob.velocity.mult(0);

  } else if (length > maxlen) {

Чи довжина не занадто довга?

    direction.setMag(maxlen);

    bob.position = p5.Vector.add(this.anchor, direction);

Утримання положення в межах обмеження.

    bob.velocity.mult(0);

  }

}

Вправа 3.14

Створіть систему з кількох тягарців і пружинних з’єднань. Як щодо з’єднання тягарця з іншим тягарцем без фіксованого якоря?

Маятник

Малюнок 3.18: Маятник з точкою опори, плечем і підвісом
Малюнок 3.18: Маятник з точкою опори, плечем і підвісом

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

Маятник — це тягарець, підвішений на так званому плечі протягнутому від опори (те, що раніше називалося якорем пружини). У стані спокою маятник висить прямо вниз, як показано на малюнку 3.18. Однак якщо підняти маятник під кутом відносно його стану спокою і відпустити, він почне гойдатися вперед-назад, малюючи форму дуги. У реальному світі маятник існував би у 3D просторі, але я збираюся розглянути простіший сценарій: маятник у 2D просторі полотна p5.js. Малюнок 3.19 зображує маятник у положенні без спокою і додає сили, що діють: гравітацію та розтяг.

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

Малюнок 3.19: Маятник, який показує \theta як кут відносно його положення у стані спокою
Малюнок 3.19: Маятник, який показує θ\theta як кут відносно його положення у стані спокою

Замість використання лінійного прискорення і швидкості я збираюся описати рух маятника в термінах кутового прискорення та кутової швидкості, які відносяться до зміни кута плеча θ\theta відносно положення маятника у стані спокою. Я маю попередити вас, особливо якщо ви досвідчений фізик, що для зручності я збираюся проігнорувати тут кілька важливих концепцій: збереження енергії, імпульс, доцентрову силу і деякі інші. Тут немає мети зробити вичерпний опис фізики маятника. Моя мета — надати вам можливість відпрацювати ваші нові навички в тригонометрії й глибше дослідити взаємозв’язок між силами та кутами на конкретному прикладі.

Щоб обчислити кутове прискорення маятника, я використаю другий закон руху Ньютона, але з невеликою тригонометричною участю. Погляньте на малюнок 3.19 і нахиліть голову так, щоб плече маятника стало вертикальною віссю. Сила тяжіння раптом спрямовується навкіс, трохи ліворуч, під кутом до вашої нахиленої голови. Якщо у вас починає боліти шия, не хвилюйтеся, я перемалюю зображення під нахилом і позначу силу FgF_g для гравітації та силу TT для натягу (малюнок 3.20, ліворуч).

Давайте тепер візьмемо силу тяжіння і розділимо її вектор на xx і yy компоненти, де плече є новою віссю ординат. Ці компоненти утворюють прямокутний трикутник із силою тяжіння у вигляді гіпотенузи (малюнок 3.20, справа). Я називатиму їх FgxF_{gx} і FgyF_{gy}, але що означають ці компоненти? Отже, компонент FgyF_{gy} представляє силу, протилежну до TT, сили розтягу. Пам’ятайте, що сила розтягу утримує тягарець від падіння.

Інший компонент, FgxF_{gx}, перпендикулярний до плеча маятника, і це та сила, яку я весь час шукав! Вона призводить до руху маятника. При коливанні маятника, вісь yy (тобто плече) завжди буде перпендикулярно до напрямку руху. Тому я можу ігнорувати сили розтягу і FgyF_{gy} та зосередитися на FgxF_{gx}, яка є сукупною силою у напрямку руху. І оскільки ця сила є частиною прямокутного трикутника, я можу обчислити її за допомогою... правильно, тригонометрії!

Малюнок 3.20: Ліворуч маятник повернутий так, що плече стає віссю y. Справа показано силу F_g збільшену і розділену на компоненти F_{gx} та F_{gy}
Малюнок 3.20: Ліворуч маятник повернутий так, що плече стає віссю yy. Справа показано силу FgF_g збільшену і розділену на компоненти FgxF_{gx} та FgyF_{gy}

Ключовим тут є те, що верхній кут прямокутного трикутника такий самий, що і кут θ\theta між плечем маятника та його положенням у спокої. Так само як було продемонстровано в обговоренні полярних координат, функції синуса та косинуса дозволяють мені відокремити компоненти сили тяжіння (гіпотенузу) відповідно до цього кута. Для FgxF_{gx} мені потрібно використовувати синус:

sin(θ)=Fgx/Fg\sin(\theta) = F_{gx} / F_g

Розв’язуючи FgxF_{gx}, я отримую таке рівняння:

Fgx=Fg×sin(θ)F_{gx} = F_g \times \sin(\theta)

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

Ось воно. Сукупна сила маятника, яка спричиняє обертання, обчислюється наступним чином:

Fp=Fg×sin(θ)F_p = F_g \times \sin(\theta)
Малюнок 3.21: F_{gx} тепер позначено через F_p, сумарної сили в напрямку руху
Малюнок 3.21: FgxF_{gx} тепер позначено через FpF_p, сумарної сили в напрямку руху

Щоб ви не забули, моя ціль — визначити кутове прискорення маятника. Коли я отримаю його, то зможу застосувати правила руху, щоб знайти новий кут θ\theta для кожного кадру анімації:

кутова швидкість=кутова швидкість+кутове прискорення\text{кутова швидкість} = \text{кутова швидкість} + \text{кутове прискорення}
кут=кут + кутова швидкість\text{кут} = \text{кут + кутова швидкість}

Добра новина в тому, що другий закон Ньютона встановлює відношення між силою та прискоренням, а саме F=M×AF = M \times A, або A=F/MA = F / M. Отже, якщо сила маятника дорівнює силі тяжіння помноженій на синус кута, тоді я матиму наступне:

кутове прискорення маятника=прискорення від гравітації×sin(θ)\text{кутове прискорення маятника} = \text{прискорення від гравітації} \times \sin(\theta)

Це слушний час, щоб нагадати, що контекст тут — це творче програмування, а не чиста фізика. Так, прискорення від гравітації на Землі становить 9.8 метрів на секунду у квадраті. Але це число не має значення тут, у світі пікселів. Замість цього я використовуватиму довільну константу (названу gravity) як змінну, яка масштабує прискорення. (До речі, кутове прискорення зазвичай записують символом альфа — α\alpha, щоб відрізнити його від лінійного прискорення AA):

α=gravity×sin(θ)\alpha = \text{gravity} \times \sin(\theta)

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

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

Крутний момент (або τ\tau, читається як тау) є мірою обертальної сили, що діє на об’єкт. У випадку маятника крутний момент пропорційний як масі підвісу, так і довжині плеча (M×rM \times r). Момент інерції (або II) маятника є мірою того, наскільки важко обертати маятник навколо точки опори. Він пропорційний масі підвісу і квадрату довжини плеча (Mr2Mr^2).

Пам’ятаєте другий закон Ньютона: F=M×AF=M \times A? Що ж, він має свій обертальний аналог: τ=I×α\tau = I \times \alpha. Переписавши рівняння для визначення кутового прискорення α\alpha, я отримав α=τ/I\alpha = \tau/I. Подальше спрощення дає Mr/Mr2Mr/Mr^2 або 1/r1/r. Кутове прискорення не залежить від маси маятника!

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

α=gravity×sin(θ)r\alpha = \frac{\text{gravity} \times \sin(\theta)}{r}

Чудово! Зрештою, формула настільки проста, що ви можете запитатися, навіщо я взагалі витрачав час на пояснення. Навчання — це звісно чудово, але я міг би просто сказати: “Отже, кутове прискорення маятника — це деяка константа, помножена на синус кута і поділена на довжину плеча”. Це було б втратою суті. Мета цієї книги не в тому, щоб дізнатися, як коливаються маятники чи працює гравітація. Суть полягає в тому, щоб творчо мислити про те, як фігури можуть рухатися по екрану в графічній системі на основі обчислень. Маятник — це лише приклад. Якщо ви можете зрозуміти підхід до програмування маятника, тоді ви можете застосувати ті самі методи до інших сценаріїв, незалежно від того, як ви вирішите спланувати свій світ у p5.js.

Але я ще не закінчив. Я можу бути задоволений своєю простою елегантною формулою для кутового прискорення, але мені все ще потрібно застосувати її в коді. Це чудова нагода попрактикувати навички ООП та створити клас Pendulum. Спочатку подумайте про всі властивості маятника, які я згадав:

  • Довжина плеча
  • Кут
  • Кутова швидкість
  • Кутове прискорення

Клас Pendulum також вимагає всі ці властивості:

class Pendulum  {

  constructor() {

    this.r = ????;

Довжина плеча.

    this.angle = ????;

Кут плеча маятника.

    this.angleVelocity = ????;

Кутова швидкість.

    this.angleAcceleration = ????;

Кутове прискорення.

  } 

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

  update() {

    let gravity = 0.4;

Довільна константа.

    this.angleAcceleration = -1 * gravity * sin(this.angle) / this.r;

Обчислення прискорення згідно формули.

    this.angleVelocity += this.angleAcceleration;

Збільшення швидкості.

    this.angle += this.angleVelocity;

Збільшення кута.

  }

Зауважте, що розрахунок прискорення тепер включає множення на -1. Коли маятник знаходиться праворуч від свого положення спокою, кут додатний, і тому синус кута також додатний. Однак сила тяжіння має “тягнути” підвіс назад до положення спокою. І навпаки, коли маятник знаходиться ліворуч від свого положення спокою, кут є від’ємним, а тому його синус також від’ємний. У цьому випадку сила тяги має бути позитивною. Множення на -1 необхідне в обох випадках.

Далі мені потрібен метод show(), щоб намалювати маятник на полотні. Але де саме я маю його намалювати? Як розрахувати декартові xyxy-координати для точки оберту маятника (назвемо її pivot) і положення підвісу (назвемо його bob)? Це може стати трохи надокучливим, але відповідь, знову ж таки, — тригонометрія, як показано на малюнку 3.22.

Ма�люнок 3.22: Діаграма, що показує положення підвісу відносно опори в полярних і декартових координатах
Малюнок 3.22: Діаграма, що показує положення підвісу відносно опори в полярних і декартових координатах

Спершу мені потрібно додати властивість this.pivot до конструктора, щоб вказати, де малювати маятник на полотні:

this.pivot = createVector(100, 10);

Я знаю, що підвіс повинен бути на певній відстані від опори, що визначається довжиною плеча. Це моя змінна r, яку я зараз і встановлю:

this.r = 125;

Я також знаю поточний кут підвісу відносно опори: він зберігається у змінній angle. Між довжиною плеча і кутом у мене є полярні координати підвісу: (r,θ)(r,\theta). Але мені насправді потрібні декартові координати й на щастя я вже знаю, як за допомоги синуса і косинуса перетворити полярні координати у декартові:

this.bob = createVector(r * sin(this.angle), r * cos(this.angle));

Зверніть увагу, що я використовую sin(this.angle) для значення xx і cos(this.angle) для yy. Це протилежне тому, що я показав вам у “Полярних та прямокутних координатах” раніше в цьому розділі. Причина в тому, що тепер я шукаю верхній кут прямокутного трикутника, спрямованого вниз, як показано на малюнку 3.18. Цей кут лежить між віссю yy і гіпотенузою, а не між віссю xx і гіпотенузою, як ви бачили раніше на малюнку 3.9.

Зараз значення this.bob припускає, що опорна точка знаходиться в точці (0,0)(0, 0). Щоб отримати позицію підвісу відносно місця опорної точки, де б вона не була, я можу просто додати вектор pivot до вектора bob:

this.bob.add(this.pivot);

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

stroke(0);

fill(127);

line(this.pivot.x, this.pivot.y, this.bob.x, this.bob.y);

circle(this.bob.x, this.bob.y, 16);

Зрештою, реальний маятник буде зазнавати певного тертя (у точці повороту) і опору повітря. У нинішньому вигляді, з даним кодом, маятник коливатиметься вічно. Щоб зробити його більш реалістичним, я можу сповільнити коливання маятника за допомогою трюку "згасання". Я кажу трюк, оскільки замість того, щоб моделювати сили опору з певною точністю (як я робив у Розділі 2), можна досягти подібного результату, просто зменшуючи кутову швидкість на деяку довільну величину під час кожного кадру анімації. Наступний код зменшує швидкість на 1 відсоток (або помножує її на 0.99) під час кожного кадру анімації:

this.angleVelocity *= 0.99;

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

let pendulum;


function setup() {

  createCanvas(640, 240);

  pendulum = new Pendulum(width / 2, 0, 175);

Створення нового об’єкта Pendulum із заданим положенням і довжиною плеча.

}


function draw() {

  background(255);

  pendulum.update();

  pendulum.show();

}


class Pendulum  {

  constructor(x, y, r) {

    this.pivot = createVector(x, y); // Положення точки опори
    this.bob = createVector();       // Положення підвісу
    this.r = r;                      // Довжина плеча
    this.angle = PI / 4;             // Кут плеча
    this.angleVelocity = 0;          // Кутова швидкість
    this.angleAcceleration = 0;      // Кутове прискорення
    this.damping = 0.99;             // Довільне згасання
    this.ballr = 24;                 // Довільний радіус підвісу

Багато змінних для відстеження різних властивостей маятника.

  }


  update() {

    let gravity = 0.4;

    this.angleAcceleration = (-1 * gravity / this.r) * sin(this.angle);

Формула кутового прискорення.

    this.angleVelocity += this.angleAcceleration;
    this.angle += this.angleVelocity;

Стандартний алгоритм кутового руху.

    this.angleVelocity *= this.damping;

Застосування згасання.

  }


  show() {

    this.bob.set(this.r * sin(this.angle), this.r * cos(this.angle));
    this.bob.add(this.pivot);

Перетворення полярних координат у декартові. Замість того, щоб щоразу створювати новий вектор, я використовую метод set(), щоб оновити його значення і відповідно положення підвісу.

    stroke(0);
    line(this.pivot.x, this.pivot.y, this.bob.x, this.bob.y);

Плече.

    fill(127);
    circle(this.bob.x, this.bob.y, this.ballr * 2);

Підвіс.

  }

}

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

Вправа 3.15

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

Вправа 3.16

Використовуючи тригонометрію, як ви обчислите величину нормальної сили зображеної на цьому малюнку (сили, перпендикулярної до схилу, на який спираються сани)? Ви можете вважати величину FgravityF_\text{gravity} відомою константою. Для початку знайдіть прямокутний трикутник. Зрештою, нормальна сила є рівно протилежною компоненту сили тяжіння. Якщо це допоможе, намалюйте діаграму і створіть більше прямокутних трикутників.

Вправа 3.17

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

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

Візьміть одне зі своїх створінь і включіть у його рух коливання. Як модель можна використати клас Oscillator з прикладу 3.7. Однак об’єкт типу Oscillator коливався навколо однієї точки (середини вікна). Спробуйте зробити коливання навколо рухомої точки.

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