Розділ 3. Коливання
Тригонометрія — це синус часу.
— Анонім
Бріджит Райлі, відома британська художниця, була рушійною силою руху оп-арту 1960-х років. Її робота містить геометричні візерунки, які кидають виклик сприйняттю глядача та викликають відчуття руху чи вібрації. Її твір “Гала” 1974 року демонструє серію криволінійних форм, які хвилюються по полотну, викликаючи природний ритм син усоїди.
У Розділі 1 і Розділі 2 я ретельно відпрацював об’єктно-орієнтовану структуру для анімації фігури на полотні p5.js, використовуючи концепцію вектора для представлення положення, швидкості та прискорення, викликаних силами середовища. Я міг би відразу перейти до таких тем, як системи частинок, керувальні сили, групова поведінка тощо. Однак це означало б пропустити фундаментальний аспект руху в природному світі: коливання (осциляція) або рух об’єкта вперед і назад навколо центральної точки або положення.
Для моделювання коливань, вам потрібно невелике розуміння тригонометрії, математики трикутників. Вивчення деяких тригонометричних можливостей надасть вам нові інструменти для генерації моделей і створення нових поведінок руху в програмах p5.js. Ви навчитесь використовувати кутову швидкість і прискорення для обертання об’єктів під час їхнього руху. Ви зможете використовувати функції синуса та косинуса для моделювання приємних плавних рухів із прискоренням чи гальмуванням. Ви також навчитеся розраховувати складніші си ли у ситуаціях де потрібно враховувати кути, наприклад, коливання маятника або рух коробки по схилу.
Я розпочну розділ з основ роботи із кутами в p5.js, а потім розгляну кілька аспектів тригонометрії. Наприкінець я зв’яжу тригонометрію з тим, що ви дізналися про сили у Розділі 2. Зміст цього розділу прокладе шлях для складніших прикладів цієї книги, які потребують тригонометрії.
Кути
Перш ніж продовжувати, мені потрібно переконатися, що ви розумієте, як концепція кута підходить до творчого кодування у p5.js. Якщо у вас є досвід роботи з p5.js, ви безсумнівно стикалися з цим питанням під час використання функції rotate()
для обертання та розкручування об’єктів. Швидше за все, ви знайомі з поняттям кута, що вимірюється у градусах (див. малюнок 3.1).
Повний оберт відбувається від 0 до 360 градусів, а 90 градусів (прямий кут) становить одну четверту від 360, що зображено на малюнку 3.1 двома перпендикулярними лініями.
У комп’ютерній графіці кути загалом використовуються для вказ івки повороту фігури. Наприклад, квадрат на малюнку 3.2 повернуто на 45 градусів навколо свого центру.
Заковика в тому, що p5.js за замовчуванням вимірює кути не в градусах, а у радіанах. Ця альтернативна одиниця вимірювання визначається відношенням довжини дуги кола (сегмента окружності кола) до радіуса цього кола. Один радіан — це кут, при якому це відношення дорівнює одиниці (див. малюнок 3.3). Повне коло (360 градусів) еквівалентно радіанам, 180 градусів еквівалентно радіанам, а 90 градусів еквівалентно радіанам.
Формула для переведення градусів у радіани:
На щастя, якщо ви віддаєте перевагу кутам у градусах, то можете ввімкнути відповідний режим, викликавши функцію angleMode(DEGREES)
, або використовувати зручну функцію radians()
для перетворення значень з градусів у радіани. Також доступні константи PI
, TWO_PI
і HALF_PI
(еквіваленти 180, 360 і 90 градусам відповідно). Наприклад, ось два способи повернути фігуру на 60 градусів у p5.js:
let angle = 60;
rotate(radians(angle));
angleMode(DEGREES);
rotate(angle);
Що таке Пі?
Математична константа пі, яку позначають грецькою літерою , — це дійсне число, яке визначається як відношення довжини кола (або периметру кола) до його діаметра (прямої лінії, що проходить через центр кола і сполучає дві його точки). Воно дорівнює приблизно 3.14159 і доступне у p5.js через вбудовану константу PI
.
Хоча градуси можуть бути корисними, у цій книзі я буду працювати з радіанами, оскільки вони є стандартною одиницею вимірювання в багатьох мовах програмування і графічних середовищах. Якщо радіани для вас новинка, це гарна нагода для практики! Крім того, якщо ви не знайомі з тим, як обертання реалізовано в p5.js, я рекомендую переглянути мою відеосерію на Coding Train про трансформації у p5.js.
Вправа 3.1
Налаштуйте обертання об’єкта, схожого на кийок, навколо його центру за допомогою функцій translate()
і rotate()
.
Кутовий рух
Іншим терміном для обертання є кутовий рух, тобто рух навколо кута. Подібно до того, як лінійний рух можна описати за допомогою швидкості — частоти, з якою змінюється положення об’єкта у часі — кутовий рух можна описати за допомогою кутової швидкості, як частоти, з якою змінюється кут об’єкта у часі. Загалом, кутове прискорення описує зміни кутової швидкості об’єкта.
На щастя, ви вже знаєте всі математичні аспекти, які потрібні для розуміння кутового руху. Пам’ятаєте матеріал, пояснення якому я майже повністю присвятив Розділ 1 і Розділ 2?
Ви можете застосувати ту саму логіку до об’єкта, що обертається:
Насправді ці формули кутового руху простіші, ніж їхні еквіваленти лінійного руху, оскільки кут тут є скалярною величиною (одним числом), а не вектором! Це тому, що у 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.
Візьміть один із непрямих кутів прямокутного трикутника. Прилегла (adjacent) сторона — та, яка дотикається до цього кута, протилежна (opposite) — та, яка не дотикається цього кута, і гіпотенуза (hypotenuse) — сторона, протилежна прямому куту. Sohcahtoa розказує, як обчислити тригонометричні функції кута на основі довжин цих сторін. Це мнемонічне скорочення побудоване з перших літер англійських слів як показано нижче:
- soh:
- cah:
- toa:
Ще раз погляньте на малюнок 3.4. Вам не потрібно це запам’ятовувати, але погляньте чи вам це зрозуміло. Спробуйте намалювати його самостійно. Далі розглянемо це трохи по-іншому (див. малюнок 3.5).
Бачите, як із вектора створюється прямокутний трикутник? Сама стрілка вектора є гіпотенузою, а компоненти вектора ( і ) — сторонами трикутника. Кут є додатковим способом визначення напрямку (або курсу). Тригонометричні функції, розглянуті у такому вигляді, проявляють зв’язок між компонентами вектора і його напрямком з магнітудою. Таким чином, тригонометрія буде дуже корисною в цій книзі. Щоб проілюструвати це, розглянемо приклад, для якого потрібна функція тангенса.
Спрямованість у напрямку руху
Згадайте весь шлях до прикладу 1.10, у якому показ ано, як об’єкт Mover
прискорюється в напрямку курсора (малюнок 3.6).
Ви можете помітити, що майже всі фігури, які я малював, були круглими. Це зручно з кількох причин, одна з яких полягає в тому, що це дозволило мені уникнути питання з обертанням. Оберніть круг і він виглядатиме точно так само. Однак, у житті всіх моушн-програмістів настає час, коли вони хочуть переміщати на екрані щось, що має іншу форму аніж круг. Можливо, це мураха, або машина, або космічний корабель. Щоб виглядати реалістично, положення цього об’єкту має бути спрямованим у напрямку свого руху.
Коли я говорю “спрямування у напрямку руху”, то насправді маю на увазі “поворот відповідно до вектора швидкості”. Швидкість — це вектор з і компонентами, але для обертання у p5.js потрібне одне число — кут. Давайте ще раз подивимося на тригонометричну діаграму, цього разу зосередившись на векторі швидкості об’єкта (малюнок 3.7).
Векторні компоненти і пов’язані з його кутом через функцію тангенса. Використовуючи toa з sohcahtoa, я можу записати це відношення наступним чином:
Проблема в тому, що хоча я знаю і компоненти вектора швидкості, мені насправді не відомий кут його напрямку. Мені потрібно визначити цей кут. Ось тут з’являється інша функція, відома як обернений тангенс або арктангенс (скорочений запис arctan або atan). Існують також функції оберненого синуса та оберненого косинуса, які називаються відповідно арксинусом (arcsin) та арккосинусом (arccos).
Якщо тангенс деякого значення дорівнює деякому значенню , тоді обернений тангенс дорівнює . Наприклад:
Якщо | |
Тоді |
Бачите, як одне є оберненим до іншого? Це дозволяє мені дізнатися кут вектора:
Якщо | |
Тоді |
Тепер, коли у мене є формула, подивимось де вона має бути у методі 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.
Хоча зовні вони схожі, обидва вектори вказують у зовсім різних напрямках — протилежних напрямках! Не зважаючи на це, подивіться, що станеться, якщо я застосую формулу оберненого тангенса для визначення кута кожного із цих векторів:
Я отримую такий самий кут! Однак це не може бути правильним, оскільки вектори направлені у протилежні напрямки. Виявляється, що це досить поширена проблема в комп’ютерній графіці. Я міг би використовувати функцію 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, вам потр ібно вказати її піксельне положення, і координати. Ці координати відомі як декартові координати, названі на честь Рене Декарта, французького математика, який розвинув ідеї, що лежать в основі прямокутного простору.
Інша корисна система координат — полярні координати — описує точку в просторі як відстань від початку координат (наприклад, радіус кола) і відповідний кут (зазвичай позначається грецькою буквою тета ). Мислячи в термінах векторів, декартові координати описують векторні компоненти та , тоді як полярні координати описують векторну магнітуду (величину, довжину) і її напрямок (кут).
Працюючи в p5.js, вам може бути зручніше мислити в полярних координатах, особливо для створення програм, які включають обертальні або кругові рухи. Однак функції малювання у p5.js розуміють лише декартові -координати. На щастя для вас, тригонометрія має ключ для перетворення між полярними та декартовими координатами (див. малюнок 3.8). Це дозволяє вам займатись проєктуванням з будь-якою системою координат, яка вам зручніша, при цьому завжди малюючи з використанням декартових координат.
Наприклад, якщо задані полярні координати із радіусом у 75 пікселів і кутом () у 45 градусів (або радіан), то декартові та можна обчислити наступним чином:
Для обчислення синуса і косинуса у 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, де .
Результат синус-функції — це гладенька крива, верхівка якої почергово переходить від до , також відома під назвою синусоїдальна хвиля або синусоїда. Така поведінка, з періодичним рухом між двома точками, є коливанням (осциляцією), про яке я згадував на початку розділу. Вібрація гітарної струни, гойдання маятника, стрибання на пого-палиці — усе це приклади коливального руху, які можна змоделювати за допомогою синус-функції.
У програмі p5.js ви можете симулювати коливання, присвоюючи результат синус-функції для положення об’єкта. Я почну з базового сценарію: я хочу, щоб кулька коливалася між лівою та правою сторонами полотна (малюнок 3.11).
Ця модель коливання вперед і назад навколо центральної точки відома як гармонічне коливання (або, щоб сказати більш вишукано, періодична синусоїдальна осциляція об’єкта). Код для досягнення цього надзвичайно простий, але перш ніж приступити до нього, я хотів би представити деякі ключові терміни, пов’язані з коливаннями (і хвилями).
Коли рухомий об’єкт демонструє простий гармонічний рух, його положення (у цьому випадку -позицію) можна виразити як функцію часу з наступними двома елементами:
- Амплітуда — відстань від центру руху до будь-якого крайнього положення
- Період — тривалість (час) одного повного циклу руху
Щоб зрозуміти ці терміни, ще раз подивіться на графік синус-функції на малюнку 3.10. Крива ніколи не підіймається вище 1 або нижче -1 вздовж осі , тому функція синуса має амплітуду 1. Водночас хвилеподібний патерн кривої повторюється кожні одиниці вздовж осі , тому період функції синуса дорівнює . (За домовленістю одиницями вимірювання тут є радіани, оскільки вхідним значенням для функції синуса зазвичай є кут у радіанах.)
Ми сказали чимало про амплітуду та період абстрактної функції синуса, але що таке амплітуда та період у світі p5.js на прикладі кульки, що коливається? Що ж, амплітуду можна досить легко виміряти в пікселях. Наприклад, якщо полотно має ширину 200 пікселів, я можу вибрати коливання навколо центру полотна, переміщуючись від 100 пікселів праворуч від центру до 100 пікселів ліворуч від центру. Іншими словами, амплітуда становитиме 100 пікселів:
let amplitude = 100;
Амплітуда виміряна в пікселях.
Період — це кількість часу для одного повного циклу осциляції. Однак що означає час у програмі p5.js? Теоретично я міг би сказати, що хочу, щоб кулька коливалася кожні три секунди, а потім придумати складний алгоритм для руху об’єкта відповідно до реального часу, використовуючи функцію millis()
для відстеження пройдених мілісекунд. Однак для того, що я намагаюсь тут досягти, реальний час не обов’язковий. Більш корисною мірою часу у p5.js є кількість кадрів яка минула, що доступна через вбудовану змінну frameCount
. Чи хочу я, щоб коливальний рух повторювався кожні 30 кадрів? Кожні 50 кадрів? Як щодо періоду у 120 кадрів:
let period = 120;
Період вимірюваний кадрами (одиницями часу для анімації).
Коли я маю амплітуду і період, настав час написати формулу для обчислення -позиції кола як функції часу (тут це поточна кількість кадрів):
let x = amplitude * sin(TWO_PI * frameCount / period);
amplitude та period — це мої власні змінні, frameCount — вбудована у p5.js.
Подумайте про те, що тут відбувається. По-перше, будь-яке значення, яке повертає функція sin()
множиться на amplitude
. Як ви бачили на малюнку 3.10, результат функції синуса коливається між -1 і 1. Помноження цього значення на обрану мною амплітуду — назвемо її — дає мені бажаний результат: значення, яке коливається між - та . (Це також місце, де ви можете використовувати функцію map()
для відображення результатів функції sin()
у власний діапазон.)
Тепер подумайте про те, що знаходиться всередині функції sin()
:
TWO_PI * frameCount / period
Що тут відбувається? Почнемо з того, що ви знаєте. Я пояснив, що синус має період , тобто починається з і повторюється при , , й так далі. Якщо мій бажаний період коливань складає 120 кадрів, тоді я хочу, щоб кулька була у тому самому положенні, коли frameCount
становитиме 120 кадрів, 240 кадрів, 360 кадрів і так далі. Тут frameCount
— це єдина змінна, яка змінюється з часом. Подивіться, які результати дає формула при збільшенні frameCount
:
frameCount | frameCount / period | TWO_PI * frameCount / period |
---|---|---|
0 | 0 | 0 |
60 | 0.5 | |
120 | 1 | |
240 | 2 | |
. . . | . . . | . . . |
Ділення 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()
це якесь значення, яке збільшується досить повільно, щоб рух об’єкта виглядав плавним від одного кадру до наступного. Щоразу, коли це значення ставатиме кратним , об’єкт завершуватиме один цикл осциляції.
Ця техніка віддзеркалює те, що я зробив із шумом Перліна у Розділі 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
на . Оскільки величина збільшення angle
контролюється кутовою швидкістю, я можу обчислити період таким чином:
Щоб проілюструвати потужність мислення про коливання у термінах кутової швидкості, я трохи розширю приклад, створивши клас Oscillator
, об’єкти якого можуть коливатися незалежно як вздовж вісі (як і раніше), так і вздовж вісі . У класі потрібні два кути, дві кутові швидкості та дві амплітуди (по одній для кожної вісі).
Це чудова нагода використати 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
, може бути корисно зосередитися в анімації на русі одного осцилятора. Спочатку спостерігайте за його горизонтальним рухом. Ви помітите, що він регулярно коливається вперед і назад уздовж вісі . Перемкнувши вашу увагу на його вертикальний рух, ви побачите, як він коливається вгору-вниз уздовж вісі . Кожен осцилятор має свій власний чіткий ритм, зумовлений випадковою ініціалізацією його кута, кутової швидкості й амплітуди.
Головне зрозуміти, що x
і y
властивості об’єктів типу p5.Vector
, що наразі містяться у this.angle
, this.angleVelocity
і this.amplitude
більше не пов’язані з просторовими векторами. Натомість вони використовуються для збереження відповідних властивостей для двох окремих коливань (одного вздовж вісі , іншого вздовж вісі ). Зрештою, ці коливання проявляються просторово, коли x
і y
обчислюються в методі show()
, відображаючи коливання на положення об’єкта.
Вправа 3.8
Спробуйте ініціалізувати кожен об’єкт Oscillator
швидкістю й амплітудою, які не будуть випадковими, щоб створити якийсь регулярний патерн. Чи можете ви зробити так, щоб осцилятори виглядали як ноги комахоподібної істоти?
Вправа 3.9
Додайте у об’єкт Oscillator
кутове прискорення.
Хвилі
Уявіть один круг, який коливається вгору і вниз відповідно до функції синуса. Це схоже на попередню симуляцію точки вздовж -вісі синусоїди. З невеликою вигадкою та циклом for
, ви можете анімувати всю хвилю, розмістивши цілу серію цих кругів один біля одного (малюнок 3.12).
Ви можете використовувати цей хвилястий шаблон для проєктування тіла чи кінцівок істоти або для імітації м’якої поверхні, такої як вода. Розглянемо, як працює код для цієї програми.
Тут застосовуються ті самі концепції амплітуди (висоти хвилі) і періоду (тривалості хвилі). Однак при малюванні всієї хвилі термін період змінює своє значення з представлення часу на опис ширини (у пікселях) повного періоду хвилі. Терміном для просторового періоду (на відміну від часового періоду) хвилі є довжина хвилі — відстань, яка потрібна хвилі для завершення одного повного циклу коливань. І так само як і в попередньому прикладі коливань, ви можете вибрати обчислення хвилеподібного патерну відповідно до точної довжини хвилі або довільно збільшити значення кута (дельта кута) для кожної точки у хвилі.
Я виберу простіший варіант зі збільшенням кута. Я знаю, що мені потрібні три змінні: кут, дельта кута (аналогічний попередній кутовій швидкості) і амплітуда:
let angle = 0;
let deltaAngle = 0.2;
let amplitude = 100;
Потім я збираюся обійти всі x
-значення кожної точки на хвилі. Наразі я додаватиму по 24 пікселі між сусідніми x
-значеннями. Для кожного x
я виконаю наступні три кроки:
- Обчислення -позиції відповідно до амплітуди й синуса кута.
- Малювання круга у точці .
- Збільшення кута відповідно до дельти кута.
У наступному прикладі ці кроки переведено у код.
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.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.7 пропонувала вам використати підхід із функцією sin()
, щоб створити симуляцію з тягарцем, що висить на пружині. Однак таке швидке й брудне рішення з одним рядком коду не підійде, якщо ви дійсно хочете, щоб тягарець, що висить на пружині, реагував на інші сили свого середовища (вітер, гравітацію тощо). Щоб досягти такої симуляції, вам потрібно змоделювати силу пружності за допомогою векторів.
Я розглядатиму пружину як з’єднання між рухомим тягарцем і нерухомою опорною точкою, інакше кажучи, опорою чи якорем (див. малюнок 3.14).
Сила пружності — це вектор, обчислений згідно із законом Гука, названим на честь Роберта Гука, британського фізика, який розробив формулу в 1660 році. Гук спочатку виклав закон латинською мовою: “Ut tensio, sic vis” або “Який розтяг, така й сила”. Подумайте про це наступним чином:
Сила пружності прямо пропорційна розтягу пружини.
Розтяг є мірою того, наскільки пружина була розтягнута або стиснута: як показано на малюнку 3.15, це різниця між поточною довжиною пружини та її довжиною в стані спокою (її станом рівноваги). Отже, закон Гука говорить, що якщо ви дуже тягнете за тягарець, то сила пружності буде великою, а якщо тягнете трохи, то сила буде слабкою.
Математично закон виражається так:
Тут — це коефіцієнт жорсткості. Його значення масштабує силу, встановлюючи, наскільки еластичною або жорсткою є пружина. Тим часом — це видовження, поточна довжина мінус довжина спокою.
Тепер згадайте, що сила — це вектор, тому вам потрібно обчислити і його величину і його напрямок. Кодування я почну з наступних трьох змінних: по вектору для позицій опори й тягарця та ще одна змінна для довжини пружини у спокої:
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;
Тепер, коли я розібрався зі складовими, необхідними для визначення величини сили (), мені потрібно визначити напрямок сили у вигляді одиничного вектору. Хороша новина в тому, що я вже маю цей вектор. Правильно? Мить тому я поставив запитання “Як я можу розрахувати цю відстань?” і відповів “Як щодо магнітуди вектора, який вказує від опорної точки до тягарця”? Цей вектор описує і напрямок сили!
На малюнку 3.16 показано, що якщо ви розтягнете пружину понад її довжину в стані спокою, має виникнути сила, яка буде тягнути її назад до якоря. А якщо пружину стиснути менше за її довжину спокою, то утворена сила повинна відштовхнути її від якоря. Формула закону Гука враховує цю зміну напрямку за допомоги -1.