Про підручник

Зміст підручника орієнтований на вивчення вибіркового модуля Креативне програмування в курсі інформатики для учнів старших класів (рівень стандарту) закладів загальної середньої освіти.

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

  • формування зацікавленості процесом програмування;

  • реалізація творчого потенціалу через інтеграцію програмування у власну творчу практику;

  • створення творів цифрового мистецтва за допомогою JavaScript і бібліотеки p5.js.

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

8 розділів, 3 додатки та словник термінів визначають теми, що розглядаються у підручнику.

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


Для вчителів інформатики, учнів та усіх, хто цікавиться програмуванням і хоче реалізувати власні творчі ідеї.

1. Вступ

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

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

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

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

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

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

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

Креативне програмування - це сучасного художника.

Креативне програмування - це процес, заснований на дослідженні, ітерації, рефлексії та відкритті, де код використовується як основний засіб для створення широкого спектра медіаартефактів.
— Mark C. Mitchell & Oliver Bown
Towards a creativity support tool in Processing. Understanding the needs of creative coders.

Цікавимось

Джерела знань

Основні джерела, які використовувалися у процесі створення підручника і можуть бути корисними в навчанні:

Вебресурси
  • discourse.processing.org - форум для обміну навичками, знаннями та кодом між учасниками спільноти Processing і p5.js.

  • HAppy Coding - колекція посібників із програмування та спільнота людей, схожих на вас, які вчаться програмувати. Сайт Кевіна Воркмена (Kevin Workman).

  • MDN Web Docs - документація з вебтехнологій CSS, HTML і JavaScript для веброзробників.

  • OpenProcessing - середовище креативного програмування для допитливого розуму з підтримкою JavaScript і p5.js.

  • p5js.org - JS-бібліотека для створення графічних та інтерактивних застосунків на основі принципів Processing.

  • p5.js Web Editor - вебредактор для p5.js, бібліотеки JavaScript, створений з метою зробити програмування доступним для художників, дизайнерів, викладачів і початківців.

  • Reference p5.js - довідка по функціях бібліотеки p5.js.

  • The Coding Train - YouTube-канал Деніела Шиффмана (Daniel Shiffman).

  • Welcome to Processing! - середовище програмування і мова програмування.

Книги
  • Arslan E. Learn JavaScript with p5.js. Berkeley, CA : Apress, 2018. 217 p.

  • Bunn T. Learn Python Visually: Creative Coding with Processing.py. No Starch Press, 2021. 296 p.

  • McCarthy L., Reas C., Fry B. Getting started with p5.js: Making interactive graphics in JavaScript and Processing / illus. Taeyoon Choi. Maker Media, 2015. 246 p.

  • Shiffman D. Learning Processing: A Beginner’s Guide to Programming Images, Animation, and Interaction. 2nd ed. Elsevier Science & Technology Books, 2015. 564 p.

2. Цифрове мистецтво та творчість

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

2.1. Цифрове мистецтво

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

У всі часи митці намагались удосконалити наявні інструменти для творчості чи створити нові з метою досягнення власних унікальних результатів.

Технології принесли в мистецтво усвідомлення програмного забезпечення як інструмента для створення творів мистецтва і відкрили нову область творчості - цифрове мистецтво.

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

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

Із середини 1990-х розпочалися експерименти з аплетами HTML , Shockwave , Flash та Java як із середовищами для креативності.

Справжня революція у цифровому мистецтві сталася із появою цифрових інструментів з відкритим кодом на зразок Processing .

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

Під таке визначення поняття «комп’ютерне мистецтво» добре вписуються цифровий живопис, фрактальне мистецтво, pixel art та інші різновиди цифрового мистецтва.

Переглядаємо Аналізуємо

Технології цифрового мистецтва відкривають принципово новий рівень обробки інформації та інтерактивної взаємодії людини з комп’ютером.

2.1.1. Ресурси

Корисні джерела

2.1.2. Контрольні запитання

Міркуємо Обговорюємо

  1. Поясніть сутність цифрового мистецтва.

  2. Назвіть різновиди цифрового мистецтва.

  3. Що є інструментами у цифровому мистецтві?

2.1.3. Практичні завдання

Початковий

  1. Знайти інформацію про українських митців, які реалізують свої творчі ідеї у цифровому мистецтві.

Середній

  1. На основі знайдених матеріалів про музеї цифрового мистецтва у світі й в Україні, розповісти про один із таких музеїв, створивши презентацію.

Високий

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

2.2. Дизайн та код. Генеративне мистецтво

Дизайн-код у багатьох країнах, наприклад, у Великобританії, існує давно: зведення правил, які визначають зовнішній вигляд Лондона, існують з 1666 року.

У Берліні скрупульозність дизайн-коду дійшла, навіть, до вуличної плитки: різне плиткове покриття позначає транспортні та пішохідні зони.

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

Застосування дизайн-коду в містах світу сьогодні стає поширеною практикою.

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

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

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

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

Такий підхід значно зменшує дослідницьку фазу у мистецтві та дизайні й часто приводить до нових дивовижних та витончених ідей.

Переглядаємо Аналізуємо

2.2.2. Контрольні запитання

Міркуємо Обговорюємо

  1. У чому полягає сутність дизайн-коду?

  2. Що таке «генеративне мистецтво»?

  3. Що таке «генеративні алгоритми»?

2.2.3. Практичні завдання

Початковий

  1. Підготувати повідомлення про використання дизайн-коду у містах світу й України.

Середній

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

Високий

  1. Чи дотримуються дизайн-коду у вашому місті? Створити презентацію чи вебсторінку на цю тему.

2.3. Програмування як середовище для творчості. Мова програмування

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

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

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

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

Будь-яка технологія чи мова програмування потенційно можуть бути використані для творчих цілей. Спробуємо об’єднати творчість із цікавим процесом програмування за допомогою Processing.

2.3.1. Що таке Processing?

Processing - мова програмування і середовище розробки для вивчення програмування в контексті цифрового мистецтва - створення зображень, анімації та інтерактивної графіки.

Processing, як мова програмування, використовує синтаксис на основі мови програмування Java з деякими спрощеннями.

Технічно, Processing - це надбудова над Java з великою колекцією бібліотек для графіки, відео і звуку.

Якщо розглядати Processing як середовище програмування, то Processing, крім того, що надає середовище для написання і виконання коду, дає можливість використовувати альтернативні інтерфейси для програмування іншими мовами, зокрема JavaScript і Python .

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

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

2.3.2. З історії створення Processing

Джон Маеда
Джон Маеда

Processing є нащадком проєкту Design By Numbers (DBN) , що був впливовим експериментом у контексті викладання програмування, започаткованим у медіалабораторії MIT (Массачусетський технологічний інститут) у 1990-х роках.

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

Кейсі Різ
Кейсі Різ

Design By Numbers вже не є активним проєктом, але він повпливав на багато інших проєктів, спрямованих на те, щоб зробити програмування більш доступним для нетехнічних професій.

Одним із таких проєктів, що досяг міжнародного успіху, став Processing.

Проєкт був створений Кейсі Різом та Беном Фраєм, студентами Джона Маеди. Для своєї роботи вони взяли за основу DBN.

Бен Фрай
Бен Фрай

Розробка Processing почалася офіційно навесні 2001 року, коли Кейсі та Бен обговорили питання щодо API та набору функцій, а перші кілька версій Processing (випуски 0001, 0003 та 0005) були використані в серпні 2001 року в Університеті мистецтв Мусашино в Токіо, Японія.

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

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

Переглядаємо Аналізуємо

2.3.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке Processing?

  2. Які мови програмування підтримує Processing, якщо розглядати Processing як середовище програмування?

  3. Яку мову програмування використовує онлайн-середовище OpenProcessing ?

  4. Чи є програмування творчим процесом? Обґрунтуйте свою відповідь.

2.3.5. Практичні завдання

Початковий

  1. Розглянути прототипи застосунків , створених за допомогою Processing. Ознайомитись з онлайн-середовищем openprocessing.org . Натиснути на Create a Sketch у вікні онлайн-середовища OpenProcessing і виконати запропонований код застосунку.

Середній

  1. Опрацювати на свій вибір один із покрокових посібників , запропонованих спільнотою OpenProcessing.

Високий

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

2.4. Особливості середовища розробки. Структура програмного проєкту

Processing - це мова програмування, яка базується на мові Java.

За допомогою мови програмування записують початковий код, запускають його і зневаджують у середовищі програмування.

Для Processing таким середовищем програмування є Processing IDE.

Processing IDE
Processing IDE: стабільна версія гілки 3.x - 3.5.4 (17 січня 2020 року)
Processing IDE
Processing IDE: версія гілки 4.x - 4.3 (26 липня 2023 року)

2.4.1. Processing IDE

Середовище програмування Processing IDE можна завантажити з офіційного сайту processing.org для платформ Windows, Linux і macOS.

Для встановлення Processing IDE на комп’ютер необхідно завантажити з офіційного сайту файл-архів відповідної розрядності для вашої операційної системи, розпакувати архів і запустити файл processing.exe (для Windows) або install.sh(для Linux).

Елементи вікна Processing IDE
Елементи вікна Processing IDE

В меню Файл  Приклади…​ (Ctrl+Shift+O) присутні багато прикладів проєктів, створених тою мовою програмування, режим якої зараз обраний. Якщо відкрити будь-який із цих прикладів, початковий код проєкту відкриється у новому вікні середовища Processing IDE. Проєкт можна переглянути, натиснувши на кнопку запуску.

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

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

У каталозі data каталогу проєкту можуть розміщуватися додаткові файли проєкту, наприклад, файли зображень, відео чи аудіоформатів.

При встановленні альтернативних режимів роботи у Processing (пункт Додати режим... у списку вибору режимів), відповідний пакунок файлів для роботи в цьому режимі розміщується у Windows по шляху С:\Users\user\Documents\Processing\modes\. Для Linux цей шлях буде /home/teacher/sketchbook/modes. Назви user і teacher - імена користувачів в операційних системах відповідно.

Цікавимось

Режим Java за стандартним налаштуванням

Основний файл проєкту в режимі Java - це файл з розширенням .pde (таких файлів може бути кілька), в якому знаходиться початковий код.

Проєкт в режимі Java не є самодостатнім, тобто, без середовища Processing IDE запустити його неможливо. Щоб отримати робочий незалежний застосунок для обраної платформи, його необхідно експортувати.

Для цього використовується пункт меню Файл  Експортувати застосунок…​ (Ctrl+Shift+E).

У цьому режимі в головному меню Processing IDE з’являється пункт Налагодження, у якому присутні команди для зневадження коду.

Режим Python

Основний файл проєкту в режимі Python - це файл з розширенням .pyde, в якому знаходиться початковий код.

Режим p5.js (JavaScript)

Проєкт в режимі p5.js запускається у вебпереглядачі, який визначений в операційній системі за стандартним налаштуванням, за адресою, наприклад, http://127.0.0.1:8292/.

Дізнавшись IP-адресу комп’ютера із запущеним застосунком (наприклад, вона може бути http://192.168.1.4:8292/), проєкт можна запустити у вебпереглядачі на мобільному пристрої, ввівши цю адресу повністю, за умови, що комп’ютер і мобільний пристрій приєднані до однієї мережі.

Проєкт в режимі p5.js зазвичай містить каталог libraries із файлом бібліотеки p5.min.js - стиснута версія (використовується для швидшого завантаження вебсторінки) бібліотеки p5.js, JavaScript-файл ескізу з назвою, на зразок sketch_210919a.js (спробуйте розшифрувати назву 🤔), і файл index.html.

Режим Android

У цьому режимі можна використовувати Processing IDE для створення і запуску застосунків для пристроїв із системою Android. Офіційна сторінка присвячена цій темі розташована за адресою android.processing.org .

sketch.properties - ще один із файлів, який створюється автоматично у проєкті й містить інформацію про поточний альтернативний режим Processing IDE.

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

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

Розділ Ескіз об’єднує команди для роботи з проєктом: запуск, зупинка і Режим презентації.

Режим презентації (лише для Java і Python, Ctrl+Shift+R ) запускає застосунок на тлі повноекранної заливки сірого кольору. Цей режим зручно використовувати для демонстрації роботи готового застосунку. Для виходу із цього режиму, необхідно натиснути на кнопку stop у лівому нижньому куті екрану або натиснути на кнопку Esc на клавіатурі.

Також, розділ Ескіз містить команди з додавання файлів (наприклад, зображень) у проєкт, відкривання каталогу проєкту і роботі із додатковими бібліотеками.

Розділ Інструменти містить корисні утиліти:

  • Створити шрифт...,

  • Вибрати колір...,

  • та інші, які можна додати за допомогою Інструменти  Керувати інструментами…​.

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

Команда Генератор відео із розділу Інструменти використовується для створення відеоролика із набору файлів зображень.

Розділ Довідка (Help) містить покликання на навчальні сайти та документацію.

2.4.2. p5.js - інтерпретація Processing для вебу

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

p5.js - це безплатна бібліотека JavaScript з відкритим початковим кодом для креативного програмування з акцентом на те, щоб зробити кодування доступним для художників, дизайнерів, викладачів, початківців та усіх охочих.

Як середовище розробки для використання p5.js і написання коду мовою JavaScript, можна обрати:

  • середовище розробки Processing IDE у режимі p5.js;

  • OpenProcessing - онлайн-середовище з підтримкою JavaScript і p5.js;

  • p5.js Web Editor - онлайн-редактор з підтримкою JavaScript і p5.js та інтерфейсом українською.

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

Цікавимось

Локальна розробка

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

Отож, ваші кроки:

  1. Завантажити файл-архів проєкту p5.js і розпакувати у певний каталог. Архів містить файли бібліотек p5.js, p5.sound.js та зразок проєкту empty-example.

  2. У каталозі проєкту empty-example міститься файл ескізу sketch.js

sketch.js
function setup() {
  // put setup code here
}

function draw() {
  // put drawing code here
}

і файл index.html

index.html
<!DOCTYPE html>
<html lang="">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.js example</title>
  <style>
    body {
      padding: 0;
      margin: 0;
    }
  </style>
  <script src="../p5.js"></script> (1)
  <!-- <script src="../addons/p5.sound.js"></script> --> (2)
  <script src="sketch.js"></script> (3)
</head>

<body>
  <main>
  </main>
</body>

</html>

Розглянемо позначені рядки.

1 Підключення бібліотеки p5.js. Для швидшого завантаження вебсторінки можна використовувати стиснуту версію бібліотеки, змінивши назву з p5.js на p5.min.js.
2 Підключення бібліотеки p5.sound.js (чи її стиснутої версії p5.sound.min.js) для роботи зі звуком. Щоб використовувати цю бібліотеку, необхідно розкоментувати цей рядок у файлі index.html.
3 Підключення файлу ескізу sketch.js.
  1. У файлі ескізу sketch.js записати початковий код.

  2. Щоб переглянути свій ескіз, необхідно відкрити файл index.html у вебпереглядачі.

Окрім того, у своєму проєкті можна використовувати версію бібліотеки p5.js (чи її стиснуту версію), що зберігається в CDN (Network Delivery Network) за адресою p5.js CDN . Отож, вміст файлу index.html після внесення змін може виглядати так:

index.html
<!DOCTYPE html>
<html lang="">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.js example</title>
  <style>
    body {
      padding: 0;
      margin: 0;
    }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.min.js"></script>
  <script src="sketch.js"></script>
</head>

<body>
  <main>
  </main>
</body>

</html>
Для належної роботи переважної більшості застосунків, які запускаються локально, необхідно використовувати локальний вебсервер . Вебсервер запускається із каталогу, у який був розпакований завантажений архів зі зразком проєкту. У цьому разі, щоб переглянути свої ескізи, необхідно перейти у вебпереглядачі за адресою http://localhost:port/empty-example/index.html, де port - номер порту.

2.4.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Які можливості надає середовище розробки Processing IDE?

  2. Яку назву має застосунок, створений за допомогою Processing?

  3. Які режими роботи підтримує Processing IDE? На скільки відрізняється структура проєкту в залежності від обраного режиму?

  4. Для чого використовується бібліотека p5.js?

  5. На вашу думку, яке середовище розробки для написання ескізів мовою JavaScript з використанням p5.js є оптимальним?

2.4.5. Практичні завдання

Початковий

  1. Увімкнути альтернативні режими (Python, JavaScript) в Processing IDE.

  2. У Processing IDE в режимі Java відкрити приклад із меню Файл  Приклади…​ (Ctrl+Shift+O) і далі Basics  Color  Saturation. Виконати код із прикладу.

  3. Створити нове вікно у середовищі Processing IDE (Ctrl+N). Обрати режим Python і довільний приклад та виконати його код.

  4. Створити нове вікно у середовищі Processing IDE (Ctrl+N). Обрати режим JavaScript і довільний приклад та виконати його код.

Середній

  1. У Processing IDE в режимі Java відкрити приклад із меню Файл  Приклади…​ (Ctrl+Shift+O) і далі Basics  Math  Arctangent. Запустити приклад в режимі Режим презентації.

  2. Створити нове вікно у середовищі Processing IDE (Ctrl+N). Обрати режим JavaScript і виконати код, поданий нижче.

function setup() {
  createCanvas(400, 400);
}

function draw() {
  if (mouseIsPressed) {
    fill(0);
  } else {
    fill(255);
  }
  ellipse(mouseX, mouseY, 80, 80);
}
  1. Розглянути прототипи застосунків , створених за допомогою p5.js.

  2. Відкрити онлайн-редактор p5.js Web Editor і запустити код одного із застосунків з попереднього завдання.

Високий

  1. У Processing IDE в режимі Java відкрити приклад із меню Файл  Приклади…​ (Ctrl+Shift+O) і далі Topics  Geometry  Toroid. Експортувати цей проєкт як застосунок для вашої операційної системи. Запустити застосунок і переконатися, що він працює окремо від середовища Processing IDE.

3. Графічні побудови та взаємодії

У цьому розділі ви ознайомитеся з мовою програмування JavaScript, основами роботи з бібліотекою p5.js й навчитеся створювати власні графічні застосунки.

3.1. Основні елементи мови програмування JavaScript. Використання змінних і виразів

Брендан Айк
Брендан Айк

Мова JavaScript була створена в 1995 році Бренданом Айком зі спеціальною метою - додати динамічну поведінку в документи, які відображаються вебпереглядачами.

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

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

Цікавимось

Версії JavaScript

Як і будь-яка мова програмування, мова JavaScript змінювалася з часом.

У 1997 році з’явилась перша версія ECMAScript (ES1)- стандартизованої специфікації, інакше стандарту мови JavaScript.

З того часу було кілька видань мовного стандарту: ES2 (1998), ES3 (1999), ES5 (2009) і ES5.1 (2011).

Версія стандарту ES6 побачила світ у 2015 році та отримала низку важливих оновлень, включаючи синтаксис класів і модулів, які перетворили мову JavaScript із мови написанні сценаріїв в універсальну мову для розробки великомасштабних застосунків.

Починаючи з ES6, специфікація ECMAScript перейшла на щорічний графік випусків і тепер версії мови - ES2016, ES2017, ES2018, ES2019, ES2020 і всі наступні версії називаються відповідно до року їх випуску.

3.1.1. Імена, змінні, константи, коментарі

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

Мова програмування JavaScript має такі елементи:

  • алфавіт;

  • зарезервовані слова;

  • змінні та константи;

  • коментарі.

Алфавіт складається з великих (A-Z) і малих (a-z) літер латинського алфавіту, цифр (0-9), спеціальних символів (+, -, *, /, =, >, <, ;, === та інші).

Зарезервовані слова мають спеціальне призначення і використовуються в конструкціях мови програмування.

Таблиця "Основні зарезервовані слова JavaScript"

as

async

await

break

case

catch

class

const

continue

debugger

default

delete

do

else

export

extends

false

finally

for

function

get

if

import

in

instanceof

let

new

null

of

return

static

super

switch

this

throw

true

try

typeof

var

void

while

with

yield

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

Змінні не є значеннями, вони є контейнерами для значень.

Змінні та константи мають імена (за нашою аналогією - наліпки з унікальними назвами на контейнерах), які формуються за допомогою алфавіту мови програмування за певними правилами.

Мова JavaScript є регістрозалежною (розрізняються великі та малі літери в іменах). Наприклад, зарезервоване слово for потрібно писати лише як for, але не For чи FOR. Аналогічно, name, Name, NAME - три різні змінні.
Зарезервовані слова не можна використовувати як імена для власних змінних чи констант!

Змінна може використовуватися в одному застосунку багато разів і змінювати своє значення під час його виконання.

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

Імена змінних і констант в JavaScript - це покликання на їх значення в пам’яті. У разі змінних - покликання на читання і запис значення, у разі констант - лише для читання значення.

Цікавимось

Імена в JavaScript

У JavaScript є два правила, які необхідно дотримуватися при створенні імен:

  • ім’я має містити лише букви, цифри чи символи $ і _

  • перший символ імені не повинен бути цифрою

Приклади допустимих імен:

  • i

  • name

  • test123

  • myAge

  • _message

  • $learning

Приклади недопустимих імен:

  • 1a

  • my-name

  • var

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

  • myName

  • myFirstName

  • myVeryLongName

Щоб перевірити ім’я вашої змінної чи константи на коректність, скористайтеся сайтом JavaScript variable name validator .

Коментарі - це текстові пояснення до написаного початкового коду.

Коментарі записуються в один рядок за допомогою // або у кілька рядків, використовуючи конструкцію /* */, і не обробляються під час виконання коду.

// однорядковий коментар

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

3.1.2. Файл ескізу sketch.js

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

У файл ескізу sketch.js запишемо такий код:

sketch.js
function setup() {
  createCanvas(200, 200); (1)
}

function draw() {
  ellipse(100, 100, 80, 80); (2)
}
1 Функція createCanvas() створює полотно та встановлює його розміри в пікселях по горизонталі й вертикалі. У цьому разі розмір полотна 200x200 пікселів.
2 Функція ellipse() малює на екрані еліпс (овал). За стандартним налаштуванням перші два параметри встановлюють розташування центру еліпса, а третій і четвертий - ширину та висоту фігури. Якщо висота не вказана, значення ширини використовується як для ширини, так і для висоти. Еліпс з однаковою шириною та висотою - це коло.
У JavaScript в кінці кожної інструкції потрібно ставити символ ; (крапка з комою). Хоча це правило у JavaScript є необов’язковим, зокрема, якщо інструкції записані у різних рядках, без розділювача ; інтерпретатор JavaScript в окремих випадках може неправильно трактувати ваш код.

Цікавимось

Що входить до складу бібліотеки p5.js?

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

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

Це значення потім можна зберегти з певним ім’ям та передати як змінну іншим функціям.

Отже, поданий вище код можна прочитати так:

створи полотно, розміром 200x200 пікселів і намалюй еліпс, центр якого знаходиться на відстані 100 пікселів від лівої межі полотна і 100 пікселів від верхньої межі полотна (центр еліпса), з шириною і висотою у 80 пікселів.

У 2D-режимі (увімкнений за стандартним налаштуванням) початок координат (точка, від якої відраховується кількість пікселів по горизонталі праворуч і по вертикалі вниз) має координати (0, 0) і розміщується у верхньому лівому куті полотна.

Цікавимось

Структура файлу sketch.js

JavaScript-файл ескізу складається зазвичай із двох функцій.

Перша з них:

function setup() {
...
}

Початковий код, записаний у функції setup() , використовується для ініціалізації застосунку і виконається 1 раз при запуску застосунку.

Друга:

function draw() {
...
}

Початковий код у функції draw() виконується постійно. Інакше кажучи, код виконується послідовно по колу - після останнього рядка коду виконується перший і т. д.

Якщо код написаний без помилок, то у вебпереглядачі буде створено полотно, на якому буде намальоване коло, хоча в коді використовується ellipse() . Чому так?

Так відбулося завдяки тому, що у функції ellipse() було вказано однакові значення висоти (80) та ширини (80) відповідно.

Вправа 1

Відредагувати код для функції ellipse(), щоб отримати еліпс.

Додамо до нашого коду ще кілька команд і коментарі з поясненнями (рядки з коментарями починаються з //):

sketch.js
function setup() {
  // створення полотна
  createCanvas(200, 200);
  // колір тла полотна
  background(0, 0, 0); // Black (1)
}

function draw() {
  // колір заливки еліпса
  fill(244, 240, 125); // Canary (2)
  // вимкнення межі еліпса
  noStroke(); (3)
  // малювання еліпса
  ellipse(100, 100, 80, 80);
}
1 Функція background() встановлює колір для тла полотна.
2 Функція fill() встановлює колір, який використовується для зафарбовування фігур, що створюються після виклику цієї функції (функція noFill() вимикає зафарбовування фігур).
3 Функція noStroke() вимикає нанесення обведення (контуру, межі) для фігур (функція stroke() створює протилежний ефект - встановлює колір межі для фігур).
У зразках коду навпроти виклику функцій для роботи з кольором (fill(), stroke()) за допомогою коментарів вказані назви кольорів.

Щоразу, коли код всередині функції draw() запускається, створюється новий кадр. Кожен наступний такий кадр змінює те, що є на полотні у попередньому кадрі. Так зміна кадрів створює на екрані ефект руху, анімацію.

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

Цікавимось

Значення кольору

Функції background() і fill() отримують значення кольору у форматі RGB (за стандартним налаштуванням), HSB залежно від поточного режиму кольору, встановленого за допомогою функції colorMode() .

Для колірного простору RGB значення кожного з трьох кольорових каналів і альфа-каналу знаходяться в діапазоні цілих значень від 0 до 255.

Якщо для кольору використовується одне ціле значення - колір встановлюється в градаціях сірого (між білим і чорним).

Вправа 2

Намалювати еліпс з власними значеннями кольорових складових для функцій background() і fill(). Якщо треба, змінити режим кольору за допомогою функції colorMode().

3.1.3. Використання змінних і виразів

Використаємо у нашому коді змінні. Збережемо ширину і висоту полотна як змінні з назвами canvasWidth і canvasHeight відповідно.

Для створення змінних в JavaScript використовується зарезервоване слово let, далі вказується ім’я змінної, потім записується оператор присвоєння = і значення змінної.

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

Звичайно, canvasWidth і canvasHeight можна оголосити як константи, якщо їхні значення не планується змінювати в процесі виконання застосунку. У цьому разі для оголошення констант необхідно використати зарезервоване слово const з одночасним присвоєнням оголошеним константам їх значень.

Отож, зі змінними код матиме наступний вигляд:

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 200;
  let canvasHeight = 200;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // колір тла полотна
  background(0, 0, 0); // Black
}

function draw() {
  // колір заливки еліпса
  fill(244, 240, 125); // Canary
  // вимкнення межі еліпса
  noStroke();
  // малювання еліпса
  ellipse(100, 100, 80, 80);
}

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

Зарезервоване слово let дозволяє оголосити локальну змінну з областю видимості, обмеженою поточним блоком коду - такі змінні «працюють» всередині блоку і недоступні поза ним.

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

У коді вище, змінні canvasWidth і canvasHeight є локальними й недоступними поза межами функції setup(). Спроба використати ці змінні поза межами функції setup(), наприклад, у функції draw(), викличе помилку на зразок canvasWidth is not defined (змінна canvasWidth не визначена).

Така сама помилка з’явиться у разі, коли ми звертаємось до змінної, якої взагалі не існує.

Змінна, яка існує, але не має значення, є порожнім контейнером. Коли ми звертаємось до такої змінної, ми отримуємо значення undefined.

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

Імена, які дають змінним і константам, повинні легко читатися, мати зрозумілий сенс і говорити про те, які дані в пам’яті зберігаються під цими іменами. Наприклад: userName, canvasWidth, footerText, sketchWrapper і т. д.

Використання різних змінних для різних значень є хорошим стилем програмування.

Вправа 3

Виконати код для друку значень змінних всередині консолі вебпереглядача за допомогою JavaScript-функції console.log() і переглянути результати (Ctrl+Shift+I).

function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 200;
  let canvasHeight = 200;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // колір тла полотна
  background(0, 0, 0); // Black
  // виведення в консоль
  console.log("Ширина полотна:", canvasWidth, "пікселів");
  console.log("Висота полотна:", canvasHeight, "пікселів");
}

function draw() {
  // колір заливки еліпса
  fill(244, 240, 125); // Canary
  // вимкнення межі еліпса
  noStroke();
  // малювання еліпса
  ellipse(100, 100, 80, 80);
}
Функції console.log() і print()

Для друку в консолі вебпереглядача значень, які створює застосунок, можна використовувати функцію print() із бібліотеки p5.js. Ця функція є аналогом функції console.log() із JavaScript.

Функція print() створює новий рядок тексту для кожного свого виклику. Щоб надрукувати порожній рядок в консолі вебпереглядача, використовують запис print("\n").

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

Запишемо код для створення зображення трьох еліпсів і використаємо для цього змінні.

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 360;
  let canvasHeight = 120;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // колір тла полотна
  background(235, 235, 235); // Platinum
}

function draw() {
  // оголошення локальних змінних і присвоєння їм значень
  let y = 60;
  let d = 80;
  // колір заливки еліпсів
  fill(255, 255, 255); // White
  // вимкнення меж еліпсів
  noStroke();
  // малювання еліпсів
  ellipse(75, y, d, d);
  ellipse(175, y, d, d);
  ellipse(275, y, d, d);
}

Вправа 4

Запустити код для створення трьох еліпсів, змінивши значення змінних y і d.

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

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

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 300;
  let canvasHeight = 200;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // колір тла полотна
  background(235, 235, 235); // Platinum
}

function draw() {
  // оголошення локальних змінних і присвоєння їм значень
  let x = 20;
  let y = 30;
  let h = 40;
  // малювання прямокутників
  rect(x, y, 150, h);
  y = y + h;
  rect(x + 150, y + h, 130, h);
  x = 0;
  rect(x, y + h * 2, 100, h);
}

У даному коді використана функція rect() , яка малює прямокутник на полотні.

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

Символи +, -, * і = в коді називаються операторами, а значення змінних - операндами. Вказані оператори визначають прості арифметичні операції та операцію присвоєння =, за допомогою якої змінним присвоюються певні значення.

Таблиця "Математичні оператори JavaScript"

Оператор

Арифметична операція

+

додавання

-

віднімання

*

множення

/

ділення

%

остача від ділення

**

піднесення до степеня

Значення змінних (операндів) в JavaScript завжди належать до даних певного типу.

Цікавимось

Коротко про типи даних в JavaScript

Типи даних JavaScript можна об’єднати у дві категорії: прості типи та об’єктні типи.

До простих типів належать числа, рядки, логічні значення true і false, спеціальні значення null і undefined.

Загалом у JavaScript можна виділити наступні прості типи даних:

let n = 123; // ціле число (тип Number)
n = 12.345;  // число з рухомою крапкою (тип Number)
let s = "JavaScript"; // рядок (тип String)
let specification = 'Одинарні лапки також використовуються';
let phrase = `Зворотні лапки в ${s} дозволяють у рядки вбудовувати значення змінних і вирази, які відразу обчислюються`;
let working = true; // істина (тип Boolean)
let studying = false; // хибність (тип Boolean)
let age = null; // "нічого", "порожньо" або "значення невідомо" (тип Null)
let name; // якщо змінна оголошена, але їй не присвоєно жодного значення (тип Undefined)
Спеціальні типи Null та Undefined мають єдині значення null і undefined відповідно, які є ознаками відсутності значення.

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

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

Для визначення типу даних в JavaScript використовують функцію typeof x, де x - значення, тип якого хочуть дізнатися.

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

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

Текст коду застосунку зазвичай складається з послідовності інструкцій.

Цікавимось

Вирази в JavaScript

У виразах оператори можуть бути унарними чи бінарними.

Унарним називається оператор, який застосовується до одного операнду. Наприклад, оператор унарний мінус змінює знак числа на протилежний: -4, -0.5.

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

  • x + 100

  • y + h * 2

  • 125 - 40

  • 24 / 3

Результат a % b - це остача від цілочисельного ділення a на b.

Наприклад:

  • 5 % 2 = 1

  • 8 % 3 = 2

  • 1 % 4 = 1

У виразі a ** b - оператор піднесення до степеня множить значення a на саме себе b разів.

  • 2 ** 2 = 4 (2 помножене на себе 2 рази)

  • 2 ** 3 = 8 (2 * 2 * 2, 3 рази)

  • 2 ** 4 = 16 (2 * 2 * 2 * 2, 4 рази)

  • 4 ** (1 / 2) = 2 (степінь 1 / 2 еквівалентна дії кореня квадратного)

  • 8 ** (1 / 3) = 2 (степінь 1 / 3 еквівалентна дії кореня кубічного)

Зазвичай за допомогою оператора + виконують додавання чисел.

Але якщо бінарний оператор + застосувати до рядків, то оператор об’єднує їх в один рядок:

s = "мій" + "рядок"; // мійрядок

Якщо хоча б один операнд є рядком, то другий буде також перетворений у рядок:

'4' + 5; // "45"
'1' + 2 + 2; // "122"
2 + 2 + '1'; // буде "41", а не "221"

Додавання і перетворення рядків - це особливість бінарного додавання +. Інші арифметичні оператори працюють тільки з числами й завжди намагаються перетворити операнди в числа.

8 - '2'; // 6, рядок '2' перетворюється у число
'8' / '2'; // 4, обидва операнди перетворюються у числа

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

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

+2;    // 2
+true; // 1
+"";   // 0

Необхідність перетворювати рядки в числа виникає дуже часто.

let apricots = "4";
let apples = "6";

apricots + apples; // "46", бінарний плюс об'єднує рядки
+apricots + +apples; // 10, обидва операнди попередньо перетворені у числа за допомогою унарних плюсів

Цікавимось Додатково

Ділення з остачею у мовах програмування

Ділення з остачею іноді може дивувати.

Ділення з остачею в консолі вебпереглядача і у калькуляторі Google
Ділення з остачею: консоль вебпереглядача і калькулятор Google
Дізнайтеся більше про правила виконання операції ділення з остачею у різних мовах програмування.

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

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

Таблиця "Пріоритети деяких операторів в JavaScript"
Пріоритет Назва Позначення

17

унарний плюс

+

17

унарний мінус

-

16

піднесення до степеня

**

15

множення

*

15

ділення

/

13

додавання

+

13

віднімання

-

3

присвоєння

=

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

Наприклад, у JavaScript ви можете складати у змінну або віднімати зі змінної за допомогою всього одного оператора:

x += 5; // еквівалентно x = x + 5
y -= 10; // еквівалентно y = y - 10

Вправа 5

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

Окрім вбудованих функцій, бібліотека p5.js використовує системні або вбудовані змінні.

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

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 500;
  let canvasHeight = 400;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // колір тла полотна
  background(52, 89, 149); // Bdazzled Blue
}

function draw() {
  fill(248, 237, 235); // Seashell
  noStroke();
  // намалювати коло
  circle(mouseX, mouseY, 25);
}

У даному коді використана функція circle() , яка малює коло на полотні: перші два параметри встановлюють розташування центру кола, а третій встановлює діаметр кола.

Вправа 6

За допомогою функції console.log() відстежити (надрукувати) в консолі вебпереглядача (Ctrl+Shift+I), як змінюються значення системних змінних mouseX і mouseY.

Цікавимось

Вбудовані змінні у бібліотеці p5.js
  • mouseX - містить поточну координату x вказівника миші на полотні (оновлюється з кожним кадром).

  • mouseY - містить поточну координату y вказівника миші на полотні (оновлюється з кожним кадром).

  • pmouseX - містить координату x вказівника миші з попереднього кадру (оновлюється з кожним кадром).

  • pmouseY - містить координату y вказівника миші з попереднього кадру (оновлюється з кожним кадром).

  • width - ширина полотна у пікселях.

  • height - висота полотна у пікселях.

  • frameCount - кількість кадрів з моменту запуску застосунку. Після першої ітерації у блоці draw() frameCount дорівнює 1.

  • displayWidth - ширина усього екрану у пікселях.

  • displayHeight - висота усього екрану у пікселях.

За стандартним налаштуванням функція draw() викликається максимум 60 разів за секунду, тобто, код всередині цієї функції виконується 60 разів за секунду. Переконаємось у цьому, використавши функцію frameRate() , яка дозволяє визначити кількість кадрів, які відображатимуться щосекунди.

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 200;
  let canvasHeight = 200;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // колір тла полотна
  background(210);
}

function draw() {
  console.log(frameRate());
}

Після запуску ескізу в консолі вебпереглядача можна спостерігати значення частоти кадрів, наближені до 60 (числа з рухомою крапкою). Як бачимо, функція draw() виконується максимум 60 разів на секунду. Чи більше це значення, тим зміна кадрів є плавнішою. У разі, коли частота кадрів низька, зміна кадрів може виглядати нерівною.

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

У наступному прикладі додатково використаємо функції для роботи з текстом:

  • text() - для створення на полотні текстового напису;

  • textSize() - для встановлення розміру тексту;

  • textAlign() - для вирівнювання тексту на полотні.

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 200;
  let canvasHeight = 200;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // вирівнювання тексту на полотні
  textAlign(CENTER, CENTER);
}

function draw() {
  background(210);
  fill(237, 34, 93); // Paradise Pink
  // розмір тексту
  textSize(24);
  // оголошення змінної fps і присвоєння їй значення з рухомою крапкою функції frameRate(), приведеного до цілого значення за допомогою JavaScript-функції parseInt()
  let fps = parseInt(frameRate(), 10);
  // виведення тексту
  text("frameRate: " + fps, width / 2, height / 2);
}
У коді використані функції JavaScript: parseInt(), яка обробляє рядковий аргумент і повертає число з вказаною основою системи числення; якщо цей аргумент не є рядком, тоді він буде перетворений на рядок за допомогою функції toString().

Переглядаємо Аналізуємо

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

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 200;
  let canvasHeight = 200;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  // вирівнювання тексту на полотні
  textAlign(CENTER, CENTER);
  frameRate(5); // створення повільної анімації
}

function draw() {
  background(210);
  fill(237, 34, 93); // Paradise Pink
  // розмір тексту
  textSize(24);
  // виведення значення frameCount
  text("frameCount: " + frameCount, width / 2, height / 2);
}

Переглядаємо Аналізуємо

Вправа 7

Змінити значення frameRate() у коді для повільної анімації та розмістити напис в іншому місці полотна.

Розглянемо ще один приклад використання змінних і вбудованої функції random().

Цікавимось

Функція random() з бібліотеки p5.js

Функція random() отримує 0, або 1, або 2 аргументів і повертає випадкове число з рухомою крапкою за такими правилами:

  • якщо аргумент не вказаний, повертає випадкове число від 0 до (але не враховуючи) 1;

  • якщо задано один аргумент, і це число, повертає випадкове число від 0 до (але не враховуючи) числа;

  • якщо задано один аргумент і це масив, повертає випадковий елемент із цього масиву;

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

Наприклад, під час виконання застосунку щоразовий виклик у блоці draw() функції random() зі значенням 10

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(245);
  let r = random(10);
  console.log(r);
}

генерує одне випадкове значення з рухомою крапкою у діапазоні від 0 до 10

sketch.js
4.332182920537193
6.386040480954347
8.972577089652496
3.6487207122549448
5.1144337926340855
4.851433236183638
3.3701078470578416
3.301716654233342
7.4925533782895055
0.4229917404261818
0.20904658694042455
0.3414970594736899
4.0879382561223725
...

Для генерації випадкових цілих чисел з певного діапазону можна поєднати дві вбудовані функції p5.js: int() і random() . Наприклад, для генерування цілих чисел 0 чи 1 вираз матиме вигляд: int(random(0, 2));.

Інший варіант такої генерації - використання засобів мови JavaScript. У цьому разі вираз матиме вигляд: Math.floor(Math.random() * 2);.

sketch.js
function setup() {
  // оголошення локальних змінних і присвоєння їм значень
  let canvasWidth = 500;
  let canvasHeight = 400;
  // створення полотна
  createCanvas(canvasWidth, canvasHeight);
  frameRate(5); // створення повільної анімації
  background(250);
}

function draw() {
  // оголошення локальних змінних і присвоєння їм значень
  let r = random(255);    // червона складова
  let g = random(255);    // зелена складова
  let b = random(255);    // синя складова
  let a = random(255);    // прозорість
  let d = random(20);     // діаметр кола
  let x = random(width);  // x-координата центра кола
  let y = random(height); // y-координата центра кола

  // використання локальних змінних
  noStroke();
  fill(r, g, b, a);
  ellipse(x, y, d, d);
}

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

Переглядаємо Аналізуємо

Вправа 8

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

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

Розглянемо код застосунку, який створює ілюзію горизонтального руху на полотні, в якому коло переміщується горизонтально зліва направо завдяки зміні значення змінної x у черговому кадрі:

sketch.js
let x = 0; (1)
let velocity = 0.5; (2)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(x, height / 2, 50); (3)
  x += velocity; (4)
}
1 Ініціалізація змінної x зі значенням 0 - це початкове значення x-координати центру кола.
2 Ініціалізація змінної velocity зі значенням приросту для x-координати у кожному наступному кадрі. Змінюючи значення velocity можна керувати швидкістю руху фігури на полотні.
3 Малювання кола з діаметром 50 у точці з x-координатою, що має значення x, і y-координатою, що дорівнює height / 2 (значення цієї координати є однаковим для усіх кадрів, тому коло по вертикалі завжди знаходиться у центрі полотна).
4 Збільшення значення змінної x на величину velocity у кожному наступному кадрі.

Переглядаємо Аналізуємо

Значення x постійно зростає на величину velocity, коли застосунок виконується, тому через певний час коло сховається за правою межею полотна.

Вправа 9

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

3.1.5. Контрольні запитання

Міркуємо Обговорюємо

  1. Які складові мови програмування JavaScript?

  2. Яких правил необхідно дотримуватись при виборі імен для змінної чи константи? Назвіть коректні й некоректні приклади таких імен.

  3. Які функції обов’язково присутні у файлі ескізу sketch.js? Які особливості роботи цих функцій?

  4. Як правильно оголосити змінну чи константу в JavaScript?

  5. Чи правильне таке твердження: у файлі ескізу sketch.js змінну чи константу можна оголосити у будь-якому місці. Обґрунтуйте свою відповідь.

  6. Для чого використовується функція console.log()?

  7. Що таке «тип даних»? Які прості типи даних є в JavaScript?

  8. Вказати тип даних JavaScript для оголошених змінних і констант:

let count = 0;
const letter = "A";
const startPoint = 5;
let y = 81.19;
let happy = true;
let resultScore = 40;
  1. Коротко опишіть призначення поданих функцій бібліотеки p5.js за назвою: ellipse(), background(), fill(), noStroke(), stroke(), rect(), circle(), frameRate(), text(), textSize(), textAlign().

  1. Яких значень набувають системні змінні mouseX і mouseY?

3.1.6. Практичні завдання

Початковий

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

function setup() {
  createCanvas(..., ...);
  background(255);
}

function draw() {
  stroke(0);
  fill(r, g, b);
  ellipse(x, y, d, d);
}
  1. Встановити на свій вибір колір тла полотна, використовуючи значення кольорів RGB.

Середній

  1. Намалювати коло з центром в точці (100, 100) та радіусом 60. Зафарбувати фігуру кольором на свій вибір.

  2. Намалювати еліпс з центром в точці (120, 100) шириною 35 і висотою 25. Зафарбувати фігуру кольором і встановити колір межі фігури. Кольори обрати на свій вибір.

  3. Намалювати прямокутник шириною 80 і висотою 30, лівий верхній кут якого має координати (40, 30). Зафарбувати фігуру кольором і встановити колір межі фігури. Кольори обрати на свій вибір.

  4. Намалювати квадрат зі стороною 75, лівий верхній кут якого має координати (30, 20). Зафарбувати фігуру кольором, встановити колір і товщину межі фігури. Кольори обрати на свій вибір.

Високий

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

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

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

Екстремальний

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

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

3.2. Полотно, пікселі, координати, кольори

Ескіз створюється у вікні певного розміру, яке називається полотном.

3.2.1. Система координат полотна

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

function setup() {
  createCanvas(400, 300);
}
Задані значення розмірів полотна містяться у системних змінних width і height відповідно. Щоб встановити розміри полотна на увесь екран, використовуються відповідно системні змінні displayWidth і displayHeight .

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

  • x-координата - це відстань, від лівої межі полотна;

  • у-координата - це відстань від верхньої межі.

Координати точки в прямокутній системі координат на площині й пікселя у системі координат полотна визначаються двома значеннями та записуються (x, y).

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

Наприклад, якщо розміри полотна 400x400 пікселів, то координати верхнього лівого пікселя будуть (0, 0), а нижнього правого - (399, 399), оскільки рахунок пікселів починається з 0, а закінчується на 399. Тому, щоб побачити повністю намальовану точку в нижньому правому куті полотна, потрібно використовувати координати (width - 1, height - 1).

У 2D-режимі (режим стандартного налаштування) початок координат (0, 0) розміщується у верхньому лівому куті полотна. У режимі 3D (третім аргументом у виклику функції createCanvas() є значення WEBGL), початок координат розміщується в центрі полотна.

Щоб краще зрозуміти, як відбувається графічна побудова в системі координат полотна у 2D-режимі, використаємо у ролі тла полотна піксельну сітку - графічний файл, розміром 1000x1000 пікселів, розкреслений у клітинку з кроком у 100 пікселів.

Графічна піксельна сітка
Відкрийте зображення у новій вкладці або збережіть на комп’ютер, щоб мати реальний розмір зображення сітки

Тепер під’єднаємо файл сітки до ескізу, використавши функцію loadImage() .

У разі використання середовища Processing IDE, створіть каталог data у каталозі вашого ескізу і скопіюйте у нього файл із зображенням сітки. Шлях до зображення у функції loadImage() повинен бути відносним до HTML-файлу ескізу.
sketch.js
let grid; (1)

function preload() { (2)
  grid = loadImage("grid_1000.png"); (3)
}

function setup() {
  createCanvas(1000, 1000);
  strokeWeight(3);
  image(grid, 0, 0); (4)
}

function draw() {
  stroke(0, 233, 245); // Electric Blue
  line(100, 100, 400, 300); (5)
}
1 Оголошуємо змінну grid, яка буде вказувати на завантажений графічний файл.
2 Щоб переконатися в тому, що зображення повністю завантажено, використаємо функцію preload() .
3 Усередині preload() функція loadImage() завантажує графічний файл і призначає його змінній з назвою grid. Тут варто бути уважним і правильно прописати шлях до зображення.
4 Функція image() розміщує завантажене зображення grid на полотні в точці з координатами (0, 0) (у початку системи координат полотна).
5 Малюємо лінію на полотні.
Сітка на полотні
Сітка на полотні
Використовуйте сітку, щоб за координатами розуміти, де саме на полотні буде намальований об’єкт.

Вправа 10

Створити полотно і намалювати точки у різних кутах полотна і на полотні (відобразити пікселі). Розмістити точки поруч, щоб отримати горизонтальні або вертикальні лінії. Використати зразок коду за основу ескізу.

function setup() {
  createCanvas(400, 300);
}

function draw() {
  background(220);
  point(200, 150);
}
Ескіз містить функцію point() , яка малює точку розміром в один піксель за переданими координатами точки.

3.2.2. Колірні моделі

Важливість кольорових і світлових ефектів в роботі сучасних цифрових художників важко переоцінити.

Для створення таких ефектів використовують різні системи опису кольорів - колірні моделі.

В комп’ютерній графіці колір створюється від світіння пікселів на екрані.

Для встановлення кольорів і прозорості тла, меж або фігур бібліотека p5.js використовує системи RGB (RGBA) і HSB.

Щоб змінити параметри кольору в ескізі, використовують функції:

Для опису кольорів у p5.js використовують рядки, що містять значення кольорів, записані різними способами:

  • background("rgb(120, 255, 15)"); - модель RGB (колір Chartreuse Web);

  • background("rgba(0, 255, 0, 0.25)"); - модель RGBA (колір Green);

  • background("#32C2AA"); - шістнадцяткові значення або Hex CSS (колір Keppel);

  • background("magenta"); - CSS назви кольорів.

Зазначені та інші назви кольорів, що використовуються у підручнику, отримані за допомогою онлайн-сервісу для створення та збирання колірних палітр coolors.co . Цей онлайн-інструмент дозволяє працювати з великою кількістю колірних просторів, включаючи RGB, CMYK, LAB, HSB і кількома популярними бібліотеками кольорів, таких як Pantone®, Copic®, Prismacolor® тощо.

Значення параметрів вказаних функцій для колірної моделі RGB належать діапазону від 0 до 255, де 255 - це білий колір, 128 - відтінок сірого кольору, і 0 - чорний колір.

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

Якщо додано друге значення - воно буде використано для прозорості.

Коли вказані три значення, вони інтерпретуються як значення RGB або HSB. Додавання четвертого значення застосовує прозорість.

Цікавимось

Глибина кольору

Колір для певної фігури потрібно зберігати в пам’яті комп’ютера. Якщо використовувати вісім бітів (один байт), то для зберігання кольору можна використати 256 різних значень з діапазону чисел від 0 до 255.

Глибина кольору або бітова глибина (англ. Color Depth, Bits per Pixel) - визначає кількість бітів, які використовуються для представлення кольору одного пікселя растрового зображення.

Восьмирозрядне значення буде утворювати лише відтінки сірого кольору.

Якщо ж взяти 24 біти, то матимемо повноколірне зображення (вісім розрядів для кожного з компонентів червоного, зеленого та синього кольорів) у понад 16,7 млн. кольорів.

Для кольорів з прозорістю використовується 32 біти - колір є 24-бітним з додатковим 8-бітним каналом, який задає прозорість зображення для кожного пікселя.

Вправа 11

Надрукувати в консолі вебпереглядача значення змінної c.

function setup() {
  createCanvas(400, 300);
  frameRate(1); // створення повільної анімації
}

function draw() {
  background(255); // білий колір тла
  let c = color(random(255)); // визначення випадкового сірого відтінку
  stroke(c); // колір, який використовується для малювання ліній та меж навколо фігур
  strokeWeight(20); // товщина обведення, що використовується для ліній, точок та межі навколо фігур
  point(200, 150); // малювання точки в центрі полотна
}
У коді оголошується змінна c і їй присвоюється значення функції color() , яка створює кольори для зберігання у змінних типу даних «колір». Це зручно, якщо необхідно створити колір, який надалі буде використовуватися у коді.

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

Розуміючи, як працює діапазон значень кольорів, можна сказати, що кожна фігура може мати stroke() (обведення, межу) і fill() (заливку).

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

stroke() визначає колір контуру фігури, а fill() - колір заповнення цієї фігури. Зрозуміло, що лінії та точки можуть мати лише контур і значення товщини, яке встановлюється за допомогою функції strokeWeight() у пікселях.

Якщо значення кольору не вказано для вищезгаданих функцій, за стандартним налаштуванням використовується чорний (0) для stroke() та білий (255) для fill().

Додатково можна використовувати функцію noFill() для вимкнення заливки або функцію noStroke() для вимкнення контуру перед малюванням фігури. Варто пам’ятати, що при записі у коді застосунку обидвох функцій перед малюванням фігур на екрані нічого не з’явиться.

Колірна модель RGB у «цифровому» світі набула широкого використання. Кольори цієї моделі подаються трійкою значень: Red (червоний), Green (зелений), Blue (синій).

Змішуючи ці кольори у різних комбінаціях, можна отримати необхідний колір:

червоний + зелений = жовтий
червоний + синій = фіолетовий
зелений + синій = блакитний (синьо-зелений)
червоний + зелений + синій = білий
немає кольорів = чорний

Вправа 12

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

function setup() {
  createCanvas(400, 300);
  frameRate(1);
}

function draw() {
  background(255);
  let c = color(..., ..., ...);
  stroke(c);
  strokeWeight(20);
  point(200, 150);
}

На додаток до червоного, зеленого та синього компонентів кожного кольору існує додатковий необов’язковий четвертий компонент, який називається альфа-каналом.

Альфа-канал відповідає за прозорість і є корисним, коли ви хочете намалювати елементи, які здаються частково прозорими один відносно одного.

Значення альфа-каналу також може набувати значень з діапазону від 0 до 255. У цьому разі значення 0 означає повністю прозорий (непрозорість 0%) і 255 - повністю непрозорий (непрозорість 100%).

Подивимось, як працює прозорість, побудувавши кілька прямокутників за допомогою функції rect() з використанням заливки fill() різної прозорості.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(251, 133, 0); // Tangerine (1)
  noStroke();
  fill(255, 200, 221); // Orchid Pink (2)
  rect(0, 0, 100, 200);
  fill(20, 33, 61, 255); // Oxford Blue (3)
  rect(0, 0, 200, 40);
  fill(20, 33, 61, 191); (4)
  rect(0, 50, 200, 40);
  fill(20, 33, 61, 127); (5)
  rect(0, 100, 200, 40);
  fill(20, 33, 61, 63); (6)
  rect(0, 150, 200, 40);
}
1 Заливка полотна помаранчевим кольором. Значення прозорості відсутнє, тому полотно 100% непрозоре.
2 Заливка рожевого кольору для наступного прямокутника. Значення прозорості відсутнє, тому прямокутник 100% непрозорий.
3 Заливка синього кольору для наступного прямокутника. Значення прозорості 255, тому прямокутник 100% непрозорий.
4 Непрозорість 75% для наступного синього прямокутника.
5 Непрозорість 50% для наступного синього прямокутника.
6 Непрозорість 25% для наступного синього прямокутника.
Альфа-канал і прозорість
Альфа-канал і прозорість

Як було вже зазначено, колір RGB із діапазоном від 0 до 255 - це не єдиний спосіб подання кольорів у p5.js.

За стандартним налаштуванням параметри background(), color(), fill() і stroke() визначаються значеннями від 0 до 255 за допомогою колірної моделі RGB.

Це еквівалентно режиму

colorMode(RGB, 255)

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

Наприклад, для режиму

colorMode(RGB, 100);

діапазон значень RGB буде від 0 до 100, а для режиму

colorMode(RGB, 100, 200, 30, 255);

значення червоного кольору будуть в діапазоні від 0 до 100, зеленого - від 0 до 200, синього - від 0 до 30 і альфа-канал від 0 до 255.

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

Використання лише кольорів RGB здебільшого буде достатньо, проте можна використовувати й іншу колірну модель - HSB:

  • Hue - відтінок - відтінок кольору (зелений, червоний тощо) за стандартним налаштуванням коливається від 0 до 360 (у градусах);

  • Saturation - насиченість - яскравість кольору, значення від 0 до 255 за стандартним налаштуванням (у %);

  • Brightness - яскравість - значення від 0 до 255 за стандартним налаштуванням (у %).

У Processing IDE є вбудований інструмент вибору кольору, який можна відкрити із головного меню за допомогою Інструменти  Вибрати колір…​ .
Вибір кольору у Processing IDE
Вибір кольору у Processing IDE

У вікні вибору кольору в Processing IDE можна отримати значення кольору (Dark Slate Blue) у моделях RGB і HSB та у шістнадцятковому вигляді (#42367A).

Вправа 13

У функції setup() випадково були видалені значення кольорів. Спробуйте відновити початковий вигляд коду відповідно до інформації із коментарів. Що зображено на полотні?

let c1, c2;

function setup() {
  createCanvas(500, 500);
  c1 = color("..."); // чорний колір, шістнадцяткове значення
  c2 = color(...); // білий колір, RGB
  background(c1);
}

function draw() {
  fill(c1);
  stroke(c2);
  rect(5, 5, 190, 190);
  fill(46, 196, 182); // Tiffany Blue
  triangle(70, 80, 130, 80, 100, 185);
  fill("#F72585"); // Flickr Pink
  ellipse(100, 80, 60, 60);
}

3.2.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «піксель»?

  2. Який вигляд має система координат полотна?

  3. Що таке «колірна модель»? Які є різновиди колірних моделей? Дайте характеристику одній із колірних моделей.

  4. Яке призначення функцій color(), background(), fill(), stroke(), colorMode() бібліотеки p5.js?

  5. Які способи запису значення кольору використовуються у вищезгаданих функціях?

  6. Який колір визначає кожен із наступних рядків коду? У разі потреби з’ясувати експериментальним шляхом.

fill(0, 120, 0);
fill(100);
fill(0);
stroke(0, 0, 200);
stroke(225);
stroke(255, 255, 0);
stroke(0, 255, 255);
stroke(200, 150, 150);
stroke(255, 0, 255);

3.2.5. Практичні завдання

Початковий

  1. Заповнити прогалини в коді значеннями довільного кольору. Для запису значення кольору використати різні способи.

function setup() {
  createCanvas(200, 200);
  background(...);
}

function draw() {
  stroke(...);
  fill(...);
  ellipse(100, 100, 85);
}

Середній

  1. Поданий зразок коду створює ефект веселки. В цьому разі застосовується колірна модель RGB. Змінити колірну модель на HSB. Порівняти результати для обох колірних моделей.

function setup() {
  createCanvas(300, 300);
  background('#04B1CE'); // Pacific Blue
  noFill();
}

function draw() {
  strokeWeight(random(3, 10));
  stroke(random(255), random(255), random(255)); // RGB
  let rainbowSize = random(200, 270);
  ellipse(150, 350, rainbowSize, rainbowSize);
}
  1. Намалювати прямокутник. В ескізі використати функції background(), stroke(), fill() і колірну модель RGB для запису значень кольорів.

Високий

  1. Створити квітку як на малюнку. Використати фрагмент коду нижче, доповнивши його відсутніми командами.

Ромашка
function setup() {
  createCanvas(300, 300);
}

function draw() {
  let flowerX = 150;
  let flowerY = 150;
  let petalSize = 100;
  let petalDistance = petalSize / 2;

  background(82, 183, 136);

  fill(255, 255, 255);

  // верхня ліва пелюстка
  circle(flowerX - petalDistance, flowerY - petalDistance, petalSize);

  // верхня права пелюстка
  ...

  // нижня ліва пелюстка
  ...

  // нижня права пелюстка
  ...

  // центральна пелюстка
  fill(255, 210, 63);
  circle(flowerX, flowerY, petalSize);
}
  1. Використати повний код застосунку з попереднього завдання для малювання квітки у випадкових точках полотна щоразу іншого розміру.

Застосувати функцію random() для створення випадкових значень для flowerX, flowerY і petalSize. У функції setup() використати функцію frameRate(1) (вказує кількість кадрів, які відображаються щосекунди) для створення повільної анімації.
  1. Використати повний код для малювання квітки та експериментальним шляхом дізнатися, як можна досягнути результату як у демонстрації. Пояснити отриманий результат.

Екстремальний

  1. Створити застосунок, який малює на полотні випадкові за розміром, кольором і місцем розташування кола різної прозорості.

  1. Створити імітацію вогнів нічного міста як у демонстрації.

Створити три еліпси різних кольорів. Замість функції background() у draw() використати функцію побудови прямокутника відповідно розмірів полотна та з певним значенням прозорості. В разі потреби у функції setup() вказати частоту кадрів за допомогою функції frameRate().

3.3. Основні форми: точка, лінія, прямокутник, еліпс

3.3.1. Примітиви

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

Примітиви: точка, лінія, прямокутник, еліпс
Примітиви: точка, лінія, прямокутник, еліпс

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

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

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

Для малювання точки на полотні використовується функція point() у такий спосіб: point(x, y), де x та y - це координати цієї точки на полотні.

Продемонструємо точку на нашому аркуші у клітинку.

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

Малювання лінії вимагає двох точок

Лінія
Лінія

Прямокутники можна малювати за допомогою функції rect() у різних режимах, які встановлюються за допомогою функції rectMode() .

Режим rectMode(CORNER), що використовується за стандартним налаштуванням, інтерпретує перші два параметри у rect() як координати лівого верхнього кута прямокутника, тоді як третім і четвертим параметрами є ширина та висота прямокутника.

Прямокутник
Прямокутник: перші два параметри - координати лівого верхнього кута прямокутника, третій і четвертий - ширина та висота прямокутника

Також, прямокутник можна створювати у режимах:

  • rectMode(CENTER)

Прямокутник
Прямокутник: перші два параметри - координати центру прямокутника, третій і четвертий параметри - ширина та висота прямокутника
  • rectMode(CORNERS)

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

Вправа 14

Заповнити прогалини у функціях rect(), щоб отримати прямокутники із вказаними у коментарях властивостями. Скористатися поданим зразком коду.

function setup() {
  createCanvas(650, 200);
}

function draw() {
  background(255);
  fill(0, 255, 0, 25); // Green
  rect(20, 50, ..., ...); // квадрат
  rect(180, 50, ..., ..., ...); // квадрат із заокругленими кутами, кожен з яких має однаковий радіус заокруглення
  rect(340, 50, ..., ..., ..., ..., ..., ...); // квадрат із заокругленими кутами, які мають різні радіуси заокруглення
  rect(500, 75, ..., ...); // звичайний прямокутник, витягнутий по горизонталі
}

Для малювання еліпса (овалу) застосовують функцію ellipse() , а сама концепція малювання є схожою як і для прямокутника, з різницею у назві функції для встановлення режимів малювання ellipseMode() .

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

Еліпс з однаковою шириною та висотою - це коло.

Отже, для створення еліпса можна використовувати наступні режими:

  • ellipseMode(CENTER)

Еліпс
Еліпс: перші два параметри - координати центру еліпса, третій і четвертий параметри - ширина та висота еліпса
  • ellipseMode(CORNER)

Еліпс
Еліпс: перші два параметри - координати лівого верхнього кута еліпса, третій і четвертий - ширина та висота еліпса
  • ellipseMode(CORNERS)

Еліпс
Еліпс: перші два параметри - координати лівого верхнього кута еліпса, третій і четвертий параметри - координати діагонально протилежного кута

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

Вправа 15

Заповнити прогалини у функціях ellipse(), щоб отримати еліпси різних форм: у формі кола, витягнутий по горизонталі й витягнутий по вертикалі. Скористатися поданим зразком коду.

function setup() {
  createCanvas(480, 200);
}

function draw() {
  background(255);
  fill(255, 204, 0); // Sunglow
  ellipse(80, 100, ..., ...);
  ellipse(240, 100, ..., ...);
  ellipse(400, 100, ..., ...);
}

Можна виділити ще кілька інструментів для малювання інших примітивів.

Інші примітиви
Інші примітиви: трикутник, дуга, багатокутник, крива

Для малювання трикутника, використовують функцію triangle() , яка приймає шість параметрів - координати вершин трикутника, а багатокутник quad() має аж вісім параметрів-координат.

Вправа 16

Поекспериментувати зі значеннями для побудови трикутників і багатокутників. Скористатися поданим зразком коду.

function setup() {
  createCanvas(650, 600);
}

function draw() {
  background(255);
  stroke(0);
  // трикутники
  fill(0, 0, 255); // Blue
  triangle(..., ..., ..., ..., ..., ...);
  triangle(..., ..., ..., ..., ..., ...);
  // багатокутники
  fill(0, 255, 255); // Aqua
  quad(..., ..., ..., ..., ..., ..., ..., ...);
  quad(..., ..., ..., ..., ..., ..., ..., ...);
}

Для малювання дуги (сектора), використовують функцію arc() , для якої необхідно вказати координати центру, ширину і висоту, початковий і кінцевий кут в радіанах.

Цікавимось

Градуси та радіани

Деякі вбудовані функції p5.js використовують значення кутів, які необхідно зазначати в радіанах.

Радіан - одиниця вимірювання кутів, визначених відношенням довжини дуги кола до радіуса цього кола. Один радіан (1 рад ≈ 57.3°) - це кут, під яким це співвідношення дорівнює одиниці.

Інші значення градусів, поданих в радіанах

  • 45° = π/4 рад

  • 90° = π/2 рад

  • 180° = π рад

  • 360° = рад

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

Радіани
Радіани

Число π дорівнює ≈ 3.14159265358979323846.

Якщо позначити g - значення кута у градусах і θ - значення кута в радіанах, можна використати формули нижче, щоб радіани перетворити у градуси

\$g = (theta * 180) / π\$

і, навпаки, градуси перетворити у радіани

\$theta = g * π / 180\$
У бібліотеці p5.js математична константа π представлена константою PI .

Окрім того, значення кута в радіанах можна записувати за допомогою вбудованих констант p5.js для градусних величин:

  • 45° (QUARTER_PI);

  • 90° (HALF_PI);

  • 180° (PI);

  • 360° (TWO_PI або TAU).

Для перетворення градусів у радіани можна скористатися функцією radians() .

Вправа 17

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

function setup() {
  createCanvas(200, 200);
  background(245);
}

function draw() {
  let c = color(25, 144, 123, 191); // Illuminating Emerald
  fill(c);
  arc(100, 100, 50, 50, 0, ...);
  noFill();
  arc(100, 100, 80, 80, ..., ...);
  arc(100, 100, 110, 110, ..., PI + QUARTER_PI);
  arc(100, 100, 140, 140, PI + QUARTER_PI, ...);
}
Використання градусних констант
Використання градусних констант

Геометричні примітиви мають властивості - це товщина лінії та згладжування (англ. anti-aliasing).

Відповідно, для згладжування (увімкнене за стандартним налаштуванням у 2D-режимі) використовується функція smooth() , а щоб вимкнути згладжування застосовують функцію noSmooth() .

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

Вправа 18

У поданому зразку коду змінити параметри фігур відповідно до інформації у коментарях.

function setup() {
  createCanvas(480, 200);
  background(245);
}

function draw() {
  fill(23, 195, 178); // Tiffany Blue
  ...; // вимкнути згладжування
  strokeWeight(...); // зменшити товщину ліній до 1 пікселя
  ellipse(75, 100, 90, 90);
  strokeWeight(...); // збільшити товщину ліній до 8 пікселів
  ellipse(175, 100, 90, 90);
  strokeWeight(...); // збільшити товщину ліній до 14 пікселів
  ellipse(279, 100, 90, 90);
  strokeWeight(...); // збільшити товщину ліній до 20 пікселів
  ellipse(389, 100, 90, 90);
}
Якщо для фігур встановлено деякі параметри, усі фігури, які описані у коді далі за ними, будуть відображатися відповідно цих параметрів.

3.3.2. Криві

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

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  strokeWeight(3);
  noFill();
  image(grid, 0, 0);
}

function draw() {
  stroke(0, 233, 245); // Electric Blue
  line(200, 200, 600, 600);
}
Малювання лінії за допомогою line()
Малювання лінії за допомогою line()

Для креслення кривих використовують функцію curve() .

Функцію curve() можна використати для малювання прямої лінії (накладається зверху):

function draw() {
  stroke(0, 233, 245); // Electric Blue
  line(200, 200, 600, 600);
  stroke(249, 199, 79); // Maize Crayola
  curve(0, 0, 200, 200, 600, 600, 500, 500);
}
Малювання лінії за допомогою curve()
Малювання лінії за допомогою curve()

Як видно, чотири значення координат усередині функції curve() збігаються зі значеннями у функції line(). Ці координати вказують початкову та кінцеву точку кривої (у цьому разі кривою є пряма лінія 😉).

Функція curve() приймає чотири додаткові зовнішні значення (0, 0 і 500, 500), які визначають дві пари координат контрольних точок. Положення цих контрольних точок визначають напрямок і величину кривини лінії.

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

function draw() {
  stroke(0, 233, 245); // Electric Blue
  line(200, 200, 600, 600);
  stroke(249, 199, 79); // Maize Crayola
  curve(100, 525, 200, 200, 600, 600, 800, 400);

  // контрольні точки
  stroke(255, 128, 64); // Fern Green
  fill(255);
  circle(100, 525, 10);
  circle(800, 400, 10);
  noFill();
}
Малювання кривої за допомогою curve()
Малювання кривої за допомогою curve(): контрольні точки

У цьому разі функція curve() приймає інші дві пари координат контрольних точок - 100, 525 і 800, 400 відповідно. За такої умови, чотири середні значення у функції curve() залишаються без змін.

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

function draw() {
  stroke(0, 233, 245); // Electric Blue
  line(200, 200, 600, 600);
  stroke(249, 199, 79); // Maize Crayola
  curve(100, 525, 200, 200, 600, 600, 800, 400);
  stroke(6, 214, 160); // Caribbean Green
  curve(0, 400, 100, 525, 200, 200, 500, 500);
  curve(0, 0, 600, 600, 800, 400, 650, 650);

  // контрольні точки
  stroke(255, 128, 64); // Mango Tango
  fill(255);
  circle(100, 525, 10);
  circle(800, 400, 10);
  noFill();
}
Малювання кривої за допомогою curve()
Малювання кривої за допомогою curve(): контрольні точки визначають кривину жовтої частини кривої

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

Контрольні точки впливають на криву таким способом, що кожен кінець жовтої кривої тягнеться до сусідньої контрольної точки. Чим ближче наближати контрольну точку до центру полотна, тим сильніше крива буде «згинатися» і навпаки.

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

Цікавимось

Сплайн

Функція curve() є реалізацією сплайнів Catmull - Rom .

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

Лекало для креслення, лекала кравця, сплайн
Лекало для креслення, лекала кравця, сплайн

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

Використовуючи функцію curveTightness() , можна змінювати властивості кривих.

Функція curveTightness() визначає, як крива наближається до точок власних вершин.

Наприклад, значення 0.0 є значенням за стандартним налаштуванням (це значення визначає криві як сплайни Кетмулла-Рома), а значення 1.0 пов’язує всі точки прямими лініями.

Значення з діапазону (-5.0, 5.0) деформують криві, але залишають їх впізнаваними, а зі збільшенням значень криві продовжують деформуватися.

function draw() {
  stroke(0, 233, 245); // Electric Blue
  line(200, 200, 600, 600);

  curveTightness(1);
  stroke(249, 199, 79); // Maize Crayola
  curve(100, 525, 200, 200, 600, 600, 800, 400);

  stroke(6, 214, 160); // Caribbean Green
  curve(0, 400, 100, 525, 200, 200, 500, 500);
  curve(0, 0, 600, 600, 800, 400, 650, 650);
}
Використання функції curveTightness()
Використання функції curveTightness(): значення -1.0, 0.0, 1.0 і 4.0

3.3.3. Криві Безьє

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

Цікавимось

Растрові та векторні зображення

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

Растрова графіка використовується у тих випадках, коли потрібно якісно передати повну гаму відтінків кольорів зображення.

Оскільки растрові зображення засновані на пікселях, вони залежать від роздільної здатності - величини, що визначає кількість пікселів на одиницю площі (або одиницю довжини).

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

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

Поширені формати растрових зображень: JPG, TIFF, GIF, PNG, BMP.

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

Наприклад, рівняння для кола з центром (a, b) та радіусом r записується так:

\$(x - a)^2 + (y - b)^2 = r^2\$

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

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

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

Масштабування растрової (ліворуч) і векторної (праворуч) графіки
Масштабування растрової (ліворуч) і векторної (праворуч) графіки

Поширені формати векторних файлів: EPS, SVG.

Формат векторної графіки SVG використовує криві Безьє для надання масштабованості для фігур будь-якого розміру без втрати якості зображення.

Криві Безьє забезпечують гладке моделювання кривих з використанням якірних (через які проходить крива) і контрольних (які задають форму кривої) точок.

Якірні та контрольні точки кривої Безьє
Крива Безьє: якірні та контрольні точки керують положенням та кривиною

Для побудови кривої Безьє у бібліотеці p5.js визначена функція bezier() , яка має низку параметрів.

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

Параметри, що містяться усередині, визначають дві контрольні точки, які утворюють форму кривої.

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

Намалюємо криву Безьє, використовуючи змінні x2, y2, x3, y3 з однаковими значеннями для пар координат контрольних точок.

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  strokeWeight(3);
  noFill();
  image(grid, 0, 0);
}

function draw() {
  let x2, y2, x3, y3;
  x2 = 100;
  y2 = 100;
  x3 = 100;
  y3 = 100;
  stroke(0, 233, 245); // Electric Blue
  bezier(100, 100, x2, y2, x3, y3, 500, 300);

  // якірні точки
  stroke(255, 128, 64); // Mango Tango
  fill(255);
  circle(100, 100, 10);
  circle(500, 300, 10);
  noFill();
}
Лінія - випрямлена крива Безьє
Лінія - випрямлена крива Безьє

Перетворимо отриману пряму лінію на криву, встановивши інші значення для змінних x2, y2, x3, y3, і намалюємо лінії, які сполучають контрольні і якірні точки.

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  strokeWeight(3);
  noFill();
  image(grid, 0, 0);
}

function draw() {
  let x2, y2, x3, y3;
  x2 = 250;
  y2 = 400;
  x3 = 330;
  y3 = 50;
  stroke(0, 233, 245); // Electric Blue
  bezier(100, 100, x2, y2, x3, y3, 500, 300);
  stroke(230, 100, 27); // Spanish Orange
  line(100, 100, x2, y2);
  line(500, 300, x3, y3);

  // якірні точки
  stroke(255, 128, 64); // Mango Tango
  fill(255);
  circle(100, 100, 10);
  circle(500, 300, 10);

  // контрольні точки
  circle(250, 400, 10);
  circle(330, 50, 10);
  noFill();
}
Крива Безьє
Крива Безьє: якірні та контрольні точки
Попрактикуватись у створенні кривих Безьє можна у вільному редакторі векторної графіки Inkscape за допомогою інструмента Малювання кривих Безьє чи пограти у гру The Bézier Game .

3.3.4. Вершини фігур

Які відомо, вершина фігури - це точка, яка використовується для з’єднання ліній фігури. Наприклад, для трикутника потрібні 3 вершини, для квадрата - 4, для зірки ⭐ - 10. Використовуючи криві для з’єднання вершин, можна створювати й власні форми.

Для цих цілей у бібліотеці p5.js є спеціальні функції: beginShape() , vertex() та endShape() .

Розглянемо код, який малює точно таку ж фігуру, як і функція rect(), встановлюючи кожну із вершин (vertex) прямокутника окремо.

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  strokeWeight(3);
  noFill();
  image(grid, 0, 0);
}

function draw() {
  stroke(0, 233, 245); // Electric Blue
  beginShape(); (1)
  vertex(200, 200); (2)
  vertex(500, 200);
  vertex(500, 400);
  vertex(200, 400);
  endShape(CLOSE); (3)
}
1 Функція beginShape() вказує, що необхідно створити власну фігуру, яка складається з деякої кількості вершин.
2 Функція vertex() визначає точки для кожної з вершин прямокутника.
3 Функція endShape() вказує на те, що додавання вершин завершено. Значення CLOSE усередині endShape(CLOSE) сигналізує, що остання точка вершини повинна з’єднатися з першою.
Побудова прямокутника за допомогою вершин
Побудова прямокутника за допомогою вершин

Після виклику функції beginShape() повинна слідувати серія команд vertex(). Щоб завершити малювання фігури, необхідно викликати endShape(). Кожна фігура буде окреслена поточним кольором для контуру і заповнена кольором, якщо заповнення увімкнено.

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

У beginShape() можна додати значення, вказуючи який саме тип фігури необхідно створити.

Наприклад, якщо створюється шість вершин, бібліотека p5.js не може знати, що потрібно намалювати: два трикутники чи один шестикутник, якщо не вказати beginShape(TRIANGLES).

Якщо ж необхідно намалювати точки або лінії, то використовуються команди beginShape(POINTS) або beginShape(LINES) відповідно.

function draw() {
  stroke(0, 233, 245); // Electric Blue

  // точки
  strokeWeight(10);
  beginShape(POINTS);
  vertex(200, 200);
  vertex(500, 200);
  vertex(500, 400);
  vertex(200, 400);
  endShape(CLOSE);

  // окремі лінії
  strokeWeight(3);
  beginShape(LINES);
  vertex(200, 500);
  vertex(300, 600);
  vertex(400, 600);
  vertex(500, 500);
  endShape(CLOSE);

  // трикутники
  beginShape(TRIANGLES);
  vertex(350, 100);
  vertex(100, 500);
  vertex(600, 500);
  vertex(350, 100);
  endShape(CLOSE);
}
Побудова фігур на основі вершин
Побудова фігур на основі вершин
Значеннями для beginShape() можуть бути: POINTS, LINES, TRIANGLES, TRIANGLE_FAN, TRIANGLE_STRIP, QUADS, QUAD_STRIP і TESS (лише для WEBGL). LINES призначений для малювання серії окремих ліній, а не безперервної лінії. Для малювання неперервної лінії значення у функції не вказують.

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

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  image(grid, 0, 0);
}

function draw() {
  // амеба
  noStroke();
  fill(244, 162, 97); // Sandy Brown

  beginShape();
  curveVertex(170, 330);
  curveVertex(165, 310);
  curveVertex(175, 300);
  curveVertex(200, 290);
  curveVertex(250, 294);
  curveVertex(290, 250);
  curveVertex(280, 150);
  curveVertex(330, 120);
  curveVertex(370, 150);
  curveVertex(380, 230);
  curveVertex(470, 220);
  curveVertex(495, 240);
  curveVertex(420, 320);
  curveVertex(435, 335);
  curveVertex(450, 360);
  curveVertex(440, 400);
  curveVertex(460, 450);
  curveVertex(440, 470);
  curveVertex(360, 440);
  curveVertex(270, 490);
  curveVertex(250, 450);
  curveVertex(250, 400);
  curveVertex(190, 360);
  curveVertex(170, 330);
  endShape(CLOSE);

  fill(250, 211, 179); // Peach Puff
  circle(345, 340, 85);
  fill(255, 255, 255); // White
  circle(325, 320, 20);
}
Амеба
Амеба

Ще одна функція, яка дозволяє малювати криві між вершинами більш гладкими, - це bezierVertex() .

Функція вказує координати вершин для кривих Безьє. Кожен виклик bezierVertex() визначає положення двох контрольних точок і однієї якірної точки кривої Безьє.

Відразу після функції beginShape() і перед bezierVertex() слід додати виклик vertex() для встановлення початкової вершини.
sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  image(grid, 0, 0);
}

function draw() {
  // крива
  noFill();
  stroke(0, 233, 245); // Electric Blue

  beginShape(); (1)
  vertex(200, 100); // початкова вершина
  bezierVertex(
    100,
    200, // контрольна точка для початкової вершини
    300,
    400, // контрольна точка для кінцевої вершини
    200,
    500 // кінцева вершина
  );
  endShape();

  // контрольні точки кривої (2)
  stroke(244, 162, 97); // Sandy Brown
  line(100, 200, 200, 100);
  line(300, 400, 200, 500);

  fill(244, 162, 97); // Sandy Brown
  circle(100, 200, 10);
  circle(300, 400, 10);

  // сердечко
  noStroke();
  fill(239, 71, 111); // Paradise Pink

  // ліва частина сердечка
  beginShape(); (3)
  vertex(500, 400);
  bezierVertex(320, 300, 450, 150, 500, 250);
  endShape(CLOSE);

  // контрольні точки сердечка (4)
  stroke(244, 162, 97); // Sandy Brown
  line(500, 400, 320, 300);
  line(500, 400, 680, 300);
  line(500, 250, 450, 150);
  line(500, 250, 550, 150);

  fill(244, 162, 97); // Sandy Brown
  circle(320, 300, 10);
  circle(680, 300, 10);
  circle(450, 150, 10);
  circle(550, 150, 10);
}
1 Крива складається з двох вершин, кожна з яких приєднана до своєї власної контрольної точки.
2 Візуалізація контрольних точок кривої.
3 Ліва частина сердечка.
4 Візуалізація контрольних точок сердечка.
Вершини кривих Безьє
Вершини кривих Безьє
Для зміни властивостей ліній, їх з’єднань і візуалізації кінців відрізків, використовують функції strokeJoin() і strokeCap() .

Вправа 19

Намалювати праву частину сердечка.

Цікавимось Додатково

Складні форми фігур

Використовуючи вершини, можна малювати складніші форми фігур.

Наприклад, створимо форму круглої монети з отвором, яка використовувалася у Стародавньому Китаї.

10 Цянів (Qian), бронза, 1102-1106 роки
10 Цянів (Qian), бронза, 1102-1106 роки

Для наших цілей використаємо дві функції з бібліотеки p5.js: beginContour() і endContour() .

Ці функції використовуються для створення контурів фігур всередині інших фігур.

Отож, форма внутрішньої фігури створює отвір у зовнішній. Вершини, що визначають форму контуру, повинні «намотуватися» у напрямку, протилежному від зовнішньої форми.

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

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  image(grid, 0, 0);
}

function draw() {
  stroke(208, 0, 0);

  // центр кола
  let x = 350;
  let y = 350;

  let r = 250; // радіус кола
  let d = 0.552 * r; // відстань від якірної точки до контрольної точки дорівнює 55.2% радіуса кола

  //circle(x, y, 2 * r); // перевірка вбудованою функцією

  // координати якірних точок кола за годинниковою стрілкою
  let x1, y1, x2, y2, x3, y3, x4, y4;

  // зліва
  x1 = 100;
  y1 = 350;

  // вгорі
  x2 = 350;
  y2 = 100;

  // справа
  x3 = 600;
  y3 = 350;

  // унизу
  x4 = 350;
  y4 = 600;

  // коло
  noStroke();
  fill(181, 228, 140); // Yellow Green Crayola
  beginShape();
  vertex(x1, y1);
  bezierVertex(x1, y1 - d, x2 - d, x1, x2, y2);
  bezierVertex(x2 + d, y2, x3, y3 - d, x3, y3);
  bezierVertex(x3, y3 + d, x2 + d, y4, x4, y4);
  bezierVertex(x2 - d, y4, x1, y3 + d, x1, y1);

  // отвір у формі квадрата, рух проти годинникової стрілки
  beginContour();
  vertex(280, 280);
  vertex(280, 420);
  vertex(420, 420);
  vertex(420, 280);
  endContour();
  endShape(CLOSE);

  // візуалізація контрольних точок кола
  /*
  stroke(244, 162, 97); // Sandy Brown
  line(x1, y1, x1, y1 - d);
  line(x2, y2, x2 - d, x1);
  line(x2, y2, x2 + d, y2);
  line(x3, y3, x3, y3 - d);
  line(x3, y3, x3, y3 + d);
  line(x4, y4, x2 + d, y4);
  line(x4, y4, x2 - d, y4);
  line(x1, y1, x1, y3 + d);

  fill(244, 162, 97); // Sandy Brown
  circle(x1, y1 - d, 10);
  circle(x2 - d, x1, 10);
  circle(x2 + d, y2, 10);
  circle(x3, y3 - d, 10);
  circle(x3, y3 + d, 10);
  circle(x2 + d, y4, 10);
  circle(x2 - d, y4, 10);
  circle(x1, y3 + d, 10);
  */
}
Монета з отвором
Монета з отвором

3.3.6. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «примітиви»?

  2. Яке призначення функцій point(), line(), rect(), ellipse(), triangle(), arc(), quad(), smooth(), strokeWeight() бібліотеки p5.js?

  3. Яке призначення функцій curve(), bezier(), beginShape(), endShape(), vertex(), curveVertex(), bezierVertex()?

  4. Для чого використовуються «криві Безьє»?

  5. Що називається контрольними і якірними точками у контексті «кривої лінії»?

  6. Як створюються складні фігури, для яких у бібліотеці p5.js не визначені готові функції?

3.3.7. Практичні завдання

Початковий

  1. Заповнити прогалини в коді для створення зображення як на малюнку.

Дорожній знак
Дорожній знак
function setup() {
  // створення полотна 200x200
  ...
  // колір тла полотна (1, 186, 240) // Cyan Process
  ...
  // інтерпретує перші два параметри прямокутника як центральну точку фігури, тоді як третій і четвертий параметри - це її ширина та висота
  rectMode(CENTER);
}

function draw() {
  // оголошення змінних
  let x = width / 2;
  let y = height / 2;

  // малювання кола
  // колір заливки кола (237, 34, 93) // Paradise Pink
  ...
  // вимкнення межі кола
  noStroke();
  // малювання еліпса з координатами (x, y, 180, 180)
  ...

  // малювання прямокутника
  // колір заливки прямокутника (255)
  ...
  // малювання прямокутника з координатами (x, y, 130, 30)
  ...
}
  1. Створити малюнок з примітивів, використовуючи сторінку зошита в клітинку або файл електронної таблиці відповідно до поданого коду.

line(0, 1, 8, 9);
point(1, 4);
point(2, 5);
point(6, 5);
point(7, 6);
rectMode(CORNER);
rect(5, 1, 4, 3);
ellipseMode(RADIUS);
ellipse(2, 8, 10, 6);

Середній

  1. Заповнити прогалини в коді для створення анімованого зображення як у демонстрації.

let count = 0; // ініціалізація глобальної змінної

function setup() {
  let canvasWidth = 200;
  let canvasHeight = 200;
  // створення полотна 200x200
  ...
  // колір тла полотна (1, 186, 240) // Cyan Process
  ...
  // інтерпретує перші два параметри прямокутника як центральну точку фігури, тоді як третій і четвертий параметри - це її ширина та висота
  rectMode(CENTER);
}

function draw() {
  // оголошення локальних змінних
  let x = width / 2;
  let y = height / 2;
  let size = 100 + count; // зміна розміру фігур

  // коло
  fill(237, 34, 93); // Paradise Pink
  noStroke();
  ellipse(x, y, size, size);

  // прямокутник
  fill(255);
  rect(x, y, size * 0.75, size * 0.15);

  count = count + 1; // збільшення значення на 1
}

Високий

  1. Створити застосунок, який щоразу генерує іншу випадкову квітку.

  1. Використати код попереднього завдання для генерації безлічі випадкових квітів.

  1. Написати код для створення примітивів відповідно до поданої на малюнку схеми. Для даного завдання може бути кілька правильних відповідей.

Напишіть код на основі малюнку
Напишіть код на основі малюнку

Екстремальний

  1. Створити візуальне представлення використання дискового простору власного смартфона у вигляді кільцевої діаграми. Орієнтовний зразок представлений на малюнку.

Дисковий простір смартфона
Дисковий простір смартфона

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

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

3.4.1. Розгалуження

Досить часто у програмуванні окремі фрагменти коду повинні виконуватися лише за певної умови.

Умову можна описати як висловлення, про яке можна однозначно сказати істинне воно (англ. true - правда) чи хибне (англ. false - хибність).

Наприклад:

  • «Мова JavaScript є інтерпретованою мовою програмування.» (Відповідь «так», тому висловлення істинне і значення умови true).

  • « JavaScript і Java - це схожі назви мови програмування JavaScript.» (Відповідь «ні», тому висловлення хибне і значення умови false).

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

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

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

if (умова) {
  // інструкції, які виконуються, якщо умова істинна (true),
  // не виконуються, якщо умова хибна (false)
}

Така форма розгалуження називається неповною.

Умова містить логічний вираз, в результаті обчислення якого одержується значення false або true.

Значення true і false (зверніть увагу, значення записуються з малої літери) є булевими значеннями або значеннями типу даних Boolean.

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

Для побудови логічних виразів використовують оператори порівняння.

Таблиця "Оператори порівняння у JavaScript"

Оператор

Опис

>

більше

<

менше

>=

більше або дорівнює

<=

менше або дорівнює

==

дорівнює (перевірка на значення, перетворення типів)

!=

не дорівнює (перевірка на значення, перетворення типів)

===

дорівнює (перевірка на ідентичність, без перетворення типів)

!==

не дорівнює (перевірка на ідентичність, без перетворення типів)

Цікавимось

Оператори =, == і ===

Як ми вже знаємо, у мові JavaScript використовується оператор присвоєння, що позначається символом =.

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

Оператори == і === порівнюють два значення і на основі результату цього порівняння повертають значення true або false.

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

Наприклад, якщо порівнювати значення двох операндів a = 2 (тип Number) і b = "2" (тип String), використовуючи ==, спочатку відбувається перетворення типів, тобто JavaScript перетворює значення другого операнда b у число і результат порівняння буде true.

У разі потрійного дорівнює ===, два значення порівнюються без перетворення типів. Якщо значення мають різні типи, то вони не можуть бути рівними. Тому і результатом порівняння a === b буде false.

Слід зазначити, що такі особливості порівняння застосовні лише для простих типів даних.

Для будь-яких двох об’єктів x і y, які мають ідентичні структури, але є двома окремими об’єктами (імена об’єктів x і y є покликаннями на різні об’єкти), оператори порівняння повернуть false.

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

Використаємо консоль вебпереглядача (Ctrl+Shift+I) і функцію console.log(), щоб поглянути на роботу операторів порівняння.

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

console.log(10 > 5);      // true
console.log(12 > 60);     // false
console.log(40 == 40);    // true
console.log(40 == "40");  // true
console.log(25 === "25"); // false
console.log(5 === 5);     // true
console.log(15 >= 10);    // true
console.log(200 !== 200); // false
console.log(1 !== 0);     // true

Подивимось, як реалізується неповна форма розгалуження у наступному прикладі.

sketch.js
function setup() {
  createCanvas(200, 200); (1)
  background(220); (2)
  noStroke(); (3)
}

function draw() {
  if (mouseX > width / 2) { (4)
    fill(57, 20, 99); // Persian indigo
    rect(width / 2, 0, width / 2, height);
  }
}

Прочитати даний код можна так:

1 Створити полотно розміром 200x200 пікселів.
2 Встановити тло сірого кольору.
3 Вимкнути межі для фігур.
4 Якщо вказівник миші знаходиться справа від точки width / 2 екрану, то намалювати прямокутник без контуру із заливкою кольору Persian indigo праворуч від цієї точки.

Переглядаємо Аналізуємо

Повна форма розгалуження має такий запис:

if (умова) {
  // інструкції, які виконуються, якщо умова істинна (true)
} else {
  // інструкції, які виконуються, якщо умова хибна (false)
}

Використаємо у коді повну форму розгалуження.

sketch.js
function setup() {
  createCanvas(200, 200); (1)
  noStroke(); (2)
}

function draw() {
  if (mouseX > width / 2) { (3)
    fill(57, 20, 99); // Persian indigo
    rect(width / 2, 0, width / 2, height);
  } else { (4)
    background(220);
  }
}

Прочитати даний код можна так:

1 Створити полотно розміром 200x200 пікселів.
2 Вимкнути межі для фігур.
3 Якщо вказівник миші знаходиться справа від точки width / 2 екрану, то намалювати прямокутник без контуру із заливкою кольору Persian indigo справа від цієї точки.
4 Якщо вказівник миші знаходиться зліва від точки width / 2 екрану, то увесь простір полотна зафарбувати сірим кольором.

Переглядаємо Аналізуємо

Для перевірки кількох умов у вказівці розгалуження можна використовувати у парі зарезервовані слова else if у такому вигляді:

if (умова1) {
  // інструкції, які виконуються, якщо умова1 істинна (true)
} else if (умова2) {
  // інструкції, які виконуються, якщо умова2 істинна (true)
} else if (умова3) {
  // інструкції, які виконуються, якщо умова3 істинна (true)
} else if (умова4) {
  // інструкції, які виконуються, якщо умова4 істинна (true)
} else {
  // інструкції, які виконуються, якщо усі умови хибні (false)
}
Як тільки умова є істинною, виконуються відповідні інструкції, а інші умови ігноруються.
Вказівка розгалуження if обчислює логічні вирази в умові та в залежності від отриманого значення (істина чи хибність) спрямовує виконання застосунку у відповідну гілку. Для значень undefined, 0 і '' (порожній рядок) вказівка розгалуження визначає результат як false, а для значень, наприклад, 41 і 'hello world' - як true.

Використаємо кілька умов у нашому коді.

sketch.js
function setup() {
  createCanvas(200, 200);
  stroke(255);
}

function draw() {
  if (mouseX < width / 3) {
    background(173, 232, 244); // Blizzard Blue
  } else if (mouseX < (2 * width) / 3) {
    background(0, 150, 199); // Blue Green
  } else {
    background(3, 4, 94); // Midnight Blue
  }
  line(width / 3, 0, width / 3, height);
  line((2 * width) / 3, 0, (2 * width) / 3, height);
}

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

Переглядаємо Аналізуємо

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

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

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

sketch.js
let x = 25;
let d = 50;
let velocity = 0.5;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(255);

  if (x > width - d / 2 || x < d / 2) {
    velocity = velocity * -1;
  }

  stroke(0);
  fill(19, 133, 53); // Forest Green Web
  ellipse(x, height / 2, d, d);

  x = x + velocity;
}

Переглядаємо Аналізуємо

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

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

Чому у цих двох логічних виразах використовується значення радіуса кульки d / 2? Це робиться для того, щоб умова зміни напрямку перевірялася для випадку, коли кулька не виходить за вертикальні межі полотна, а лише торкається цих меж.

Умова складається з двох логічних виразів: х більше ширини полотна мінус радіус кульки та x менше радіуса кульки. Тобто, умова є складеною з двох простих умов, з’єднаних логічним оператором АБО (||).

Складена умова у разі використання || буде істинною, якщо принаймні одна із простих умов буде істинною.

У нашому прикладі, якщо кулька намагається вийти за ліву межу полотна, проста умова x < d / 2 стане істинною і складена умова поверне значення true.

Аналогічно, якщо кулька досягне правої межі полотна і намагатиметься вийти за цю межу, проста умова x > width - d / 2 стане істинною і складена умова поверне значення true.

Поруч з логічним оператором || використовуються й інші логічні оператори: І (&&) і НЕ (!).

У разі використання && умова буде істинною, якщо усі прості умови будуть істинними.

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

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

true && true   // true
true && false  // false
true || false  // true
false || false // false
!true          // false
!false         // true

Вправа 20

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

Цікавимось

Моделювання руху кульки вертикально із гальмуванням на поверхні

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

розташування = розташування + швидкість

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

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

швидкість = швидкість + прискорення

Код застосунку, який імітує падіння кульки додолу, може мати такий вигляд:

sketch.js
let x = 100;
let y = 0;
let d = 50;

let velocity = 0;
let gravity = 0.1;

function setup() {
  createCanvas(200, 200);
  noStroke();
}

function draw() {
  background(255);
  fill(19, 133, 53); // Forest Green Web

  ellipse(x, y, d, d);

  if (y > height - d / 2) {
    velocity = velocity * -0.95;
    y = height - d / 2;
  }

  y = y + velocity;
  velocity = velocity + gravity;
}

Множення на -0,95 замість –1 уповільнює кульку кожного разу, коли вона відскакує, тобто, її швидкість зменшується.

Переглядаємо Аналізуємо

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

sketch.js
let moment; (1)
const name = "user"; (2)

function setup() {
  createCanvas(200, 100);
  textSize(18); (3)
  textAlign(CENTER, CENTER); (4)
  frameRate(1); (5)
}

function draw() {
  noStroke();
  fill(255);
  moment = random(0, 25); (6)
  if (moment >= 5 && moment < 11) { (7)
    background(224, 177, 203); // Pink Lavender
    text(`Good morning, ${name}!`, width / 2, height / 2); (8)
  } else if (moment >= 11 && moment < 16) {
    background(190, 149, 196); // Lilac
    text(`Good day, ${name}!`, width / 2, height / 2);
  } else if (moment >= 16 && moment < 24) {
    background(94, 84, 142); // Purple Navy
    text(`Good evening, ${name}!`, width / 2, height / 2);
  } else if (moment == 24 || (moment >= 0 && moment < 5)) {
    background(59, 42, 113); // Spanish Violet
    text(`Good night, ${name}!`, width / 2, height / 2);
  } else {
    background(159, 134, 192); // Purple Mountain Majesty
    text(`Hi, ${name}!`, width / 2, height / 2);
  }
}
1 Оголошення змінної moment, яка буде зберігати значення часу доби в годинах.
2 Оголошення константи name зі значенням імені користувача.
3 Встановлення за допомогою функції textSize() поточного розміру шрифту.
4 Встановлення поточного вирівнювання для тексту за допомогою функції textAlign() . Функція приймає значення LEFT, CENTER або RIGHT (для горизонтального вирівнювання, відносно значення x-координати функції text() ) і TOP, BOTTOM, CENTER і BASELINE (для вертикального вирівнювання, відносно значення y-координати функції text()).
5 Уповільнення анімації за допомогою функції frameRate() .
6 Генерування випадкового значення години доби в діапазоні від 0 до 25 (не включаючи 25). Результат роботи функції random() - число з рухомою крапкою, на зразок 0.007746035769928827, 18.804574102871392, 24.197599339160092.
7 Вказівка розгалуження if зі складеними умовами у логічних виразах. В умовах перевіряється чи входить згенероване значення години в певний проміжок часу (ранок, день, вечір, ніч).
8 Виведення текстового повідомлення відповідно до часу доби з ім’ям користувача за допомогою шаблонного літерала і вирівнювання тесту по центру полотна.

Цікавимось

Літерали в JavaScript

Значення в JavaScript називаються літералами. Це не змінні, а фіксовані значення, які буквально вказуються у коді.

Наприклад, логічний тип має два літеральні значення: true і false.

Цілочисельні літерали: 255, 0, 45.

Літерали з рухомою крапкою: 3.1415926, 0.5, .5.

Рядкові літерали: "foo", 'bar', "some text".

Літерал об’єкта: {x: 200, y: 300}.

У стандарті ES6 мови програмування JavaScript з’явилась можливість використовувати шаблонні літерали - звичайні рядки, які дозволяють використовувати вирази.

Для запису шаблонних літералів використовують зворотні лапки ` `.

Щоб вставити вираз або змінну у шаблонний літерал, необхідно у шаблонному рядку додати символ долара $ та фігурні дужки {}, всередині яких ввести сам вираз чи змінну.

let name = "Сашко";
let job = "розробник";
let tools = "JavaScript і p5.js";
// Мене звати Сашко. Я розробник і пишу на JavaScript і p5.js.
console.log(`Мене звати ${name}. Я ${job} і пишу на ${tools}.`);

Для розміщення значень кількох змінних в рядку можна також зустріти не дуже оптимальний спосіб вставки за допомогою оператора + і одинарних (' ') або подвійних (" ") лапок:

console.log("My name is " + name + " and I am a " + job + ". I write " + tools + ".");

Переглядаємо Аналізуємо

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

Це зручно реалізувати за допомогою вказівки розгалуження if і функції millis() , яка повертає значення лічильника часу.

Час вимірюється в мілісекундах (одна тисячна секунди), так, після секунди роботи застосунку лічильник часу матиме значення 1000, після 3 секунд - 3000, після хвилини - 60000 і т. д.

Наприклад, змусимо коло рухатися не відразу після запуску застосунку, а через 2 секунди після його запуску.

sketch.js
let t = 2000;
let x = 0;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  let ms = millis();
  if (ms > t) {
    x += 0.5;
  }

  fill(19, 133, 53); // Forest Green Web
  circle(x, height / 2, 50);
}

Переглядаємо Аналізуємо

Цікавимось

Конструкція switch

В мові JavaScript є конструкція switch, яка замінює собою відразу кілька вказівок розгалуження if.

Структура switch складається з одного або більше блоків case і необов’язкового блоку default:

switch(expression) {
  case 'value1':  // if (expression === 'value1')
    // ці інструкції виконуються, якщо результат expression відповідає value1 (за значенням і за типом)
    break;

  case 'value2':  // if (expression === 'value2')
    // ці інструкції виконуються, якщо результат expression відповідає з value2 (за значенням і за типом)
    break;

  default:
    // ці інструкції виконуються, якщо результат expression не відповідає жодному значенню
    break;
}

Значення змінної expression перевіряється на ідентичність першому значенню value1, потім другому value2 і т. д.

Оператор ідентичності (===) перевіряє, чи рівні два його операнди, повертаючи булевий результат. На відміну від оператора рівності (==), оператор === завжди вважає операнди різних типів різними.

Якщо відповідність встановлено - switch починає виконуватися для відповідного блоку case і далі, до найближчого break (або до кінця switch).

Якщо жоден case не відповідає expression - виконується (якщо є) варіант default.

Зарезервоване слово break зупиняє виконання switch. Виконання коду програми продовжується з місця, що розташоване після вказівки switch.

Кілька варіантів case, що використовують однаковий код, можна згрупувати.

switch (expression) {
  case 'value1':
    // ці інструкції виконуються, якщо результат expression відповідає value1 (за значенням і за типом)
    break;

  case 'value2': // групування case
  case 'value3':
    // ці інструкції виконуються, якщо результат expression відповідає value1 або value2 (за значенням і за типом)
    break;

  default:
    // ці інструкції виконуються, якщо результат expression не відповідає жодному значенню
    break;
}

3.4.2. Цикли

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

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

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

Цикл дозволяє виконувати блок коду скільки завгодно разів.

Функція draw() - це також цикл, який називається нескінченним, оскільки усі інструкції всередині цієї функції виконуються знову і знову, поки ми не зупинимо виконання застосунку.

Розглянемо такий код.

sketch.js
function setup() {
  createCanvas(400, 200);
  background(255);
}

function draw() {
  stroke(158, 0, 89); // Jazzberry Jam
  circle(100, 100, 15);
  circle(120, 100, 15);
  circle(140, 100, 15);
  circle(160, 100, 15);
  circle(180, 100, 15);
  circle(200, 100, 15);
  circle(220, 100, 15);
  circle(240, 100, 15);
  circle(260, 100, 15);
  circle(280, 100, 15);
}
Функція stroke() встановлює колір для малювання ліній та меж навколо фігур.

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

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

Оптимізуймо код.

sketch.js
function setup() {
  createCanvas(400, 200);
  background(255);
}

function draw() {
  stroke(158, 0, 89); // Jazzberry Jam

  let y, d;
  y = 100;
  d = 15;

  circle(100, y, d);
  circle(120, y, d);
  circle(140, y, d);
  circle(160, y, d);
  circle(180, y, d);
  circle(200, y, d);
  circle(220, y, d);
  circle(240, y, d);
  circle(260, y, d);
  circle(280, y, d);
}

Тепер змінити значення y і d для усіх команд дуже зручно, оскільки змінні знаходяться в одному місці. Як видно з коду, перше значення у виклику функції circle() також зазнає повторень - змінюється на 20 пікселів щоразу для малювання кожного наступного кола. Тому тут можна застосувати цикл.

while

Отож, використаємо у нашому коді один із циклів з назвою while.

sketch.js
function setup() {
  createCanvas(400, 200);
  background(255);
}

function draw() {
  stroke(158, 0, 89); // Jazzberry Jam

  let x, y, d;
  y = 100;
  d = 25;
  x = 100;
  i = 0;

  while (i < 10) { (1)
    circle(x, y, d); (2)
    x = x + 30; (3)
    i = i + 1; (4)
  }
}

Розглянемо на цьому прикладі структуру циклу while.

1 Зарезервоване слово while, після якого йде у круглих дужках умова, а далі, у фігурних дужках записують інструкції (тіло циклу), які будуть повторюватися. Цикл буде виконуватися до тих пір, доки значення логічного виразу умови не стане хибним (false). Тобто, як тільки значення простої умови i < 10 стане false, відбудеться вихід із циклу.
2 Функція малювання кола, у якій значення d і y не змінюються, а значення x змінюється на 30 пікселів.
3 Збільшення значення x на 30 пікселів.
4 Лічильник циклу - змінна, яка оголошена зі значенням 0 перед циклом, а у тілі циклу збільшується на 1. Коли лічильник набуде значення 10, відбудеться вихід з циклу.

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

while (умова) {
  // інструкції, які виконуються, доки умова істинна (true)
}

Вправа 21

Намалювати 5 кіл, розташованих вертикально одне під одним.

for

Ще одним різновидом циклічної структури в JavaScript є цикл for. Він дозволяє повторювати дії задану кількість разів.

Структура циклу for має наступну форму:

for (let i = 0; i < 10; i = i + 1) {
  // інструкції
}
  • let i = 0 - ініціалізація змінної i, яка буде відстежувати скільки разів цикл виконується - це змінна лічильника.

  • i < 10 - умова для циклу, яка перевіряється на істинність - логічний вираз в умові обчислюється щоразу (на кожній ітерації - кроці виконання циклу), коли цикл починається. У нашому прикладі виконується перевірка, чи змінна i менша від числа 10.

  • i = i + 1 - збільшення значення лічильника.

  • Всередині фігурних дужок записується код, який має повторюватися. Цей код - тіло циклу.

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

Описати словами хід виконання циклу for можна так:

  • Оголосити змінну i та встановити її початкове значення 0. Змінна i використовується лише у циклі.

  • Поки i менш як 10, повторити код у тілі циклу.

  • В кінці кожної ітерації (кроку циклу) додати одиницю до i.

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

Попри те, що нескінченні цикли є допустимим варіантом використання, цикли зазвичай використовуються для виконання операцій з відомою кількістю разів чи унаслідок виконання/невиконання умови.

Цікавимось

Оператори інкременту та декременту

Оновлення значення лічильника у циклах виконують за допомогою оператора збільшення (інкременту) чи зменшення (декременту):

  • i++ еквівалентно i = i + 1 (ідеться про "збільшити i на 1" або "додати 1 до поточного значення i")

  • i-- еквівалентно i = i - 1.

Інші приклади використання:

  • i += 2 - те саме, що i = i + 2;

  • i *= 3 - те саме, що i = i * 3 і т. д.

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

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

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(57, 20, 99); // Persian indigo
  fill(237, 34, 93); // Paradise Pink
  noStroke();
  let diameter = 50;

  for (let i = 0; i < width / diameter; i = i + 1) {
    ellipse(diameter / 2 + i * diameter, diameter / 2, diameter, diameter);
  }
}

Переглядаємо Аналізуємо

Доповнимо попередній код застосунку вкладеним циклом для малювання кіл на всьому полотні.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(57, 20, 99); // Persian indigo
  fill(237, 34, 93); // Paradise Pink
  noStroke();
  let diameter = 50;

  for (let i = 0; i < width / diameter; i = i + 1) { (1)
    for (let j = 0; j < height / diameter; j = j + 1) { (2)
      ellipse(
        diameter / 2 + i * diameter,
        diameter / 2 + j * diameter,
        diameter,
        diameter
      );
    }
  }
}
1 Зовнішній цикл.
2 Вкладений цикл.

Переглядаємо Аналізуємо

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

Розглянемо детальніше використання циклів і розгалужень на прикладі плиток Трюше.

Плитки Трюше

У 1704 році французький священник Жан Трюше , активний у галузі математики, гідравліки, графіки та типографіки, розробив схему створення візерунків з використанням плиток, які з того часу називаються його ім’ям.

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

Оригінальна плитка Трюше квадратна і розділена діагональною лінією між протилежними кутами. Плитку можна повертати на 90 градусів, щоб створити чотири можливі орієнтації.

Плитки Трюше
Плитки Трюше

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

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

Така плитка буде мати дві модифікації.

sketch.js
function setup() {
  createCanvas(1000, 1000);
}

function draw() {
  background(0, 175, 185); // Maximum Blue Green
  noFill();
  stroke(255);
  strokeWeight(5);

  // плитка ліворуч
  arc(200, 200, 300, 300, 0, PI / 2);
  arc(500, 500, 300, 300, PI, PI * 1.5);
  rect(200, 200, 300, 300);

  // плитка праворуч
  arc(600, 500, 300, 300, (3 * PI) / 2, 0);
  arc(900, 200, 300, 300, PI / 2, PI);

  rect(600, 200, 300, 300);
}
Можливі орієнтації плитки з дугами
Можливі орієнтації плитки з дугами

«Застелимо» полотно великою кількістю плиток з дугами для утворення візерунка.

Для цього спочатку, зменшимо розміри нашої плитки і помістимо її у верхній лівий кут полотна за допомогою такого коду:

arc(0, 0, 100, 100, 0, PI / 2);
arc(100, 100, 100, 100, PI, PI * 1.5);

У цьому разі, кожна плитка має розмір 100x100 пікселів. Для розміру полотна 1000x1000 пікселів, по горизонталі й вертикалі можна розмістити рівно по 10 (1000 / 100 = 10) плиток відповідно.

Застосуємо цикл for для заповнення полотна плитками, де x і y - значення координат побудови дуг на окремій плитці, а (0, PI / 2) і (PI, PI * 1.5) - значення кутів, поданих у радіанах, початку і кінця дуг на окремій плитці.

sketch.js
function setup() {
  createCanvas(1000, 1000);
  background(0, 175, 185); // Maximum Blue Green
  noFill();
  stroke(255);
  strokeWeight(3);
}

function draw() {
  for (let x = 0; x < 1000; x += 100) {
    for (let y = 0; y < 1000; y += 100) {
      arc(x, y, 100, 100, 0, PI / 2);
      arc(x + 100, y + 100, 100, 100, PI, PI * 1.5);
    }
  }
}

Тут використано два цикли for - зовнішній цикл (заповнення полотна плитками по горизонталі) і внутрішній цикл (заповнення полотна плитками по вертикалі).

Візерунок із плиток з дугами
Візерунок із плиток з дугами: прокручуючи колесо миші можна спостерігати ефект біжучої хвилі

Для наших цілей можна також використати лише один цикл for. З міркувань вище щодо розмірів полотна й окремої плитки, для заповнення полотна плитками необхідно 100 (10 × 10) плиток.

Отже, код для одного циклу for і з аналогічним результатом виконання, як і для випадку двох циклів, матиме наступний вигляд:

sketch.js
function setup() {
  createCanvas(1000, 1000);
  background(0, 175, 185); // Maximum Blue Green
  noFill();
  stroke(255);
  strokeWeight(3);
}

function draw() {
  let x = 0; (1)
  let y = 0;

  for (let k = 1; k < 101; k++) { (2)
    arc(x, y, 100, 100, 0, PI / 2); (3)
    arc(x + 100, y + 100, 100, 100, PI, PI * 1.5);

    x += 100; (4)

    if (k % 10 == 0) { (5)
      x = 0;
      y += 100;
    }
  }
}
1 Оголошення двох змінних x і y з початковими значеннями.
2 Прохід циклом for по усіх k-плитках, які помістяться на полотні (всього 100). Тут використана умова k < 101, яка враховує те, що цикл припинить своє виконання, коли значення k буде дорівнювати 101.
3 Побудова візерунка на плитці у формі двох дуг за допомогою функцій arc().
4 Збільшення координати x на 100 при кожній ітерації - рух праворуч щоразу на 100 пікселів.
5 Такий код з вказівкою розгалуження if дозволяє переходити на новий рядок нижче (значення змінної x стає рівним 0, а значення змінної y збільшується на 100 - відбувається переміщення нижче на полотні), коли поточний рядок буде заповнений плитками. Це дозволяє зробити операція порівняння остачі від ділення виразу k % 10 і 0. Результат виразу k % 10 для значень змінної лічильника k циклу набуває значень, які представлено у таблиці.
Таблиця "Результат операції k % 10 для різних значень лічильника k циклу"
Значення лічильника k циклу Операція k % 10 Результат k % 10

1

1 % 10

1

2

2 % 10

2

3

3 % 10

3

4

4 % 10

4

5

5 % 10

5

6

6 % 10

6

7

7 % 10

7

8

8 % 10

8

9

9 % 10

9

10

10 % 10

0

11

11 % 10

1

12

12 % 10

2

...

...

...

40

40 % 10

0

41

41 % 10

1

...

...

...

99

99 % 10

9

100

100 % 10

0

Модифікуємо наш візерунок за допомогою вказівки розгалуження if...else і функцій:

sketch.js
function setup() {
  createCanvas(1000, 1000);
  background(0, 175, 185); // Maximum Blue Green
  noFill();
  stroke(255);
  strokeWeight(3);
}

function draw() {
  let x = 0;
  let y = 0;

  for (let k = 1; k < 101; k++) {
    if (int(random(2))) { (1)
      arc(x, y, 100, 100, 0, PI / 2);
      arc(x + 100, y + 100, 100, 100, PI, PI * 1.5);
    } else { (2)
      arc(x + 100, y, 100, 100, PI / 2, PI);
      arc(x, y + 100, 100, 100, PI * 1.5, TWO_PI);
    }

    x += 100;

    if (k % 10 == 0) {
      x = 0;
      y += 100;
    }
  }
  noLoop(); (3)
}
1 Генерація випадкового числа з рухомою крапкою за допомогою функції random() з діапазону від 0 до 2 (не включаючи 2) і перетворення отриманого числа в ціле за допомогою функції int(). Тобто, результатом буде або число 0, або число 1. Якщо отримується 1 (інтерпретується як true), то виконується код у фігурних дужках відразу після if.
2 Якщо отримується 0 (інтерпретується як false) - виконується код у фігурних дужках після слова else.
3 Функція noLoop() зупиняє виконання коду у функції draw(). Її використовуємо, щоб зробити «знімок» візерунку.
Оновлений візерунок із плиток з дугами
Оновлений візерунок із плиток з дугами

Наведемо ще кілька варіацій візерунків, використовуючи попередній код як зразок.

У першому варіанті у функції draw() використані функції noStroke() і fill().

sketch.js
function setup() {
  createCanvas(1000, 1000);
  background(0, 175, 185); // Maximum Blue Green
  noFill();
  stroke(255);
  strokeWeight(3);
}

function draw() {
  let x = 0;
  let y = 0;
  noStroke();
  fill(255);
  for (let k = 1; k < 101; k++) {
    if (int(random(2))) {
      arc(x, y, 100, 100, 0, PI / 2);
      arc(x + 100, y + 100, 100, 100, PI, PI * 1.5);
    } else {
      arc(x + 100, y, 100, 100, PI / 2, PI);
      arc(x, y + 100, 100, 100, PI * 1.5, TWO_PI);
    }

    x += 100;

    if (k % 10 == 0) {
      x = 0;
      y += 100;
    }
  }
  noLoop();
}
Оновлений візерунок
Оновлений візерунок: використані функції noStroke() і fill(255)

У другому варіанті у функцію draw() додано більше умовних виразів і використані функції circle() і line().

sketch.js
function setup() {
  createCanvas(1000, 1000);
  background(0, 175, 185); // Maximum Blue Green
  noFill();
  stroke(255);
  strokeWeight(8);
}

function draw() {
  let x = 0;
  let y = 0;

  for (let k = 1; k < 101; k++) {
    let q = int(random(4));

    if (q == 0) {
      arc(x, y, 100, 100, 0, PI / 2);
      arc(x + 100, y + 100, 100, 100, PI, PI * 1.5);
    } else if (q == 1) {
      arc(x + 100, y, 100, 100, PI / 2, PI);
      arc(x, y + 100, 100, 100, PI * 1.5, TWO_PI);
    } else if (q == 2) {
      line(x + 50, y, x + 50, y + 100);
      line(x, y + 50, x + 100, y + 50);
    } else if (q == 3) {
      line(x + 50, y, x + 50, y + 10);
      line(x + 50, y + 90, x + 50, y + 100);
      circle(x + 50, y + 50, 80);
      line(x, y + 50, x + 10, y + 50);
      line(x + 90, y + 50, x + 100, y + 50);
    }

    x += 100;

    if (k % 10 == 0) {
      x = 0;
      y += 100;
    }
  }
  noLoop();
}
Оновлений візерунок
Оновлений візерунок: більше умовних виразів і використані функції circle() і line()

Цікавимось

Програмування сітки на полотні

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

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

sketch.js
let stepGrid;

function setup() {
  createCanvas(600, 500); // розміри полотна
  stepGrid = 50; // крок сітки
}

function draw() {
  console.log(mouseX, mouseY); // координати вказівника миші
  background(255);
  stroke('#a5a58d'); // Artichoke
  for (let i = 0; i < width / 10; i++) {
    line(i * stepGrid, 0, i * stepGrid, height);
  }
  for (let i = 0; i < height / 10; i++) {
    line(0, i * stepGrid, width, i * stepGrid);
  }
  stroke(0);
  noFill();

  // тут код вашого застосунку
}

3.4.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «літерали»?

  2. Яка алгоритмічна конструкція називається «лінійною»?

  3. Що таке «розгалуження» і які має форми запису у мові JavaScript?

  4. Як будуються складені умови?

  5. Визначити результат логічних виразів (true чи false) у коді без його запуску, якщо x = 1, y = 4:

!(x > 4)
(x == 4 && x == 1)
(x == 4 || x == 1)
(x > 0 && y < 5)
(x > 7 && x < 5)
  1. Що називають «циклом»? Які види циклів використовуються у мові JavaScript?

3.4.5. Практичні завдання

Початковий

  1. Заповнити прогалини в коді для малювання мішені із концентричних кіл різного радіуса в центрі полотна.

let d = 0;
function setup() {
  createCanvas(800, 600);
  background(255);
}

function draw() {
  while (...) {
    stroke(0);
    fill(d, 0);
    circle(..., ..., d);
    d = ... + 40;
  }
}
  1. Застосунок малює вертикальні лінії, як представлено у першій демонстрації. Змінити код, поданий нижче, так, щоб в результаті лінії малювалися горизонтально, як у другій демонстрації.

let x = 0;
function setup() {
  createCanvas(200, 200);
  background(255);
  frameRate(5);
}

function draw() {
  stroke(66, 54, 122); // Dark Slate Blue
  line(x, 0, x, height);
  x += 10;

  if (x > width) {
    x = 0;
  }
}
  1. Щоразу при запуску застосунку результат не змінюється - на екрані з’являється повідомлення Error!. З’ясувати, при яких значеннях змінної output на екрані буде повідомлення про успіх.

const fail = "Error!";
const hit = "Successfully!";
let output;

function setup() {
  createCanvas(200, 100);
  textSize(20);
}

function draw() {
  fill(255);
  if (output) {
    background(0, 150, 0); // Slimy Green
    text(hit, 40, 60);
  } else {
    background(255, 0, 0); // Red
    text(fail, 73, 60);
  }
}

Середній

  1. Створити застосунок, який виводить на екран інформацію про навчальні успіхи студентів у формі повідомлень як на малюнку. В залежності від набраної кількості балів (шкала від 0 до 100) виводиться інший текст: якщо оцінка ≥ 90 - Congratulations!, ≥ 80 - Good job!, ≥ 70 - Just okay., ≥ 60 - Not good!, інакше - Study more!.

Шкала навчальних досягнень
Шкала навчальних досягнень
  1. Створити застосунок, який створює горизонтальний градієнт в колірній системі HSB.

Шкала колірних тонів моделі HSB (характеристика Hue)
Шкала колірних тонів моделі HSB: характеристика Hue
  1. Створити застосунок, який фарбує кожен піксель полотна у випадковий колір.

  1. Створити застосунок, який малює фігуру в залежності від числового значення змінної. Наприклад: 1 - коло, 2 - еліпс, 3 - прямокутник, 4 - квадрат і т. д., а за стандартним налаштуванням - лише зафарбовує полотно певним кольором.

Для реалізації цієї задачі можна використати вказівку розгалуження switch.
Перемикач фігур
Перемикач фігур

Високий

  1. Намалювати сітку, яка заповнює полотно, незалежно від розміру полотна. Розміри клітинок сітки - 10х10 пікселів.

Сітка з розміром клітинок 10х10
Сітка з розміром клітинок 10х10
  1. Створити застосунок - генератор кіл. Орієнтовний результат представлений на малюнку.

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

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

  1. Створити застосунок, що на полотні малює квадрати, у які вписує кола. Орієнтовний результат представлений в демонстрації.

Екстремальний

  1. Створити візерунок, використовуючи плитки Трюше, наприклад як на малюнку.

Візерунок на плитці
Візерунок на плитці
  1. Створити текстову екранну заставку. Орієнтовний варіант заставки представлений в демонстрації.

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

3.5. Інтерактивність

Інтерактивність (англ. Interaction - взаємодія) - поняття, яке розкриває характер і ступінь взаємодії між об’єктами.

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

Безперервне виконання коду стає можливим, коли ми записуємо його у функцію draw().

sketch.js
function setup() {
  createCanvas(500, 400);
  frameRate(12);
}

function draw() {
  background(220);
  console.log("🐑", frameCount);
}

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

🐑 1
🐑 2
🐑 3
🐑 4
...

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

Одноразове виконання коду у функції draw() називається ітерацією. У контексті p5.js кожна така ітерація є кадром.

У функції setup() за допомогою функції frameRate() ми вказали кількість кадрів, які відображатимуться щосекунди, а функція console.log() буде друкувати смайлик Emoji 🐑 і номер кадру за допомогою вбудованої змінної frameCount.

За стандартним налаштуванням код у функції draw() виконується із частотою 60 кадрів за секунду.

У підсумку:

  1. Код всередині функції setup() запускається лише один раз.

  2. Код всередині функції draw() працює безперервно - циклічно.

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

3.5.1. Проста взаємодія

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

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

Вправа 22

Створити малюнок за допомогою переміщення вказівника миші, використовуючи поданий код.

function setup() {
  createCanvas(500, 400);
  fill(34, 124, 157, 100); // CG Blue
  noStroke();
}

function draw() {
  ellipse(mouseX, mouseY, 24, 24);
}

У цьому прикладі щоразове виконання коду в блоці draw() створює на полотні нове коло. Оскільки зафарбовування кіл кольором CG Blue є прозорим, темніші області показують місця, над якими вказівник миші знаходився більше часу і де він рухався повільніше.

Щоб відобразити лише найновіше коло, використаємо функцію background() у функції draw() перед тим, як намалювати фігуру.

sketch.js
function setup() {
  createCanvas(500, 400);
  fill(34, 124, 157, 100); // CG Blue
  noStroke();
}

function draw() {
  background(220);
  ellipse(mouseX, mouseY, 24, 24);
}
Функція background() очищає полотно повністю, тому варто переконатися, що вона записана перед іншими функціями в блоці draw(), інакше фігури, намальовані функціями, що стоять перед нею, будуть стерті.

Вправа 23

Використати іншу фігуру для малювання вказівником миші.

Окрім mouseX та mouseY, можна використовувати пару вбудованих змінних pmouseX та pmouseY .

Ці дві змінні містять попередні значення координат mouseX та mouseY розташування вказівника миші, тобто значення, де вказівник миші перебував востаннє.

Координати вказівника миші на полотні розміром 80х80
Полотно розміром 80х80: координати вказівника миші

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

Вправа 24

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

function setup() {
  createCanvas(500, 400);
  background(255);
  stroke(34, 124, 157); // CG Blue
}

function draw() {
  line(pmouseX, pmouseY, mouseX, mouseY);
}

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

Для цих цілей використаємо функцію strokeWeight() , яка встановлює товщину лінії, і функцію dist() , яка обчислює відстань між двома точками у двовимірному або тривимірному просторах.

Вправа 25

Як змінюється товщина ліній при зміні швидкості переміщення вказівника миші?

function setup() {
  createCanvas(500, 400);
  stroke(34, 124, 157, 100); // CG Blue
}

function draw() {
  let d = dist(mouseX, mouseY, pmouseX, pmouseY);
  strokeWeight(d);
  line(mouseX, mouseY, pmouseX, pmouseY);
}

Змінна mouseX може набувати значень в межах від 0 до ширини полотна, однак діапазон значень mouseX можна змінити за допомогою функції map() .

Перший параметр map() - змінна, другий і третій - мінімальне і максимальне значення змінної, четверте і п’яте - бажане мінімальне і максимальне значення змінної.

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

sketch.js
function setup() {
  createCanvas(200, 200); (1)
}

function draw() {
  let g = map(mouseX, 0, width, 0, 255); (2)
  background(0, g, 0); (3)
  console.log(mouseX, g); (4)
}
1 Створення полотна розміром 200х200 пікселів.
2 Оголошення змінної g і присвоєння їй значення із функції map(). Тут функція map() змінює поточний діапазон значень (від 0 до width) змінної mouseX на діапазон від 0 (коли mouseX = 0) до 255 (коли mouseX = width).
3 Зафарбовування полотна за допомогою функції background(). Змінна g, яка є зеленою складовою кольору, набуває значень x-координати вказівника миші, але вже із нового діапазону від 0 до 255.
4 Виведення в консоль вебпереглядача значень x-координати вказівника миші у початковому (mouseX) і зміненому (g) діапазонах.

Переглядаємо Аналізуємо

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

Розглянемо початковий код застосунку.

sketch.js
let mx = 0; // початкова x-координата малювання еліпса
let my = 0; // початкова y-координата малювання еліпса
let coming = 0.05; (1)
let radius = 24; (2)

function setup() {
  createCanvas(500, 300);
  ellipseMode(RADIUS); (3)
}

function draw() {
  background(40, 54, 24); // Kombu Green

  stroke(255);
  line(0, height / 2, width, height / 2); (4)
  line(width / 2, 0, width / 2, height); (5)

  if (abs(mouseX - mx) > 0.1) { (6)
    mx = mx + (mouseX - mx) * coming;
  }
  if (abs(mouseY - my) > 0.1) { (7)
    my = my + (mouseY - my) * coming;
  }

  fill(170, 186, 120); // Olivine
  ellipse(mx, my, radius, radius); (8)
}
1 Оголошення змінної coming, значення якої буде впливати на плавність наближення еліпса до положення вказівника миші.
2 Оголошення змінної radius і присвоєння їй значення радіуса еліпса.
3 Функція ellipseMode() встановлює режим RADIUS для малювання еліпса, а саме, перші два параметри у функції ellipse() є координатами x та y центра еліпса, а третій та четвертий параметри вказують половину ширини та висоти еліпса.
4 Створення горизонтальної лінії, яка ділить полотно горизонтально порівну на дві частини за допомогою функції line() .
5 Створення вертикальної лінії, яка ділить полотно вертикально порівну на дві частини.
6 Вказівка розгалуження перевіряє чи відстань між поточною x-координатою і координатою вказівника миші mouseX є більшою за значення 0.1. Якщо умова abs(mouseX - mx) > 0.1 є істиною, то відбувається плавний рух еліпса до точки, в якій перебуває вказівник миші, інакше рух припиняється. Функція abs() обчислює абсолютне значення виразу mouseX - mx.
7 Аналогічно, як у пункті 6, тільки для координат my і mouseY.
8 Малювання еліпса з координатами центру mx і my та радіусом radius. Оскільки третій і четвертий параметри є однаковими у функції ellipse() в результаті отримуємо коло.

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

Переглядаємо Аналізуємо

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

Для встановлення значення координат малювання еліпса лише у вказаній області полотна використаємо функцію constrain() , яка утворює діапазон для значення певної величини.

Код застосунку зараз матиме наступний вигляд (наведено фрагмент зі змінами):

sketch.js
...
function draw() {
  ...
  // ліва верхня частина полотна
  mx = constrain(mx, radius, width / 2 - radius);
  my = constrain(my, radius, height / 2 - radius);

  fill(170, 186, 120); // Olivine
  ellipse(mx, my, radius, radius);
}

Переглядаємо Аналізуємо

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

// права верхня частина полотна
mx = constrain(mx, width / 2 + radius, width - radius);
my = constrain(my, radius, height / 2 - radius);

// ліва нижня частина полотна
mx = constrain(mx, radius, width / 2 - radius);
my = constrain(my, height / 2 + radius, height - radius);

// права нижня частина полотна
mx = constrain(mx, width / 2 + radius, width - radius);
my = constrain(my, height / 2 + radius, height - radius);

3.5.2. Полярні координати

До цього часу усі побудови фігур були реалізовані у прямокутній системі координат на площині, в якій точки визначалися парою координат (x, y).

Ці координати відомі як декартові координати, названі на честь відомого французького математика Рене Декарта , який розвинув ідеї декартового простору .

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

Зазвичай нам немає потреби записувати ці рівняння в коді, оскільки вони описані всередині вбудованих функцій бібліотеки p5.js. Наприклад, для обчислення відстані між двома точками, що мають координати (x1, y1) і (x2, y2) на площині, ми використовуємо із бібліотеки p5.js функцію dist() .

Відстань d між двома точками з координати (x1, y1) і (x2, y2) на площині можна обчислити також за допомогою рівняння:

\$d = sqrt((x2 - x1)^2 + (y2 - y1)^2)\$

Це рівняння є версією теореми Піфагора у декартовій системі координат. Теорема Піфагора встановлює співвідношення між сторонами прямокутного трикутника .

Прямокутний трикутник
Прямокутний трикутник: a і b - катети, c - гіпотенуза

Формулювання цієї теореми багатьом відоме і звучить як квадрат гіпотенузи дорівнює сумі квадратів катетів та записується за допомогою рівняння:

\$c^2 = a^2 + b^2\$

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

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

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

Використовуючи тригонометричні функції, співвідношення між сторонами прямокутного трикутника на малюнку вище можна записати так (θ, theta - грецька літера тета):

\$sin(θ) = a / c, a = c * sin(θ), c = a / sin(θ)\$
\$cos(θ) = b / c, b = c * cos(θ), c = b / cos(θ)\$

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

Полярна система координат задається променем, який називають полярною віссю. Будь-яка точка на площині у полярній системі координат визначається двома полярними координатами: радіальною r та кутовою θ.

Радіальна координата r відповідає відстані від точки на площині до початку координат (полюса). Кутова координата θ, що також зветься азимутом, дорівнює куту між полярною віссю та напрямком на точку.

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

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

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

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

Загалом точка з полярними координатами (r, θ) може бути представлена у вигляді

\$(r, θ ± n × 360°)\$

або

\$(-r, θ ± (2 * n + 1) * 180°)\$

де n - довільне ціле число.

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

Якщо відомий радіус r та кут θ, декартові координати x та y точки можна обчислити за допомогою формул:

\$sin(theta) = y / r, y = r * sin(theta)\$
\$cos(theta) = x / r, x = r * cos(theta)\$

Як видно, ці перетворення можна виконувати завдяки тригонометричним функціям sin() і cos() , які входять до складу бібліотеки p5.js.

Кожна із наведених функцій отримує один аргумент - значення кута у радіанах за стандартним налаштуванням, а результатами обчислення функцій sin() і cos() є значення з рухомою крапкою в діапазоні від –1 до 1.

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

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

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

sketch.js
let x, y; // декартові координати

// початкові значення полярних координат
let r = 150;
let theta = 0;

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);
}

function draw() {
  // тло
  background("#2D3142"); // Space Cadet

  // осі на полотні
  strokeWeight(3);
  stroke("#4F5D75"); // Black Coral
  line(width / 2, height, width / 2, 0);
  line(0, height / 2, width, height / 2);

  // коло
  noFill();
  circle(width / 2, height / 2, r);

  // перехід від полярних у декартові координати
  x = r * cos(theta);
  y = r * sin(theta);

  // малювання точки на колі
  noStroke();
  fill("#FFFFFF"); // White
  ellipse(x + width / 2, height / 2 - y, 5, 5);

  // збільшення значення кута повороту
  theta += 0.01;
}

Переглядаємо Аналізуємо

Функція ellipse() малює точку у вказаних координатах на лінії кола, а кут повороту theta щоразу визначає нове місце для малювання. Так ми спостерігаємо імітацію руху проти годинникового напрямку.

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

Прикладами циклічних рухів є: рух стрілки механічного годинника, рух поршня в циліндрі двигуна автомобіля, рух супутників навколо планети тощо.

Якщо у коді використати інструкцію для зменшення значення кута повороту theta -= 0.01;, рух відбуватиметься за годинниковим напрямком.

Вправа 26

Збільшити/зменшити швидкість руху точки.

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

sketch.js
let r = 150; // радіус кола
let theta = 1; // початкове значення кута theta в радіанах
let x, y, g, pointX, pointY;

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);
}

function draw() {
  // тло
  background("#2D3142"); // Space Cadet

  // осі на полотні
  strokeWeight(3);
  stroke("#4F5D75"); // Black Coral
  line(width / 2, height, width / 2, 0);
  line(0, height / 2, width, height / 2);

  // коло
  noFill();
  circle(width / 2, height / 2, r);

  // співвідношення у прямокутному трикутнику:
  // x - прилеглий катет, y - протилежний катет, r - гіпотенуза
  x = r * cos(theta);
  y = r * sin(theta);

  // координати точки на колі
  pointX = width / 2 + x;
  pointY = height / 2 - y;

  // гіпотенуза
  stroke("#FFFFFF"); // White
  line(width / 2, height / 2, width / 2 + x, height / 2 - y);

  // протилежний катет
  stroke("#EF8354"); // Mandarin
  line(width / 2 + x, height / 2 - y, width / 2 + x, height / 2);

  // прилеглий катет
  stroke("#FFD23F"); // Sunglow
  line(width / 2, height / 2, width / 2 + x, height / 2);

  // малювання дуги кута theta
  noFill();
  stroke("#D8315B"); // Cerise
  if (pointX > width / 2 && pointY < height / 2) {
    arc(width / 2, height / 2, 20, 20, TWO_PI - theta, TWO_PI);
  } else if (pointX < width / 2 && pointY < height / 2) {
    arc(width / 2, height / 2, 20, 20, PI, TWO_PI - theta);
  } else if (pointX < width / 2 && pointY > height / 2) {
    arc(width / 2, height / 2, 20, 20, TWO_PI - theta, PI);
  } else if (pointX > width / 2 && pointY > height / 2) {
    arc(width / 2, height / 2, 20, 20, 0, TWO_PI - theta);
  }

  // напис значення кута theta
  strokeWeight(1);
  textSize(12);
  text("theta = " + theta + " рад", 20, 20);

  // побудова точки на колі
  noStroke();
  fill("#FFFFFF"); // White
  circle(pointX, pointY, 5);

  // обчислення значення кута theta у градусах
  // g = (theta * 180) / PI;

  // друк в консолі значення кута в градусах і координат точки
  // console.log(g);
  // console.log(pointX, pointY);
}

У результаті виконання застосунку для вказаного кута theta буде будуватися дуга світло-вишневого кольору (Cerise), прямокутний трикутник з кутом theta зі сторонами: протилежним катетом помаранчевого кольору (Mandarin), прилеглим катетом жовтого кольору (Sunglow) і гіпотенузою білого кольору (White).

Співвідношення у прямокутному трикутнику і полярні координати
Співвідношення у прямокутному трикутнику і полярні координати

Отож, змусьмо точку на колі рухатися вздовж лінії кола.

Щоб це реалізувати, значення кута theta має змінюватися при виконанні застосунку. Запишемо на початку функції draw() інструкцію, яка буде щоразу збільшувати значення theta:

sketch.js
function draw() {
  theta += 0.02;
  ...
}

Переглядаємо Аналізуємо

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

sketch.js
...
let k = 1; (1)

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);
}

function draw() {
  k += 0.4; (2)
  theta += 0.02;
  ...

  // співвідношення у прямокутному трикутнику:
  // x - прилеглий катет, y - протилежний катет, r - гіпотенуза
  x = k * cos(theta); (3)
  y = k * sin(theta);
  ...
}
1 Оголошуємо нову змінну k, яка слугуватиме лічильником, який збільшує значення радіуса обертання точки.
2 Значення змінної k збільшується на величину 0.4 у циклі функції draw().
3 Для визначення координат точки підставляється щоразу нове значення k.

Переглядаємо Аналізуємо

Змінимо код застосунку так, щоб відтворити лінію траєкторії точки, відкинувши усі інші елементи.

sketch.js
let r = 150; // радіус кола
let theta = 1; // початкове значення кута theta в радіанах
let x, y, pointX, pointY;
let k = 1;

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);

  // тло
  background("#2D3142"); // Space Cadet

  // осі на полотні
  strokeWeight(3);
  stroke("#4F5D75"); // Black Coral
  line(width / 2, height, width / 2, 0);
  line(0, height / 2, width, height / 2);
}

function draw() {
  // радіус обертання
  k += 0.4;

  // завдяки збільшенню значення кута точка рухається по криволінійній траєкторії
  theta += 0.02;

  // коло
  noFill();
  circle(width / 2, height / 2, r);

  // координати точки на колі
  x = cos(theta) * k;
  y = sin(theta) * k;
  pointX = width / 2 + x;
  pointY = height / 2 - y;

  // побудова точки на колі
  noStroke();
  fill("#FFBD00"); // Amber
  circle(pointX, pointY, 5);
}

Частину коду ми перемістили у функцію setup(), щоб на полотні залишався слід траєкторії руху і змінили значення кольору на Amber для лінії траєкторії.

Переглядаємо Аналізуємо

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

sketch.js
let r = 150; // радіус кола
let theta = 1; // початкове значення кута theta в радіанах
let x, y, pointX, pointY;
let k = 1;

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);
}

function draw() {
  // радіус обертання
  k += 0.4;

  // завдяки збільшенню значення кута точка рухається по криволінійній траєкторії
  theta += 0.02;

  // тло
  background("#2D3142"); // Space Cadet

  // осі на полотні
  strokeWeight(3);
  stroke("#4F5D75"); // Black Coral
  line(width / 2, height, width / 2, 0);
  line(0, height / 2, width, height / 2);

  // коло
  noFill();
  circle(width / 2, height / 2, r);

  // координати точки на колі
  x = cos(theta) * k;
  y = sin(theta) * k;
  pointX = width / 2 + x;
  pointY = height / 2 - y;

  // побудова точки на колі
  noStroke();
  fill("#FFFFFF"); // White
  circle(pointX, pointY, 5);
}

Переглядаємо Аналізуємо

Вправа 27

Реалізувати спіральний рух точки до центру кола і зупинки точки в центрі.

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

Оголосимо ще дві змінні h і v, які будуть містити значення висоти й ширини еліптичної орбіти.

sketch.js
let r = 150; // радіус кола
let theta = 1; // початкове значення кута theta в радіанах
let x, y, pointX, pointY;
let h = 1; // початкове значення ширини еліптичної орбіти
let v = 1; // початкове значення висоти еліптичної орбіти

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);
}

function draw() {
  // зміна характеристик еліптичної орбіти
  h += 0.4;
  v += 0.2;

  // завдяки збільшенню значення кута точка рухається по криволінійній траєкторії
  theta += 0.02;

  // тло
  background("#2D3142"); // Space Cadet

  // осі на полотні
  strokeWeight(3);
  stroke("#4F5D75"); // Black Coral
  line(width / 2, height, width / 2, 0);
  line(0, height / 2, width, height / 2);

  // коло
  noFill();
  circle(width / 2, height / 2, r);

  // координати точки на колі
  x = cos(theta) * h;
  y = sin(theta) * v;
  pointX = width / 2 + x;
  pointY = height / 2 - y;

  // побудова точки на колі
  noStroke();
  fill("#FFFFFF"); // White
  circle(pointX, pointY, 5);
}

Переглядаємо Аналізуємо

Вправа 28

Реалізувати рух точки по еліптичній орбіті вертикально.

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

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

Візуальне представлення (графік) функції синуса sin() називається синусоїдою. Зобразимо цей графік як сукупність еліпсів.

Переглядаємо Аналізуємо

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

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

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

Ще однією характеристикою коливань є частота (ν), яка пов’язана із періодом коливань (T) співвідношенням:

\$ν = 1 / T\$

Тепер проаналізуємо код нашого застосунку, де насамперед оголошуємо глобальні змінні.

sketch.js
let r = 150; // радіус кола
let theta = 0; // початкове значення кута theta в радіанах
let x = 0, y = 0; // початкові значення координат точок графіка
let n = 100; // кількість точок на графіку
let k = 1; // кількість хвиль
let w = 0; // циклічна частота
let pointX, pointY; // координати точки, що коливається вертикально
let angle;

function setup() {
  createCanvas(500, 400);
  ellipseMode(RADIUS);
}

function draw() {
  // тло
  background("#2D3142"); // Space Cadet

  // осі на полотні
  strokeWeight(3);
  stroke("#4F5D75"); // Black Coral
  line(width / 2, height, width / 2, 0);
  line(0, height / 2, width, height / 2);

  // коло
  noFill();
  circle(width / 2, height / 2, r);

  // синусоїда
  noStroke();

  theta += 0.01;

  for (let i = 0; i < n; i++) { (1)
    w = 0.062 * k; (2)
    angle = w * i + theta; (3)

    x = i * 5; // 5, 25, 50 (4)
    y = r * sin(angle);

    if (x === width / 2) { (5)
      fill("#FFB4A2"); // Melon
    } else {
      fill("#FFFFFF"); // White
    }
    circle(x, y + height / 2, 5);
  }
}
1 У циклі for проходимо по кожній точці майбутнього графіка, кількість яких визначена у змінній n.
2 Підбираємо певне числове значення, наприклад, 0.062 для циклічної частоти w, що визначає кількість повторень коливань або обертань, які здійснюються за фіксований період часу, так, щоб у змінній k вказувати ціле число хвиль на графіку, які поміщаються на полотні у ширину.
3 Формуємо значення кута angle для обчислення у функції sin() із кута theta, який щоразу збільшується на величину 0.01, значення лічильника циклу i, значення якого збільшується на одиницю з плином часу і величини w.
4 Обчислюємо декартові координати (x, y) точок графіка. Координата x визначається добутком значення лічильника циклу i на числовий коефіцієнт (5, 25, 50 обрані не випадково, а для малювання кольорової точки, що коливається вертикально).
5 Якщо координата x точки збігається з центром полотна, зафарбовуємо точку кольором Melon і додаємо її на графік, інакше - просто малюємо точку білого кольору.

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

Типовий графік тригонометричної функції синус можна побудувати, скориставшись вбудованою функцією sin() бібліотеки p5.js.

sketch.js
let theta = 0.0; (1)

function setup() {
  createCanvas(470, 200);
}

function draw() {
  background(31, 32, 65); // Space Cadet

  theta += 0.014; (2)
  fill(0);
  let angle = theta; (3)

  for (let x = 0; x <= width; x += 8) { (4)
    let y = map(sin(angle), -1, 1, 0, height); (5)

    stroke(17, 157, 164); // Viridian Green (6)
    strokeWeight(5); (7)
    point(x, y); (8)

    angle += 0.1; (9)
  }
}
1 Встановлення початкового значення кута theta.
2 Інкремент theta. Величина інкременту визначає швидкість появи наступних відрізків графіка синусоїди. Отже, можна спостерігати ефект переміщення графіка. Чим більша величина збільшення, тим більша швидкість і навпаки. Якщо значення theta дорівнює 0 - графік є статичним.
3 Оголошення змінної angle з початковим значенням theta, значення якої обчислюється у функції sin().
4 Цикл for використовується для малювання всіх точок уздовж синусоїди.
5 Обчислення значення y-координати, виходячи з функції синуса. Тут використовується функція map(), яка перетворює діапазон значень від -1 до 1 для sin(angle) в новий діапазон від 0 до height. Отож, амплітуда графіка буде дорівнювати висоті полотна.
6 Встановлення кольору точки.
7 Встановлення товщини точки.
8 Створення точки з координатами (x, y).
9 Інкремент значення кута в циклі.

Переглядаємо Аналізуємо

3.5.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «інтерактивність»?

  2. Як визначити координати вказівника миші на полотні?

  3. Як визначаються координати в полярній системі координат?

  4. Для чого використовують функції map() і constrain()?

3.5.5. Практичні завдання

Початковий

  1. Змінити код, щоб за допомогою вказівника миші можна було б малювати на полотні.

function setup() {
  createCanvas(200, 200);
  noStroke();
}

function draw() {
  background(25, 100, 126); // // Blue Sapphire
  ellipse(mouseX, mouseY, 15, 15);
}
  1. Заповнити прогалини в коді, щоб частини розділеного полотна реагували на вказівник миші.

function setup() {
  createCanvas(200, 200);
  noStroke();
  fill(217, 197, 178); // Desert Sand
}

function draw() {
  background(20, 17, 15); // Smoky Black
  if (...) {
    rect(0, ..., width / 2, ...); // ліворуч
  } else {
    rect(..., ..., ..., height); // праворуч
  }
}

Середній

  1. Створити застосунок, в якому усі три частини розділеного полотна реагують на вказівник миші - змінюється колір тла.

  1. Створити застосунок, в якому полотно вертикально розділяється порівну на дві частини лінією. Якщо вказівник миші перебуває ліворуч від лінії розділу - колір полотна поступово стає чорним, якщо праворуч - поступово набуває максимального значення однієї зі складових RGB. Кольорову складову для цього ефекту обрати на свій вибір.

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

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

Високий

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

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

Екстремальний

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

  1. Створити застосунок, який на полотні зображує графіки двох тригонометричних функцій: sin() і cos() . За допомогою вказівника миші графіками можна інтерактивно керувати - змінювати їхнє положення: амплітуда функції синуса регулюється змінною mouseY, а амплітуда функції косинуса - mouseX. Для розуміння процесів, що відбуваються, переглянути демонстрацію.

3.6. Обробка подій

Дії користувача, як от натискання кнопок миші чи клавіш на клавіатурі, визначаються як події.

Якщо необхідно, наприклад, змінити колір тла при натисканні кнопки миші, то пишуть блок коду для обробки цієї події. Цей блок коду називається обробником події. Він повідомляє застосунку, який код виконувати, коли подія настає.

Спробуймо розібратися як обробляються такі події, використовуючи можливості бібліотеки p5.js.

3.6.1. Миша

Багато прикладів інтерактивності можна реалізувати разом з вказівкою розгалуження if і системними змінними бібліотеки p5.js, які відстежують поведінку миші.

Цікавимось

Деякі системні змінні p5.js для роботи з мишею
  • mouseIsPressed - чи натиснута миша? true, чи false?

  • mouseButton - яка кнопка миші натиснута? LEFT, RIGHT чи CENTER?

Розглянемо код простого застосунку, який змінює колір полотна, якщо натиснути будь-яку кнопку миші.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  if (mouseIsPressed) {
    background(181, 131, 141); // Burnished Brown
  } else {
    background(109, 104, 117); // Old Lavender
  }
}

Переглядаємо Аналізуємо

Доки кнопка миші натиснута, змінна mouseIsPressed набуває значення true і виконується гілка if - функція background() для тла полотна встановлює колір Burnished Brown. Як тільки кнопка миші буде відпущена - значення mouseIsPressed стане false і виконується гілка else - встановлюється інший колір тла - Old Lavender.

Використовуючи будь-яку функцію для створення примітивів, можна перетворити вказівник миші на інструмент для малювання.

sketch.js
function setup() {
  createCanvas(200, 200);
  background("#2A2A2A"); // Jet
}

function draw() {
  fill("#00F4D3"); // Sea Green Crayola
  noStroke();
  if (mouseIsPressed) {
    circle(mouseX, mouseY, 10);
  }
}

Переглядаємо Аналізуємо

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

Ось деякі з таких функцій:

  • mousePressed() - викликається один раз, коли користувач натискає кнопку миші.

  • mouseReleased() - викликається один раз, коли користувач відпускає кнопку миші.

  • mouseClicked() - викликається один раз після натискання і відпускання кнопки миші.

  • mouseMoved() - викликається щоразу, коли вказівник миші рухається і кнопка миші не натискається.

  • mouseDragged() - викликається щоразу, коли вказівник миші рухається під час натиснутої кнопки миші.

  • mouseWheel() - викликається під час прокрутки за допомогою колеса миші.

Вправа 29

Виконати код і перевірити твердження: Після запуску тло має сірий колір. Коли користувач натискає кнопку миші, тло стає червоним, а коли відпускає кнопку - жовтим. Тло стає синього кольору, коли користувач перетягує вказівник миші із натиснутою кнопкою.

sketch.js
let r = 52;
let g = 52;
let b = 52;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(r, g, b);
}

function mousePressed() {
  r = 245;
  g = 0;
  b = 0;
}

function mouseReleased() {
  r = 245;
  g = 245;
  b = 0;
}

function mouseDragged() {
  r = 0;
  g = 245;
  b = 0;
}
Функції подій, як от mousePressed() та інші, записуються окремо поза межами функцій setup() і draw(), але за допомогою аналогічного синтаксису.

Об’єднаємо в одному застосунку використання змінної mouseIsPressed і функції mouseClicked().

sketch.js
function setup() {
  createCanvas(200, 200);
  background(45);
}

function draw() {
  if (mouseIsPressed) {
    fill(255, 210, 63); // Sunglow
    circle(mouseX, mouseY, 30);
  }
}

function mouseClicked() {
  fill(14, 173, 105); // GO Green
  circle(mouseX, mouseY, 15);
}

Усередині функції draw() (яка викликається 60 разів за секунду) виконується перевірка значення змінної mouseIsPressed і, якщо кнопка миші натиснута, під вказівником миші малюється велике жовте коло.

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

Якщо потрібно дізнатися, яка кнопка миші натиснута, перевіряють змінну mouseButton.

Змінна mouseButton приймає три значення: LEFT, CENTER і RIGHT. Щоб визначити, яка з кнопок миші натиснута, використовують оператор порівняння ===.

Вправа 30

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

sketch.js
function setup() {
  createCanvas(100, 100);
}

function draw() {
  background(237, 34, 93); // Paradise Pink
  fill(0);
  if (mouseIsPressed) {
    if (mouseButton === LEFT) {
      ellipse(50, 50, 50, 50);
    }
    if (mouseButton === RIGHT) {
      rect(25, 25, 50, 50);
    }
    if (mouseButton === CENTER) {
      triangle(23, 75, 50, 20, 78, 75);
    }
  }
  console.log(mouseButton);
}

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

sketch.js
let s; (1)
let r = 0; (2)
let g = 0; (3)
let b = 0; (4)

function setup() {
  createCanvas(500, 400); (5)
  background(255); (6)
}

function draw() {
  if (mouseX > width / 2) { (7)
    r = r + 1;
    g = g - 1;
  } else {
    r = r - 1;
    g = g + 1;
  }

  if (mouseY > height / 2) { (8)
    b = b + 1;
    g = g - 1;
  } else {
    b = b - 1;
    g = g + 1;
  }

  if (mouseIsPressed) { (9)
    if (mouseButton === CENTER) { (10)
      background(255);
    } else if (mouseButton === LEFT) { (11)
      fill(r, g, b, 135);
      s = random(10, 100);
      ellipse(mouseX, mouseY, s, s);
    } else if (mouseButton === RIGHT) { (12)
      stroke(0);
      line(mouseX, mouseY, pmouseX, pmouseY);
    }
  }
  r = constrain(r, 0, 255); (13)
  g = constrain(g, 0, 255); (14)
  b = constrain(b, 0, 255); (15)
}

Переглядаємо Аналізуємо

1 Оголошення глобальної змінної s, що зберігатиме розмір кола.
2 Оголошення глобальної змінної, що зберігатиме червону складову кольору. Початкове значення 0.
3 Оголошення глобальної змінної, що зберігатиме зелену складову кольору. Початкове значення 0.
4 Оголошення глобальної змінної, що зберігатиме синю складову кольору. Початкове значення 0.
5 Створення полотна розміром 500x400.
6 Зафарбовування полотна білим кольором.
7 Якщо вказівник миші знаходиться праворуч від точки width / 2 екрану, то значення кольору r збільшується на 1, а кольору g - зменшується на 1, якщо ліворуч - навпаки.
8 Якщо вказівник миші знаходиться вище від точки height / 2 екрану, то значення кольору b зменшується на 1, а кольору g - збільшується на 1, якщо нижче - навпаки.
9 Якщо будь-яка кнопка миші натиснута (mouseIsPressed === true), то виконати вкладене розгалуження.
10 Якщо натиснута середня кнопка миші (змінна mouseButton має значення CENTER), то зафарбувати полотно білим кольором.
11 Якщо натиснута ліва кнопка миші (змінна mouseButton має значення LEFT), то встановити колір зафарбовування (r, g, b, 135), у змінну s зберегти випадкове значення із діапазону [10, 100) для розміру еліпса по ширині й висоті (значення однакові, тому у результаті буде малюватися коло), намалювати еліпс у місці розташування вказівника миші.
12 Якщо натиснута права кнопка миші (змінна mouseButton має значення RIGHT), то намалювати лінію чорного кольору у місці розташування вказівника миші.
13 Встановлення обмеження значення кольору r у діапазоні від 0 до 255 включно.
14 Встановлення обмеження значення кольору g у діапазоні від 0 до 255 включно.
15 Встановлення обмеження значення кольору b у діапазоні від 0 до 255 включно.
Функція constrain() визначає для наданого їй значення діапазон зміни його від мінімального значення до максимального значення.

Вправа 31

Створити малюнок за допомогою прототипа графічного редактора. Зробити скриншот малюнка і зберегти.

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

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

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

Пригадаємо, що якщо точка належить колу, то її координати задовольняють рівнянню кола

\$(x - a)^2 + (y - b)^2 = r^2\$

де (a, b) - координати центра кола, (x, y) - координати будь-якої точки кола.

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

\$(x - a)^2 + (y - b)^2 ≤ r^2\$

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

Поглянемо на код, який реалізує цю задачу.

sketch.js
let cx;
let cy;
let cSize = 75;
let h;

function setup() {
  createCanvas(200, 200);
  cx = width / 2;
  cy = height / 2;
  strokeWeight(4);
}

function draw() {
  background(48, 52, 63); // Gunmetal
  h = (mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy); (1)
  if (h <= ((cSize / 2) * cSize) / 2) { (2)
    stroke(255, 217, 218); // Pale Pink
    fill(153);
  } else {
    stroke(153);
    fill(153);
  }
  circle(cx, cy, cSize);
}
1 Обчислення лівої частини нерівності, де cx і cs - координати центра кола, mouseX і mouseY - координати вказівника миші.
2 Обчислення правої частини нерівності й порівняння обох частин нерівності. В залежності від значення порівняння (true чи false), виконуються відповідні гілки вказівки розгалуження.

Переглядаємо Аналізуємо

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

Перетягування (Drag & Drop) - форма виконання певних дій у графічних інтерфейсах. У дослівному перекладі з англійської означає «тягни та кинь».

Для цього завдання застосуємо функції подій mousePressed(), mouseDragged() і mouseReleased().

sketch.js
let cx;
let cy;
let cSize = 75;
let overCircle = false; (1)
let locked = false; (2)
let xOffset = 0.0;
let yOffset = 0.0;
let h;

function setup() {
  createCanvas(200, 200);
  cx = width / 2.0;
  cy = height / 2.0;
  strokeWeight(4);
}

function draw() {
  background(48, 52, 63); // Gunmetal
  h = (mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy);
  if (h <= ((cSize / 2) * cSize) / 2) {
    overCircle = true;
    if (!locked) {
      stroke(255, 217, 218); // Pale Pink
      fill(153);
    }
  } else {
    stroke(153);
    fill(153);
    overCircle = false;
  }
  circle(cx, cy, cSize);
}

function mousePressed() {
  if (overCircle) {
    locked = true;
    fill(255, 255, 255); // White
  } else {
    locked = false;
  }
  xOffset = mouseX - cx; (3)
  yOffset = mouseY - cy;
}

function mouseDragged() {
  if (locked) {
    cx = mouseX - xOffset; (4)
    cy = mouseY - yOffset;
  }
}

function mouseReleased() {
  locked = false; (5)
}
1 У змінній overCircle буде зберігатися значення true чи false - відповідь на запитання чи перебуває вказівник миші над колом.
2 Ще одна булева змінна locked - якщо вказівник миші перебуває в області кола і була натиснута кнопка миші набуває значення true і при переміщенні кола на полотні обчислюються нові координати cx і cy кола.
3 Обчислення значень зміщень xOffset і yOffset при натиснутій кнопці миші. Ці значення використовуються для обчислення нових координат кола.
4 Обчислення нових значень координат кола при переміщенні кола на полотні.
5 Змінна locked змінює значення на false при відпусканні кнопки миші.

Переглядаємо Аналізуємо

3.6.2. Клавіатура

Подібно до того, як змінна mouseIsPressed щоразу набуває значення true, коли користувач натискає кнопку миші, для подій клавіатури використовується змінна keyIsPressed , яка щоразу набуває значення true, коли користувач натискає будь-яку клавішу на клавіатурі.

Використаємо змінну keyIsPressed у вказівці розгалуження if.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  if (keyIsPressed) {
    background(252, 221, 188); // Bisque
  } else {
    background(105, 88, 95); // Deep Taupe
  }
}
Змінні mouseIsPressed і keyIsPressed не приймають інших значень, окрім значень true і false. Це значить, що записувати порівнювання як mouseIsPressed === true чи keyIsPressed === true немає потреби.

Відстежувати можна не лише натиснення клавіш на клавіатурі, але і яка із клавіш була натиснута останньою.

Для цього можна використати змінну key , яка містить символ останньої натиснутої клавіші.

sketch.js
function setup() {
  createCanvas(200, 200);
  textAlign(CENTER, CENTER);
}

function draw() {
  background(76, 76, 76); // Davys Grey
  fill(219, 219, 219); // Gainsboro
  textSize(28);
  text(key, width / 2, height / 2);
}

Переглядаємо Аналізуємо

Для відстежування подій натиснення клавіш на клавіатурі можна використовувати функцію keyIsDown() , яка перевіряє, чи зараз певна клавіша натиснута.

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

Функція keyIsDown() отримує число, яке відповідає значенню keyCode для клавіші. Наприклад, для спеціальних клавіш змінна keyCode може набувати таких значень:

  • BACKSPACE,

  • DELETE,

  • ENTER,

  • RETURN,

  • TAB,

  • ESCAPE,

  • SHIFT,

  • CONTROL,

  • OPTION,

  • ALT,

  • UP_ARROW,

  • DOWN_ARROW,

  • LEFT_ARROW,

  • RIGHT_ARROW.

Перевірити числові коди для клавіш клавіатури можна на сайті JavaScript Key Code .

Розглянемо застосунок, у кому можна керувати об’єктом на полотні за допомогою клавіатури.

sketch.js
let x = 100;
let y = 100;

function setup() {
  createCanvas(200, 200);
  fill(252, 221, 188); // Bisque
}

function draw() {
  background(100);
  if (keyIsDown(LEFT_ARROW)) {
    x -= 5;
  }

  if (keyIsDown(RIGHT_ARROW)) {
    x += 5;
  }

  if (keyIsDown(UP_ARROW)) {
    y -= 5;
  }

  if (keyIsDown(DOWN_ARROW)) {
    y += 5;
  }
  ellipse(x, y, 50, 50);
  text(key, 130, 20);
}

Переглядаємо Аналізуємо

У вказівці розгалуження перевіряється, яка із клавіш-стрілок є натиснутою і координата, що відповідає напрямку руху, змінюється з кроком 5. Що відбудеться, якщо запустити застосунок без функції background();?

Використаємо у функції keyIsDown() числові значення keyCode клавіш клавіатури.

sketch.js
let d = 50;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(75, 88, 66); // Rifle Green
  // 107 і 187 числові значення keyCode клавіші "+"
  if (keyIsDown(107) || keyIsDown(187)) {
    d += 1;
  }

  // 109 і 189 числові значення keyCode клавіші "-"
  if (keyIsDown(109) || keyIsDown(189)) {
    d -= 1;
  }

  fill(143, 179, 57); // Apple Green
  ellipse(50, 50, d, d);
}

Переглядаємо Аналізуємо

Подібно до того, як бібліотека p5.js автоматично викликає функції mousePressed(), mouseReleased() і mouseClicked(), залучені на подіях миші, автоматично викликаються й функції keyPressed() , keyReleased() , keyTyped() , залучені на подіях клавіатури.

Створимо застосунок, який дозволить відображати на полотні введені з клавіатури символи. Оголосимо змінну message, яка буде утворювати рядок з усіх введених символів.

sketch.js
let message = "";

function setup() {
  createCanvas(200, 200);
  textSize(36);
  textAlign(CENTER, CENTER);
}

function draw() {
  background(50);
  fill(255, 214, 10); // Gold Web Golden
  text(message, width / 2, height / 2);
}

function keyTyped() {
  message += key;
}

У коді використовується функція keyTyped(), яка викликається один раз при щоразовому натисканні клавіші, за цієї умови дії клавіш Backspace, Delete, Ctrl, Shift та Alt ігноруються.

Щоб простежити keyCode для однієї з клавіш, як-от Backspace, Delete, Ctrl, Shift та Alt, використовують функцію keyPressed() .

Переглядаємо Аналізуємо

Напишемо код для відображення на екрані такої інформації:

  • останнього введеного символу із клавіатури;

  • усіх введених англійських великих і малих літер, символів пропуску, інші символи - ігноруються.

sketch.js
let s = ""; // останній символ, введений з клавіатури
let row = ""; // рядок, в якому зберігатимуться введені символи
function setup() {
  createCanvas(400, 150);
  textAlign(LEFT, TOP);
}

function draw() {
  background(0, 29, 61); // Oxford Blue
  fill(255);

  textSize(14);
  text("Останній введений символ: " + s, 20, 30); (1)
  text(`Рядок складається з: ${row.length} символів`, 20, 60); (2)

  textSize(36);
  text(row, 20, 90); (3)
}

function keyTyped() {
  // змінна key завжди містить значення клавіші,
  // яка була натиснута останньою
  if ((key >= "A" && key <= "z") || key == " ") { (4)
    s = key; (5)
    row = row + key; (6)
    console.log(key); // виведення символів в консоль
  }
}
1 Виведення на екран останнього введеного символу s за допомогою функції text() . Тут використовується конкатенація (об’єднання) двох рядків за допомогою операції +.
2 Виведення на екран кількості введених символів. Тут використовується метод length з JavaScript, який визначає довжину рядка. Для виведення значення довжини рядка (кількість символів у рядку) використовуються синтаксис шаблонних рядків з JavaScript.
3 Виведення на екран рядка із введеними символами.
4 Перевірка введених з клавіатури символів: враховуються лише англійські великі й малі символи та пропуск.
5 Змінна s набуває значення key введеного символу.
6 У рядок row додаються послідовно усі введені символи з клавіатури, які пройшли перевірку у пункті 4.

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

sketch.js
let f = ""; // назва функції події
function setup() {
  createCanvas(320, 190);
}

function draw() {
  background(45, 49, 66); // Space Cadet
  textSize(16);
  fill(255);

  text("mouseIsPressed: " + mouseIsPressed, 20, 30);
  text("mouseButton: " + mouseButton, 20, 50);
  text("mouseX: " + mouseX, 20, 70);
  text("mouseY: " + mouseY, 20, 90);
  text("keyIsPressed: " + keyIsPressed, 20, 110);
  text("key: " + key, 20, 130);
  text("keyCode: " + keyCode, 20, 150);
  text(`event function: ${f}`, 20, 170);
  console.log(f);
}

function keyPressed() {
  f = "keyPressed: " + key;
}

function keyReleased() {
  f = "keyReleased: " + key;
}

function keyTyped() {
  f = "keyTyped: " + key;
}

function mousePressed() {
  f = "mousePressed";
}

function mouseReleased() {
  f = "mouseReleased";
}

function mouseClicked() {
  f = "mouseClicked";
}

function mouseMoved() {
  f = "mouseMoved";
}

function mouseDragged() {
  f = "mouseDragged";
}

function mouseWheel() {
  f = "mouseWheel";
}

Переглядаємо Аналізуємо

Вправа 32

Використовуючи застосунок, дізнатися коди клавіш: BackSpace, Delete, клавіш-стрілок, Enter, пропуск, Alt, Ctrl.

3.6.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що належить до подій миші та клавіатури?

  2. Які функції подій визначені для миші?

  3. Які функції подій визначені для клавіатури?

3.6.5. Практичні завдання

Початковий

  1. Заповнити прогалини в коді, щоб по натисканню кнопки миші лінії ставали помаранчевими, а після натискання будь-якої клавіші на клавіатурі - білими.

function setup() {
  createCanvas(200, 200);
  strokeWeight(5);
}

function draw() {
  background(45, 49, 66); // Space Cadet
  if (...) {
    stroke(239, 131, 84); // Mandarin
  }
  if (...) {
    stroke(255, 255, 255); // White
  }
  line(0, 0, width, height / 2);
  line(width, height / 2, 0, height);
  line(0, height, width / 2, 0);
  line(width / 2, 0, width, height);
}
  1. Заповнити прогалини в коді, який додає квадрат, коли натискається будь-яка кнопка миші, і очищає тло полотна при кожному натисканні будь-якої клавіші на клавіатурі.

function setup() {
  createCanvas(200, 200);
  background(255);
}

function draw() {
  if (...) {
    noStroke();
    fill(0, 109, 119); // Ming
    rectMode(CENTER);
    rect(mouseX, mouseY, 10, 10);
  }
  if (...) {
    ...;
  }
}

Середній

  1. Створити застосунок, в якому можна малювати як олівцем.

  1. Створити застосунок, у якому за допомогою клавіш-стрілок буде переміщуватися на полотні коло, а клавіші R, L, B, C, M і Y будуть змінювати колір зафарбовування кола.

  1. Створити застосунок, який генерує різноколірні кола різного розміру за натискання кнопки миші.

  1. Створити застосунок для малювання, в якому при натисканні кнопки миші змінюється випадковим чином розмір і колір «пензля». Орієнтовний результат роботи застосунку представлено в демонстрації.

  1. Створити застосунок для малювання, в якому вказівник миші є «пензликом, що пульсує», а при натисканні будь-якої кнопки миші полотно очищається.

Високий

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

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

  1. «Полагодити» вмикач/вимикач освітлення з попереднього завдання. Тобто, коли натискаємо на прямокутник (вмикач/вимикач) будь-якою кнопкою миші - змінюється колір тла полотна (вмикається світло). Коли ж відпускаємо кнопку миші - колір тла залишається «увімкненим». Якщо ще раз натискаємо на прямокутник - колір тла стає чорним (світло вимикається).

Екстремальний

  1. Створити прототип простого текстового редактора. У редакторі можна вводити текст і видаляти його справа наліво клавішею BACKSPACE по одному символу.

  1. Реалізувати виконання дії Drag & Drop у графічних інтерфейсах - перетягування фігури на полотні за допомогою вказівника миші.

3.7. Правила написання читабельного коду. Коментарі у тексті програми

Кодування - це вид письма, який має певні правила.

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

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

Рядок коду з помилкою часто знаходиться на один рядок над виділеним рядком.

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

Розглянемо основні правила написання коду.

3.7.1. Змінні

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

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

Наприклад, розглянемо такий код:

let r; (1)
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background("#685369"); // Eggplant
  r += 1; (2)
  fill("#efa48b"); // Dark Salmon
  circle(width / 2, height / 2, r);
}

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

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

1 При оголошенні r змінній не було присвоєно початкове значення, тому значення r має тип undefined.
2 Змінна r, що бере участь в обчисленнях, а саме в інструкції інкременту (збільшення на 1), не має значення, тому результат додавання undefined + 1 дає результат NaN - значення не число (Not-A-Number).

Змінній r потрібно присвоїти початкове значення, наприклад так: let r = 0;. Тепер змінна матиме тип number і математична операція інкременту буде виконуватися успішно.

3.7.2. Функції та параметри

Застосунки складаються з багатьох невеликих фрагментів коду, зібраних разом у цілу структуру. У коді всі ці частини мають різні імена і виконують різні дії.

Функції та параметри є важливими частинами будь-якого застосунку.

Функції - це основні будівельні блоки p5.js. Параметри - це значення, що визначають, як буде працювати функція.

Розглянемо функцію background(), яка встановлює колір тла полотна. У функції є три параметри, що визначають колір. Ці числа задають кількість червоного, зеленого і синього компонентів потрібного кольору.

Наприклад, такий код задає синє тло:

background(51, 102, 153); // Lapis Lazuli

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

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

Вправа 33

Знайти та проаналізувати помилки у коді.

background 51, 102, 153;
background(51 102, 153);
background(51, 102, 153)

3.7.3. Коментарі

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

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

// однорядковий коментар

Можна зробити коментар у кілька рядків, починаючи коментар /* і закінчуючи */.

/*
коментар
у
кілька
рядків
*/

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

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

3.7.4. Регістр літер

Середовище розробки відрізняє великі літери від малих і тому слово Background є відмінним від background.

Наприклад, якщо необхідно намалювати прямокутник за допомогою функції rect(), а в коді написати Rect(), то це викличе помилку.

3.7.5. Стиль коду

Оскільки p5.js використовує мову програмування JavaScript, то усі правила по стилю написання коду походять з JavaScript.

Для JavaScript неважливо, скільки місця використовується для форматування коду. Можна написати так

rect(50, 20, 30, 40);

або

rect(50,20,30,40);

або

rect   (50, 20,
30, 40)       ;

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

Наприклад, для розгалуження можна використовувати такий стиль запису:

if (...) {
  ...;
}

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

Для вказівки розгалуження if, у разі надто довгих умов, використовують наступний синтаксис:

if (
  ... &&
  ... &&
  ...
) {
  ...;
}

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

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

createCanvas(200, 200)
background(0)

В цьому разі JavaScript інтерпретує перенесення рядка як «неявну» крапку з комою. У більшості випадків новий рядок має на увазі крапку з комою. Але «в більшості випадків» не означає «завжди»!

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

3.7.6. Консоль вебпереглядача

Консоль вебпереглядача - окрема область у вебпереглядачі, яка відкривається за допомогою Ctrl+Shift+I.

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

Щоб надрукувати повідомлення у консолі вебпереглядача, використовують функцію console.log().

Наприклад, цей код друкує в консолі повідомлення про поточний час:

sketch.js
let m;
let h;
let s;
function setup() {
  createCanvas(400, 400);
}

function draw() {
  if (second() >= 0 && second() < 10) {
    s = 0;
  } else {
    s = "";
  }
  if (minute() >= 0 && minute() < 10) {
    m = 0;
  } else {
    m = "";
  }
  if (hour() >= 0 && hour() < 10) {
    h = 0;
  } else {
    h = "";
  }
  console.log(
    "На годиннику " + h + hour() + ":" + m + minute() + ":" + s + second()
  );
}

Вправа 34

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

3.7.7. Загальні рекомендації

Дотримуйтесь таких загальних рекомендацій при написанні коду:

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

  • Об’єднуйте свій код у простіші фрагменти та виконуйте їх по черзі.

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

3.7.9. Контрольні запитання

Міркуємо Обговорюємо

  1. У яких випадках доцільно використовувати коментарі?

  2. Що таке «читабельний код»?

  3. Для чого використовується консоль вебпереглядача?

3.7.10. Практичні завдання

Початковий

  1. Виправити помилки у рядках коду.

createcanvas(200, 200;
background();
stroke 255;
fill(150)
rectMode(center);
rect(100, 100, 50);

Середній

  1. Записати коментарі до фрагментів коду, де використовується розгалуження. Перевірити, чи зрозумілий коментований код іншим.

let x = 0;
let y = 0;
let prevX, prevY;

function setup() {
  createCanvas(600, 300);
  background(0);
  x = random(width);
  y = random(height);
  prevX = x + random(-20, 20);
  prevY = y + random(-20, 20);
  frameRate(10);
}

function draw() {
  stroke(random(255), random(255), random(0));
  strokeWeight(1);
  circle(x, y, random(50));

  prevX = x;
  prevY = y;
  x += random(-20, 20);
  y += random(-20, 20);

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

  if (y < 0) {
    y = height;
  } else if (y > height) {
    y = 0;
  }
}
  1. Записати відсутній код, використовуючи інформацію з коментарів. Результат роботи застосунку представлений в демонстрації.

function setup() {
  createCanvas(320, 230);
  background(255);
  noStroke();
}

function draw() {
  // ймовірності для 3-ох різних компонентів кольору
  let red = ...; // 60% для червоного кольору
  ...; // 10% для зеленого кольору
  ...; // 30% для синього кольору

  // випадкове число з рухомою крапкою з діапазону від 0 до 1, яке позначає складову кольору
  let n = ...;

  // якщо n менше .6
  if (...) {
    fill(255, 53, 2, 150); // Coquelicot
    // якщо n має значення в діапазоні від .6 і до .7
  } else if (...) {
    fill(156, 255, 28, 150); // French Lime
    // n в інших випадках (діапазон від .7 до 1.0)
  } else {
    fill(10, 52, 178, 150); // UA Blue
  }

  // намалювати коло
  ...
}

Високий

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

  1. Створити застосунок, який використовує інтерактивний взірець як у демонстрації.

Екстремальний

  1. Створити застосунок, який реалізує один із варіантів алгоритму Random walk («Випадкова прогулянка»), призначеного для вивчення простору (вихід з лабіринту, пошук шляху від однієї точки до іншої тощо). Він працює так: уявіть собі сітку у формі шахової дошки. Кожна клітинка сітки має 4-ох сусідів: зверху, знизу, ліворуч та праворуч. А тепер уявіть, що ви перебуваєте в цій сітці. Щоб вибрати, куди йти далі, ви навмання вибираєте сусідню клітинку. Далі обираєте наступну сусідню клітинку і повторюєте це знову й знову, щоб створити шлях. Це все виконується випадково, тому іноді можна повернутися назад або шляхи будуть перетинатися. Якщо алгоритму надати трохи часу для малювання шляху, можна отримати цікаві візерунки.

Малювати по точках, зміщуючись на одиницю у випадковому напрямку, не виходячи за межі полотна. Кольорові компоненти змінювати на невелике значення і «тримати» в межах від 0 до 255.
Випадкова прогулянка
Випадкова прогулянка (зображення з покликанням)

4. Функції

У цьому розділі ви детально зможете розібратися з тим, як створити власну функцію і використати її у своєму застосунку.

4.1. Метод функціональної декомпозиції задачі. Модульність

Функція - фрагмент коду, до котрого можна неодноразово звертатись під час виконання застосунку. Функція може приймати певні значення та повертати результати.

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

Ми вже знаємо, як викликати попередньо визначені функції із бібліотеки p5.js, на зразок, ellipse(), background() та інші. Вбудовані функції виконують конкретні задачі і їх розміщують всередині ще двох функцій: setup() і draw().

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

Для написання власної функції можна використовувати вбудовані функції, на зразок, line(), ellipse() та інші. Після того, як код для побудови автомобіля буде написаний у вигляді функції, назву функції, наприклад createAuto(), записують в коді застосунку стільки разів, скільки потрібно разів намалювати автомобіль.

Функції допомагають керувати складністю початкового коду, групуючи інструкції, що можуть повторно виконуватися, під одним виконуваним ім’ям.

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

Щоб викликати власну функцію, її ім’я записують всередині функції draw().

function draw() {
  background(0);    // виклик вбудованої функції
  createAuto();     // виклик власної функції
}

Цікавимось

Для чого використовують власні функції?

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

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

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

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

Модульність - функції розбивають увесь код застосунку на менші частини, роблячи код більше керованим і читабельним.

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

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

Багаторазове використання - функції дозволяють повторно використовувати код без необхідності його повторного введення.

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

Наприклад, використаємо власну функцію createAuto() для створення автомобіля. Наша власна функція у застосунку нічим не відрізнятиметься за структурою від інших вбудованих функцій p5.js, тому викликається за своїм іменем.

function draw() {
  background(0); // виклик вбудованої функції
  createAuto(); // виклик власної функції
}

Але запустивши застосунок, з’явиться помилка з повідомленням createAuto is not defined, оскільки застосунку невідоме ім’я createAuto, інакше кажучи, функція з таким ім’ям не визначена (не оголошена) в коді.

Визначення функції передбачає опис усіх інструкцій, котрі має виконати ця функція.

Визначення (створення, оголошення) нової функції починається в JavaScript із використання зарезервованого слова function, пропуску та імені функції з парою круглих дужок, після яких йде пара фігурних дужок, всередині яких записують тіло функції - інструкції, які виконуються при виклику функції.

function ім'яФункції() {
  // тіло функції
}

Отже, перед тим як викликати власну функцію, її насамперед необхідно оголосити в коді.

Після оголошення функції її ім’я стає ім’ям змінної, значенням якої є сама функція.

Наприклад, коли записується команда line(0, 0, 100, 100);, по суті, викликається вбудована функція line(), яка вже оголошена і має свій код.

Визначимо функцію, яка буде малювати коло червоного кольору у центрі полотна.

function drawRedCircle() {
  ...
  ...
}

Ім’я drawRedCircle() для функції довільне, але записане відповідно до правил запису імен, а тіло функції буде складатися з двох інструкцій. Зверніть увагу, що це лише оголошення функції - при запуску застосунку тіло функції не буде виконуватися.

Щоб функція була виконана, її необхідно викликати з тої частини коду, яка виконується. Це досягається записом імені функції у draw().

Вправа 35

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

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(255);
  drawRedCircle();
}

function drawRedCircle() {
  ...
  ...
}

Загалом функції у JavaScript, які оголошені за допомогою зарезервованого слова function в певному блоці коду, можна викликати у будь-яких місцях цього блоку.

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

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

Цікавимось Додатково

Способи створення функцій у JavaScript

Поширеним способом створення функції в JavaScript є оголошення назви функції за допомогою function.

Наприклад:

function myFunc() { // оголошення функції
  // тіло функції
}
myFunc(); // виклик функції

У наведеному вище коді створюється функція з назвою myFunc.

Інший варіант створення функції - використання лише зарезервованого слова function:

let myFunc = function() { // оголошення функції
  // тіло функції
}
myFunc(); // виклик функції

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

В JavaScript існує лаконічний синтаксис для створення функцій без назви, який називається стрілочна функція і має наступний вигляд:

let arrowFunc = (a1, a2, ...aN) => expression;

Цей запис створює покликання з ім’я arrowFunc на функцію, яка приймає аргументи a1..aN, а потім обчислює вираз expression у правій частині від стрілки з використанням аргументів та повертає результат.

Наведену лаконічну версію функції arrowFunc можна перетворити на звичайну функцію без назви:

let arrowFunc = function(a1, a2, ...aN) {
  return expression;
};

Для прикладу, напишемо дві еквівалентні функції без назви для обчислення добутку двох чисел двома способами:

  1. Спосіб, який використовується зазвичай для функцій без назви:

const multiply = function (a, b) {
	return a * b;
};
console.log(multiply(6, 7)); // 42
  1. Спосіб за допомогою стрілочної функції:

const multiply = (a, b) => a * b;
console.log(multiply(6, 8)); // 48

4.1.2. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «функція»?

  2. Яка суть методу функціональної декомпозиції задачі?

  3. Які кроки необхідно виконати, щоб написати та використати власну функцію у коді застосунку?

  4. Поміркувати, які імена для функцій варто обрати, якщо тіло функції виконує наступні дії: малює зафарбований зеленим кольором прямокутник; малює два кола різного розміру; малює горизонтальну лінію, яка розділяє полотно на дві однакові частини?

  5. Навести приклади задач, для розв’язання яких варто використати функціональний підхід.

4.1.3. Практичні завдання

Початковий

  1. Створити функцію для побудови зображення будинку, використовуючи зразок коду.

stroke(0);
fill(95, 89, 128); // Purple Navy
rect(50, 70, 100, 100);
fill(47, 48, 97); // Space Cadet
triangle(50, 70, 150, 70, 100, 20);
fill(255);
rect(65, 95, 25, 25);
rect(110, 95, 25, 25);
fill(201, 203, 163); // Sage
rect(100, 136, 25, 33);
Будинок
Будинок

Середній

  1. Створити функцію для побудови зображення сови, використовуючи зразок коду.

stroke(71, 104, 44); // Dark Olive Green
strokeWeight(40);
line(100, 100, 100, 120);
noStroke();
fill(255);
arc(100, 100, 40, 40, 0, PI);
ellipse(90, 100, 20, 20);
ellipse(110, 100, 20, 20);
fill(0);
ellipse(110, 100, 5, 5);
ellipse(90, 100, 5, 5);
quad(100, 105, 102, 107, 100, 110, 97, 107);
Сова
Сова

Високий

  1. Намалювати персонажа, використовуючи функції для створення окремих його частин. Організувати код відповідно до наведеної структури.

function setup() {
  ...
}
function draw() {
  ...
  drawCreature(); // виклик функції користувача
  ...
}

function drawCreature() {
  // намалювати тіло у формі кола
  ...

  // намалювати дві ноги у формі прямокутників
  ...

  // намалювати два ока
  ...

  // намалювати райдужки очей
  ...

  // намалювати зіниці очей
  ...

  // намалювати рот
  ...
}

function body() {
  ...
}

function legs() {
  ...
}

function eyes() {
  ...
}

function irises() {
  ...
}

function pupils() {
  ...
}

function mouth() {
  ...
}
Прибулець
Прибулець

Екстремальний

  1. Створити імітацію говоріння персонажа, коли відбувається введення символів з клавіатури. Введений текст відображається на екрані. Реалізувати можливість видалення тексту посимвольно - у цей момент персонаж «мовчить». Орієнтовний результат роботи застосунку представлений в демонстрації.

У застосунку можна використати метод substring() із JavaScript для реалізації видалення символів.

4.2. Функції. Бібліотеки та модулі

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

sketch.js
// оголошення глобальних змінних
let x = 0;
let velocity = 1;

function setup() {
  createCanvas(200, 200);
}
function draw() {
  background(255);
  // зміна координати x (швидкість руху кульки)
  x = x + velocity;

  // якщо кулька досягла лівої межі полотна,
  // зворотний напрямок руху
  if (x < 16) {
    velocity = 1;
  }

  // якщо кулька досягла правої межі полотна,
  // зворотний напрям руху
  if (x > width - 16) {
    velocity = velocity * -1;
  }

  // намалювати кульку в точці з координатою x
  stroke(0);
  fill(136, 80, 83); // Tuscan Red
  ellipse(x, 100, 32, 32);
}

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

Визначимо такі функції:

  • movement() - рух кульки;

  • bouncing() - відбивання кульки;

  • painting() - малювання кульки на екрані.

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

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

sketch.js
// оголошення глобальних змінних
let x = 0;
let velocity = 1;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  movement();
  bouncing();
  painting();
}

function movement() {
  // зміна координати x (швидкість руху кульки)
  x = x + velocity;
}

function bouncing() {
  // якщо кулька досягла лівої межі полотна,
  // зворотний напрямок руху
  if (x < 16) {
    velocity = 1;
  }

  // якщо кулька досягла правої межі полотна,
  // зворотний напрям руху
  if (x > width - 16) {
    velocity = velocity * -1;
  }
}

function painting() {
  // намалювати кульку в точці з координатою x
  stroke(0);
  fill(136, 80, 83); // Tuscan Red
  ellipse(x, 100, 32, 32);
}

Зараз код у функції draw() став організованим і читабельним, оскільки складається лише з викликів функцій.

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

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

4.2.1. Файли та модулі

Якщо далі продовжувати розширювати застосунок, принцип модульності можна реалізувати, помістивши код у кілька JavaScript-файлів.

Наприклад, створимо поруч з файлом sketch.js файл movement.js і приєднаємо його у файл index.html

index.html
...
<script src="./movement.js"></script>
<script src="./sketch.js"></script>
...

Далі з файлу sketch.js перемістимо у файл movement.js код функції movement()

movement.js
function movement() {
  // зміна координати x (швидкість руху кульки)
  x = x + velocity;
}

Цікавимось Додатково

Модулі в JavaScript

Код застосунку можна організовувати також за допомогою спеціальних файлів - модулів.

Можливість використовувати модулі для організації коду з’явилася у стандарті JavaScript у 2015 році.

Модулі можуть завантажувати один одного, викликати функції одного модуля з іншого. Для обміну функціональністю система модулів JavaScript використовує команди export та import:

  • export визначає змінні та функції, які мають бути доступні поза поточним модулем;

  • import дозволяє імпортувати функціональність з інших модулів.

Розглянемо приклад, який ілюструє використання файлів як модулів.

У разі локального завантаження файлів-модулів JavaScript на вебсторінку, необхідно налаштувати локальний вебсервер.
Таблиця "Підтримка модулів JavaScript (стандарт ES6, 2015) у деяких середовищах запуску застосунків"
Середовище Підтримка

p5.js Web Editor

ні

OpenProcessing

так

Processing IDE

так

Локальна розробка

так, у разі запуску на локальному вебсервері

Створимо модуль - JavaScript-файл rectangle.js з таким вмістом:

rectangle.js
let r = 30;

function rectangle(p, d) {
  p.rect(20, 40, 160, d * 2);
}

export { r, rectangle };

Команда експорту export робить змінну r і функцію rectangle() доступними поза модулем.

У цьому файлі є оголошення змінної r із числовим значенням 30 і власної функції rectangle() для малювання прямокутника. Функції rectangle() отримує числове значення d, на основі якого вказуються розміри прямокутника, і p - змінну, що є екземпляром або примірником бібліотеки p5.js. Це значить, що до змінної p будуть «прив’язані» усі функції бібліотеки p5.js.

За стандартним налаштуванням усі функції p5.js знаходяться в глобальному просторі імен («прив’язані» до об’єкта вікна вебпереглядача), тому викликаються просто за іменем: ellipse(), rect(), fill() і т. д.

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

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

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

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

У файлі sketch.js увімкнемо режим екземпляра і виконаємо імпорт функції rectangle() і змінної r:

sketch.js
import { rectangle, r } from "./rectangle.js";

new p5(function(p) {

  p.setup = function() {
    p.createCanvas(200, 200);
  }

  p.draw = function() {
    p.background(220);
    p.noStroke();
    p.fill(35, 100, 170); // Green Blue
    rectangle(p, r);
    p.fill(254, 198, 1); // Mikado Yellow
    p.rect(20, 100, 160, r * 2);
  }
});

Команда імпорту import завантажує модуль за шляхом ./rectangle.js відносно поточного файлу sketch.js та записує експортовану функцію rectangle() і експортовану змінну r у змінні з відповідними іменами.

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

При імпорті можна змінити імена імпортованих змінних, використовуючи зарезервоване слово as.

У наступному прикладі експортована функція rectangle() буде імпортована під ім’ям createRectangle() і у файлі sketch.js необхідно використовувати для виклику цієї функції її нове ім’я createRectangle().

sketch.js
import { rectangle as createRectangle, p } from "./rectangle.js";

new p5(function(p) {

  p.setup = function() {
    p.createCanvas(200, 200);
  }

  p.draw = function() {
    p.background(220);
    p.noStroke();
    p.fill(35, 100, 170); // Green Blue
    createRectangle(p, r); (1)
    p.fill(254, 198, 1); // Mikado Yellow
    p.rect(20, 100, 160, r * 2); (2)
  }
});
1 Імпортована функція з новим ім’ям createRectangle() будує прямокутник синього кольору на основі імпортованого значення r. Щоб отримати доступ до вбудованої функції rect() бібліотеки p5.js, у функцію createRectangle() ми передаємо екземпляр p.
2 З екземпляра p бібліотеки p5.js використовуємо вбудовану функцію rect() для побудови жовтого прямокутника.

Тепер в коді сторінки index.html необхідно приєднати файл sketch.js так:

index.html
...
<script type="module" src="./sketch.js"></script>
...

Оскільки модулі відрізняються від звичайних файлів, вебпереглядачу необхідно явно вказати за допомогою атрибута <script type="module">, що файл є модулем.

Відкривши файл index.html у вебпереглядачі отримаємо зображення прапора України.

Український стяг
Український стяг

4.2.2. Бібліотеки

Інформацію про будь-яку вбудовану функцію бібліотеки p5.js можна отримати зі сторінки її довідки по функціях .

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

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

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

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

  • основні бібліотеки, які є частиною p5.js (наприклад, p5.sound.js);

  • зовнішні бібліотеки, що розробляються і підтримуються членами спільноти p5.js.

Щоб приєднати бібліотеку у свій ескіз, необхідно зв’язати її з файлом index.html у тезі <head> за допомогою тега <script> у рядку нижче приєднаного файлу p5.js, але вище файлу ескізу sketch.js.

У цьому разі вміст HTML-файлу може виглядати так:

index.html
<!DOCTYPE html>
<html lang="">
<head>
  ...
  <script src="./p5.js"></script>
  <script src="./path/to/library"></script> <!-- приєднання зовнішньої бібліотеки -->
  <script src="./sketch.js"></script>
</head>

<body>
  <main>
  </main>
</body>

</html>
В середовищі Processing IDE є можливість встановити деякі зовнішні бібліотеки та імпортувати їх у свій проєкт. Для цього необхідно виконати команду Ескіз  Імпортувати бібліотеку…​.
p5.shape.js

Як приклад роботи із зовнішньою бібліотекою, розглянемо бібліотеку p5.shape.js , яка розширює список простих фігур , які можна будувати за допомогою p5.js.

Щоб використати бібліотеку p5.shape.js, необхідно:

  • Завантажити архів бібліотеки p5.shape.js з файлом бібліотеки та іншими файлами зі сторінки GitHub -сховища (зелена кнопка з написом Code).

  • Файл бібліотеки p5.shape.js із розпакованого архіву скопіювати у каталог libraries (за потреби створити) проєкту.

  • У файлі index.html проєкту правильно прописати шлях до файлу бібліотеки.

index.html
<!DOCTYPE html>
<html lang="">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.js example</title>
  <style>
    body {
      padding: 0;
      margin: 0;
    }
  </style>
  <script src="./libraries/p5.min.js"></script>
  <script src="./libraries/p5.shape.js"></script> <!-- приєднання бібліотеки p5.shape.js -->
  <script src="./sketch.js"></script>
</head>

<body>
  <main>
  </main>
</body>

</html>
Для роботи з бібліотекою p5.shape.js можна скористатися Processing IDE, розмістивши файл бібліотеки у каталозі libraries проєкту та правильно прописавши шлях до p5.shape.js у файлі index.html.

Використаємо можливості p5.shape.js для малювання нових фігур, записавши код у файл ескізу sketch.js.

sketch.js
function setup() {
  createCanvas(650, 600);
}

function draw() {
  textSize(15);
  background(245);
  fill(55, 150, 250); // Dodger Blue
  center(5);
  text("center(size)", width / 2 - 20, height / 2 + 15);
  pent(250, 220, 65, 1);
  text("pent(x, y, size, lineT)", 200, 270);
  hexagon(100, 200, 50, 1);
  text("hexagon(x, y, size, lineT)", 30, 275);
  tri(100, 60, 50, 1);
  text("tri(x, y, size, lineT)", 50, 125);
  hept(100, 400, 50, 1);
  text("hept(x, y, size, lineT)", 50, 450);
  octo(270, 430, 60, 1);
  text("octo(x, y, size, lineT)", 200, 480);
  nona(450, 450, 50, 1);
  text("nona(x, y, size, lineT)", 400, 500);
  fill(255, 10, 200); // Hot magenta
  heart(550, 95, 50, 1);
  text("heart(x, y, size, lineT)", 500, 170);
  fill(55, 150, 250);
  deca(550, 300, 55, 1);
  text("deca(x, y, size, lineT)", 490, 350);
  trap(400, 150, 100, 1);
  text("trap(x, y, size, lineT)", 340, 220);
  rightTri(270, 70, 60, 1);
  text("rightTri(x, y, size, lineT)", 210, 120);
}

Вправа 36

Намалювати на полотні за допомогою бібліотеки p5.shape.js два серця синього і жовтого кольорів відповідно.

Відомості про те, як використовувати зовнішні бібліотеки для розширення функціонала p5.js, можна прочитати на сторінці офіційної документації .
TurtleGL.js

JavaScript Turtle Graphics Library (TurtleGL.js) - зовнішня бібліотека, яка використовує можливості p5.js і JavaScript для створення Черепашачої графіки (Turtle Graphics) у вебпереглядачі. Є навчальним середовищем для програмування, яке використовує об’єктоорієнтований підхід.

Для знайомства з бібліотекою TurtleGL.js перейдіть у Додаток C.

4.2.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Навіщо основний код застосунку об’єднують: а) у модульні частини одного файлу? б) у кількох файлах?

  2. Опишіть основні кроки алгоритму приєднання зовнішньої бібліотеки до проєкту.

4.2.5. Практичні завдання

Початковий

  1. В основному коді є виклик функції painting():

let x = 0;
let velocity = 1;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  movement();
  bouncing();
  painting();
}

function movement() {
  x = x + velocity;
}

function bouncing() {
  if (x < 16) {
    velocity = 1;
  }

  if (x > width - 16) {
    velocity = velocity * -1;
  }
}

// оголошення функції

Оголосити у вказаному місці функцію painting() з наступним кодом:

background(255);
rectMode(CENTER);
noFill();
stroke(19, 3, 3); // Xiketic
rect(x, x, 32, 32);
fill(255);
rect(x - 4, x - 4, 4, 4);
rect(x + 4, x - 4, 4, 4);
line(x - 4, x + 4, x + 4, x + 4);

Середній

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

let x = 0;
let y = 0;

function setup() {
  createCanvas(600, 400);
  background(41, 41, 46); // Raisin Black
  frameRate(10);
}

function draw() {
  x = random(mouseX - 70, mouseX + 100);
  y = random(mouseY - 50, mouseY + 70);

  fill(49, 36, 102, 10); // St Patricks Blue
  stroke(0, 206, 209, 60); // Dark Turquoise
  strokeWeight(1);
  ellipse(x, y, 100, 100);
}

function keyPressed() {
  x = random(width);
  y = random(height);

  stroke(204, 102, 0, 30); // Alloy Orange
  strokeWeight(2);
  rect(x, y, 100, 50);
  stroke(255, 255, 255, 80);
  rect(x + 10, y + 20, 100, 100);
  stroke(0, 0, 0, 80);
  rect(x + 25, y + 25, 40, 40);
}

Високий

  1. Використати бібліотеку TurtleGL.js для створення зображення на свій задум.

Використання бібліотеки TurtleGL.js
Черепашача графіка за допомогою TurtleGL.js

Екстремальний

  1. Використати у своєму проєкті бібліотеку з колекції на вибір.

4.3. Передавання значень у функцію та з неї. Формальні та фактичні параметри

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

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

4.3.1. Параметри функції

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

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

Наприклад, у функціях background(100); і strokeWeight(4); аргументами є цілі числа 100 і 4 відповідно.

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

Ці імена називаються параметрами або формальними параметрами.

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

Проілюструємо розуміння аргументів і параметрів функції.

Для наших цілей використаємо зображення (510х480 пікселів) картини Лихвар і його дружина (1514 рік) художника Квентіна Массейса.

Використавши функцію loadImage() , під’єднаємо зображення до нашого ескізу.

У разі використання середовища Processing IDE, створіть каталог data для зберігання додаткових файлів проєкту у каталозі вашого ескізу. У каталог data скопіюйте файл зображення. Шлях до зображення у функції loadImage() повинен бути відносним до HTML-файлу ескізу.
Завантаження зображення з URL-адреси або іншого віддаленого розташування може бути заблоковано через вбудовану безпеку вашого вебпереглядача.

Отже, додамо код в ескіз, щоб завантажити зображення на полотно:

sketch.js
let art;

function preload() {
  art = loadImage("./data/Massysm_Quentin_—_The_Moneylender_and_his_Wife_—_1514.jpg");
}

function setup() {
  createCanvas(510, 480);
  image(art, 0, 0, width, height);
}

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

Лихвар: Пам’ятаєш, ми проходили біля ювелірної крамниці пана Елігія, яка зображена на картині Петруса Крістуса?

Дружина лихваря: Авжеж.

Лихвар: Там я пригледів для тебе перлову сережку, як у дівчини з картини Яна Вермера.

Дружина лихваря: (подумки) Цікаво, яку книгу читає людина, що сидить біля вікна?

— за мотивами картини Квентіна Массейса
Лихвар і його дружина

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

Визначимо функцію speechBalloon() для малювання словесної бульбашки з текстом Пам’ятаєш, ми проходили поруч з ювелірною крамницею пана Елігія, яка зображена на картині Петруса Крістуса? у точці з координатами (25, 120):

sketch.js
let art;

function preload() {
  art = loadImage(
    "./data/Massysm_Quentin_—_The_Moneylender_and_his_Wife_—_1514.jpg"
  );
}

function setup() {
  createCanvas(510, 480);
  image(art, 0, 0, width, height);
}

function draw() {
  speechBalloon();
}

function speechBalloon() {
  let x = 25;
  let y = 120;
  let t = "Пам'ятаєш, ми проходили поруч з ювелірною крамницею пана Елігія, яка зображена на картині Петруса Крістуса?";

  // трикутник словесної бульбашки
  noStroke();
  fill(255);
  beginShape();
  vertex(x + 100, y - 10);
  vertex(x + 60, y - 80);
  vertex(x + 30, y - 80);
  endShape(CLOSE);

  // розмір тексту і відступи
  textSize(12);
  let txtWidth = textWidth(t);
  let txtHeight = txtWidth / (txtWidth / 3);
  let py = y - 80;

  if (txtWidth >= width / 2) {
    txtWidth = txtWidth / 2 * 0.85;
    txtHeight = txtHeight * 21.5;
  } else if (txtWidth >= width / 3) {
    txtWidth = txtWidth / 3 * 1.95;
    txtHeight = txtHeight * 14.5;
  } else if (txtWidth >= width / 4) {
    txtWidth = txtWidth / 4 * 2.95;
    txtHeight = txtHeight * 14.5;
  } else {
    txtWidth = txtWidth / 5 * 9.85;
    txtHeight = txtHeight * 14.5;
  }

  // прямокутник словесної бульбашки
  rect(x, py - txtHeight / 2, txtWidth, txtHeight, 12);

  // текст у словесній бульбашці
  fill(0);
  textAlign(LEFT, CENTER);
  text(t, x + 6, py, txtWidth - 6);
}

а потім викличемо її всередині функції draw()

sketch.js
function draw() {
  speechBalloon();
}

Результатом буде розміщення на зображенні словесної бульбашки, утвореної із фігур трикутника і прямокутника з округленими кутами та вказаним текстом всередині.

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

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

Щоб надрукувати напис в іншому місці зображення, необхідно змінити значення координат і текст напису у тілі функції. За таких умов, на зображення можна вивести одночасно лише один певний текстовий напис.

Змінимо це, додавши параметри в оголошення функції, які дозволяють передавати значення у функцію за допомогою різних аргументів у виклику функції.

Відредагуємо оголошення функції speechBalloon() так, щоб функція могла приймати чотири аргументи: x-координату, y-координату, текст напису і напрямок direction бульбашки (вгору - top або вниз - bottom). Також необхідно закоментувати (або видалити) рядки із присвоєнням значень змінним x, y і t, щоб уникнути перезапису значень, які будуть передані за допомогою виклику функції.

sketch.js
function draw() {
  speechBalloon(240, 385, "Авжеж.", "bottom"); (1)
}

function speechBalloon(x, y, t, direction) { (2)
  // x = 25;
  // y = 120;
  // t = "Пам'ятаєш, ми проходили біля ювелірної крамниці пана Елігія, яка зображена на картині Петруса Крістуса?";

  // трикутник словесної бульбашки
  noStroke();
  fill(255);

  if (direction === "top") { (3)
    beginShape();
    vertex(x + 100, y - 10);
    vertex(x + 60, y - 80);
    vertex(x + 30, y - 80);
    endShape(CLOSE);
  } else {
    beginShape();
    vertex(x + 100, y - 140);
    vertex(x + 60, y - 80);
    vertex(x + 30, y - 80);
    endShape(CLOSE);
  }

  // розмір тексту і відступи
  ...
1 Змінні x, y, t, direction в оголошенні функції speechBalloon() є параметрами або формальними параметрами функції.
2 Значення 240, 385, "Авжеж." і "bottom" передані у функцію speechBalloon() при її виклику є аргументами або фактичними параметрами функції.
3 В залежності від значення параметра direction, фігури трикутників у словесній бульбашці змінюють напрямок.

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

Фактичний параметр - це конкретне значення, яке підставляється на місце формального параметра.

Тобто, виклик функції speechBalloon(240, 385, "Авжеж.", "bottom") означає передачу у функцію конкретних значень 240, 385, "Авжеж." і "bottom", які всередині функції будуть використовуватися з іменами x, y, t і direction відповідно.

З аргументами функцій ми вже зустрічалися неодноразово, коли використовували вбудовані функції p5.js. Наприклад, щоб намалювати лінію, застосовують вбудовану функцію line() із чотирма аргументами: line(10, 25, 100, 75);.

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

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

Перший аргумент 240 призначається параметру x, другий аргумент 385 призначається параметру y, третій аргумент "Авжеж." призначається параметру t, четвертий параметр "bottom" призначається параметру direction і так далі, у тому самому порядку як параметри з’являються в рядку оголошення функції.

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

Запустивши ескіз, можна переконатися, що результат візуально зміниться.

Словесна бульбашка
Словесна бульбашка: застосування функції з параметрами

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

sketch.js
function draw() {
  speechBalloon(25, 120, "Пам'ятаєш, ми проходили поруч з ювелірною крамницею пана Елігія, яка зображена на картині Петруса Крістуса?", "top");
  speechBalloon(240, 385, "Авжеж.", "bottom");
  speechBalloon(30, 385, "Там я пригледів для тебе перлову сережку, як у дівчини з картини Яна Вермера.", "bottom");
}
Кілька словесних бульбашок
Декілька словесних бульбашок: застосування функції з параметрами

Важливо дотримуватись таких правил передачі значень у функцію:

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

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

  • Параметри є локальними змінними для функції та доступні лише в межах цієї функції.

Іноді, функцію викликають з меншою кількістю аргументів, ніж кількість оголошених у ній параметрів.

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

Тобто, якщо функцію для побудови прямокутника викликати з чотирма аргументами, тоді отримаємо прямокутник з кутами 90 градусів. Але якщо вказати п’ятий аргумент, відмінний від нуля, то отримаємо прямокутник з округленими кутами.

У коді функції speechBalloon() використовується функція rect() з п’ятьма аргументами, останній з яких відповідає за радіус округлення кутів прямокутника.

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

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

Цікавимось Додатково

Стандартні значення параметрів функцій у мовах програмування

Проілюструємо присвоєння параметрам функції стандартних значень у двох мовах програмування JavaScript і Python на прикладі створення функції, яка додає елементи у масив (JavaScript) і у список (Python).

Оголосимо і викличемо кілька разів функцію у JavaScript:

function addToArray(e, a = []) {
    a.push(e);
    return a;
}

addToArray(10);  // [10]
addToArray(50);  // [50]
addToArray(100); // [100]

Стандартне значення порожнього масиву [] присвоюється змінній a щоразу при виклику функції addToArray, а не коли функція оголошується. Тому щоразу створюється новий порожній масив.

У мові Python ситуація буде дещо іншою. Змінна a отримує стандартне значення порожнього списку [] в оголошенні функції. Тому щоразовий виклик функції add_to_list буде використовувати той самий список:

def add_to_list(e, a=[]):
    a.append(e)
    return a

print(add_to_list(10))  # [10]
print(add_to_list(50))  # [10, 50]
print(add_to_list(100)) # [10, 50, 100]
При оголошенні функцій, параметри, що мають стандартні значення, розміщують у кінці списку параметрів функції.

Додамо в оголошення функції speechBalloon() стандартне значення "Книга, яку читаєш, цікава?" для параметра t, враховуючи порядок параметрів:

sketch.js
function speechBalloon(x, y, direction, t = "Книга, яку читаєш, цікава?") {
  ...
}

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

sketch.js
function draw() {
  speechBalloon(5, 345, "bottom"); (1)
  speechBalloon(240, 385, "bottom", "Авжеж."); (2)
  ...
}

function speechBalloon(x, y, direction, t = "Книга, яку читаєш, цікава?") {
  ...
}
1 Застосовується стандартне значення для параметра t, оскільки у виклику функції для параметра t не вказано значення позиційного аргументу.
2 Значення за стандартним налаштуванням для параметра t не використовується, оскільки у виклику функції для параметра t вказано значення позиційного аргументу.
Використання в оголошенні функції стандартного значення параметра
Використання в оголошенні функції стандартного значення параметра

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

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

У підсумку, функція speechBalloon() матиме наступний вигляд:

sketch.js
function speechBalloon(x, y, direction, t = "Книга, яку читаєш, цікава?", type = "speech") { (1)
  noStroke();
  fill(255);

  switch (type) { (2)
    // трикутник словесної бульбашки
    case "speech": (3)
      if (direction === "top") {
        beginShape();
        vertex(x + 100, y - 10);
        vertex(x + 60, y - 80);
        vertex(x + 30, y - 80);
        endShape(CLOSE);
      } else {
        beginShape();
        vertex(x + 100, y - 140);
        vertex(x + 60, y - 80);
        vertex(x + 30, y - 80);
        endShape(CLOSE);
      }
      break;
    // кола думок
    case "thought": (4)
      if (direction === "top") {
        circle(x + 70, y - 20, 7);
        circle(x + 60, y - 35, 15);
      } else {
        circle(x + 50, y - 130, 7);
        circle(x + 40, y - 115, 15);
      }
      break;
  }

  // розмір тексту і відступи
  textSize(12);
  let txtWidth = textWidth(t);
  let txtHeight = txtWidth / (txtWidth / 3);
  let py = y - 80;

  if (txtWidth >= width / 2) {
    txtWidth = (txtWidth / 2) * 0.85;
    txtHeight = txtHeight * 21.5;
  } else if (txtWidth >= width / 3) {
    txtWidth = (txtWidth / 3) * 1.95;
    txtHeight = txtHeight * 14.5;
  } else if (txtWidth >= width / 4) {
    txtWidth = (txtWidth / 4) * 2.95;
    txtHeight = txtHeight * 14.5;
  } else {
    txtWidth = (txtWidth / 5) * 9.85;
    txtHeight = txtHeight * 14.5;
  }

  // прямокутник словесної бульбашки
  rect(x, py - txtHeight / 2, txtWidth, txtHeight, 12);

  // текст у словесній бульбашці
  fill(0);
  textAlign(LEFT, CENTER);
  text(t, x + 6, py, txtWidth - 6);
}
1 В оголошенні функції speechBalloon() є два параметри зі стандартними значеннями (t = "Книга, яку читаєш, цікава?", type = "speech"). Зверніть увагу, що вони йдуть після параметрів, що не мають значень за стандартним налаштуванням. Як вже було зазначено, якщо оголошується будь-яка функція зі значеннями за стандартним налаштуванням, варто розмістити ці параметри у кінці списку.
2 Щоб не використовувати вказівки if, для розгалуження використовується інструкція switch, яка отримує значення параметра type і порівнює його із case-значеннями "speech" і "thought", переліченими всередині неї. Після перевірки виконуються відповідні інструкції.
3 У разі, якщо type дорівнює "speech", тобто персонаж на картині «говорить», будується трикутник словесної бульбашки.
4 У разі, якщо type дорівнює "thought", тобто персонаж на картині «думає», будуються кола словесної бульбашки.

Виконаємо виклики функції для ілюстрації усього діалогу.

sketch.js
let art;

function preload() {
  ...
}

function setup() {
  ...
}

function draw() {
  speechBalloon(
    25,
    120,
    "top",
    "Пам'ятаєш, ми проходили поруч з ювелірною крамницею пана Елігія, яка зображена на картині Петруса Крістуса?"
  );
  speechBalloon(240, 385, "bottom", "Авжеж.");
  speechBalloon(
    5,
    345,
    "bottom",
    "Там я пригледів для тебе перлову сережку, як у дівчини з картини Яна Вермера."
  );
  speechBalloon(
    215,
    190,
    "top",
    "Цікаво, яку книгу читає людина, що сидить біля вікна?",
    "thought"
  );
}

function speechBalloon(x, y, direction, t = "Книга, яку читаєш, цікава?", type = "speech") {
  ...
}
Діалог, що міг би відбутися
Діалог, що міг би відбутися

4.3.2. Функція, яка повертає значення

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

Існує ще один вид функцій, які при виклику не просто виконують якісь конкретні дії, але й повертають («повідомляють») результат своєї роботи в основний код застосунку.

Пригадаємо, як працює вбудована функція random(), яка генерує випадкові числа з певного діапазону. Кожного разу, коли викликається функція random(), вона «повертає» випадкове значення в межах вказаного діапазону.

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

Якщо необхідно написати власну функцію, яка повертає значення в основний код застосунку, використовують зарезервоване слово return, яке одночасно є точкою виходу з функції.

Отже, напишемо власну функцію з іменем changeText(), яка буде доповнювати текст словесної бульбашки.

sketch.js
function speechBalloon(x, y, direction, t = "Книга, яку читаєш, цікава?", type = "speech") {
  ...
}

function changeText(t, newText) {
  t += newText;
  return t;
}

Функція викликатиметься з двома позиційними аргументами: перший аргумент t - основний текст словесної бульбашки, другий аргумент newText - новий текст, який буде доповнювати основний текст словесної бульбашки.

Зарезервоване слово return повертає значення із функції changeText(). Якщо додати будь-який код в тіло функції changeText() після рядка return, цей код буде проігнорований.

Отже, розширимо основний текст словесної бульбашки лихваря текстом "…​42, 43, 44, 45…​ 50. Ніби все гаразд.", а текст хмаринки думок дружини лихваря текстом "У дзеркалі, бачите?". Для цього оголосимо дві змінні newText1 і newText2, які будуть зберігати текстові доповнення відповідно.

Використаємо функцію changeText(), яка повертає значення, як аргументи у виклику функції speechBalloon().

sketch.js
let art;

function preload() {
  ...
}

function setup() {
  ...
}

function draw() {
  let newText1 = "...42, 43, 44, 45... 50. Ніби все гаразд.";
  let newText2 = "У дзеркалі, бачите?";
  speechBalloon(
    5,
    345,
    "bottom",
    changeText("Треба порахувати монети. ", newText1)
  );
  speechBalloon(
    235,
    160,
    "top",
    changeText("Цікаво, яку книгу читає людина, що сидить біля вікна? ", newText2),
    "thought"
  );
}

function speechBalloon(x, y, direction, t = "Книга, яку читаєш, цікава?", type = "speech") {
  ...
}

function changeText(t, newText) {
  t += newText;
  return t;
}
Функція, яка повертає значення

Вправа 37

Виконати код, який друкує на екрані поточні дату й час. Дослідити, як змінються результати на екрані при зміні значення другого аргумента функції nf().

sketch.js
function setup() {
  createCanvas(200, 100);
}

function draw() {
  let currentYear = year(); (1)
  let currentMonth = month();
  let currentDay = day();
  let currentHour = hour();
  let currentMinute = minute();
  let currentSecond = second();

  let d = currentDate(currentYear, currentMonth, currentDay); (2)
  let t = currentTime(currentHour, currentMinute, currentSecond);

  display(d, t); (3)
}

// оголошення функції для обчислення дати
function currentDate(y, m, d) {
  let currentDate = y + "-" + nf(m, 2) + "-" + nf(d, 2);
  return currentDate;
}

// оголошення функції для обчислення часу
function currentTime(h, m, s) {
  let currentTime = nf(h, 2) + ":" + nf(m, 2) + ":" + nf(s, 2);
  return currentTime;
}

// оголошення функції для виведення дати й часу на екран
function display(date, time) {
  background(245, 224, 183); // Wheat

  fill(133, 126, 123); // Rocket Metallic
  noStroke();
  textSize(24);

  text(date, 40, 40);
  text(time, 40, 70);
}
1 Цей код використовує функції для роботи з датою (year() , month() , day() ) і часом (hour() , minute() , second() ), щоб надрукувати на екрані поточні дату та час. Ці функції повертають значення, які присвоюються змінним currentYear, currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond.
2 Значення цих змінних передаються як аргументи у функції currentDate() і currentTime(), які зі свого боку повертають результат роботи на основі переданих значень за допомогою return і присвоюють його змінним d і t. Всередині функцій використовується конкатенацію рядків для створення текстових повідомлень і функція nf() - форматування при виведенні числових даних.
3 Значення d і t передаються як аргументи іншій функції - display(), яка виводить результат дати й часу на полотно.

4.3.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Які різновиди функцій можна виділити, взявши за основу принцип їхньої роботи?

  2. Які параметри функції називають: а) формальними; б) фактичними?

  3. Яка мета використання стандартних значень для параметрів в оголошенні функції?

  4. Що таке «виклик функції»?

  5. Як розуміти вислів функція повертає результат?

4.3.5. Практичні завдання

Початковий

  1. Використовуючи код, створити функцію, яка має єдиний формальний параметр - значення координати для малювання точок двох графіків. Викликати функцію з різними аргументами.

sketch.js
let x;

function setup() {
  createCanvas(250, 200);
}

function draw() {
  background(46, 196, 182); // Tiffany Blue
  stroke(255, 191, 105); // Mellow Apricot
  line(0, 50, width, 50);
  line(0, 150, width, 150);
  line(0, 250, width, 250);
  stroke(255);

  x = 0;
  while (x < width) {
    point(x, 50 + random(-15, 15));
    point(x, 150 + 20 * sin(x / 20));
    x = x + 1;
  }
}

Середній

  1. Створити функцію drawHouse(), яка малює будинок, використовуючи поданий нижче код. Формальними параметрами функції мають бути координати розташування будинку. Викликати функцію кілька разів із різними значеннями аргументів для малювання однакових будинків у різних місцях полотна.

stroke(0);

// каркас
fill(95, 89, 128); // Purple Navy
rect(x, y, 100, 100);

// дах
fill(47, 48, 97); // Space Cadet
triangle(x, y, x + 100, y, x + 50, y - 50);

// вікна
fill(255);
rect(x + 15, y + 25, 25, 25);
rect(x + 60, y + 25, 25, 25);

// двері
fill(201, 203, 163); // Sage
rect(x + 50, y + 63, 25, 36);
Однакові будинки, створені за допомогою функції drawHouse()
Однакові будинки, створені за допомогою функції drawHouse()
  1. Використовуючи код, написати функцію, яка приймає чотири аргументи (x, y, s - масштаб, c - колір), для побудови зображення сови. Викликати функцію кілька разів для побудови групи сов різного розміру та кольору.

Використати вбудовану функцію scale() , яка змінює масштаб фігур. Новий аргумент виклику функції множиться на значення попереднього.
stroke(c);
scale(s);
strokeWeight(40);
line(x, y, x, y + 20);
noStroke();
fill(255);
arc(x, y, 40, 40, 0, PI);
ellipse(x - 10, y, 20, 20);
ellipse(x + 10, y, 20, 20);
fill(c);
ellipse(x + 10, y, 5, 5);
ellipse(x - 10, y, 5, 5);
quad(x, y + 4, x + 3, y + 7, x, y + 10, x - 3, y + 7);
Популяція сов
Популяція сов

Високий

  1. Створити дизайн космічного корабля і намалювати кілька зорельотів з невеликими варіаціями на основі аргументів, які передаються у функцію: x-координата, y-координата, розмір і колір. Один із можливих варіантів дизайну зорельотів представлений на малюнку.

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

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

Для створення окремої іграшки на дереві використайте готову функцію, що визначає координати x та y цієї іграшки всередині трикутної крони дерева.
function toys(x1, y1, x2, y2, x3, y3) {
  const r1 = random();
  const r2 = random();

  const x = (1 - sqrt(r1)) * x1 + sqrt(r1) * (1 - r2) * x2 + sqrt(r1) * r2 * x3;
  const y = (1 - sqrt(r1)) * y1 + sqrt(r1) * (1 - r2) * y2 + sqrt(r1) * r2 * y3;

  noStroke();
  fill(random(255), random(255), random(255));
  circle(x, y, random(5, 10));
}

Екстремальний

  1. Створити застосунок, який генерує візерунок із квітів. Орієнтовний результат представлений в демонстрації.

  1. Написати функцію, яка будує безліч будинків як на малюнку.

Мегаполіс
Мегаполіс

4.4. Рекурсія. Рекурсивні побудови

Багато речей в реальному світі можна описати ідеалізованими геометричними формами: смартфон має прямокутну форму, баскетбольний м’яч - круглу і т. д.

Однак, багато об’єктів у природі описати такими простими засобами неможливо. Наприклад, сніжинки, дерева, узбережжя та гори тощо. Ці структури називають самоподібними.

Самоподібні структури - об’єкти, малі частини яких в довільному збільшенні/зменшенні є подібними до самих об’єктів.

Як можна відтворити самоподібні структури? Один із процесів створення самоподібних структур відомий як рекурсія.

Цікавимось

Рекурсія в мистецтві

У літературі, кінематографі, живописі та загалом у мистецтві можна зустріти спеціальну рекурсивну техніку, яка називається Міз-ан-абім (фр. Mise en abyme).

Цей технічний спосіб відомий в просторіччі як «сон уві сні», «розповідь у розповіді», «вистава у виставі», «фільм у фільмі» або «картина в картині».

Окремим випадком mise en abyme у мистецтві є ефект Дросте - ефект появи зображення всередині самого себе, у місці, де в реальності очікується, що з’явиться подібне зображення.

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

Жінка на пакунку какао-порошку фірми Дросте 1904 року тримає 2 предмети, на яких зображені менші версії її, які тримають ті самі два предмети й так далі рекурсивно.

Жінка на пакунку какао-порошку фірми Дросте 1904 року
Ефект Дросте: жінка на пакунку какао-порошку фірми Дросте 1904 року (зображення з покликанням)

На картині «Портрет подружжя Арнольфіні» Яна ван Ейка на стіні за постатями портретованих висить невелике опукле дзеркало, в якому видно, як художник малює портретованих і те саме дзеркало, в якому відображений він сам.

Ян ван Ейк, «Портрет подружжя Арнольфіні» (1435)
Ефект Дросте: Ян ван Ейк, «Портрет подружжя Арнольфіні» (1434) (зображення з покликанням)

Також ефект Дросте зустрічається у творах цифрового мистецтва сучасних художників, наприклад в роботі з назвою Світ Vaskange.

У програмуванні рекурсія - виклик функції безпосередньо з неї самої з іншими значеннями вхідних аргументів.

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

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

Якщо про це не подбати (або ж зробити це неправильно), операційна система (або інтерпретатор) аварійно завершить роботу застосунку як тільки доступні ресурси будуть вичерпані. Тому рекурсивні функції повинні мати умову виходу, як і у разі використання циклів for і while.

Типовим прикладом задачі, яку можна розв’язати рекурсивно, є обчислення факторіала.

Факторіал будь-якого числа n, який зазвичай записується як n!, визначається як:

\$n! = n * (n - 1) * (n - 2) * (n - 3) * ... * 1\$

Інакше кажучи, факторіал є добутком усіх цілих чисел від 1 до n.

Розгляньмо 4! та 3!.

\$4! = 4 * 3 * 2 * 1\$
\$3! = 3 * 2 * 1\$

Отже

\$4! = 4 * 3!\$

Загалом для будь-якого додатного цілого числа n формула для обчислення факторіала буде:

\$n! = n * (n - 1)!\$
\$1! = 1\$
Факторіал n визначається як n разів помножити на факторіал (n - 1). Така концепція, коли функція у своєму тілі викликає саму себе, і є рекурсією, а сама функція називається рекурсивною.

Вправа 38

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

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
  // викликати функції для обчислення 3!, 4!
}

function factorial(n) {
  if (n == 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

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

function factorial(n) {
  let f = 1;
  for (let i = 0; i < n; i++) {
    f = f * (i + 1);
  }
  return f;
}

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

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

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  drawCircle(100, 100, 150);
}

function drawCircle(x, y, r) {
  fill(254, r, 183); // Peach Puff
  ellipse(x, y, r, r);
  if (r > 5) {
    r *= 0.78;
    drawCircle(x, y, r);
  }
}

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

Щоразові виклики рекурсивної функції надають r таких значень:

150
117
91.26
71.1828
55.522584
43.307615520000006
33.779940105600005
26.348353282368006
20.551715560247047
16.030338136992697
12.503663746854304
9.752857722546358
7.607229023586159
5.933638638397205
4.62823813794982 // вихід з рекурсії
150
117
...

Результат - низка кіл, де кожне з кіл намальоване всередині попереднього кола.

Малювання кіл відбувається доти, доки виконується умова r > 5. При черговому значенні r = 4.62823813794982 умова r > 5 не виконується і функція drawCircle() перестає викликати саму себе. Після цього у функції draw() знову робиться перший виклик функції drawCircle() і все повторюється.

Використання рекурсії дозволяє створювати красиві та складні зображення.

Реалізуймо складніший сценарій з функцією drawCircle(). Для кожного відображеного кола намалюємо коло вдвічі менше його ліворуч та праворуч від цього кола, а також вгорі й внизу.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  stroke(253, 252, 220); // Light Yellow
  drawCircle(width / 2, height / 2, 100);
}

function drawCircle(x, y, r) {
  fill(254, r, 183); // // Peach Puff
  ellipse(x, y, r, r);
  if (r > 7) {
    drawCircle(x + r / 2, y, r / 2);
    drawCircle(x - r / 2, y, r / 2);
    drawCircle(x, y + r / 2, r / 2);
    drawCircle(x, y - r / 2, r / 2);
  }
}
Рекурсія
Рекурсія

Вправа 39

Виконати код для малювання кіл, змінюючи значення радіуса у виклику drawCircle() і в умові виходу з рекурсії.

4.4.2. Контрольні запитання

Міркуємо Обговорюємо

  1. Навести приклади самоподібних структур.

  2. Що таке «рекурсія»?

  3. Навести приклади використання рекурсії у мистецтві.

4.4.3. Практичні завдання

Початковий

  1. Записати виклик рекурсивної функції drawCircle(), яка створює малюнок як на зразку.

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(255);
  stroke(0, 119, 182); // Star Command Blue
  noFill();
  // виклик функції
  ...
}

function drawCircle(x, y, r) {
  ellipse(x, y, r, r);
  if (r > 2) {
    drawCircle(x + r / 2, y, r / 2);
    drawCircle(x - r / 2, y, r / 2);
  }
}
Рекурсивні кола
Рекурсивні кола

Середній

  1. Заповнити прогалини в коді, який генерує рекурсивний шаблон як на малюнку.

Рекурсивний шаблон
Рекурсивний шаблон
function setup() {
  createCanvas(400, 200);
}

function draw() {
  background(255);
  stroke(229, 68, 109); // Paradise Pink
  // виклик функції
  ...
  noLoop();
}

function branch(x, y, h) {
  line(x, y, x - h, y - h);
  line(x, y, x + h, y - h);

  if (...) {
    // функція викликає себе двічі
    ...(x - h, y - h, h / 2);
    ...(x + h, y - h, h / 2);
  }
}

Високий

  1. Написати функцію squares(), використовуючи зразок коду. Функція має три параметри (координати x і y розташування лівого верхнього кута квадрата, n - встановлює розмір сторони квадрата) і малює «рекурсивні квадрати». Орієнтовний результат представлений на малюнку.

Рекурсивні квадрати
Рекурсивні квадрати
// зразок коду для рекурсивної функції
if (n > 1) {
    // визначення кольорів
    let x2 = x + n;
    let y2 = y + n;
    rect(x, y, n, n);
    n = int(n / 2);
    // тут функція викликає саму себе
}

Екстремальний

  1. Створити рекурсивну структуру, яка складається з кіл. Орієнтовний результат представлений на малюнку.

Рекурсивна структура
Рекурсивна структура

4.5. Фрактали як самоподібні структури

Одним із видів комп’ютерної графіки, поруч з векторною і растровою, є фрактальна графіка.

Фрактальна графіка - технологія створення зображень на основі фракталів і базується на фрактальній геометрії.

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

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

Переглядаємо Аналізуємо

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

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

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

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

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

А як описати дерево, контури острова, з чим порівняти будову морської хвилі, форму сніжинки або листка папороті, чи описати розгалужену структуру бронхів або кровоносної системи? Ці об’єкти та явища є об’єктами дослідження фрактальної геометрії.

Чому геометрію часто називають холодною і сухою? Одна з причин полягає в її нездатності описати форму хмари, гори, дерева або берега моря. Хмари - це не сфери, гори - не конуси, лінії берега - це не кола, і кора не є гладкою, і блискавка не поширюється по прямій. Природа демонструє нам не просто вищий ступінь, а зовсім інший рівень складності.
— Бенуа Мандельброт
Фрактальна геометрія природи

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

Термін фрактал запропонував французько-американський математик Бенуа Мандельброт у 1975 році, щоб описати самоподібні структури, знайдені в природі. Відомою працею по фракталах є його книга «Фрактальна геометрія природи» («The Fractal Geometry of Nature», 1982).

У роботі Мандельброта використані наукові результати інших вчених, які працювали в цій області й заклали математичну базу для появи теорії фракталів: Анрі Пуанкаре, Фелікс Кляйн, П’єр Фату, Ґастон Жюліа, Георг Кантор, Нільс Фабіан Гельґе фон Кох, Поль Леві, Фелікс Хаусдорф.

Фрактали бувають різних видів:

  • геометричні;

  • алгебричні;

  • стохастичні;

  • концептуальні (соціокультурні, непросторові).

4.5.1. Види фракталів

Геометричні

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

Прикладами геометричних фракталів є:

Розглянемо принцип побудови геометричних фракталів на прикладі кривої Коха.

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

Крива Коха
Крива Коха: кількість ітерацій - 1

У результаті утворюється ламана, що складається з чотирьох ланок з довжиною 1/3 довжини початкового відрізка.

Крива Коха
Крива Коха: кількість ітерацій - 2

На наступному кроці повторюємо операцію для кожного з чотирьох отриманих ланок

Крива Коха
Крива Коха: кількість ітерацій - 3

і так далі

Крива Коха
Крива Коха: кількість ітерацій - 4
Крива Коха
Крива Коха: кількість ітерацій - 5
Крива Коха
Крива Коха: кількість ітерацій - 6

У підсумку, повторення операції побудови (ітерацій) утворюють криву, яка є кривою Коха.

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

Сніжинка Коха
Сніжинка Коха: кількість ітерацій - 6
Сніжинка Коха є основою фрактальних антен, які використовуються в мобільних пристроях. Завдяки такій формі антени мають компактний розмір і широкий діапазон дії.

Розглянемо ще один принцип побудови фракталів відомий як трикутник Серпінського.

Візьмемо рівносторонній трикутник, відзначимо середини його сторін.

Трикутник Серпінського
Трикутник Серпінського: глибина рекурсії - 1

З’єднаємо серединні точки прямими лініями. Утворюється 4 трикутники. Центральний трикутник виймаємо.

Трикутник Серпінського
Трикутник Серпінського: глибина рекурсії - 2

Тепер повторимо цю операцію з кожним із новоутворених трикутників.

Трикутник Серпінського
Трикутник Серпінського: глибина рекурсії - 3
Трикутник Серпінського
Трикутник Серпінського: глибина рекурсії - 4
Трикутник Серпінського
Трикутник Серпінського: глибина рекурсії - 5
Трикутник Серпінського
Трикутник Серпінського: глибина рекурсії - 6

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

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

Проілюструємо принципи побудови й деяких інших геометричних фракталів.

Переглядаємо Аналізуємо

Алгебричні

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

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

Яскравим прикладом алгебричних фракталів є фрактал Мандельброта.

Фрактал Мандельброта
Фрактал Мандельброта (зображення з покликанням)

Можна виділити й інші приклади фракталів цього виду.

Фрактал Жюліа
Фрактал Жюліа (зображення з покликанням)
Фрактал Ньютона
Фрактал Ньютона (зображення з покликанням)

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

Стохастичні

Геометричні фрактали, на зразок, кривої Коха чи трикутника Серпінського, є детермінованими, тобто вони не мають випадковості й завжди дають однаковий результат кожного разу, коли їх відтворюють. Вони є надто точні, щоб бути природними.

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

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

Переглядаємо Аналізуємо

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

У темі Трансформації та моделювання руху розділу Мультимедіа розглядається створення стохастичного фракталу - дерева.
Концептуальні

Принцип багаторівневої самоподібності закладений в культурних творах. У художніх текстах (віршах для дітей, народних піснях, у музичних творах і казках) часто зустрічається «оповідання в оповіданні», на зразок, «Притча про філософа, якому сниться, що він метелик, якому сниться, що він філософ, якому сниться…​», «Дім, який збудував Джек» тощо.

Фрактальність спостерігається в організації людських поселень (країна - місто - квартал); у розподілі суспільства на групи (народ - соціокультурна група - сім’я - людина).

Сюди ж можна віднести фрактальність взаємовідносин, які починаються з самої людини. Змінюється людина, її сприйняття, внутрішній стан - змінюються взаємовідносини в сім’ї, колективі, в результаті перетворюється все суспільство. Фрактальність простежується і в ієрархічних системах управління.

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

4.5.2. Фрактали у природі

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

Розгалуження гілок дерева
Розгалуження гілок дерева

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

Рослини теж несуть в собі форми самоподібності.

Соняшник, ананас, цвітна капуста Романеско
Соняшник, ананас, цвітна капуста Романеско

Фрактальними властивостями володіють і тварини.

Корали, морські їжаки та зірки, павич
Корали, морські їжаки та зірки, павич

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

Наприклад, узбережжя на кілометровому відрізку виглядає таким же «порізаним», як і на стокілометровому. Тобто, криві, подібні до кривої Коха, у природі становлять швидше правило, ніж виняток.

Блискавка, берегові лінії, лід на склі
Блискавка, берегові лінії, лід на склі

Цікавимось Додатково

4.5.3. Шум Перліна

Коли необхідно створити певну непередбачуваність у поведінці об’єктів, використовують застосунки - генератори випадкових чисел.

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

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

По суті, функція random() з бібліотеки p5.js є генератором псевдовипадкових чисел.

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

Шум Перліна (Perlin Noise) був створений Кеном Перліном (Ken Perlin) у 1983 році та був названий на честь свого творця.

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

Зокрема, розробка Perlin Noise дозволила художникам комп’ютерної графіки краще представити складність природних явищ у візуальних ефектах для кіноіндустрії. Також шум Перліна використовується в розробці комп’ютерних ігор для генерації ігрових світів.

Бібліотека p5.js містить реалізацію цього алгоритму у вигляді функції noise() .

Аргументами у виклику функції noise() є значення координат. Залежно від кількості переданих значень координат у функцію noise(), можна обчислювати 1D-, 2D- і 3D-шум.

Функція noise() повертає фіксоване (протягом виконання застосунку) псевдовипадкове значення шуму Перліна завжди у діапазоні від 0.0 до 1.0. На це значення впливає різниця між послідовно переданими координатами у noise(), наприклад, під час використання noise() у циклі. Як правило, чим менша різниця між координатами, тим плавнішою буде кінцева шумова послідовність, яка генерується за лаштунками.

Отож, розглянемо код застосунку, в якому реалізуємо анімацію хаотичного руху кола.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(245);
  fill(131, 103, 199); // Amethyst
  let x = random(width);
  let y = random(height);
  circle(x, y, 20);
}

Переглядаємо Аналізуємо

Як бачимо, виклик функції random() визначає щоразу випадкові значення координат (x, y) центру побудови кола, внаслідок цього виникає ілюзія, що рухаються багато кіл.

Використаємо значення шуму Перліна, яке повертає функція noise(), для створення плавної анімації.

sketch.js
let xoff1 = 0; (1)
let xoff2 = 100000; (2)
let i = 0.01; (3)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(245);
  fill(131, 103, 199); // Amethyst
  let x = map(noise(xoff1), 0, 1, 0, width); (4)
  let y = map(noise(xoff2), 0, 1, 0, height); (5)
  circle(x, y, 20);
  xoff1 += i; (6)
  xoff2 += i; (7)
}
1 Ініціалізуємо змінну з ім’ям xoff1, значення якої буде передаватися як аргумент у функцію noise().
2 Ініціалізуємо змінну з ім’ям xoff2, значення якої буде передаватися як аргумент у функцію noise().
3 Ініціалізуємо змінну i, яка буде збільшувати значення xoff1 та xoff2 на 0.01 відповідно.
4 Обчислюємо x-координату для побудови кола, використовуючи функцію map(), яка перетворює значення шуму Перліна з діапазону від 0 до 1 у діапазон від 0 до width.
5 Обчислюємо y-координату для побудови кола, використовуючи функцію map(), яка перетворює значення шуму Перліна з діапазону від 0 до 1 у діапазон від 0 до height.
6 Збільшуємо значення xoff1 на величину i, щоб змінити псевдовипадкове значення шуму Перліна на наступній ітерації виконання застосунку.
7 Збільшуємо значення xoff2 на величину i, щоб змінити псевдовипадкове значення шуму Перліна на наступній ітерації виконання застосунку.

Значення 0 та 100000, якими ініціалізуються змінні xoff1 і xoff2, є значеннями координат в нескінченному одновимірному просторі, в якому визначається шум Перліна.

Якби ці значення не змінювалися у пунктах 6 і 7, то псевдовипадкові значення шуму Перліна залишалися б однаковими протягом виконання застосунку, а при повторному запуску застосунку набували інших значень в діапазоні від 0 до 1 і знову протягом виконання застосунку були однаковими.

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

Переглядаємо Аналізуємо

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

Візуалізуємо відмінності у результатах викликів функцій random() і noise().

Спочатку розглянемо код застосунку для функції random().

sketch.js
function setup() {
  createCanvas(200, 200);
  noFill();
  noLoop(); (1)
}

function draw() {
  background(245);
  stroke(131, 103, 199); // Amethyst
  beginShape();
  for (let x = 0; x < width; x++) {
    let y = random(height);
    vertex(x, y); (2)
  }
  endShape();
}
1 Використовуємо у блоці setup() функцію noLoop() (це має бути останній рядок усередині блоку setup()), яка зупиняє безперервне виконання коду у draw(). Одна ітерація у блоці draw() точно відбудеться.
2 Малюємо фігуру, використовуючи beginShape() і endShape(). У циклі for малюємо вершини фігури із координатами x та y за допомогою функції vertex(x, y). Значення y-координати обчислюється випадково за допомогою функції random().
Чисті псевдовипадкові числа з плином часу
Функція random(): чисті псевдовипадкові числа з плином часу

А тепер подивимось на код застосунку для функції noise().

sketch.js
let xoff = 0;
let i = 0.02; (1)

function setup() {
  createCanvas(200, 200);
  noFill();
  noLoop(); (2)
}

function draw() {
  background(245);
  stroke(131, 103, 199); // Amethyst
  beginShape();
  for (let x = 0; x < width; x++) {
    let y = noise(xoff) * height; (3)
    vertex(x, y);
    xoff += i;
  }
  endShape();
}
1 Значення змінної i впливає на плавність графіка, оскільки визначає різницю між переданими значеннями координат у функцію noise(). Чим більше значення i, тим кінцевий графік буде більше схожий на графік для random().
2 Знову зупиняємо безперервне виконання коду у draw(). Одна ітерація у блоці draw() точно відбудеться.
3 Тепер y-координату для малювання вершин фігури обчислюємо як добуток значення висоти height полотна і значення шуму Перліна, яке повертає функція noise() на кожній ітерації циклу for.

У результаті виконання застосунку отримаємо статичний плавний графік функції шуму Перліна.

Послідовність псевдовипадкових чисел
Функція noise(): послідовність псевдовипадкових чисел (одновимірний шум Перліна з плином часу)

Вправа 40

Закоментувати рядок noLoop();. Перемістити рядок let xoff = 0; і розмістити після beginShape();. Виконати застосунок. Чому графік залишається статичним?

Змінимо код застосунку візуалізації шуму Перліна так, щоб початкове значення координати xoff у кожній ітерації блоку draw() набувало нових значень.

sketch.js
let i = 0.02;
let start = 0; (1)

function setup() {
  createCanvas(200, 200);
  noFill();
}

function draw() {
  background(245);
  stroke(131, 103, 199); // Amethyst
  beginShape();
  let xoff = start; (2)
  for (let x = 0; x < width; x++) {
    let y = noise(xoff) * height;
    vertex(x, y);
    xoff += i; (3)
  }
  endShape();
  start += i; (4)
}
1 Ініціалізуємо змінну з ім’ям start, яке буде покликанням на поточне значення координати, що передаватиметься у цикл for.
2 У кожній ітерації в блоці draw() ініціалізуємо змінну з ім’ям xoff і значенням start, яке буде початковим значенням координати для функції noise().
3 Збільшуємо значення xoff на величину i, щоб у циклі функція noice() за допомогою нового значення xoff повернула нове значення шуму Перліна.
4 По завершенні виконання циклу for у черговій ітерації в блоці draw(), збільшуємо на величину i значення start початкової координати для noice().

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

Переглядаємо Аналізуємо

Якщо у коді застосунку замість noise() використати функцію sin(), отримаємо знайому криву - графік функції sin().

sketch.js
let i = 0.02;
let start = 0;

function setup() {
  createCanvas(200, 200);
  noFill();
}

function draw() {
  background(245);
  stroke(131, 103, 199); // Amethyst
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    let y = map(sin(xoff), -1, 1, 0, height);
    vertex(x, y);
    xoff += i;
  }
  endShape();
  start += i;
}

У коді знову було використано функцію map() для перетворення значення функції sin() з діапазону від -1 до 1 у діапазон від 0 до height.

Переглядаємо Аналізуємо

Наприкінці, розглянемо код застосунку, який використовує функції sin() і noise() в тандемі для формування y-координати.

sketch.js
let i = 0.02;
let start = 0;
let mix = 50; (1)

function setup() {
  createCanvas(200, 200);
  noFill();
}

function draw() {
  background(245);
  stroke(131, 103, 199); // Amethyst
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    let n = map(noise(xoff), 0, 1, 0, mix); (2)
    let s = map(sin(xoff), -1, 1, 0, height - mix); (3)
    let y = n + s; (4)
    vertex(x, y);
    xoff += i;
  }
  endShape();
  start += i;
}
1 Ініціалізуємо змінну з ім’ям mix за допомогою якої додамо трішки шуму до візуалізації графіка функції sin().
2 Використовуємо функцію map(), яка перетворює значення, що повертає функція noise(), з діапазону від 0 до 1 у діапазон від 0 до mix.
3 Використовуємо функцію map(), яка перетворює значення, що повертає функція sin(), з діапазону від -1 до 1 у діапазон від 0 до height - mix.
4 Обчислюємо значення y-координати як суму значень, отриманих у пунктах 2 і 3, для побудови вершин графіка.

У результаті отримуємо графік синусоїди з додаванням шуму Перліна.

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

Переглядаємо Аналізуємо

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

Для налаштування рівня деталізації функції шуму Перліна використовується функція noiseDetail() .

Функція noiseDetail() може отримувати як аргументи два значення:

  • кількість октав, які будуть використані шумом - за стандартним налаштуванням 4; чим більше октав, тим дрібніші деталі, але зменшується плавність;

  • коефіцієнт падіння для кожної октави - за стандартним налаштуванням параметри окремої октави зменшується рівно вдвічі, порівняно з попередньою октавою, починаючи з 50% для 1-ї октави.

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

sketch.js
let i = 0.02;
let start = 0;

function setup() {
  createCanvas(200, 200);
  noFill();
  noiseDetail(15);
}

function draw() {
  background(245);
  stroke(131, 103, 199); // Amethyst
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    let y = map(noise(xoff), 0, 1, 0, height);
    vertex(x, y);
    xoff += i;
  }
  endShape();
  start += i;
}

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

Переглядаємо Аналізуємо

4.5.4. Використання фракталів

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

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

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

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

Фрактальні криві, що нескінченно самоподібні, мають нескінченну довжину. Відповідно, програмно їх неможливо намалювати повністю. Тому фрактальні криві малюють в деякому наближенні, заздалегідь фіксуючи максимально допустиму глибину рекурсії.

Розглянемо кілька прикладів використання рекурсії для побудови різних фракталів.

4.5.5. Створення фракталів

Множина Кантора

Створимо застосунок, який побудує множину Кантора , що є прототипом фракталу.

Множина Кантора будується за допомогою видалення середніх третин сегментів прямої.

На першому кроці видаляється середня третина з одиничного інтервалу [0, 1], залишаючи [0, 1/3] і [2/3, 1]. На наступному кроці, видаляється середня третина кожного з отриманих інтервалів.

Цей процес повторюється до нескінченності. Множина Кантора складається із всіх точок інтервалу [0, 1], які залишаються після всіх повторних видалень.

Переглядаємо Аналізуємо

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

sketch.js
function cantor(x, y, len) {
  line(x, y, x + len, y);
}

Змінні x і y є координатами лінії, а len - довжина лінії.

Викличемо функцію cantor() за допомогою поданого нижче коду і подивимось на результат.

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(230);
  stroke("#3F37C9"); // Persian Blue
  cantor(10, 20, width - 20);
}

function cantor(x, y, len) {
  line(x, y, x + len, y);
}

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

Правило побудови множини Кантора
Правило побудови множини Кантора

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

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(230);
  stroke("#3F37C9"); // Persian Blue
  cantor(10, 20, width - 20);
}

function cantor(x, y, len) {
  line(x, y, x + len, y);
  y += 20;
  line(x, y, x + len / 3, y); // від початку лінії до 1/3
  line(x + (len * 2) / 3, y, x + len, y); // від 2/3 до кінця лінії
}

Щоб намалювати наступний (третій) рядок ліній в множині Кантора, необхідно написати вже чотири рядки виклику функцій line().

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(230);
  stroke("#3F37C9"); // Persian Blue
  cantor(10, 20, width - 20);
}

function cantor(x, y, len) {
  line(x, y, x + len, y);
  y += 20;
  line(x, y, x + len / 3, y); // від початку лінії до 1/3
  line(x + (len * 2) / 3, y, x + len, y); // від 2/3 до кінця лінії
  y += 20;
  line(x, y, x + len / 9, y); // від початку лінії до 1/9
  line(x + (len * 2) / 9, y, x + (len * 3) / 9, y); // від 2/9 до 3/9
  line(x + (len * 6) / 9, y, x + (len * 7) / 9, y); // від 6/9 до 7/9
  line(x + (len * 8) / 9, y, x + len, y); // від 8/9 до кінця лінії
}

Далі знадобиться вісім, а потім шістнадцять викликів line() і так далі. Щоб не писати стільки коду, застосуємо цикл for. Але розрахувати формулу для кожної ітерації є також справою не з легких.

Замість того, щоб безпосередньо викликати функцію line(), можна просто викликати саму функцію cantor(), оскільки функція cantor() малює лінію від точки (x, y) із заданою довжиною len.

Тобто, можна замінити рядки

line(x, y, x + len / 3, y); // від початку лінії до 1/3
line(x + (len * 2) / 3, y, x + len, y); // від 2/3 до кінця лінії

на виклики функції cantor() рекурсивно

cantor(x, y, len / 3);
cantor(x + (len * 2) / 3, y, len / 3);

У підсумку, функція cantor() відпрацює і для наступних рядків множини Кантора, оскільки викликає себе знову і знову.

Залишається останній і дуже важливий крок - функція cantor() повинна зупинитися, коли довжина лінії стане менше як 1 піксель. Тому у код додамо перевірку, при якій рекурсивна функція буде виконуватися, якщо довжина лінії більшою чи дорівнюватиме 1 пікселю.

Вправа 41

Виконати код для перегляду візуалізації множини Кантора.

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(230);
  stroke("#3F37C9"); // Persian Blue
  cantor(10, 20, width - 20);
}

function cantor(x, y, len) {
  if (len >= 1) {
    line(x, y, x + len, y);
    y += 20;
    cantor(x, y, len / 3);
    cantor(x + (len * 2) / 3, y, len / 3);
  }
}
Крива Коха

Вправа 42

Виконати код, який візуалізує криву Коха. Провести 7 ітерацій. За потреби звернутися до довідки по функціях p5.js.

sketch.js
let i = 0; // кількість ітерацій

function setup() {
  createCanvas(600, 350);
}

function draw() {
  background(253, 255, 252);

  // відобразити номер ітерації в лівому верхньому куті полотна
  noStroke();
  fill(4, 167, 119);
  text(i, 10, 20);

  // намалювати криву Коха за допомогою рекурсивних викликів
  createKoch(i, 0, 250, 600, 250);
}

function createKoch(i, x1, y1, x2, y2) {
  // кінець рекурсивним викликам функції
  if (i == 0) {
    // намалювати лінію від точки (x1, y1) до (x2, y2)
    strokeWeight(2);
    stroke(217, 3, 104);
    line(x1, y1, x2, y2);
    return;
  }

  let alpha = atan2(y2 - y1, x2 - x1); // кут повороту
  let r = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); // відстань між точками

  // обчислення координат 3 точок трикутника
  //       C
  //      / \
  // ----A   B----
  // ліва точка (ліва вершина трикутника)
  let xa = x1 + (r * cos(alpha)) / 3;
  let ya = y1 + (r * sin(alpha)) / 3;

  // центральна точка (верхня вершина трикутника)
  let xc = xa + (r * cos(alpha - PI / 3)) / 3;
  let yc = ya + (r * sin(alpha - PI / 3)) / 3;

  // права точка (права вершина трикутника)
  let xb = x1 + (2 * r * cos(alpha)) / 3;
  let yb = y1 + (2 * r * sin(alpha)) / 3;

  // рекурсивні виклики функції для 4 отриманих ліній
  createKoch(i - 1, x1, y1, xa, ya);
  createKoch(i - 1, xa, ya, xc, yc);
  createKoch(i - 1, xc, yc, xb, yb);
  createKoch(i - 1, xb, yb, x2, y2);
}

// збільшення кількості ітерацій натисканням миші
function mousePressed() {
  i += 1;
}

Переглядаємо Аналізуємо

4.5.7. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «фрактали»?

  2. Навести приклади фракталів.

  3. Як класифікують фрактали?

4.5.8. Практичні завдання

Початковий

  1. У Processing IDE в режимі JavaScript відкрити, переглянути та запустити код прикладів побудови відомих фракталів із меню Файл  Приклади…​ (Ctrl+Shift+O), далі Simulate  ext16_Recursive_Tree, Simulate  ext17_Mandelbrot і Simulate  ext18_Koch.

Середній

  1. Побудувати фігуру у фігурі. За основу взяти код, який будує трикутник у трикутнику.

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(255);
  stroke(195, 172, 206); // Lilac
  fill(195, 172, 206, 100);
  tria(width / 2, height / 2, 200);
}

function tria(a, b, c) {
  triangle(a - c / 2, b + c / 2, a + c / 2, b + c / 2, a, b - c / 2);
  if (c > 5) {
    tria(a, b, c / 2);
  }
}

Високий

  1. Використовуючи код із попереднього завдання про трикутники створити трикутник Серпінського.

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

Гірський пейзаж
Гірський пейзаж

Екстремальний

  1. Намалювати фрактал із квадратів, орієнтовний зразок якого представлений на малюнку.

Фрактальні квадрати
Фрактальні квадрати

5. Об’єкти та класи

У цьому розділі ви ознайомитеся з основами об’єктоорієнтованого програмування в JavaScript і використання його принципів у роботі з бібліотекою p5.js.

5.1. Поняття об’єкта, класу як об’єктного типу даних

У навколишньому світі нас оточують об’єкти різних класів.

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

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

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

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

Клас - це певний шаблон, на основі якого створюються його об’єкти.

Окрім властивостей, об’єкти класів можуть мати здатність щось робити. Наприклад, об’єкти класу птахів - літати, полювати тощо, об’єкти класу легкових автомобілів - їхати, повертати, гальмувати тощо.

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

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

5.1.1. Основи об’єктоорієнтованого програмування

Розглянемо основи об’єктоорієнтованого програмування загалом, не в контексті певної мови програмування.

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

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

Щоб створити конкретний об’єкт, необхідно визначити клас з описом властивостей і методів об’єктів.

Наприклад, визначимо клас з назвою Персона з такими характеристиками:

  • властивості (Ім’я, Вік, Стать та Інтереси);

  • метод РозповістиПроСебе, який друкує повідомлення на основі значень властивостей: Мені [Вік] років. Мої захоплення: [Інтереси].;

  • метод Привітатися, який друкує повідомлення: Привіт, я - [Ім’я]!.

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

Клас: Персона

Атрибути:
  Ім'я
  Вік
  Стать
  Інтереси

Методи:
  РозповістиПроСебе(Мені [Вік] років. Мої захоплення: [Інтереси].)
  Привітатися(Привіт, я - [Ім'я]!)
Створення простої моделі складнішої сутності, яка представляє її найбільш важливі аспекти таким способом, щоб з нею було зручно працювати, називається абстракцією. Абстракція - це один із принципів об’єктоорієнтованого програмування.

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

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

Перша людина:

Об'єкт: персона1

Властивості:
  Ім'я: Дарина
  Вік: 18
  Стать: Жіноча
  Інтереси: Танці, Плавання, Іноземні мови

Методи:
  РозповістиПроСебе(Мені 18 років. Мої захоплення: Танці, Плавання, Іноземні мови.)
  Привітатися(Привіт, я - Дарина!)

Друга людина:

Об'єкт: персона2

Властивості:
  Ім'я: Максим
  Вік: 14
  Стать: Чоловіча
  Інтереси: Футбол, Комп'ютерні ігри

Методи:
  РозповістиПроСебе(Мені 14 років. Мої захоплення: Футбол, Комп'ютерні ігри.)
  Привітатися(Привіт, я - Максим!)
Дані та функції об’єкта збережені (інкапсульовані) всередині об’єкта. Це спрощує структуру і доступ до них. Інкапсуляція - один з механізмів в сучасних об’єктоорієнтованих мовах програмування.

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

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

Коли функціональність різниться між класами, безпосередньо у класах можна визначати спеціалізовані методи.

Успадкування - це створення нового класу об’єктів шляхом додавання нових елементів (атрибутів і/або методів) до вже наявного. Успадкування - один з механізмів в сучасних об’єктоорієнтованих мовах програмування.

Отже, на основі батьківського класу Персона

Клас: Персона

Атрибути:
  Ім'я
  Вік
  Стать
  Інтереси

Методи:
  РозповістиПроСебе(Мені [Вік] років. Мої захоплення: [Інтереси].)
  Привітатися(Привіт, я - [Ім'я]!)

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

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

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

Успадковано від: Персона

Клас: Вчитель

Атрибути:
  Предмет

Методи:
  Привітатися(Привіт, моє ім'я - [Ім'я]. Я викладаю предмет [Предмет].)

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

Успадковано від: Персона

Клас: Учень

Методи:
  Привітатися(Мої вітання! Я - [Ім'я].)

Класи-нащадки Вчитель і Учень та батьківський клас Персона містять методи з однаковою назвою Привітатися, але для методів вказані різні параметри.

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

Тепер можна створити об’єкти із дочірніх класів. Наприклад, створимо об’єкт на основі дочірнього класу Вчитель.

Об'єкт: вчитель1

Властивості:
  Ім'я: Ірина
  Вік: 29
  Стать: Жіноча
  Інтереси: IT, Іноземні мови
  Предмет: Англійська мова

Методи:
  РозповістиПроСебе(Мені 29 років. Мої захоплення: ІТ, Іноземні мови.)
  Привітатися(Привіт, моє ім'я - Ірина. Я викладаю предмет Англійська мова.)

5.1.2. Об’єкти в JavaScript

JavaScript спроєктований на основі концепції простих об’єктів.

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

Об’єкт можна уявити у вигляді книжкової шафи.

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

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

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

Розглянемо, як створювати об’єкти в JavaScript в контексті роботи з бібліотекою p5.js.

Щоб створити порожній об’єкт, використовують спосіб із фігурними дужками {}:

sketch.js
let colors = {};

Отож, ми створили порожній об’єкт і отримали покликання на цей об’єкт, використовуючи змінну colors.

Якщо об’єкт оголосити як константу з використанням const, він може бути змінений. Річ у тім, що зарезервоване слово const захищає від змін саму змінну colors, а не її вміст. Оголошення через const викличе помилку (Uncaught TypeError: Assignment to constant variable) у тому разі, коли змінній colors буде спроба присвоїти значення.

При використанні літерального синтаксису (застосування фігурних дужок {}) можна відразу помістити в об’єкт властивості у вигляді пар ключ: значення, розділених комою.

Створимо об’єкт із такими властивостями:

sketch.js
let colors = { (1)
  black: 0, // Black (2)
  white: 255, // White (3)
  "light gray": "#CDCED0" // Light Gray (4)
}
1 Ініціалізація об’єкта colors.
2 Під ключем black в об’єкті зберігається значення 0.
3 Під ключем white в об’єкті зберігається значення 255.
4 Під ключем "light gray" в об’єкті зберігається значення "#CDCED0".
Імена (ключі) властивостей об’єкта є рядками JavaScript. Якщо ім’я властивості складається з декількох слів ("light gray"), воно обов’язково записується в лапках або застосовується верблюжа нотація (lightGray). Всі інші типи даних, які використовуються як ключі, будуть автоматично перетворені у рядки. Наприклад, якщо використовувати число 1 як ключ, то воно перетвориться в рядок "1".

Цікавимось

Впорядкування властивостей об’єкта

Чи упорядковані властивості об’єкта? Інакше кажучи, якщо в циклі проходити через усі властивості об’єкта, чи отримаємо їх в тому ж порядку, в якому їх додавали?

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

Значеннями властивостей об’єкта можуть бути різні типи даних. В об’єкті colors - це два числа і рядок.

Застосувавши аналогію із книжковою шафою, можна сказати, що об’єкт colors - це книжкова шафа з трьома полицями (об’єкт містить три властивості), підписаними black, white і "light gray". Тепер на ці полиці можна додати нові книги, видалити книги, прочитати назви книг на полицях тощо, тобто, змінити значення властивостей об’єкта colors.

Щоб переглянути, що містить об’єкт colors, можна використати консоль вебпереглядача:

sketch.js
let colors = {
  ...
}

console.log(colors);

Результатом буде об’єкт з ключами (назвами кольорів) і значеннями (числовими й рядковим значеннями кольору):

{black: 0, white: 255, light gray: "#CDCED0"}
Об’єкти - це пари ключ: значення. Кожен ключ зберігає значення, а кожна пара ключ: значення є властивістю об’єкта.

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

sketch.js
let colors = {
  ...
}

console.log(colors.black); // 0
console.log(colors.white); // 255

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

sketch.js
let colors = {
  ...
}

colors.tyrianPurple = "#5F0F40"; // Tyrian Purple
colors.rubyRed = "#9A031E"; // Ruby Red
colors.darkOrange = "#FB8B24"; // Dark Orange
console.log(colors);

Тепер об’єкт colors міститиме нові властивості:

{black: 0, white: 255, light gray: "#CDCED0", tyrianPurple: "#5F0F40", rubyRed: "#9A031E", darkOrange: "#FB8B24"}

Для видалення властивості з об’єкта використовують оператор delete:

sketch.js
let colors = {
  ...
}

delete colors.black;

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

sketch.js
let colors = {
  ...
}

colors.black = 0; // крапкова нотація спрацьовує, оскільки ім'я властивості складається з одного слова
colors.Midnight Green Eagle Green = "#0F4C5C"; // синтаксична помилка

Для таких випадків існує альтернативний спосіб доступу до властивостей через квадратні дужки []. Такий спосіб спрацює з будь-яким ім’ям властивості:

sketch.js
let colors = {
  ...
}

colors["Midnight Green Eagle Green"] = "#0F4C5C";
console.log(colors);

Тепер об’єкт має такі властивості:

{black: 0, white: 255, light gray: "#CDCED0", tyrianPurple: "#5F0F40", rubyRed: "#9A031E", darkOrange: "#FB8B24", Midnight Green Eagle Green: "#0F4C5C"}
У разі, коли ключами в об’єкті є цілі числа або ключі є рядками, які містять прогалини, крапкова нотація не спрацює і для доступу до значень використовують квадратні дужки. Наприклад, colors[4], colors["4"], colors["Midnight Green Eagle Green"].
Об’єкти іноді називають асоціативними масивами, оскільки кожна властивість об’єкта має ключ у вигляді рядка, який можна використовувати для доступу до властивості.

У разі, коли необхідно перевірити існування певної властивості в JavaScript-об’єкті, використовують зарезервоване слово in:

"ключ" in об'єкт

Перевіримо, чи має об’єкт colors певні властивості:

sketch.js
let colors = {
  ...
}

console.log("light gray" in colors); // true
console.log("Dark Cornflower Blue" in colors); // false
console.log("white" in colors); // true

let currentColor = "darkOrange";
console.log(currentColor in colors); // true

Для перегляду усіх властивостей об’єкта використовується цикл for...in, який відрізняється від звичайного циклу for:

for (let key in object) {
  // тіло циклу виконується для кожної властивості об'єкта
}

Наприклад, надрукуємо усі властивості об’єкта colors:

sketch.js
let colors = {
  ...
}

for (let color in colors) {
  // ключі та значення ключів
  console.log(color, colors[color]);
}

Вправа 43

Додати нові властивості для об’єкта colors. Надрукувати в консолі вебпереглядача вміст об’єкта colors.

На цю мить об’єкт colors містить лише дані. Отож, всередині об’єкта colors визначимо метод з назвою paintItTyrianPurple, який буде встановлювати колір Tyrian Purple для тла полотна.

sketch.js
let colors = {
  ...
  tyrianPurple: "#5F0F40", // Tyrian Purple
  ...
  paintItTyrianPurple: function() {
    background(this.tyrianPurple);
  }
};

Назва для методу обирається відповідно до того, що він має робити. Для оголошення методів об’єкта існує короткий синтаксис: не використовувати двокрапку і зарезервоване слово function.

sketch.js
let colors = {
  ...
  paintItTyrianPurple() {
    ...
  }
};

А тепер використаємо наш об’єкт colors у застосунку, який зафарбовує тло полотна кольором Tyrian Purple через певний проміжок часу.

sketch.js
let colors = { (1)
  black: 0, // Black
  white: 255, // White
  "light gray": "#CDCED0", // Light Gray
  tyrianPurple: "#5F0F40", // Tyrian Purple
  rubyRed: "#9A031E", // Ruby Red
  darkOrange: "#FB8B24", // Dark Orange
  "Midnight Green Eagle Green": "#0F4C5C", // Midnight Green Eagle Green
  paintItTyrianPurple() { (2)
    background(this.tyrianPurple);
  }
};

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(colors.darkOrange); (3)

  // викликаємо метод paintItTyrianPurple() після 200 кадру
  if (frameCount > 200) { (4)
    colors.paintItTyrianPurple();
  }
}
1 Оголошення глобальної змінної colors, яка є покликанням на створений об’єкт із вказаними властивостями.
2 Однією із властивостей об’єкта є метод paintItTyrianPurple(), який при виклику зафарбує тло полотна значенням властивості tyrianPurple цього ж об’єкта colors. Щоб мати можливість використовувати ключі, які визначені для цього ж об’єкта, необхідно мати можливість посилатися на сам об’єкт. У JavaScript для таких цілей використовується зарезервоване слово this.
3 Встановлення кольору Dark Orange для тла при запуску застосунку, використовуючи властивість colors.darkOrange зі створеного об’єкта.
4 Якщо значення системної змінної frameCount стає понад 200, тоді з об’єкта colors викликається метод paintItTyrianPurple() за допомогою крапкової нотації. Це відбудеться через три секунди (трішки більше) від запуску застосунку, оскільки за стандартним налаштуванням кадри оновлюються з частотою 60 кадрів в секунду.
Зарезервоване слово this завжди буде гарантувати застосування правильного значення, коли контекст даних змінюється.

Як ми переконалися, об’єкти є корисними як структури для зберігання даних.

Вправа 44

Визначити ще один метод в об’єкті colors і викликати його.

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

sketch.js
let newCircle; (1)

function setup() {
  createCanvas(200, 200);
  newCircle = { (2)
    x: width / 2,
    y: height / 2,
    sizeCircle: 75,
    paint() { (3)
      ellipse(this.x, this.y, this.sizeCircle, this.sizeCircle);
    },
    enlarge() { (4)
      if (this.sizeCircle < 180) {
        this.sizeCircle += 1;
      }
    }
  };
}

function draw() {
  background(220);
  noStroke();
  fill(46, 196, 182); // Tiffany Blue
  newCircle.paint(); (5)
  newCircle.enlarge(); (6)
}
1 Оголошення глобальної змінної newCircle.
2 Створення об’єкта із властивостями: x та y, які визначають координати кола, sizeCircle, що визначає розмір кола. Змінна newCircle є покликанням на створений об’єкт. Створення об’єкта відбувається у функції setup(), оскільки для значень властивостей координат використовуються системні змінні width і height бібліотеки p5.js, які не визначені поза межами функції setup().
3 Оголошення методу paint(), у якому використовується вбудована функція ellipse() бібліотеки p5.js, параметрами якої є властивості цього ж об’єкта, доступ до яких відбувається завдяки використанню зарезервованого слова this. Нагадаємо, що зарезервоване слово this вказує на сам об’єкт і дозволяє отримувати значення властивостей об’єкта, які визначені всередині об’єкта.
4 Оголошення методу enlarge(), який збільшує розмір кола на одну одиницю при кожному його виклику, доки розмір не стане понад 180. Доступ до властивості sizeCircle також відбувається за допомогою зарезервованого слова this.
5 Виклик методу paint() для малювання кола на полотні.
6 Виклик методу enlarge() для збільшення розміру кола на одиницю, доки розмір не перевищує 180.

Переглядаємо Аналізуємо

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

Вправа 45

Додати до об’єкта ще одну властивість, яка визначає колір, і метод, який зафарбовує коло значенням цієї властивості.

5.1.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Назвати приклади класів і об’єктів, що належать цим класам.

  2. Що називають «атрибутами» і «методами» об’єкту? Навести приклади.

  3. Що таке «об’єктоорієнтоване програмування»?

  4. Пояснити такі поняття: а) «абстракція»; б) «інкапсуляція»; в) «успадкування»; г) «поліморфізм».

  5. Як створюється об’єкт в JavaScript?

  6. Як отримати доступ до конкретної властивості об’єкта? Як переглянути відразу усі властивості об’єкта?

  7. Для чого використовується зарезервоване слово this?

5.1.5. Практичні завдання

Початковий

  1. Розглянути автомобіль як об’єкт. Які дані може мати автомобіль? Які методи? Продовжити ланцюжки властивостей.

// дані
color
...

// методи
toggleHeadlights()
...

Середній

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

Високий

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

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

Екстремальний

  1. Створити об’єкт персонажа гри Pac-Man . Додатково «навчити» об’єкт рухатися, використовуючи клавіші клавіатури, як представлено у демонстрації.

5.2. Об’єкти, властивості, конструктори, методи

5.2.1. Функція-конструктор

Звичайний синтаксис {} дозволяє створити лише один об’єкт. Але частіше виникає потреба створити багато однотипних об’єктів, наприклад, автомобілів, будинків, геометричних фігур (прямокутників, кіл тощо) і т. д.

Для створення багатьох об’єктів можна використати синтаксис звичайної функції JavaScript.

Скористаємось цим і опишемо функцію createRectangle() для створення об’єктів - прямокутників. Функція createRectangle() буде приймати значення координат x та y об’єкта прямокутника, значення його розміру d і кольору заливки c як аргументи.

sketch.js
function setup() {
  ...
}

function draw() {
  ...
}

function createRectangle(x, y, d, c) {
  const obj = {}; (1)
  obj.x = x; (2)
  obj.y = y;
  obj.d = d;
  obj.c = c;
  obj.paint = function() { (3)
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  };
  return obj; (4)
}
1 В тілі функції запишемо код для створення порожнього об’єкта obj за допомогою літерального синтаксису, що використовує фігурні дужки {}.
2 Визначимо для об’єкта obj властивості x, y, d і c, значеннями яких будуть значення параметрів функції createRectangle().
3 Визначимо для об’єкта obj метод paint(), який у функціях fill() і rect() бібліотеки p5.js використовує значення властивостей об’єкта.
4 Повернемо із функції створений об’єкт obj.

Тепер створимо кілька прямокутників, викликавши функцію createRectangle(). Збережемо покликання на створені об’єкти у змінних r1, r2 і r3 та звернемось до методів створених об’єктів.

sketch.js
let r1, r2, r3;

function setup() {
  createCanvas(400, 400);
  r1 = createRectangle(140, 170, 60, "#8A1543"); // Claret
  r2 = createRectangle(40, 270, 60, "#D55C39"); // Flame
  r3 = createRectangle(240, 70, 60, "#D6BA3A"); // Old Gold
}

function draw() {
  background(220);
  r1.paint();
  r2.paint();
  r3.paint();
}

function createRectangle(x, y, d, c) {
  const obj = {};
  obj.x = x;
  obj.y = y;
  obj.d = d;
  obj.c = c;
  obj.paint = function() {
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  };
  return obj;
}

Результатом використання синтаксису {} і звичайної функції для створення об’єктів буде три об’єкти прямокутника.

JavaScript надає також інший спосіб створення об’єктів - за допомогою функції-конструктора.

Перепишемо звичайну функцію вище у вигляді функції-конструктора:

sketch.js
function setup() {
  ...
}

function draw() {
  ...
}

function Rectangle(x, y, d, c) {
  // const this = {}; неявно
  this.x = x;
  this.y = y;
  this.d = d;
  this.c = c;
  this.paint = function() {
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  };
  // return this; неявно
}
Ім’я функції-конструктора зазвичай починається з великої літери - ця умовність використовується для спрощення розпізнавання функції-конструктора в коді.

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

Тепер викличемо функцію-конструктор для створення трьох прямокутників за допомогою оператора new:

sketch.js
let r4, r5, r6;

function setup() {
  createCanvas(400, 400);
  r4 = new Rectangle(140, 170, 60, "#8A1543"); // Claret
  r5 = new Rectangle(40, 70, 60, "#67158A"); // Blue Violet Color Wheel
  r6 = new Rectangle(240, 270, 60, "#D67E3A"); // Bronze
}

function draw() {
  background(220);
  r4.paint();
  r5.paint();
  r6.paint();
}

function Rectangle(x, y, d, c) {
  // const this = {}; неявно
  this.x = x;
  this.y = y;
  this.d = d;
  this.c = c;
  this.paint = function() {
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  };
  // return this; неявно
}

Коли виконується виклик new Rectangle(...), за лаштунками відбуваються такі дії:

  1. Створюється новий порожній об’єкт, покликанням на який буде зарезервоване слово this.

  2. Виконується код функції-конструктора, який зазвичай модифікує this, наприклад, додає в нього нові властивості.

  3. Повертається значення this.

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

Тепер за допомогою функції-конструктора Rectangle() можна створювати будь-які інші прямокутники зі своїми значеннями параметрів. Доступ до властивостей чи методів створених у такий спосіб об’єктів відбувається через крапкову нотацію.

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

Функцію-конструктор можна розглядати як шаблон для створення нових об’єктів.

Вправа 46

Створити кілька об’єктів за допомогою функції-конструктора Rectangle().

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

Водночас використаємо для опису функції-конструктора синтаксис не як у звичайної функції, а як у функції без назви, як такої. Покликання на функцію-конструктор збережемо з ім’ям Circle. Спосіб опису функції за допомогою зарезервованого слова function і без її назви, як такої, використовується для опису методів у функції-конструкторі.

sketch.js
let circle1; (1)

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle(); (2)
}

function draw() {
  background(220);
  fill(183, 9, 76); // Amaranth Purple
  noStroke();

  circle1.paint();
  circle1.enlarge();
}

let Circle = function() { (3)
  this.x = width / 2;
  this.y = height / 2;
  this.sizeCircle = 75;
  this.paint = function() {
    ellipse(this.x, this.y, this.sizeCircle, this.sizeCircle);
  };

  this.enlarge = function() {
    if (this.sizeCircle < 180) {
      this.sizeCircle += 1;
    }
  };
};
1 Оголошуємо глобальну змінну circle1.
2 Використовуємо зарезервоване слово new для створення нового об’єкта кола під назвою circle1 з функції-конструктора Circle(). Об’єкт отримує свої властивості з функції-конструктора.
3 Оголошення функції-конструктора Circle(). Для запису у методах значень властивостей використовуються зарезервоване слово this, крапкова нотація та імена властивостей.

Переглядаємо Аналізуємо

Використовуючи функцію-конструктор можна продовжувати створювати нові кола. Новостворені кола - це окремі об’єкти, які можуть мати різні властивості. Значення цих властивостей можна змінювати після створення об’єктів.

Продемонструємо це, використовуючи такий код:

sketch.js
let circle1, circle2, circle3; (1)

function setup() {
  createCanvas(600, 200);
  circle1 = new Circle(); (2)
  circle2 = new Circle();
  circle3 = new Circle();
}

function draw() {
  background(220);
  fill(229, 195, 209); // Queen Pink
  noStroke();

  circle1.paint();
  circle1.enlarge();

  circle2.x = 100; (3)
  circle2.paint();
  circle2.enlarge();

  circle3.x = 500; (3)
  circle3.paint();
  circle3.enlarge();
}

let Circle = function() {
  this.x = width / 2;
  this.y = height / 2;
  this.sizeCircle = 75;
  this.paint = function() {
    ellipse(this.x, this.y, this.sizeCircle, this.sizeCircle);
  };

  this.enlarge = function() {
    if (this.sizeCircle < 180) {
      this.sizeCircle += 1;
    }
  };
};
1 Оголошення трьох глобальних змінних circle1, circle2 та circle3.
2 Ці змінні стають трьома об’єктами кола при виклику функції-конструктора Circle().
3 Тепер, коли є три окремі об’єкти кола, можна змінити їх властивості. У цьому разі відбувається зміна значення x-координати об’єктів.

Переглядаємо Аналізуємо

5.2.2. Класи в JavaScript

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

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

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

Абстракція - один із важливих принципів об’єктоорієнтованого програмування.

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

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

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

Класи в JavaScript були введені в стандарті ECMAScript 2015.

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

sketch.js
function setup() {
  ...
}

function draw() {
  ...
}

class Shape { (1)
  constructor() { (2)
    this.x = 100; (3)
    this.y = 100;
    this.d = 25;
    this.xSpeed = random(1);
    this.ySpeed = random(1);
  }
}
1 Створення класу з ім’ям Shape за допомогою зарезервованого слова class.
2 Оголошення метода constructor() для створення та ініціалізації об’єктів.
3 Використання зарезервованого слова this для встановлення властивостей та їх значень (даних) для створених об’єктів. Даними створених об’єктів будуть: значення координат x та y, розмір d і початкові випадкові значення xSpeed і ySpeed приросту кожної із координат об’єкта під час його руху на полотні.

Використаємо клас Shape для створення конкретного об’єкта - кола.

sketch.js
let circle1; (1)

function setup() {
  createCanvas(200, 200);
  circle1 = new Shape(); (2)
  print(circle1.x, circle1.y, circle1.d); (3)
}

function draw() {
  background(220);
  ellipse(circle1.x, circle1.y, circle1.d); (4)
}

class Shape { (5)
  constructor() {
    this.x = 100;
    this.y = 100;
    this.d = 25;
    this.xSpeed = random(1);
    this.ySpeed = random(1);
  }
}
1 Оголошення глобальної змінної circle1.
2 Створення конкретного об’єкта кола з ім’ям circle1 за допомогою зарезервованого слова new і класу Shape.
3 Доступ до даних створеного об’єкта circle1 і друк в консолі вебпереглядача, використовуючи функцію print() із бібліотеки p5.js, значень x-координати та y-координати й діаметра кола d.
4 Використання даних об’єкта (circle1.x, circle1.y і circle1.d) для малювання на полотні кола за допомогою функції ellipse().
5 Опис класу Shape.

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

100 100 25

Окрім даних, які надає клас Shape об’єктам, створених на його основі, у класі можна описувати методи об’єктів.

Отож, опишемо у класі Shape метод movement(), який визначатиме рух об’єктів, і метод paint() для малювання об’єкта та створимо кілька кіл на основі класу.

sketch.js
let circle1, circle2; (1)

function setup() {
  createCanvas(200, 200);
  circle1 = new Shape(); (3)
  circle2 = new Shape();
}

function draw() {
  background(220);
  circle1.paint(); (4)
  circle1.movement();
  circle2.paint();
  circle2.movement();
}

class Shape { (2)
  constructor() {
    this.x = 100;
    this.y = 100;
    this.d = 25;
    this.xSpeed = random(1);
    this.ySpeed = random(1);
  }
  paint() {
    noFill();
    strokeWeight(4);
    stroke("#527B57"); // Amazon;
    ellipse(this.x, this.y, this.d, this.d);
  }
  movement() {
    this.x = this.x + this.xSpeed;
    this.y = this.y + this.ySpeed;

    if (this.x - this.d / 2 < 0 || this.x > width - this.d / 2) {
      this.xSpeed = this.xSpeed * -1;
    }

    if (this.y - this.d / 2 < 0 || this.y > height - this.d / 2) {
      this.ySpeed = this.ySpeed * -1;
    }
  }
}
Синтаксис класів відрізняється від літералів об’єктів. Усередині класів ставити кому між методами класу не потрібно, оскільки це викликає синтаксичну помилку.
1 Оголошення глобальних змінних circle1 і circle2.
2 Визначення в описі класу Shape двох методів: paint() - для малювання кола на полотні та movement() - для зміни положення кола на полотні. У цих методах для доступу до значень властивостей об’єкта використовується зарезервоване слово this.
3 Створення об’єктів двох кіл. Імена змінних circle1 і circle2 вказують на ці два об’єкти.
4 Виклик методів для об’єктів кіл за допомогою крапкової нотації.

Переглядаємо Аналізуємо

Цього разу дані й функції кола збережені (інкапсульовані) всередині самого об’єкта кола.

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

За допомогою метода movement() здійснюється рух кіл на полотні.

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

Значення будь-яких даних об’єктів можна змінювати в момент створення самих об’єктів. Це реалізується наданням вхідних аргументів для методу constructor() класу.

sketch.js
let circle1, circle2;

function setup() {
  createCanvas(200, 200);
  circle1 = new Shape(120, 50, 25, "#872D78"); // Violet Crayola (1)
  circle2 = new Shape(60, 130, 55, "#E2A41E"); // Goldenrod
}

function draw() {
  background(220);
  circle1.paint();
  circle1.movement();
  circle2.paint();
  circle2.movement();
}

class Shape {
  constructor(x, y, d, c) { (2)
    this.x = x;
    this.y = y;
    this.d = d;
    this.c = c;
    this.xSpeed = random(1);
    this.ySpeed = random(1);
  }
  paint() {
    noFill();
    strokeWeight(4);
    stroke(this.c);
    ellipse(this.x, this.y, this.d);
  }
  movement() {
    this.x = this.x + this.xSpeed;
    this.y = this.y + this.ySpeed;

    if (this.x - this.d / 2 < 0 || this.x > width - this.d / 2) {
      this.xSpeed = this.xSpeed * -1;
    }

    if (this.y - this.d / 2 < 0 || this.y > height - this.d / 2) {
      this.ySpeed = this.ySpeed * -1;
    }
  }
}

Коли викликається new Shape(...):

1 Створюються нові об’єкти circle1 і circle2.
2 Запускається constructor із вказаними аргументами, які стають властивостями this.x, this.y, this.d і this.c об’єкта.

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

Переглядаємо Аналізуємо

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

У прикладах вище опис класу знаходився поза межами функцій setup() і draw(), а сам клас використовувався для створення об’єктів у функції setup().

Код класу можна розмістити також в окремому файлі з ім’ям як у самого класу Shape.js поруч з файлом index.html.

Shape.js
class Shape {
  constructor(x, y, d, c) {
    this.x = x;
    this.y = y;
    this.d = d;
    this.c = c;
    this.xSpeed = random(1);
    this.ySpeed = random(1);
  }
  paint() {
    noFill();
    strokeWeight(4);
    stroke(this.c);
    ellipse(this.x, this.y, this.d);
  }
  movement() {
    this.x = this.x + this.xSpeed;
    this.y = this.y + this.ySpeed;

    if (this.x - this.d / 2 < 0 || this.x > width - this.d / 2) {
      this.xSpeed = this.xSpeed * -1;
    }

    if (this.y - this.d / 2 < 0 || this.y > height - this.d / 2) {
      this.ySpeed = this.ySpeed * -1;
    }
  }
}

Тепер, щоб використовувати клас, необхідно приєднати файл Shape.js у файл index.html перед файлом sketch.js.

index.html
    ...
    <script src="./Shape.js"></script>
    <script src="./sketch.js"></script>
  </body>
</html>

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

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

Успадкування - це одна з ключових концепцій об’єктоорієнтованого програмування.

Створимо на основі суперкласу Shape підклас Rectangle.

Спочатку створимо файл Rectangle.js, який буде містити клас Rectangle, і приєднаємо файл до сторінки index.html:

index.html
    ...
    <script src="./Shape.js"></script>
    <script src="./Rectangle.js"></script>
    <script src="./sketch.js"></script>
  </body>
</html>

У JavaScript для зв’язку між класами на зразок Shape (суперклас) і Rectangle (дочірній клас) використовується зарезервоване слово extends.

Суперклас:

Shape.js
class Shape {
  constructor(x, y, d, c) {
    this.x = x;
    this.y = y;
    this.d = d;
    this.c = c;
    this.xSpeed = random(1);
    this.ySpeed = random(1);
  }
  paint() {
    noFill();
    strokeWeight(4);
    stroke(this.c);
    ellipse(this.x, this.y, this.d);
  }
  movement() {
    this.x = this.x + this.xSpeed;
    this.y = this.y + this.ySpeed;

    if (this.x - this.d / 2 < 0 || this.x > width - this.d / 2) {
      this.xSpeed = this.xSpeed * -1;
    }

    if (this.y - this.d / 2 < 0 || this.y > height - this.d / 2) {
      this.ySpeed = this.ySpeed * -1;
    }
  }
}

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

Rectangle.js
class Rectangle extends Shape { (1)
  constructor(x, y, d, c) {
    super(x, y, d, c); (2)
  }
  paint() {
    fill(this.c);
    strokeWeight(3);
    stroke(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  }
  movement() {
    super.movement();
  }
}
1 Запис class Rectangle extends Shape можна прочитати так: клас Rectangle розширює клас Shape.
2 Підклас Rectangle викликає конструктор свого суперкласу Shape за допомогою зарезервованого слова super.

У підкласі Rectangle ми визначили конструктор підкласу, який приймає аргументи x, y, r, c. У цьому разі необхідно обов’язково вказати виклик конструктора суперкласу за допомогою зарезервованого слова super(...), а усередині дужок розмістити аргументи, які потрібно передати конструктору суперкласу.

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

Зверніть увагу, методи paint() і movement() у дочірньому класі і суперкласі мають однакові назви. В методі movement() у дочірньому класі використовується зарезервоване слово super для виклику методу movement() із суперкласу. У цьому разі об’єкти, створені на основі класів Shape і Rectangle матимуть однакову поведінку руху.

А от метод paint() хоча й має однакові назви для суперкласу і підкласу, але відрізняється вмістом. Який із методів paint() буде викликатися при створенні об’єктів на основі цих класів?

Створимо об’єкти кола і прямокутника на основі класів.

sketch.js
let circle1, rectangle1;

function setup() {
  createCanvas(200, 200);
  circle1 = new Shape(120, 50, 25, "#872D78"); // Violet Crayola (1)
  rectangle1 = new Rectangle(60, 130, 55, "#E2A41E"); // Goldenrod (2)
}

function draw() {
  background(220);
  circle1.paint(); (3)
  circle1.movement();
  rectangle1.paint(); (4)
  rectangle1.movement();
}
1 Створення об’єкта circle1 на основі суперкласа Shape.
2 Створення об’єкта rectangle1 на основі підкласа Rectangle.
3 Виклик методів paint() і movement() для об’єкта circle. Це методи суперкласа.
4 Виклик методів paint() і movement() для об’єкта rectangle1. Метод movement() - із суперкласа, оскільки в самому методі підкласа записаний виклик метода movement() із суперкласа Shape за допомогою зарезервованого слова super. Метод paint() - це метод із підкласа Rectangle і він перевизначає (підміняє) метод paint() в класі Shape.

Переглядаємо Аналізуємо

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

З демонстрації роботи застосунку видно, що відбивання прямокутника від меж полотна відбувається не зовсім так, як очікується. Тому змінимо код у методі movement() підкласу Rectangle і використаємо механізм поліморфізму.

Rectangle.js
class Rectangle extends Shape {
  constructor(x, y, d, c) {
    super(x, y, d, c);
  }
  paint() {
    ...
  }
  movement() {
    this.x = this.x + this.xSpeed;
    this.y = this.y + this.ySpeed;

    if (this.x < 0 || this.x > width - this.d * 2) {
      this.xSpeed = this.xSpeed * -1;
    }

    if (this.y < 0 || this.y > height - this.d) {
      this.ySpeed = this.ySpeed * -1;
    }
  }
}

Переглядаємо Аналізуємо

Тепер обидва методи paint() і movement() із підкласу Rectangle перевизначають однойменні методи в класі Shape.

Вправа 47

Створити кілька об’єктів за допомогою суперкласу Shape і підкласу Rectangle і надрукувати в консолі вебпереглядача певні властивості створених об’єктів.

Цікавимось Додатково

Прототипи

JavaScript - об’єктоорієнтована прототипна мова програмування і має поняття прототипа об’єкта. Це означає, що кожен об’єкт має об’єкт-прототип, який виступає як шаблон і від якого об’єкт успадковує методи й властивості.

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

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

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

Ключове слово extends, яке застосовується для реалізації успадкування за допомогою класів JavaScript, працює, використовуючи прототипи, а поліморфізм є наслідком пошуку в ланцюжку прототипів - будь-який метод, який не оголошений у підкласі, буде шукатися в його суперкласі. У прототипі зібрані загальні властивості для усіх об’єктів.

Поглянемо на механізм прототипів, використовуючи консоль вебпереглядача (Ctrl+Shift+I). Для цього оголосимо спрощений варіант функції-конструктора Circle():

function Circle(x, y, r, c) {
  this.x = x;
  this.y = y;
  this.r = r;
  this.c = c;
  this.paint = function() {
    console.log(`Намалювали коло із заливкою кольором ${this.c}`);
  };
}

і створимо конкретний об’єкт circle1:

let circle1 = new Circle(100, 100, 30, "red");

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

Список доступних елементів для об’єкта circle1
Список доступних елементів для об’єкта circle1

В цьому списку є елементи, визначені у функції-конструкторі Circle():

  • x

  • y

  • r

  • c

  • paint

Однак, поруч можна побачити елементи

  • valueOf

  • hasOwnProperty

  • constructor

та інші, які визначені в прототипі функції-конструктора Circle(). Таким об’єктом-прототипом є Object.

Схематично це можна проілюструвати так:

Успадкування від прототипа
Успадкування від прототипа

Отже, що відбудеться, якщо до об’єкта circle1 застосувати метод, який фактично визначений в Object? Спробуємо це пояснити, наприклад, для методу valueOf():

circle1.valueOf();
Метод valueOf() повертає значення для вказаного об’єкта.

Об’єкт circle1 успадковує метод valueOf(), оскільки функцією-конструктором circle1 є Circle(), а прототипом Circle() є Object.

Отож, відбуваються такі дії:

  • Спочатку вебпереглядач перевіряє чи має об’єкт circle1 метод valueOf(), визначений в його функції-конструкторі Circle().

  • Це не так, тому наступним кроком вебпереглядач перевіряє, чи має прототип (Object) функції-конструктора Circle() метод valueOf(). Це справді так, тому метод valueOf() і викликається.

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

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

В JavaScript об’єкти мають спеціальну властивість [[Prototype]] (назва відповідає специфікації для мови JavaScript), яка або дорівнює null (спеціальне значення, яке являє собою «нічого», «порожньо» або «значення невідомо»), або вказує на інший об’єкт-прототип.

Результат виклику circle1.valueOf() демонструє нам цю властивість:

🢓Circle {x: 100, y: 100, r: 30, c: 'red', paint: ƒ}
  c: "red"
  🢒paint: ƒ ()
  r: 30
  x: 100
  y: 100
  🢒[[Prototype]]: Object

У мові JavaScript для роботи з прототипами використовують функції:

  • Object.getPrototypeOf(obj) - доступ до прототипа (властивості [[Prototype]]) вказаного об’єкта obj.

  • Object.setPrototypeOf(obj, newPrototype) - для об’єкта obj встановлюється новий прототип (властивість [[Prototype]]) - покликання на новий об’єкт newPrototype або null.

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

Наступний приклад демонструє прототипне успадкування. Визначимо порожній об’єкт за допомогою фігурних дужок {} і встановимо для нього значення прототипа:

let circle2 = {}; (1)
Object.setPrototypeOf(circle2, circle1); (2)
circle2.r // 30 (3)
1 Створення порожнього об’єкта circle2.
2 Встановлюємо об’єкт circle1 як прототип для об’єкта circle2.
3 Коли відбувається читання властивості circle2.r, виявляється, що ця властивість відсутня для об’єкта circle2, тому JavaScript переходить за покликанням [[Prototype]] і знаходить цю властивість в об’єкті circle1.

Якщо поглянути на значення об’єкта circle2 за допомогою circle2.valueOf()

🢓Circle {}
  🢓[[Prototype]]: Circle
    c: "red"
    🢒paint: ƒ ()
    r: 30
    x: 100
    y: 100
    🢒[[Prototype]]: Object

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

Отримаємо інформацію про прототип об’єкта circle2:

Object.getPrototypeOf(circle2);

В circle1 є метод paint() і він може бути викликаний для circle2 (метод автоматично береться із прототипа):

circle2.paint(); // Намалювали коло із заливкою кольором red

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

Наприклад, визначимо для об’єкта circle2 власний метод paint():

circle2.paint = function() {
  console.log("Намалювали ще одне коло");
};

Тепер виклик circle2.paint() знаходить метод безпосередньо в об’єкті circle2 і виконує його, не використовуючи прототип:

circle2.paint(); // Намалювали ще одне коло

Виконаємо чергове створення об’єкта

let circle3 = new Circle(120, 120, 40, "green");

при якому зарезервоване словао new створює новий об’єкт circle3, а потім викликає функцію-конструктор. У тілі функції-конструктора Circle() встановлюються властивості об’єкта за допомогою параметра this, який вказує на щойно створений об’єкт.

Крім виклику функції-конструктора, зарезервоване слово new виконує ще одну важливу дію: заповнює властивість [[Prototype]]. В цю властивість записується спеціальний об’єкт, прикріплений до функції-конструктора (функція є об’єктом, тому може мати властивості).

У кожної функції у JavaScript є властивість prototype, значенням якої є об’єкт. Цей об’єкт надає місце для додавання методів, наприклад:

Circle.prototype.enlarge = function() {
  console.log("Коло збільшується");
}
Значення властивості prototype - це об’єкт, який є контейнером для зберігання властивостей і методів, які ми хочемо успадкувати об’єктами, розташованими далі у ланцюжку прототипів. Успадковані властивості це ті, що визначені у властивості prototype, тобто ті, які починаються з Object.prototype..

Загалом виконуються такі кроки:

  1. Оператор new створює новий об’єкт circle3.

  2. У властивість [[Prototype]] новоствореного об’єкта circle3 записується об’єкт Circle.prototype.

  3. Оператор new викликає функцію конструктора Circle() з відповідними параметрами. Зарезервоване слово this вказує на щойно створений об’єкт.

  4. В тілі функції Circle() встановлюються властивості об’єкта за допомогою this.

  5. Змінна circle2 ініціалізується покликанням на об’єкт.

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

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

circle1.enlarge();
circle2.enlarge();
circle3.enlarge();

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

Наприклад:

// визначення конструктора
function Test(a, b, c, d) {
  // оголошення властивостей
}

// оголошення першого методу x
Test.prototype.x = function() { ... };

// оголошення другого методу y
Test.prototype.y = function() { ... };

//...і т. д.

5.2.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Для чого використовується функція-конструктор?

  2. Як створити клас в JavaScript?

  3. Пояснити, що означають поняття: а) «абстракція»; б) «інкапсуляція»; в) «успадкування»; г) «поліморфізм».

5.2.5. Практичні завдання

Початковий

  1. Описана функція-конструктор Rectangle(), яка створює три об’єкти прямокутника. Отримати доступ до значень властивостей створених об’єктів і надрукувати їх в консолі вебпереглядача.

let r1, r2, r3;

function setup() {
  createCanvas(400, 400);
  r1 = new Rectangle(140, 170, 60, "#8A1543"); // Claret
  r2 = new Rectangle(240, 70, 60, "#67158A"); // Blue Violet Color Wheel
  r3 = new Rectangle(40, 270, 60, "#808A15"); // Olive
}

function draw() {
  background(220);
  r1.paint();
  r2.paint();
  r3.paint();
}

function Rectangle(x, y, d, c) {
  this.x = x;
  this.y = y;
  this.d = d;
  this.c = c;
  this.paint = function () {
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  };
}
  1. Заповнити прогалини у файлі sketch.js, щоб утворився застосунок, в якому на основі класу створюється кілька різноколірних кіл різного розміру. Орієнтовний взірець представлено у демонстрації.

let circle1, ..., circle3;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle(...);
  circle2 = new Circle(...);
  ... = new Circle(...);
}

function draw() {
  background(220);
  noStroke();
  circle1.paint();
  circle2.paint();
  ...;
}

class Circle {
  constructor(c) {
    this.x = random(width);
    this.y = random(height);
    this.d = random(150);
    ... = c;
  }
  paint() {
    fill(...);
    ellipse(..., ..., ..., ...);
  }
}

Середній

  1. Використати клас Rectangle для створення застосунку, в якому відбуватиметься рух кількох об’єктів - прямокутників, як представлено у демонстрації.

class Rectangle {
  constructor(x, y, d, c) {
    this.x = x;
    this.y = y;
    this.d = d;
    this.c = c;
  }
  paint() {
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  }
  movement() {
    if (this.x > width + this.d) {
      this.x = -this.d;
    }
    this.x += 1;
  }
}
  1. Змінити швидкості руху прямокутників із попереднього завдання, як представлено у демонстрації.

Високий

  1. Описати структуру класу Людина, використовуючи інструментарій бібліотеки p5.js. Наприклад, властивість hairColor (колір волосся) матиме значення кольору у форматі RGB, властивість stature (зріст) буде цілим числом і т. д. Такий підхід застосувати також для методів класу. Ініціалізувати конкретний об’єкт класу і викликати методи для цього об’єкта.

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

Екстремальний

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

5.3. Події та обробники подій. Взаємодія об’єктів

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

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

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

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

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
}

class Circle {
  constructor(x, y, r, c) { (1)
    this.x = x;
    this.y = y;
    this.r = r;
    this.c = c;
    this.overlapping = false;
  }

  movement() { (2)
    this.x = this.x + random(-3, 3);
    this.y = this.y + random(-3, 3);
  }

  paint() { (3)
    ellipseMode(RADIUS);
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
}
1 Конструктор класу отримує кілька вхідних параметрів майбутніх об’єктів: значення координат x та y об’єкта кола на полотні, радіус кола r і колір його заливки c. Ці значення стають властивостями об’єкта при його створенні. Ще одна властивість, яку отримує об’єкт, - this.overlapping - визначена в самому конструкторі. Властивість this.overlapping містить логічне значення, що визначає, чи об’єкт торкається іншого об’єкта на полотні (true) чи ні (false). Початкове значення цієї властивості false.
2 Метод movement() визначає поведінку об’єктів - випадкові переміщення на полотні на невеликі відстані від точки їх створення.
3 Метод paint() використовується для малювання об’єктів кіл на полотні.
У методі paint() увімкнений режим ellipseMode(RADIUS) - перші два параметри функції ellipse() - це координати x і y центральної точки фігури, а третій і четвертий параметри - це половина ширини та висоти фігури. Причина використання цього режиму - інтерпретація значення властивості this.r об’єкта як значення радіуса кола.

Створимо два об’єкти кола на основі описаного нами класу Circle і продемонструємо визначену для них поведінку:

sketch.js
let circle1;
let circle2;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle(100, 100, 20, "rgba(255, 214, 64, 0.8)"); // Mustard
  circle2 = new Circle(100, 100, 40, "rgba(0, 214, 196, 0.8)"); // Turquoise
}

function draw() {
  background(220);

  circle1.paint();
  circle2.paint();

  circle1.movement();
  circle2.x = mouseX;
  circle2.y = mouseY;
}

class Circle {
  constructor(x, y, r, c) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.c = c;
    this.overlapping = false;
  }
  movement() {
    this.x = this.x + random(-3, 3);
    this.y = this.y + random(-3, 3);
  }
  paint() {
    ellipseMode(RADIUS);
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
}

Переглядаємо Аналізуємо

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

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

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

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

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

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

\$d &lt; r1 + r2\$

Об’єкти кіл почнуть взаємодіяти, як тільки відстань d між ними буде меншою за суму їх радіусів r1 + r2 (без врахування товщини межі кіл).

Додамо у клас Circle метод, який буде слідкувати за тим, чи настала подія наближення об’єктів на відстань d і використаємо цей метод у функції draw().

sketch.js
let circle1;
let circle2;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle(100, 100, 20, "rgba(255, 214, 64, 0.8)"); // Mustard
  circle2 = new Circle(100, 100, 40, "rgba(0, 214, 196, 0.8)"); // Turquoise
}

function draw() {
  background(220);

  circle1.intersects(circle2); (4)

  if (circle1.overlapping || circle2.overlapping) { (5)
    background(128, 113, 130); // Old Lavender
  }

  circle1.paint();
  circle2.paint();

  circle1.movement();
  circle2.x = mouseX;
  circle2.y = mouseY;
}

class Circle {
  constructor(x, y, r, c) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.c = c;
    this.overlapping = false;
  }
  movement() {
    this.x = this.x + random(-3, 3);
    this.y = this.y + random(-3, 3);
  }
  paint() {
    ellipseMode(RADIUS);
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
  intersects(other) { (1)
    this.overlapping = false;
    other.overlapping = false;
    let d = dist(this.x, this.y, other.x, other.y); (2)
    if (d < this.r + other.r) { (3)
      this.overlapping = true;
      other.overlapping = true;
    }
  }
}
1 Описуємо метод intersects(), який приймає один параметр - об’єкт other. В методі встановлюємо значення false для властивостей this.overlapping (поточний об’єкт, для якого викликається метод) і other.overlapping (об’єкт other) - спочатку об’єкти не перекриваються. Умовно метод можна назвати слухачем події наближення об’єктів на відстань d, меншу суми радіусів об’єктів, оскільки він постійно виконується у функції draw() і обчислює значення відстаней d між щоразу новими положеннями об’єктів кіл.
2 Оголошуємо змінну d, яка буде вказувати на обчислене значення відстані між об’єктами за допомогою функції dist() . Перша пара координат - this.x і this.y - координати центра поточного об’єкта кола, друга пара координат other.x і other.y - координати центра об’єкта кола other.
3 За допомогою вказівки розгалуження if перевіряємо виконання умови, яку ми обрали для взаємодії об’єктів d < this.r + other.r. Якщо вона виконується, то властивості this.overlapping і other.overlapping отримують значення true, тобто об’єкти перекриваються.
4 Викликаємо метод intersects() для поточного об’єкта circle1 і з аргументом у вигляді другого об’єкта circle2.
5 За допомогою вказівки розгалуження if перевіряємо значення властивостей circle1.overlapping або circle2.overlapping на істинність. У разі значення true для будь-якої властивості, змінюється колір тла полотна. Умовно цей блок коду можна назвати обробником події, оскільки він містить код, який змінює роботу застосунку з настанням події. У разі двох об’єктів перевіряти можна лише одну із властивостей.

Переглядаємо Аналізуємо

Вправа 48

Змінити поведінку для другого об’єкта кола, викликавши для нього метод movement().

Базові поняття події, слухача і обробника події детально розглядаються в Додатку B.

5.3.2. Контрольні запитання

Міркуємо Обговорюємо

  1. Завдяки чому у застосунку реалізується взаємодія між об’єктами?

  2. Оберіть два об’єкти з реального світу та опишіть їхню взаємодію.

5.3.3. Практичні завдання

Початковий

  1. У класі Circle описаний метод changeColor(), який змінює колір заливки об’єкта. Викликати цей метод для об’єкта при його взаємодії з іншим об’єктом, як представлено у демонстрації.

let circle1;
let circle2;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle(100, 100, 20);
  circle2 = new Circle(100, 100, 40);
}

function draw() {
  background(220);

  circle1.intersects(circle2);

  if (circle1.overlapping || circle2.overlapping) {
    // виклик методу у разі, коли об'єкти взаємодіють
  } else {
    // виклик методу у разі, коли об'єкти не взаємодіють
  }

  circle1.paint();
  circle2.paint();

  circle2.x = mouseX;
  circle2.y = mouseY;
}

class Circle {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.c = "rgba(255, 214, 64, 0.8)"; // Mustard
    this.overlapping = false;
  }
  paint() {
    ellipseMode(RADIUS);
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
  intersects(other) {
    this.overlapping = false;
    other.overlapping = false;
    let d = dist(this.x, this.y, other.x, other.y);
    if (d < this.r + other.r) {
      this.overlapping = true;
      other.overlapping = true;
    }
  }
  changeColor(currentColor) {
    this.c = color(currentColor);
  }
}

Середній

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

Високий

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

Екстремальний

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

5.4. Проєктування взаємодії програмних об’єктів

При взаємодії об’єктів в інтерактивних 2D-застосунках застосовують різні алгоритми для виявлення зіткнень між об’єктами.

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

Алгоритми виявлення зіткнень залежать від типу фігур, які можуть зіткнутися, наприклад, прямокутник з прямокутником, прямокутник з колом, коло з колом тощо.

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

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

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

При виявленні зіткнень необхідно правильно розуміти координати розташування фігур на полотні. Тому обов’язково враховуйте те, який режим інтерпретації координат зараз увімкнений. Окремі режими вмикаються функціями на зразок ellipseMode(), rectMode() та іншими.

5.4.1. Зіткнення з межею

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

sketch.js
let ball;

function setup() {
  createCanvas(200, 200);
  ball = new Ball(); (4)
}

function draw() {
  background(220);
  ball.paint(); (5)
  ball.movement();
}

class Ball {
  constructor() { (1)
    this.c = color("#07A9A3"); // Light Sea Green
    this.r = 30;
    this.x = width / 2;
    this.y = this.r;
    this.velocity = 0;
    this.gravity = 0.1;
  }
  paint() { (2)
    ellipseMode(RADIUS);
    fill(this.c);
    stroke(255);
    strokeWeight(2);
    ellipse(this.x, this.y, this.r, this.r);
  }
  movement() { (3)
    this.y = this.y + this.velocity;
    this.velocity = this.velocity + this.gravity;
    if (this.y + this.r > height) {
      this.velocity = this.velocity * -0.75;
      this.y = height - this.velocity - this.r;
    }
  }
}
1 У конструкторі класу Ball визначені властивості об’єкта м’яча: this.c - колір м’яча, this.r - радіус м’яча, this.x і this.y - початкові координати м’яча на полотні, this.velocity - початкова швидкість руху м’яча (величина, яка змінює значення y-координати, у такий спосіб імітуючи вертикальний рух) і this.gravity - значення «гравітації» (величина, яка змінює значення швидкості руху). Ключове слово this є покликанням на об’єкт м’яча.
2 Метод paint() використовується для створення об’єкта м’яча, використовуючи його властивості.
3 У методі movement() реалізовано виявлення зіткнення з нижньою межею полотна за допомогою вказівки розгалуження if.
4 Створення об’єкта м’яча ball.
5 Виклик методів об’єкта ball.

Розглянемо детальніше метод movement(), у якому реалізовано виявлення зіткнення для цієї задачі.

Як тільки м’яч починає рухатися донизу, значення його властивості ball.velocity починає зростати від нуля, а отже відповідно зростає значення координати ball.y.

В умові вказівки розгалуження if щоразу перевіряється істинність нерівності this.y + this.r > height - м’яч перетинає нижню межу полотна?

В момент дотику м’яча до нижньої межі полотна властивість ball.velocity отримує від’ємне значення ball.velocity * -0.75 і знову починає збільшуватися. При цьому рух продовжується тепер вертикально вгору, оскільки ball.y зменшується.

При майже нульовому значенню ball.velocity координата ball.y суттєво не змінюється - м’яч «зависає». Значення ball.velocity знову продовжує зростати від нуля, а отже відповідно зростає значення координати ball.y - м’яч рухається донизу знову.

Процеси підіймання і падіння м’яча продовжуються доти, доки такі зміни властивості ball.velocity в додатній і від’ємний боки стають незначними. В цей момент координата ball.y набуває практично однакових значень і знаходиться на відстані радіуса м’яча ball.r від нижньої межі полотна. Рядок this.y = height - this.velocity - this.r; дозволяє зробити зупинку м’яча більш плавною.

Переглядаємо Аналізуємо

Вправа 49

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

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

sketch.js
let ball;

function setup() {
  createCanvas(200, 200);
  ball = new Ball(); (4)
}

function draw() {
  background(220);
  ball.paint(); (5)
  ball.movement();
}

class Ball {
  constructor() { (1)
    this.c = color("#07A9A3"); // Light Sea Green
    this.r = 20;
    this.x = this.r;
    this.y = height / 2;
    this.xVelocity = 0.7;
    this.yVelocity = 1;
  }
  paint() { (2)
    ellipseMode(RADIUS);
    fill(this.c);
    stroke(255);
    strokeWeight(2);
    ellipse(this.x, this.y, this.r, this.r);
  }
  movement() { (3)
    this.x += this.xVelocity;
    this.y += this.yVelocity;
    if (this.x < this.r || this.x > width - this.r) {
      this.xVelocity *= -1;
    }

    if (this.y < this.r || this.y > height - this.r) {
      this.yVelocity *= -1;
    }
  }
}
1 У конструкторі класу Ball визначені властивості об’єкта м’яча: this.c - колір м’яча, this.r - радіус м’яча, this.x і this.y - початкові координати м’яча на полотні, this.xVelocity - горизонтальна складова швидкості руху м’яча (величина, яка змінює значення x-координати) і this.yVelocity - вертикальна складова швидкості руху м’яча (величина, яка змінює значення y-координати).
2 Метод paint() використовується для створення об’єкта м’яча, використовуючи його властивості.
3 У методі movement() реалізовано виявлення зіткнення з усіма межами полотна за допомогою вказівок розгалуження if.
4 Створення об’єкта м’яча ball.
5 Виклик методів об’єкта ball.

В методі movement() збільшуються значення координат м’яча, а вказівка розгалуження if перевіряє, чи зіткнувся м’яч з лівою, чи правою межею полотна, і якщо так, властивість this.xVelocity змінює своє значення на протилежне (якщо значення було додатнім - стає від’ємним і навпаки). Як наслідок змінюється значення координати this.x - м’яч змінює напрямок руху, інакше кажучи, відбивається від лівої чи правої меж полотна.

Аналогічно відбувається і для верхньої та нижньої меж. Також в умовах вказівок розгалуження if було враховано значення радіуса м’яча this.r, щоб відбивання відбувалось при дотику м’яча до межі, а не десь за межею.

Переглядаємо Аналізуємо

Розглянемо випадок руху об’єктів прямокутної форми та відбивання їх від меж полотна.

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

sketch.js
let rectangle1;
let rectangle2;
let rectangle3;

function setup() {
  createCanvas(200, 200);
  rectangle1 = new Rectangle();
  rectangle2 = new Rectangle();
  rectangle3 = new Rectangle();
}

function draw() {
  background(120);
  rectangle1.paint();
  rectangle2.paint();
  rectangle3.paint();
  rectangle1.movement();
  rectangle2.movement();
  rectangle3.movement();
}

class Rectangle {
  constructor() {
    this.d = random(20, 40);
    this.x = width / 2;
    this.y = height / 2;
    this.c = color(random(255), random(255), random(255), random(200, 220));
    this.xVelocity = random(0.5, 1);
    this.yVelocity = random(0.5, 1);
  }
  paint() {
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  }
  movement() {
    this.x += this.xVelocity;
    this.y += this.yVelocity;
    if (this.x < 0 || this.x > width - this.d * 2) {
      this.xVelocity *= -1;
    }
    if (this.y < 0 || this.y > height - this.d) {
      this.yVelocity *= -1;
    }
  }
}

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

Якщо значення координат this.x і this.y є більшими за ці відстані, то властивість this.xVelocity змінює своє значення на протилежне (якщо значення було додатнім - стає від’ємним і навпаки). Прямокутник змінює напрямок руху, інакше кажучи, відбивається від лівої чи нижньої меж полотна.

Така перевірка відбувається і для верхньої (this.y < 0) і лівої (this.x < 0) меж.

Переглядаємо Аналізуємо

Вправа 50

Змінити код застосунку так, щоб від меж полотна відбивалися об’єкти квадратної форми.

5.4.2. Зіткнення між колами

У попередньому розділі ми розглянули, як відбувається виявлення зіткнень між об’єктами кіл. Ще раз наведемо код застосунку, який візуалізує виявлення зіткнень між колами.

sketch.js
let circle1;
let circle2;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle();
  circle2 = new Circle();
}

function draw() {
  background(220);

  circle1.intersects(circle2);

  if (circle1.overlapping || circle2.overlapping) {
    circle2.changeColor("rgba(233, 218, 84, 0.8)"); // Minion Yellow
  }

  circle1.paint();
  circle2.paint();
  circle2.changeColor("rgba(193, 71, 119, 0.8)"); // Fuchsia Rose

  circle2.x = mouseX;
  circle2.y = mouseY;
}

class Circle {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    this.r = random(20, 35);
    this.c = color("rgba(7, 169, 163, 0.8)"); // Light Sea Green
    this.overlapping = false;
  }
  paint() {
    ellipseMode(RADIUS);
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
  intersects(other) {
    this.overlapping = false;
    other.overlapping = false;
    let d = dist(this.x, this.y, other.x, other.y);
    if (d < this.r + other.r) {
      this.overlapping = true;
      other.overlapping = true;
    }
  }
  changeColor(currentColor) {
    this.c = color(currentColor);
  }
}

Переглядаємо Аналізуємо

5.4.3. Зіткнення із вказівником миші

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

Вказівник миші як об’єкт має дві властивості - координати точки вказівника на полотні. Значення цих властивостей можна отримати із системних змінних бібліотеки p5.js - mouseX і mouseY відповідно.

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

Умовою виявлення зіткнення буде нерівність:

\$d &lt; this.r\$

де r - радіус кола, а зарезервоване слово this вказує на об’єкт кола.

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

sketch.js
let circle1;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle();
}

function draw() {
  circle1.intersects();

  if (circle1.overlapping) {
    circle1.changeColor("rgba(233, 218, 84, 0.8)"); // Minion Yellow
    background("rgba(7, 169, 163, 0.8)"); // Light Sea Green
  } else {
    circle1.changeColor("rgba(7, 169, 163, 0.8)"); // Light Sea Green
    background("rgba(233, 218, 84, 0.8)"); // Minion Yellow
  }
  circle1.paint();
}

class Circle {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    this.r = random(20, 35);
    this.overlapping = false;
  }
  paint() {
    ellipseMode(RADIUS);
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
  intersects() {
    this.overlapping = false;
    let d = dist(this.x, this.y, mouseX, mouseY);
    if (d < this.r) {
      this.overlapping = true;
    }
  }
  changeColor(currentColor) {
    this.c = color(currentColor);
  }
}

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

Результатом взаємодії вказівника миші й кола є зміна кольору тла полотна і кола.

Переглядаємо Аналізуємо

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

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

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

Умовою виявлення зіткнення будуть нерівності:

\$mouseX &gt; this.x\$
\$mouseX &lt; this.x + this.d * 2\$
\$mouseY &gt; this.y\$
\$mouseY &lt; this.y + this.d\$

де mouseX і mouseY - координати точки вказівника миші, this.x і this.y - координати верхнього лівого кута об’єкта прямокутника, this.x + this.d * 2 - ширина об’єкта прямокутника, this.y + this.d - висота об’єкта прямокутника.

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

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

sketch.js
let rectangle1;

function setup() {
  createCanvas(200, 200);
  rectangle1 = new Rectangle();
}

function draw() {
  rectangle1.intersects();

  if (rectangle1.overlapping) {
    background("rgb(7, 169, 163)"); // Light Sea Green
    rectangle1.changeColor("rgb(233, 218, 84)"); // Minion Yellow
  } else {
    background("rgb(233, 218, 84)"); // Minion Yellow
    rectangle1.changeColor("rgb(7, 169, 163)"); // Light Sea Green
  }
  rectangle1.paint();
}

class Rectangle {
  constructor() {
    this.x = random(width / 2);
    this.y = random(height / 2);
    this.d = random(40, 60);
    this.c = 0;
    this.overlapping = false;
  }
  paint() {
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  }
  intersects() {
    this.overlapping = false;
    if (
      mouseX > this.x &&
      mouseX < this.x + this.d * 2 &&
      mouseY > this.y &&
      mouseY < this.y + this.d
    ) {
      this.overlapping = true;
    }
  }
  changeColor(currentColor) {
    this.c = color(currentColor);
  }
}

Переглядаємо Аналізуємо

5.4.4. Зіткнення між прямокутниками

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

Умова виявлення зіткнення знову складатиметься із кількох нерівностей, в яких використовуватимуться властивості обох об’єктів:

\$this.x + this.d * 2 ≥ other.x\$
\$this.x ≤ other.x + other.d * 2\$
\$this.y + this.d ≥ other.y\$
\$this.y ≤ other.y + other.d\$

Ключові слова this і other вказують на різні об’єкти, значення властивостей яких використовуються у нерівностях: this.x і this.y - координати верхнього лівого кута першого прямокутника, other.x та other.y - координати верхнього лівого кута другого прямокутника, this.d * 2 та other.d * 2 - значення ширина прямокутників, а this.y + this.d та other.y + other.d - значення висоти прямокутників відповідно.

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

sketch.js
let rectangle1;
let rectangle2;

function setup() {
  createCanvas(200, 200);
  rectangle1 = new Rectangle();
  rectangle2 = new Rectangle();
}

function draw() {
  rectangle1.intersects(rectangle2);

  if (rectangle1.overlapping || rectangle2.overlapping) {
    background("rgb(7, 169, 163)"); // Light Sea Green
    rectangle1.changeColor("rgb(233, 218, 84)"); // Minion Yellow
  } else {
    background("rgb(233, 218, 84)"); // Minion Yellow
    rectangle1.changeColor("rgb(7, 169, 163)"); // Light Sea Green
  }
  rectangle1.paint();
  rectangle2.paint();
  rectangle2.x = mouseX;
  rectangle2.y = mouseY;
}

class Rectangle {
  constructor() {
    this.x = width / 3;
    this.y = height / 3;
    this.d = random(20, 40);
    this.overlapping = false;
    this.c = 0;
  }
  paint() {
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  }
  intersects(other) {
    this.overlapping = false;
    other.overlapping = false;
    if (
      this.x + this.d * 2 >= other.x &&
      this.x <= other.x + other.d * 2 &&
      this.y + this.d >= other.y &&
      this.y <= other.y + other.d
    ) {
      this.overlapping = true;
      other.overlapping = true;
    }
  }
  changeColor(currentColor) {
    this.c = color(currentColor);
  }
}

Переглядаємо Аналізуємо

Хітбокс

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

Хітбокс (англ. Hitbox) - технічний об’єкт, що визначає розміри та положення ігрових сутностей в ігровому світі.

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

Часто такою рамкою є прямокутник (у 2D-іграх) або куб (у 3D-іграх), які прикріплюються до точки на видимому об’єкті (наприклад, моделі чи спрайті) і слідують за точкою.

Гравець, факел, корова, важіль
Хітбокси в Minecraft: гравець, факел, корова, важіль

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

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

Отже, окрім виявлення зіткнення між рухомими прямокутниками та межами полотна, виявлення зіткнення між статичним і рухомими прямокутниками можна описати за допомогою нерівностей, які враховують горизонтальну xVelocity

\$other.x + other.d * 2 + other.xVelocity > this.x\$
\$other.x + other.xVelocity &lt; this.x + this.d * 2\$
\$other.y + other.d > this.y\$
\$other.y &lt; this.y + this.d\$

і вертикальну yVelocity

\$other.x + other.d * 2 > this.x\$
\$other.x &lt; this.x + this.d * 2\$
\$other.y + other.d + other.yVelocity > this.y\$
\$other.y + other.yVelocity &lt; this.y + this.d\$

складові руху.

За допомогою слів this (нерухомий прямокутник) і other (рухомі прямокутники) відбувається доступ до значень властивостей прямокутників: x і y - координати верхнього лівого кута прямокутників, d * 2 - ширина прямокутника, y + d - висота прямокутника, x + xVelocity - горизонтальна координата наступної появи рухомого прямокутника, y + yVelocity - вертикальна координата наступної появи рухомого прямокутника.

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

sketch.js
let rectangle1;
let rectangle2;
let rectangle3;

function setup() {
  createCanvas(200, 200);
  rectangle1 = new Rectangle();
  rectangle2 = new Rectangle();
  rectangle3 = new Rectangle();
}

function draw() {
  background(120);

  rectangle1.intersects(rectangle2);
  rectangle1.intersects(rectangle3);

  rectangle1.paint();
  rectangle2.paint();
  rectangle3.paint();
}

class Rectangle {
  constructor() {
    this.d = random(20, 30);
    this.x = random(width - this.d * 2);
    this.y = random(height - this.d);
    this.c = color(random(255), random(255), random(255), random(200, 220));
    this.xVelocity = random(0.5, 1);
    this.yVelocity = random(0.5, 1);
  }
  paint() {
    stroke(255);
    strokeWeight(3);
    fill(this.c);
    rect(this.x, this.y, this.d * 2, this.d);
  }
  intersects(other) {
    other.x += other.xVelocity;
    other.y += other.yVelocity;

    if (other.x < 0 || other.x > width - other.d * 2) {
      other.xVelocity *= -1;
    }
    if (other.y < 0 || other.y > height - other.d) {
      other.yVelocity *= -1;
    }

    if (
      other.x + other.d * 2 + other.xVelocity > this.x &&
      other.x + other.xVelocity < this.x + this.d * 2 &&
      other.y + other.d > this.y &&
      other.y < this.y + this.d
    ) {
      other.xVelocity *= -1;
    }
    if (
      other.x + other.d * 2 > this.x &&
      other.x < this.x + this.d * 2 &&
      other.y + other.d + other.yVelocity > this.y &&
      other.y + other.yVelocity < this.y + this.d
    ) {
      other.yVelocity *= -1;
    }
  }
}

У класі оголошений метод intersects(), який враховує рух і відбивання прямокутників як від меж полотна, так і від меж статичного прямокутника.

Виклики метода rectangle1.intersects(rectangle2) і rectangle1.intersects(rectangle3); визначають об’єкт rectangle1 як статичний прямокутник, а rectangle2 і rectangle3 - рухомі прямокутники.

Переглядаємо Аналізуємо

У разі потреби можна змінити як об’єкт, для якого метод intersects() викликається, так і аргумент метода, з яким він викликається.

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

5.4.6. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «detect collision»?

  2. Які труднощі можуть виникати при проєктуванні взаємодії програмних об’єктів?

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

5.4.7. Практичні завдання

Початковий

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

Середній

  1. Створити застосунок, який демонструє взаємодію двох об’єктів квадратної форми. Орієнтовний взірець роботи застосунку представлено у демонстрації.

Високий

  1. Створити застосунок, в якому відбувається взаємодія круглого об’єкта з прямокутним об’єктом. Результатом взаємодії може бути будь-яка поведінка, наприклад, як у демонстрації.

Екстремальний

  1. Створити застосунок, в якому текстовий рядок розділяється на символи, які наносяться на квадратні об’єкти-наліпки різного розміру і кольору. Кожну наліпку з символом можна переміщувати на полотні вказівником миші та складати з символів початковий текстовий рядок. Орієнтовний взірець роботи застосунку представлено у демонстрації.

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

5.5. Доцільність створення класів та об’єктів для розв’язання задач

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

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

Поміркуймо над перевагами й недоліками використання об’єктоорієнтованого програмування для розв’язання задач в контексті використання бібліотеки p5.js.

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

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

Очевидно, що для простих застосунків немає сенсу використовувати класи, хіба що з навчальною метою. У цьому разі можна скористатися звичайними глобальними змінними й весь функціонал застосунку описати у вбудованих функціях setup() і draw(). Якщо треба, у файл ескізу можна додати кілька власних функцій.

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

Наприклад, для зберігання властивостей кольору можна створити об’єкт colors

sketch.js
let colors = {
  black: 0, // Black
  white: 255, // White
  "light gray": "#CDCED0" // Light Gray
}

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

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

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

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

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

Наприклад, у коді застосунку

sketch.js
let circle1, circle2;

function setup() {
  createCanvas(200, 200);
  circle1 = new Circle();
  circle2 = new Circle();
}

function draw() {
  background(220);

  circle1.intersects(circle2);

  if (circle1.overlapping) {
    background(120);
  }

  circle1.paint();
  circle2.changeColor("rgba(135, 35, 250, 0.8)"); // Electric Violet
  circle2.paint();

  circle1.move();
  circle2.move();
}

class Circle {
  constructor() {
    this.c = "rgba(255, 214, 64, 0.8)"; // Mustard
    this.r = 20;
    this.x = random(this.r, width - this.r);
    this.y = random(this.r, height - this.r);
    this.xVelocity = random(1, 1.5);
    this.yVelocity = random(1, 1.5);
    this.overlapping = false;
  }
  paint() {
    ellipseMode(RADIUS);
    noStroke();
    fill(this.c);
    ellipse(this.x, this.y, this.r, this.r);
  }
  move() {
    this.x += this.xVelocity;
    this.y += this.yVelocity;
    if (this.x < this.r || this.x > width - this.r) {
      this.xVelocity *= -1;
    }

    if (this.y < this.r || this.y > height - this.r) {
      this.yVelocity *= -1;
    }
  }
  intersects(other) {
    this.overlapping = false;
    let d = dist(this.x, this.y, other.x, other.y);
    if (d < this.r + other.r) {
      this.overlapping = true;
    }
  }
  changeColor(currentColor) {
    this.c = color(currentColor);
  }
}

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

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

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

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

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

Переглядаємо Аналізуємо

Вправа 51

Переписати код взаємодії рухомих об’єктів у процедурному стилі без використання принципів об’єктоорієнтованого програмування.

5.5.2. Контрольні запитання

Міркуємо Обговорюємо

  1. За яких умов у створенні застосунку варто застосувати принципи об’єктоорієнтованого програмування?

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

5.5.3. Практичні завдання

Початковий

  1. Наведений нижче код застосунку, який обчислює площу прямокутника за відомими значеннями його сторін, використовує процедурний стиль програмування. Перепишіть код у стилі об’єктоорієнтованого програмування. Напишіть клас Rectangle, який має властивості w (ширина прямокутника), h (висота прямокутника), c - колір заливки. Клас має містити також два методи: paint() - для малювання прямокутника і calculateArea() - для обчислення площі прямокутника як добуток його ширини й висоти. Орієнтовний зразок роботи застосунку представлено на малюнку.

Обчислення площі прямокутника у різних парадигмах
Обчислення площі прямокутника у різних парадигмах
sketch.js
let w = 120;
let h = 60;

function setup() {
  createCanvas(200, 200);
  rectMode(CENTER);
  textAlign(CENTER, CENTER);
  textSize(40);
  console.log(calculateArea(w, h));
}

function draw() {
  background(245);
  fill(61, 52, 139); // Tekhelet
  rect(width / 2, height / 2, w, h);
  fill(230, 175, 46); // Xanthous
  text(calculateArea(w, h), width / 2, height / 2);
}

function calculateArea(w, h) {
  return w * h;
}

Середній

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

Високий

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

Екстремальний

  1. Створити власну версію гри Snake Game (Змійка ). Для гри можна визначити свої правила. Наприклад, коли змія торкається межі полотна, то вона з’являється біля протилежної межі полотна. Якщо змія торкається перешкоди, то її хвіст виростає, а лічильник, розташований на її голові, і швидкість її руху збільшуються. Гравець керує клавішами-стрілками або клавішами WASD і програє у разі, коли змія наткнеться на саму себе. Орієнтовний взірець роботи застосунку представлено у демонстрації.

6. Мультимедіа

В цьому розділі ви ознайомитесь з поняттями масиву, трансформації та їх застосування при роботі з мультимедійними даними.

6.1. Поняття, реалізація та застосування масивів

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

Щоб імітувати рух кола горизонтально, необхідно збільшувати значення його x-координати. Оскільки є два кола, опишемо дві глобальні змінні, значення яких будуть змінюватися у функції draw().

sketch.js
let x1 = -5;
let x2 = 5;

function setup() {
  createCanvas(200, 200);
  noStroke();
}

function draw() {
  background(75);
  fill(255, 200); // White
  x1 += 0.5;
  x2 += 0.5;
  ellipse(x1, 20, 20, 20);
  ellipse(x2, 40, 20, 20);
}

Змінимо код, щоб на полотні з’явилась більша кількість фігур.

sketch.js
let x1 = -5;
let x2 = 5;
let x3 = -20;
let x4 = 20;
let x5 = 45;

function setup() {
  createCanvas(200, 200);
  noStroke();
}

function draw() {
  background(75);
  fill(255, 200); // White
  x1 += 0.5;
  x2 += 0.5;
  x3 += 0.5;
  x4 += 0.5;
  x5 += 0.5;
  ellipse(x1, 30, 20, 20);
  ellipse(x2, 60, 20, 20);
  ellipse(x3, 90, 20, 20);
  ellipse(x4, 120, 20, 20);
  ellipse(x5, 150, 20, 20);
}

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

6.1.1. Масиви в JavaScript

Масив - тип даних в JavaScript, який є впорядкованою колекцією значень, що має ім’я. Кожне значення масиву називається елементом масиву. Кожний елемент масиву має індекс - позицію в масиві, яка позначається цілим числом. Відлік індексів починається від нуля.

В JavaScript елемент масиву може належати будь-якому типу даних, а різні елементи масиву можуть мати різні типи даних. Можливість зберігати в масивах JavaScript елементи будь-якого типу дозволяє створювати складні структури даних, на зразок масиву об’єктів чи масиву масивів.

Проілюструємо концептуальну структуру масиву на прикладі масиву об’єктів.

Структура масиву об’єктів
Структура масиву об'єктів з ім'ям shapes: у масиві 10 елементів, значення індексу першого елемента дорівнює 0, значення індексу останнього елемента дорівнює 9

Найпростіший спосіб створити масив - використати літерал масиву - квадратні дужки []. Типовий алгоритм створення масиву можна описати так:

  1. Використати зарезервоване слово let (або const).

  2. Вказати назву масиву.

  3. Записати значення елементів масиву, розділених комами, у квадратних дужках [].

Приклади створення масивів:

let r = 155;
let lavenderGray = "#C3BDD0";
let empty = []; (1)
let coordsX = [-5, 5, -20, 20, 45]; (2)
let misc = [2.5, r, true, lavenderGray, [r + 100, r - 100], {x: 3, y: 7}, coordsX] (3)
1 Створення порожнього масиву empty без елементів.
2 Створення масиву coordsX з п’ятьма елементами, значеннями яких є цілі числа.
3 Створення масиву misc із сімома елементами, значеннями яких є різні типи даних, зокрема об’єкти та інші масиви.

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

Наприклад:

let coordsX = [-5, 5, -20, 20, 45];
console.log(coordsX[0]); // -5
console.log(coordsX[3]); // 20
console.log(coordsX[5]); // undefined
console.log(coordsX[100]); // undefined

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

Цікавимось

Особливості масивів в JavaScript

Масив в JavaScript є спеціалізованою формою об’єкта JavaScript, тому індекси масиву насправді є іменами властивостей масиву. Квадратні дужки, які використовуються для доступу до елементів масиву, працюють так само як квадратні дужки, що застосовуються для отримання властивостей масиву.

Масиви в JavaScript є об’єктами, тому в них можна створювати властивості з будь-якими іменами.

Як розрізнити індекс масиву та ім’я властивості об’єкта? Усі індекси є іменами властивостей, але лише ті імена властивостей, які є цілими числами, будуть індексами.

З огляду на те, що індекси є особливими іменами властивостей об’єкта, у масивах JavaScript відсутнє поняття помилки вихід за межі діапазону. При спробі доступу до властивості, що не існує, будь-якого об’єкта ви отримаєте не помилку, а значення undefined. Така поведінка характерна як для об’єктів, так і для масивів.

Будь-який масив в JavaScript має властивість length, яка відрізняє масиви від звичайних об’єктів JavaScript. Властивість length вказує на кількість елементів у масиві, інакше кажучи, містить інформацію про довжину масиву.

Отримаємо значення length для масиву coordsX:

let coordsX = [-5, 5, -20, 20, 45];
console.log(coordsX[4]); // 45
console.log(coordsX.length); // 5
Значення властивості length на одиницю більше найбільшого індексу в масиві.

Квадратні дужки можна використовувати як для отримання (читання), так і для зміни (запису) значень елементів масиву.

let coordsX = [-5, 5, -20, 20, 45];
coordsX[0] = 10; // записати елемент 0 (1)
coordsX[5] = 75; // записати елемент 5 (2)
coordsX[99] = 0; // записати елемент 99 (3)
console.log(coordsX[0]); // прочитати елемент 0 (4)
console.log(coordsX[4]); // прочитати елемент 4 (5)
console.log(coordsX[5]); // прочитати елемент 5 (6)

Проаналізуємо наші дії.

1 Змінюємо у масиві coordsX значення елемента з індексом 0. Було значення -5, стало - 10.
2 Змінюємо у масиві coordsX значення елемента з індексом 5. Елемент з таким індексом у масиві відсутній. Якби ми звернулися до цього елемента, то отримали б значення undefined. Для цього елемента встановлюємо значення 75.
3 Змінюємо у масиві coordsX значення елемента з індексом 99. Елемент з таким індексом у масиві відсутній. Якби ми звернулися до цього елемента, то отримали б значення undefined. Для цього елемента встановлюємо значення 0.
4 Отримуємо значення елемента з індексом 0. Це число 10, яке ми встановили у першому пункті.
5 Отримуємо значення елемента з індексом 4. Це число 45. Воно залишилося з моменту ініціалізації масиву.
6 Отримуємо значення елемента з індексом 5. Це число 75, яке ми встановили у другому пункті.

Зараз значення властивості length для масиву coordsX дорівнює 100, що є набагато більше, ніж елементів у масиві. Перевіримо це в консолі вебпереглядача, надрукувавши значення length і увесь масив coordsX загалом:

console.log(coordsX.length); // 100
console.log(coordsX); // (100) [10, 5, -20, 20, 45, 75, пусто × 93, 0]

Щоб краще зрозуміти отримані результати, запишемо їх інакше:

(100) [10, 5, -20, 20, 45, 75, undefined, undefined, ..., undefined, undefined, 0]

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

У цьому разі масив coordsX називається розрідженим.

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

Зазвичай властивість length вказує на кількість елементів у масиві, але коли масив є розрідженим, значення length може бути більшим за кількість елементів у масиві. Властивість length завжди перевищує індекс будь-якого елемента у масиві.

Наприклад:

let a = []; // масив без елементів, властивість length = 0
a[1000] = 0; // масив отримав один елемент, властивість length = 1001

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

Наприклад:

let coordsX = [-5, 5, -20, 20, 45]; // спочатку у масиві 5 елементів
coordsX.length = 3; // тепер у масиві три елементи [-5, 5, -20]
coordsX.length = 0; // видалення усіх елементів масиву, масив став порожнім []
coordsX.length = 1000; // довжина масиву 1000, але елементи у масиві відсутні
Масиви в JavaScript є динамічними, тобто в разі потреби можуть збільшуватися чи зменшуватися.

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

Наприклад:

let colors = []; (1)
colors.push("red"); (2)
colors.push("green", "blue"); (3)
1 Розпочинаємо з порожнього масиву colors.
2 Додати значення "red" в кінець масиву colors. Тепер масив має вигляд: ["red"].
3 Додати ще два значення "green" і "blue" в кінець масиву colors. Тепер масив має вигляд: ["red", "green", "blue"].

Як і при видаленні властивостей об’єктів, для видалення елементів масиву використовують оператор delete.

Наприклад:

let colors = ["red", "green", "blue"];
delete colors[2]; // тепер у масиві colors з індексом 2 міститься undefined
console.log(colors.length); // 3, оскільки оператор delete не впливає на довжину масиву

Використання оператора delete з елементом масиву не змінює значення властивості length і не зсовує елементи з більшими індексами, щоб заповнити прогалину, яка залишається після видалення. Після видалення елемента з масиву за допомогою delete масив стає розрідженим.

Цікавимось Додатково

Методи масиву у JavaScript

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

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

Одним із таких методів є forEach().

Приклад використання:

let coords = [15, 50, 25, 150, 100];

coords.forEach(function(item, index, arr) {
  arr[index] = item + 1;
});

console.log(coords); // [16, 51, 26, 151, 101]

Метод forEach() проходить по масиву і змінює його, викликаючи один раз для кожного елемента масиву зазначену функцію. Функція викликається з трьома аргументами:

  • значення елемента масиву item,

  • індекс елемента масиву index,

  • масив arr.

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

Наприклад:

sketch.js
let coords = [15, 50, 25, 150, 100];

function setup() {
  createCanvas(250, 200);
  noStroke();
}

function draw() {
  background(220);
  fill("rgba(120, 53, 135, 0.7)"); // Maximum Purple
  coords.forEach((item) => {
    circle(item, 100, item);
  });
}

Метод map() передає кожний елемент масиву, на якому він викликається, зазначеній функції й повертає новий масив зі значеннями, які повернула функція.

Приклад використання методу map(), коли у функцію передається один аргумент - значення елемента масиву:

let coords = [15, 50, 25, 150, 100];
const newCoords = coords.map((item) => item * 2);
console.log(newCoords); // [30, 100, 50, 300, 200]

Метод filter() повертає новий масив, який містить значення тих елементів, що відповідають певній умові. У цьому разі функція, яка передається в метод, повинна повертати логічні значення: true або false.

Приклад використання методу filter(), коли у функцію передається один аргумент - значення елемента масиву:

let coords = [15, 50, 25, 150, 100];
const newCoords = coords.filter((item) => item < 100);
console.log(newCoords); // [15, 50, 25]

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

let coords = [15, 50, 25, 150, undefined, 100];
const newCoords = coords.filter((item, index) => index > 2 && item !== undefined);
console.log(newCoords); // [150, 100]

Методи find() і findIndex() проходять по масиву в пошуку елементів, для яких функція повертає істинне значення true. Як тільки перше таке значення буде знайдено, прохід по масиву зупиняється. У цьому разі метод find() повертає елемент масиву (інакше повертає undefined), а метод findIndex() - індекс елемента масиву (інакше - -1).

Наприклад:

let coords = [15, 50, 25, 150, 100];
console.log(coords.find((item) => item === 150)); // 150
console.log(coords.find((item) => item === 0)); // undefined
console.log(coords.findIndex((item) => item === 100)); // 4
console.log(coords.findIndex((item) => item < 0)); // -1

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

Наприклад:

let coords = [15, 50, 25, 150, 100];
console.log(coords.reduce((x, y) => x + y, 0)); // 340

Метод reduce() приймає два аргументи. Перший - це функція, яка виконує «скорочення» масиву до єдиного значення, а другий (необов’язковий) - це початкове значення, яке передається у функцію. У прикладі вище першим аргументом метода є функція (x, y) ⇒ x + y, а другим аргументом є 0.

У прикладі вище функція, яка передається у метод, «скорочує» масив сумуванням його елементів і повертає результат обчислення суми елементів як єдине значення.

Щоб зрозуміти роботу метода reduce(), подивимось ближче на функцію, яка використовується у методі.

let coords = [15, 50, 25, 150, 100];
coords.reduce(function (result, elem, index, arr) {
  console.log(`${result} + ${elem} = ${result + elem}`);
  return result + elem;
}, 0); // 340

Функція, яку використовує reduce(), відрізняється від функцій, які застосовуються у forEach() і map().

Для reduce(), функція отримує чотири аргументи: накопичений на цей момент результат result «скорочення» масиву, значення елемента elem масиву, індекс елемента index масиву і сам масив arr. При першому виклику функції перший аргумент має значення, яке передається як другий аргумент методу reduce() - 0 у прикладі вище.

Результат виконання метода reduce() для нашого розширеного прикладу:

0 + 15 = 15
15 + 50 = 65
65 + 25 = 90
90 + 150 = 240
240 + 100 = 340

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

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

let coords = [15, 50, 25, 150, 100];
console.log(coords.reduce((x, y) => (x > y) ? x : y)); // 150

У коді використовується скорочений варіант запису вказівки розгалуження if - тернарний оператор.

Тернарний оператор містить три операнди:

  • (x > y) - умова, за якою слідує знак питання ?;

  • вираз, який виконується, якщо умова істинна, після чого йде двокрапка :;

  • вираз, який виконується, якщо умова є хибною.

Якщо функцію, яка надається для методу reduce() як перший аргумент, записати повністю

let coords = [15, 50, 25, 150, 100];
coords.reduce(function (result, elem, index, arr) {
  console.log(`Що більше: ${result} чи ${elem}?`);
  return result > elem ? result : elem;
}); // 150

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

Що більше: 15 чи 50?
Що більше: 50 чи 25?
Що більше: 50 чи 150?
Що більше: 150 чи 100?

Методи стеків і черг додають і видаляють елементи на початку і в кінці масивів.

Стек - лінійний список, в якому усі операції вставки та видалення елементів виконуються лише на одному з кінців списку. Черга - лінійний список, у якому всі операції вставки здійснюються на одному з кінців списку, а всі операції видалення - на іншому.

Метод push(), який додає один чи кілька елементів в кінець масиву і повертає нове значення довжини масиву, ми вже використовували.

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

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

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

Приклади використання зазначених методів:

let coords = [15, 50, 25, 150, 100];
console.log(coords.pop()); // 100
console.log(coords.shift()); // 15
coords.unshift(1000); // [1000, 50, 25, 150]
coords.push(0); // [1000, 50, 25, 150, 0]

Розглянемо методи для роботи з частинами масиву - підмасивами або зрізами.

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

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

Наприклад:

let coords = [15, 50, 25, 150, 100];
console.log(coords.slice(1, 3)); // [50, 25]
console.log(coords.slice(2)); // [25, 150, 100]
console.log(coords.slice(2, -1)); // [25, 150]
console.log(coords.slice(-3, -2)); // [25]

Метод splice() - це універсальний метод для вставки чи видалення елементів з масиву. На відміну від slice(), метод splice() змінює масив.

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

Метод splice() повертає масив з видаленими елементами або порожній масив, якщо елементи не були видалені.

Наприклад:

let coords = [15, 50, 25, 150, 100];
console.log(coords.splice(1, 3)); // [50, 25, 150]
console.log(coords); // [15, 100]

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

Наприклад:

let coords = [15, 50, 25, 150, 100];
console.log(coords.splice(1, 3, 0, [true, false])); // [50, 25, 150]
console.log(coords); // [15, 0, Array(2), 100]

Метод fill() встановлює певне значення елементам масиву чи зрізу масиву. Метод змінює масив, на якому викликається, і повертає змінений масив.

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

Проілюструємо приклади використання метода fill():

let coords = new Array(5); // [undefined, undefined, undefined, undefined, undefined] - створення масиву з п'яти елементів
coords.fill(0); // [0, 0, 0, 0, 0] - заповнення масиву нулями з індексу 0
coords.fill(5, 2); // [0, 0, 5, 5, 5] - заповнення масиву п'ятірками з індексу 2
coords.fill(7, 1, -1); // [0, 7, 7, 7, 5] - заповнення масиву сімками від індексу 1 до індексу -1, не включаючи останній

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

В масивах JavaScript реалізовані методи indexOf() і lastIndexOf(), які шукають в масиві елементи із вказаним значенням, яке передається методам як перший аргумент, і повертають індекс першого знайденого елемента або -1, якщо пошук був неуспішним.

Метод indexOf() виконує пошук в масиві з початку і до кінця, а метод lastIndexOf() - у зворотному напрямку. Методи порівнюють аргумент з елементами масиву із застосуванням операції ===.

Методи можуть отримувати другий необов’язковий аргумент, що позначає індекс в масиві, з якого повинен починатися пошук. Якщо другий аргумент не використовується, то indexOf() починає пошук спочатку, а lastIndexOf() - з кінця.

Наприклад:

let coords = [15, 50, 25, 15, 100];
console.log(coords.indexOf(25)); // 2, оскільки coords[2] дорівнює 25
console.log(coords.lastIndexOf(15)); // 3, оскільки coords[3] дорівнює 15, пошук виконується справа наліво, індекси відраховуються зліва направо
console.log(coords.lastIndexOf(0)); // -1, елементи зі значенням 0 відсутні

Якщо необхідно перевірити наявність певного значення в масиві, можна скористатися методом includes(), який приймає єдиний аргумент і повертає логічне значення true, якщо масив містить значення, інакше - false.

Наприклад:

let coords = [15, 50, 25, 15, 100];
console.log(coords.includes(25)); // true
console.log(coords.includes(0)); // false

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

Наприклад:

let colors = ["black", "white", "red", "green", "blue"];
let coords = [15, 50, 25, 15, 100];
colors.sort();
coords.sort();
console.log(colors); // ["black", "blue", "green", "red", "white"]
console.log(coords); // [100, 15, 15, 25, 50]

Щоб відсортувати масив не в алфавітному порядку, необхідно передати методу sort() як аргумент функцію порівняння. Така функція буде вирішувати, який із двох її аргументів повинен бути першим у відсортованому масиві.

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

Приклади коду для різних видів сортування:

  • алфавітний порядок

let coords = [15, 50, 25, 15, 100];
coords.sort();
console.log(coords); // [100, 15, 15, 25, 50]
  • числовий порядок

let coords = [15, 50, 25, 15, 100];
coords.sort(function (a, b) {
  return a - b;
});
console.log(coords); // [15, 15, 25, 50, 100]
  • зворотний числовий порядок

let coords = [15, 50, 25, 15, 100];
coords.sort((a, b) => b - a);
console.log(coords); // [100, 50, 25, 15, 15]

Використання метода reverse() - ще один спосіб змінити порядок елементів масиву. Метод не створює новий масив, а змінює порядок слідування елементів у поточному масиві на протилежний.

Наприклад:

let coords = [15, 50, 25, 15, 100];
coords.reverse();
console.log(coords); // [100, 15, 25, 50, 15]
Дізнайтеся про функції p5.js для роботи з масивами із розділу Data офіційної документації бібліотеки.

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

Усередині функції setup() за допомогою циклу for заповнимо масив circles випадковими значеннями з рухомою крапкою, що позначатимуть x-координати кіл, а потім ці числа, взяті із масиву, використаємо за допомогою for у функції draw() для встановлення значень координат кіл та імітації їхнього руху.

sketch.js
let circles = []; (1)
let k = 100; (2)

function setup() {
  createCanvas(200, 200);
  noStroke();
  fill(255, 200); // White
  for (let i = 0; i < k; i++) {
    circles.push(random(-20, 200)); (3)
  }
}

function draw() {
  background(75);
  for (let i = 0; i < circles.length; i++) { (4)
    circles[i] += 0.5; (5)
    let y = i * 2; (6)
    ellipse(circles[i], y, 20, 20); (7)
  }
}

Розглянемо наведений код детальніше.

1 Оголошуємо порожній масив з ім’ям circles за допомогою квадратних дужок [].
2 Оголошуємо змінну k - кількість елементів масиву.
3 Заповнення масиву circles випадковими числами з рухомою крапкою з діапазону [-20, 200) (включно із -20, але не включаючи 200) за допомогою методу push().
4 Проходження по масиву circles з метою отримання значень його елементів.
5 Збільшення i-го елемента масиву circles на 0.5 (збільшення x-координати кола для імітації його горизонтального руху). Запис colors[i] - доступ до i-того елемента масиву colors.
6 Оголошення змінної y і присвоєння їй значення y-координати кола.
7 Використання функції ellipse() для малювання кола за x-координатою, яка береться з масиву, та y-координатою, що обчислюється в тілі циклу.

Переглядаємо Аналізуємо

Для циклічного проходу по масиву можна також використовувати цикл for..of. Створимо масив colors, який буде містити значення кольорів, і візуалізуємо роботу циклу for..of.

let colors = [
  "rgba(255, 194, 81, 0.8)", // Maximum Yellow Red
  "rgba(82, 255, 200, 0.8)", // Sea Green Crayola
  "rgba(245, 127, 76, 0.8)", // Mandarin
  "rgba(135, 129, 11, 0.8)", // Olive
  "rgba(135, 11, 87, 0.8)"   // Pansy Purple
];

function setup() {
  createCanvas(200, 200);
  noStroke();
  noLoop(); // зупиняє безперервне виконання коду у draw()
}

function draw() {
  background(75);
  for (let c of colors) {
    fill(c);
    ellipse(random(width), random(height), 20, 20);
  }
}

У циклі for..of із масиву береться черговий елемент, значення якого використовується як аргумент функції fill() для кольору зафарбовування кола. Функція noLoop() зупиняє безперервне виконання коду у draw(), тому утворюється статичний малюнок із п’яти кольорових кіл на полотні.

Переглядаємо Аналізуємо

Щоб використовувати цикл for..of і знати індекс кожного елемента масиву, необхідно використовувати метод entries() масиву разом з деструктурованим присвоєнням. Застосуємо такий підхід для масиву статичних кіл.

sketch.js
let colors = [
  "rgba(255, 194, 81, 0.8)", // Maximum Yellow Red
  "rgba(82, 255, 200, 0.8)", // Sea Green Crayola
  "rgba(245, 127, 76, 0.8)", // Mandarin
  "rgba(135, 129, 11, 0.8)", // Olive
  "rgba(135, 11, 87, 0.8)", // Pansy Purple
];

function setup() {
  createCanvas(200, 200);
  noStroke();
  noLoop(); // зупиняє безперервне виконання коду у draw()
}

function draw() {
  background(75);
  for (let [index, item] of colors.entries()) { (1)
    let x = random(width); (2)
    let y = random(height);
    fill(255); // White
    textSize(10);
    textAlign(CENTER, CENTER);
    text(index, x, y); (3)
    fill(item); (4)
    ellipse(x, y, 20, 20);
  }
}
1 Застосовуємо метод entries() до масиву colors, щоб отримати пари значень [index, item] у формі масиву. На кожній ітерації циклу виконується деструктуроване присвоєння: елемент index отримує значення індексу елемента масиву colors, а елемент item - значення елемента масиву colors. Імена для index і item можна обрати на свій вибір.
2 Оголошення змінних для зберігання випадкових значень координат кіл на полотні.
3 Використання index, що містить значення індексу елемента масиву colors, для відображення на полотні числа за допомогою функції text() . Для налаштування числового напису на полотні використовуються функції textSize() і textAlign() .
4 Використання item, що містить значення елемента масиву colors, як аргументу функції fill() для зафарбовування кола.

Переглядаємо Аналізуємо

У підсумку, об’єднаємо два застосунки в один.

sketch.js
let colors = [
  "rgba(255, 194, 81, 0.8)", // Maximum Yellow Red
  "rgba(82, 255, 200, 0.8)", // Sea Green Crayola
  "rgba(245, 127, 76, 0.8)", // Mandarin
  "rgba(135, 129, 11, 0.8)", // Olive
  "rgba(135, 11, 87, 0.8)", // Pansy Purple
];

let circles = [];
let k = 100;

function setup() {
  createCanvas(200, 200);
  noStroke();
  for (let i = 0; i < k; i++) {
    circles.push(random(-20, 200));
  }
}

function draw() {
  background(75);
  for (let [index, item] of circles.entries()) {
    circles[index] += 0.5;
    let y = index * 2;
    let c = index % colors.length;

    fill(255); // White
    textSize(10);
    textAlign(CENTER, CENTER);
    text(index, item, y);

    fill(colors[c]);
    ellipse(item, y, 20, 20);
  }
}

Оскільки у масиві circles є сто елементів (від 0 до 99 включно), а у масиві colors - всього п’ять (від 0 до 4 включно), то у застосунку за допомогою виразу index % colors.length реалізовано повторне використання значення кольору заливки кіл.

У виразі index % colors.length використовується оператор %, який позначає операцію остача від ділення. Механізм повторного використання кольору представлений у таблиці.

Таблиця "Результат операції index % colors.length для різних значень індексів index масиву circles"
Значення index Операція index % colors.length Результат index % colors.length

0

0 % 5

0

1

1 % 5

1

2

2 % 5

2

3

3 % 5

3

4

4 % 5

4

5

5 % 5

0

6

6 % 5

1

7

8 % 5

2

9

9 % 5

3

...

...

...

95

95 % 5

0

96

96 % 5

1

97

97 % 5

2

98

98 % 5

3

99

99 % 5

4

Вправа 52

Використати масив, що зберігає значення розмірів кіл, для побудови фігур.

6.1.2. Масив масивів

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

Кількість індексів визначає розмірність масиву.

Проілюструємо концептуальну структуру двовимірного масиву на прикладі двовимірного масиву об’єктів.

Структура двовимірного масиву об’єктів
Структура двовимірного масиву об'єктів з ім'ям shapes: у масиві 20 елементів, місце конкретного елемента масиву визначається двома індексами

Для доступу до значень у двовимірному масиві використовується подвійна операція []. Спочатку необхідно записати покликання на масив (ім’я масиву), а потім всередині перших квадратних дужок записати індекс рядка, в якому перебуває елемент масиву, а у других квадратних дужках - індекс стовпця.

Щоб отримати значення конкретного об’єкта у двовимірному масиві, наприклад як на ілюстрації, необхідно записати shapes[2][1].

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

sketch.js
let rows = 10; (1)
let columns = 10; (2)
let p = 0; (3)
let colors = new Array(rows); (4)

function setup() {
  createCanvas(200, 200);
  noStroke();
  for (let i = 0; i < colors.length; i++) {
    colors[i] = new Array(columns); (5)
    for (let j = 0; j < colors[i].length; j++) {
      colors[i][j] = random(0, 230); (6)
    }
  }
  noLoop();
}

function draw() {
  background(220);

  for (let i = 0; i < colors.length; i++) {
    let y = i * rows * 2; (7)

    for (let j = 0; j < colors[i].length; j++) {
      let x = j * columns * 2; (7)

      fill(colors[i][j]); (8)
      rect(x, y, rows * 2, columns * 2);

      fill(255); // White
      textSize(10);
      textAlign(CENTER, CENTER);
      text(p, x + rows, y + columns); (9)

      p += 1; (10)
    }
  }
}

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

1 Оголошуємо глобальну змінну rows, яка визначатиме кількість рядків у двовимірному масиві.
2 Оголошуємо глобальну змінну columns, яка визначатиме кількість стовпців у двовимірному масиві.
3 Оголошуємо глобальну змінну p, яка буде позначати номер пікселя. Черговий піксель на полотні буде мати значення на одиницю більше p.
4 Створення одномірного масиву colors за допомогою функції-конструктора Array(). Це спосіб створення масиву із вказаною довжиною rows і значеннями undefined. Створення масиву за допомогою Array() можна використовувати, коли наперед відомо скільки елементів буде містити масив.
5 У зовнішньому циклі for на кожній ітерації у масив colors додаємо черговий елемент - новий одномірний масив довжиною columns. Інакше кажучи, наповнюємо масив colors елементами, які є масивами.
6 У внутрішньому циклі for на кожній ітерації звертаємось до елементів двовимірного масиву, який був утворений в пункті 5, за допомогою запису colors[i][j], де i та j - це індекси рядків та стовпців двовимірного масиву colors відповідно. Для конкретного елемента двовимірного масиву присвоюємо випадкове значення кольору.
7 Заповнивши двовимірний масив значеннями, проходимо знову по масиву за допомогою двох циклів for і на кожній ітерації обчислюємо координати x і y верхнього лівого кута чергового прямокутника, який буде відігравати роль одного пікселя.
8 Встановлюємо колір пікселя, взявши значення кольору з масиву, і малюємо піксель на полотні у координатах, обчислених у пункті 7.
9 Друкуємо порядковий номер пікселя.
10 Збільшуємо значення номера пікселя на одиницю.

Переглядаємо Аналізуємо

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

(10) [Array(10), Array(10), Array(10), Array(10), Array(10), Array(10), Array(10), Array(10), Array(10), Array(10)]
0: Array(10)
  0: 119.63071363090914
  1: 103.34870238078383
  2: 196.1683547436366
  3: 149.147481174876
  4: 207.27953901302422
  5: 164.28415434344262
  6: 55.499118390416854
  7: 124.06536828614632
  8: 108.8618965094095
  9: 39.526639550447705
1: Array(10)
2: Array(10)
3: Array(10)
4: Array(10)
5: Array(10)
6: Array(10)
7: Array(10)
8: Array(10)
9: Array(10)

Вправа 53

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

6.1.3. Масив об’єктів

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

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

Отже, код застосунку може бути таким:

sketch.js
let circles = []; (1)

function setup() {
  createCanvas(200, 200);
  noStroke();
}

function draw() {
  background(220);
  for (let i = 0; i < circles.length; i++) {
    circles[i].paint(); (4)
  }
}

function mousePressed() {
  circles.push(new Circle(mouseX, mouseY)); (3)
}

function keyPressed() {
  circles.pop(); (5)
}

class Circle { (2)
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.r = random(10, 30);
    this.c = color(random(255), random(255), random(255), random(200, 255));
  }
  paint() {
    ellipseMode(RADIUS);
    fill(this.c);
    ellipse(this.x, this.y, this.r * 2, this.r * 2);
  }
}
1 Оголошуємо порожній масив circles.
2 Описуємо клас Circle для створення об’єктів кола. Усі об’єкти кола будуть мати такі властивості: координати центру кола, які будуть надаватися конструктору класу при створенні кола, і випадкові значення радіуса кола та кольору заливки, визначені всередині конструктора класу. Клас міститиме метод paint() для малювання об’єктів на полотні.
3 За допомогою метода push() додаємо в кінець масиву circles черговий об’єкт кола, який створюється за допомогою виклику new Circle(mouseX, mouseY), де mouseX і mouseY - це значення координат вказівника миші, які стають координатами центру побудови щоразу нового об’єкта кола. Додавання об’єкта кола як елемента у масив circles відстежується і виконується за допомогою функції mousePressed() , яка викликається один раз після кожного натискання будь-якої кнопки миші на полотні.
4 У функції draw() у циклі for для кожного елемента масиву circles застосовується метод paint(), який отримує кожний об’єкт кола при його створенні у пункті 3.
5 За допомогою метода pop() з кінця масиву видаляється по одному елементу. Це відбувається тоді, коли спрацьовує функція keyPressed() , яка викликається один раз під час кожного натискання будь-якої клавіші на клавіатурі.

Переглядаємо Аналізуємо

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

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

Отже, проаналізуємо код застосунку:

sketch.js
let k = 10;
let circles = new Array(k); (1)

function setup() {
  createCanvas(200, 200);
  for (let i = 0; i < circles.length; i++) {
    circles[i] = new Circle(); (3)
  }
}

function draw() {
  background(120);

  for (let i = 0; i < circles.length; i++) {
    circles[i].paint();
    circles[i].movement();
    for (let j = 0; j < circles.length; j++) {
      if (i !== j && circles[i].intersects(circles[j])) { (4)
        circles[i].changeColor();
        circles[j].changeColor();
      }
    }
  }
}

class Circle { (2)
  constructor() {
    this.r = random(3, 10);
    this.x = random(this.r, width - this.r);
    this.y = random(this.r, height - this.r);
    this.a = 200;
    this.c = color(random(255), random(255), random(255), this.a);
    this.xSpeed = random(0.5);
    this.ySpeed = random(0.5);
  }
  paint() {
    stroke("#AFEAF5"); // Blizzard Blue
    ellipseMode(RADIUS);
    fill(this.c);
    ellipse(this.x, this.y, this.r * 2, this.r * 2);
  }
  movement() {
    this.x = this.x + this.xSpeed;
    this.y = this.y + this.ySpeed;
    if (this.x - this.r * 2 < 0 || this.x > width - this.r * 2) {
      this.xSpeed = this.xSpeed * -1;
    }

    if (this.y - this.r * 2 < 0 || this.y > height - this.r * 2) {
      this.ySpeed = this.ySpeed * -1;
    }
  }
  intersects(other) {
    let d = dist(this.x, this.y, other.x, other.y);
    if (d < this.r * 2 + other.r * 2) {
      return true;
    }
    return false;
  }
  changeColor() {
    this.a = 0;
    this.c = color(120, this.a);
  }
}
1 Створюємо масив circles з k елементами. На цей момент усі значення елементів масиву - undefined.
2 Описуємо клас Circle, який визначатиме властивості створених на основі класу об’єктів. Об’єктами будуть кола різного розміру і кольору. Оголошуємо кілька методів: paint() - малювання об’єктів полотні, movement() - рух об’єктів на полотні, intersects() - виявлення зіткнень об’єктів, changeColor - встановлення прозорості об’єктів.
3 Наповнюємо масив circles об’єктами кіл.
4 У функції draw() використовуємо два цикли for. Перший використовується для проходження по масиву з метою застосування для кожного елемента методів paint() і movement(). Другий - також для проходження по елементах масиву і перевірки умови на виявлення зіткнень між колами за допомогою метода intersects(). Частина i !== j умови вказівки розгалуження if виключає випадок зіткнення об’єкта із самим собою. У разі зіткнень на об’єктах, що взаємодіють, викликається метод changeColor(), який встановлює їхню прозорість.

Переглядаємо Аналізуємо

Вправа 54

Використати двовимірний масив для зберігання об’єктів.

6.1.5. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «масив»? Навести приклади даних, які можна зберігати у масиві.

  2. Які є способи створення масиву в JavaScript?

  3. Назвати особливості одномірних і двовимірних масивів.

  4. Що таке «розріджений масив» в JavaScript?

  5. Як використати в коді застосунку певне значення елемента масиву?

6.1.6. Практичні завдання

Початковий

  1. У коді застосунку не вистачає фрагмента, який описує масив colors зі значеннями кольорів. Виправте це, щоб застосунок запрацював.

sketch.js
function setup() {
  createCanvas(200, 200);
  noStroke();
  noLoop();
}

function draw() {
  background(220);

  for (let i = 0; i < colors.length; i++) {
    fill(colors[i]);
    rect(random(200), random(200), random(20, 60), random(10, 30));
  }
}
  1. Використати проходження по масиву для покращення структури коду застосунку.

sketch.js
let d = [15, 75, 25, 45, 20];

function setup() {
  createCanvas(200, 200);
  noStroke();
  noLoop();
}

function draw() {
  background(220);

  fill(random(255), random(255), random(255), random(200, 230));
  ellipse(random(200), random(200), d[0], d[0]);

  fill(random(255), random(255), random(255), random(200, 230));
  ellipse(random(200), random(200), d[1], d[1]);

  fill(random(255), random(255), random(255), random(200, 230));
  ellipse(random(200), random(200), d[2], d[2]);

  fill(random(255), random(255), random(255), random(200, 230));
  ellipse(random(200), random(200), d[3], d[3]);

  fill(random(255), random(255), random(255), random(200, 230));
  ellipse(random(200), random(200), d[4], d[4]);
}

Середній

  1. Використовуючи масив, в якому елементами є значення горизонтальних координат, створити застосунок, що імітує рух хмаринок на небі. Для відображення зображення хмаринки (☁️) використати функцію text() . Орієнтовний зразок роботи застосунку представлено в демонстрації.

  1. Створити застосунок, що генерує випадкову мапу лабіринту. Один з екземплярів мапи представлений на малюнку.

Мапа лабіринту
Мапа лабіринту

Високий

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

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

  1. Створити застосунок, в якому при натисканні на будь-якому об’єкті на полотні вказівником миші об’єкт зникає з полотна. Орієнтовний взірець роботи застосунку представлено в демонстрації.

  1. Створити застосунок, в якому при натисканні на будь-якому об’єкті на полотні вказівником миші з’являється ще кілька об’єктів. Орієнтовний зразок виконання застосунку представлено в демонстрації.

Екстремальний

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

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

Розміщення кіл на поверхні без перекриття
Розміщення кіл на поверхні без перекриття

6.2. Текстові рядки як масиви символів

В JavaScript будь-які текстові дані є рядками.

Текстові рядки є масивами символів Unicode в кодуванні UTF-16. Текстові рядки неможливо змінити, а доступ до символів рядка відбувається за допомогою квадратних дужок [].

Наприклад:

let s = "JavaScript";
console.log(s[0]); // J
console.log(s[6]); // r
console.log(s[1000]); // undefined
В JavaScript текстові рядки розміщуються в лапках різних типів. Одинарні та подвійні лапки працюють, по суті, однаково, а якщо використовувати зворотні лапки, то в такий рядок можна вставляти довільні вирази, загорнувши їх в конструкцію ${вираз}.

В JavaScript за допомогою методу join(), можна перетворити усі елементи масиву в рядки та виконати їх об’єднання (конкатенацію) в єдиний рядок. Метод join() може отримувати як аргумент рядок, що буде розділювачем елементів масиву в кінцевому рядку. Якщо рядок розділювача не вказувати, то як розділювач буде використовуватися кома.

Наприклад:

let words = ["Javascript", "is", "my", "favorite"];
console.log(words.join()); // Javascript,is,my,favorite
console.log(words.join(", ")); // Javascript, is, my, favorite

Метод split() в JavaScript працює навпаки - він створює масив, розділяючи рядок на окремі частини, які стають елементами масиву.

Наприклад:

let s = "Javascript is my favorite";
console.log(s.split()); // ["Javascript is my favorite"]
console.log(s.split(" ")); // ["Javascript", "is", "my", "favorite"]

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

sketch.js
let r = "JavaScript і p5.js";
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(240);
  textSize(20); (1)
  textFont("Helvetica", 20); (2)
  textAlign(CENTER); (3)
  fill("#ED225D"); // Paradise Pink (4)
  text(r, width / 2, 40); (5)
  text(r.length, width / 2, 80); (6)
  text(r.toUpperCase(), width / 2, 120); (7)
  text(r.toLowerCase(), width / 2, 160); (8)
}

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

1 Функція textSize() встановлює розмір шрифту у пікселях.
2 Функція textFont() встановлює назву і розмір шрифту.
3 Функція textAlign() встановлює вирівнювання намальованого тексту і може отримувати два аргументи: вирівнювання по горизонталі (LEFT, CENTER і RIGHT) стосується значення x функції text() і вирівнювання по вертикалі (TOP, BOTTOM, CENTER і BASELINE) стосується значення y функції text() .
4 Функція fill() у цьому разі встановлює колір тексту.
5 Функція text() малює зазначений у першому аргументі текстовий рядок r на полотні у координатах x та y, зазначених у наступних двох аргументах відповідно.
6 Обчислення довжини текстового рядка r.
7 Переведення текстового рядка r у верхній регістр.
8 Переведення текстового рядка r у нижній регістр.

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

За потреби у застосунок можна додати власні шрифти.

У цьому разі необхідно викликати функцію loadFont() всередині функції preload() . Це гарантуватиме, що операція завантаження шрифту буде завершена до викликів setup() і draw().

Наприклад:

sketch.js
let myFont;
function preload() {
  myFont = loadFont("fontName.otf"); // .otf, .ttf
}

function setup() {
  textFont(myFont);
  ...
}

function draw() {
  ...
}
Дізнайтеся про функції p5.js для роботи з текстом з розділів Typography і Data офіційної документації бібліотеки.

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

sketch.js
let s = "Harry Potter";
let x = 0;

function setup() {
  createCanvas(200, 200);
  noLoop();
}

function draw() {
  background(240);
  fill(random(255), random(255), random(255));
  textAlign(CENTER, CENTER);
  for (let i = 0; i < s.length; i++) {
    x += width / s.length - s[i].length; (1)
    textSize(random(15, 65)); (2)
    text(s[i], x, height / 2 + random(-1, 1) * 20); (3)
  }
}
1 Проходимо у циклі посимвольно до кінця текстового рядка s і збільшуємо координату x окремого символу, яка використовується для малювання його на полотні.
2 Встановлюємо випадкове значення для розміру шрифту в пікселях.
3 Малюємо окремий символ s[i] на полотні у зазначених координатах.

У результаті виконання застосунку отримуємо текстовий рядок з випадковим розміщенням літер на полотні.

Текстовий заголовок
Текстовий заголовок

Вправа 55

Створити застосунок, який малює на полотні вертикальний текстовий заголовок.

6.2.2. Контрольні запитання

Міркуємо Обговорюємо

  1. Що називають «рядками» в JavaScript?

  2. Які функції для роботи з текстом входять до складу бібліотеки p5.js?

  3. Описати алгоритм додавання власного шрифту до ескізу.

6.2.3. Практичні завдання

Початковий

  1. Перетворити зазначений у коді текстовий рядок row у масив слів з ім’ям words. Результат використання утвореного масиву слів представлено у демонстрації.

sketch.js
let i = 0;
let row = "I love programming with JavaScript and p5.js";

const colors = [
  [63, 184, 175], // Verdigris
  [147, 7, 240], // Electric Violet
  [125, 211, 132], // Emerald
  [255, 158, 157], // Salmon Pink
  [240, 200, 8], // Jonquil
  [158, 242, 255], //Blizzard Blue
  [255, 61, 127], // French Rose
];

function setup() {
  createCanvas(200, 200);
  textAlign(CENTER, CENTER);
  frameRate(2); // використання нижчої частоти кадрів для уповільнення появи тексту
}
function draw() {
  let currentIndex = i % words.length;
  let currentColor = colors[currentIndex];
  let currentWord = words[currentIndex];
  background(currentColor);
  fill(255);
  textSize(25);
  text(currentWord, width / 2, height / 2);
  i += 1;
}

Середній

  1. Створити застосунок, в якому по натисканню миші змінюється текст на полотні. Орієнтовний взірець роботи застосунку представлено в демонстрації.

  1. Створити застосунок, що імітує роботу світлофора. Орієнтовний зразок роботи застосунку представлено в демонстрації.

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

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

Графіка на основі тексту
Графіка на основі тексту

Високий

  1. Створити застосунок, у якому текст відображається дзеркально. Орієнтовний зразок ефекту представлено в демонстрації. У застосунку використані додаткові шрифти, які можна завантажити за покликанням.

Екстремальний

  1. Створити текстову візитівку фільму на вибір. Орієнтовний зразок візитівки фільму «Зоряні війни Епізод VI: Повернення джедая» представлено в демонстрації. У застосунку використані додаткові шрифти, які можна завантажити за покликанням.

  1. Створити анімаційний ефект для тексту, який відтворюється за натисканням миші. Орієнтовний зразок ефекту анімації наведений у демонстрації. У застосунку використаний додатковий шрифт, який можна завантажити за покликанням.

Скористайтеся функцією lerp() , яку можна застосувати для створення руху по прямій траєкторії.
  1. Створити анімацію з емодзі. Орієнтовний зразок анімації наведений у демонстрації.

6.3. Зображення як цілісний об’єкт та як масив пікселів

Можливості бібліотеки p5.js не обмежуються малюванням геометричних фігур.

Інструментарій p5.js дозволяє використовувати файли растрових зображень, які, як відомо, є масивом пікселів - мініатюрних квадратів, що відображаються певним кольором.

6.3.1. Завантаження зображень

Розглянемо, як приєднувати графічні файли до ескізів.

Якщо розробка виконується в онлайн-редакторі, для додавання графічних файлів в ескіз необхідно зареєструватися на сайті редактора та увійти у свій обліковий запис.

У разі використання середовища Processing IDE у режимі p5.js для зберігання графічних файлів використовується каталог data, який створюється автоматично щоразу, коли відбувається додавання графічного файлу в ескіз.

Де знаходиться каталог data?

В середовищі Processing IDE зображення можна автоматично додавати в каталог data, перетягуючи файл у вікно поточного ескізу. Також графічні файли можна додати до ескізу за допомогою Ескіз  Додати файл…​ або вручну Ескіз  Показати папку ескізу Ctrl+K.

Якщо каталогу data немає, його можна створити та розмістити всередині файли зображень.

Підтримуються наступні графічні формати: GIF, JPG та PNG.

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

Отже, завантажимо растрове зображення (640х360 пікселів) в ескіз і відобразимо його на полотні.

Марсохід NASA Perseverance (Наполегливість) і марсіанський вертоліт-дрон Ingenuity (Винахідливість)
Марсохід NASA Perseverance (Наполегливість) і марсіанський вертоліт-дрон Ingenuity (Винахідливість)
sketch.js
let img; (1)

function setup() {
  createCanvas(640, 360);
  // завантаження зображення із файлу
  img = loadImage("ingenuity.jpg"); (2)
}

function draw() {
  background(220);
  // розміщення зображення в реальному розмірі в точці (0, 0)
  image(img, 0, 0); (3)
}
1 Оголошуємо змінну з ім’ям img.
2 За допомогою методу loadImage() , який приймає один аргумент - рядок із зазначенням шляху до файлу, завантажуємо цей файл у пам’ять. Змінна з ім’ям img - це покликання, яке вказує на місце в пам’яті, в якому зберігаються значення ширини й висоти зображення, масив пікселів, які визначають значення кольору для кожного пікселя у зображенні. Шлях до зображення повинен бути вказаний відносно HTML-файлу застосунку. Також, можна передати рядок кодованого зображення base64 як альтернативу шляху до файлу. Завантаження зображення з URL-адреси може бути заблоковано через вбудовану безпеку вебпереглядача.
3 Метод image() відтворює зображення на полотні повністю, коли приймає три аргументи: покликання на об’єкт зображення і дві координати лівого верхнього кута зображення. Якщо ж метод приймає п’ять аргументів, до вищезазначених аргументів додаються ширина й висота прямокутника, в межах якого зображення буде показано. Це корисно, якщо необхідно показати не усе зображення, а лише його частину.

Може так статися, що зображення не відразу стає доступним для візуалізації. Щоб бути впевненим, що зображення завантажене і готове до використання, необхідно помістити виклик методу loadImage() у функцію preload() за аналогією додавання у застосунок сторонніх шрифтів.

sketch.js
let img;

function preload() {
  // preload() виконується лише один раз
  img = loadImage("ingenuity.jpg");
}

function setup() {
  // setup() чекає, поки preload() буде виконано
  createCanvas(640, 360);
}

function draw() {
  background(220);
  // розміщення зображення в реальному розмірі в точці (0, 0)
  image(img, 0, 0);
}

У виклику методу image() додатково можна додати ще два параметри для встановлення ширини та висоти зображення. Наприклад, розташуємо у різних місцях на полотні копію зображення іншого розміру.

sketch.js
let img;

function preload() {
  img = loadImage("ingenuity.jpg");
}

function setup() {
  createCanvas(640, 360);
}

function draw() {
  background(220);
  // розміщення зображення в реальному розмірі в точці (0, 0)
  image(img, 0, 0);
  // розміщення зображення у 2 рази меншому розмірі в точці (0, 180)
  image(img, 0, height / 2, img.width / 2, img.height / 2);
}

У коді застосунку ми звернулися до властивостей ширини img.width і висоти img.height зображення, використовуючи крапкову нотацію.

Вправа 56

Змінити код застосунку, щоб розмір другого зображення змінювався відповідно місця розташування вказівника миші.

Завантажимо ще одне зображення у наш застосунок, щоб отримати багатошарову структуру, кожен шар якої існує окремо.

Астронавт
Астронавт
sketch.js
let img1, img2;

function preload() {
  img1 = loadImage("ingenuity.jpg");
  img2 = loadImage("astronaut.png");
}

function setup() {
  createCanvas(640, 360);
}

function draw() {
  background(220);
  image(img1, 0, 0);
  image(img2, width - mouseX * 1.5, 40);
}

Візуалізацію завантаженого зображення можна змінювати, накладаючи на нього фільтри.

Просте застосування до зображення фільтра досягається використанням функції tint() .

Функція tint() - це, по суті, еквівалент функції fill(), який встановлює колір та прозорість для зображення. Аргументи для tint() визначають, скільки зазначеного кольору використовувати для кожного пікселя цього зображення, а також наскільки прозорими повинні бути ці пікселі.

Для видалення поточного значення заливки для зображення і повернення зображення до оригінальних відтінків, використовують функцію noTint() .

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

Наприклад, виклик функції tint(255, 128) зробить зображення прозорим на 50% (за стандартним налаштуванням значення прозорості належить діапазону 0-255, який можна змінити за допомогою colorMode() ).

Отже, застосуємо фільтр до завантаженого зображення квітки.

Індійський лотос
Індійський лотос
sketch.js
let flower;

function preload() {
  flower = loadImage("flower.jpg");
}

function setup() {
  createCanvas(650, 600);
}

function draw() {
  // зображення зберігає початковий стан
  tint(255);
  image(flower, 0, 0);

  // зображення виглядає темнішим
  tint(100);
  image(flower, 100, 100);

  // непрозорість зображення становить 50%
  tint(255, 128);
  image(flower, 200, 200);

  // червоний відсутній, більша частина зображення - зеленого і вся - синього кольорів
  tint(0, 200, 255);
  image(flower, 300, 300);

  // зображення в червоному кольорі й прозоре
  tint(255, 0, 0, 100);
  image(flower, 400, 400);
}

Функція filter() надає багато фільтрів, які можна застосувати до зображення, використовуючи різні аргументи в поєднанні з додатковими параметрами:

  • THRESHOLD - перетворює зображення на чорно-білі пікселі залежно від того, чи є вони вище або нижче порогу, зазначеного параметром рівня. Параметр має бути від 0.0 (чорний) до 1.0 (білий). Якщо рівень не вказано, використовується 0.5.

  • GRAY - перетворює будь-які кольори зображення на еквіваленти градацій сірого. Жоден параметр не використовується.

  • OPAQUE - встановлює абсолютну непрозорість. Жоден параметр не використовується.

  • INVERT - встановлює для кожного пікселя його зворотне значення. Жоден параметр не використовується.

  • POSTERIZE - обмежує кожну складову зображення кількістю кольорів, указаних як параметр. Для параметра можна встановити значення від 2 до 255, але результати найбільш помітні в нижчих діапазонах.

  • BLUR - виконує розмиття за Гаусом із параметром рівня, що визначає ступінь розмиття. Якщо параметр не використовується, розмиття еквівалентне розмиттю за Гаусом радіуса 1. Більші значення збільшують розмиття.

  • ERODE - зменшує світлі ділянки. Жоден параметр не використовується.

  • DILATE - збільшує освітлені області. Жоден параметр не використовується.

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

sketch.js
let img1, img2, img3, img4, img5, img6;

function preload() {
  img1 = loadImage("flower.jpg");
  img2 = loadImage("flower.jpg");
  img3 = loadImage("flower.jpg");
  img4 = loadImage("flower.jpg");
  img5 = loadImage("flower.jpg");
  img6 = loadImage("flower.jpg");
}
function setup() {
  createCanvas(774, 378);
  noLoop();
}

function draw() {
  background(222, 231, 230); // Platinum

  image(img1, 0, 0); // без фільтрів

  img2.filter(GRAY); // сірий
  image(img2, 258, 0);

  img3.filter(THRESHOLD, 0.5); // чорно-білий
  image(img3, 516, 0);

  img4.filter(POSTERIZE, 3); // обмеження кількості кольорів
  image(img4, 0, 189);

  img5.filter(INVERT); // інверсія кольорів
  image(img5, 258, 189);

  img6.filter(BLUR, 2); // ефект розмиття
  image(img6, 516, 189);
}
Застосування до зображення фільтрів
Застосування до зображення фільтрів

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

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

Кіт
Кіт
Пес
Пес
Панда
Панда
Папуга
Папуга
Жирафа
Жирафа

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

sketch.js
let filenames = [ (1)
  "cat.jpg",
  "dog.jpg",
  "panda.jpg",
  "parrot.jpg",
  "giraffe.jpg",
];

let animals = []; (2)
let index = 0; (3)

function preload() {
  for (let f of filenames) {
    animals.push(loadImage(f)); (4)
  }
}

function setup() {
  createCanvas(500, 400);
}

function draw() {
  background(220);
  image(animals[index], 0, 0); (5)
}

function mousePressed() {
  index = (index + 1) % animals.length; (6)
}
1 Оголошуємо масив filenames із назвами графічних файлів, що містять зображення.
2 Оголошуємо порожній масив animals для зберігання об’єктів зображень тварин.
3 Початковий індекс об’єкта зображення.
4 Заповнюємо масив animals об’єктами зображень тварин, використовуючи циклічний прохід по масиву filenames, що містить назви файлів зображень.
5 Створюємо зображення над тлом полотна, звертаючись до об’єкта зображення у масиві animals за допомогою значення індексу index для поточного зображення.
6 Обчислюємо значення індексу для наступного зображення у функції mousePressed(), яка викликається один раз після кожного натискання кнопки миші.

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

Переглядаємо Аналізуємо

Використовуючи метод image() можна додати у застосунок не лише зображення із файлу, але й екземпляри полотна.

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

Розглянемо, як реалізуються два полотна в одному застосунку. У нашому прикладі буде відбуватися рух і відбивання від меж двох куль. Кулі матимуть власні характеристики та рухатимуться на окремих полотнах, «не знаючи» про існування одна одної.

sketch.js
let cnv;
let x, y, xCnv, yCnv;
let r, rCnv;
let xSpeed, ySpeed, xSpeedCnv, ySpeedCnv;

function setup() {
  createCanvas(200, 200); (1)
  x = width / 2;
  y = height / 2;
  r = 50;
  xSpeed = random(0.7);
  ySpeed = random(1);

  cnv = createGraphics(100, 100); (2)
  xCnv = cnv.width / 2;
  yCnv = cnv.height / 2;
  rCnv = 15;
  xSpeedCnv = random(0.8);
  ySpeedCnv = random(1.1);
}

function draw() {
  // основне полотно (3)
  background(220);
  noStroke();
  fill(255);
  ellipseMode(RADIUS);
  ellipse(x, y, r);

  if (x - r < 0 || x > width - r) {
    xSpeed = xSpeed * -1;
  }
  if (y - r < 0 || y > height - r) {
    ySpeed = ySpeed * -1;
  }

  x = x + xSpeed;
  y = y + ySpeed;

  // екземпляр нового полотна (4)
  cnv.background(0, 63, 136); // Dark Cornflower Blue
  cnv.noStroke();
  cnv.fill(255, 213, 0); // Gold Web Golden
  cnv.ellipseMode(RADIUS);
  cnv.ellipse(xCnv, yCnv, rCnv);

  if (xCnv - rCnv < 0 || xCnv > cnv.width - rCnv) {
    xSpeedCnv = xSpeedCnv * -1;
  }
  if (yCnv - rCnv < 0 || yCnv > cnv.height - rCnv) {
    ySpeedCnv = ySpeedCnv * -1;
  }

  xCnv = xCnv + xSpeedCnv;
  yCnv = yCnv + ySpeedCnv;

  image(cnv, width / 2, 0); (5)
}
1 Створюємо екземпляр основного полотна і визначаємо у змінних характеристики для першої кулі.
2 Створюємо екземпляр нового полотна cnv за допомогою createGraphics() і визначаємо у змінних характеристики для другої кулі.
3 Записуємо функції бібліотеки p5.js для першої кулі як зазвичай. Перша куля буде рухатися на основному полотні.
4 Записуємо функції бібліотеки p5.js для другої кулі за допомогою крапкової нотації для нового об’єкту полотна cnv.
5 Розміщуємо нове полотно cnv над основним полотном.
Як і у разі завантаження кількох зображень на полотно, екземпляри полотна утворюють багатошарову структуру, кожен шар якої існує окремо.

Переглядаємо Аналізуємо

6.3.2. Пікселі

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

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

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

Пікселі зображення при збільшенні
Пікселі зображення при збільшенні

Цю двовимірну таблицю можна розкласти в лінію у вигляді одномірного масиву.

Пікселі зображення при розкладанні в лінію
Пікселі зображення при розкладанні в лінію

З огляду на таке розкладання масиву пікселів, щоб знайти піксель з координатами x = 3, y = 1 двовимірного масиву (піксель з номером 10), потрібно обчислити вираз

\$i = x + y * p\$

в якому x - значення координати зліва направо, y - значення координати згори донизу, p - кількість пікселів зображення у ширину

\$i = 3 + 1 * 7 = 10\$

Для зберігання усіх пікселів зображення бібліотека p5.js використовує вбудований масив pixels .

Перш ніж отримати доступ до масиву pixels, дані про пікселі повинні бути завантажені у масив pixels за допомогою функції loadPixels() .

Якщо дані масиву будуть змінені, необхідно виконати функцію updatePixels() , щоб оновити зміни.

Поглянемо на розмір заповненого масиву pixels для розміру зображення полотна 200x200 пікселів.

sketch.js
function setup() {
  createCanvas(200, 200);
  noLoop();
}

function draw() {
  background(67, 19, 141); // Indigo
  loadPixels();
  console.log(pixels.length); // 160000
}

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

\$200 * 200 = 40000\$

Якщо поглянути на сам масив pixels, то він є масивом типу Uint8ClampedArray , а його вміст у нашому прикладі є таким:

Uint8ClampedArray {0: 67, 1: 19, 2: 141, 3: 255, 4: 67, ...}

Як бачимо, масив pixels містить четвірки чисел у порядку R, G, B, A для кожного пікселя, рухаючись зліва направо по кожному рядку, спускаючись донизу. З цього стає зрозуміло, чому наші розрахунки були некоректними. Загалом масив pixels є розміром ширина зображення у пікселях x висота зображення пікселях x 4.

Якщо використовувати ідею двовимірної таблиці, у якій координати пікселя позначаються (x, y) (x - значення координати зліва направо по горизонталі, y - значення координати згори донизу по вертикалі), перші чотири значення (індекси 0-3) у масиві pixels будуть значеннями R, G, B, A пікселя в точці (0, 0). Другі чотири значення (індекси 4-7) міститимуть значення R, G, B, A пікселя в точці (1, 0) і т. д.

Пікселі зображення в контексті двовимірної таблиці
Пікселі зображення в контексті двовимірної таблиці
Пікселі зображення в контексті одновимірного масиву
Пікселі зображення в контексті одновимірного масиву

Загалом, щоб встановити значення для пікселя (x, y), потрібно пройтися по масиву pixels як по двовимірному масиву і використати вищезгадану формулу розрахунку номера пікселя i, враховуючи, що для кожного пікселя у масиві pixels відводиться чотири значення: червоної, зеленої та синьої складових кольору й прозорості.

sketch.js
function setup() {
  createCanvas(200, 200);
  noLoop();
}

function draw() {
  background(67, 19, 141); // Indigo
  loadPixels();
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let i = (x + y * width) * 4; (1)
      pixels[i] = 0; (2)
      pixels[i + 1] = 100; (3)
      pixels[i + 2] = 0; (4)
      pixels[i + 3] = 255; (5)
    }
  }
  updatePixels();
}
При навігації по звичайному двовимірному масиві зовнішній цикл проходить по рядках, а внутрішній - по стовпцях. У цьому разі координати конкретного елемента масиву (x, y), де x - індекс рядка, y - індекс стовпця. Для масиву пікселів зовнішній цикл проходить по стовпцях, а внутрішній - по рядках. Позначення координат пікселя (x, y) залишається таким самим, але самі значення стають перевернутими, оскільки x - індекс стовпця, y - індекс рядка.
1 Обчислюємо індекс i пікселя у представленні двовимірного масиву.
2 Присвоюємо значення червоній складовій кольору пікселя з індексом i.
3 Присвоюємо значення зеленій складовій кольору пікселя з індексом i.
4 Присвоюємо значення синій складовій кольору пікселя з індексом i.
5 Присвоюємо значення для прозорості пікселя з індексом i.

У результаті виконання застосунку кожен піксель полотна застосунку буде світитися зеленим кольором.

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

Вправа 57

Створити застосунок, в якому кожен піксель полотна світиться одним кольором та є наполовину прозорим.

Цікавимось Додатково

Двовимірний шум Перліна

У розділі Шум Перліна була створена візуалізація одновимірного шуму Перліна у формі графіка функції, що нагадував гірський хребет.

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

У цьому разі функція noise() буде приймати як аргументи пару координат.

sketch.js
let i = 0.015;

function setup() {
  createCanvas(200, 200);
  noFill();
}

function draw() {
  let yoff = 0; (1)
  loadPixels();
  for (let y = 0; y < height; y++) {
    let xoff = 0; (2)
    for (let x = 0; x < width; x++) {
      let index = (x + y * width) * 4;
      let r = noise(xoff, yoff) * 50; (3)
      let g = noise(xoff, yoff) * 150;
      let b = noise(xoff, yoff) * 200;
      let a = noise(xoff, yoff) * 255;
      pixels[index + 0] = r;
      pixels[index + 1] = g;
      pixels[index + 2] = b;
      pixels[index + 3] = a;
      xoff += i; (4)
    }
    yoff += i; (5)
  }
  updatePixels();
}
1 Ініціалізуємо змінну з ім’ям yoff, значення якої буде передаватися у функцію noise() як друга координата.
2 Ініціалізуємо змінну з ім’ям xoff, значення якої буде передаватися у функцію noise() як перша координата.
3 Виклики функції noise() для кожної колірної складової конкретного пікселя повернуть псевдовипадкові значення шуму Перліна, помножені на цілочисельні значення, менші ніж 256.
4 Збільшуємо значення xoff на величину i, щоб змінити псевдовипадкове значення шуму Перліна на наступній ітерації виконання внутрішнього циклу for.
5 Збільшуємо значення yoff на i, щоб змінити псевдовипадкове значення шуму Перліна на наступній ітерації виконання зовнішнього циклу for.

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

Хмари в небі
Хмари в небі

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

Для наших цілей використаємо зображення (750x501 пікселів) картини Недільне післяобіддя на острові Гранд Жатт Жоржа Сера (1884-1886).

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

sketch.js
let img;
let sizePoint = 10; // розмір крапок
let a = 200; // прозорість крапок
let x, y, i; // координати та індекс поточного пікселя зображення

function preload() {
  img = loadImage("Georges_Seurat_Sunday_afternoon_on_the_island_of_La_Grande_Jatte_before.jpg"); (1)
}

function setup() {
  createCanvas(750, 501);
  background(0);
  noStroke();
  img.loadPixels(); (2)
}

function draw() {
  x = int(random(img.width)); (3)
  y = int(random(img.height));

  i = (x + y * img.width) * 4; (4)

  let r = img.pixels[i]; (5)
  let g = img.pixels[i + 1];
  let b = img.pixels[i + 2];

  fill(r, g, b, a); (6)
  ellipse(x, y, sizePoint, sizePoint);
}

function mousePressed() { (7)
  saveCanvas("myCanvas" + frameCount, "png");
}
1 Завантажуємо у застосунок зображення. img - ім’я, за яким можна звернутися до зображення. Розмір полотна відповідає розміру самого зображення.
2 Заповнюємо масив pixels значеннями пікселів завантаженого зображення img.
3 Випадково обчислюємо значення x-координати та y-координати пікселя зображення img.
4 Обчислюємо індекс i пікселя на основі координат, ширини завантаженого зображення img і знань про розміщення значень колірних складових пікселя у масиві pixels.
5 Використовуючи індекс i пікселя отримуємо доступ до значень r, g і b колірних складових пікселя у масиві pixels
6 Малюємо на полотні коло на основі отриманих значень r, g і b та наперед визначеної прозорості a та встановленого розміру sizePoint.
7 За допомогою функції mousePressed(), яка викликається завдяки натисканню кнопки миші, зберігаємо вигляд полотна на цей момент у файл з ім’ям myCanvasN.png, де N - номер кадру, за допомогою функції saveCanvas() .

Результат виконання коду застосунку - зображення в стилі пуантилізму, створене за допомогою крапок. Крапки відображаються по черзі, тобто, у кожному виклику draw() створюється одна крапка, положення якої є випадковим.

Техніка пуантилізму
Техніка пуантилізму

Ще один спосіб отримати доступ до окремого пікселя зображення - застосування до зображення методу get() .

Особливості роботи цього методу наступні:

  • якщо метод get() викликається для зображення без параметрів - отримуємо усе зображення;

  • якщо x і y є єдиними переданими у метод параметрами, отримуємо один піксель, який розташований у зазначених координатах;

  • якщо передано усі параметри (x, y, w, h), отримуємо із зображення прямокутну область пікселів шириною w і висотою h.

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

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

sketch.js
let img;

function preload() {
  img = loadImage("https://picsum.photos/200/300/?random");
}

function setup() {
  createCanvas(img.width, img.height);
}

function draw() {
  background(255);
  image(img, 0, 0);
  let p = img.get(mouseX, mouseY);
  console.log(
    `red: ${red(p)}, green: ${green(p)}, blue: ${blue(p)}, alpha: ${alpha(p)}`
  );
  console.log(`red: ${p[0]}, green: ${p[1]}, blue: ${p[2]}, alpha: ${p[3]}`);
}

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

red: 90, green: 82, blue: 71, alpha: 255

Як видно з коду, для виведення у консоль значень колірних характеристик пікселів використовується два способи: перший - за допомогою функцій, які застосовуються до пікселя і повертають значення червоної, зеленої та синьої складових й прозорості, другий - звертаючись до пікселя як до елемента, що містить значення r, g, b і a, масиву pixels.

У застосунку використовуються випадкові зображення із сайту Lorem Picsum - аналог класичного варіанту беззмістовного тексту Lorem Ipsum , але для зображень.

6.3.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Назвати особливості растрових зображень.

  2. Описати алгоритм візуалізації растрового зображення на полотні застосунку.

  3. Які ефекти можна застосувати до зображень, використовуючи засоби бібліотеки p5.js?

  4. Яка структура масиву pixels?

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

6.3.5. Практичні завдання

Початковий

  1. Завантажити в ескіз зображення на вибір і застосувати до зображення фільтри.

  1. Використати код застосунку для створення на полотні колірних градієнтів - плавних переходів від одного кольору до іншого. Один із можливих варіантів колірного градієнта представлений на малюнку.

Колірний градієнт
Колірний градієнт
sketch.js
function setup() {
  createCanvas(200, 200);
  noLoop();
}

function draw() {
  background(48, 150, 138); // Persian Green
  loadPixels();
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let i = (x + y * width) * 4;
      pixels[i] = 0;
      pixels[i + 1] = 100;
      pixels[i + 2] = 0;
      pixels[i + 3] = 255;
    }
  }
  updatePixels();
}

Середній

  1. Створити на полотні піксельну мозаїку як на малюнку.

Піксельна мозаїка
Піксельна мозаїка
  1. Створити на полотні чорно-білу піксельну мозаїку як на малюнку.

Піксельна мозаїка
Піксельна мозаїка
  1. Завантажити в ескіз зображення на вибір. Зробити копію зображення і повернути її по горизонталі. Як зразок результату роботи застосунку, на малюнку представлена картина Ньютон в Саду Ідей українського художника Олега Шупляка .

Олег Шупляк, український художник, Ньютон в Саду Ідей
Картина Ньютон в Саду Ідей українського художника Олега Шупляка: ліворуч - перевернуте по горизонталі, праворуч - оригінальне зображення
Для створення потрібного ефекту використайте функцію scale() бібліотеки p5.js.

Високий

  1. Створити застосунок із застосуванням фільтрів до фрагментів зображення. Щоб працювати з окремими фрагментами одного зображення, розділіть зображення на однакові частини, наприклад, скориставшись сервісом pinetools.com . Орієнтовний взірець роботи застосунку представлений в демонстрації. Файли фрагментів використаного в ескізі зображення можна завантажити за покликанням.

Екстремальний

  1. Створити застосунок з кількома екземплярами полотна для реалізації ефекту як у демонстрації.

  1. Створити застосунок, який генерує випадковий колаж з об’єктів світлин. При натисканні миші на фото зображення зникає, а на його місці залишається напівпрозора прямокутна рамка. Її створення відбувається завдяки зміні масиву пікселів конкретного зображення. Орієнтовний зразок роботи застосунку представлений в демонстрації. У застосунку використані світлини зірок світової кіноіндустрії.

6.4. Відео як цілісний об’єкт та як масив зображень. Анімації

6.4.1. Анімація

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

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

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

Для нашого прикладу використаємо зображення тла комп’ютерної гри Super Mario Bros.

Тло зі світу гри Super Mario Bros.
Тло зі світу гри Super Mario Bros.

і три зображення із різними положеннями персонажа гри Маріо

Персонаж Маріо гри Super Mario Bros.
Персонаж Маріо гри Super Mario Bros.: перша фаза руху
Персонаж Маріо гри Super Mario Bros.
Персонаж Маріо гри Super Mario Bros.: друга фаза руху
Персонаж Маріо гри Super Mario Bros.
Персонаж Маріо гри Super Mario Bros.: третя фаза руху

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

sketch.js
let nFrames = ["mario1", "mario2", "mario3"]; (1)
let marioImages = []; (2)
let bg; (3)

let isAnimate = true; (4)
let currentFrame = 0; (5)
let x = 0; (6)

function preload() { (7)
  bg = loadImage("./mario/mariobg.png");
  for (let f of nFrames) {
    marioImages.push(loadImage(`./mario/${f}.png`));
  }
}

function setup() {
  createCanvas(bg.width, bg.height);
  frameRate(25); (8)
}

function draw() {
  fill(255); // White
  image(bg, 0, 0); (9)
  text(`${frameCount} / ${currentFrame + 1}`, 10, 20); (10)
  noTint(); (14)
  if (isAnimate) { (11)
    // зміна кадрів
    image(marioImages[currentFrame], x, bg.height - 130);
    currentFrame += 1;
    if (currentFrame > marioImages.length - 1) {
      currentFrame = 0;
    }
    // рух по горизонталі (13)
    x = x + 4;
    if (x > bg.width - 175) {
      tint(255, 128);
      x = -60;
    }
  } else { (12)
    image(marioImages[currentFrame], x, bg.height - 130);
  }
}

function keyPressed() { (15)
  isAnimate = !isAnimate;
}
1 Оголошуємо масив nFrames, елементами якого є імена графічних файлів, які завантажені у створений каталог mario застосунку.
2 Оголошуємо порожній масив marioImages, який зберігатиме об’єкти зображень, завантажених із графічних файлів.
3 Змінна bg буде містити зображення тла гри.
4 Змінна isAnimate буде містити значення про те, чи відбувається на цей момент анімація, чи ні.
5 Змінна currentFrame міститиме значення номера зображення (кадру) анімації.
6 Змінна x буде містити значення x-координати на полотні зображення з масиву marioImages. Водночас з анімацією персонажа він буде рухатися горизонтально.
7 Усередині функції preload() завантажуємо зображення із каталогу mario в ескіз за допомогою функції loadImage() і поміщаємо об’єкти зображень персонажу у масив marioImages, а зображення тла - у bg.
8 За допомогою функції frameRate() уповільнюємо виконання функції draw() до 25 разів в секунду.
9 За допомогою функції image() розміщуємо зображення тла гри на полотні.
10 Виводимо у лівому верхньому куті полотна значення frameCount, яке містить кількість викликів функції draw(), і номер зображення (кадру) анімації, розпочавши відлік з одиниці.
11 Якщо isAnimate містить істинне значення true, за допомогою функції image() розміщуємо у зазначених координатах на полотні зображення з масиву marioImages, яке має індекс currentFrame. Збільшуємо значення currentFrame, щоб розмістити наступне зображення. Це відбувається доти, доки номер currentFrame поточного зображення є на одиницю меншим довжини масиву marioImages. Як тільки значення currentFrame стане більшим за індекс останнього зображення, то відлік розпочнеться спочатку, з нуля. У такий спосіб відбувається зациклення появи зображень персонажу на полотні.
12 Якщо isAnimate набуває значення false, то анімація зупиняється. На полотні візуалізується зображення з індексом currentFrame і це буде наступне зображення у послідовності. Рахунок кадрів виклику функції draw() продовжується.
13 Анімація персонажу рухається горизонтально завдяки зміні значення x-координати. Як тільки анімація персонажу досягає зеленої труби, до зображень персонажу застосовується функція tint(), яка робить анімовані зображення напівпрозорими. Так досягається ефект входу персонажа у трубу. Після цього анімація отримує від’ємне значення x-координати й тому з’являється поза лівою межею полотна. Усі вищенаведені дії повторюється знову.
14 Використовуємо функцію noTint() для повернення зображень персонажу до оригінальних відтінків.
15 Функція keyPressed() викликається щоразу, коли натискається будь-яка клавіша на клавіатурі. Вона керує зупинкою та продовженням анімації, перемикаючи значення isAnimate на протилежне.

Переглядаємо Аналізуємо

Тепер виконаємо зворотний процес - перетворимо виконання застосунку (покадрова анімація персонажа гри і його горизонтальний рух) на послідовність окремих зображень (кадрів) за допомогою функції saveFrames() .

Функцію saveFrames() помістимо у функцію mousePressed(), а це значить, що зберігати кадри застосунку будемо за натисканням миші.

Застосувати функцію saveFrames() можна двома способами.

Перший спосіб - завантаження на комп’ютер зображень (кадрів) як окремих файлів за допомогою діалогового вікна вебпереглядача.

function mousePressed() {
  saveFrames(`mario-world-${frameCount}`, "png", 2, 3);
}

Перший аргумент функції saveFrames() - це назва графічного файлу. Для нашого ескізу назви будуть на зразок mario-world-60.png, mario-world-61.png, mario-world-62.png і т. д.

У формуванні імені графічного файлу використовується системна змінна frameCount, яка містить кількість кадрів, що були відображені з моменту запуску застосунку. Всередині функції setup() значення frameCount дорівнює 0, після першого виклику draw() значення frameCount дорівнює 1.

Другий аргумент зазначає розширення графічного файлу (png або jpg). Третій аргумент - це тривалість в секундах для збереження кадрів, а четвертий - кількість кадрів за секунду.

У підсумку, вебпереглядач запропонує зберегти 6 зображень (2 * 3 = 6).

Значення тривалості і кількості кадрів повинні бути менше або дорівнювати 15 і 22 відповідно, що означає, що можна завантажувати максимум кадри тривалістю 15 секунд зі швидкістю 22 кадри на секунду, додаючи до 330 кадрів. Це робиться для того, щоб уникнути проблем з пам’яттю, оскільки досить велике полотно може дуже легко заповнити пам’ять комп’ютера та призвести до збою застосунку або вебпереглядача.

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

function mousePressed() {
  saveFrames(`mario-world-${frameCount}`, "png", 2, 5, data => {
    print(data);
  });
}

У цьому разі розмір масиву data дорівнює загальній кількості кадрів - 10 (2 * 5 = 10)

Вправа 58

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

Реалізуємо ще один приклад анімації - ефект переходу - плавної зміни прозорості двох зображень на полотні.

Спочатку перше зображення візуалізується прозорим, а друге - непрозорим. При переміщенні вказівника миші по екрану зліва направо рівень прозорості першого зображення зменшується, а другого, відповідно, збільшується. У разі руху у зворотному напрямку має спостерігатися зворотний ефект.

Для наших цілей використаємо зображення роботів Boston Dynamics, що танцюють, та зображення картини Танець в Остерії Вільгельма Марстранда (1860 рік).

Для створення ефекту переходу у коді застосунку використаємо механізм розрахунку значення x-координати вказівника миші у діапазоні значень прозорості від 0 до 255.

sketch.js
let img1, img2;

function preload() {
  img1 = loadImage("Wilhelm_Marstrand_Dance_in_an_Osteria.jpg");
  img2 = loadImage("Boston_Dynamics_dance.png");
}

function setup() {
  createCanvas(600, 480);
  background(220);
}

function draw() {
  let t1 = map(mouseX, 0, img1.width, 0, 255); (1)
  let t2 = map(mouseX, 0, img2.width, 255, 0);
  tint(255, t1); (2)
  image(img1, 0, 0);
  tint(255, t2);
  image(img2, 0, 0);
}

Розглянемо особливості коду:

1 По горизонталі вказівник миші змінює свою x-координату від 0 до значення ширини зображення. Щоб значення x-координати перетворити у значення діапазону від 0 до 255, використовуємо функцію map() , яка отримує п’ять значень. Перший аргумент - значення x-координати вказівника миші, яке необхідно перевести. Другий і третій аргументи - це значення меж діапазону з якого треба перевести, тобто вхідні значення - 0 і ширина зображення. Четвертий і п’ятий аргументи - це значення меж діапазону, у який необхідно перевести, відповідно від 0 до 255 і від 255 до 0. Результати розрахунків зберігаємо під іменами t1 і t2.
2 Використовуємо функцію tint() , яка робить обидва зображення прозорими на величини t1 і t2 відповідно.

Переглядаємо Аналізуємо

6.4.2. Відео

Відео можна розглядати як масив нерухомих зображень (кадрів), які послідовно змінюються одне за одним, створюючи ефект руху об’єктів на екрані. Чим більша частота кадрів (кількість кадрів за секунду), тим плавнішим і природнішим буде здаватися рух.

Мінімальне значення, за якого рух буде сприйматися однорідним - приблизно 10 кадрів за секунду (це значення індивідуальне для кожної людини). У традиційному плівковому кінематографі використовується частота 24 кадри за секунду, у телебаченні використовують від 25 кадрів до 30 кадрів за секунду, а комп’ютерні зацифровані відеоматеріали гарної якості, як правило, використовують частоту 30 кадрів за секунду.

Використаємо бібліотеку p5.js для створення застосунків, в яких відтворюється відео. Як зразок відео візьмемо відео з паркуром роботів Boston Dynamics.

Щоб розмістити на вебсторінці застосунку відео, використаємо функцію createVideo() , яка у DOM вебсторінки застосунку створює HTML-елемент <video> для простого відтворення аудіо/відео.

Робота із DOM детально розглядається в Додатку B.

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

Для конвертації відео в інші формати використовуйте онлайн-сервіси.
Виклики функцій createVideo(), createAudio() , createCapture() створюють відповідні об’єкти на основі класу p5.MediaElement , який розширює клас p5.Element - базовий клас для усіх елементів, доданих до ескізу, включаючи полотно та HTML-елементи - в частині обробки аудіо та відео.

Використаємо кілька методів для керування опціями відтворення відео, а саму функцію createVideo() розмістимо всередині функції preload() , яка викликається безпосередньо перед setup() і використовується для обробки асинхронного завантаження зовнішніх файлів - у цьому разі для завантаження відео.

sketch.js
let atlas; (1)

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height); (2)
  atlas.speed(1); (3)
  atlas.volume(0.1); (4)
  atlas.showControls(); (5)
  atlas.noLoop();  (6)
}

function draw() {
  background(94, 144, 114); // Viridian
  image(atlas, 0, 0, width, height); (7)
  fill(255);
  text(atlas.time(), width - 70, 20); (8)
  text(atlas.duration(), width - 70, 40); (9)
}
1 Оголошуємо змінну atlas, яка буде посилатися на об’єкт завантаженого відео.
2 Встановлюємо ширину і висоту HTML-елемента <video> на вебсторінці - розміри вікна програвача відео. Вікно програвача відео розміщується на вебсторінці поруч із полотном ескізу.
3 Встановлюємо швидкість відтворення відео: 2.0 - у два рази швидше, 1.0 - стандартно, 0.5 - у два рази повільніше.
4 Встановлюємо гучність відтворення звуку у діапазоні від 0.0 до 1.0.
5 Відображаємо у програвачі стандартні елементи керування відтворенням відео.
6 Опція зупинки відтворення відео після досягнення кінця.
7 Розміщення копії вікна програвача відео на полотні.
8 Відображення на полотні у правому верхньому куті позначки часу в секундах від початку відтворення відео.
9 Відображення на полотні у правому верхньому куті позначки часу в секундах тривалості відтворення відео.

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

Вікно програвача на вебсторінці у разі потреби можна приховати. Для цього необхідно використати метод hide() (по суті, це CSS-правило display:none) для приховування HTML-елементів: atlas.hide();. Водночас вікно відтворення відео на полотні також буде схованим, а відображатимуться полотно і позначки часу.

Приховування елементів на вебсторінці
Приховування елементів на вебсторінці: використання методу hide()

Щоб відео усе-таки відтворити на полотні, потрібно скористатися методом atlas.play();, який відтворює медіаелементи.

sketch.js
let atlas;

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  atlas.hide(); (1)
  atlas.play(); (2)
  atlas.showControls();
  atlas.noLoop();
}

function draw() {
  background(94, 144, 114); // Viridian
  image(atlas, 0, 0, width, height);
  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}
1 Приховування вікна програвача відео.
2 Відтворювати відео як тільки запустити застосунок.
З іншими методами для медіаелементів можна ознайомитися на сторінці p5.MediaElement довідки по функціях бібліотеки p5.js.

Цікавимось

Захоплення відео з вебкамери

Якщо необхідно отримати наживо відео з вебкамери, можна скористатися функцією createCapture() , яка створює HTML-елемент <video>, що містить аудіо/відео із вебкамери.

sketch.js
let capture;

function setup() {
  createCanvas(400, 300);
  capture = createCapture(VIDEO);
  capture.hide();
}

function draw() {
  background(94, 144, 114); // Viridian
  image(capture, 0, 0, width / 2, height / 2);
}
У разі потреби необхідно надати вебпереглядачу доступ до вебкамери.

Вправа 59

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

Захоплення відео з вебкамери
Захоплення відео з вебкамери: чотири вікна трансляції наживо на полотні

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

sketch.js
let atlas;
let isPlaying = false; (1)
let btnPlay; (2)

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  atlas.hide();
  btnPlay = createButton("Відтворити"); (3)
  btnPlay.mousePressed(btnPlayClick); (4)
}

function draw() {
  background(94, 144, 114); // Viridian
  image(atlas, 0, 0, width, height);
  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}

function btnPlayClick() { (5)
  if (isPlaying) {
    atlas.pause();
    btnPlay.html("Відтворити");
  } else {
    atlas.play();
    btnPlay.html("Пауза");
  }
  if (atlas.duration() === atlas.time()) {
    atlas.stop();
    btnPlay.html("Відтворити");
  }
  isPlaying = !isPlaying;
}
1 Ініціалізуємо змінну isPlaying із логічним значеннням false, яка сигналізуватиме, чи відтворюється відео (true) чи ні (false). При запуску застосунку відео відразу відтворюватися не буде.
2 Оголошуємо змінну з ім’ям btnPlay, що буде покликатися на об’єкт кнопки.
3 Створюємо кнопку з написом Відтворити, використовуючи функцію createButton() і зберігаємо покликання на об’єкт кнопки з ім’ям btnPlay.
4 Застосовуємо на кнопці btnPlay метод mousePressed() , щоб визначити поведінку під час натискання кнопки btnPlay. Метод mousePressed() викликається один раз після кожного натискання кнопки миші на кнопці btnPlay і є слухачем події натискання на кнопку btnPlay. Аргумент btnPlayClick методу mousePressed() є обробником події натискання на кнопку btnPlay.
5 Функція btnPlayClick() є обробником події натискання на кнопку btnPlay.
В оголошенні функції btnPlayClick() після її назви записуються круглі дужки відповідно до правил оголошення функцій у JavaScript, а у методі-слухачі mousePressed() як аргумент записуємо лише назву функції-обробника події, тобто btnPlayClick.

Після запуску застосунку на вебсторінці ескізу на полотні з’явиться вікно з відео, а під ним - кнопка з написом Відтворити. Після натискання на кнопку Відтворити, розпочнеться відтворення відео.

Відео розпочне відтворюватися завдяки тому, що змінна isPlaying має значення false і тому в тілі функції btnPlayClick() спрацює гілка else вказівки розгалуження, в якій до об’єкта відео atlas буде застосований метод play(), який, власне, і запускає відтворення. Також на кнопці зміниться напис на Пауза завдяки методу html(), який буде застосований до об’єкта кнопки btnPlay, а змінна isPlaying набуде значення true.

На момент наступного натискання на кнопку вже з написом Пауза змінна isPlaying має значення true і тому у тілі функції btnPlayClick() спрацює гілка вказівки розгалуження if, в якій до об’єкту відео atlas буде застосований метод pause(), що зупинить відтворення відео. Відповідно напис за допомогою методу html() знову зміниться на Відтворити, а змінна isPlaying набуде значення false.

Коли відео завершить своє відтворення, кнопка матиме напис Пауза. Якщо ж після цього натиснути на кнопку, то у функції-обробнику btnPlayClick() відбудеться перевірка умови atlas.duration() === atlas.time() (час тривалості відео дорівнює позначці часу відтворення відео), яка поверне результат true, і до об’єкта відео atlas буде застосований метод stop(), що повертає нас на перший кадр відео, а метод html() змінить напис на кнопці на Відтворити.

Вправа 60

Додати до застосунку кнопку зупинки відтворення відео.

Додамо до нашого застосунку ще одну кнопку btnSnap для створення знімків-зображень із відео.

Спочатку в коді застосунку у draw() закоментуємо рядки з функціями background() та image(), щоб знімки не перекривались об’єктами, що створюються цими функціями. А в setup() закоментуємо рядок з методом hide(), щоб відобразити вікно програвача відео поруч з полотном.

sketch.js
let atlas;
let isPlaying = false;
let btnPlay;
let btnSnap; (1)

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  // atlas.hide();
  btnPlay = createButton("Відтворити");
  btnPlay.mousePressed(btnPlayClick);
  btnSnap = createButton("Знімок"); (2)
  btnSnap.mousePressed(btnSnapClick); (3)
}

function draw() {
  // background(94, 144, 114); // Viridian
  // image(atlas, 0, 0, width, height);
  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}

function btnPlayClick() {
  if (isPlaying) {
    atlas.pause();
    btnPlay.html("Відтворити");
  } else {
    atlas.play();
    btnPlay.html("Пауза");
  }
  if (atlas.duration() === atlas.time()) {
    atlas.stop();
    btnPlay.html("Відтворити");
  }
  isPlaying = !isPlaying;
}

function btnSnapClick() { (4)
  image(atlas.get(), 0, 0, width / 2, height / 2);
}
1 Оголошуємо змінну з ім’ям btnSnap, що буде покликатися на об’єкт кнопки.
2 Створюємо кнопку з написом Знімок, використовуючи функцію createButton() і зберігаємо покликання на об’єкт кнопки з ім’ям btnSnap.
3 Застосовуємо на кнопці btnSnap метод mousePressed() , щоб визначити поведінку під час натискання кнопки btnSnap. Метод mousePressed() викликається один раз після кожного натискання кнопки миші на кнопці btnSnap і є, як було вже зазначено вище, слухачем події натискання на кнопку btnSnap. Аргумент btnSnapClick методу mousePressed() є обробником події натискання на кнопку btnSnap.
4 Функція btnSnapClick() є обробником події натискання на кнопку btnSnap. У тілі функції btnSnapClick() використовуємо метод get() (якщо метод викликається без параметрів - отримуємо усе зображення; якщо x і y є єдиними переданими параметрами, отримуємо один піксель; якщо передано усі параметри, отримуємо прямокутну область пікселів із зображення) на об’єкті відео atlas для створення миттєвого знімка вікна відтворення відео і функцію image() для розміщення знімка-зображення на полотні у зазначених координатах.

Розглянемо випадок, коли нам необхідно зберігати багато знімків екрана вікна відтворення відео та у разі потреби звертатися до будь-якого з них. Отже, для цих цілей використаємо масив з ім’ям snaps і внесемо зміни у код застосунку.

sketch.js
let atlas;
let isPlaying = false;
let btnPlay;
let btnSnap;
let snaps = []; (1)

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  atlas.hide();
  btnPlay = createButton("Відтворити");
  btnPlay.mousePressed(btnPlayClick);
  btnSnap = createButton("Знімок");
  btnSnap.mousePressed(btnSnapClick);
}

function draw() {
  background(94, 144, 114); // Viridian
  image(atlas, 0, 0, width, height);
  image(snaps[0] || atlas, 0, 0, width / 2, height / 2); (2)
  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}

function btnPlayClick() {
  if (isPlaying) {
    atlas.pause();
    btnPlay.html("Відтворити");
  } else {
    atlas.play();
    btnPlay.html("Пауза");
  }
  if (atlas.duration() === atlas.time()) {
    atlas.stop();
    btnPlay.html("Відтворити");
  }
  isPlaying = !isPlaying;
}

function btnSnapClick() {
  snaps.push(atlas.get()); (3)
}
1 Оголошуємо змінну з ім’ям snaps, що буде покликанням на масив зроблених знімків.
2 Використовуємо функцію image() для розміщення на полотні зробленого знімка-зображення у два рази меншого розміру за вікно відтворення відео. Логічний вираз snaps[0] || atlas можна інтерпретувати так: якщо у масиві є перший знімок, то він як аргумент потрапляє у функцію image, яка розміщує цей знімок на полотні, якщо ж значення першого елемента масиву snaps[0] дорівнює undefined (першого знімку ще робили), то у функцію image потрапляє об’єкт atlas, і на місці знімка відтворюється відео. Якщо перший знімок зроблено, то лише він буде демонструватися на полотні незалежно від кількості зроблених знімків.
3 Додаємо знімок-зображення у масив snaps щоразу при натисканні кнопки Знімок.

Переглядаємо Аналізуємо

Змінимо код застосунку, щоб на полотні відображався останній зроблений знімок. Для цьго у draw() запишемо цикл for, який буде переглядати усі зроблені знімки від першого до останнього і відображати саме останній.

sketch.js
let atlas;
let isPlaying = false;
let btnPlay;
let btnSnap;
let snaps = [];

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  atlas.hide();
  btnPlay = createButton("Відтворити");
  btnPlay.mousePressed(btnPlayClick);
  btnSnap = createButton("Знімок");
  btnSnap.mousePressed(btnSnapClick);
}

function draw() {
  background(94, 144, 114); // Viridian
  image(atlas, 0, 0, width, height);
  for (let i = 0; i < snaps.length; i++) {
    image(snaps[i], 0, 0, width / 2, height / 2);
  }
  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}

function btnPlayClick() {
  if (isPlaying) {
    atlas.pause();
    btnPlay.html("Відтворити");
  } else {
    atlas.play();
    btnPlay.html("Пауза");
  }
  if (atlas.duration() === atlas.time()) {
    atlas.stop();
    btnPlay.html("Відтворити");
  }
  isPlaying = !isPlaying;
}

function btnSnapClick() {
  snaps.push(atlas.get());
}

Переглядаємо Аналізуємо

У підсумку розташуємо зроблені знімки в окремих віконцях на полотні.

sketch.js
let atlas;
let isPlaying = false;
let btnPlay;
let btnSnap;
let snaps = [];

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  atlas.hide();
  btnPlay = createButton("Відтворити");
  btnPlay.mousePressed(btnPlayClick);
  btnSnap = createButton("Знімок");
  btnSnap.mousePressed(btnSnapClick);
}

function draw() {
  background(94, 144, 114); // Viridian
  image(atlas, 0, 0, width, height);

  let x = 0; (1)
  let y = 0;
  let w = width / 10;
  let h = height / 10;

  for (let i = 0; i < snaps.length; i++) { (2)
    image(snaps[i], x, y, w, h);
    x = x + w;
    if (x >= width) {
      x = 0;
      y = y + h;
    }
  }

  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}

function btnPlayClick() {
  if (isPlaying) {
    atlas.pause();
    btnPlay.html("Відтворити");
  } else {
    atlas.play();
    btnPlay.html("Пауза");
  }
  if (atlas.duration() === atlas.time()) {
    atlas.stop();
    btnPlay.html("Відтворити");
  }
  isPlaying = !isPlaying;
}

function btnSnapClick() {
  snaps.push(atlas.get());
}
1 У функції draw() щоразу ініціалізуємо змінні x, y, w, h. Значення, на які будуть покликатися ці змінні будуть визначати положення і розміри зображень на полотні.
2 За допомогою циклу for переглядаємо у масиві snaps усі його елементи - знімки-зображення, зроблені за допомогою кнопки Знімок. Використовуючи функцію image() розташовуємо знімки горизонтально у рядок, змінюючи x на ширину w зображеня. Коли x набуває значення ширини полотна або більше, присвоюємо x значення 0, щоб зображення розташовувались із початку рядка, а значення y збільшуємо на висоту зображення, щоб переміститись нижче у новий рядок.

Переглядаємо Аналізуємо

Вправа 61

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

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

Отож, реалізуємо відтворення відео в інших барвах в порівнянні з оригінальним відео.

sketch.js
let atlas;
let isPlaying = false;
let btnPlay;
let btnSnap;
let snaps = [];

function preload() {
  atlas = createVideo("Atlas_Parkour.mp4");
}

function setup() {
  createCanvas(640, 360);
  atlas.size(width, height);
  atlas.speed(1);
  atlas.volume(0.1);
  atlas.hide();

  btnPlay = createButton("Відтворити");
  btnPlay.mousePressed(btnPlayClick);

  btnSnap = createButton("Знімок");
  btnSnap.mousePressed(btnSnapClick);
}

function draw() {
  background(94, 144, 114); // Viridian

  atlas.loadPixels(); (1)
  for (let y = 0; y < atlas.height; y++) {
    for (let x = 0; x < atlas.width; x++) {
      const index = (x + y * atlas.width) * 4; (2)
      const r = atlas.pixels[index]; (3)
      const g = atlas.pixels[index + 1];
      const b = atlas.pixels[index + 2];
      const a = atlas.pixels[index + 3];

      atlas.pixels[index + 0] = r; (4)
      atlas.pixels[index + 1] = b;
      atlas.pixels[index + 2] = r;
      atlas.pixels[index + 3] = a;
    }
  }
  atlas.updatePixels(); (5)
  image(atlas, 0, 0, width, height);

  let x = 0;
  let y = 0;
  let w = width / 2;  // нові розміри для знімків-зображень
  let h = height / 2;

  for (let i = 0; i < snaps.length; i++) {
    image(snaps[i], x, y, w, h);
    x = x + w;
    if (x >= width) {
      x = 0;
      y = y + h;
    }
  }

  fill(255);
  text(atlas.time(), width - 70, 20);
  text(atlas.duration(), width - 70, 40);
}

function btnPlayClick() {
  if (isPlaying) {
    atlas.pause();
    btnPlay.html("Відтворити");
  } else {
    atlas.play();
    btnPlay.html("Пауза");
  }
  if (atlas.duration() === atlas.time()) {
    atlas.stop();
    btnPlay.html("Відтворити");
  }
  isPlaying = !isPlaying;
}

function btnSnapClick() {
  snaps.push(atlas.get());
}

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

1 Для зберігання усіх пікселів зображення бібліотека p5.js використовує вбудований масив pixels . Перш ніж отримати доступ до масиву pixels, дані про пікселі повинні бути завантажені у масив pixels за допомогою функції loadPixels() .
2 Масив pixels містить четвірки чисел у порядку R, G, B, A для кожного пікселя. Для роботи з пікселями безпосередньо на екрані, використовують ідею подання пікселів, за якої пікселі мають положення (x, y) у двовимірному просторі. Однак, пікселі масиву pixels мають лише один вимір, зберігаючи значення кольорів у лінійній послідовності. Тому використовуємо вираз index = (x + y * atlas.width) * 4 для обчислення індексу index пікселя у представленні двовимірного масиву.
3 Щоразу ініціалізуємо змінні r, g, b, a зі значеннями четвірки чисел для кожного пікселя.
4 Змінюємо значення складових кольору, зокрема, складову r (червоний колір) залишаємо без змін, для складової g (зелений колір) присвоюємо значення складової b (синій колір) і для складової b (синій колір) присвоюємо значення складової r (червоний колір). Значення прозорості a залишаємо без змін.
5 Після редагування даних масиву pixels необхідно виконати функцію updatePixels() , щоб оновити зміни у масиві.

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

sketch.js
for (let i = 0; i < atlas.pixels.length; i = i + 4) {
  const r = atlas.pixels[i + 0];
  const g = atlas.pixels[i + 1];
  const b = atlas.pixels[i + 2];
  const a = atlas.pixels[i + 3];

  atlas.pixels[i + 0] = r;
  atlas.pixels[i + 1] = b;
  atlas.pixels[i + 2] = r;
  atlas.pixels[i + 3] = a;
}

Переглядаємо Аналізуємо

6.4.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «комп’ютерна анімація»?

  2. Як захопити відео із вебкамери, використовуючи бібліотеку p5.js?

  3. Для яких цілей можна використати масив pixels при опрацюванні відеофайлу за допомогою бібліотеки p5.js?

6.4.5. Практичні завдання

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

Початковий

  1. Створити застосунок, який призупиняє відтворення відео у певний момент. Використайте власний відеофайл або відео із запропонованого списку, в якому демонструється приземлення першого ступеня ракети-носія Falcon 9 на океанську платформу. Орієнтовний зразок роботи застосунку представлений в демонстрації.

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

Середній

  1. Створити застосунок, який повідомляє колір пікселя над яким перебуває вказівник миші у вікні відтворення відео. Використайте власний відеофайл або відео з трояндою із запропонованого списку. Орієнтовний зразок роботи застосунку представлений в демонстрації.

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

Високий

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

  1. Створити застосунок, який розповідає певну історію за допомогою зображень, що змінюються. Зупинити/продовжити анімацію можна за допомогою натискання будь-якої клавіші на клавіатурі. Файли із зображеннями, які використовуються в ескізі, можна завантажити за покликанням. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Екстремальний

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

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

6.5. Трансформації та моделювання руху

6.5.1. Основи моделювання руху

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

Як відомо, код у тілі функції draw() виконується у циклі аж до зупинки застосунку. Один такий прохід (ітерація) через тіло функції draw() називається кадром, а кількість кадрів, які малюються щосекунди - частотою кадрів.

Пригадаємо, що це означає, на прикладі коду нижче.

sketch.js
function setup() {
  createCanvas(200, 200)
}

function draw() {
  background(220);
  circle(100, 100, 50);
}

Щоразовий виклик функцій background() і circle() у тому порядку, як вони записані, буде окремим кадром, в якому буде малюватися полотно сірого кольору, а на полотні - коло білого кольору. За стандартним налаштуванням за секунду намалюється 60 кадрів, але за потреби це значення можна змінити за допомогою функції frameRate() . Наприклад, виклик функції frameRate(15) встановлює швидкість 15 кадрів за секунду.

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

Наприклад, використаємо для діаметра кола як аргумент у функції circle() змінну d, значення якої буде збільшуватися чи зменшуватися у кожному кадрі.

sketch.js
let d = 0; (1)
let outside = false; (2)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  if (d > width) { (3)
    outside = true;
  } else if (d < 0) {
    outside = false;
  }

  if (outside) { (4)
    d -= 1;
  } else {
    d += 1;
  }

  circle(100, 100, d); (5)
}
1 Ініціалізуємо змінну d зі значенням 0, яка буде набувати цілих значень від нуля до значення ширини полотна й у зворотному порядку.
2 Ініціалізуємо змінну outside з логічним значенням false, яка буде змінювати своє значення на true, якщо діаметр кола d збільшиться до значення ширини полотна, а коли діаметр кола d зменшиться до нуля - на false.
3 У вказівці розгалуження if перевіряємо значення діаметра кола d: якщо діаметр кола став більшим за ширину полотна, встановлюємо true для outside, інакше, якщо діаметр кола став менше нуля, - false.
4 Перевіряємо за допомогою наступної вказівки розгалуження if значення outside і збільшуємо чи зменшуємо значення діаметра на одиницю відповідно.
5 Малюємо коло з діаметром d.

Пункти 3-5 визначають дії, що відбувається в одній ітерації, в одному кадрі, в якому намальоване коло з певним значенням діаметра d. Кожен наступний кадр містить також намальоване коло, але вже з іншим значенням d.

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

Переглядаємо Аналізуємо

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

У наступному прикладі фігура переміщується зліва направо завдяки зміні значення x у черговому кадрі:

sketch.js
let d = 50;
let x = -d / 2; (1)
let velocity = 0.5; (2)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(x, height / 2, d); (3)
  x += velocity; (4)
}
1 Ініціалізація змінної x зі значенням -d / 2 (-d / 2 = -50 / 2 = -25) - це початкове значення x-координати центру кола. Завдяки від’ємному значенню центр кола буде розташовуватись зліва від лівої межі полотна на відстані, яка дорівнює радіусу кола.
2 Змінна velocity визначає крок збільшення значення змінної x у кожному кадрі. Змінюючи значення velocity можна керувати швидкістю руху фігури на полотні (саме тому обрана для змінної назва velocity).
3 Малювання кола з діаметром d у точці з x-координатою, що дорівнює значенню x, і y-координатою, що дорівнює height / 2 (значення цієї координати є однаковим для усіх кадрів, тому коло по вертикалі завжди знаходиться у центрі полотна). На полотні рух кола можна буде побачити, коли значення x-координати центра кола за модулем буде меншою за радіус кола.
4 Збільшення значення змінної x на величину velocity у кожному наступному кадрі.

Переглядаємо Аналізуємо

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

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

Змінимо код так, щоб коло з’являлося знову біля лівої межі полотна після зникнення за його правою межею.

sketch.js
let d = 50;
let x = -d / 2;
let velocity = 0.5;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(x, height / 2, d);
  if (x > width + d / 2) {
    x = -d / 2;
  }
  x += velocity;
}

У кожній ітерації виконання функції draw() величина x порівнюється зі значенням ширини полотна width плюс радіус кола (d / 2). Якщо результат порівняння true, x присвоюється значення -d / 2 і коло малюється у точці з координатами (-25, height / 2), тобто зліва від лівої межі полотна. Далі процес повторюється.

Проілюструємо принцип роботи застосунку.

Переміщення фігури за ліву межу полотна
Переміщення фігури за ліву межу полотна

Додамо у застосунок можливість змінювати напрямок руху кола після досягнення межі полотна, тобто відбиватися від вертикальних меж полотна. Спочатку змінимо початкове значення x-координати побудови центру кола (x = d = 50), щоб коло починало свій рух вже на полотні.

sketch.js
let d = 50;
let x = d;
let velocity = 0.5;
let side = 1; (1)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(x, height / 2, d);
  if (x > width - d / 2 || x < d / 2) { (2)
    side = -side;
  }
  x += velocity * side; (3)
}
1 Ініціалізуємо змінну side зі значенням 1. Коло буде рухатися вправо при значенні side = 1, а вліво - при side = -1.
2 Перевіряємо умови відбивання кола від правої (x > width - d / 2) або від лівої (x < d / 2) меж полотна.
3 Враховуємо напрям у процесі зміни x-координати центру кола.

Переглядаємо Аналізуємо

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

sketch.js
let d = 50;
let y = d;
let velocity = 0.5;
let side = 1;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(width / 2, y, d);
  if (y > width - d / 2 || y < d / 2) {
    side = -side;
  }
  y += velocity * side;
}

Переглядаємо Аналізуємо

Для реалізації руху на полотні одночасно у горизонтальному і вертикальному напрямках, необхідно враховувати зміну значень x-координати та y-координати та перевіряти вихід об’єкта за 4 межі. Цього разу не будемо використовувати окремої змінної, яка буде визначати напрямок (у попередніх прикладах це була змінна side), а відразу використовуватимемо значення -1 для зміни руху в обидвох напрямках.

sketch.js
let d = 50;
let x = d;
let y = d;
let velocityX = 0.5; (1)
let velocityY = 0.8; (2)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(x, y, d);

  if (x > width - d / 2 || x < d / 2) { (3)
    velocityX = velocityX * -1;
  }
  if (y > height - d / 2 || y < d / 2) { (4)
    velocityY = velocityY * -1;
  }

  x += velocityX;
  y += velocityY;
}
1 Ініціалізація змінної velocityX з початковим значенням швидкості під час руху горизонтально.
2 Ініціалізація змінної velocityY з початковим значенням швидкості під час руху вертикально.
3 Якщо коло, що рухається, виходить за вертикальні межі полотна, змінюємо крок зміни x-координати на протилежне за знаком значення множенням на -1 (якщо було додатне, то змінюється на від’ємне, і навпаки). Так коло почне рухатися горизонтально у протилежний бік.
4 Якщо коло, що рухається, виходить за горизонтальні межі полотна, змінюємо крок зміни y-координати на протилежне за знаком значення множенням на -1 (якщо було додатне, то змінюється на від’ємне, і навпаки). Так коло почне рухатися вертикально у протилежний бік.

Переглядаємо Аналізуємо

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

Використовуючи з бібліотеки p5.js функцію random() , яка повертає випадкове дробове десяткове число (число з рухомою крапкою), можна моделювати рух з випадковою траєкторію.

sketch.js
let d = 50;
let x = d;
let y = d;
let velocity = 2.5;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  circle(x, y, d);
  x += random(-velocity, velocity);
  y += random(-velocity, velocity);
  x = constrain(x, d / 2, width - d / 2);
  y = constrain(y, d / 2, height - d / 2);
}

У цьому прикладі для зміни позиції кола на полотні у кожному кадрі використовуються випадкові числа, згенеровані функцією random() з діапазону (-2.5, 2.5), не включаючи значення верхньої межі.

Щоб коло не виходило за межі полотна можна скористатися вказівками розгалуження if, як це продемонстровано у попередніх прикладах, або ще однією функцією з бібліотеки p5.js з назвою constrain() , як у цьому прикладі.

Функція constrain() обмежуватиме значення x і y за допомогою діапазонів (d / 2, width - d / 2) і (d / 2, height - d / 2) відповідно, що дозволить генерувати x і y в межах розмірів полотна.

Переглядаємо Аналізуємо

Щоб використовувати одну і ту ж послідовність випадкових чисел при кожному запуску застосунку, застосовуйте функцію randomSeed() .

Враховуючи те, що p5.js завжди відраховує час, що минув із запуску застосунку, можна моделювати рух з прив’язуванням до певної позначки часу за допомогою функції millis() , яка повертає значення лічильника часу у мілісекундах (1 секунда = 1000 мілісекунд).

Наприклад, у наступному застосунку коло буде сповільнювати свій рух на 1-й і 2-й секундах від запуску застосунку. Для кращої візуалізації етапів сповільнення додатково використаємо у коді функцію fill().

sketch.js
let d = 50;
let t1 = 1000;
let t2 = 2000;
let x = 0;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);

  let ms = millis();
  if (ms > t2) {
    fill(246, 174, 45); // Hunyadi yellow
    x += 0.3;
  } else if (ms > t1) {
    fill(51, 101, 138); // Lapis Lazuli
    x += 0.6;
  } else {
    x += 0.9;
  }
  circle(x, height / 2, d);
}

Переглядаємо Аналізуємо

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

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

sketch.js
let d = 50;
let angle = 0;
let amplitude = 30;
let velocity = 0.05;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  let y = sin(angle) * amplitude;
  circle(width / 2, height / 2 + y, d);
  angle += velocity;
}

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

Так y-координата набуває значень орієнтовно в діапазоні від (-30, 30), що ми й бачимо на полотні - коло у черговому кадрі малюється все вище, а досягнувши верхньої межі, малювання відбувається з кожним наступним кадром нижче, і досягнувши нижньої межі, все повторюється знову. Значення кута angle постійно зростає у кожному кадрі на величину velocity.

Переглядаємо Аналізуємо

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

sketch.js
let d = 50;
let x = d / 2;
let angle = 0;
let amplitude = 30;
let velocity = 0.05;

function setup() {
  createCanvas(400, 200);
}

function draw() {
  background(220);
  let y1 = sin(angle + 0.0) * amplitude;
  let y2 = sin(angle + 0.3) * amplitude;
  let y3 = sin(angle + 0.6) * amplitude;
  let y4 = sin(angle + 0.9) * amplitude;
  let y5 = sin(angle + 1.2) * amplitude;
  let y6 = sin(angle + 1.5) * amplitude;
  circle(x + d * 1, height / 2 + y1, d);
  circle(x + d * 2, height / 2 + y2, d);
  circle(x + d * 3, height / 2 + y3, d);
  circle(x + d * 4, height / 2 + y4, d);
  circle(x + d * 5, height / 2 + y5, d);
  circle(x + d * 6, height / 2 + y6, d);
  angle += velocity;
}

Для кращого розуміння того, що робить код, використаємо не цикл, а декілька змінних, які визначатимуть y-координати для окремих шести кіл. Щоб коливання кожного з кіл було із певним зсувом, як аргументи для функції sin() використовуємо доданки 0.0, 0.3, 0.6, 0.9, 1.2 і 1.5.

У результаті отримуємо ефект поширення хвилі у просторі, форма якої відома під назвою синусоїда.

Переглядаємо Аналізуємо

Для моделювання руху по колу визначимо x-координату, яка буде змінюватися по функції cos(), і y-координату - по функції sin() відповідно. Для регулювання радіуса уявного кола, вздовж якого здійснюватиметься рух, знову використаємо змінну amplitude.

sketch.js
let d = 50;
let angle = 0;
let amplitude = 30;
let velocity = 0.05;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  let x = cos(angle) * amplitude;
  let y = sin(angle) * amplitude;
  circle(width / 2 + x, height / 2 + y, d);
  angle += velocity;
}

Переглядаємо Аналізуємо

Якщо у кожному кадрі змінювати значення amplitude, то отримаємо ще один різновид руху - по спіралі.

let d = 50;
let angle = 0;
let amplitude = 0;
let velocity = 0.05;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  let x = cos(angle) * amplitude;
  let y = sin(angle) * amplitude;
  circle(width / 2 + x, height / 2 + y, d);
  angle += velocity;
  amplitude += velocity;
}

Переглядаємо Аналізуємо

6.5.2. Трансформації у 2D

За стандартним налаштуванням полотно використовує прямокутну систему координат з початком відліку у точці (0, 0) в лівому верхньому куті, де вісь X спрямована вправо, а вісь Y - вниз.

Змінюючи систему координат полотна за стандартним налаштуванням, можна створювати різні її трансформації: переміщення, обертання та масштабування.

Переміщення

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

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

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

Переміщення системи координат (1 клітинка = 100x100 пікселів)
Переміщення системи координат (1 клітинка = 100x100 пікселів)

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

Графічна піксельна сітка
Відкрийте зображення у новій вкладці або збережіть на комп’ютер, щоб мати реальний розмір зображення сітки
Графічна піксельна сітка
Відкрийте зображення у новій вкладці або збережіть на комп’ютер, щоб мати реальний розмір зображення сітки

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

У разі використання середовища Processing IDE, створіть каталог data у каталозі вашого ескізу і скопіюйте у нього файли із зображенням сіток. Шлях до зображень у функції loadImage() повинен бути відносним до HTML-файлу ескізу.
sketch.js
let grid; (1)
let gridOver; (2)

function preload() { (3)
  grid = loadImage("grid_1000.png");
  gridOver = loadImage("grid_1000_over.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0); (4)

  translate(200, 200); (5)
  image(gridOver, 0, 0); (6)
  fill(237, 43, 98); // Cerise
  rect(300, 200, 300, 500); (7)
}
1 Оголошуємо змінну grid, яка вказуватиме на завантажений графічний файл першої сітки, що буде статичною і відіграватиме роль тла застосунку.
2 Оголошуємо змінну gridOver, яка вказуватиме на завантажений графічний файл другої прозорої сітки. Цю сітку будемо переміщувати.
3 Щоб переконатися в тому, що зображення першої й другої графічних сіток повністю завантажені, використаємо функцію preload() . Усередині preload() функція loadImage() завантажує графічні файли та призначає їх змінним з назвами grid і gridOver відповідно. Тут варто бути уважним і правильно прописати шлях до зображень.
4 Функція image() розміщує завантажене зображення grid на полотні в точці з координатами (0, 0) (у початку системи координат полотна).
5 Переміщення початку координат в точку з координатами (200, 200) за допомогою функції translate().
6 Функція image() розміщує завантажене зображення gridOver на полотні в точці з координатами (200, 200) (нова точка відліку системи координат полотна).
7 Малюємо прямокутник на полотні відносно нового початку системи координат.

У результаті виконання застосунку отримаємо результат як на малюнку.

Переміщення системи координат за допомогою функції translate()
Переміщення системи координат за допомогою функції translate()

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

Як працює функція translate()
Використання функції transalte() (зліва направо): не застосовується, translate(200, 200), translate(500, 600)

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

sketch.js
let grid;
let gridOver;

function preload() {
  grid = loadImage("grid_1000.png");
  gridOver = loadImage("grid_1000_over.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0);

  translate(200, 200);
  image(gridOver, 0, 0);
  fill(237, 43, 98); // Cerise
  rect(300, 200, 300, 500);

  fill(240, 200, 8); // Jonquil
  rect(100, 100, 100, 100);
}
Як працює функція translate()
У блоці draw() функція translate() застосовується до всіх наступних за нею функцій малювання фігур

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

Цікавий ефект спостерігається, коли у функції draw() використовується більше одного виклику функції translate().

sketch.js
let grid;
let gridOver;

function preload() {
  grid = loadImage("grid_1000.png");
  gridOver = loadImage("grid_1000_over.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0);

  translate(200, 200); (1)
  image(gridOver, 0, 0);
  fill(237, 43, 98); // Cerise
  rect(300, 200, 300, 500);

  translate(300, 300); (2)
  fill(240, 200, 8); // Jonquil
  rect(100, 100, 100, 100);
}
1 Виклик функції translate(200, 200) встановлює новий початок системи координат в точці з координатами (200, 200). Перший прямокутник (Cerise) малюється відносно нового початку системи координат у точці з координатами (200 + 300 = 500, 200 + 200 = 400).
2 Виклик функції translate(300, 300) встановлює знову новий початок системи координат, але вже у точці з координатами (300, 300) відносно точки (200, 200), яка була початком відліку системи координат до цього. Другий прямокутник (Jonquil) малюється відносно вже нового початку системи координат у точці з координатами (200 + 300 + 100 = 600, 200 + 300 + 100 = 600).
Як працює функція translate()
Виклики у блоці draw() кількох функцій translate()

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

Переміщення координат у функції draw() скидаються під час кожного повторного її виконання. Хоча у блоці draw() може бути кілька функцій translate(), результат роботи цих функцій не переноситься у наступний кадр. Для зберігання таких перетворень між кадрами можна використовувати глобальні змінні як аргументи таких перетворень.

Вправа 62

Поміркувати, як за допомогою виклику функції circle(0, 0, 100); із зазначеними аргументами намалювати коло у центрі полотна.

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

  1. Перемістити систему координат у точку з координатами, де будуть намальовані фігури.

  2. Намалювати фігури.

  3. Перемістити систему координат у точку з тими самими координатами, записаними зі знаком мінус.

Запишемо цей алгоритм за допомогою коду.

sketch.js
let grid;
let gridOver;

function preload() {
  grid = loadImage("grid_1000.png");
  gridOver = loadImage("grid_1000_over.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0);

  translate(200, 200); (1)
  image(gridOver, 0, 0);

  fill(237, 43, 98); // Cerise
  rect(300, 200, 300, 500);

  translate(-200, -200); (2)

  fill(240, 200, 8); // Jonquil
  rect(100, 100, 100, 100);
}
1 Виклик функції translate(200, 200) встановлює новий початок системи координат в точці з координатами (200, 200). У цій системі координат малюється графічна сітка gridOver і маленький прямокутник (Cerise).
2 Виклик функції translate(-200, -200), встановлює новий початок системи координат в точці з координатами (0, 0), тобто у системі координат за стандартним налаштуванням. У цій системі координат малюється графічна сітка grid і великий прямокутник (Jonquil).
Як працює функція translate()
Створення фігур у різних системах координат
Обертання

Функція rotate() обертає систему координат полотна відносно початку координат - точки (0, 0).

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

Щоб значення кутів у функціях сприймалось у градусах, а не у радіанах, необхідно встановити (наприклад, у блоці setup()) відповідний режим angleMode(DEGREES) за допомогою функції angleMode() .

У разі додатного значення аргументу обертання здійснюється за годинниковою стрілкою, інакше - проти. Як і для функції translate(), результат роботи функції rotate() також накопичується, але скидається щоразу, коли draw() починає свою наступну ітерацію.

Результат роботи функції rotate() проілюстровано на малюнку нижче.

Обертання системи координат
Обертання системи координат (зліва направо): не застосовується, rotate(PI / 10), rotate(-PI / 12)
Значення PI - це математична константа, яка дорівнює 3.14159265358979323846 і визначається відношенням довжини кола до його діаметра.

Застосуємо обертання системи координат в реальному застосунку, використовуючи код нижче.

sketch.js
let grid;
let gridOver;

function preload() {
  grid = loadImage("grid_1000.png");
  gridOver = loadImage("grid_1000_over.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0);

  rotate(PI / 10); (1)
  // rotate(-PI / 12); (2)

  image(gridOver, 0, 0);
  fill(237, 43, 98); // Cerise
  rect(300, 200, 300, 500);

  fill(240, 200, 8); // Jonquil
  rect(100, 100, 100, 100);
}
1 Обертання системи координат відбувається навколо точки (0, 0), яка позначає початок координат, за годинниковою стрілкою на кут PI / 10 = 0.3141592653589793 радіан = 18°.
2 Обертання системи координат відбувається навколо точки (0, 0), яка позначає початок координат, проти годинникової стрілки на кут -PI / 12 = -0.2617993877991494 радіан = -15°.
Обертання системи координат
Обертання системи координат (зліва направо): за годинниковою стрілкою на кут PI / 10, проти годинникової стрілки на кут -PI / 12

Більше можливостей для трансформацій з’являється, коли функції translate() та rotate() застосовуються разом.

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

Наприклад, щоб запрограмувати обертання фігури навколо її центру у певному місці на полотні, слід дотримуватися такого алгоритму:

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

  2. Викликати функцію rotate() з певним значенням кута повороту.

  3. Намалювати фігуру в точці (0, 0).

Запишемо вищенаведений алгоритм за допомогою коду.

sketch.js
let grid;
let angle = 0; (1)

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0);

  translate(300, 300); (2)
  rotate(angle); (3)
  fill(240, 200, 8); // Jonquil
  rect(0, 0, 100, 100); (4)
  angle += 0.05; (5)
}
1 Ініціалізуємо змінну angle з початковим значенням кута повороту.
2 Викликаємо функцію translate() - переміщуємо початок системи координат в точку (300, 300).
3 Викликаємо функцію rotate() - повертаємо систему координат на поточне значення кута angle.
4 Малюємо прямокутник в точці (0, 0) - початку системи координат після її переміщення.
5 Збільшуємо значення кута angle на величну 0.05.

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

Переглядаємо Аналізуємо

Вправа 63

Змінити код застосунку так, щоб прямокутник обертався навколо свого центру.

Щоб спростити малювання фігур відносно їх центру, використовуйте функції rectMode() , ellipseMode() та інші, які змінюють спосіб інтерпретації параметрів. Ще один спосіб - малювання фігури так, щоб точка початку системи координат (0, 0) опинилася в центрі фігури. Наприклад, у разі малювання прямокутника значення координат його центру можна записати як від’ємні значення половини ширини й висоти фігури відповідно: rect(-50, -50, 100, 100).
Масштабування

Функція scale() дозволяє масштабувати координати полотна. Завдяки цьому можна збільшувати або зменшувати розміри об’єктів на полотні.

Якщо функцію scale() викликають з одним аргументом, то він визначає на скільки відсотків масштабувати об’єкт. Наприклад, виклик scale(1.5) збільшує усі фігури на полотні у півтора раза (150% від початкового розміру), а scale(3) - утричі (300% від початкового розміру). А от використання scale(1) не дасть жодного ефекту, тому що у всіх фігур залишиться розмір 100% від початкового розміру.

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

Повторні виклики функції scale() у блоці draw() посилюють ефект масштабування, але на початку чергової ітерації у draw() масштабування скидається.

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

Масштабування системи координат
Масштабування системи координат (зліва направо): не застосовується (1 клітинка = 100x100 пікселів), збільшення у 1,5 раза, збільшення утричі

Щоб зменшити розмір фігур удвічі, використовують значення масштабу 0.5 (50% від початкового розміру).

sketch.js
let grid;

function preload() {
  grid = loadImage("grid_1000.png");
}

function setup() {
  createCanvas(1000, 1000);
  noStroke();
}

function draw() {
  image(grid, 0, 0);

  scale(0.5);
  fill(237, 43, 98); // Cerise
  rect(300, 200, 300, 500);
}
Використання функції scale()
Використання функції scale(): зменшення розміру фігури удвічі

Використовуючи scale() з від’ємними значеннями аргументів можна перевертати/віддзеркалювати систему координат.

Вправа 64

Застосувати функцію scale(-1, 1) для дзеркального відображення об’єктів на полотні горизонтально.

6.5.3. Трансформації у 3D

WebGL

Бібліотека p5.js є потужним інструментом для створення у вебпереглядачі 2D-графіки. Побудова зображень на полотні у двовимірному просторі відбувається у системі координат з двома осями X та Y, використовуючи значення координат (x, y) для позначення конкретного пікселя зображення.

Водночас p5.js містить засоби для створення 3D-ескізів. У цьому разі до двох вищезгаданих осей додається третя вісь Z, яка визначає глибину будь-якої заданої точки, створюючи ілюзію тривимірного реального простору. Для того, щоб вказати тривимірні координати точки, координати задаються в порядку (x, y, z).

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

sketch.js
let d = 0;

function setup() {
  createCanvas(200, 200);
  rectMode(CENTER);
}

function draw() {
  background(255);
  stroke(34, 111, 84); // Dark spring green
  fill(244, 240, 187); // Lemon chiffon
  rect(width / 2, height / 2, d, d);
  d += 1;
}

Переглядаємо Аналізуємо

Як відомо, у 2D-режимі початок системи координат міститься в точці (0, 0), яка розташована у верхньому лівому куті полотна.

Для створення ескізів у 3D-режимі бібліотека p5.js використовує спеціальний режим WEBGL, в якому початок системи координат міститься в точці (0, 0, 0), що розташована в центрі полотна.

Щоб запам’ятати, в яких напрямках спрямовані осі у WEBGL-режимі, використовуйте правило лівої руки: направте вказівний палець праворуч (вісь X), три пальці вниз (вісь Y), тоді великий палець автоматично вкаже на вас (вісь Z).
3D-система координат
3D-система координат

Щоб відобразити ескіз за допомогою WebGL, все, що нам потрібно зробити, це додати третій аргумент, константу WEBGL, до виклику функції createCanvas(). Отож, перетворимо попередній приклад у 3D-ескіз.

sketch.js
let z = 0;

function setup() {
  createCanvas(200, 200, WEBGL); (1)
  rectMode(CENTER);
}

function draw() {
  background(255);
  smooth(); (2)
  stroke(34, 111, 84); // Dark spring green
  fill(244, 240, 187); // Lemon chiffon
  translate(0, 0, z); (3)
  rect(0, 0, 20, 20); (4)
  z += 1;
}
1 Попереджуємо p5.js, що необхідно отримати 3D-ескіз. Це досягається додаванням третього аргументу WEBGL до виклику функції createCanvas().
2 За допомогою функції smooth() вмикаємо режим малювання фігур зі згладженими краями, який у WEBGL вимкнений. Це покращить якість зображення, розміри якого змінюються.
3 Для того, щоб використовувати тривимірні координати (x, y, z) для прямокутника, використовуємо функцію translate() . Завдяки черговому збільшенню значення z-координати, прямокутник буде рухатися вздовж осі Z, наближаючись до глядача.
4 Хоча координати прямокутника мають значення (0, 0), він розташований у центрі полотна, оскільки увімкнений режим WebGL, в якому початок системи координат міститься у центрі полотна, а не у верхньому лівому куті.

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

Вправа 65

Змінити код застосунку для кола, яке віддаляється від глядача.

Переміщення

Як відомо, функція translate() переміщує початок координат у заданому напрямку. Все, що намальовано після виклику translate(), буде розташовано відносно цієї точки. У контексті тривимірного простору translate() приймає аргументи для значень x, y та z.

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

sketch.js
function setup() {
  createCanvas(300, 300, WEBGL);
  rectMode(CENTER);
}

function draw() {
  background(245);
  stroke(34, 111, 84); // Dark spring green
  fill(244, 240, 187); // Lemon chiffon

  // по центру
  translate(0, 0, 0);
  box();

  translate(-100, 0, 0);
  box();

  translate(200, 0, 0);
  box(60);

  // вгорі
  translate(-200, -100, 0);
  box(20, 20);

  translate(100, 0, 0);
  box(50, 30, 30);

  translate(100, 0, 0);
  box(45);

  // внизу
  translate(-200, 200, 0);
  box(20, 20, 75);

  translate(100, 0, -100);
  box(50, 20, 100);

  fill(255, 137, 102); // Coral
  translate(100, 0, 100);
  box();
}
Паралелепіпеди
Паралелепіпеди

Алгоритм побудови у цьому разі можна описати так:

  1. Переносимо за допомогою функції translate() початок координат у місце побудови 3D-фігури, враховуючи попереднє значення початку системи координат.

  2. Будуємо фігуру за допомогою функції box(), за потреби викликавши її з аргументами. Для візуалізації конкретного паралелепіпеда використовуємо функцію fill().

  3. Переходимо до першого пункту.

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

Переглядаємо Аналізуємо

Обертання

Третій вимір також відкриває можливість обертання навколо різних осей.

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

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

Обертатися можна також навколо осі X або Y за допомогою функцій rotateX() та rotateY() , кожна з яких потребує режиму WEBGL. Функція rotateZ() також існує і є еквівалентом rotate().

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

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

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

sketch.js
function setup() {
  createCanvas(300, 300, WEBGL);
  rectMode(CENTER);
}

function draw() {
  background(245);
  stroke(34, 111, 84); // Dark spring green
  fill(244, 240, 187); // Lemon chiffon

  // по центру
  translate(0, 0, 0);
  rotateY(frameCount * 0.01); (1)
  box();

  translate(-100, 0, 0);
  rotateX(frameCount * 0.01); (2)
  box();

  translate(200, 0, 0);
  box(60);

  // вгорі
  translate(-200, -100, 0);
  box(20, 20);

  translate(100, 0, 0);
  box(50, 30, 30);

  translate(100, 0, 0);
  box(45);

  // внизу
  translate(-200, 200, 0);
  box(20, 20, 75);

  translate(100, 0, -100);
  box(50, 20, 100);

  fill(255, 137, 102); // Coral
  translate(100, 0, 100);
  box();
}
1 Обертання навколо осі Y. Значення кута повороту формується за допомогою frameCount - системної змінної, яка накопичує кількість кадрів, що пройшли з моменту запуску застосунку.
2 Обертання навколо осі X.

Переглядаємо Аналізуємо

Вправа 66

Додати у вищенаведений код обертання навколо осі Z.

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

Переглядаємо Аналізуємо

Масштабування

Для зміни розмірів фігур у тривимірному просторі використовується функція scale() , яка змінює розмір того, що намальовано після її виклику. Як і описані вище функції трансформації, вона приймає аргументи для значень x, y і z.

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

Переглядаємо Аналізуємо

Порядок трансформацій

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

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

6.5.4. Матриця трансформації

Будь-які елементи, які додаються до ескізу, розташовуються відносно початку системи координат. Кожна нова функція трансформації впливає на положення або орієнтацію початку координат, а на кожну нову трансформацію впливає будь-яка, яка їй передує.

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

Матриця - це таблиця чисел (масив), які розміщені у рядках і у стовпцях.

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

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

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

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

sketch.js
let a = 0;

function setup() {
  createCanvas(200, 200, WEBGL);
  rectMode(CENTER);
  noStroke();
}

function draw() {
  background(255);

  translate(-50, -50, 0); (1)
  rotateZ(a); (2)
  fill(152, 95, 153); // Pomp and Power
  rect(0, 0, 50, 50); (3)
  a += 0.01; (4)
}
1 Переміщуємо систему координат з центру полотна (у 3D-просторі початок координат у центрі полотна) у точку (-50, -50, 0), яка розташована у верхньому лівому куті полотна.
2 Повертаємо систему координат на кут a.
3 Малюємо прямокутник відповідних розмірів у точці початку координат (0, 0).
4 Збільшуємо значення кута на величину 0.01.

Описані вище кроки виконуються у блоці draw() в циклі. Завдяки глобальній змінній a, значення якої в кожній ітерації у draw() збільшується на величину 0.01 і увімкненому режиму rectMode(CENTER) прямокутник обертається навколо свого центру.

Переглядаємо Аналізуємо

Другий прямокутник змусимо обертатися навколо осі Y у нижньому правому куті полотна.

sketch.js
let b = 0;

function setup() {
  createCanvas(200, 200, WEBGL);
  rectMode(CENTER);
  noStroke();
}

function draw() {
  background(255);

  translate(50, 50); (1)
  rotateY(b); (2)
  fill(239, 121, 138); // Bright pink (Crayola)
  rect(0, 0, 50, 50); (3)
  b += 0.02; (4)
}
1 Переміщуємо систему координат з центру полотна (у 3D-просторі початок координат у центрі полотна) у точку (50, 50, 0), яка розташована у нижньому правому куті полотна.
2 Повертаємо систему координат на кут b.
3 Малюємо прямокутник відповідних розмірів у точці початку координат (0, 0).
4 Збільшуємо значення кута на величину 0.02.

Як і у попередньому випадку, описані кроки виконуються у блоці draw() в циклі. Завдяки глобальній змінній b, значення якої в кожній ітерації у draw() збільшується на величину 0.02 та увімкненому режиму rectMode(CENTER) прямокутник обертається навколо свого центру.

Переглядаємо Аналізуємо

Тепер об’єднаймо два ескізи в один.

sketch.js
let a = 0;
let b = 0;

function setup() {
  createCanvas(200, 200, WEBGL);
  rectMode(CENTER);
  noStroke();
}

function draw() {
  background(255);

  translate(-50, -50, 0);
  rotateZ(a);
  fill(152, 95, 153); // Pomp and Power
  rect(0, 0, 50, 50);
  a += 0.01;

  translate(50, 50);
  rotateY(b);
  fill(239, 121, 138); // Bright pink (Crayola)
  rect(0, 0, 50, 50);
  b += 0.02;
}

Переглядаємо Аналізуємо

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

Поміркуємо, як це можна виправити.

Отож, щоб скасувати обертання другого прямокутника навколо осі Z, перед переміщенням системи координат для другого прямокутника можна викликати функцію rotateZ(-a).

А для розміщення другого прямокутника у правому нижньому куті, необхідно замінити виклик функції translate(50, 50) на translate(100, 100).

Переглядаємо Аналізуємо

На розглянутому вище прикладі ми переконалися, що через ефект накопичування трансформацій можна отримати неочікуваний результат.

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

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

Система координат відновлюється до початкового стану (точка початку у верхньому лівому куті полотна для 2D-режиму чи у центрі полотна для 3D-режиму) без ефектів обертання і масштабування щоразу, коли починає виконуватися тіло функції draw().

Для керування перебігом трансформацій бібліотека p5.js використовує спеціальні функції push() та pop() .

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

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

Якщо не використовувати push() і pop(), то необхідно відстежувати будь-які трансформації, які вже відбулися, а це може стати складним завданням. До речі, у цьому ми вже неодноразово переконувалися на прикладі застосунків, в яких виконувалися трансформації для різних елементів.

Цікавимось Додатково

Стек

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

Стек - це один зі способів організації та зберігання інформації, лінійна структура даних, яка працює за принципом останнім прийшов - першим пішов (англ. LIFO - last in, first out).

Знання того, як працює стек, допоможе правильно використовувати ці функції.

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

Скасовувати зміни в текстовому редакторі можна до тих пір, доки файл не стане порожнім, тобто в стек не буде додано жодного нового тексту.

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

У цьому разі дія зберігання (push()) належить до процесу покладання зошита у стос зверху чи додавання нового тексту в документ, а відновлення (pop()) - виймання верхнього зошита зі стосу чи скасування доданого тексту в документ. Не можна відновити щось, якщо його не існує. Ось чому необхідно використовувати однакову кількість викликів push() та pop().

Протилежним стеку є принцип черги - перший прийшов - перший пішов (англ. FIFO - first in, first out). Черга повністю аналогічна звичній базарній черзі, у якій спочатку обслуговують того, хто прийшов першим, потім наступного і т. д. Інший приклад - події у вебпереглядачі, як-от натискання миші, додаються в чергу подій вебпереглядача й обробляються в порядку, в якому вони потрапили в чергу.

Тепер повернемось до об’єднаного ескізу і перепишемо його код із застосуванням функцій push() і pop().

sketch.js
let a = 0;
let b = 0;

function setup() {
  createCanvas(200, 200, WEBGL);
  rectMode(CENTER);
  noStroke();
}

function draw() {
  background(255);

  push(); (1)
  translate(-50, -50, 0); (2)
  rotateZ(a); (3)
  fill(152, 95, 153); // Pomp and Power
  rect(0, 0, 50, 50); (4)
  a += 0.01;
  pop(); (5)

  push(); (6)
  translate(50, 50); (7)
  rotateY(b); (8)
  fill(239, 121, 138); // Bright pink (Crayola)
  rect(0, 0, 50, 50); (9)
  b += 0.02;
  pop(); (10)
}

Результат виконання застосунку той самий, але використання функцій push() і pop() добре структурує фрагменти коду, в яких відбуваються трансформації. Проаналізуємо код в частині застосування в ескізі функцій push() і pop().

1 Зберігаємо поточну матрицю трансформацій. Тепер матриця трансформацій містить інформацію про те, що початок координат в точці (0, 0) у верхньому лівому куті полотна.
2 Переміщуємо початок системи координат (0, 0) у точку (-50, -50, 0).
3 Повертаємо навколо осі Z перший прямокутник на кут a за годинниковою стрілкою відносно точки початку координат.
4 Малюємо перший прямокутник відносно початку системи координат.
5 Відновлюємо матрицю трансформацій з кроку 1, щоб на другий прямокутник не впливали кроки 2 і 3.
6 Зберігаємо поточну матрицю трансформацій. Тепер матриця трансформацій містить інформацію про те, що початок координат в точці (0, 0) у верхньому лівому куті полотна.
7 Переміщуємо початок системи координат (0, 0) у точку (50, 50, 0).
8 Повертаємо навколо осі Y другий прямокутник на кут b за годинниковою стрілкою відносно точки початку координат.
9 Малюємо другий прямокутник відносно початку системи координат.
10 Відновлюємо матрицю трансформацій з кроку 6, щоб на наступні фігури, якщо такі будуть, не впливали кроки 7 і 8.

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

Як відомо, кількість викликів як push(), так і pop() повинна бути однаковою, але вони не завжди повинні йти один за одним.

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

sketch.js
...
push();
// трансформація першого прямокутника
pop();

push();
// трансформація другого прямокутника
pop();
...

Розглянемо код 2D-ескізу в якому застосування функцій push() і pop() мають вкладену структуру. Спочатку матриця трансформації має початковий стан (позначимо стан 1), в якому початок (0, 0) системи координат розташований в лівому верхньому куті полотна.

sketch.js
function setup() {
  createCanvas(200, 200);
  rectMode(CENTER);
  ellipseMode(CENTER);
}

function draw() {
  background(245);

  push(); (1)
  translate(width / 2, height / 2);
  rotate(radians((frameCount * 2) % 360));
  fill(19, 70, 17); // Pakistan green
  rect(0, 0, width / 2, 5);

  push(); (2)
  translate(width / 4, 0);
  fill(61, 163, 93); // Pigment green
  circle(0, 0, 30);
  pop(); (3)

  fill(232, 252, 207); // Nyanza
  circle(0, 0, 10);
  pop(); (4)
}
1 Зберігаємо стан 1 матриці трансформації. Далі переміщуємо систему координат в центр полотна у точку (width / 2, height / 2), обертаємо та малюємо прямокутник відносно точки (0, 0) початку системи координат. Тепер матриця трансформації має стан 2.
2 Зберігаємо стан 2 матриці трансформації. Далі переміщуємо систему координат в точку (width / 4, 0) і малюємо велике коло відносно точки (0, 0) початку системи координат. Тепер матриця трансформації має стан 3.
3 Відновлюємо стан 2 матриці трансформації. Малюємо мале коло. Мале коло і прямокутник перебувають в одній системі координат.
4 Відновлюємо стан 1 (початковий стан) матриці трансформації.

Переглядаємо Аналізуємо

Вправа 67

Закоментуйте пункти 2 і 3 та внесіть зміни в коді, щоб результат виконання застосунку відповідав представленому у демонстрації.

Годинник

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

Спочатку дізнаємось і надрукуємо на екрані поточний час (години, хвилини, секунди). Будемо відштовхуватися від наступного початкового коду:

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(255);

  let hr = hour(); (1)
  let mn = minute();
  let sc = second();

  fill(0);
  noStroke();

  textSize(50);
  text(correctTime(hr) + ":" + correctTime(mn) + ":" + correctTime(sc), 100, 210); (3)
}

function correctTime(t) { (2)
  if (t >= 0 && t < 10) {
    t = "0" + t;
  }
  return t;
}

Переглядаємо Аналізуємо

Отже, трохи пояснень до коду.

1 Щоб дізнатися поточний час, використаємо три функції для роботи з часом: hour() , minute() , second() . Усі функції отримують значення системного часу на комп’ютері.
2 Опишемо функцію correctTime(), яка приймає одне значення (годин або хвилин або секунд) і повертає коректне значення, наприклад: 19:53:4 буде перетворений у 19:53:04, 16:4:25 у 16:04:25, 8:11:44 у 08:11:44 тощо.
3 Виклики функції correctTime() для поточних значень годин, хвилин і секунд та друк на екрані часу у форматі hh:mm:ss за допомогою функції text() .

Тепер перейдемо до створення циферблата в центрі полотна.

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

Продемонструємо вказані зміни у тілі функції draw().

sketch.js
function draw() {
  background(255);

  translate(width / 2, height / 2);

  let hr = hour();
  let mn = minute();
  let sc = second();

  strokeWeight(8);

  stroke(0);
  point(0, 0);

  fill(0);
  noStroke();

  textSize(12);
  text(correctTime(hr) + ":" + correctTime(mn) + ":" + correctTime(sc), -20, 180);
}

Переглядаємо Аналізуємо

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

Відомо, що секундна стрілка проходить повне коло в 360°. Використаємо малювання дуги відповідно плину секунд.

Щоб значення кутів у функціях сприймалось у градусах, а не у радіанах, у функції setup() встановимо відповідний режим angleMode(DEGREES) за допомогою функції angleMode() .

Для цього візьмемо функцію arc() , яка малює дугу, і функцію map() , що перетворить діапазон секунд (0, 60) у діапазон значень кута в градусах (0, 360) для використання у функції arc().

Фрагмент коду для плину секунд буде наступним:

sketch.js
stroke(231, 29, 54); (1)
noFill(); (2)
let secondsAngle = map(sc, 0, 60, 0, 360); (3)
arc(0, 0, 300, 300, 0, secondsAngle); (4)
1 Колір лінії дуги.
2 Вимикання зафарбовування, щоб малювалась лише лінія дуги, а не сегмент.
3 Ініціалізація змінної secondsAngle, яка буде містити значення кута із функції map().
4 Малювання лінії дуги.
Значення кутів для функції arc() відраховуються за годинниковою стрілкою від позначки годинної стрілки на 3 години. Тому, щоб циферблат був як у справжнього годинника, його необхідно повернути проти годинникової стрілки на 90° (-90°) за допомогою функції rotate() .

Остаточний код на цю мить для плину секунд буде таким:

sketch.js
function setup() {
  createCanvas(400, 400);
  angleMode(DEGREES);
}

function draw() {
  background(255);

  translate(width / 2, height / 2);
  rotate(-90);

  let hr = hour();
  let mn = minute();
  let sc = second();

  strokeWeight(8);

  stroke(231, 29, 54);
  noFill();
  let secondsAngle = map(sc, 0, 60, 0, 360);
  arc(0, 0, 300, 300, 0, secondsAngle);

  stroke(0);
  point(0, 0);

  rotate(90);
  fill(0);
  noStroke();

  textSize(12);
  text(
    correctTime(hr) + ":" + correctTime(mn) + ":" + correctTime(sc),
    -20,
    180
  );
}

function correctTime(t) {
  if (t >= 0 && t < 10) {
    t = "0" + t;
  }
  return t;
}

Переглядаємо Аналізуємо

Реалізація плину хвилин буде аналогічною як і для секунд. У визначенні кута для годинного плину часу використаємо 24-годинний формат.

Фактично, годинна стрілка проходить по колу біля позначки 12 годин два рази на добу (12 година дня і 00 годин - дванадцята година ночі), тому для значення годин обираємо діапазон (0, 12) і перетворюємо його у діапазон значень кута (0, 360) для годинного плину часу. Для визначення поточного часу у 24-годинному форматі від значення, яке повертає функція hour(), беремо остачу від ділення на 12 за допомогою оператора %.

Отже, додамо фрагменти коду для плину хвилин і годин.

sketch.js
stroke(200);
let minutesAngle = map(mn, 0, 60, 0, 360);
arc(0, 0, 280, 280, 0, minutesAngle);

stroke(0);
let hoursAngle = map(hr % 12, 0, 12, 0, 360);
arc(0, 0, 260, 260, 0, hoursAngle);

Переглядаємо Аналізуємо

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

Стрілки побудуємо за допомогою функції line() .

Для того, щоб кожна стрілка (окремо від інших стрілок) малювалася відносно центру циферблата, використаємо функції push() і pop() .

Повороти на кут секундної, хвилинної та годинної стрілок буде здійснювати функція rotate() .

Фрагмент коду, який буде реалізовувати обертання стрілок буде

sketch.js
push();
rotate(secondsAngle);
stroke(231, 29, 54);
line(0, 0, 100, 0);
pop();

push();
rotate(minutesAngle);
stroke(200);
line(0, 0, 75, 0);
pop();

push();
rotate(hoursAngle);
stroke(0);
line(0, 0, 50, 0);
pop();

а остаточний код застосунку матиме вигляд:

sketch.js
function setup() {
  createCanvas(400, 400);
  angleMode(DEGREES);
}

function draw() {
  background(255);

  translate(width / 2, height / 2);
  rotate(-90);

  let hr = hour();
  let mn = minute();
  let sc = second();

  strokeWeight(8);

  stroke(231, 29, 54);
  noFill();
  let secondsAngle = map(sc, 0, 60, 0, 360);
  arc(0, 0, 300, 300, 0, secondsAngle);

  stroke(200);
  let minutesAngle = map(mn, 0, 60, 0, 360);
  arc(0, 0, 280, 280, 0, minutesAngle);

  stroke(0);
  let hoursAngle = map(hr % 12, 0, 12, 0, 360);
  arc(0, 0, 260, 260, 0, hoursAngle);

  push();
  rotate(secondsAngle);
  stroke(231, 29, 54);
  line(0, 0, 100, 0);
  pop();

  push();
  rotate(minutesAngle);
  stroke(200);
  line(0, 0, 75, 0);
  pop();

  push();
  rotate(hoursAngle);
  stroke(0);
  line(0, 0, 50, 0);
  pop();

  stroke(0);
  point(0, 0);

  rotate(90);
  fill(0);
  noStroke();

  textSize(12);
  text(
    correctTime(hr) + ":" + correctTime(mn) + ":" + correctTime(sc),
    -20,
    180
  );
}

function correctTime(t) {
  if (t >= 0 && t < 10) {
    t = "0" + t;
  }
  return t;
}

Переглядаємо Аналізуємо

Цікавимось Додатково

Дерево

Розглянемо застосування матриці трансформації на прикладі побудови стохастичного фракталу - дерева.

Переглядаємо Аналізуємо

Алгоритм побудови дерева такий:

  1. Намалювати лінію.

  2. Наприкінці лінії повернути на певний кут ліворуч й намалювати коротку лінію і водночас повернути праворуч на певний кут і намалювати коротку лінію.

  3. Повторити крок 2 для нових ліній.

Алгоритм побудови дерева
Алгоритм побудови дерева

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

Функція rotate(), за стандартним налаштуванням виконує повороти відносно точки початку координат. Тому точку початку координат необхідно щоразу переносити у кінець поточної гілки-лінії.

Розпочнемо малювання першої гілки - стовбура дерева. Використаємо функцію translate() для зміщення початку координат в нижню частину полотна, звідки буде «рости» корінь дерева, і проведемо лінію догори за допомогою line().

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
  translate(width / 2, height); (1)
  line(0, 0, 0, -100); (2)
}
1 Перенесення точки початку системи координат у (width / 2, height).
2 Малювання лінії від нового початку координат вгору на 100 пікселів.

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

Перенесення початку координат і поворот при малюванні поточної гілки
Перенесення початку координат і поворот при малюванні поточної гілки

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

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
  translate(width / 2, height);
  line(0, 0, 0, -100);
  translate(0, -100); (1)
  rotate(PI / 6); (2)
  line(0, 0, 0, -100); (3)
}
1 Перенесення точки початку системи координат у (0, -100).
2 Поворот праворуч на кут beta заданий в радіанах PI / 6 (30°).
3 Малювання лінії від нового початку координат вгору на 100 пікселів.

Тепер, побудуємо гілку, яка «росте» ліворуч. Для цього використаємо функції:

  • push() - зберігає стан матриці трансформації перед поворотом;

  • pop() - відновлює стан матриці трансформації, щоб намалювати гілку ліворуч.

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
  translate(width / 2, height); // корінь
  line(0, 0, 0, -100);
  translate(0, -100);

  push();
  rotate(PI / 6); // гілка праворуч
  line(0, 0, 0, -100);
  pop();

  rotate(-PI / 6); // гілка ліворуч
  line(0, 0, 0, -100);
}

Виклик функції line() тут можна розглядати як «окрему гілку» дерева і додавати щоразу все більше «гілок». Але в цьому разі код стане громіздким і неймовірно складним, тому створимо рекурсивну функцію branch(), яка замінить прямі виклики line().

sketch.js
function branch() {
  line(0, 0, 0, -100); (1)
  translate(0, -100); (2)

  push();
  rotate(PI / 6); (3)
  branch();
  pop();

  push();
  rotate(-PI / 6); (4)
  branch();
  pop();
}
1 Намалювати гілку.
2 Перемістити початок координат.
3 Повернути праворуч і викликати рекурсивну функцію branch().
4 Повернути ліворуч і викликати рекурсивну функцію branch().

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

sketch.js
function branch(len) { (1)
  line(0, 0, 0, -len);
  translate(0, -len);

  len = len * 0.66; (2)

  if (len > 2) {
    push();
    rotate(PI / 6);
    branch(len); (3)
    pop();

    push();
    rotate(-PI / 6);
    branch(len);
    pop();
  }
}
1 Кожна гілка тепер отримує свою довжину як аргумент.
2 Довжина кожної гілки зменшується на дві третини.
3 Наступні виклики рекурсивної функції branch() містять аргумент len.

Запишемо остаточний код застосунку для малювання дерева.

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(39, 38, 64);
  stroke(0, 181, 184);
  translate(width / 2, height);
  branch(60);
}

function branch(len) {
  line(0, 0, 0, -len);
  translate(0, -len);

  len = len * 0.66;

  if (len > 2) {
    push();
    rotate(PI / 6);
    branch(len);
    pop();

    push();
    rotate(-PI / 6);
    branch(len);
    pop();
  }
}
Крона дерева
Крона дерева

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

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

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

sketch.js
let beta;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(39, 38, 64);
  stroke(0, 181, 184);
  translate(width / 2, height);
  branch(60);
}

function branch(len) {
  beta = random(0, PI / 3);
  line(0, 0, 0, -len);
  translate(0, -len);

  len = len * 0.66;

  if (len > 2) {
    push();
    rotate(beta);
    branch(len);
    pop();

    push();
    rotate(-beta);
    branch(len);
    pop();
  }
  noLoop();
}
Варіанти дерев при різних кутах розгалуження
Варіанти дерев при різних кутах розгалуження

А це варіант для випадкових кута розгалуження і довжини гілок дерева.

sketch.js
let beta;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(39, 38, 64);
  stroke(0, 181, 184);
  translate(width / 2, height);
  branch(60);
}

function branch(len) {
  line(0, 0, 0, -len);
  translate(0, -len);

  len = len * 0.66;

  if (len > 2) {
    let n = int(random(1, 4));

    for (let i = 0; i < n; i++) {
      beta = random(-PI / 2, PI / 2);

      push();
      rotate(beta);
      branch(len);
      pop();
    }
  }
  noLoop();
}
Стохастичне дерево
Стохастичне дерево

6.5.5. 3D-примітиви

Однією з важливих відмінностей між створенням примітивів у 2D і у 3D є те, що функції бібліотеки p5.js для побудови 3D-примітивів як аргументи приймають значення розмірів, а не положення. Для зміни положення 3D-примітивів необхідно викликати функції translate() і rotate().

У бібліотеці p5.js є попередньо визначені базові функції для створення 3D-примітивів:

Функції для малювання 3D-фігур можна викликати без аргументів. У цьому разі будуть застосовуватися значення розміру за стандартним налаштуванням для кожної з фігур.

Розглянемо код застосунку, в якому за натискання миші на полотні відображаються по черзі усі 3D-примітиви.

sketch.js
let i = 0;

function setup() {
  createCanvas(200, 200, WEBGL);
}

function draw() {
  background(245);

  translate(mouseX - width / 2, mouseY - height / 2, 0);
  rotateY(frameCount * 0.02);

  if (i == 0) {
    plane(100, 100); (1)
  } else if (i == 1) {
    box(100, 100, 100); (2)
  } else if (i == 2) {
    sphere(75); (3)
  } else if (i == 3) {
    cylinder(50, 100); (4)
  } else if (i == 4) {
    cone(70, 100); (5)
  } else if (i == 5) {
    ellipsoid(60, 70, 70); (6)
  } else if (i == 6) {
    torus(50, 25); (7)
  }
}

function mousePressed() {
  i = (i + 1) % 7;
}
1 Площина із заданими шириною та висотою.
2 Паралелепіпед із заданою шириною, висотою та глибиною.
3 Сфера заданого радіуса.
4 Циліндр із заданими радіусом і висотою.
5 Конус із заданими радіусом і висотою.
6 Еліпсоїд із заданим радіусом.
7 Тор із заданими радіусом і радіусом трубки.

Використовуючи функцію mousePressed(), яка відстежує подію натискання миші у вікні застосунку, змінюється значення i. Відповідно значення i виконується одна з гілок if для малювання 3D-фігури у місці вказівника миші.

Переглядаємо Аналізуємо

Якщо необхідно намалювати одночасно кілька 3D-фігур у різних положеннях на полотні, тут знову приходять на допомогу функції push() і pop(), які викликають щоразу, коли малюють іншу фігуру.

sketch.js
function setup() {
  createCanvas(400, 200, WEBGL);
}

function draw() {
  background(245);

  push();
  translate(-width / 4, 0, 0);
  rotateZ(frameCount * 0.02);
  cone();
  pop();

  push();
  rotateY(frameCount * 0.02);
  box();
  pop();

  push();
  translate(width / 4, 0, 0);
  rotateX(frameCount * 0.02);
  torus();
  pop();
}

Переглядаємо Аналізуємо

Для створення інших тривимірних фігур використовують функції beginShape() , vertex() та endShape() .

У WEBGL-режимі перелічені функції працюють аналогічно, як і у 2D-режимі, за винятком того, що у WEBGL вершини отримують значення x, y і z як координати їх розташування.

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

sketch.js
let s = 50; (1)
let angle = 0;

function setup() {
  createCanvas(200, 200, WEBGL);
  noStroke();
}

function draw() {
  background(245);
  rotateY(-angle);
  rotateZ(angle);
  fill(100, 87, 166); // Ultra Violet
  sphere(25); (2)
  createPyramid(s); (3)
  angle += 0.01;
}

function createPyramid(s) { (4)
  beginShape(TRIANGLES);

  fill(255, 227, 71); // Mustard
  vertex(0, 0, s);
  vertex(-s, -s, -s);
  vertex(s, -s, -s);

  fill(239, 118, 122); // Light coral
  vertex(0, 0, s);
  vertex(s, -s, -s);
  vertex(0, s, -s);

  fill(0, 196, 154); // Mint
  vertex(0, 0, s);
  vertex(0, s, -s);
  vertex(-s, -s, -s);

  fill(114, 76, 249); // Majorelle Blue
  vertex(-s, -s, -s);
  vertex(s, -s, -s);
  vertex(0, s, -s);

  endShape(CLOSE);
}
1 Ініціалізуємо змінну s, яка визначатиме розміри піраміди.
2 Малюємо сферу для позначення центра системи координат тривимірного простору.
3 Викликаємо користувацьку функцію createPyramid() з аргументом s для побудови піраміди.
4 Описуємо користувацьку функцію createPyramid(), яка прийматиме аргумент s, значення якого буде використовуватися для формування координат x, y і z у побудові вершин піраміди.

Переглядаємо Аналізуємо

Вправа 68

Змінити код застосунку для малювання прозорих граней 3D-піраміди.

6.5.6. Використання 3D-моделей

Усі 3D-моделі, які використані у підручнику, завантажені із сайту rigmodels.com .

Бібліотека p5.js, окрім низки 3D-примітивів, також здатна відтворювати 3D-моделі з OBJ-файлів або STL-файлів, створених в інших застосунках, як-от Blender .

Для цього спочатку потрібно завантажити модель у тіло функції preload() за допомогою функції loadModel() , а потім намалювати її, використовуючи функцію model() .

Щоб нормалізувати імпортовану із файлу в ескіз 3D-модель до розміру для перегляду, у виклику loadModel() другим аргументом необхідно вказати значення true.

Розглянемо застосунок, який завантажує 3D-модель з OBJ-файлу і відображає її на полотні.

Завантажити файл 3D-моделі можна за покликанням.
sketch.js
let dog; (1)

function preload() {
  dog = loadModel("frankie.obj", true); (2)
}

function setup() {
  createCanvas(200, 200, WEBGL);
}

function draw() {
  background(245);
  translate(mouseX - width / 2, mouseY - height / 2, 0); (3)
  rotateX(PI); (4)
  rotateY(frameCount * 0.01); (5)
  scale(0.8); (6)
  model(dog); (7)
}
1 Оголошуємо змінну dog, яка буде покликатися на об’єкт завантаженої із файлу 3D-моделі.
2 Імпортуємо 3D-модель із файлу frankie.obj за допомогою функції loadModel(), у виклику якої другим аргументом зазначаємо true для нормалізації вигляду моделі. Зберігаємо імпортовану модель під ім’ям dog.
3 Переміщуємо систему координат в точку, в якій перебуває вказівник миші.
4 Перевертаємо модель вертикально.
5 Обертаємо модель навколо осі Y.
6 Зменшуємо розміри моделі.
7 Відображаємо модель на полотні за допомогою виклику функції model(), аргументом для якої буде об’єкт під ім’ям dog завантаженої із файлу моделі.

Переглядаємо Аналізуємо

Цікавимось Додатково

6.5.7. 3D-сцена та її складові

Коли бібліотека p5.js виконує рендеринг 3D-сцени, вона абстрагує багато складнощів, пов’язаних із перетворенням 3D-фігури у 2D-зображення, яке ми бачимо на екрані комп’ютера.

Ці складнощі пов’язані з процесом створення 3D-сцени, в якому важливу роль відіграють камера, світло та матеріали.

Камера

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

Камера є важливою складовою 3D-сцени, оскільки вона впливає на реалістичність та сприйняття тривимірної графіки.

Камера - точка огляду 3D-сцени. Звичайно, мова йде не про фізичну камеру, а про імітацію камери, яка визначає, як об’єкти у тривимірному просторі відображаються на двовимірному екрані.

Керувати положенням і напрямком огляду камери у p5.js можна за допомогою функції camera() .

Перші три аргументи для camera() - це положення камери (x, y, z) у 3D-просторі.

Переглядаємо Аналізуємо

Наступні три аргументи - це координати (x, y, z) точки на полотні, куди спрямована камера і за стандартним налаштуванням дорівнюють (0, 0, 0). Останні три аргументи - це напрямок «вгору» від камери (орієнтація камери), зазвичай (0, 1, 0).

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

Розглянемо застосунок, який використовує камеру для огляду завантаженої із файлу 3D-моделі.

За допомогою вкладених циклів і функції map(), яка змінює числове значення з одного діапазону в інший, обчислимо координати x та z для малювання імпортованої в ескіз 3D-моделі за допомогою виклику translate(x, 0, z).

Завантажити файл 3D-моделі можна за покликанням.
sketch.js
let bird;

function preload() {
  bird = loadModel("chuck.obj", true);
}

function setup() {
  createCanvas(200, 200, WEBGL);
  camera(0, -120, 300, 0, 0, 0, 0, 1, 0);
}

function draw() {
  background(245);

  for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
      let x = map(i, 0, 1, -90, 90);
      let z = map(j, 0, 1, -100, 100);
      push();
      translate(x, 0, z);
      rotateX(PI);
      rotateY(frameCount * 0.01);
      model(bird);
      pop();
    }
  }
}

Переглядаємо Аналізуємо

Камера, яку створює за стандартним налаштуванням функція camera(), є перспективною, оскільки вона створює ілюзію глибини, відтворюючи 3D-об’єкти поблизу камери в їх фактичному розмірі та зменшуючи об’єкти, які розташовані далі.

Змінити стандартні параметри камери можна за допомогою викликів функцій:

  • perspective() - встановлює перспективну проєкцію для поточної камери в 3D-ескізі;

  • ortho() - встановлює ортогональну проєкцію для поточної камери в 3D-ескізі.

Перспективна проєкція:

  • створює видимість глибини, завдяки чому об’єкти на відстані здаються меншими, а ті, що розташовані ближче до глядача в z-площині, виглядають більшими.

Ортогональна (прямокутна) проєкція:

  • об’єкти однакових розмірів виглядають однаковими, навіть якщо вони розташовані далі на z-площині, що створює враження двовимірності.

Одним із параметрів, які можна змінити за допомогою функції perspective() - це поле зору (англ. field of view, FoV - широта спостережуваного світу, яка спостерігається в будь-який момент) або інакше кут огляду, який впливає на те, як форми змінюють розмір на відстані.

Розглянемо застосунок, в якому кут огляду камеру для завантаженої із файлу 3D-моделі змінюється за допомогою переміщення вказівника миші.

Завантажити файл 3D-моделі можна за покликанням.
sketch.js
let bird;

function preload() {
  bird = loadModel("red.obj", true);
}

function setup() {
  createCanvas(200, 200, WEBGL);
  camera(0, 0, 200, 0, 0, 0, 0, 1, 0);
}

function draw() {
  background(245);

  let fieldOfView = radians(map(mouseY, 0, height, 60, 30));
  perspective(fieldOfView);
  translate(0, 0, 0);
  rotateX(PI);
  rotateY(frameCount * 0.01);
  model(bird);
}

Переглядаємо Аналізуємо

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

sketch.js
let bird;

function preload() {
  bird = loadModel("chuck.obj", true);
}

function setup() {
  createCanvas(200, 200, WEBGL);
  ortho(-width / 2, width / 2, height / 2, -height / 2, 0, 500);
}

function draw() {
  background(245);

  rotateX(0.2);
  rotateY(-0.2);

  push();
  translate(-50, 0, sin(frameCount / 30) * 100);
  model(bird);
  pop();

  push();
  translate(50, 0, sin(frameCount / 30 + PI) * 100);
  model(bird);
  pop();
}

Переглядаємо Аналізуємо

Для кращого розуміння того, як працює камера, скористайтеся інтерактивною демонстрацією за покликанням.

Переглядаємо Аналізуємо

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

  • щоб змінити масштаб камери, прокручуйте колесо миші;

  • щоб повернути камеру, натисніть ліву кнопку миші та перетягніть полотно;

  • щоб перемістити камеру, натисніть праву кнопку миші та перетягніть полотно.

Ще однією корисною функцією для роботи з камерою є функція debugMode() , яка вміє малювати сітку та індикатор осей на полотні.

У наступному застосунку намалюємо сітку та осі координат.

sketch.js
let bird;

function preload() {
  bird = loadModel("red.obj", true);
}

function setup() {
  createCanvas(200, 200, WEBGL);
  camera(0, 0, 200, 0, 0, 0, 0, 1, 0);
  debugMode(300, 10, 0, 0, 0, 150, 0, -100, 0); (1)
}

function draw() {
  background(245);

  orbitControl(1, 1); (2)

  translate(0, 0, 0);
  rotateX(PI);
  rotateY(frameCount * 0.01);
  model(bird);
}
1 Перший аргумент (300) у виклику функції - розмір однієї сторони сітки, другий (10) - кількість поділок одної сторони сітки, наступні три (0, 0, 0) - зміщення сітки від точки (0, 0, 0), шостий (150) - розмір індикатора осей, останні три (0, -100, 0) - зміщення осей відносно точки (0, 0, 0).
2 Аргументи у виклику функції визначають чутливість до руху миші/дотику вздовж осей X та Y. Виклик функції orbitControl() без аргументів еквівалентний виклику orbitControl(1, 1). Щоб змінити напрямок руху будь-якої осі, необхідно ввести від’ємне число для чутливості.

Переглядаємо Аналізуємо

Вправа 69

Змінити параметри застосунку, щоб індикатор осей розташувався в центрі об’єкта, а сам об’єкт - на поверхні сітки.

Освітлення

Освітлення, як ще одна складова 3D-сцени, - простий, але потужний спосіб забезпечити глибину та реалістичність ескізів p5.js.

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

Для увімкнення освітлення за стандартним налаштуванням можна скористатися функцією lights() .

Освітлення потрібно «вмикати» у блоці draw().

Наприклад, використаємо функцію lights() для освітлення сфери (зауважте, сфера не виглядає тривимірною, доки її не освітлять) за умови, коли будь-яка з кнопок миші натиснута.

Для цього перевірятимемо значення змінної lightIsOn, яка набуватиме по черзі значень true або false при натисканні кнопки миші. За натисканням кнопок миші слідкуватиме функція mousePressed() , яка у момент натискання кнопки миші змінюватиме значення lightIsOn на протилежне.

sketch.js
let lightIsOn = false; // перемикач

function setup() {
  createCanvas(200, 200, WEBGL);
  noStroke();
}

function draw() {
  background(45, 132, 138); // Teal

  if (lightIsOn) {
    lights();
  } else {
    fill(237, 255, 122); // Mindaro
  }

  translate(0, 0, 0);
  sphere(50);
}

function mousePressed() {
  lightIsOn = !lightIsOn;
}

Переглядаємо Аналізуємо

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

  • ambientLight() - створює навколишнє освітлення заданим кольором, предмети рівномірно освітлені з усіх боків;

  • directionalLight() - створює спрямоване світло із заданим кольором, яке надходить з одного напрямку та є сильнішим, якщо воно потрапляє на поверхню прямо, і слабшим, якщо воно потрапляє під кутом;

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

  • pointLight() - створює точкове джерело світла із заданим кольором і положенням, яке випромінює з однієї точки в усіх напрямках та має різний ефект, коли джерело світла розташоване далі та ближче до об’єкта;

  • noLights() - видаляє всі джерела світла в ескізі.

Проаналізуємо код застосунку, який наведений нижче, закоментувавши та розкоментувавши виклик кожного джерела світла.

sketch.js
let lightIsOn = false; // перемикач (1)

function setup() {
  createCanvas(200, 200, WEBGL);
  noStroke();
}

function draw() {
  background(45, 132, 138); // Teal

  if (lightIsOn) { (2)
    lights();

    // ambientLight(255, 0, 0); (3)

    /*
    directionalLight( (4)
      255,
      0,
      0,
      -(mouseX / width - 0.5) * 2,
      -(mouseY / height - 0.5) * 2,
      -1
    );
    */

    /*
    spotLight( (5)
      255,
      0,
      0,
      mouseX - width / 2,
      mouseY - height / 2,
      100,
      0,
      0,
      -1,
      PI / 6,
      1
    );
    */

    // pointLight(255, 0, 0, mouseX - width / 2, mouseY - height / 2, 50); (6)
  } else {
    noLights();
    fill(237, 255, 122); // Mindaro
  }

  translate(0, 0, 0);
  sphere(50);
}

function mousePressed() {
  lightIsOn = !lightIsOn;
}
1 Ініціалізуємо змінну lightIsOn зі значенням false, яка позначатиме увімкнення ефектів освітлення.
2 Якщо lightIsOn має значення false, спрацьовує виклик функції noLights() - усі джерела світла видаляються і сфера зафарбовується в колір Mindaro, інакше - відбувається виклик функції lights() джерела світла або іншої з розкоментованих пунктів 3-7.
3 Рівномірне освітлення сфери з усіх боків червоним кольором 255, 0, 0.
4 Спрямоване освітлення червоним кольором 255, 0, 0 і напрямком освітлення -(mouseX / width - 0.5) * 2, -(mouseY / height - 0.5) * 2, -1 по осях у діапазоні від -1 до 1 включно.
5 Освітлення прожектором червоного кольору 255, 0, 0 з точки із координатами mouseX - width / 2, mouseY - height / 2, 100, напрямком світла 0, 0, -1 вздовж конічної форми, яка визначається кутом PI / 6 (за стандартним налаштуванням PI / 3) і концентрацією 1 (за стандартним налаштуванням 100).
6 Освітлення від точкового джерела світла із червоним кольором 255, 0, 0 і положенням джерела світла у точці mouseX - width / 2, mouseY - height / 2, 50.

Переглядаємо Аналізуємо

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

Переглядаємо Аналізуємо

Матеріали

Матеріали дозволяють надати 3D-об’єктам реалістичний вигляд, залежно від того, як вони взаємодіють зі світлом. Наприклад, матеріал може визначати, чи об’єкт відбиває світло, якого кольору має бути об’єкт, чи має він блискучість, чи об’єкт випромінює світло.

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

Бібліотека p5.js має низку вбудованих функцій для встановлення різних типів матеріалів, таких як:

  • normalMaterial() - встановлює поточний матеріал як звичайний, який не реагує на окремі джерела світла, тому його часто використовують як матеріал-заповнювач в RGB-кольорах під час налагодження;

  • ambientMaterial() - встановлює для матеріалу колір зі складовими (компонентами) кольору функції освітлення ambientLight(), які відбиває об’єкт;

  • emissiveMaterial() - встановлює колір для випромінювання світла з поверхні об’єкта (ефект світіння), який не змінюється в залежності від освітлення 3D-сцени;

  • specularMaterial() - встановлює колір, зі складовими кольору функції освітлення ambientLight(), які об’єкт відбиває. Однак, на відміну від ambientMaterial(), для всіх інших типів світла (directionalLight(), pointLight(), spotLight()), дзеркальний матеріал відображатиме колір джерела світла і саме це надає йому «блискучого» вигляду (блиск можна контролювати за допомогою функції shininess() ).

Поміркуємо над утворенням кольору (матеріалу) об’єкта, який реагує на амбієнтне освітлення в 3D-сцені. Для цього розглянемо функцію ambientMaterial() із жовтим кольором (255, 255, 0) і функцію ambientLight() з різними значеннями кольору освітлення.

Колір ambientMaterial() утворюється із компонентів кольору ambientLight(), які відбиває об’єкт. Якщо ambientLight() випромінює світло білого кольору (255, 255, 255), тоді об’єкт виглядатиме жовтим, оскільки для глядача він відбиває червоний і зелений компоненти світла, а синій - «поглинає».

Якщо ambientLight() випромінює світло червоного кольору (255, 0, 0), тоді об’єкт для глядача виглядатиме червоним, оскільки він відбиватиме червоний компонент світла, а зелений і синій - «поглинатиме».

Якщо ambientLight() випромінює світло синього кольору (0, 0, 255), то об’єкт для глядача буде виглядати чорним, оскільки немає компонентів світла, які він може відбити.

Колір об’єктів виникає внаслідок того, що певні кольори світла відбиваються від них.

У підсумку розглянемо код застосунку, в якому створимо об’єкти різних матеріалів.

sketch.js
function setup() {
  createCanvas(200, 200, WEBGL);
}
function draw() {
  background(245);

  // сфера
  push();
  translate(0, 0, 0);
  ambientLight(255, 0, 255); (1)
  ambientMaterial(255, 255, 255); (2)
  noStroke();
  sphere(30);
  pop();

  stroke(255);

  // куб у лівому верхньому куті
  push();
  translate(-50, -50, 35);
  ambientLight(255, 255, 255); (3)
  ambientMaterial(70, 130, 230); // Azure (4)
  box(30);
  pop();

  // куб у правому верхньому куті
  push();
  translate(50, -50, 35);
  ambientLight(255, 255, 0); (5)
  ambientMaterial(70, 130, 230); (6)
  box(30);
  pop();

  // куб у правому нижньому куті
  push();
  translate(50, 50, 35);
  ambientLight(0, 255, 0); (7)
  ambientMaterial(255, 0, 255); (8)
  box(30);
  pop();

  // куб у лівому нижньому куті
  push();
  translate(-50, 50, 35);
  ambientLight(255, 200, 87); // Sunglow (9)
  ambientMaterial(192, 253, 251); // Celeste (10)
  box(30);
  pop();
}
1 Сфера освітлюється світлом, яке має лише компоненти червоного і синього кольорів.
2 Для сфери встановлений білий колір матеріалу. Сфера відбиває лише червону та синю компоненти світла, що падає, і тому колір її матеріалу є 255, 0, 255 (Fuchsia).
3 Куб, який розміщений у лівому верхньому куті, освітлюється білим світлом.
4 Для куба, який розміщений у лівому верхньому куті, встановлений колір матеріалу 70, 130, 230 (Azure). Куб відбиває червоне, зелене та синє світло і має колір матеріалу 70, 130, 230 (Azure).
5 Куб, який розміщений у правому верхньому куті, освітлюється світлом, яке має лише компоненти червоного і зеленого кольорів.
6 Для куба, який розміщений у правому верхньому куті, встановлений колір матеріалу 70, 130, 230 (Azure). Куб відбиває червоне та зелене світло і має колір матеріалу 70, 130, 0 (Avocado).
7 Куб, який розміщений у правому нижньому куті, освітлюється світлом, яке має лише компонент зеленого кольору.
8 Для куба, який розміщений у правому нижньому куті, встановлений колір матеріалу 255, 0, 255 (Fuchsia). Оскільки куб не містить зеленого кольору, він не відбиває світло і має колір матеріалу чорний.
9 Куб, який розміщений у лівому нижньому куті, освітлюється світлом, яке має усі компоненти кольору і відповідно значення 255, 200, 87 (Sunglow).
10 Для куба, який розміщений у лівому нижньому куті, встановлений колір матеріалу 192, 253, 251 (Celeste). Куб відбиває червоне, зелене та синє світло і має після обчислень колір матеріалу 192, 198, 86 (Citron).
Створення матеріалів
Створення матеріалів
Для перегляду результату відбивання світла для різних типів матеріалів скористайтеся інтерактивною демонстрацією за покликанням.

Переглядаємо Аналізуємо

Текстури
Текстури - це зображення, які можуть бути прикріплені до 2D- або 3D-об’єктів, щоб надати об’єктам додатковий вигляд і візуальні деталі.

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

У p5.js для застосування текстур до об’єктів використовується функція texture() , яка дозволяє прикріпити зображення до геометричного об’єкта.

Розглянемо код застосунку, який використовує текстури для створення планети Юпітер у міжзоряному просторі.

Завантажити файли текстур можна за покликанням.
sketch.js
let jupiter, stars; (1)

function preload() { (2)
  jupiter = loadImage("2k_jupiter.jpg");
  stars = loadImage("2k_stars.jpg");
}

function setup() {
  createCanvas(200, 200, WEBGL);
  noStroke();
  textureMode(NORMAL);
}

function draw() {
  texture(stars); (3)
  plane(width, height);

  rotateY(frameCount * 0.0035);
  scale(0.6);
  texture(jupiter); (4)
  sphere(75);
}
1 Оголошуємо змінні jupiter і stars, які будуть покликатися на об’єкти текстур.
2 За допомогою функції preload() завантажуємо файли із зображеннями текстур та зберігаємо з іменами jupiter і stars відповідно.
3 Викликаємо функцію texture() для додавання текстури stars міжзоряного простору до 3D-об’єкта (площина), створеного за допомогою plane().
4 Викликаємо функцію texture() для додавання текстури jupiter планети до 3D-об’єкта (сфера), створеного за допомогою sphere().

Переглядаємо Аналізуємо

Функцію texture() можна використовувати для роботи з текстом у режимі WEBGL.

Розглянемо код застосунку, який малює текст як текстуру. Для наших цілей приєднаємо до ескізу шрифт та використаємо функції push() і pop(), які будуть зберігати й відновлювати матрицю трансформацій, щоб не впливати на решту 3D-сцени.

Також, змінюючи значення координати z у translate(), будемо контролювати взаємне розташування об’єктів з різними текстурами у тривимірному просторі.

Завантажити файл шрифту можна за покликанням.
sketch.js
let jupiter, stars, myFont, textTexture;

function preload() {
  jupiter = loadImage("2k_jupiter.jpg");
  stars = loadImage("2k_stars.jpg");
  myFont = loadFont("CooperHewitt-Medium.otf"); (1)
}

function setup() {
  createCanvas(200, 200, WEBGL);
  noStroke();
  textureMode(NORMAL);

  textTexture = createGraphics(100, 100); (2)
  textTexture.textFont(myFont);
  textTexture.textSize(30);
  textTexture.fill(214, 153, 207); // Plum (web)
  textTexture.textAlign(CENTER, CENTER);
  textTexture.text("Jupiter", textTexture.width / 2, textTexture.height / 2);
}

function draw() {
  background(245);

  // міжзоряна площина
  push();
  translate(0, 0, -50); (3)
  texture(stars);
  plane(width + 50, height + 50);
  pop();

  // планета Юпітер
  push();
  translate(0, 0, 0); (4)
  rotateY(frameCount * 0.009);
  scale(0.6);
  texture(jupiter);
  sphere(75);
  pop();

  // текстовий напис
  push();
  translate(90, 100, -40); (5)
  texture(textTexture); (6)
  plane();
  pop();
}
1 Завантажуємо шрифт у тіло функції preload() за допомогою loadFont().
2 Створюємо текстуру з ім’ям textTexture для текстового напису. За допомогою функції createGraphics() малюємо полотно в закадровому графічному буфері вказаних розмірів і встановлюємо параметри тексту для цього полотна.
3 Розташовуємо площину зоряного неба якнайдалі.
4 Розташовуємо планету у центрі системи координат.
5 Розташовуємо текстовий напис ближче до глядача, порівняно з площиною зір.
6 Застосовуємо текстуру textTexture, яка містить зрендерований текст із вказаними налаштуваннями шрифту, за допомогою функції texture() на площині, яку будуємо за допомогою функції plane().

Переглядаємо Аналізуємо

6.5.9. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «матриця трансформації»?

  2. Як працюють трансформації переміщення, обертання і масштабування?

  3. Які налаштування необхідно зробити для створення застосунків у 3D-режимі?

  4. Навести приклади 3D-примітивів.

  5. Як імпортувати 3D-модель із файлу в ескіз?

  6. Що таке «3D-сцена»? Схарактеризувати складові 3D-сцени.

6.5.10. Практичні завдання

Початковий

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

  1. Створити застосунок, в якому у центрі полотна обертається текстовий рядок. Орієнтовний взірець роботи застосунку представлений в демонстрації.

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

  1. Створити 3D-застосунок, в якому за допомогою миші можна обертати дві площини, одна з яких є меншою за розмірами та розташована за іншою. Орієнтовний зразок роботи застосунку представлений в демонстрації.

Середній

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

Завантажити файл зображення можна за покликанням.
  1. Створити застосунок, в якому Tony вчиться обертатися у польоті. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Тоні
Тоні
  1. У бібліотеці p5.js функція box() використовується для створення куба. Створити застосунок, який будує тривимірний простір за допомогою функцій beginShape(), vertex() і endShape(). Орієнтовний взірець роботи застосунку представлений в демонстрації.

Високий

  1. Створити 3D-застосунок, в якому у тривимірному просторі обертаються з різними швидкостями навколо свого центру площини різних розмірів. За потреби, можна вказувати, навколо яких осей відбувається обертання. Орієнтовний взірець роботи застосунку, в якому обертання відбувається навколо осі Z, представлений в демонстрації.

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

  1. Створити 3D-застосунок, в якому у тривимірному просторі хаотично обертаються піраміди. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Екстремальний

  1. Створити застосунок-годинник. Орієнтовні зразки годинників представлені у демонстраціях.

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

6.6. Бібліотеки для роботи з мультимедійними даними

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

6.6.1. ASCII Art

p5.asciiart - це простий у використанні графічний конвертер зображень в ASCII-символи для p5.js. Розробником бібліотеки p5.asciiart є Павел Яніцкі (Pawel Janicki) .

Цікавимось

ASCII-графіка (ASCII art)

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

Приклад ASCII-арту:

\|/          (__)
     `\------(oo)
       ||    (__)
       ||w--||     \|/
   \|/

Існує більш технічно удосконалене розширення ASCII-графіки - ANSI-графіка (ANSI art).

ANSI art - це комп’ютерна форма мистецтва, яка побудована з більшого набору символів - розширеної таблиці ASCII, 16 кольорів шрифту і 8 кольорів тла, і використовувалася в середовищах операційних систем MS-DOS та Unix.

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

б
Перша відома картина мистецтва друкарських машинок: зображення метелика, зроблене Флорою Стейсі, британським секретарем, у 1898 році

Щоб використати бібліотеку p5.asciiart, її необхідно завантажити зі сторінки tetoki.eu/asciiart і розпакувати завантажений архів.

У разі, якщо бібліотека p5.asciiart приєднується локально вручну, для запуску застосунків ASCII-арту необхідно запустити локальний вебсервер із каталогу, де містяться каталоги asciiart, examples, p5 і переглянути результат у вебпереглядачі, перейшовши за адресою http://127.0.0.1:port/examples/asciiart_stillimage_example.html, де port - номер порту.

Також, для роботи з бібліотекою p5.asciiart можна використовувати Processing IDE, розмістивши файли зображень у каталозі проєкту, правильно прописати шляхи до asciiart_stillimage_example.js і p5.asciiart.min.js у файлі index.html і закоментувати вміст файлу sketch.js задля уникнення конфліктів.

Щоб додати власне зображення для конвертації в ASCII, необхідно «покласти» зображення у каталог examples, а у файлі asciiart_stillimage_example.js у функції preload() записати шлях до зображення згідно із наведеним зразком.

sciiart_stillimage_example.js
...
function preload() {
  // "Young man reading by candlelight", Matthias Stom, 1600-1650
  images[0] = loadImage('example_image_young_man_reading.jpg');
  // "Le Penseur", Auguste Rodin, 1880
  images[1] =loadImage('example_image_Thinking-Man.jpg');
  // "American Gothic", Grant DeVolson Wood, 1930
  images[2] = loadImage('example_image_American_Gothic.jpg');
  // "La Liseuse", Jean-Honoré Fragonard, 1770
  images[3] = loadImage('example_image_young_girl_reading.jpg');
}
...

Переглядаємо Аналізуємо

Вправа 70

Використати бібліотеку p5.asciiart.js для конвертації власних світлин в ASCII-символи.

6.6.2. Shape5

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

Розробником бібліотеки Shape5.js є Патрік Естер (Patrick Ester) . Ідея проєкту Shape5.js полягає у зниженні порогу входження у створенні статичного мистецтва за допомогою p5.js.

Встановимо і приєднаємо бібліотеку локально, виконавши наступні кроки.

  1. Завантажити архів з файлом бібліотеки та іншими файлами (зелена кнопка з написом Code) із GitHub -сховища.

  2. Розпакувати архів і відкрити каталог Shape5 Template. Файл shape5.js - це і є файл бібліотеки Shape5, який вже приєднаний до проєкту (у цьому можна переконатися, відкривши файл index.html у редакторі коду).

  3. Записати свій код у файл ескізу sketch.js.

  4. Відкрити файл index.html у вебпереглядачі, щоб переглянути результати виконання коду.

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

Поняття об’єктоорієнтованого програмування більш детально розглядаються у розділі Об’єкти та класи.

Файл бібліотеки shape5.js містить класи об’єктів (об’єктами є коло, прямокутник, овал тощо) та їхні атрибути (розмір, колір тощо). Кожен клас має метод .show(), який використовується для малювання фігури на полотні.

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

Загалом алгоритм створення фігури за допомогою бібліотеки Shape5.js такий:

  1. Оголошення змінної на основі класу фігури.

  2. Модифікації фігури - зміна значень її атрибутів.

  3. Малювання фігури.

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

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

variableName.attribute = value;

де

  • variableName - ім’я змінної, що позначає конкретний об’єкт;

  • . - символ крапки, який використовується для доступу до атрибутів об’єкта;

  • символ = - операція присвоєння;

  • value - значення, яке присвоюють атрибуту об’єкта;

  • ; - символ завершення інструкції.

За стандартним налаштуванням Shape5.js використовує Hex CSS-кольори. Також можна використовувати традиційну функцію color() із бібліотеки p5.js.

Отже, напишемо код для створення статичного малюнка за допомогою бібліотеки Shape5.js.

sketch.js
let b; (1)

function setup() {
  createCanvas(200, 200);
  b = new Square(); (2)
}

function draw() {
  background("#4C1E4F"); // Palatinate (3)
  b.x = 100;
  b.y = 100;
  b.size = 50;
  b.color = color(255, 239, 213); // Papaya whip
  b.spin = 45; (4)
  b.show(); (5)
}
1 Оголошення глобальної змінної з ім’ям b.
2 Створення екземпляра класу Square. Тобто, на основі класу Square створюється конкретний об’єкт - квадрат з ім’ям b і набором атрибутів за стандартним налаштуванням (переглянути атрибути за стандартним налаштуванням можна у файлі shape5.js для класу Square).
3 Встановлення кольору полотна за допомогою синтаксису Hex CSS.
4 Модифікація значення фігури - встановлення для квадрата b кута повороту 45° за допомогою звернення до атрибута spin.
5 Використання методу show() для малювання квадрата b на полотні.

В результаті виконання коду отримуємо фігуру - ромб.

Ромб
Ромб
До речі, бібліотека Shape5.js має клас Rhombus для створення ромбів 😉 .

Вправа 71

Створити застосунок, використовуючи фігури із бібліотеки Shape5.js.

Для миттєвого застосування Shape5.js відкрийте зразок ескізу , до якого вже приєднана бібліотека.

6.6.3. p5.sound.js

Бібліотека p5.sound.js , яка використовується в тандемі з p5.js, розширює останню за допомогою функцій Web Audio - API, який надає інструменти для керування відтворенням аудіо на вебсторінці, вибору аудіоджерела, додавання ефектів до аудіо, записування і зберігання звуку та інші.

Розробником бібліотеки p5.sound.js є Джейсон Сігал (Jason Sigal) .
Архівна адреса вебсторінки бібліотеки розташована за адресою p5.sound.js . Покликання на описи функцій за архівною адресою позначаються міткою Архів .

Для роботи зі звуком бібліотека має широкий набір інструментів. Обмежимось знайомством лише із базовими можливостями бібліотеки та приєднаємо її стиснену версію p5.sound.min.js у файлі index.html.

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <meta charset="utf-8" />
  </head>
  <body>
    <main></main>
    <script src="sketch.js"></script>
  </body>
</html>
Зразки аудіофайлів (семпли) для власних експериментів, зокрема й ті, що використовуються у прикладах ескізів цього розділу, можна безплатно завантажити з бібліотеки звуків Sample Focus - онлайн-платформи для обміну та пошуку звуків, яку підтримує Інтернет-спільнота. Покликання є запрошувальним, тобто, якщо ви зареєструєтесь за ним на сайті Sample Focus, ви отримаєте 5 (+ 20) кредитів для безплатного завантаження звуків (1 кредит - 1 завантаження).

Цікавимось

Що таке звук?

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

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

Для людського вуха доступний діапазон сприйняття звуків від 16 Гц до 20 000 Гц. Всі звуки нижчого діапазону називаються інфразвуками, а ті, які вище, - ультразвуками.

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

Завантаження та відтворення звуку

Розглянемо код застосунку, за допомогою якого в ескіз завантажується аудіофайл і створюється кілька HTML-елементів у DOM для керування відтворенням звуку у вебпереглядачі.

Робота із DOM детально розглядається в Додатку B.
Щоб використовувати звукові файли в ескізі, дотримуйтесь того ж алгоритму, що й у разі використання бібліотек для роботи із зображеннями. А саме, спочатку потрібно відшукати потрібний аудіофайл, скопіювати його у каталог ескізу, а потім завантажити дані звукового файлу в ескіз.
Звуковий файл, що використовується у застосунку, можна завантажити за покликанням.
sketch.js
let sliderSetVolume, sliderRate, checkboxLoop, btn; (1)
let soundFile, bgColor; (2)

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav"); (3)
}

function setup() {
  createCanvas(200, 200);

  sliderRate = createSlider(0.5, 1.5, 1, 0.1); (4)
  sliderSetVolume = createSlider(0.0, 1.0, 0.5, 0.1); (5)

  checkboxLoop = createCheckbox("циклічно", false); (6)
  checkboxLoop.changed(сheckedLooping); (7)

  btn = createButton("відтворити"); (8)
  btn.mousePressed(switchPlaying); (9)

  soundFile.addCue(2, changeBgColor, color(97, 83, 204)); // Iris (10)
  soundFile.addCue(5, changeBgColor, color(4, 114, 77)); // Dark spring green
  soundFile.addCue(9, changeBgColor, color(166, 0, 103)); // Murrey

  bgColor = color(22, 27, 51); // Oxford Blue (11)

  textAlign(CENTER, CENTER);
}

function draw() {
  background(bgColor); (12)

  soundFile.setVolume(sliderSetVolume.value()); (13)
  soundFile.rate(sliderRate.value()); (14)

  let t = soundFile.duration() - soundFile.currentTime(); (15)

  fill(255);
  text(t, width / 2, height / 6); (16)
}

function changeBgColor(c) { (17)
  bgColor = c;
}

function switchPlaying() { (18)
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() { (19)
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Оголошуємо змінні, які позначатимуть елементи інтерфейсу: sliderSetVolume (регулювання звуку відтворення), sliderRate (регулювання швидкості відтворення звукового файлу й зміни тону відповідно), checkboxLoop (чи відтворення звуку буде циклічним) і btn (кнопка відтворення/паузи).
2 Оголошуємо змінні: soundFile - назва покликання на об’єкт завантаженого звукового файлу, bgColor - значення кольору полотна.
3 Завантажуємо у функції preload() звуковий файл за допомогою функції loadSound() і зазначаємо, що покликанням на звуковий файл буде ім’я soundFile.
4 Створюємо слайдер під ім’ям sliderRate для регулювання швидкості відтворення звукового файлу й зміни тону відповідно.
5 Створюємо слайдер під ім’ям sliderSetVolume для встановлення максимальної гучності звуку відтворення.
6 Створюємо чекбокс checkboxLoop для увімкнення/вимкнення циклічного відтворення звукового файлу. За стандартним налаштуванням циклічне відтворення звукового файлу вимкнено.
7 Прив’язуємо до checkboxLoop функцію changed() - слухача події зміни значення чекбокса. Аргумент сheckedLooping() - функція-обробник цієї події.
8 Створюємо кнопку btn з написом Відтворити.
9 Прив’язуємо до btn функцію mousePressed() - слухача події натискання кнопки. Аргумент switchPlaying() - функція-обробник цієї події.
10 Використовуємо функцію addCue() , за допомогою якої плануємо виклики користувацької функції changeBgColor() зі значенням кольору color(97, 83, 204) (color(4, 114, 77), color(166, 0, 103)) щоразу коли відтворення звукового файлу досягне 2 секунди (5 секунд, 9 секунд) від початку відтворення.
11 Встановлюємо початкове значення кольору bgColor для полотна.
12 У блоці draw() для функції background() використовується поточне значення кольору bgColor.
13 Для soundFile за допомогою функції setVolume() Архів встановлюємо максимальну гучність, використовуючи поточне значення слайдера sliderSetVolume, де гучність (амплітуда звукової хвилі) відтворення звукового файлу вимірюється у діапазоні між 0.0 (тиша) і 1.0 (повна гучність).
14 Для soundFile за допомогою функції rate() встановлюємо швидкість відтворення звукового файлу і тон відповідно, використовуючи поточне значення слайдера sliderRate.
15 Обчислюємо час t, який залишився до кінця одного відтворення звукового файлу. Для цього обчислюємо різницю результатів викликів двох функції: duration() (повертає тривалість звукового файлу в секундах) і currentTime() Архів (повертає поточне положення позиції відтворення від початку у секундах).
16 Розміщуємо значення t з пункту 15 у центрі полотна.
17 Оголошення користувацької функції-обробника changeBgColor(), яка що виклику буде змінювати значення кольору для полотна.
18 Оголошення користувацької функції-обробника switchPlaying(), яка що виклику буде змінювати стан кнопки btn. Якщо значення soundFile.isPlaying(), де функція isPlaying() повертає true, якщо звук відтворюється, інакше - false (тобто відтворення призупинено або зупинено), набуває значення false, файл soundFile буде відтворюватися за допомогою play() і на кнопці за допомогою функції html() буде встановлений напис пауза. Інакше - файл soundFile буде призупинений за допомогою pause() і на кнопці за допомогою функції html() буде встановлений напис продовжити.
19 Оголошення користувацької функції-обробника сheckedLooping(), яка для soundFile за допомогою функції setLoop() буде вмикати відтворення звуку у циклі (true) чи ні (false) в залежності від того, чи відзначений чекбокс (checkboxLoop.checked()). Якщо звук вже відтворюється, ця зміна почне діяти після завершення поточного відтворення.

Переглядаємо Аналізуємо

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

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

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

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

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

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

Доповнимо код нашого плеєра згідно із нашими міркуваннями.

sketch.js
let sliderSetVolume, sliderRate, checkboxLoop, btn;
let soundFile, bgColor;
let amplitude; (1)

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav");
}

function setup() {
  createCanvas(200, 200);

  sliderRate = createSlider(0.5, 1.5, 1, 0.1);
  sliderSetVolume = createSlider(0.0, 1.0, 0.5, 0.1);

  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);

  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);

  soundFile.addCue(2, changeBgColor, color(97, 83, 204)); // Iris
  soundFile.addCue(5, changeBgColor, color(4, 114, 77)); // Dark spring green
  soundFile.addCue(9, changeBgColor, color(166, 0, 103)); // Murrey

  bgColor = color(22, 27, 51); // Oxford Blue

  textAlign(CENTER, CENTER);

  amplitude = new p5.Amplitude(); (2)
}

function draw() {
  background(bgColor);

  soundFile.setVolume(sliderSetVolume.value());
  soundFile.rate(sliderRate.value());

  let t = soundFile.duration() - soundFile.currentTime();

  fill(255);
  text(t, width / 2, height / 6);

  let currentAmplitude = amplitude.getLevel(); (3)
  let d = map(currentAmplitude, 0, sliderSetVolume.value(), 1, width); (4)

  noStroke();
  fill(218, 98, 125); // Blush
  ellipse(width / 2, height / 2, d); (5)
}

function changeBgColor(c) {
  bgColor = c;
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Оголошуємо змінну з ім’ям amplitude, яке буде покликанням на об’єкт, створений класом p5.Amplitude.
2 Створюємо сам об’єкт поточного значення амплітуди за допомогою класу p5.Amplitude і надаємо йому ім’я amplitude.
3 За допомогою виклику методу getLevel() на об’єкті amplitude отримуємо щоразу поточне значення амплітуди currentAmplitude.
4 Перетворюємо значення currentAmplitude з діапазону від 0 до sliderSetVolume.value() (максимальне значення гучності, встановлене слайдером, може бути 1.0) у діапазон від 1 до значення ширини полотна width і зберігаємо під ім’ям d.
5 Малюємо коло в центрі полотна розміром d.

Переглядаємо Аналізуємо

Використаємо інший спосіб візуалізації звуку - намалюємо статичний графік значень амплітуди звукового файлу.

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

Отож, код застосунку з попереднього прикладу зазнає таких змін.

sketch.js
let btn, checkboxLoop, soundFile;
let peaks; (1)

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav");
}

function setup() {
  createCanvas(700, 200);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);

  peaks = soundFile.getPeaks(width); (2)
}

function draw() {
  background(245);

  stroke(106, 76, 147); // Ultra Violet
  for (let i = 0; i < peaks.length; i++) { (3)
    line(
      i,
      height / 2 + peaks[i] * height / 2,
      i,
      height / 2 - peaks[i] * height / 2
    );
  }

  let moment = map(soundFile.currentTime(), 0, soundFile.duration(), 0, width); (4)
  stroke(242, 73, 163); // Rose Bonbon
  line(moment, 0, moment, height); (5)
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Оголошуємо змінну з ім’ям peaks, яке буде покликанням на масив піків амплітуди звукового файлу soundFile.
2 Застосовуємо на об’єкті soundFile метод getPeaks(). Метод приймає один параметр - розмір масиву. Більші масиви забезпечують більш точну візуалізацію форми звукового сигналу (за стандартним налаштуванням, значення параметра - 5 * ширина вікна вебпереглядача). Цього разу використовується розмір масиву, що дорівнює ширині полотна width, тому у масиві 700 елементів. За ім’ям peaks можна отримати доступ до цього масиву відповідно.
3 У блоці draw() проходимо по масиву peaks, отримуємо значення peaks[i] конкретного піку амплітуди та використовуємо це значення для формування y-координат початку і кінця окремої вертикальної лінії. x-координати початку і кінця лінії збільшується завдяки зростанню лічильника i у циклі for. Оскільки значення масиву peaks є доволі маленькими, то для формування y-координат початку і кінця лінії використовуємо добуток peaks[i] * height / 2. Половина конкретної лінії малюється вниз, а половина - вгору від центру полотна.
4 За допомогою функції map() змінюємо для значень soundFile.currentTime() (поточна позиція відтворення звукового файлу у реальному часі) діапазон значень від 0 до soundFile.duration() (тривалість звукового файлу в секундах) на діапазон від 0 до ширини полотна width і зберігаємо під ім’ям moment. Іншими словами, тривалість відтворення звукового файлу тепер дорівнює ширині полотна.
5 Малюємо вертикальну лінію одномоментно під час відтворення звукового файлу.

Переглядаємо Аналізуємо

Вигляд графіка піків амплітуди для звукового файлу можна отримати, наприклад, відкривши аудіофайл в застосунку Audacity .

Тепер змусимо графік динамічно рухатися в міру відтворення звукового файлу.

Для наших цілей створимо окремий масив amplitudes, який буде містити поточні значення амплітуди, отримані у процесі відтворення аудіофайлу. А сам графік буде побудований за допомогою функцій beginShape(), vertex() і endShape().

sketch.js
let btn, sliderSetVolume, checkboxLoop, soundFile, amplitude;
let amplitudes = []; (1)

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav");
}

function setup() {
  createCanvas(700, 200);
  sliderSetVolume = createSlider(0.0, 1.0, 1.0, 0.1);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);
  amplitude = new p5.Amplitude();
  noFill();
}

function draw() {
  background(245);
  soundFile.setVolume(sliderSetVolume.value());
  let currentAmplitude = amplitude.getLevel();
  amplitudes.push(currentAmplitude); (2)

  stroke(106, 76, 147); // Ultra Violet
  push(); (3)
  translate(0, -height / 2); (4)
  beginShape(); (5)
  for (let i = 0; i < amplitudes.length; i++) { (6)
	let x = i;
    let y = map(amplitudes[i], 0, 1, height, 0);
    vertex(x, y); (7)
  }
  endShape(); (8)
  pop(); (9)

  if (amplitudes.length >= width / 2) { (10)
    amplitudes.splice(0, 1);
  }

  stroke(242, 73, 163); // Rose Bonbon
  line(amplitudes.length, 0, amplitudes.length, height); (11)
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Оголошуємо змінну з ім’ям amplitudes, яке буде покликанням на масив значень амплітуди.
2 Заповнюємо масив amplitudes поточними значеннями амплітуди під час відтворення звуку. Якщо звук не відтворюється, то масив заповнюється нулями (горизонтальна лінія у центрі полотна).
3 Зберігаємо поточний стан матриці трансформації, в якому початок системи координат у лівому верхньому куті полотна з координатами (0, 0).
4 Переміщуємо початок системи координат у точку (0, -height / 2) - центр лівої вертикальної межі полотна.
5 Початок побудови фігури.
6 Проходячи у циклі по масиву amplitudes, значення лічильника i буде визначати x-координату, а y-координата буде формуватися з елементів amplitudes[x] масиву, діапазон значень яких від 0 до 1 буде перетворено у діапазон від height до 0.
7 Побудова вершини з координатами (x, y), обчисленими з пункту 6.
8 Завершення побудови фігури.
9 Відновлюємо стан матриці трансформації.
10 Умова, за виконання якої значення по одному будуть видалятися з масиву amplitudes від його початку. Це зроблено для імітації руху графіка горизонтально, справа наліво. Доки ця умова не виконується - графік рухається горизонтально, зліва направо.
11 Малюємо вертикальну лінію, x-координати побудови початку і кінця якої визначаються розміром масиву amplitudes. Коли масив amplitudes заповнюється (пункт 2), x-координата збільшується, а отже лінія рухається горизонтально зліва направо. Як тільки умова з пункту 10 виконується, розмір масиву стає сталим - x-координата не змінюється і лінія малюється увесь час у центрі полотна.

Переглядаємо Аналізуємо

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

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

sketch.js
let btn, sliderSetVolume, checkboxLoop, soundFile, amplitude;
let amplitudes = [];

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav");
}

function setup() {
  createCanvas(200, 200);
  sliderSetVolume = createSlider(0.0, 1.0, 1.0, 0.1);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);
  amplitude = new p5.Amplitude();
  angleMode(DEGREES); (1)
}

function draw() {
  background(245);
  soundFile.setVolume(sliderSetVolume.value());
  let currentAmplitude = amplitude.getLevel();
  amplitudes.push(currentAmplitude); (2)

  stroke(106, 76, 147); // Ultra Violet
  translate(width / 2, height / 2); (3)
  fill(65, 66, 136); // Marian blue
  beginShape();
  for (let i = 0; i < 360; i++) { (4)
    if (amplitudes[i]) {
      let r = map(amplitudes[i], 0, 1, 1, height * 1.2); (5)
      let x = r * cos(i); (6)
      let y = r * sin(i);
      vertex(x, y);
    }
  }
  endShape();

  if (amplitudes.length > 360) { (7)
    amplitudes.splice(0, 1);
  }
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Вмикаємо за допомогою функції angleMode() режим інтерпретації DEGREES (градуси) значень для функцій sin() і cos().
2 Заповнюємо масив amplitudes поточними значеннями амплітуди під час відтворення звуку. Якщо звук не відтворюється, то масив заповнюється нулями.
3 Переміщуємо систему координат у центр полотна.
4 Проходимо у циклі до значення 360, де i - значення кута у градусах (повне коло 360 градусів) і формуємо координати вершин, які будуть побудовані за допомогою функції vertex(). Утворення координат вершин відбувається за умови існування елемента amplitudes[i] зі значенням, відмінним від 0. Як вже було зазначено у пункті 2, якщо звук не відтворюється, а застосунок вже запущений, то масив буде заповнюватися нулями. Щоб функція map() коректно спрацювала, для цього і використовується умова існування елемента amplitudes[i] зі значенням, відмінним від 0.
5 В ході циклу змінюємо діапазон від 0 до 1 у діапазон від 1 до height * 1.2 (за потреби обрати власні межі) для значення елемента amplitudes[i] масиву і зберігаємо це значення під ім’ям r. Саме r (радіус кола) та i (кут, під яким радіус проведений з центру кола до точки на колі) - полярні координати, які визначають координати точки - у цьому разі поточне значення амплітуди.
6 Оскільки вершина будується у декартових координатах (x, y), перетворюємо полярні координати у декартові використовуючи тригонометричні функції.
7 Як тільки розмір масиву amplitudes стає більшим за 360, значення з нього по одному будуть видалятися від його початку. Це зроблено для імітації обертання діаграми амплітуд. Доки ця умова не виконується - діаграма не обертається.

Переглядаємо Аналізуємо

Використання мікрофона

Створимо візуальний ефект вуста, що говорять, використавши цього разу як джерело не завантажений в ескіз аудіофайл, а звук, який надходить на вхід системного чи зовнішнього мікрофонів.

sketch.js
let microphone; (1)

function setup() {
  createCanvas(200, 200);
  microphone = new p5.AudioIn(); (2)
  microphone.start(); (3)
}

function draw() {
  background(245);
  let a = microphone.getLevel(); (4)
  noStroke();
  fill(252, 161, 125); // Atomic tangerine
  ellipse(width / 2, height / 2, width, a * height); (5)
}
1 Оголошуємо змінну з ім’ям microphone, яке буде покликанням на об’єкт мікрофона.
2 Створюємо на основі класу p5.AudioIn об’єкт мікрофона і надаємо йому ім’я microphone.
3 Застосовуємо метод start() на об’єкті microphone, щоб розпочати обробку вхідного аудіосигналу.
4 Зчитуємо амплітуду (рівень гучності) аудіовходу за допомогою методу getLevel() Архів (клас p5.AudioIn містить власний екземпляр класу p5.Amplitude, який допомагає легко отримати рівень гучності мікрофона) і зберігаємо під ім’ям a.
5 Малюємо вуста, що говорять у формі еліпса, використовуючи поточне значення амплітуди a, отримане у пункті 4.

Переглядаємо Аналізуємо

Для коректної роботи застосунків із застосуванням мікрофона необхідно використовувати вебсервер.

Цікавимось Додатково

Аналіз звуку

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

У цьому разі швидке перетворення Фур’є (абревіатура FFT від англ. Fast Fourier Transform) може надати такі можливості.

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

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

Розглянемо код застосунку, в якому візуалізуємо обчислені значення амплітуди звукового файлу в частотному діапазоні.

sketch.js
let btn, sliderSetVolume, checkboxLoop, soundFile;
let fft, spectrum; (1)

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav");
}

function setup() {
  createCanvas(600, 200);
  sliderSetVolume = createSlider(0.0, 1.0, 1.0, 0.1);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);

  fft = new p5.FFT(0.9); (2)
}

function draw() {
  background(245);
  soundFile.setVolume(sliderSetVolume.value());

  spectrum = fft.analyze(); (3)
  let w = width / 128; (4)

  stroke(106, 76, 147); // Ultra Violet
  fill(119, 118, 188); // Glaucous
  for (let i = 0; i < spectrum.length; i++) { (5)
    let s = spectrum[i];
    let y = map(s, 0, 255, height, 0);
    rect(i * w, y, w, height - y);
  }
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Оголошуємо дві змінні з іменами: fft - покликання на об’єкт, створений на основі класу p5.FFT, spectrum - покликання на масив значень амплітуди (значення від 0 до 255) у частотному діапазоні.
2 Створюємо об’єкт fft. Значення 0.9 - це необов’язковий параметр, що визначає рівень згладжування у діапазоні від 0.0 до 1.0 (за стандартним налаштуванням 0.8). Другим необов’язковим параметром є довжина кінцевого масиву (16, 32, 64, 128 і т. д. до 1024 так званих bins - бункерів, за стандартним налаштуванням їх 1024).
3 Використовуємо метод аналізу analyze() , який повертає spectrum - масив значень амплітуди (значення від 0 до 255) у частотному діапазоні довжиною 1024 за стандартним налаштуванням. Індекси масиву відповідають частотам (тобто висоті звуку), від найнижчої до найвищої, які може почути людина. Кожне значення представляє амплітуду в цій частині частотного діапазону.
4 Значення w визначає ширину стовпчика при візуалізації кожного значення масиву spectrum.
5 Проходимо через масив spectrum і малюємо прямокутники шириною w і висотою height - y, де y - вертикальна координата, яка формується за допомогою перетворення діапазону від 0 до 255 на діапазон від height до 0 для значень амплітуди spectrum[i] у частотному діапазоні з масиву spectrum.

Переглядаємо Аналізуємо

Окрім того, для об’єкта, який створений на основі класу p5.FFT, можна застосувати метод getEnergy() Архів для обчислення кількості енергії (амплітуди) на певній частоті або в діапазоні частот.

При виклику, цей метод як аргументи отримує числові значення частоти у герцах або рядок, що відповідає попередньо визначеним діапазонам частот: "bass" - низькі, "lowMid" - низькі середні, "mid" - середні, "highMid" - високі середні, "treble" - високі.

Результат виклику getEnergy() - діапазон від 0 (немає енергії на цій частоті) до 255 (максимальна енергія).

Оскільки метод analyze() змушує FFT аналізувати частотні дані, а getEnergy() використовує результати для визначення значення на певній частоті або діапазоні частот, метод analyze() потрібно викликати перед getEnergy().

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

Алгоритм виконання нашого завдання може складатися із таких кроків:

  1. Створити малюнок на полотні.

  2. Проаналізувати звук приєднаного аудіофайлу.

  3. Додати анімацію до фрагментів малюнка на основі різних частотних діапазонів.

Отже, намалюємо на полотні кілька фігур, наприклад, як на малюнку.

Малюнок для візуалізатора звуку
Малюнок для візуалізатора звуку

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

sketch.js
let bins = 1; (1)

function setup() {
  createCanvas(200, 200);
  noFill();
}

function draw() {
  background(41, 39, 76); // Space cadet

  let d = height / 4; (2)
  translate(width / 2, height / 2); (3)
  stroke(22, 152, 115); // Shamrock green
  circle(0, 0, d);

  for (i = 0; i < bins; i++) {
    rotate(TWO_PI / bins); (4)

    // лінія 1
    stroke(244, 159, 188); // Amaranth pink
    line(10, d / 2, 0, d);

    // лінія 2
    stroke(128, 93, 147); // Pomp and Power
    line(-10, d / 2, 0, d);

    // коло
    stroke(255, 211, 186); // Apricot
    circle(20, d * 1.2, d / 6);

    // квадрат
    stroke(158, 189, 110); // Olivine
    square(0, d * 1.4, 10);

    // трикутник
    stroke(48, 242, 242); // Fluorescent cyan
    triangle(d * 1.3, d * 1.1, d * 1.2, d * 1.1, d * 1.2, d * 1.3);
  }
}
1 Ініціалізуємо змінну bins зі значенням 1, яка визначатиме кількість фігур одного типу.
2 Ініціалізумо змінну d зі значенням height / 4 (або на свій задум), яка буде використовувати у побудові фігур на полотні.
3 Переміщуємо систему координат в центр полотна.
4 Повертаємо систему координат на кут TWO_PI / bins і малюємо фігури із зазначеними властивостями.

Тепер розглянемо код застосунку для аналізу звукового файлу.

Звуковий файл, що використовується у застосунку, можна завантажити за покликанням.
sketch.js
let btn, sliderSetVolume, checkboxLoop, soundFile;
let fft, spectrum;
let bins = 32; (1)

function preload() {
  soundFile = loadSound("space_125bpm_C_minor.wav");
}

function setup() {
  createCanvas(200, 200);
  sliderSetVolume = createSlider(0.0, 1.0, 1.0, 0.1);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);
  noFill();
  fft = new p5.FFT(0.9, bins); (2)
}

function draw() {
  background(41, 39, 76); // Space cadet
  soundFile.setVolume(sliderSetVolume.value());
  spectrum = fft.analyze(); (3)

  let bass = fft.getEnergy("bass"); (4)
  let lowMid = fft.getEnergy("lowMid");
  let mid = fft.getEnergy("mid");
  let highMid = fft.getEnergy("highMid");
  let treble = fft.getEnergy("treble");
  let custom = fft.getEnergy(100, 200);

  let mapBass = map(bass, 0, 255, -100, 100); (5)
  let mapLowMid = map(lowMid, 0, 255, -150, 150);
  let mapMid = map(mid, 0, 255, -100, 100);
  let mapHighMid = map(highMid, 0, 255, -50, 50);
  let mapTreble = map(treble, 0, 255, -200, 200);
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Ініціалізуємо змінну bins зі значенням 32, яке буде використовуватися для встановлення розміру масиву значень амплітуди, що повертає об’єкт FFT.
2 Створюємо новий об’єкт fft за допомогою класу p5.FFT з двома необов’язковими аргументами: 0.9 - визначає рівень згладжування у діапазоні від 0.0 до 1.0 (за стандартним налаштуванням 0.8) і bins - довжина кінцевого масиву значень амплітуди в частотній області (16, 32, 64, 128 і т. д. до 1024 так званих bins - бункерів, за стандартним налаштуванням їх 1024), який отримується викликом методу analyze() на об’єкті fft.
3 Виклик методу analyze() на об’єкті fft. Доступ до масиву значень амплітуди в частотній області можна отримати за ім’ям spectrum.
4 Отримуємо амплітуди різних частотних діапазонів за допомогою виклику методу getEnergy() на об’єкті fft.
5 Створюємо діапазони значень на свій задум для кожного частотного діапазону.

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

sketch.js
let btn, sliderSetVolume, checkboxLoop, soundFile;
let fft, spectrum;
let bins = 32;

function preload() {
  soundFile = loadSound("space_125bpm_C_minor.wav");
}

function setup() {
  createCanvas(200, 200);
  sliderSetVolume = createSlider(0.0, 1.0, 1.0, 0.1);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);
  noFill();
  fft = new p5.FFT(0.9, bins);
}

function draw() {
  background(41, 39, 76); // Space cadet
  soundFile.setVolume(sliderSetVolume.value());
  spectrum = fft.analyze();

  let bass = fft.getEnergy("bass");
  let lowMid = fft.getEnergy("lowMid");
  let mid = fft.getEnergy("mid");
  let highMid = fft.getEnergy("highMid");
  let treble = fft.getEnergy("treble");
  let custom = fft.getEnergy(100, 200);

  let mapBass = map(bass, 0, 255, -100, 100);
  let mapLowMid = map(lowMid, 0, 255, -150, 150);
  let mapMid = map(mid, 0, 255, -100, 100);
  let mapHighMid = map(highMid, 0, 255, -50, 50);
  let mapTreble = map(treble, 0, 255, -200, 200);

  let d = height / 4;
  translate(width / 2, height / 2);
  stroke(22, 152, 115); // Shamrock green
  circle(0, 0, d);

  for (i = 0; i < spectrum.length; i++) {
    rotate(TWO_PI / bins);

    // лінія 1
    stroke(244, 159, 188); // Amaranth pink
    line(mapBass, d / 2, 0, d);

    // лінія 2
    stroke(128, 93, 147); // Pomp and Power
    line(mapMid, d / 2, 0, d);

    // коло
    stroke(255, 211, 186); // Apricot
    circle(mapTreble, d * 1.2, d / 6);

    // квадрат
    stroke(158, 189, 110); // Olivine
    square(mapHighMid, d * 1.4, 10);

    // трикутник
    stroke(48, 242, 242); // Fluorescent cyan
    triangle(
      mapLowMid * 1.3,
      mapLowMid * 1.1,
      mapLowMid * 1.2,
      mapLowMid * 1.1,
      mapLowMid * 1.2,
      mapLowMid * 1.3
    );
  }
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}

У кінцевому коді візуалізатора аудіо на місці значень координат у функціях побудови фігур були використані змінні mapBass, mapLowMid, mapMid, mapHighMid, mapTreble, що містять значення попередньо перетворених за допомогою функції map() амплітуд різних частотних діапазонів.

Переглядаємо Аналізуємо

Ще один метод аналізу, який надає клас p5.FFT, - це waveform() . Цей метод використовується для обчислення значення амплітуди в часовому діапазоні.

Застосування методу waveform() на об’єкті, створеному на основі класу p5.FFT, повертає масив значень амплітуди (від -1.0 до 1.0) довжиною за стандартним налаштуванням 1024. Індекси масиву відповідають коротким відрізкам часу, а кожне значення масиву представляє амплітуду хвилі за цей відрізок часу.

Отож, використаємо метод waveform() для малювання звукової хвилі.

sketch.js
let btn, sliderSetVolume, checkboxLoop, soundFile, fft;
let wf; (1)

function preload() {
  soundFile = loadSound("space-synth-melody_138bpm.wav");
}

function setup() {
  createCanvas(200, 200);
  sliderSetVolume = createSlider(0.0, 1.0, 1.0, 0.1);
  checkboxLoop = createCheckbox("циклічно", false);
  checkboxLoop.changed(сheckedLooping);
  btn = createButton("відтворити");
  btn.mousePressed(switchPlaying);
  fft = new p5.FFT(0.9);
}

function draw() {
  background(245);
  soundFile.setVolume(sliderSetVolume.value());

  wf = fft.waveform(); (2)

  noFill();
  beginShape();
  stroke(20);
  for (let i = 0; i < wf.length; i++) { (3)
    let x = map(i, 0, wf.length, 0, width);
    let y = map(wf[i], -1, 1, 0, height);
    stroke(131, 50, 172); // Grape
    vertex(x, y);
  }
  endShape();
}

function switchPlaying() {
  if (!soundFile.isPlaying()) {
    soundFile.play();
    btn.html("пауза");
  } else {
    soundFile.pause();
    btn.html("продовжити");
  }
}

function сheckedLooping() {
  if (checkboxLoop.checked()) {
    soundFile.setLoop(true);
  } else {
    soundFile.setLoop(false);
  }
}
1 Оголошуємо змінну з ім’ям wf, яке буде покликанням на масив значень амплітуди.
2 Застосовуємо на об’єкті fft, створеному на основі класу p5.FFT, метод waveform(). Метод поверне масив значень амплітуди (від -1.0 до 1.0) довжиною за стандартним налаштуванням 1024, доступ до якого можна буде отримати за допомогою імені wf.
3 Проходимо у циклі по масиву wf і за допомогою функцій beginShape(), vertex() і endShape() малюємо вершини, які утворюють форму хвилі. Координати кожної з вершин формуються за допомогою функції map(), а саме: для x-координати використовуємо значення лічильника i (відрізки часу) циклу for, яке перетворюємо з діапазону від 0 до wf.length у діапазон від 0 до width, а y-координату отримуємо зі значень елементів wf[i] масиву wf, яку аналогічно перетворюємо з діапазону від -1 до 1 у діапазон від 0 до height.

Переглядаємо Аналізуємо

Синтез звуку

Окрім перелічених вище можливостей, за допомогою бібліотеки p5.sound.js можна створювати (синтезувати) звук.

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

Осцилятор (від лат. oscillo - гойдаюся) - система, яка здійснює коливання, тобто показники якої періодично повторюються в часі.
Щоб переглянути різні форми осциляторів-генераторів, завітайте до музичної лабораторії Chrome - вебсайту, який робить навчання музики доступнішим за допомогою веселих практичних експериментів.

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

sketch.js
let osc; (1)

function setup() {
  createCanvas(200, 200);
  osc = new p5.Oscillator(); (2)
  osc.setType("triangle"); (3)
}

function draw() {
  background(225);
  let fr = map(mouseX, 0, width, 0, 1000); (4)
  let vo = map(mouseY, 0, height, 0.2, 0); (5)
  osc.freq(fr); (6)
  osc.amp(vo); (7)
}

function mousePressed() { (8)
  osc.start();
}

function mouseReleased() { (9)
  osc.stop();
}
1 Оголошуємо змінну з ім’ям osc, яке буде покликанням на об’єкт осцилятора.
2 Створюємо об’єкт осцилятора за допомогою класу p5.Oscillator .
3 Зазначаємо за допомогою методу setType() тип для об’єкта осцилятора osc. За стандартним налаштуванням коливання має форму синусоїди ("sine"). Додатковими типами осциляторів є: трикутник ("triangle"), пилкоподібний ("sawtooth") і квадрат ("square").
4 За допомогою функції map() змінюємо діапазон значень для координати mouseX вказівника миші, яка формуватиме частоту коливань осцилятора fr, з діапазону від 0 до width на діапазон від 0 до 1000. Частота за стандартним налаштуванням для створеного об’єкта осцилятора становить 440 Гц, що дорівнює висоті звуку ноти ля.
5 За допомогою функції map() змінюємо діапазон значень для координати mouseY вказівника миші, яка формуватиме амплітуду коливань (гучність звуку) осцилятора vo, з діапазону від 0 до height на діапазон від 0.2 до 0.
6 Встановлюємо значення частоти fr у герцах для осцилятора osc за допомогою методу freq() .
7 Встановлюємо значення амплітуди vo для осцилятора osc за допомогою методу amp() .
8 Використання функції mousePressed() , яка викликається натисканням будь-якої кнопки миші будь-де. Настання цієї події запускає осцилятор за допомогою методу start() .
9 Використання функції mouseReleased() , яка викликається після того, як буде скасовано натискання будь-якої кнопки миші. Настання цієї події зупиняє осцилятор за допомогою методу stop() .

Переглядаємо Аналізуємо

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

Цікавимось Додатково

Терменвокс

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

Це уможливлюють дві антени, що отримують інформацію про розміщення рук терменіста. Одна з антен керує коливаннями (частотою звуку), інша - амплітудою (гучність інструмента). Отримані електричні сигнали з терменвокса подаються на динамік через підсилювач.

Інструмент винайшов винахідник Лев Термен у 1919 році (запатентував у 1928 році).

Про оригінальний музейний експонат у селі на Тернопільщині читайте на сторінці Терменвокс - дивовижні історії з музичної скриньки .

Вправа 72

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

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

Для синтезу звуків більш наближених до природних та імітації звуків музичних інструментів у бібліотеці p5.sound.js використовується клас p5.Envelope .

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

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

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

Абревіатура ADSR складається з перших букв у їх назвах англійською мовою чотирьох параметрів, за допомогою яких вона задається:

  • Attack (Атака) - визначає час, потрібний для того, щоб гучність ноти досягла свого максимального рівня.

  • Decay (Спадання) - визначає час, протягом якого відбувається перехід від максимального рівня до рівня Підтримки (Sustain).

  • Sustain (Підтримка) - описує рівень звуку, що грає під час утримання клавіші (після того, як інші складові Атака й Спадання уже відіграли).

  • Release (Згасання) - визначає час, потрібний для остаточного згасання звучання ноти до нуля, після того, як клавішу відпустили.

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

sketch.js
let osc, env; (1)

function setup() {
  createCanvas(200, 200);

  env = new p5.Envelope(); (2)
  env.setADSR(0.01, 0.1, 0.5, 1); (3)

  osc = new p5.Oscillator();
  osc.setType("triangle");
  osc.start();
  osc.amp(env); (4)
}

function draw() {
  background(245);
}

function mousePressed() { (5)
  env.play();
}
1 Оголошуємо змінну з ім’ям env, яке буде покликанням на об’єкт конверта.
2 За допомогою класу p5.Envelope створюємо об’єкт конверта з ім’ям env.
3 Застосовуємо до об’єкта env метод setADSR() з аргументами A, D, S і R.
4 Встановлюємо значення амплітуди для об’єкта осцилятора osc, використовуючи env - об’єкт, який визначає розподіл амплітуди в часі.
5 За допомогою методу play() конверт починає керувати гучністю.

Переглядаємо Аналізуємо

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

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

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

Розглянемо код застосунку, який використовує частоти з музичного діапазону (звуки нот) стандарту MIDI .

У класичному буквеному позначенню звуків нот застосовується латинська абетка: А (ля), В (сі-бемоль), Н (сі), С (до), D (ре), Е (мі), F (фа), G (соль). Назви, номери MIDI та частоти нот можна переглянути за покликанням .
sketch.js
let osc, env;

function setup() {
  createCanvas(100, 100);

  env = new p5.Envelope();
  env.setADSR(0.01, 0.1, 0.5, 0.25);

  osc = new p5.Oscillator("triangle");
  osc.start();
  osc.amp(env);
}

function draw() {
  if (mouseX < width / 2 && mouseIsPressed) { (1)
    playNote(osc, 69, color(217, 114, 255), 0, 0, width / 2, height); // Heliotrope
  } else if (mouseX > width / 2 && mouseIsPressed) { (2)
    playNote(osc, 74, color(140, 255, 218), width / 2, 0, width, height); // Aquamarine
  } else { (3)
    background(245);
    stroke(132, 71, 255); // Veronica
    line(width / 2, 0, width / 2, height);
  }
}

function mousePressed() {
  env.play();
}

function playNote(o, midiNumber, c, x1, y1, x2, y2) { (4)
  fill(c);
  rect(x1, y1, x2, y2);
  o.freq(midiToFreq(midiNumber));
}
1 Якщо вказівник миші перебуває ліворуч від лінії, яка розділяє полотно вертикально, і будь-яка кнопка миші натиснута, то відбувається виклик користувацької функції playNote() з такими аргументами як: osc - об’єкт осцилятора (корисна опція у разі використання кількох осциляторів), 69 - MIDI-номер ноти, колір для зафарбовування частини полотна у формі клавіші і координати малювання лівої клавіші.
2 Якщо вказівник миші перебуває праворуч від лінії, яка розділяє полотно вертикально, і будь-яка кнопка миші натиснута, то відбувається виклик користувацької функції playNote() для малювання правої клавіші.
3 Інакше - полотно зафарбовується суцільним кольором і малюється вертикальна лінія.
4 Опис користувацької функції playNote(), у тілі якої використовується функція midiToFreq() Архів, яка повертає значення частоти для midiNumber - цілочисельного значення MIDI-ноти. Отримане значення частоти у герцах встановлюється для переданого у функцію playNote осцилятора o.
Протилежно midiToFreq() працює функція freqToMidi() Архів - вона повертає найближче значення MIDI-ноти для зазначеної частоти.

Переглядаємо Аналізуємо

6.6.5. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «мультимедійні дані»?

  2. Опишіть алгоритм приєднання зовнішньої бібліотеки до ескізу.

  3. Які можливості надає бібліотека p5.sound.js для створення та обробки звуку?

6.6.6. Практичні завдання

Початковий

  1. Перетворити зображення, яке утворюється будь-яким із ваших застосунків, в ASCII art за допомогою бібліотеки p5.asciiart.js.

  1. Створити застосунок Ударна установка, в якому окремі клавіші клавіатури відтворюють звуки ударних інструментів. До ударної установки входять тарілки креш, райд, хай-хети, навісні та підлоговий том-томи, малий барабан та бас-барабан, а також іноді інші ударні інструменти. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Звукові файли, що використовуються у застосунку, можна завантажити за покликанням.

Середній

  1. Використати у своєму проєкті бібліотеку з колекції на вибір.

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

  1. Використовуючи поданий нижче код за основу, додати звукове оформлення у застосунок, щоб відбивання кульки від меж полотна супроводжувалося звуком.

sketch.js
let d = 50;
let x = d;
let y = d;
let velocityX = 0.5;
let velocityY = 0.8;

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(245);
  noStroke();
  fill(0, 159, 183); // Moonstone
  circle(x, y, d);
  if (x > width - d / 2 || x < d / 2) {
    velocityX = velocityX * -1;
  }
  if (y > height - d / 2 || y < d / 2) {
    velocityY = velocityY * -1;
  }
  x += velocityX;
  y += velocityY;
}

Високий

  1. Створити застосунок, у якому за допомогою натискання кнопки миші на полотні малюються випадковим чином фігури довільного кольору. Розміри фігур визначаються частотою коливань синтезованого звуку. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок - MIDI-клавіатуру. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, який візуалізує звук. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Звуковий файл, що використовується у застосунку, можна завантажити за покликанням.

Екстремальний

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

Звуковий файл і зображення тла, що використовуються у застосунку, можна завантажити за покликанням.
  1. Додати звуки у гру Змійка із практичного завдання 4 Розділу 5.5.3.

Звукові файли, що використовуються в застосунку, можна завантажити за покликанням.

7. Інтерфейс програмного продукту

У цьому розділі ви навчитеся створювати інтерфейси користувача, отримувати дані від зовнішніх джерел та їх візуалізувати.

Більшість із нас щодня взаємодіє з різними типами програмних продуктів (англ. software product, software as a product - SaaP) - програмним забезпеченням, що розроблене для розв’язування різноманітних задач як у навчанні, так і у дозвіллі.

Прикладами таких програмних продуктів є пакунки офісних застосунків (наприклад, LibreOffice) для роботи з різними типами файлів документів, засоби для проведення відеоконференцій та онлайн-зустрічей (наприклад, Google Meet, Zoom), хмарні сховища даних (Google Диск, OneDrive), середовища розробки (наприклад, Processing, Replit) та інші.

Зазвичай програмне забезпечення встановлюється на комп’ютерний пристрій, але й надається як сервіс (англ. Software as a service, SaaS).

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

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

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

7.1. Програмний код, графічний інтерфейс користувача та джерела даних

7.1.1. Типи інтерфейсів

Якщо для взаємодії з користувачем програмний продукт використовує елементи дизайну, такі як кнопки, меню, поля для введення, графіки, візуалізації даних тощо, то у цьому разі мова йде про графічний інтерфейс (англ. Graphical user interface, GUI).

Графічний інтерфейс операційної системи Android
Графічний інтерфейс операційної системи Android

Загалом графічний інтерфейс може мати такі складові:

  • меню та навігацію - допомагають користувачу переходити між різними функціями чи сторінками застосунку;

  • елементи керування - елементи, за допомогою яких користувач може інтерактивно взаємодіяти із застосунком (кнопки, чекбокси, радіокнопки, перемикачі, списки тощо);

  • вікна - відкриваються для введення або відображення додаткової інформації, а також діалогові вікна для попереджень, підтверджень, сповіщень про результати дій, помилки тощо;

  • адаптивність - можливість інтерфейсу пристосовуватися до різних типів пристроїв і розмірів екранів, щоб забезпечити комфортну роботу застосунку на різних пристроях.

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

Інший тип інтерфейсу - інтерфейс командного рядка (англ. Command-Line Interface, CLI), особливостями якого є:

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

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

  • окрім інструкцій, CLI дозволяє виконувати скрипти - набори інструкцій, які автоматизують рутинні завдання;

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

У CLI користувач вводить команди в текстовому форматі у спеціальному вікні, яке називають вікном командного рядка (Windows) чи терміналом (Linux), або в консолі (наприклад, середовище виконання інструкцій мови програмування JavaScript у вебпереглядачі) та отримує результати їх виконання.

Вікно командного рядка у Windows
Вікно командного рядка у Windows: перехід на рівень угору у структурі каталогів
Вікно терміналу в Linux Ubuntu
Вікно терміналу в Linux Ubuntu: запуск текстового редактора Gedit
Вікно консолі у вебпереглядачі Google Chrome
Вікно консолі у вебпереглядачі Google Chrome: обчислення виразу

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

7.1.2. Графічний інтерфейс

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

Розглянемо приклади створення складових графічного інтерфейсу на вебсторінці ескізу за допомогою створення HTML-елементів у DOM, які визначатимуть кнопки, слайдери (повзунки), текстові поля, чекбокси (множинний вибір з декількох варіантів), радіокнопки (перемикачі), списки із вибором, палітру кольорів.

Робота із DOM детально розглядається в Додатку B.
Кнопка

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

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

let g, btn; (1)

function setup() {
  createCanvas(200, 200);
  btn = createButton("Встановити колір"); (2)
  btn.mousePressed(changeColor); (3)
  g = color(42, 157, 143); // Persian green (4)
  noStroke();
}

function draw() {
  background(220);
  fill(g); (6)
  circle(width / 2, height / 2, width / 4);
}

function changeColor() { (5)
  g = color(random(0, 255), random(0, 255), random(0, 255));
}
1 Оголошуємо глобальні змінні: btn - для об’єкта кнопки та g - для кольору тла полотна.
2 Створюємо HTML-елемент кнопки (<button>) за допомогою функції createButton() з написом на ній.
3 Для кнопки btn застосовуємо метод mousePressed() , який викликається один раз після кожного натискання кнопки миші на елементі кнопки. Цей метод є слухачем події натискання кнопки миші на елементі кнопки. Він отримує як аргумент функцію changeColor(), яка є обробником цієї події.
4 Ініціалізуємо змінну g початковим значенням кольору для зафарбовування кола.
5 Описуємо обробник події натиснення кнопки btn - функцію changeColor(), у тілі якої встановлюємо випадкове нове значення кольору і зберігаємо його з ім’ям g.
6 При натисканні будь-якої кнопки миші вбудована функція fill() зафарбовує коло значенням g.
Метод mousePressed() відстежує лише подію натиснення на кнопці btn, тому зміна кольору відбувається лише у разі натискання кнопки btn, а не у будь-якому місці вікна перегляду.

Переглядаємо Аналізуємо

Елемент кнопки створюється поза полотном за стандартним налаштуванням.

Напишемо код ще одного застосунку, в якому використовуватиметься кнопка, натискання на яку викличе зміну кольору тла полотна ескізу.

sketch.js
let btn, colorBg = 120; (1)

function setup() {
  createCanvas(200, 200);
  btn = createButton("Змінити колір"); (2)
  btn.position(10, 10); (3)
  btn.mousePressed(changeColor); (4)
}

function draw() {
  background(colorBg); (5)
}

function changeColor() { (6)
  colorBg = color(random(255), random(255), random(255));
}
1 Оголошуємо глобальну змінну btn для об’єкта кнопки та ініціалізуємо глобальну змінну colorBg з початковим значенням 120 кольору тла полотна.
2 Створюємо HTML-елемент кнопки (<button>) за допомогою функції createButton() з написом на ній.
3 Використовуємо метод position() для встановлення координат положення кнопки на полотні відносно точки (0, 0) - верхнього лівого кута полотна.
4 Як і в попередньому прикладі для кнопки btn застосовуємо метод mousePressed() , який викликається один раз після кожного натискання кнопки миші на елементі кнопки. Цей метод є слухачем події натискання кнопки миші на елементі кнопки. Він отримує як аргумент функцію changeColor(), яка є обробником цієї події.
5 Встановлюємо колір полотна відповідно до значення colorBg.
6 Оголошуємо функцію з назвою changeColor(), яка буде обробником події у пункті 4. У тілі цієї функції змінна colorBg щоразу при натисканні кнопки набуватиме випадкового значення кольору.

Коли запустити застосунок, щоразове натискання вказівника миші на кнопці змінюватиме колір тла полотна.

Метод mousePressed() відстежує лише подію натиснення на кнопці btn, тому зміна кольору відбувається лише у разі натискання кнопки btn, а не у будь-якому місці вікна перегляду.

Перед тим, як переглянути результати роботи застосунку, додамо оформлення для самої кнопки.

Зробимо це за допомогою файлу стилів style.css

style.css
html,
body {
  margin: 0;
  padding: 0;
}
canvas {
  display: block;
}
button {
  padding: 4px;
  background-color: transparent;
  color: white;
  border: 1px solid #efefef;
  border-radius: 3px;
  box-shadow: 3px 3px 2px 1px rgba(0, 0, 0, 0.1);
}

який приєднаємо у файлі index.html

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <meta charset="utf-8" />
  </head>
  <body>
    <main></main>
    <script src="sketch.js"></script>
  </body>
</html>

Стилі, які будуть застосовані до кнопки:

  • padding: 4px; - внутрішні відступи (відступи зазначають у такому порядку: вгорі-справа-унизу-зліва; оскільки є єдине значення 4px, воно використовується для усіх напрямків);

  • background-color: transparent; - прозоре тло;

  • color: white; - колір тексту на кнопці - білий;

  • border: 1px solid #efefef; - суцільна межа завтовшки в 1px кольору #efefef;;

  • border-radius: 3px; - округлені кути зовнішньої рамки елемента радіусом 3px;

  • box-shadow: 3px 3px 2px 1px rgba(0, 0, 0, 0.1); - тінь з параметрами у зазначеному порядку: 3px - зсув по горизонталі (додатне значення, тому вправо), 3px - зсув по вертикалі (додатне значення, тому униз), 2px - радіус розмиття, 1px - розтягування тіні, rgba(0, 0, 0, 0.1) - колір тіні.

Переглядаємо Аналізуємо

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

Для цих цілей скористаємось функцією resizeCanvas() , яка змінює розміри полотна до заданих ширини та висоти. Також використаємо для стилізації кнопки файл зі стилями із попереднього прикладу.

У коді застосунку ініціалізуємо дві змінні x та y значеннями кроків зміни полотна по горизонталі й вертикалі відповідно.

sketch.js
let btn;
let x = 2;
let y = -4;

function setup() {
  createCanvas(200, 200);

  btn = createButton("Змінити розміри");
  btn.position(10, 10);
  btn.mousePressed(changeSize);
}

function draw() {
  background(9, 188, 138); // Mint
}

function changeSize() {
  resizeCanvas(width + x, height + y);
}

Обробник події натискання кнопки, функція changeSize(), у своєму тілі використовує функцію зміни розмірів полотна resizeCanvas(). Початкові значення (200 і 200) розмірів полотна зберігаються у системних змінних width і height відповідно.

Розміри полотна після чергового натискання на кнопку будуть такими (у рядку перше число - розміри по горизонталі, друге число - по вертикалі):

sketch.js
202 196
204 192
206 188
...

Переглядаємо Аналізуємо

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

Вправа 73

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

Слайдер

Слайдери (англ. slider; укр. повзунок) дозволяють користувачам вибирати значення в заданому діапазоні даних. Даними у цьому разі можуть бути числові значення складових кольору, координат тощо.

Розглянемо код застосунку, в якому при переміщенні повзунка слайдера змінюється діаметр кола у діапазоні від 0 до ширини квадратного полотна.

sketch.js
let slider; (1)

function setup() {
  createCanvas(200, 200);
  slider = createSlider(0, width, width / 2, 2); (2)
  slider.position(10, 10); (3)
  slider.style("width", "80px"); (4)
  noStroke();
}

function draw() {
  background(220);
  fill(96, 225, 224); // Tiffany Blue
  let d = slider.value(); (5)
  circle(width / 2, height / 2, d); (6)
}
1 Оголошуємо глобальну змінну slider, яка зберігатиме покликання на об’єкт слайдера.
2 Створюємо слайдер (HTML-елемент <input>) за допомогою функції createSlider() . Аргументами, які передаються у функцію, є: початкове значення (0) діапазону даних, кінцеве значення (width) діапазону даних, значення за стандартним налаштуванням (width / 2), необов’язковий крок зміни значень (2).
3 Використовуємо метод position() для встановлення координат слайдера на полотні відносно точки (0, 0) - верхнього лівого кута полотна.
4 Застосовуємо метод style() , який встановлює для слайдера ширину - додає CSS-властивість width зі значенням 80px. Аналогічно це можна зробити за допомогою методу size() , який встановлює ширину і висоту елемента. У цьому разі використовується лише значення ширини: slider.size(80).
5 Ініціалізуємо локальну змінну d із поточним значенням слайдера (значення, яке відповідає розташуванню повзунка), використовуючи метод value() .
6 Використовуємо значення d як значення діаметра кола у функції circle().

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

Переглядаємо Аналізуємо

Вправа 74

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

Текстове поле

Важливим елементом будь-якого інтерфейсу є текстове поле, що використовується для введення текстових даних.

У бібліотеці p5.js для створення текстового поля використовують функцію createInput() , яка створює у DOM елемент <input> для введення тексту.

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

let inp; (1)

function setup() {
  createCanvas(200, 200);
  inp = createInput("тут ввести текст"); (2)
  inp.input(printInput); (3)
  noStroke();
}

function draw() {
  background(220);
  text(inp.value(), 10, 20); (5)
}

function printInput() {
  console.log("ви друкуєте:", inp.value()); (4)
}
  1. Оголошуємо глобальну змінну inp, яка буде містити покликання на текстове поле.

  2. Створюємо текстове поле за допомогою функції createInput() зі значенням за стандартним налаштуванням (атрибут value для елемента <input>).

  3. Використовуємо функцію input() - слухач події введення у текстове поле inp, який отримує обробник цієї події - користувацьку функцію printInput().

  4. Оголошуємо користувацьку функцію printInput() - обробник події введення у текстове поле inp. Функція друкує в консолі значення введеного тексту у текстове поле за допомогою метода value(), який застосовується для inp.

  5. За допомогою функції text() на полотні у визначених координатах виводимо значення введеного тексту за допомогою методу value(), який застосовується для inp.

Результати введення тексту в текстове поле після його очищення від тексту за стандартним налаштуванням:

ви друкуєте:  ""
ви друкуєте:  J
ви друкуєте:  Ja
ви друкуєте:  Jav
ви друкуєте:  Java
ви друкуєте:  JavaS
ви друкуєте:  JavaSc
ви друкуєте:  JavaScr
ви друкуєте:  JavaScri
ви друкуєте:  JavaScrip
ви друкуєте:  JavaScript

Використаємо у нашому прикладі з текстовим полем як слухача функцію changed() , яка відстежує випадок, коли значення елемента змінюється.

Для текстового поля зміна значення - введення тексту і підтвердження клавішею Enter чи натискання поза межами текстового поля. Замінивши у коді слухача події

...
inp.changed(printInput);
...

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

Розглянемо застосунок в якому колір тексту на полотні змінюється відповідно введеної у текстове поле назви кольору англійською. Наприклад, якщо у текстове поле введено слово red, текст стає червоного кольору і т. д.

let inp, textColor = 0;

function setup() {
  createCanvas(200, 200);
  inp = createInput("Введіть назву кольору");
  inp.position(10, 10);
  inp.changed(changeTextColor);
  textSize(40);
  textAlign(CENTER, CENTER);
}

function draw() {
  background(220);
  fill(textColor);
  text("p5.js", width / 2, width / 2);
}

function changeTextColor() {
  textColor = inp.value();
}

Переглядаємо Аналізуємо

Вправа 75

Використати код попереднього застосунку для зміни розміру тексту на полотні за допомогою введеного у текстове поле цілочисельного значення розміру літер у пікселях.

Чекбокс, радіокнопка, список із вибором

Розглянемо код застосунку, у якому використовуються вже розглянуті елементи графічного інтерфейсу та нові, серед яких:

  • чекбокси (множинний вибір з декількох варіантів);

  • радіокнопки (перемикачі);

  • списки із вибором.

У коді застосунку використовується функція createP() , яка створює у DOM елемент абзацу <p>.
let inp, btn, radio, sel, checkbox; (1)
function setup() {
  createCanvas(200, 200);
  createP("Оцініть застосунок:");
  radio = createRadio(); (2)
  radio.option("чудово");
  radio.option("добре");
  radio.option("так собі");
  radio.selected("чудово");
  radio.style("width", "80px");

  createP("Що змінити в застосунку насамперед?");
  sel = createSelect(); (3)
  sel.option("жодних змін не потрібно");
  sel.option("стильове оформлення");
  sel.option("додати більше об'єктів");
  sel.option("збільшити розмір полотна");
  sel.selected("жодних змін не потрібно");

  createP("Додатковий коментар:");
  inp = createInput("відсутній"); (4)

  createP("Опублікувати відгук для усіх?");
  checkbox = createCheckbox("так", false); (5)

  createP("Перевірте введені дані та натисніть кнопку");
  btn = createButton("Надіслати"); (6)
  btn.mousePressed(sendFeedback); (7)

  noStroke();
}

function draw() {
  background(220);
  fill(42, 157, 143); // Persian green
  circle(width / 2, height / 2, width / 4);
}

function sendFeedback() { (8)
  console.log("Ваша оцінка:", radio.value());
  console.log("Ваші зауваги:", sel.value());
  console.log("Ваш коментар:", inp.value());
  if (checkbox.checked()) {
    console.log("Вашу оцінку будуть бачити усі.");
  }
  console.log("Дякуємо за відгук!\n");
}
1 Описуємо глобальні змінні для зберігання елементів інтерфейсу.
2 За допомогою функції createRadio() створюємо елемент перемикача у DOM і зберігаємо з ім’ям radio. За допомогою властивості option для radio визначаємо значення усіх перемикачів. Метод selected() робить обраний перемикач позначеним, а властивість style додає вбудований стиль для елемента <div>, який є батьківським для перемикачів, - ширину 80 пікселів.
3 За допомогою функції createSelect() створюємо список із вибором у DOM і зберігаємо з ім’ям sel. За допомогою властивості option визначаємо елементи списку, а метод selected() встановлює обраний елемент списку за стандартним налаштуванням.
4 Використовуємо функцію createInput() для створення текстового поля з ім’ям inp.
5 Використовуємо функцію createCheckbox() для створення елемента прапорця у DOM з ім’ям checkbox.
6 Використовуючи функцію createButton() створюємо кнопку з ім’ям btn.
7 До кнопки приєднуємо функцію mousePressed() - слухач події натиснення на кнопку btn і передаємо їй як аргумент обробник цієї події - функцію sendFeedback().
8 Оголошуємо функцію sendFeedback() - обробник події натиснення на кнопку btn. У тілі функції-обробника звертаємось до значень створених елементів графічного інтерфейсу за допомогою метода value(). Виклик метода checked(), який повертає істину, якщо прапорець позначений, інакше - хибність, для прапорця checkbox використовуємо у розгалуженні. Для зручності читання результатів символ нового рядка \n розділяє виведення повідомлень в консолі вебпереглядача.

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

Ваша оцінка: чудово
Ваші зауваги: жодних змін не потрібно
Ваш коментар: відсутній
Дякуємо за відгук!
 

Вправа 76

Поміркуйте, значення яких елементів інтерфейсу зазнали змін, якщо в консолі отримали наступні результати:

Ваша оцінка: добре
Ваші зауваги: додати більше об'єктів
Ваш коментар: цікавий застосунок
Вашу оцінку будуть бачити усі.
Дякуємо за відгук!
 
Палітра кольорів

У комп’ютерній графіці термін «палітра» вказує на набір доступних кольорів, які можуть бути використані для створення зображення.

Бібліотека p5.js має спеціальну функцію createColorPicker() для створення палітри кольорів. Ця функція створює у DOM елемент colorPicker для вибору кольору.

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

let colorPicker, bgColor = 220; (1)

function setup() {
  createCanvas(200, 200);
  colorPicker = createColorPicker("#ff0000"); (2)
  colorPicker.position(10, 10); (3)
  colorPicker.input(changeBgColor); (4)
}

function draw() {
  background(bgColor); (6)
}

function changeBgColor() {
  bgColor = this.color(); (5)
}
1 Оголошуємо глобальну змінну colorPicker, ім’я якої буде посиланням на об’єкт палітри, та ініціалізуємо bgColor зі значенням 220, що визначає початковий колір (сірий) тла полотна.
2 Створюємо об’єкт палітри за допомогою функції createColorPicker() зі значенням рядка, який описує колір у шістнадцятковому вигляді, що буде встановлений на палітрі за стандартним налаштуванням. У цьому разі це червоний колір.
3 Використовуємо метод position() для розташування палітри кольорів на полотні відносно точки (0, 0) - верхнього лівого кута полотна.
4 Використовуємо функцію input() - слухач події вибір кольору, який отримує обробник цієї події - користувацьку функцію changeBgColor().
5 Користувацька функція changeBgColor() у своєму тілі за допомогою методу color() повертає об’єкт p5.Color із поточним вибраним кольором. Значення кольору присвоюємо змінній bgColor.
6 Використовуємо значення bgColor для встановлення кольору тла полотна.

Переглядаємо Аналізуємо

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

7.1.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Які елементи можуть входити до графічного інтерфейсу?

  2. Що таке «слухач події»?

  3. Для чого використовується «обробник події»?

7.1.5. Практичні завдання

Початковий

  1. Створити застосунок, який дозволяє користувачеві вибирати колір з палітри для зафарбовування полотна. На полотні також друкується значення кольору. Орієнтовний взірець роботи застосунку представлений в демонстрації.

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

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

Середній

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

  1. Створити застосунок, в якому за допомогою елементів інтерфейсу можна змінювати властивості геометричної фігури як представлено у демонстрації.

Високий

  1. Розробити гру «Вгадай число» з графічним інтерфейсом. Орієнтовний взірець роботи застосунку представлений в демонстрації.

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

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

Екстремальний

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

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

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

7.2. Зовнішні джерела даних

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

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

Розглянемо можливості бібліотеки p5.js для роботи з даними, отриманими із зовнішніх джерел, як-от CSV- і JSON-файли.

7.2.1. CSV

CSV-файли - звичайні текстові файли з розширенням .csv, в яких дані розміщені у рядках і стовпцях та відокремлені один від одного символом коми (,).

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

Усі рядки CSV-файлу повинні мати однакову кількість розділювачів, щоб структура даних була коректною.

Наприклад, CSV-файл з даними про комп’ютерні ігри може мати наступну структуру:

games.csv
Назва гри,Рік випуску,Жанр
The Witcher 3: Wild Hunt,2015,Action RPG
Grand Theft Auto V,2013,Open World
Minecraft,2011,Sandbox
The Legend of Zelda: Breath of the Wild,2017,Action-Adventure
Fortnite,2017,Battle Royale
Civilization VI,2016,Turn-Based Strategy
Age of Empires II: Definitive Edition,2019,Real-Time Strategy
Heroes of Might and Magic III,1999,Turn-Based Strategy
Disciples II: Dark Prophecy,2002,Turn-Based Strategy

Така організація даних подібна до традиційної електронної таблиці.

Результат імпорту даних із CSV-файлу у файл електронної таблиці
Результат імпорту даних із CSV-файлу у файл електронної таблиці

Назва формату CSV (Comma-Separated Values або значення, розділені комами) вказує на використання коми як розділювача даних.

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

Файли, які використовують символ табуляції (засіб відступу, який дорівнює по ширині кільком пропускам) замість коми, є альтернативою CSV-формату і має назву TSV - Tab Separated Values або значення, розділені табуляцією.

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

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

Для належної роботи застосунку, який завантажує дані з локального файлу, необхідно використовувати локальний вебсервер .
sketch.js
let table; (1)

function preload() {
  table = loadTable("games.csv", "csv", "header"); (2)
}

function setup() {
  print(table.getRowCount() + " рядків у таблиці"); (3)
  print(table.getColumnCount() + " стовпців у таблиці"); (4)
  print(table.getColumn("Назва гри")); (5)
  print(table.getColumn(0)); (6)
}
1 Оголошуємо змінну table, ім’я якої буде покликанням на об’єкт із даними CSV-файлу games.csv.
2 Викликаємо функцію loadTable() , яка дозволяє прочитати вміст CSV-файлу. Аргументами для loadTable() є: games.csv - шлях до локального CSV-файлу, csv - формат файлу, header - враховувати рядок заголовків.
3 На об’єкті table застосовуємо метод getRowCount() , який повертає загальну кількість рядків у таблиці.
4 На об’єкті table застосовуємо метод getColumnCount() , який повертає загальну кількість стовпців у таблиці.
5 Використовуючи на об’єкті table метод getColumn() , отримуємо всі значення у вказаному стовпці (Назва гри) у вигляді масиву. До стовпця звертаємось за допомогою його назви.
6 Використовуючи на об’єкті table метод getColumn() , отримуємо всі значення у вказаному стовпці (Назва гри) у вигляді масиву. До стовпця звертаємось за допомогою його індексу. Результат виконання ідентичний пункту 5.
Виклик loadTable() є асинхронним, тобто він може не завершитися до виконання наступної інструкції ескізу. Тому, щоб вміст файлу games.csv був повністю завантажений і лише після цього використаний для опрацювання даних, записуємо виклик функції loadTable() у тілі функції preload().

Якщо аргумент header передано у виклик функції loadTable(), то кількість рядків у файлі буде обчислюватися, не враховуючи рядка заголовка (пункт 3), а доступ до елементів стовпця можна буде отримати як за назвою стовпця (пункт 5), так і за індексом (пункт 6). При цьому, в обидвох випадках назви стовпця не буде серед отриманих елементів.

Інакше, виклик функції loadTable() без header буде враховувати усі рядки файлу, включно із рядком заголовків, і серед елементів стовпця буде й значення його заголовка.

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

9 рядків у таблиці
3 стовпців у таблиці
(9) ["The Witcher 3: Wild Hunt", "Grand Theft Auto V", "Minecraft", "The Legend of Zelda: Breath of the Wild", "Fortnite", "Civilization VI", "Age of Empires II: Definitive Edition", "Heroes of Might and Magic III", "Disciples II: Dark Prophecy"]
Оскільки результати друкуються лише в консолі вебпереглядача, у коді ескізу блок draw() не використовуємо.

Щоб отримати доступ до конкретної комірки даних - значення на перетині рядка і стовпця, можна скористатися двома циклами for, як і у разі з двовимірними масивами.

sketch.js
let table;

function preload() {
  table = loadTable("games.csv", "csv", "header");
}

function setup() {
  for (let r = 0; r < table.getRowCount(); r++) { (1)
    print(table.getRow(r).arr); (2)
    print(table.getRow(r).obj); (3)
    for (let c = 0; c < table.getColumnCount(); c++) { (4)
      print(table.getString(r, c)); (5)
    }
    print(" "); (6)
  }
}
1 Виконуємо зовнішній цикл for по рядках CSV-файлу (без врахування рядка заголовків).
2 Друкуємо у консолі вебпереглядача значення поточного рядка даних у форматі масиву рядків.
3 Друкуємо у консолі вебпереглядача значення поточного рядка даних у форматі об’єкта, в якому ключами є назви заголовків, а значеннями - відповідні дані із поточного рядка.
4 Виконуємо вкладений цикл for по стовпцях CSV-файлу.
5 За допомогою методу getString() , який застосовується до об’єкта table, отримуємо рядкове значення із рядка з індексом r та стовпця з індексом c таблиці. Номер рядка визначається його індексом, тоді як стовпець може бути визначений або своїм індексом, або заголовком.
6 Застосовуємо функцію print(" "), яка цього разу використовується для створення порожніх рядків між надрукованими в консолі вебпереглядача рядками CSV-файлу.

У результаті отримаємо дані кожної комірки файлу games.csv порядково.

(3) ["The Witcher 3: Wild Hunt", "2015", "Action RPG"]
{Назва гри: "The Witcher 3: Wild Hunt", Рік випуску: "2015", Жанр: "Action RPG"}
The Witcher 3: Wild Hunt
2015
Action RPG

(3) ["Grand Theft Auto V", "2013", "Open World"]
{Назва гри: "Grand Theft Auto V", Рік випуску: "2013", Жанр: "Open World"}
Grand Theft Auto V
2013
Open World

(3) ["Minecraft", "2011", "Sandbox"]
{Назва гри: "Minecraft", Рік випуску: "2011", Жанр: "Sandbox"}
Minecraft
2011
Sandbox

(3) ["The Legend of Zelda: Breath of the Wild", "2017", "Action-Adventure"]
{Назва гри: "The Legend of Zelda: Breath of the Wild", Рік випуску: "2017", Жанр: "Action-Adventure"}
The Legend of Zelda: Breath of the Wild
2017
Action-Adventure

(3) ["Fortnite", "2017", "Battle Royale"]
{Назва гри: "Fortnite", Рік випуску: "2017", Жанр: "Battle Royale"}
Fortnite
2017
Battle Royale

(3) ["Civilization VI", "2016", "Turn-Based Strategy"]
{Назва гри: "Civilization VI", Рік випуску: "2016", Жанр: "Turn-Based Strategy"}
Civilization VI
2016
Turn-Based Strategy

(3) ["Age of Empires II: Definitive Edition", "2019", "Real-Time Strategy"]
{Назва гри: "Age of Empires II: Definitive Edition", Рік випуску: "2019", Жанр: "Real-Time Strategy"}
Age of Empires II: Definitive Edition
2019
Real-Time Strategy

(3) ["Heroes of Might and Magic III", "1999", "Turn-Based Strategy"]
{Назва гри: "Heroes of Might and Magic III", Рік випуску: "1999", Жанр: "Turn-Based Strategy"}
Heroes of Might and Magic III
1999
Turn-Based Strategy

(3) ["Disciples II: Dark Prophecy", "2002", "Turn-Based Strategy"]
{Назва гри: "Disciples II: Dark Prophecy", Рік випуску: "2002", Жанр: "Turn-Based Strategy"}
Disciples II: Dark Prophecy
2002
Turn-Based Strategy

Вправа 77

Створити CSV-файл з даними довільної тематики.

7.2.2. JSON

JSON або JavaScript Object Notation - це текстовий формат даних, що використовується для передачі структурованих даних в Інтернеті. Він був створений як складова мови JavaScript, але нині підтримується та активно використовується у більшості мов програмування як формат обміну даними.

JSON побудований з використанням двох структур даних: об’єкти та масиви.

Об’єкти в JSON - це невпорядковані набори пар ключ: значення, де ключ - це рядок, а значення може бути рядком, числом, булевим значенням, об’єктом, масивом або null. Об’єкти в JSON записуються у фігурних дужках {}.

Щоб зрозуміти, як дані організовуються в JSON-об’єкти, варто пригадати, як створюються і використовуються об’єкти у JavaScript.

Для цього розглянемо код застосунку, який використовує властивості об’єкта cnv для встановлення параметрів полотна застосунку.

sketch.js
let cnv = { (1)
  w: 300,
  h: 200,
  c: "rgb(157, 117, 203)" // Amethyst
};

function setup() {
  createCanvas(cnv.w, cnv.h); (2)
}

function draw() {
  background(cnv.c); (3)
}
1 Ініціалізуємо створення об’єкта cnv, використовуючи літеральний синтаксис (за допомогою фігурних дужок {}), із трьома властивостями, які визначатимуть розміри полотна по ширині w та висоті h зі значеннями 300 і 200 відповідно, а також, встановлять для полотна колір c зі значенням "rgb(157, 117, 203)".
2 Отримуємо значення властивостей cnv.w і cnv.h об’єкта cnv, використовуючи крапкову нотацію (вказується назва об’єкта і через крапку записується ім’я його властивості, значення якої необхідно отримати) і застосовуємо їх як аргументи у виклику функції createCanvas() для встановлення розмірів полотна.
3 Отримуємо значення властивості cnv.с об’єкта cnv, використовуючи крапкову нотацію і застосовуємо його як аргумент у виклику функції background() для встановлення кольору полотна.
Для належної роботи застосунків, які запускаються локально, необхідно використовувати локальний вебсервер . Вебсервер запускається із каталогу ескізу. У цьому разі, щоб переглянути свої ескізи, необхідно перейти у вебпереглядачі за адресою http://localhost:port/index.html, де port - номер порту.

У результаті виконання застосунку полотно змінить свій колір та розміри, використовуючи значення властивостей об’єкта cnv.

Тепер, використовуючи текстовий редактор, створимо новий файл і збережемо його з назвою cnv.json поруч з файлом sketch.js.

А далі, з файлу sketch.js перенесемо літерал об’єкта cnv у файл cnv.json і додамо подвійні лапки навколо ключів і значень властивостей

cnv.json
{
  "w": "300",
  "h": "200",
  "c": "rgb(157, 117, 203)"
}

залишивши у sketch.js лише оголошення змінної cnv.

Отже, ми вручну створили JSON-файл з даними у вигляді об’єкта.

Дані у JSON-форматі можна генерувати автоматично, використовуючи численні онлайн-ресурси, призначені для цього. Одним із таких сайтів є jsondataai.com , який генерує структуру і дані у форматі JSON, використовуючи штучний інтелект.

Цікавимось

Об’єкти JavaScript і JSON

JSON-файли використовують синтаксис об’єктів JavaScript і не містять коментарів.

На відміну від коду JavaScript, у якому властивості об’єкта можуть бути без лапок, у JSON, як властивості, можна використовувати лише рядки в подвійних лапках.

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

Доступ до даних у JSON-файлі відбувається за аналогією з об’єктами у JavaScript - використовуючи крапкову нотацію або синтаксис квадратних дужок.

На відміну від об’єктів JavaScript, JSON містить лише властивості, але не методи.

У JSON-форматі можна зберегти основні типи даних, що й у стандартному об’єкті JavaScript - рядки, числа, масиви, булеві значення та інші літерали об’єктів, вибудовуючи ієрархію даних.

Тепер прочитаємо дані зі створеного JSON-файлу.

Зробимо це за допомогою виклику функції loadJSON() , яка, як аргумент, отримує шлях до JSON-файлу і повертає об’єкт, що містить дані JSON-файлу.

Оскільки виклик loadJSON() є асинхронним, виконаємо його у тілі функції preload(), що гарантуватиме завершення завантаження даних із JSON-файлу до виклику функцій setup() і draw().

У підсумку, код файлу sketch.js матиме наступний вигляд:

sketch.js
let cnv;

function preload() {
  cnv = loadJSON("cnv.json");
}

function setup() {
  createCanvas(cnv.w, cnv.h);
}

function draw() {
  background(cnv.c);
}

Для доступу до даних, як і у разі з об’єктом JavaScript, використовується крапкова нотація (cnv.c), а якщо є потреба, можна використовувати нотацію квадратних дужок (cnv["c"]).

Результат виконання застосунку залишиться тим самим, оскільки значення властивостей, які завантажуються із файлу cnv.json, є однаковими як і для прикладу з об’єктом JavaScript. Проте, помістивши дані у JSON-файлі, код застосунку, який опрацьовує ці дані, тепер став краще структурованим і легшим для читання.

Тепер розглянемо приклад використання масиву у JSON-об’єкті.

Масиви в JSON - це упорядковані набори значень. Масиви в JSON записуються за допомогою квадратних дужок [] і можуть містити значення будь-якого типу.

Створимо JSON-файл, який буде містити масив об’єктів. Кожен об’єкт зберігатиме дані про відомих творців ІТ-технологій.

creators.json
{
  "creators":[
    {
      "firstName":"Дуглас",
      "lastName":"Крокфорд",
      "photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Douglas_Crockford.jpg/800px-Douglas_Crockford.jpg"
    },
    {
      "firstName":"Брендан",
      "lastName":"Айк",
      "photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Brendan_Eich_Mozilla_Foundation_official_photo.jpg/1024px-Brendan_Eich_Mozilla_Foundation_official_photo.jpg"
    },
    {
      "firstName":"Тім",
      "lastName":"Бернерс-Лі",
      "photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Tim_Berners-Lee_April_2009.jpg/800px-Tim_Berners-Lee_April_2009.jpg"
    },
    {
      "firstName":"Хокон",
      "lastName":"Віум Лі",
      "photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/H%C3%A5kon-Wium-Lie-2009-03.jpg/800px-H%C3%A5kon-Wium-Lie-2009-03.jpg"
    }
  ]
}

Розглянемо код застосунку, який використовує дані із файлу creators.json для створення елементів у DOM вебсторінки, на якій відображатиметься зображення відомого творця, а при наведенні вказівника на зображення - його ім’я.

Робота із DOM детально розглядається в Додатку B.
sketch.js
let data; (1)

function preload() {
  data = loadJSON("creators.json"); (2)
}

function setup() {
  createCanvas(200, 200);

  let firstName = data.creators[0].firstName; (3)
  let lastName = data.creators[0].lastName;
  let photo = data.creators[0].photo;

  let img = createImg(photo, `${firstName} ${lastName}`); (4)
  img.position(0, 0); (5)
  img.style("width", `${width}px`); (6)
  img.attribute("title", `${firstName} ${lastName}`); (7)
}

function draw() {} (8)
1 Оголошуємо змінну з ім’ям data, яке буде покликатися на об’єкт JSON, що міститиме дані з файлу creators.json.
2 Завантажуємо у застосунок дані з файлу creators.json.
3 Ініціалізуємо змінну з ім’ям firstName і значенням data.creators[0].firstName. Використовуючи крапкову нотацію звертаємось до JSON-об’єкта data, далі до масиву з ім’ям creators, а саме до першого його елемента з індексом 0, який є об’єктом ({"firstName":"Дуглас", "lastName":"Крокфорд", "photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Douglas_Crockford.jpg/800px-Douglas_Crockford.jpg"}), а потім отримуємо з цього об’єкта значення його властивості firstName (Дуглас). Для lastName і photo застосовується той самий алгоритм.
4 Використовуємо функцію createImg() для створення у DOM елемента зображення <img> з атрибутом src, який набуде значення photo (шлях до файлу зображення) і атрибутом alt зі значенням назви творця (тут використовується шаблонний рядок).
5 Застосовуємо до елемента img функцію position() , яка встановлює положення елемента відносно точки з координатами (0, 0) - лівий верхній кут полотна (початок системи координат полотна).
6 Застосовуємо до елемента img стиль за допомогою функції style() . Встановлюємо для властивості зображення width значення ширини полотна у пікселях (px). Висота зображення буде встановлена автоматично.
7 Додаємо до елемента img атрибут title зі значенням назви творця (шаблонний рядок), використовуючи функцію attribute() , яка додає атрибути до елементів.
8 Функція draw() у цьому разі не використовується, тому її тіло залишаємо порожнім.
Дуглас Крокфорд (Douglas Crockford) - творець JSON
Дуглас Крокфорд (Douglas Crockford) - творець JSON

Вправа 78

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

Як засіб перевірки та форматування даних JSON використовуйте JSON Formatter & Validator .

Вправа 79

З’ясувати, чи містить файл volcanoes.json помилки. Якщо так, то виправити їх.

JSON-файли можуть мати складнішу ієрархічну структуру. Розглянемо JSON-файл, який містить дані про певний університет.

university.json
{
  "університет":"Назва університету",
  "ректор":{
    "ім'я":"Ім'я Ректора",
    "вік":45,
    "освіта":"Доктор наук"
  },
  "факультети":[
    {
      "назва":"Факультет інформаційних технологій",
      "декан":{
        "ім'я":"Ім'я Декана",
        "вік":40,
        "освіта":"Кандидат наук"
      },
      "спеціальності":[
        {
          "назва":"Інформатика",
          "кількість_студентів":300
        },
        {
          "назва":"Комп'ютерні науки",
          "кількість_студентів":250
        }
      ]
    },
    {
      "назва":"Факультет економіки",
      "декан":{
        "ім'я":"Ім'я Декана",
        "вік":50,
        "освіта":"Доктор наук"
      },
      "спеціальності":[
        {
          "назва":"Економіка підприємства",
          "кількість_студентів":200
        },
        {
          "назва":"Міжнародна економіка",
          "кількість_студентів":150
        }
      ]
    }
  ],
  "студенти":[
    {
      "ім'я":"Ім'я Студента 1",
      "факультет":"Факультет інформаційних технологій",
      "курс":2
    },
    {
      "ім'я":"Ім'я Студента 2",
      "факультет":"Факультет економіки",
      "курс":3
    }
  ]
}

Розглянемо код застосунку, який змінює DOM вебсторінки ескізу, додаючи на неї інформацією про вищий навчальний заклад із файлу university.json.

sketch.js
let data; (1)

function preload() {
  data = loadJSON("university.json"); (2)
}

function setup() {
  noCanvas(); (3)
  createElement("h1", `${data.університет}`); (4)
  createElement("h2", `Ректор: ${data.ректор["ім'я"]}`); (5)
  createElement("h3", `Вік: ${data.ректор.вік}`);
  createElement("h3", `Освіта: ${data.ректор.освіта}`);

  for (let факультет of data.факультети) { (6)
    createElement("h2", `Факультет: ${факультет.назва}`);
    createElement("h2", `Декан: ${факультет.декан["ім'я"]}`);
    createElement("h3", `Вік: ${факультет.декан.вік}`);
    createElement("h3", `Освіта: ${факультет.декан.освіта}`);

    for (let спеціальність of факультет.спеціальності) { (7)
      createElement("p", `Спеціальність: ${спеціальність.назва}`);
      createElement(
        "p",
        `Кількість студентів: ${спеціальність.кількість_студентів}`
      );
    }
  }
}

function draw() {} (8)
1 Оголошуємо змінну з ім’ям data, яке буде покликатися на об’єкт JSON, що міститиме дані з файлу university.json.
2 Завантажуємо у застосунок дані з файлу university.json.
3 Використовуємо функцію noCanvas() для видалення стандартного полотна, оскільки для цього ескізу воно не використовується.
4 Використовуємо функцію createElement() , яка створює об’єкт на основі класу p5.Element для опису HTML-елементів. Перший аргумент функції - це назва елемента, який необхідно створити, а другий аргумент - вміст елемента. Для доступу до даних використовуємо крапкову нотацію і шаблонний рядок.
5 Оскільки назви властивостей у JSON-файлі записані українською, щоб отримати значення властивості ім’я, при зверненні до цієї властивості використовуємо нотацію квадратних дужок, а не крапкову нотацію.
6 Зовнішнім циклом for проходимо по факультетах (назва; ім’я, вік і освіта декана).
7 Внутрішнім циклом for проходимо по спеціальностях на кожному факультеті (назва спеціальності; кількість студентів).
8 Функція draw() для цього ескізу не використовується, тому її тіло залишаємо порожнім.

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

Вправа 80

Файл university.json містить дані про студентів. Вивести ці дані в консоль вебпереглядача.

7.2.3. Серіалізація та десеріалізація JSON

JavaScript надає спеціальні методи JSON.stringify() та JSON.parse() для роботи з даними у форматі JSON, які використовуються для серіалізації та десеріалізації JSON відповідно.

JSON.stringify() використовується для перетворення JavaScript-об’єкта в рядок JSON. Цей процес називається серіалізація об’єкта.

Проілюструємо як це працює на прикладі.

Створимо об’єкт country, використовуючи літерал об’єкта {} і додамо у створений об’єкт властивості name, continent, capital, facts, cuisine зі своїми значеннями.

let country = {};
country.name = "Україна";
country.continent = "Європа";
country.capital = "Київ";
country.facts = [];
country.facts.push({"валюта": "гривня"});
country.facts.push({"домен": "ua"});
country.facts.push({"телефонний код": 380});
country.cuisine = ["борщ", "вареники"];
let countrySerialize = JSON.stringify(country);
console.log(countrySerialize);

За допомогою JSON.stringify() об’єкт country перетворюється (запаковується) в JSON-рядок з ім’ям countrySerialize і результат друкується в консолі вебпереглядача.

{"name":"Україна","continent":"Європа","capital":"Київ","facts":[{"валюта":"гривня"},{"домен":"ua"},{"телефонний код":380}],"cuisine":["борщ","вареники"]}

До речі, серіалізацію можна застосувати до окремих властивостей об’єкта. Наприклад, серіалізуємо з об’єкта country лише властивості name і cuisine, надавши їх у вигляді масиву як другий аргумент для JSON.stringify.

...
let countrySerialize = JSON.stringify(country, ["name", "cuisine"], 4);
console.log(countrySerialize);

Третім аргументом для JSON.stringify є число 4 - це значення кількості пропусків у відступах при форматуванні структури отриманого JSON-рядка.

{
    "name": "Україна",
    "cuisine": [
        "борщ",
        "вареники"
    ]
}

А тепер використаємо JSON.parse() для перетворення JSON-рядка з ім’ям countrySerialize в протилежному напрямку - у JavaScript-об’єкт. Цей процес називається десеріалізація.

let countrySerialize = '{"name":"Україна","continent":"Європа","capital":"Київ","facts":[{"валюта":"гривня"},{"домен":"ua"},{"телефонний код":380}],"cuisine":["борщ","вареники"]}';
let country = JSON.parse(countrySerialize);
console.log(country);

У цьому прикладі відбувається перетворення (розпакування, парсинг) JSON-рядка у JavaScript-об’єкт.

{name: "Україна", continent: "Європа", capital: "Київ", facts: Array(3), cuisine: Array(2)}

7.2.5. Контрольні запитання

Міркуємо Обговорюємо

  1. Що спільного і відмінного у форматах зберігання даних CSV і TSV?

  2. У чому популярність JSON-формату для зберігання структурованих даних?

  3. Як виконується обробка даних, які зберігаються у JSON-форматі?

7.2.6. Практичні завдання

Файли з даними для виконання практичних завдань можна завантажити за покликанням.

Початковий

  1. Зберегти подані дані у CSV-файл і створити застосунок для зчитування даних з файлу у консоль вебпереглядача порядково у вигляді списків.

books_and_authors.csv
книга,автор,жанр,рік
1984,Джордж Орвелл,Наукова фантастика,1949
Великий Гетсбі,Ф. Скотт Фіцджеральд,Класика,1925
Хоббіт,Дж.Р.Р. Толкін,Фентезі,1937
Гаррі Поттер і філософський камінь,Дж.К. Роулінг,Фентезі,1997
  1. Виконати cеріалізацію об’єкта vacanciesIT з даними про вакансії популярних IT-компаній в Україні та деcеріалізацію отриманого результату.

const vacanciesIT = {
  посада: ["Frontend developer", "Data Scientist"],
  компанія: ["EPAM Systems", "SoftServe"],
  місце: ["Київ, Україна", "Сан-Франциско, США"],
  навички: [
    ["JavaScript", "React", "CSS", "HTML"],
    ["Python", "Машинне навчання", "Аналіз даних"],
  ],
  досвід: ["2+ роки", "3+ роки"],
  типЗайнятості: ["Повна зайнятість", "Віддалено"],
};

Середній

  1. Створити JSON-файли з даними на довільну тематику і різним ступенем вкладеності.

  1. Створити застосунок для читання даних із файлу countries.json і друку в консолі вебпереглядача назв країн, які розташовані в Європі.

Високий

  1. Використовуючи дані із файлу countries.tsv, визначити, які із країн мають кількість населення більше мільярда людей.

Екстремальний

  1. Надрукувати в консолі вебпереглядача назви комп’ютерних ігор та роки їх випуску, які розроблені для платформи PC жанру Real-Time Strategy (стратегія в реальному часі), використовуючи дані із файлу games.json.

7.3. Прикладний програмний інтерфейс

7.3.1. Що таке API?

Прикладний програмний інтерфейс, або інтерфейс програмування застосунків (API) дозволяє різним застосункам взаємодіяти один з одним.

Цікавимось

Що таке API?

Щоб краще зрозуміти сутність API, розглянемо його функції в контексті обов’язків офіціанта в ресторані.

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

У цьому разі, офіціант (API) приймає замовлення від клієнта (застосунок), приносить його на кухню (вебсервер, на якому працює шеф-кухар - застосунок, який обробляє замовлення/запит і готує страви/відповіді), каже, що клієнт замовив.

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

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

Багато сайтів мають свої API, які надають розробникам доступ до функцій та даних цих сайтів.

Сайти соціальних мереж (Facebook, X донедавна Twitter, Instagram, LinkedIn та інші) мають свої власні API, які дозволяють розробникам отримувати доступ до профілів користувачів, публікацій, коментарів і інших функцій соціальних мереж. Наприклад, всі ми звикли до того, що вхід на сайт можна здійснити без реєстрації, як такої, а через свої акаунти в соціальних мережах. Такі сайти за допомогою API використовують бази даних соціальних мереж.

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

Бази даних та хмарні сервіси (наприклад, Firebase, Amazon Web Services (AWS) та інші) також надають API, які дозволяють розробникам працювати з базами даних та різноманітними хмарними послугами.

Геолокаційні сервіси, як-от Google Maps, мають API для відображення мап та отримання географічних даних.

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

Розповсюдженим у вебі типом API є REST API (Representational State Transfer), за допомогою якого різні компоненти вебзастосунків можуть спілкуватися між собою.

Цікавимось Додатково

Типи API

Загалом існують два різні підходи до розробки API для обміну даними через Інтернет - це GraphQL і REST.

REST (Representational State Transfer) дозволяє клієнтським застосункам обмінюватися даними з сервером за допомогою HTTP - стандартного протоколу зв’язку в Інтернеті.

GraphQL - це мова запитів API, яка визначає специфікації того, як клієнтський застосунок має запитувати дані з віддаленого сервера.

API, розроблені за допомогою REST, відомі як RESTful API або REST API, а за допомогою GraphQL - GraphQL API, мають спільні та відмінні риси. Наприклад, і REST API, і GraphQL API підтримують формат даних JSON, проте мають і ключові відмінності у процесі обміну даними.

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

У разі, коли потрібно дізнатися номер телефону людини та останню її покупку, знадобляться кілька запитів REST API: запит, який повертає номер телефону, і запит, який повертає історію покупок. Внаслідок цього, довелося б написати багато коду лише для обробки запитів API, що вплинуло б на продуктивність застосунку, який використовує API.

GraphQL дозволяє повертати лише ті дані, які вказані в структурі, наданій застосунком.

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

Операційні системи, як-от Windows, Linux та інші, також мають власні API. Створення, копіювання, видалення файлів і каталогів - ці найвживаніші дії виконуються завдяки API операційної системи.

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

Наприклад, при розробці мобільного застосунку, в ролі API може використовуватися бібліотека для роботи з розумним будинком. Розробник може, навіть, не знати, як реалізована бібліотека, а лише звертатися до її API у своєму коді.

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

7.3.2. Отримання даних з Інтернету

API є мостом між різними сервісами та вебзастосунками в Інтернеті, дозволяючи ефективно обмінюватися даними та використовувати функціонал інших сайтів у власних застосунках.

Отримання даних відбувається за допомогою запитів до визначених URL-адрес сайтів з API.

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

Розглянемо, як створювати GET-запити - запити на отримання даних з сервера за вказаною URL-адресою.

Github API

Розпочнемо із найпростішого запиту в адресному рядку вебпереглядача до Github API (API для взаємодії з GitHub) за URL-адресою https://api.github.com/users/mojombo . Цей запит не вимагатиме автентифікації, оскільки ми намагаємось отримати загальнодоступну інформацію.

GitHub - один з найбільших вебсервісів для спільної розробки програмного забезпечення. Базується на системі керування версіями Git і розроблений на Ruby on Rails і Erlang компанією GitHub .

У результаті отримаємо відповідь у JSON-форматі. Це будуть дані про першого користувача та водночас одного із засновників GitHub.

Загальнодоступні API найчастіше повертають дані у JSON-форматі, які легко обробляти програмно.

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

sketch.js
let data; (1)

function preload() {
  data = loadJSON("https://api.github.com/users/mojombo"); (2)
}

function setup() {
  noCanvas();
  print(data.name); (3)
  print(data.blog); (4)
}

function draw() {}
1 Оголошуємо змінну з ім’ям data, яке буде покликатися на JSON-об’єкт, що міститиме дані, завантажені за вказаною URL-адресою.
2 Завантажуємо у застосунок дані за вказаною URL-адресою за допомогою функції loadJSON() , яка повертає JSON-об’єкт.
3 За допомогою крапкової нотації отримуємо значення властивості name.
4 За допомогою крапкової нотації отримуємо значення властивості blog.
Для належної роботи застосунків, які запускаються локально, необхідно використовувати локальний вебсервер . Вебсервер запускається із каталогу ескізу. У цьому разі, щоб переглянути свої ескізи, необхідно перейти у вебпереглядачі за адресою http://localhost:port/index.html, де port - номер порту.

У результаті виконання застосунку в консолі вебпереглядача надрукується результат із двох рядків.

Tom Preston-Werner
http://tom.preston-werner.com
My JSON Server

У GET-запитах часто передаються, як частина URL-адреси, параметри запиту. Коли сервер отримує такий запит з параметрами, він може обробити дані та повернути відповідь відповідно до параметрів, які були передані. Наприклад, будь-який запит в пошуковій системі Google містить параметри в URL-адресі.

Параметри запиту - це частина URL-адреси після адреси сторінки, яка визначає конкретний вміст або дії на основі даних, що передаються від вебпереглядача до сервера з метою отримання інформації, і починається зі знаку питання (?). Окремий параметр містить ключ та значення, розділені символом дорівнює (=). Якщо використовується кілька параметрів, вони розділяються символом амперсанда (&).

Використаємо параметри запиту у нашому запиті, щоб відфільтрувати отримані дані, які повертає сервер.

Для цього скористаємось сервісом My JSON Server - Fake online REST server for teams . Цей сервіс - це вебсервер з API, який може повертати вигадані дані у JSON-форматі.

За потреби можна створити власний JSON Server зі своїм REST API.

Перейдемо на сторінку demo та оберемо ресурс posts (дописи).

Унаслідок наших дій у вікні вебпереглядача відкриється набір даних у JSON-форматі:

[
  {
    "id": 1,
    "title": "Post 1"
  },
  {
    "id": 2,
    "title": "Post 2"
  },
  {
    "id": 3,
    "title": "Post 3"
  }
]

Використаємо параметр запиту id=1, записавши його у кінець URL-адреси після знака питання (?): https://my-json-server.typicode.com/typicode/demo/posts?id=1.

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

[
  {
    "id": 1,
    "title": "Post 1"
  }
]

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

Тепер у кінець URL-адреси після знака питання (?) запишемо параметр запиту title=Post 2: https://my-json-server.typicode.com/typicode/demo/posts?title=Post%202.

В URL-адресах, записаних в адресному рядку вебпереглядача, деякі символи кодуються за допомогою послідовностей інших символів автоматично. Наприклад, пропуски кодуються символами %20 або замінюються символом плюса (+), кома - символами %2C, відсоток - %25, лапки - %22 тощо.

У результаті отримаємо дані, що відповідають допису з назвою Post 2:

[
  {
    "id": 2,
    "title": "Post 2"
  }
]

Використаємо у нашому запиті кілька параметрів, розділивши їх символом амперсанда (&): https://my-json-server.typicode.com/typicode/demo/posts?id=1&id=3.

У вікні вебпереглядача виводиться перший (відповідає першому параметру запиту) і третій дописи (відповідає другому параметру запиту):

[
  {
    "id": 1,
    "title": "Post 1"
  },
  {
    "id": 3,
    "title": "Post 3"
  }
]

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

Вправа 81

Виконати GET-запит за URL-адресою https://my-json-server.typicode.com/typicode/demo/comments для отримання коментаря зі значенням id, що дорівнює 2.

The Space Devs APIs

Розглянемо ще один приклад роботи із загальнодоступними API під назвою The Space Devs .

The Space Devs - це група розробників-ентузіастів, які працюють над низкою послуг, об’єднаних спільною метою - покращити знання та доступність інформації про космічні польоти, надаючи корисні дані та інструменти, доступні кожному безплатно.
Безплатне використання обмежене 15 запитами на годину.

Сайт має велику базу даних та зручні інструменти API для роботи з даними. На сторінці Launch Library API представлені категорії даних, зразки даних, покликання на документацію і відповідні API Endpoint.

Endpoint (кінцева точка) - це URL-адреса, куди API надсилає запити та де знаходиться ресурс.

Створимо запит для отримання даних, пов’язаних з програмою Artemis - програмою дослідження Місяця під керівництвом NASA .

Алгоритм наших дій буде таким:

  1. На сторінці Launch Library API обрати категорію Events.

  2. На сторінці Event List натиснути кнопку API ENDPOINT.

  3. У вікні Field filters обрати поле з назвою Program і встановити для нього значення Artemis, позначивши його у списку.

  4. У вікні Field filters натиснути кнопку SUBMIT для створення GET-запиту.

У результаті отримаємо кілька сторінок даних у JSON-форматі відповідно до нашого запиту.

Стандартним налаштуванням є 10 елементів даних на сторінку.

Тепер використаємо сторінку документації API Launch Library Docs , яка надає ширший інструментарій для роботи із базою даних подій космічних польотів. Для прикладу, дізнаємось, скільки зараз астронавтів перебуває у космосі.

Алгоритм наший дій буде таким:

  1. Знайти розділ з назвою astronaut.

  2. Відкрити налаштування GET-запиту.

  3. Натиснути на кнопку Try it out, щоб мати змогу змінювати параметри запиту.

  4. Серед параметрів знайти параметр з назвою in_space і встановити для нього значення true.

  5. Натиснути кнопку Execute для виконання запиту.

У підсумку, відповідь сервера у JSON-форматі міститиме дані про усіх астронавтів, які перебувають у космосі.

З рядка Request URL скопіюємо URL-адресу запиту, щоб використати її у коді застосунку, який буде визначати чи є серед астронавтів, які перебувають у космосі, представник, наприклад, з Японії. Якщо так, то виведемо його ім’я.

sketch.js
let data; (1)

function preload() {
  data = loadJSON("https://ll.thespacedevs.com/2.2.0/astronaut/?in_space=true"); (2)
}

function setup() {
  noCanvas();
  for (let i = 0; i < data.results.length; i++) { (3)
    let astronaut = data.results[i]; (4)
    if (astronaut.nationality === "Japanese") { (5)
      print(astronaut.name); (6)
    }
  }
}

function draw() {}

Проаналізуємо наведений код.

1 Оголошуємо змінну з ім’ям data, яке буде покликатися на об’єкт JSON, що міститиме дані, завантажені за URL-адресою.
2 Завантажуємо у застосунок дані за вказаною URL-адресою. Зверніть увагу на використання параметра in_space зі значенням true, який дозволить відфільтрувати дані, які стосуються лише астронавтів, що перебувають у космосі.
3 У циклі for проходимо по властивості data.results, яка є масивом об’єктів.
4 Ініціалізуємо змінну з ім’ям astronaut значенням поточного об’єкта data.results[i].
5 Перевіряємо, чи властивість nationality поточного об’єкта astronaut має значення "Japanese".
6 Якщо виконується пункт 5, друкуємо в консолі вебпереглядача ім’я астронавта, звертаючись до властивості name об’єкта astronaut.

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

Satoshi Furukawa

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

Це можна зробити, як за допомогою інструментів на сторінці API Launch Library Docs у розділі з назвою launch, так і за допомогою коду. Використаємо другий спосіб.

У цьому разі наш запит буде містити параметр net__gte зі значенням дати у форматі 2024-01-01T21:45:00Z.

Назва "net" розшифровується як "No Earlier Than" (не раніше, ніж) і позначає час запуску для конкретної місії або події пов’язаної із запуском ракети, а "gte" - це is greater than or equal to - більше або дорівнює.

Отже, відповіддю на запит мають бути дані про запуски, які заплановано не раніше, ніж на 1 січня 2024 року о 21:45:00 за Всесвітнім координованим часом (UTC) .

Розглянемо код застосунку, який це реалізує.

sketch.js
let data;

function preload() {
  data = loadJSON(
    "https://ll.thespacedevs.com/2.2.0/launch/?limit=20&net__gte=2024-01-01T21:45:00Z" (1)
  );
}

function setup() {
  noCanvas();
  print(data); (2)
}

function draw() {}
1 Додатковий параметр limit=20 у рядку запиту використовується для встановлення значення 20 для ліміту на кількість даних, отриманих від сервера, оскільки за стандартним налаштуванням сервер надсилає лише 10 елементів даних. Також у запиті використовується два параметри, тому вони об’єднуються за допомогою символу амперсанда (&).
2 Друк у консолі вебпереглядача вмісту об’єкта JSON.

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

{count: 345, next: "https://ll.thespacedevs.com/2.2.0/launch/?limit=20&net__gte=2024-01-01T21%3A45%3A00Z&offset=20", previous: null, results: Array(20)}

Цікавимось

Організації та виведення результатів запиту до API

Проаналізуємо результати виведення. Поле count містить кількість запусків всього.

Поле next містить URL-адресу, за допомогою якої можна отримати наступну порцію даних (наприклад, дані про наступні 20 запусків). Сервер може включити поле previous у відповідь, яке містить URL-адресу для отримання попередньої порції даних. У цьому разі значення у previous відсутнє (null).

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

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

Параметр offset вказує, з якого номера елемента даних починати виводити результати на наступній сторінці. У нашому запиті offset=20 означає, що ми хочете отримати результати починаючи з 21-го елемента. Зазначивши offset=20, ми отримаємо наступні 20 результатів (з 21-го по 40-й).

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

Отже, значення поля results містить масив із 20 елементів даних. Витягнемо із кожного елементу даних інформацію про місце, рік і місяць запусків та назви ракет-носіїв.

sketch.js
let data;
const months = [ (1)
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

function preload() {
  data = loadJSON(
    "https://ll.thespacedevs.com/2.2.0/launch/?limit=20&net__gte=2024-01-01T21:45:00Z"
  );
}

function setup() {
  noCanvas();
  for (let i = 0; i < data.results.length; i++) { (2)
    let launch = data.results[i]; (3)
    let d = new Date(`${launch.net}`); (4)
    let y = d.getFullYear(); (5)
    let m = months[d.getMonth()]; (6)
    print(launch.name, y, m); (7)
    print(launch.pad.location.name); (8)
  }
}

function draw() {}
1 Ініціалізуємо масив з ім’ям months, який містить назви місяців року.
2 Завантаживши у застосунок дані за вказаною URL-адресою, у циклі for проходимо по властивості data.results, яка є масивом об’єктів.
3 Ініціалізуємо змінну з ім’ям launch значенням поточного об’єкта data.results[i].
4 Ініціалізуємо змінну з ім’ям d значенням об’єкта дати, що створюється за допомогою класу Date(), у конструктор якого передається launch.net - дані про дату запуску у форматі 2024-01-01T21:45:00Z. У результаті під ім’ям d зберігається дата у форматі на зразок Mon Mar 04 2024 02:00:00 GMT+0200 (за східноєвропейським стандартним часом).
5 Ініціалізуємо змінну з ім’ям y значенням чотиризначного цілого числа року, яке отримуємо за допомогою методу getFullYear(), що застосовуємо на об’єкті d.
6 Ініціалізуємо змінну з ім’ям m значенням назви місяця. Спочатку отримуємо за допомогою методу getMonth(), що застосовуємо на об’єкті d, номер місяця як ціле число, а потім звертаємось до масиву months для отримання назви місяця, використовуючи значення номера місяця як індекс.
7 Друкуємо у консолі вебпереглядача значення launch.name - назву ракети-носія, y - рік запуску, m - місяць запуску.
8 Друкуємо у консолі вебпереглядача значення launch.pad.location.name - назву місця запуску.

У підсумку, в консолі вебпереглядача отримаємо результат (показаний лише фрагмент даних):

...
LVM-3 | Gaganyaan-1 2024 February
Satish Dhawan Space Centre, India
Falcon 9 Block 5 | Dragon CRS-2 SpX-30 2024 March
Kennedy Space Center, FL, USA
WTIA API

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

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

Для візуалізації у вебпереглядачі відповіді у JSON-форматі з відступами, необхідно у запиті використовувати параметр indent=4, значення якого визначає число пропусків, які використовуються для відступів.

Отож, отримаємо дані про об’єкти на орбіті, виконавши запит https://api.wheretheiss.at/v1/satellites?indent=4 в адресному рядку вебпереглядача. У вікні вебпереглядача отримаємо відповідь у відформатованому вигляді, включаючи загальну назву та ідентифікатор об’єкта за супутниковим каталогом NORAD:

[
    {
        "name":"iss",
        "id":25544
    }
]

Як бачимо, API має інформацію лише про Міжнародну космічну станцію (International Space Station Current Location, ISS) - пілотовану космічну станцію на орбіті Землі, яка створена для наукових досліджень у космосі.

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

sketch.js
let data;

function preload() {
  data = loadJSON("https://api.wheretheiss.at/v1/satellites/25544");
}

function setup() {
  noCanvas();
  for (let k in data) {
    print(k, data[k]);
  }
}

function draw() {}

У консолі вебпереглядача буде надрукована інформація про космічну станцію цей момент часу:

name iss
id 25544
latitude 31.807244912515
longitude -39.070205528184
altitude 416.35076448151
velocity 27596.312138889
visibility daylight
footprint 4488.8509955455
timestamp 1703339614
daynum 2460302.0788657
solar_lat -23.430429373069
solar_lon 331.35290669011
units kilometers

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

Цікавимось

Концепція AJAX

Одна із корисних можливостей JavaScript - отримувати дані та обробляти їх у вебпереглядачі без перезавантаження вебсторінки.

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

Така поведінка реалізується завдяки AJAX (Asynchronous JavaScript and XML або асинхронний JavaScript і XML) - набору методів веброзробки, який використовує різні вебтехнології для створення асинхронних вебзастосунків.

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

Спочатку AJAX призначався для отримання даних у форматі XML - розширеної мови розмітки структурованих даних для обміну даними між різними застосунками, але зараз - використовується як узагальнений термін різних способів обміну даними між віддаленим сервером і вебпереглядачем.

fetch

Для реалізації асинхронного підходу до розробки застосунків мова JavaScript має власний вбудований інтерфейс з назвою Fetch API .

Щоб використати Fetch API, необхідно викликати метод fetch() і передати йому як обов’язковий параметр URL-адресу API. Отож, перепишемо попередній код з використанням метода fetch().

sketch.js
function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544"; (1)
  fetch(url) (2)
    .then((response) => { (3)
      return response.json(); (4)
    })
    .then((data) => { (5)
      print(data);
    });
}

function draw() {}

Проаналізуємо цей новий синтаксис у створенні запитів за допомогою fetch().

1 Ініціалізуємо константу url зі значенням рядка запиту.
2 Виклик метода fetch(), передавання йому як обов’язкового параметра значення url та виконання GET-запиту.
3 Метод fetch() повертає Promise (проміс) JavaScript. Далі метод then() проміса повертає об’єкт response, який містить HTTP-відповідь з кодом стану, URL-адресою запиту та інші дані.
4 Коли метод then() поверне відповідь, до відповіді застосовуємо метод json(), оскільки відповідь надійшла у JSON-форматі, а результат у формі об’єкта повертаємо за допомогою return.
5 Ще раз викликаємо метод then(), аргументом в якому використовуємо ім’я data як покликання на об’єкт проміса, для виведення у консоль вебпереглядача.

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

{name: "iss", id: 25544, latitude: -40.345611695911, longitude: 93.681884921012, altitude: 428.1503111178…}
Проміси у JavaScript - це спеціальні об’єкти, що містять асинхронні методи, які замість негайного надсилання певних даних, надсилають обіцянку надати значення в якийсь момент у майбутньому. Це подібно ситуації, коли ви підписані на певну сторінку у соціальній мережі та отримуєте повідомлення про нові дописи на ній, коли такі дописи з’являються.

Змінимо код, щоб обробити появу помилок.

sketch.js
function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  fetch(url)
    .then((response) => {
      if (response.status >= 200 && response.status < 400) { (1)
        return response.json();
      } else { (2)
        // якщо отримали помилку від сервера
        console.log(`${response.status} ${response.statusText}`);
      }
    })
    .then((data) => {
      print(data);
    })
    .catch((error) => { (3)
      // обробка інших помилок
      console.log(error);
    });
}

function draw() {}

Проаналізуємо код, який ми додали для обробки помилок.

1 Перевіряємо код стану HTTP-відповіді від сервера. Якщо запит був успішний, то застосовуємо метод json() для синтаксичного аналізу відповіді, яка надійшла у JSON-форматі.
2 Інакше - друкуємо в консолі вебпереглядача повідомлення про помилку, використовуючи шаблонний рядок і значення полів response.statusText та response.status.
3 Обробляємо інші помилки за допомогою метода catch().

Використовуючи дані, отримані зі сервера, дізнаємось висоту (altitude) орбіти космічної станції у км і швидкість (velocity) її руху у км/год на цей момент часу.

sketch.js
function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  fetch(url)
    .then((response) => {
      if (response.status >= 200 && response.status < 400) {
        return response.json();
      } else {
        // якщо отримали помилку від сервера
        console.log(`${response.status} ${response.statusText}`);
      }
    })
    .then((data) => {
      print(data.altitude); (1)
      print(data.velocity); (2)
    })
    .catch((error) => {
      // обробка інших помилок
      console.log(error);
    });
}

function draw() {}
1 Отримаємо значення висоти орбіти data.altitude МКС і виводимо в консоль вебпереглядача.
2 Отримаємо значення швидкості руху data.velocity МКС і виводимо в консоль вебпереглядача.

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

417.15298199586
27582.034295679

Вправа 82

Перевірити код застосунку, наведений вище, в обробці помилок. Для цього змусити код згенерувати помилку 404 Not Found (сервер не може знайти запитуваний ресурс) і проаналізувати інші помилки, якщо такі будуть. У разі успішного запиту в консолі вебпереглядача виводиться інформація про видимість (visibility) об’єкта на небі.

async/await

Для кращого розуміння і простоти використання промісів, приклад роботи з якими розглядався вище, застосовують зарезервовані слова async і await.

Зарезервоване слово async записується перед оголошенням функції, внаслідок чого функція стане асинхронною. А зарезервоване слово await змушує інтерпретатор JavaScript чекати доти, доки проміс праворуч від await виконається. Після чого await поверне його результат і виконання коду продовжиться.

Використаємо метод fetch() в синтаксисі async/await у нашому застосунку про міжнародну космічну станцію.

Цікавимось

Відповідність коду версії ECMAScript

При написанні коду на JavaScript, він має відповідати певним правилам. JSHint - один із засобів для статичного аналізу коду на JavaScript, який якраз перевіряє, чи початковий код на JavaScript відповідає правилам кодування, які визначені ECMAScript - версією стандарту мови JavaScript.

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

/* jshint esversion: 8 */

У цьому разі використовуватиметься версія ECMAScript 8 (ECMAScript 2017), в якій помітним нововведенням стали асинхронні функції.

sketch.js
/* jshint esversion: 8 */ (1)

function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  async function whereTheISSat() { (2)
    const response = await fetch(url); (3)
    const data = await response.json(); (4)
    print(data); (5)
  }
  whereTheISSat(); (6)
}

function draw() {}
1 Встановлення версії ECMAScript, яка визначатиме правила написання коду.
2 Оголошення асинхронної функції whereTheISSat() за допомогою зарезервованого слова async.
3 Інтерпретатор JavaScript чекає доти, доки проміс у методі fetch() праворуч від зарезервованого слова await виконається, тобто буде отримана відповідь від сервера на запит за адресою url. Після цього відбудеться ініціалізація значення відповіді з ім’ям response.
4 Інтерпретатор JavaScript чекає доти, доки до response буде застосований метод json(). Після цього відбудеться ініціалізація отриманого результату з ім’ям data.
5 Значення data друкується в консолі вебпереглядача.
6 Виклик асинхронної функції whereTheISSat().

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

Оголосимо ще одну асинхронну функцію з назвою getImageISS(), яка буде запитувати за URL-адресою зображення МКС і розміщувати його на полотні. Оскільки WTIA API не надає графічних даних, використаємо зображення МКС із сайту The Space Devs APIs.

sketch.js
/* jshint esversion: 8 */

function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  async function whereTheISSat() {
    const response = await fetch(url);
    const data = await response.json();
    print(data.name); (1)
  }
  whereTheISSat();

  async function getImageISS() { (2)
    const response = await fetch(  (3)
      "https://spacelaunchnow-prod-east.nyc3.digitaloceanspaces.com/media/spacestation_images/international2520space2520station_image_20190220215716.jpeg"
    );
    const blob = await response.blob(); (4)
    let srcImage = URL.createObjectURL(blob); (5)
    let img = createImg(srcImage, "ISS"); (6)
    img.position(0, 0); (7)
    img.attribute("width", "200px"); (8)
  }
  getImageISS(); (9)
}

function draw() {}
1 Результат роботи асинхронної функції whereTheISSat() - виконання запиту і виведення у консоль вебпереглядача абревіатури МКС англійською.
2 Оголошення асинхронної функції getImageISS().
3 Асинхронний запит за URL-адресою зображення та ініціалізація відповіді з ім’ям response.
4 Застосування до response методу blob() - метод синтаксичного аналізу тіла запиту, представленого як Blob (Binary Large Object) та ініціалізація отриманого результату з ім’ям blob.
5 Створення за допомогою метода createObjectURL рядка, що містить URL-адресу, яка представляє об’єкт blob, вказаний як аргумент, та ініціалізація рядка з ім’ям srcImage.
6 Використання функції createImg() , яка створює елемент <img> у DOM з атрибутами src і значенням srcImage та alt зі значенням "ISS" відповідно.
7 Застосування методу position() , який встановлює положення елемента з ім’ям img у точці з координатами (0, 0), що розміщена у лівому верхньому куті вікна перегляду.
8 За допомогою метода attribute() додамо атрибут width до елемента з ім’ям img зі значенням 200px для встановлення ширини зображення розміром 200 пікселів.
9 Виконуємо виклик асинхронної функції getImageISS().
International Space Station Current Location
Міжнародна космічна станція
Тип даних Blob (Binary Large Object) використовується для зберігання та обробки мультимедійних даних, таких як зображення, відео та аудіо.
Робота із DOM детально розглядається в Додатку B.

Для обидвох функцій whereTheISSat() і getImageISS() можна написати розширену обробку помилок з урахуванням стану відповіді, який повертається сервером.

Використаємо для цього інструкцію try...catch , яка складається з блоку try і блоку catch, блоку finally або усіх перелічених блоків коду.

Код у блоці try виконується першим, і якщо він генерує помилку, буде виконано код у блоці catch. Код у блоці finally завжди виконується, незалежно чи виконується try, чи catch.

У підсумку код застосунку матиме наступну структуру:

sketch.js
/* jshint esversion: 8 */

function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  async function whereTheISSat() {
    try {
      const response = await fetch(url);
      if (response.status >= 200 && response.status < 400) {
        const data = await response.json();
        print(data.name);
      } else {
        // якщо отримали помилку від сервера
        console.log(`${response.status} ${response.statusText}`);
      }
    } catch (error) {
      // обробка інших помилок
      console.log(error);
    } finally {
      console.log("Завершено для whereTheISSat");
    }
  }
  whereTheISSat();

  async function getImageISS() {
    try {
      const response = await fetch(
        "https://spacelaunchnow-prod-east.nyc3.digitaloceanspaces.com/media/spacestation_images/international2520space2520station_image_20190220215716.jpeg"
      );
      if (response.status >= 200 && response.status < 400) {
        const blob = await response.blob();
        let srcImage = URL.createObjectURL(blob);
        let img = createImg(srcImage, "ISS");
        img.position(0, 0);
        img.attribute("width", "200px");
      } else {
        // якщо отримали помилку від сервера
        console.log(`${response.status} ${response.statusText}`);
      }
    } catch (error) {
      // обробка інших помилок
      console.log(error);
    } finally {
      console.log("Завершено для getImageISS");
    }
  }
  getImageISS();
}

function draw() {}

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

Завершено для getImageISS
iss
Завершено для whereTheISSat

Цікавимось Додатково

XMLHttpRequest API

XMLHttpRequest - це API, який, як і Fetch API, надає спосіб отримання даних за URL-адресою без перезавантаження вебсторінки.

Попри наявність у його назві абревіатури XML , його можна застосовувати для роботи з усіма типами даних.

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

Попередній приклад з використанням XMLHttpRequest матиме код

sketch.js
function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  const request = new XMLHttpRequest();
  request.open("GET", url);
  request.send();
  request.onload = () => {
    if (request.status >= 200 && request.status < 400) {
      const data = JSON.parse(request.response);
      print(data.id);
    } else {
      // якщо отримали помилку від сервера
      console.log(`${request.status} ${request.statusText}`);
    }
  };
  // помилка запиту
  request.onerror = () => console.log(request.statusText);
}

function draw() {}

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

25544

Оскільки космічна станція рухається, відповідно змінюються її координати. Напишемо код застосунку, який буде виконувати запит з частотою 1 раз за 5 секунд (WTIA API має обмеження на 1 запит за секунду), на отримання значень широти (latitude) і довготи (longitude) місця перебування МКС та відображення їх у консолі. Для створення запитів, які повторюються через кожні 5 секунд, використаємо метод fetch().

sketch.js
/* jshint esversion: 8 */

function setup() {
  noCanvas();

  const url = "https://api.wheretheiss.at/v1/satellites/25544";
  async function whereTheISSat() {
    const response = await fetch(url);
    const data = await response.json();
    let [latitude, longitude] = [data.latitude, data.longitude]; (1)
    print(`${latitude.toFixed(3)}\u00B0, ${longitude.toFixed(3)}\u00B0`); (2)
  }
  setInterval(whereTheISSat, 5000); (3)
}

function draw() {}
1 Ініціалізуємо змінні з іменами latitude і longitude значеннями data.latitude та data.longitude відповідно до отриманих даних відповіді від сервера.
2 Друкуємо в консолі вебпереглядача значення latitude і longitude. Застосовуючи метод toFixed() до значень ширити й довготи, встановлюємо кількість цифр після десяткової крапки, у цьому разі 3. Послідовність \u00B0 - шістнадцяткове значення символу градуса.
3 Використовуємо метод setInterval() , який багаторазово викликає асинхронну функцію whereTheISSat() з фіксованою затримкою між кожним викликом, яка дорівнює 5000 мілісекунд (5 секунд).
Щоб скасувати повторення викликів setInterval(), використовується метод clearInterval() , який в ролі аргументу використовує ідентифікатор дії, яку потрібно скасувати. Ідентифікатор дії створюється і повертається відповідним викликом setInterval().

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

Переглядаємо Аналізуємо

dummyJSON

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

Використаємо для ілюстрації цього DummyJSON - API, який використовує вигадані набори даних у JSON-форматі.

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

sketch.js
function setup() {
  noCanvas();

  const url =
    "https://dummyjson.com/products?limit=3&select=id,title,stock"; (1)
  fetch(url)
    .then((response) => {
      if (response.status >= 200 && response.status < 400) {
        return response.json();
      } else {
        console.log(`${response.status} ${response.statusText}`);
      }
    })
    .then((data) => { (2)
      let stuff = data.products;
      for (let i = 0; i < stuff.length; i++) { (3)
        let good = stuff[i]; (4)
        print(good.id, good.title, good.stock); (5)
      }
    })
    .catch((error) => {
      console.log(error);
    });
}

function draw() {}
1 У рядку запиту використовуємо два параметри: limit=3 - визначає скільки елементів даних отримувати (limit=0 без обмежень) і select=id,title,stock,category, значенням якого є низка назв полів, розділених комами, які будуть присутні у кожному з елементів даних.
2 Після успішного запиту, в метод then() як аргумент передаємо (ініціалізуємо) змінну з ім’ям data (за потреби можна обрати інше ім’я), яке буде покликатися на об’єкт з отриманими даними. Далі використовуємо data для наших завдань, наприклад у цьому разі друкуємо в консолі вебпереглядача номер, назву та кількість одиниць 3-ох товарів.
3 Проходимо у циклі for по елементах масиву stuff, які є об’єктами з даними конкретних товарів.
4 Ініціалізуємо змінну з ім’ям good, яке буде покликатися на значення об’єкта stuff[i], що позначає конкретний товар.
5 Друкуємо в консолі вебпереглядача значення обраних властивостей об’єкта good.
1 "iPhone 9" 94
2 "iPhone X" 34
3 "Samsung Universe 9" 36

Інший спосіб роботи з даними, отриманими асинхронним способом, полягає у використанні метода finally() , код у тілі якого завжди виконується незалежно від результату виконання запиту. Це дає змогу проаналізувати дані, які були отримані, чи просто виконати певний фрагмент коду у будь-якому разі.

sketch.js
let fetchedData; (1)

function setup() {
  noCanvas();

  const url =
    "https://dummyjson.com/products?limit=3&select=id,title,stock";
  fetch(url)
    .then((response) => {
      if (response.status >= 200 && response.status < 400) {
        return response.json();
      } else {
        console.log(`${response.status} ${response.statusText}`);
      }
    })
    .then((data) => {
      fetchedData = data; (2)
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => { (3)
      let stuff = fetchedData.products;
      for (let i = 0; i < stuff.length; i++) {
        let good = stuff[i];
        print(good.id, good.title, good.stock);
      }
    });
}

function draw() {}
1 Оголошуємо глобальну змінну з ім’ям fetchedData, яке буде покликатися на об’єкт отриманих даних data.
2 Зберігаємо об’єкт отриманих даних під ім’ям fetchedData.
3 У метод finally() як аргумент передаємо у стрілочну функцію без параметрів (() >), а у тілі стрілочної функції ({ }) використовуємо код для обробки отриманих даних.

Варто звернути увагу на те, що у вищенаведених прикладах обробка даних відбувається у самій конструкції асинхронного запиту. А як бути у разі, коли отримані дані необхідно використати в інших місцях коду, наприклад у блоці draw()?

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

sketch.js
let fetchedData;
let results = []; (1)

function setup() {
  noCanvas();

  const url = "https://dummyjson.com/products?limit=3&select=id,title,stock";
  fetch(url)
    .then((response) => {
      if (response.status >= 200 && response.status < 400) {
        return response.json();
      } else {
        console.log(`${response.status} ${response.statusText}`);
      }
    })
    .then((data) => {
      fetchedData = data;
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
      let stuff = fetchedData.products;
      for (let i = 0; i < stuff.length; i++) {
        let good = stuff[i];
        results.push(good); (2)
      }
    });
}

function draw() {
  print(results); (3)
}
1 Ініціалізуємо порожній масив з ім’ям results.
2 Об’єкти good з даними про конкретний товар додаємо у масив results використовуючи метод push().
3 У блоці draw() друкуємо в консолі вебпереглядача значення масиву results.

Оскільки тіло функції draw() виконується безперервно, коли застосунок працює, можемо спостерігати таку картину, як спочатку друкуються порожні масиви ([]), а через деякий час вже виводяться масиви, які містять по три об’єкти кожен.

...
[]
[]
[]
(3) [Object, Object, Object]
(3) [Object, Object, Object]
...

Для отримання даних асинхронним методом потрібен певний час, тому коли ще дані не отримані, а значення масиву вже друкуються в консолі завдяки інструкції print(results); у блоці draw(), ми спостерігаємо літерали порожніх масивів. Як тільки асинхронний запит виконається, масив друкуватиметься із доданими у нього даними.

Таку поведінку можна змінити.

sketch.js
let fetchedData;
let success = false; (1)
let results = [];

function setup() {
  noCanvas();

  const url = "https://dummyjson.com/products?limit=3&select=id,title,stock";
  fetch(url)
    .then((response) => {
      if (response.status >= 200 && response.status < 400) {
        return response.json();
      } else {
        console.log(`${response.status} ${response.statusText}`);
      }
    })
    .then((data) => {
      fetchedData = data;
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
      let stuff = fetchedData.products;
      for (let i = 0; i < stuff.length; i++) {
        let good = stuff[i];
        results.push(good);
      }
      success = true; (2)
    });
}

function draw() {
  if (success) { (3)
    print(results);
    success = false; (4)
  }
}
1 Ініціалізуємо змінну з ім’ям success значенням false. За допомогою неї будемо визначати, чи завершився процес отримання даних.
2 Дані отримані, тому у тілі метода finally() змінюємо значення success на true.
3 У блоці draw() перевіряємо значення success. Якщо true (дані отримано), друкуємо вміст масиву results, який містить дані відповідно до нашого завдання або виконуємо додаткові з його вмістом, наприклад візуалізуємо дані.
4 Повертаємо значення false для success, щоб уникнути повторення виведення у draw(). Після цього зміна значення success на true не відбудеться, оскільки перший і єдиний раз це стається у тілі метода finally(), в якому код виконується лише раз.

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

Змінимо код застосунку у разі використання синтаксису async/await.

sketch.js
let results = [];

function setup() {
  noCanvas();

  const url = "https://dummyjson.com/products?limit=3&select=id,title,stock";
  async function fetchData() {
    try {
      const response = await fetch(url);
      if (response.status >= 200 && response.status < 400) {
        const data = await response.json();
        return data; (1)
      } else {
        console.log(`${response.status} ${response.statusText}`);
      }
    } catch (error) {
      console.log(error);
    }
  }
  // виклик асинхронної функції fetchData()
  let result = fetchData(); (2)
  result.then((fetchedData) => { (3)
    parseData(fetchedData); (4)
  });
}

function parseData(receivedData) { (5)
  let stuff = receivedData.products;
  for (let i = 0; i < stuff.length; i++) {
    let good = stuff[i];
    results.push(good);
  }
  print(results); (6)
}
1 Значення data буде повертатися з асинхронної функції fetchData() при її виклику.
2 Ініціалізація змінної з ім’ям result, яке буде покликанням на результат виклику асинхронної функції fetchData(), зі значенням, яке функція повертає (проміс).
3 Застосовуємо до проміса result метод then(), у який як аргумент передаємо ім’я fetchedData, ініціалізуючи змінну з таким ім’ям (за потреби можна обрати інше ім’я), яке буде покликатися на об’єкт з отриманими даними. Тут змінна fetchedData є локальною, на відміну від попереднього прикладу.
4 Викликаємо функцію parseData() з аргументом fetchedData.
5 Оголошення функції parseData(), у тілі якої розбираємо отримані дані та додаємо у масив results.
6 Друк масиву results.

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

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

OpenWeather API

Розглянемо ще одну онлайн-платформу під назвою OpenWeather , яка надає низку API для отримання даних про погоду у будь-якій точці земної кулі.

Безплатний план використання платформи має обмеження в кількості запитів (60 за хвилину / 1 мільйон на місяць) і передбачає надання таких послуг:

  • поточна погода;

  • 3-годинний прогноз на 5 днів;

  • основні мапи погоди;

  • візуальний інструмент (дашборд) для роботи з даними про погоду;

  • Air Pollution API - дані про забруднення повітря;

  • Geocoding API - інструмент для полегшення пошуку місць під час роботи з географічними назвами та координатами.

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

Для безплатного користування послугами OpenWeather API, необхідно виконати декілька кроків, які стосуються реєстрації:

  1. Зареєструватися на сайті.

  2. Підтвердити реєстрацію у листі, який надійде на електронну скриньку, що була використана при реєстрації.

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

В усі наведених нижче прикладах запитів замість назви myAPIKEY використовуйте власний унікальний ключ API.

Використаємо готовий варіант запиту

api.openweathermap.org/data/2.5/weather?q=London,uk&APPID=myAPIKEY

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

У цьому запиті використовується два параметри зі значеннями:

  • q - назва міста та код країни, розділені комою;

  • APPID - унікальний ключ API, який завжди можна знайти у розділі My API keys свого облікового запису у кабінеті на сайті, де ви також можете створити додаткові ключі API.

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

sketch.js
/* jshint esversion: 8 */

function setup() {
  noCanvas();

  const apiHost = "https://api.openweathermap.org/data/2.5/weather"; (1)
  const apiKey = "myAPIKEY"; (2)
  const queryGET = `?q=London,uk&APPID=${apiKey}`; (3)
  const url = apiHost + queryGET; (4)

  async function getWeather() {
    const response = await fetch(url);
    const data = await response.json();
    print(data.name); (5)
    print(data.wind.speed); (6)
    print(data.weather[0].description); (7)
  }
  getWeather();
}

function draw() {}
1 Ініціалізуємо константу з ім’ям apiHost рядком URL-адреси API.
2 Ініціалізуємо константу з ім’ям apiKey рядком, який містить унікальний ключ myAPIKEY.
3 Ініціалізуємо константу з ім’ям queryGET шаблонним рядком, вміст якого починається зі знака питання і містить параметри запиту - назву міста та код країни й унікальний ключ apiKey.
4 Ініціалізуємо константу з ім’ям url рядком, який містить конкатенацію (об’єднання) рядків із пунктів 1 і 3.
5 Отримуємо доступ до назви міста і друкуємо її в консолі вебпереглядача.
6 Отримуємо доступ до значення швидкості вітру у метрах на секунду і друкуємо його в консолі вебпереглядача.
7 Отримуємо доступ до загального опису погодних умов і друкуємо його в консолі вебпереглядача.

У результаті виконання застосунку результати можуть бути такими:

London
9.77
overcast clouds
За потреби виведення результатів можна отримати різними мовами. Наприклад, для української необхідно додати у рядок запиту в параметрі lang=ua код країни ua. Коди країн записуються у форматі ISO 3166 .

Запити за географічними координатами є найточнішим способом вказати будь-яке місцеперебування. Якщо потрібно автоматично перетворити назви міст та поштові індекси в географічні координати та навпаки, використовують Geocoding API .

Наприклад, запит на отримання координат за назвою міста (місто Луцьк, Волинська область, Україна) може бути таким:

https://api.openweathermap.org/geo/1.0/direct?q=Lutsk,ua&APPID=myAPIKEY

А так виглядатиме запит на отримання координат за поштовим індексом (45101, місто Рожище, Волинська область, Україна):

https://api.openweathermap.org/geo/1.0/zip?zip=45101,ua&APPID=myAPIKEY

Використовуючи зворотне геокодування можна отримати назву місця (назву міста або назву області) за допомогою географічних координат lat (широта) і lon (довгота) (Київ, Україна).

http://api.openweathermap.org/geo/1.0/reverse?lat=50.4547&lon=30.5238&APPID=myAPIKEY

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

Насамкінець дізнаємось погодні дані у населеному пункті, що має координатами lat=50.9154 і lon=25.2691.

sketch.js
/* jshint esversion: 8 */

function setup() {
  noCanvas();

  const apiHost = "https://api.openweathermap.org/data/2.5/weather";
  const apiKey = "myAPIKEY";
  const queryGET = `?lat=50.9154&lon=25.2691&units=metric&lang=ua&APPID=${apiKey}`; (1)
  const url = apiHost + queryGET;
  print(url);
  async function getWeather() {
    const response = await fetch(url);
    const data = await response.json();
    print(data.name); (2)
    print(data.main.temp + `\u00B0C`); (3)
    print(data.weather[0].description); (4)
  }
  getWeather();
}

function draw() {}

Проаналізуємо використані у запиті параметри і отримані результати.

1 У рядку запиту, окрім параметрів lat=50.9154, lon=25.2691 і ключа apiKey, використовуються параметр lang=ua для виведення даних у консоль вебпереглядача українською і параметр units=metric, який встановлює для різних значень одиниці вимірювання метричної системи. У такій системі вимірювання величин, наприклад, температура вимірюється у градусах Цельсія, а не у градусах Кельвіна чи Фаренгейта.
2 Друкуємо в консолі вебпереглядача назву data.name населеного пункту.
3 Друкуємо в консолі вебпереглядача значення температури data.main.temp у градусах (шістнадцяткове значення \u00B0 використовується для позначення символу градуса) для населеного пункту.
4 Отримуємо доступ до загального опису погодних умов і друкуємо його в консолі вебпереглядача.

Результатом виконання застосунку буде коротка інформація про погоду у населеному пункті:

Rozhyshche
-1.04°C
хмарно
Перегляньте сторінку документації , щоб знайти всю технічну інформацію із фактичними прикладами та вичерпним описом запитів, відповідей і параметрів.

7.3.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Для чого використовується API?

  2. Наведіть приклади вебсервісів, які мають API.

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

  4. Як інструменти надають бібліотека p5.js і мова програмування JavaScript для роботи з API?

7.3.5. Практичні завдання

Початковий

  1. Створити застосунок, який радить вам чим зайнятися, коли ви нудьгуєте, - пропонує вам назву активності для певної кількості учасників. Нижче наведені приклади активностей для різної кількості учасників та покликання для завантаження даних для застосунку. Для роботи із запитами в адресному рядку вебпереглядача використовуйте сайт Bored API Documentation .

Файл-архів з даними у JSON-форматі можна завантажити за покликанням. Дані для цього завдання отримані зі сховища api-example-bored-api .
Learn about the Golden Ratio
education
1
Compliment someone
social
2
Play basketball with a group of friends
social
5
Bake a pie with some friends
cooking
3
Go to a music festival with some friends
social
4
  1. Перейти на сторінку API Таємниці Марса і виконати в адресному рядку вебпереглядача наведені приклади простих і складених запитів.

Середній

  1. Створити застосунок для отримання даних про загадкові артефакти в області Cydonia Mensae на Марсі, використавши API Таємниці Марса . У консолі вебпереглядача надрукувати інформацію відповідно до наведеної структури.

артефакт №1
таємниця: опис
пояснення: опис
артефакт №2
таємниця: опис
пояснення: опис
...
  1. Створити застосунок, який виконує запит за URL-адресою https://my-json-server.typicode.com/typicode/demo/db і друкує у консоль вебпереглядача дані як у наведеному прикладі.

1: Post 1
2: Post 2
3: Post 3
1: some comment
2: some comment
  1. Створити застосунок, який використовує Launch Library API для отримання даних про назви та деталі космічних місій NASA , про які лише відомо, що вони заплановані на грудень 2030 року.

Високий

  1. Створити застосунок, який отримує дані (номер, ціна, категорія) про усі товари, використовуючи кінцеву точку https://dummyjson.com/products і метод fetch(). У консолі вебпереглядача надрукувати лише ті назви категорій, які містять принаймні один товар з ціною понад 1000 умовних одиниць. Назви категорій не повинні повторюватися.

["smartphones", "laptops", "motorcycle"]
  1. Створити застосунок, який використовує синтаксис async/await та кінцеву точку https://dummyjson.com/quotes для отримання усіх цитат Albert Einstein та їх виведення у консоль вебпереглядача.

Strive not to be a success, but rather to be of value.
A person who never made a mistake never tried anything new.
  1. Використовуючи OpenWeather і Geocoding API , отримати інформацію про погоду на цей момент у вашому населеному пункті.

Екстремальний

  1. Створити застосунок, який використовує кінцеву точку https://jsonplaceholder.typicode.com/users/1/todos для отримання даних про усі незавершені справи у списку справ кожного із 10 користувачів (наведений приклад URL-адреси використовується для першого користувача). Результат необхідно вивести в консоль вебпереглядача відповідно до зразка для двох перших користувачів.

{
  "user1": [
    1,
    2,
    3,
    5,
    6,
    7,
    9,
    13,
    18
  ],
  "user2": [
    21,
    23,
    24,
    28,
    29,
    31,
    32,
    33,
    34,
    37,
    38,
    39
  ]
}

7.4. Дані сенсорів та датчиків

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

Ось лише невелика частина прикладів застосування датчиків і сенсорів:

  • вимірювання за допомогою датчиків параметрів фізичних явищ та перебігу процесів у лабораторних умовах;

  • датчики для моніторингу здоров’я людини, які вимірюють пульс, температуру тіла та інші показники;

  • використання сенсорів для автоматичного управління освітленням та температурою у розумному будинку;

  • автоматизації процесу виробництва тощо.

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

Терміни «датчик» і «сенсор» часто використовуються як синоніми, але загалом, датчик - це пристрій, який вимірює фізичні величини, як-от температура чи прискорення, водночас сенсор - це пристрій, який реагує на конкретний стимул, як-от дотик або світло.

Сьогодні ці пристрої стали важливими джерелами даних і є невіддільною частиною мобільних пристроїв, як-от смартфони, навігатори, планшети тощо.

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

Для наших цілей будемо використовувати:

  • мобільний пристрій - смартфон;

  • вебпереглядач на мобільному пристрої - Google Chrome;

  • середовище запуску ескізів - онлайн-редактор p5.js Web Editor .

7.4.1. Акселерометр і гіроскоп

Розглянемо код застосунку, який виводить на полотні назву орієнтації екрана смартфона на цей момент: portrait (книжкова) або landscape (альбомна).

sketch.js
function setup() {
  createCanvas(200, 200);
  textAlign(CENTER, CENTER);
  textSize(16);
}

function draw() {
  background(220);
  let oriented = deviceOrientation; (1)
  fill(51, 102, 153); // Lapis Lazuli
  text(oriented, width / 2, height / 2); (2)
}
1 Ініціалізуємо змінну з ім’ям oriented значенням вбудованої у бібліотеку p5.js змінної deviceOrientation , яка містить значення орієнтації екрана смартфона. Якщо дані про орієнтацію не можна отримати, змінна deviceOrientation набуває значення undefined.
2 Виводимо у центрі полотна значення oriented.

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

Положення смартфона у просторі регулюється за допомогою таких датчиків:

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

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

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

Для отримання даних акселерометра смартфона використовуються змінні accelerationX , accelerationY , accelerationZ , які містять значення прискорення пристрою вздовж відповідних осей, виміряні у квадратних метрах за секунду.

Напишемо код застосунку, у якому величина прискорення смартфона вздовж осі X відображатиметься як розмір кола на полотні.

sketch.js
function setup() {
  createCanvas(200, 200);
  noStroke();
}

function draw() {
  background(220);
  fill(255, 140, 66); // Pumpkin
  ellipse(width / 2, height / 2, accelerationX * 10); (1)
  print(accelerationX, accelerationY, accelerationY); (2)
}
1 Оскільки величина accelerationX набуває невеликих значень, для відображення суттєвих змін розміру кола використовуємо множення на 10.
2 В консоль вебпереглядача друкуємо значення прискорення за трьома осями.

Тепер, якщо смартфон, що перебуває у горизонтальному положенні, різко повертати вліво-вправо, на полотні спостерігатимемо зміну розміру кола.

Переглядаємо Аналізуємо

Роботу гіроскопа смартфона можна фіксувати, використовуючи змінні rotationX , rotationY , rotationZ , що містять значення кутів обертання смартфона навколо відповідних осей.

sketch.js
function setup() {
  createCanvas(200, 200, WEBGL); (1)
  debugMode(AXES, 200, -75, -75, 0); (2)
}

function draw() {
  background(220);
  orbitControl(); (3)
  rotateY(radians(rotationY)); (4)
  box(75, 75, 75); (5)
}
1 Увімкнення режиму 3D (третім аргументом у виклику функції createCanvas() є значення WEBGL), початок координат розміщується в центрі полотна.
2 Використання функції debugMode() для малювання індикатора осей на полотні, де 200 - довжина осей, -75 - зсув осей X та Y від центру полотна вліво і вгору, 0 - вісь Z починається в центрі полотна.
3 Використання функції orbitControl() , яка дозволяє переміщатися по тривимірному ескізу за допомогою миші або дотику.
4 Використання змінної rotationY (на відміну від rotationX і rotationY, rotationY доступна лише для мобільних пристроїв із вбудованим компасом), яка набуває значення у радіанах від 0 до 2 * PI. Функція rotateY() повертає фігуру на величину, визначену параметром кута rotationY у радіанах.
5 Малювання куба за допомогою функції box() .
Якщо використовуються одночасно усі функції (rotateX, rotateY, rotateZ) для поворотів навколо осей, важливим є порядок їх викликів, а саме, Z-X-Y, щоб уникнути несподіваної поведінки.

За допомогою дотику до екрана можна обертати куб і осі одночасно, а повертання смартфона вправо-вліво буде впливати на обертання лише куба навколо осі Y зеленого кольору (вісь X - червоного кольору, вісь Z - синього кольору).

Переглядаємо Аналізуємо

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

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

Розглянемо код застосунку, який змінюватиме зображення на полотні у разі руху смартфона у просторі.

sketch.js
let c = 0; (1)
let threshold = 0.5; (2)

function setup() {
  createCanvas(200, 200);
  setMoveThreshold(threshold); (3)
}

function draw() {
  background(46, 41, 78); // Space cadet
  stroke(c); (4)
  fill(46, 41, 78);
  strokeWeight(25);
  circle(width / 2, height / 2, width / 2);
}
function deviceMoved() { (5)
  c = c + 10;
  threshold = threshold + 0.1;
  if (c > 255) {
    c = 0;
    threshold = 0.5;
  }
  setMoveThreshold(threshold);
}
1 Ініціалізуємо змінну c значенням 0. Її ім’я буде покликатися на значення кольору, який у процесі руху смартфона буде змінюватися. Початковий колір - чорний.
2 Ініціалізуємо змінну threshold пороговим значенням 0.5, вихід за межі якого буде викликати функцію deviceMoved().
3 Встановлюємо порогове значення threshold за допомогою функції setMoveThreshold().
4 Встановлюємо поточне значення кольору для контуру об’єкта на полотні. Об’єктом є коло, яке має контур, товщина якого встановлена за допомогою функції strokeWeight() у пікселях. Кольори зафарбовування кола і полотна однакові, що утворює на полотні ілюзію зображення кільця.
5 Оголошення функції deviceMoved(), у тілі якої змінюється значення кольору c і порогу threshold. За умови, коли значення c стає понад 255, c і threshold набувають початкових значень. При кожному виклику функції deviceMoved() за допомогою функції setMoveThreshold() встановлюється значення threshold на цей момент.

Переміщуючи смартфон у просторі вздовж осей X, Y або Z можна фіксувати на полотні зміну кольору контуру кола у відтінках сірого кольору.

Переглядаємо Аналізуємо

Коли пристрій повертати більш ніж на 90 градусів, викликається функція deviceTurned() .

Назва осі, яка викликає deviceTurned(), зберігається в змінній під ім’ям turnAxis. Завдяки цьому можна передбачити у коді, яку вісь потрібно брати до уваги, шляхом порівняння значення turnAxis з X, Y або Z. Проілюструємо таку поведінку за допомогою наступного коду.

sketch.js
let c, c1, c2; (1)

function setup() {
  createCanvas(200, 200);
  c1 = color(231, 29, 54); // Red (Pantone)
  c2 = color(27, 153, 139); // Persian green
  c = c1; (2)
  rectMode(CENTER);
  noStroke();
}

function draw() {
  background(220);
  fill(c); (3)
  rect(width / 2, height / 2, 100, 100);
}

function deviceTurned() {
  if (turnAxis === "X") { (4)
    if (c === c1) {
      c = c2;
    } else if (c === c2) {
      c = c1;
    }
  } else {
    print(turnAxis); (5)
  }
}
1 Оголошуємо змінну c, ім’я якої буде покликатися на значення кольору на цей момент, та змінні c1 і c2, що матимуть сталі значення кольорів відповідно.
2 Встановлюємо значення c1 для c як значення кольору на цей момент.
3 Використовуємо значення поточного кольору c для зафарбовування квадрата на полотні.
4 У тілі функції deviceTurned() перевіряємо, чи пристрій здійснює обертання навколо осі X (обертання від користувача і до нього) більш ніж на 90 градусів. Якщо так, то поточний колір c набуває почергово значень c1 і c2.
5 Якщо ж обертання пристрою відбувається навколо інших осей, назви цих осей друкуються в консолі вебпереглядача.

Отож, обертаючи смартфон навколо осі X (від себе чи до себе) на понад 90 градусів, можна спостерігати, як квадрат змінює колір.

Переглядаємо Аналізуємо

Розглянемо ще одну функцію з ім’ям deviceShaken() , яка викликається, коли зміни загального прискорення пристрою для значень accelerationX і accelerationY перевищують порогове значення. Інакше, коли пристрій струшують.

Стандартним налаштуванням порогового значення є 30, втім це значення можна змінити за допомогою функції setShakeThreshold() .

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

sketch.js
let a = 255; (1)
let threshold = 30;

function setup() {
  createCanvas(200, 200);
  setShakeThreshold(threshold);
  noStroke();
}

function draw() {
  background(220);
  fill(color(68, 169, 94, a)); // Pigment green
  circle(width / 2, height / 2, 100);
}
function deviceShaken() {
  a = a - 1;
  threshold = threshold - 1;
  if (a < 0) { (2)
    a = 255;
    threshold = 30;
  }
  setShakeThreshold(threshold);
}
1 Ініціалізуємо змінну a зі значенням 255, чиє ім’я буде покликанням на значення прозорості на цей момент. Початкове значення (255) - повністю непрозорий.
2 У тілі функції deviceShaken() зменшуємо значення прозорості при кожному струшуванні пристрою і перевіряємо, чи стало воно меншим нуля. Якщо так, повертаємо початкове значення для a.

Переглядаємо Аналізуємо

Вправа 83

Змінити код застосунку, щоб при струшуванні смартфона полотно зафарбовувалось випадковим кольором.

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

  • датчик наближення - визначає факт наближення до смартфона будь-яких об’єктів, наприклад, у разі наближення смартфона до вуха під час розмови, вимикає підсвічування екрана;

  • датчик освітлення - автоматично регулює яскравість екрана, роблячи роботу із пристроєм комфортнішою, у такий спосіб економиться споживана енергія;

  • компас - відстежує орієнтацію пристрою у просторі відносно магнітних полюсів Землі.

7.4.2. Сенсорний екран

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

Для отримання даних про позиції усіх точок дотику використовується системна змінна touches .

touches є масивом, кожен елемент якого є об’єктом із властивостями id (ідентифікатор дотику), x та y (координати точки дотику відносно початку системи координат полотна, який за стандартним налаштуванням розташований у верхньому лівому куті полотна у точці (0, 0)).

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

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  let numberTouches = touches.length + " дотиків"; (1)
  text(numberTouches, 10, 20); (2)
  for (let i = 0; i < touches.length; i++) { (3)
    print(touches[i]);
  }
}
1 Ініціалізуємо змінну numberTouches значенням кількості елементів масиву touches.
2 Відображаємо значення numberTouches на полотні.
3 У циклі for проходимо вмістом масиву touches і друкуємо його елементи (об’єкти) в консолі вебпереглядача.

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

Переглядаємо Аналізуємо

Один раз після кожного дотику викликається функція touchStarted() . Якщо функція touchStarted() не оголошена у коді, замість неї буде викликана функція mousePressed() (викликається один раз після кожного натискання кнопки миші), якщо вона оголошена.

Щоразу, коли дотик завершується, викликається інша функція - touchEnded() . Якщо функція touchEnded() не оголошена у коді, замість неї буде викликана функція mouseReleased() (викликається щоразу, коли відпускається кнопка миші), якщо вона оголошена.

Вебпереглядачі за стандартним налаштуванням можуть мати різну поведінку, пов’язану з різними подіями дотику. Щоб запобігти будь-якій поведінці за стандартним налаштуванням для цієї події, додайте return false у кінець тіла функцій для роботи із сенсорним екраном.

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

sketch.js
function setup() {
  createCanvas(200, 200);
  background(220);
  noStroke();
}

function draw() {
  fill(188, 182, 255); // Periwinkle
}

function touchStarted() { (1)
  createCircle(100);
  return false;
}

function touchEnded() { (2)
  createCircle(50);
  return false;
}

function createCircle(d) { (3)
  background(220);
  ellipse(width / 2, height / 2, d);
}
1 Оголошення функції touchStarted(), яка буде викликатися щоразу після дотику до екрана смартфона. У її тілі буде викликатися користувацька функція createCircle(), яка як аргумент отримуватиме значення діаметра кола.
2 Оголошення функції touchEnded(), яка буде викликатися щоразу після завершення дотику до екрана смартфона. У її тілі також буде викликатися користувацька функція createCircle(), яка як аргумент отримуватиме значення діаметра кола, меншого у два рази.
3 Оголошення користувацької функції createCircle() для малювання кола.

Щоб створити ефект збільшення розміру кола за дотиком і повернення його до початкового розміру, у тілах функцій createCircle() і setup() використовується зафарбовування тла полотна однаковим (сірим) кольором.

Переглядаємо Аналізуємо

Функція touchMoved() - ще одна функція, яку можна використовувати для взаємодії із сенсорним екраном пристрою.

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

Розглянемо приклад застосування функції touchMoved(), створивши просту версію графічного редактора для малювання ліній на полотні за допомогою дотику.

sketch.js
function setup() {
  createCanvas(200, 200);
  background(239, 216, 29); // Citrine
  stroke(0);
  strokeWeight(5);
}

function draw() {}

function touchMoved() {
  line(mouseX, mouseY, pmouseX, pmouseY);
  return false;
}

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

Переглядаємо Аналізуємо

Вправа 84

Змінити код застосунку для малювання дотиком ліній у формі кіл.

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

7.4.4. Контрольні запитання

Міркуємо Обговорюємо

  1. Що таке «акселерометр»?

  2. Які основні функції гіроскопа і які дані він надає?

  3. В чому відмінність між «датчиком» та «сенсором»?

7.4.5. Практичні завдання

Початковий

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

Середній

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

Орієнтації екрана смартфона
Орієнтації екрана смартфона
  1. Створити застосунок, який реагує на повертання смартфона у просторі. Якщо обертання відбувається навколо осі X, у випадкових точках на полотні малюються кола малого діаметра, якщо обертання відбувається навколо осі Y - великого. У колах записуються назви відповідних осей. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Високий

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

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

Екстремальний

  1. Створити застосунок, в якому вгорі полотна у випадковій точці з’являється кулька і відразу починає рухатися вертикально вниз. Її можна піймати дотиком, утримувати та переміщувати горизонтально. Кульку необхідно скерувати на платформу, що з’являється внизу полотна. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, в якому користувач, роблячи нахили смартфона керує кулькою на полотні. Кулька має зібрати на полотні інші кульки меншого діаметра. Орієнтовний взірець роботи застосунку представлений в демонстрації.

7.5. Візуалізація даних, отриманих із зовнішніх джерел

В сучасному світі збір та обробка даних є невіддільною частиною багатьох програмних продуктів. Щоб наочно представити отримані дані, розробники використовують різноманітні інструменти для візуалізації.

Візуалізація даних - це процес перетворення числової інформації в графічні або візуальні форми, які допомагають розуміти, аналізувати та знаходити зв’язки у даних. Це дозволяє людям швидше і легше сприймати складну інформацію, яку було б важко розгледіти у вигляді сухих цифр.

Інтерфейс програмного продукту тісно пов’язаний з візуалізацією даних, оскільки через інтерфейс користувач взаємодіє з даними та може їх сприймати та аналізувати.

Сучасні програмні продукти надають інтерактивні можливості для візуалізації даних у вигляді графіків, діаграм, мап. Ці можливості включають масштабування вигляду графіків, перетягування об’єктів на мапі, фільтрацію даних за певними параметрами в реальному часі, вибір діапазонів даних тощо на будь-яких пристроях.

Бібліотека p5.js дозволяє легко візуалізувати дані із різних джерел, роблячи їх доступними для розуміння та аналізу. Розглянемо, як можна використовувати p5.js для візуалізації даних, отриманих із зовнішніх джерел, як-от API, файли форматів TXT, CSV та JSON.

7.5.1. Файли

Збережемо декілька афоризмів відомого польського письменника-сатирика XX століття Станіслава Єжи Леца у файлі quotes.txt (формат для зберігання звичайного тексту) і візуалізуємо його текстовий вміст.

Обчислимо кількість слів у кожному рядку тексту, використовуючи пропуск як розділювач, і намалюємо на полотні низку кіл відповідного діаметру.

sketch.js
let quotes; (1)

function preload() {
  quotes = loadStrings("quotes.txt"); (2)
}

function setup() {
  createCanvas(200, 200);
  noStroke();
  textAlign(CENTER, CENTER);
  background(96, 77, 83); // Wenge

  for (let i = 0; i < quotes.length; i++) {
    let words = quotes[i].split(" "); (3)
    let d = words.length; (4)
    fill(255, 219, 218, 128); // Misty rose
    ellipse(i * 35 + 25, height / 2, d * 5, d * 5); (5)
    fill(255);
    text(d, i * 35 + 25, height / 2); (6)
    print(`${i + 1}. ${quotes[i]}`); (7)
  }
}

function draw() {}

Проаналізуємо наведений код.

1 Оголошуємо змінну з ім’ям quotes, що буде покликатися на масив, елементами якого будуть текстові рядки афоризмів.
2 Використовуємо функцію loadStrings() , яка прочитає вміст файлу quotes.txt, для заповнення масиву quotes окремими рядками файлу. На місці імені файлу може бути URL-адреса для завантаження даних. Оскільки функція є асинхронною, тобто її виклик може не завершитися до виконання наступного рядка ескізу, тому використовуємо функцію preload().
3 Проходимо у циклі for по масиву quotes і на кожній ітерації ініціалізуємо змінну words масивом слів кожного афоризму quotes[i]. Масив слів створюється за допомогою метода split() , який використовує як розділювач символ пропуску.
4 Обчислюємо кількість слів d у поточному афоризмі, обчисливши довжину масиву words за допомогою метода length() .
5 Малюємо на полотні коло, діаметр якого має значення d * 5, де d - кількість слів у поточному афоризмі. Оскільки кількість слів у афоризмі є невеликою, число 5 застосовується як множник для додаткового збільшення розміру для усіх кіл.
6 В центрі кола відображаємо значення кількості слів у афоризмі.
7 Використовуючи шаблонний рядок, друкуємо в консолі вебпереглядача текст афоризму. Нумерація афоризмів починається з 1.
Кількість слів в тексті афоризмів
Кількість слів в тексті афоризмів

Окрім аналізу зображення на полотні, у консолі вебпереглядача можна прочитати самі афоризми.

1. Щоб дістатися джерела, треба пливти проти течії.
2. Є люди, котрі полюбляють поразки, бо їх тоді запрошують на бенкети переможців.
3. Болото створює часом враження глибини.
4. Багато речей так і не з’явились через неможливість їхнього найменування.
5. Коли народ позбавлений голосу, це можна помітити навіть при співанні гімнів.

Дуже часто дані візуалізують за допомогою різноманітних графіків та діаграм. Отже, створимо стовпчикову діаграму (гістограму) популяції птахів на основі даних з файлу birds.csv. Напишемо і проаналізуємо код застосунку для побудови гістограми.

sketch.js
let birds; (1)

function preload() {
  birds = loadTable("birds.csv", "csv", "header"); (2)
}

function setup() {
  createCanvas(400, 200);
}

function draw() {
  background(220);
  let x = 15; (3)
  let y = height / 2 + height / 3;
  let barWidth = 45;

  for (let i = 0; i < birds.getRowCount(); i++) { (4)
    let birdPopulation = birds.getNum(i, "кількість");
    let barHeight = map(birdPopulation, 100000, 1500000, 50, 150);
    let birdName = birds.getString(i, "назва");
    let birdColor = birds.getString(i, "колір");

    fill(getColor(birdColor)); (5)
    rect(x, y - barHeight, barWidth, barHeight); (6)
    fill(0);
    textAlign(CENTER, TOP);
    text(birdName, x + barWidth / 2, y + 10); (7)

    x += barWidth + 20; (8)
  }
}

function getColor(birdColor) { (9)
  switch (birdColor) {
    case "Рожевий":
      return color(229, 134, 123); // Coral pink
    case "Сірий":
      return color(128); // Gray
    case "Білий":
      return color(255); // White
    case "Коричневий":
      return color(154, 80, 27); // Brown
    default:
      return color(0); // Black
  }
}
1 Оголошуємо змінну з ім’ям birds, що буде покликатися на об’єкт завантажених даних.
2 Використовуємо функцію loadTable() для завантаження даних з CSV-файлу.
3 Ініціалізуємо змінні із початковими даними щодо розміщення стовпчиків діаграми, а саме: x - відступ першого стовпця діаграми від лівої межі полотна по горизонталі, y - відступ від верхньої межі до основи діаграми по вертикалі, barWidth - ширина стовпчиків діаграми.
4 Отримання при проході у циклі for доступу до значень табличних даних, які містяться у двох масивах з назвами columns (стовпці) і rows (рядки) відповідно: birdPopulation - значення на перетині стовпця з назвою "кількість" та i-го рядка, barHeight - формування висоти стовпця, використовуючи функцію map() , що змінює поточний діапазон значень (від 100000 до 1500000) на новий діапазон значень (від 50 до 150) для birdPopulation, birdName - значення на перетині стовпця з назвою "назва" та i-го рядка, birdColor - значення на перетині стовпця з назвою "колір" та i-го рядка.
5 Отримання значення кольору для стовпця даних на діаграмі із користувацької функції getColor(), яка викликається з аргументом birdColor.
6 Малювання стовпця гістограми у вигляді прямокутника. За стандартним налаштуванням у функції rect() перші два параметри (x, y - barHeight) встановлюють розташування верхнього лівого кута прямокутника, а третій (barWidth) і четвертий (barHeight) - ширину і висоту фігури відповідно.
7 Відображення під основою гістограми назви птаха birdName.
8 Збільшення відступу від лівої межі полотна по горизонталі для наступного стовпця діаграми на значення barWidth + 20.
9 Оголошення користувацької функції getColor(), яка повертає значення кольору за назвою birdColor, яка передається їй як аргумент при її виклику. У тілі функції використовується конструкція розгалуження switch .

Результатом виконання застосунку буде стовпчикова діаграма з даними про популяцію птахів. Проаналізувавши гістограму можна легко зробити висновок, що найменша популяція серед птахів у представників фламінго.

Гістограма популяції птахів
Гістограма популяції птахів

Розглянемо як джерело даних для візуалізації вміст JSON-файлу cities.json про міста світу.

Використовуючи географічні координати міст, візуалізуємо точки розташування міст на мапі світу, зображення якої можна безплатно завантажити на сайті GISGeography або завантажити локально.

Запишемо код застосунку, який намалює напівпрозорі різноколірні кола на карті у тих точках, де розташовані міста. Діаметри кіл відповідатимуть значенням кількості населення у містах.

sketch.js
let cities, world; (1)
let finished = false; (2)

function preload() {
  cities = loadJSON("cities.json"); (3)
  world = loadImage("World-Map-Latitude-Longitudes.jpg"); (4)
}

function setup() {
  createCanvas(windowWidth, windowHeight);
}

function draw() {
  if (cities && world && !finished) { (5)
    image(world, 0, 0, width, height);
    drawCities();
    finished = true;
  }
}

function drawCities() { (6)
  for (let i = 0; i < cities.cities.length; i++) {
    let city = cities.cities[i];
    let population = city.population; (7)
    let d = map(population, 0, 3562000, 10, 50); (8)

    // перетворення координат на мапі до координат на полотні
    let x = map(city.coordinates.longitude, -180, 180, 0, width); (9)
    let y = map(city.coordinates.latitude, 90, -90, 0, height);

    // відображення кола на мапі
    noStroke();
    fill(random(255), random(255), random(255), 128);
    ellipse(x - 3, y - 27, d, d); (10)
  }
}
1 Оголошуємо змінні cities і world, імена яких будуть покликаннями на об’єкти завантажених даних із файлу cities.json і завантаженого зображення World-Map-Latitude-Longitudes.jpg відповідно.
2 Ініціалізуємо змінну finished значенням false, яка визначатиме завершення процесу завантаження як даних, так і зображення.
3 Завантаження в ескіз даних з файлу cities.json.
4 Завантаження в ескіз зображення з файлу World-Map-Latitude-Longitudes.jpg.
5 У блоці draw() виконуємо перевірку виразу cities && world && !finished на істинність. Якщо вираз істинний, розміщуємо завантажене зображення на полотні за допомогою виклику функції image() , перший аргумент якої - це об’єкт world, що містить завантажене зображення. Також, у разі істинності виразу, змінюємо значення finished на true, що сигналізує, усі дані, які мали бути завантажені в ескіз, вже завантажені та викликаємо користувацьку функцію drawCities().
6 Оголошення користувацької функції drawCities(). У тілі функції в циклі for отримуємо доступ до кожного міста cities.cities[i] масиву міст cities.cities та ініціалізуємо змінну з ім’ям city значенням об’єкта конкретного міста.
7 Отримуємо з об’єкта city значення властивості city.population під ім’ям population.
8 Для population змінюємо поточний діапазон значень (від 0 до 3562000) на новий діапазон значень (від 10 до 50).
9 Змінюємо діапазони значень для географічних координат city.coordinates.longitude та city.coordinates.latitude на діапазони для координат x та y на полотні.
10 Малюємо напівпрозоре кольорове коло діаметра d у координатах x та y із врахуванням певного зміщення, яке підбираємо вручну.

У результаті виконання застосунку отримаємо варіант бульбашкової діаграми.

Бульбашкова діаграма міст світу
Бульбашкова діаграма міст світу

7.5.2. API

Розглянемо кілька зовнішніх API для отримання даних та їх візуалізації.

Використаємо простий Dog API для отримання випадкового фото собаки.

sketch.js
let data, breed, breedImage; (1)

function preload() {
  data = loadJSON("https://dog.ceo/api/breeds/list/all"); (2)
}

function setup() {
  noCanvas();

  let breeds = Object.keys(data.message); (3)
  breed = random(breeds); (4)

  const urlDog = `https://dog.ceo/api/breed/${breed}/images/random`; (5)
  async function getBreed() { (6)
    const responseDog = await fetch(urlDog);
    const dataDog = await responseDog.json();
    return dataDog;
  }

  getBreed().then((b) => { (7)
    breedImage = b.message;
    let img = createImg(breedImage, breed);
    img.attribute("width", "200px");
    img.attribute("title", breed);
    img.position(0, 0);
  });
}

function draw() {}
1 Оголошуємо змінні з іменами data (об’єкт з отриманими даними про усі породи собак) , breed (назва породи) і breedImage (покликання на зображення собаки).
2 Завантажуємо дані про породи собак.
3 Отримуємо усі назви властивостей об’єкта data.message, інакше - усі назви порід собак.
4 Випадково обираємо назву однієї породи.
5 Створюємо запит відповідно до обраної назви породи для отримання випадкового зображення собаки саме цієї породи.
6 Запит виконується у тілі асинхронної функції getBreed().
7 Обробляємо відповідь на запит, яку повертає функція getBreed(). Отримуємо покликання breedImage на фото собаки та створюємо елемент <img> у DOM вебсторінки ескізу за допомогою функції createImg() , в яку як аргументи передаємо значення URL-адреси зображення собаки (breedImage) та назву її породи для атрибута alt (breed). За допомогою attribute() встановлюємо ширину зображення 200 пікселів і додаємо ще один атрибут title зі значенням назви породи собаки. Використовуючи position() розміщуємо зображення у верхньому лівому куті вікна перегляду.

Тепер, при наведенні вказівника миші на випадковому зображенні собаки, над ним буде з’являтися назва її породи.

Переглядаємо Аналізуємо

Використаємо сервіси OpenWeather і Geocoding API для отримання метеорологічних значень швидкості та поривів вітру у м/с в певному населеному пункті, а далі застосуємо їх для створення анімації.

sketch.js
/* jshint esversion: 8 */

let weatherData, weather; (1)
let completed = false; (2)
let x = 0; (3)

function setup() {
  createCanvas(200, 200);
  noStroke();
  rectMode(CENTER);

  const coords =
    "https://api.openweathermap.org/geo/1.0/zip?zip=01001,ua&APPID=myAPIKEY"; (4)

  async function getWeather() { (5)
    const responseCoords = await fetch(coords);
    const dataCoords = await responseCoords.json();
    const [lat, lon] = [dataCoords.lat, dataCoords.lon];

    const apiHost = "https://api.openweathermap.org/data/2.5/weather";
    const apiKey = "myAPIKEY";
    const queryGET = `?lat=${lat}&lon=${lon}&units=metric&lang=ua&APPID=${apiKey}`;
    const url = apiHost + queryGET;

    const responseWeather = await fetch(url);
    const dataWeather = await responseWeather.json();

    return dataWeather;
  }
  weatherData = getWeather(); (6)
  weatherData.then((wd) => {
    weather = wd;
    completed = true;
  });
}

function draw() {
  background(220);
  if (completed) { (7)
    fill(104, 48, 239); // Electric indigo
    rect(x, height / 2, weather.wind.speed * 4); // швидкість вітру
    fill(255, 104, 62, 128); // Tomato
    circle(x, height / 2, weather.wind.gust * 4); // пориви вітру
    x = x + weather.wind.speed / 10 + weather.wind.gust / 10;
    if (x > width) {
      x = 0;
    }
  }
}
1 Оголошуємо змінні з іменами weatherData і weather. Ім’я weatherData буде покликатися на результат роботи асинхронної функції getWeather(), а weather - на об’єкт з отриманими за запитом даними.
2 Ініціалізуємо змінну completed значенням false, яка визначатиме завершення запиту на отримання даних.
3 Ініціалізуємо змінну x значенням 0. Це початкове значення x-координати фігур, які будуть рухатися на полотні. Значення x у процесі руху буде змінюватися.
4 Рядок запиту на отримання географічних координат населеного пункту, який має індекс 01001 і розташований в Україні (ua). myAPIKEY - ваш ключ API.
5 Оголошення асинхронної функції getWeather(), яка повертає результат запиту на отримання погодних даних за координатами, взятими із пункту 4.
6 Ім’ям weather буде покликанням на отримані погодні дані з асинхронної функції getWeather(). Змінна completed набуває значення true.
7 Якщо змінна completed має значення true, малюємо на полотні квадрат і коло. Перевіряємо значення x-координати, яке змінюється при виконанні застосунку, чи воно більше за праву межу полотна width, якщо так - присвоюємо x початкове значення 0. Для повільнішого темпу анімації значення weather.wind.speed і weather.wind.gust ділимо на 10, а для кращої візуалізації - множимо на 10 при малюванні фігур на полотні. Ці коефіцієнти підбираємо залежно від отриманих значень weather.wind.speed і weather.wind.gust.

Розміри квадрата визначаються даними про швидкість вітру у м/с (weather.wind.speed), розміри кола - даними про пориви вітру у м/с (weather.wind.gust). Аналізуючи розміри цих двох фігур і швидкість анімації можна зробити висновки про погодні вітрові умови в обраному населеному пункті.

Переглядаємо Аналізуємо

7.5.3. Датчики й сенсори

У попередньому розділі вже створювалися застосунки, які отримують дані за допомогою датчиків і сенсорів смартфона. Наведемо ще один приклад такої взаємодії, візуалізуючи дані зі смартфона за допомогою анімації руху двох об’єктів.

sketch.js
let x = 0, y = 0; (1)

function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  let aX = accelerationX; (2)
  let aY = accelerationY; (3)
  x += aX; (4)
  y += aY; (5)
  fill(245, 238, 158); // Vanilla
  ellipse(x, height / 2, 50); (6)
  fill(36, 110, 185); // Azul
  ellipse(width / 2, y, 50); (7)
}
1 Ініціалізуємо змінні з іменами x та y початковими нульовими значеннями. Імена цих змінних будуть покликатися на поточні значення координат об’єктів на полотні.
2 Ініціалізуємо змінну aX значенням accelerationX прискорення пристрою вздовж осі X.
3 Ініціалізуємо змінну aY значенням accelerationY прискорення пристрою вздовж осі Y.
4 Збільшуємо значення x-координати на величину aX.
5 Збільшуємо значення y-координати на величину aY.
6 Малюємо на полотні коло, яке буде рухатися горизонтально у центрі полотна.
7 Малюємо на полотні коло, яке буде рухатися вертикально у центрі полотна.

Тепер, керуючи нахилом смартфона, об’єктами кіл можна керувати.

Переглядаємо Аналізуємо

7.5.5. Контрольні запитання

Міркуємо Обговорюємо

  1. Які зовнішні джерела можна використовувати для отримання даних та їх візуалізації за допомогою p5.js?

  2. Як здійснити візуалізацію даних, отриманих зовнішнім API, у застосунку на основі p5.js?

  3. Поміркуйте, які труднощі можуть виникати при отриманні та візуалізації даних за допомогою бібліотеки p5.js? Які шляхи їх подолання?

7.5.6. Практичні завдання

Початковий

  1. Створити застосунок, який отримує числові дані з файлу data.txt і візуалізує їх у вигляді вертикальних ліній відповідної довжини. Орієнтовний взірець результату візуалізації представлений на малюнку.

Проста гістограма
Проста гістограма
  1. Створити застосунок, який отримує дані з файлу color.csv і візуалізує їх. Орієнтовний зразок результату візуалізації представлений на малюнку.

Стовпчикова діаграма складових кольору Avocado
Стовпчикова діаграма складових кольору Avocado
  1. Використовуючи TheCatAPI та кінцеву точку https://api.thecatapi.com/v1/images/search отримати дані для розміщення на полотні випадкового фото кота. Орієнтовний зразок результату візуалізації представлений на малюнку.

Кіт
Кіт

Середній

  1. Створити застосунок, який використовуючи кінцеву точку https://api.github.com/users візуалізує на полотні випадкові імена розробників. Орієнтовний зразок роботи застосунку представлений в демонстрації.

  1. Створити застосунок, який візуалізує коливання частинки, використовуючи функцію sin() . Коливання описуються рівнянням y = sin(x * v) * d + height / 2, де v - значення, що впливає на швидкість коливання, і d - відхилення вгору/вниз на полотні отримуються з файлу f.txt. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, який отримує дані із датчиків та сенсорів смартфона та використовує їх для візуалізації точок на полотні, координати яких визначаються нахилами пристрою. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Високий

  1. Створити застосунок, який отримує дані про стан хмарності та вологості у населеному пункті за допомогою OpenWeather і Geocoding API та візуалізує їх. Орієнтовний взірець результату візуалізації представлений на малюнку.

Візуалізація даних про хмарність та вологість у населеному пункті
Візуалізація даних про хмарність та вологість у населеному пункті
  1. Створити застосунок, який наносить на карту світу назви морів та візуалізує значення максимальної глибини морів. Орієнтовний взірець результату візуалізації представлений на малюнку.

Зображення мапи світу можна безплатно завантажити на сайті GISGeography або завантажити локально.
Візуалізація максимальної глибини морів
Візуалізація максимальної глибини морів (відкрити зображення у новій вкладці для повного перегляду)

Екстремальний

  1. Створити застосунок, до якого приєднати інтерактивну мапу за допомогою бібліотеки Leaflet та візуалізувати рух Міжнародної космічної станції в реальному часі. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Завантажити файл із зображенням МКС можна за покликанням.
Для ознайомлення з бібліотекою Leaflet скористайтеся покроковим підручником або перегляньте навчальне відео .

7.6. Поняття та приклади інтерактивних інсталяцій

Інтерактивні інсталяції - це художні або технічні конструкції, які дозволяють взаємодіяти із глядачами або іншими системами в реальному часі. Тобто користувач може активно впливати на об’єкт чи систему, а не просто спостерігати чи отримувати інформацію.

Інтерактивність (від англ. Interaction - взаємодія) може стосуватися не лише фізичної взаємодії, а й взаємодії з електронними та програмними системами, що робить цей процес більш динамічним і захопливим для користувача.

Інсталяція може охоплювати різні елементи, такі як скульптура, малюнки, світлові та звукові ефекти, а також інші художні чи технічні компоненти. Цей термін також використовується для позначення самого процесу створення та розташування такого об’єкта в певному просторі.

Інтерактивні інсталяції використовують різні технології та засоби, як-от сенсори, кінетику (дослідження явищ і процесів, що змінюються з часом, наприклад, рух тіл у просторі), проєкції, голосові команди тощо. Створення інтерактивних інсталяцій може містити програмування застосунків для обробки введених даних від сенсорів та інших джерел даних.

Інтерактивні інсталяції застосовуються у різних сферах, включаючи мистецтво, науку, освіту, рекламу, розваги та часто встановлюються в галереях мистецтва, музеях, виставкових залах, публічних просторах.

Вони можуть використовуватися для підвищення емоційного зв’язку аудиторії з твором мистецтва, для навчання та розваг, бути платформою для висловлення та дослідження ідей або концепцій через взаємодію та сприйняття.

Захопливим прикладом інтерактивної платформи для проведення практичних музичних експериментів є сервіс Chrome Music Lab , створений Google. Сервіс є музичною лабораторією, що містить бібліотеку інтерактивних інструментів, які можна спробувати відразу у вікні вебпереглядача.

Розгляньмо деякі приклади інтерактивних інсталяцій, які вражають не лише своєю естетикою, креативністю та технологічними рішеннями, але і підкреслюють важливість взаємодії мистецтва та технологій.

7.6.1. Дощова кімната

Інтерактивна інсталяція Rain Room (Дощова кімната) від Random International спричинила справжній фурор у світі мистецтва.

Інсталяція використовує систему камер та датчиків для відстежування рухів глядача, дозволяючи йому буквально керувати дощем, який зупиняється у тому місці простору, де фіксується присутність людини.

Rain Room
Студія Random International: Rain Room (Дощова кімната) (зображення з покликанням)

Це створює унікальне середовище, в якому людські відносини один з одним і з природою все більше переплітаються із технологіями.

Інші роботи студії Random International можна переглянути тут .

7.6.2. Басейн

Інтерактивна інсталяція The Pool (Басейн) від Jen Lewin , всесвітньо визнаної художниці-інженерки з Нью-Йорку.

The Pool - це гігантське поле концентричних кіл, створене за допомогою світлодіодних сенсорів, які світяться, коли їх активують дотиком.

The Pool
Jen Lewin: The Pool (Басейн) (зображення з покликанням)

Активуючи кола, відвідувачі створюють своєрідний басейн зі світла.

Інші роботи Jen Lewin можна переглянути тут .

7.6.3. Абстрактні ландшафти

Інтерактивна інсталяція XYZT: Abstract Landscapes (Абстрактні ландшафти) від The company Adrien M & Claire B поєднує рух і світло для створення абстрактних ландшафтів.

XYZT: Abstract Landscape
The company Adrien M & Claire B: Abstract Landscape (Абстрактні ландшафти) (зображення з покликанням)

Експозиційна доріжка інсталяції містить кілька інсталяцій, призначених для фізичного залучення аудиторії, яка може створювати світлові абстрактні поверхні за допомогою рухів.

XYZT: Abstract Landscape
The company Adrien M & Claire B: Abstract Landscape (Абстрактні ландшафти) (зображення з покликанням)

Зображення створюються в режимі реального часу.

Інші роботи The company Adrien M & Claire B можна переглянути тут .

7.6.4. Занурення

Інтерактивна інсталяція Submergence (Занурення) від Squidsoup використовує багато тисяч світлодіодних підвішених джерел світла, щоб створити відчуття присутності та руху у фізичному просторі.

Submergence
Squidsoup: Submergence (Занурення) (зображення з покликанням)

Джерела світла реагують на рухи глядачів і створюють неповторні світлові пейзажі.

Інші роботи Squidsoup можна переглянути тут .

Вищенаведені інтерактивні інсталяції спроєктовані для активної взаємодії з глядачами (реагують на рухи, дотик тощо) і мають свої технічні особливості, які визначають їхній рівень складності. Окрім того, інсталяції орієнтовані на певні теми та концепції, такі як природа, абстракція та інші й намагаються викликати емоції або думки.

Усі ці відмінності та спільні риси роблять кожну інтерактивну інсталяцію унікальною, представляючи різноманітність в сучасному мистецтві.

Вправа 85

Запропонувати власну ідею інтерактивної інсталяції. З’ясувати, які засоби та технології потрібно використати для реалізації задуму.

Як бачимо, поєднання технології та креативності відкривають нові можливості для взаємодії глядачів із творами мистецтва, перетворюючи спостерігання в експеримент та відчуття, а інтерактивні інсталяції стають дивовижним зразком того, як сучасне мистецтво вплітається в цифровий світ.

7.6.6. Контрольні запитання

Міркуємо Обговорюємо

  1. Що називають «інтерактивною інсталяцією»? Наведіть приклади.

  2. Які технології та засоби використовуються для створення інтерактивних інсталяцій?

  3. Пригадайте, які можливості для творців, які бажають поєднати програмування та мистецтво в єдиному творі, надає бібліотека p5.js?

7.6.7. Практичні завдання

Початковий

  1. Створити застосунок, в якому коло завжди слідує за вказівником миші. Орієнтовний зразок роботи застосунку представлений в демонстрації.

Середній

  1. Намалювати коло у центрі полотна. При натиканні будь-якої кнопки миші на полотні поза фігурою випадково змінюється розмір фігури. Натискання кнопкою миші в межах кола не викликає зміну розмірів фігури. Якщо вказівник миші розташовується в межах кола, то коло зафарбовується. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, в якому за рух вказівника миші змінює колір тла полотна. Орієнтовний зразок роботи застосунку представлений в демонстрації.

Високий

  1. Створити застосунок, в якому створюється гравець, який позначений кольоровою цяткою, та його клон. За допомогою клавіатури можна керувати рухом гравця. Водночас клон також повторює рухи гравця, він розташовується недалеко від гравця і може бути іншого кольору. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, який візуалізує рух великої кількості частинок в межах полотна. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, в якому обертається площина, з розміщеними на ній різноколірними сферами довільних розмірів. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Екстремальний

  1. Створити застосунок, який імітує вертикальний рух м’ячів під дією сили тяжіння та їх зупинку після кількох відбивань від поверхні. М’ячі падають з різної висоти. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, в якому м’яч кидають горизонтально з певної висоти. Він рухається горизонтально, відбиваючись від поверхні, до повної зупинки. Орієнтовний взірець роботи застосунку представлений в демонстрації.

  1. Створити застосунок, що імітує дощ. Орієнтовний взірець роботи застосунку представлений в демонстрації.

Завантажити файл зі звуком дощу можна за покликанням.

8. Проєктна діяльність

У цьому розділі представлені теми та демонстрації проєктів, які можна використати як ідеї для власних проєктів.

8.1. Виконання індивідуальних та колективних проєктів

Середній

8.1.1. Проєкт #1 з образотворчого мистецтва "Графічний редактор"

Розробити простий інтерфейс графічного редактора з базовим функціоналом. Орієнтовний зразок роботи застосунку представлений у демонстрації.

Високий

8.1.2. Проєкт #2 з технологій "Кавова машина"

Створити симуляцію процесу приготування кави за допомогою кавової машини. Орієнтовний зразок роботи застосунку представлений у демонстрації.

8.1.3. Проєкт #3 з алгоритмізації "Візуалізація алгоритму"

Створити візуалізацію алгоритму сортування бульбашкою чи будь-якого іншого алгоритму. Орієнтовний взірець роботи застосунку представлений у демонстрації.

8.1.4. Проєкт #4 з астрономії "Сонячна система"

Створити модель Сонячної системи, яка відтворює планетарні рухи навколо Сонця. Орієнтовний взірець роботи застосунку представлений у демонстрації.

Завантажити файл із зображенням зоряного неба можна за покликанням.

Екстремальний

8.1.5. Проєкт #5 з біології "Клітини"

Створити симуляцію руху і взаємодії клітин між собою, реакції на зовнішні фактори (наприклад, вказівник миші) тощо. Орієнтовний взірець роботи застосунку представлений у демонстрації.

У цьому проєкті для опису положення, швидкості об’єктів у двовимірному просторі використовується клас p5.Vector . Ознайомитись із поняттям вектора можна у розділі про вектори книги Деніела Шиффмана «The Nature of Code».

8.1.6. Проєкт #6 "Розробка гри"

Розробити комп’ютерну гру. У демонстрації представлена аркадна гра-шутер з назвою Geometric Chase. Мета гри полягає в тому, щоб керувати об’єктом у формі квадрата в просторі, в якому рухаються інші багатокутники різних форм і розмірів, стріляти та розбивати їх, не стикаючись із жодними з них чи з їх фрагментами, а також відбивати атаки від окремих багатокутників.

Гра з демонстрації розроблена за мотивами Asteroids - популярної гри на ігрових автоматах, що була випущена Atari у 1979 році.

8.1.7. Проєкт на свій задум

Творчий проєкт на вільну тему.

8.2. Представлення та захист проєктів

Представлення та захист проєктів за допомогою презентацій і вебсторінок.

Для розміщення на вебсторінці коду застосунку можна скористатися віджетом .

Словник

Автентифікація

процедура встановлення належності користувачеві інформації в системі пред’явленого ним ідентифікатора.

Адреса ресурсу (англ. Uniform Resource Locator - єдиний вказівник на ресурс, URL)

стандартизована адреса певного ресурсу (як-от документ або зображення) в Інтернеті (чи деінде).

Алгоритм

набір інструкцій, які описують порядок дій виконавця для отримання результату розв’язання задачі за скінченну кількість кроків.

Альфа-канал

кольорова складова, яка визначає ступінь прозорості (або непрозорості) кольору (тобто червоного, зеленого та синього каналів) для колірної моделі RGB. Альфа-канал використовується переважно для альфа-композитингу - комбінування зображення з тлом з метою створення ефекту часткової прозорості.

Амплітуда

найбільше відхилення величини, яка періодично змінюється від деякого значення, умовно прийнятого за нульове. У гармонічних коливаннях амплітуда є сталою величиною. Термін «амплітуда» часто вживають у ширшому сенсі - неформально називають рамки, в яких відбуваються зміни будь-якої природи, - щодо величин, які змінюються за законом, більше чи менше наближеним до гармонічного (амплітуда кров’яного тиску), а часом до коливань, що є далекими від гармонічних (амплітуда коливань річної температури). Амплітудою звукової хвилі є гучність, а світлової - яскравість.

Аплет (англ. Applet)

невеликий прикладний застосунок, що функціонально розширює можливості основного застосунку.

Асинхронність

полягає у відкладенні в часі виконання коду, доки не будуть отримані певні дані чи не настануть деякі події. Наприклад, код JavaScript у вебпереглядачі може зупинятися у своєму виконанні, очікуючи настання певних подій, як-от натискання користувача на кнопці, рух вказівником миші тощо.

База даних (англ. database)

організований набір структурованої інформації або даних, які зазвичай зберігаються в електронному вигляді в комп’ютерній системі. Зазвичай базою даних керує система керування базами даних (СКБД).

Бібліотека WebGL (Web Graphics Library)

міжплатформний API> JavaScript для відтворення високопродуктивної інтерактивної 3D- і 2D-графіки в будь-якому сумісному вебпереглядачі без використання плагінів. WebGL використовує графічний процесор (англ. graphics processing unit, GPU) комп’ютера, спеціалізований апаратний компонент, призначений для ефективного обчислення кольорів багатьох пікселів одночасно для відтворення комп’ютерної графіки на екрані. Бібліотека p5.js для роботи з WebGL використовує спеціальний режим WebGL.

Вебпереглядач

програмне забезпечення для комп’ютера або іншого електронного пристрою, як правило, під’єднаного до Інтернету, що дає можливість користувачеві взаємодіяти з текстом, зображеннями або іншою інформацією на гіпертекстовій вебсторінці.

Вебсервер (Web Server)

термін може стосуватися апаратного (комп’ютер, який приєднаний до Інтернету і на якому зберігається програмне забезпечення вебсервера та файли сайтів) чи програмного (застосунки, які приймають запит від вебпереглядача, оброблять його, наприклад, шукають у базі даних запитувану вебсторінку і надсилають її (інакше повідомлення про помилку з кодом помилки) назад у вебпереглядач користувача, використовуючи протокол передачі даних HTTP, HyperText Transfer Protocol) забезпечення або обох, які працюють разом.

Вебсторінка (англ. webpage)

інформаційний ресурс, доступний в мережі Інтернет, який можна переглянути у вебпереглядачі. Зазвичай інформація на вебсторінці зберігається у HTML-форматі.

Генеративний дизайн

підхід до проєктування і дизайну цифрового (сайт, зображення, мелодія, анімація) або фізичного продукту (архітектурна модель, деталь машини). Дизайнер, інженер або інший замовник безпосередньо не шукає розв’язання поставленої задачі, а описує її параметри в застосунку, після чого той створює (генерує) варіанти рішення, які формують бачення продукту. Генеративні системи напівавтономно створюють і відбирають варіанти рішень. Це змінює характер взаємодії людини з системою: застосунок сприймається не як засіб, а як повноцінний учасник творчого процесу.

Гіпертекст (англ. Hypertext)

текст для перегляду на комп’ютері, який містить зв’язки з іншими документами. Ці зв’язки реалізуються за допомогою гіперпосилань - активний (виділений кольором) текст, зображення чи кнопка на вебсторінці, натиснення на які (активізація гіперпосилання) викликає перехід на іншу сторінку чи іншу частину поточної сторінки.

Деструктуризація

дія, що дозволяє розбивати об’єкт чи масив на змінні під час присвоєння. Використовуючи спеціальний синтаксис, можна розпакувати масиви або об’єкти в купу змінних. Деструктуризація також чудово працює із функціями, які мають багато параметрів.

Дизайн-код

набір правил, який встановлює деталі оформлення забудови міського простору.

Інтернет речей (англ. Internet of Things, IoT)

концепція мережі, яка складається із пристроїв, які мають вбудовані технології, що дозволяють здійснювати взаємодію з зовнішнім середовищем, передавати відомості про свій стан і приймати дані ззовні.

Комп’ютерна графіка

розділ інформатики, який вивчає методи цифрового синтезу та обробки зорового контенту засобами комп’ютерної техніки. Розрізняють три види комп’ютерної графіки: растрова графіка, векторна графіка і фрактальна графіка. Вони відрізняються принципами формування зображення при відображенні на екранах пристроїв чи у разі друку на папері. Растрову графіку використовують при розробці електронних (мультимедійних) і поліграфічних видань. Більшість графічних редакторів, призначених для роботи з растровими зображеннями, орієнтовані більше на обробку, а не створення зображення. Прикладами растрових зображень є цифрові світлини. Векторна графіка стосується застосування шрифтів і простих геометричних елементів у створенні зображень. Програмні засоби для роботи з фрактальною графікою призначені для автоматичної генерації зображення шляхом математичних розрахунків. Принцип створення фрактальної художньої композиції полягає не в малюванні чи оформленні, а у програмуванні.

Комп’ютерне мистецтво, цифрове мистецтво (англ. Digital art)

напрямок медіамистецтва, заснований на використанні комп’ютера, інформаційних технологій, як основи для художнього твору.

Літерал об’єкта (object literal)

спосіб створити значення об’єкта, буквально записавши його у свій застосунок, наприклад {} або {flavor: "vanilla"}. Усередині {} можна зберігати кілька властивостей: пари значень, розділені комами.

Масив

структура даних для зберігання сукупності елементів, впорядкованих за індексами, які репрезентовані натуральними числами та визначають положення елемента в масиві.

Мова програмування (англ. Programming language)

штучна мова для опису алгоритмів, які виконуються комп’ютером.

3D-модель

об’ємне цифрове зображення об’єкта. Тривимірні (3D) моделі представляють об’єкт за допомогою набору точок у 3D-просторі, з’єднаних різними геометричними об’єктами, як-от трикутники, лінії, вигнуті поверхні тощо. Сам процес створення об’ємного зображення об’єктів у цифровому форматі називається 3D-моделюванням.

Модель HSV (також HSB)

колірна модель, заснована на трьох характеристиках кольору: колірному тоні (Hue, варіюється в межах 0-360°, але іноді приводиться до діапазону 0-100 або 0-1), насиченості (Saturation, в межах 0-100 або 0-1, чим ближчий цей параметр до нуля, тим колір ближчий до нейтрального сірого) і значенні кольору (Value, задається в межах 0-100 або 0-1).

Модель RGB

адитивна (сприйнятий колір можна передбачити шляхом підсумовуванням числових коефіцієнтів кольорів компонентів) колірна модель, що описує спосіб синтезу кольору, за якою червоне, зелене та синє світло накладаються разом, змішуючись у різноманітні кольори.

Модель RGBA

триканальна колірна модель RGB, доповнена четвертим альфа-каналом.

Об’єкт

ключове поняття об’єктоорієнтованих технологій проєктування та програмування; втілення абстрактної моделі окремої сутності (предмету або поняття), що має чітко виражене функціональне призначення в деякій області, належить до визначеного класу та характеризується своїми властивостями та поведінкою. Об’єкти є базовими елементами побудови застосунку — застосунок в об’єктоорієнтованому програмуванні розглядається як сукупність об’єктів, що знаходяться у визначених відношеннях та обмінюються повідомленнями.

Об’єктоорієнтоване програмування

метод програмування, який розглядає програму як сукупність об’єктів, які взаємодіють між собою. Кожен з об’єктів є екземпляром певного класу, а класи є членами певної ієрархії наслідування. Клас можна визначити як певну сукупність даних (характеристик об’єкта) та методів роботи з ними.

Об’єктна модель документа (англ. Document Object Model, DOM)

міжплатформний інтерфейс програмування застосунків (API) для маніпулювання HTML-документами. DOM дозволяє застосункам отримувати доступ до вмісту HTML-документів, змінювати вміст, структуру та оформлення таких документів. HTML і JavaScript взаємодіють між собою за допомогою DOM.

Перспектива камери

ефект, який відтворює враження глибини та простору в тривимірному зображенні, завдяки чому об’єкти на відстані здаються меншими, тоді як об’єкти, що знаходяться ближче, здаються більшими. Коли ми дивимося на об’єкти в реальному світі, об’єкти, розташовані далеко, здаються меншими, оскільки лінії їхнього перетину з нашим зором прямують до однієї точки в безмежності, відомої як точка зникнення. Цей ефект перспективи можна відтворити в тривимірному комп’ютерному зображенні за допомогою перспективної проєкції камери. Перспектива камери є важливим аспектом в тривимірній графіці, оскільки вона додає візуальну глибину, реалістичність та враження простору до 3D-сцен і допомагає відтворити сприйняття простору та розташування об’єктів в тривимірному просторі.

Піксель (елемент зображення)

найдрібніша одиниця цифрового зображення в растровій графіці. Він являє собою неподільний об’єкт зазвичай квадратної форми, що має певний колір. Будь-яке растрове комп’ютерне зображення складається з пікселів, розташованих рядами по горизонталі й вертикалі. Якщо збільшувати масштаб зображення, пікселі перетворюються на великі зерна і їх можна побачити. Пікселі не є мірою розміру зображення, оскільки багато цифрових апаратних пристроїв використовують показник пікселі на дюйм (англ. Pixels Per Inch, PPI) - одиницю вимірювання щільності пікселів, яка визначає число пікселів, що припадають на дюйм поверхні. Це впливає більше на якість зображення, ніж на його фізичний розмір.

Піксельна графіка (англ. Pixel art)

напрямок цифрового мистецтва, яке полягає у створенні зображень на рівні пікселів. Далеко не всі растрові зображення є піксель-артом, хоча всі вони складаються з пікселів. Зрештою, поняття pixel art вміщує в себе не стільки результат, скільки процес створення ілюстрації піксель за пікселем. Якщо ви візьмете цифрову світлину і сильно її збільшите (так, щоб пікселі стали видимі), то це не вважатиметься піксель-артом.

Початковий код, програмний код (англ. source code)

набір інструкцій, написаних мовою програмування у формі, що її може прочитати та модифікувати людина.

Прикладний програмний інтерфейс, інтерфейс програмування застосунків (англ. Application Programming Interface, API)

набір протоколів взаємодії та засобів для створення програмного забезпечення. API надає розробнику засоби для швидкої розробки програмного забезпечення і використовується для вебсистем, операційних систем, баз даних тощо.

Програмне забезпечення, програмні засоби, ПЗ (англ. software)

сукупність програм, призначених для розв’язання різних завдань за допомогою комп’ютера.

Програмування

процес проєктування, написання, тестування, налагодження і підтримки комп’ютерних застосунків. У більш конкретному значенні програмування розглядається як кодування - реалізація у вигляді застосунку одного чи кількох взаємопов’язаних алгоритмів за допомогою мови програмування.

Процедурне програмування

метод програмування, заснований на концепції виклику процедури. Процедури, також відомі як підпрограми, методи, або функції.

Растрова графіка

складова комп’ютерної графіки, яка має справу зі створенням, обробкою та зберіганням растрових зображень. Растрове зображення є масивом кольорових точок - пікселів.

Рендеринг

процес створення зображення або відео з тривимірної сцени (3D-сцени), який охоплює обчислення освітлення, тіней, кольорів та інших візуальних ефектів для отримання реалістичного зображення або анімації. У контексті тривимірної графіки, рендеринг відбувається за допомогою графічного процесора (GPU) або спеціалізованого програмного забезпечення, яке обробляє геометрію об’єктів, розраховує освітлення та тіні, застосовує текстури та матеріали до поверхонь об’єктів, та виконує інші обчислення, щоб згенерувати тривимірне зображення. Рендеринг використовується в багатьох галузях, включаючи комп’ютерну графіку, відеоігри, візуалізацію даних, архітектурне моделювання, виробництво фільмів.

Робототехніка (англ. robotics)

прикладна наука, що опікується проєктуванням, розробкою, виготовленням та використанням роботів, а також комп’ютерних систем для керування ними.

Розумний дім (розумний будинок, smart home)

система домашніх пристроїв, здатних виконувати дії й вирішувати певні повсякденні завдання без участі людини. Пристрої можуть бути під’єднані до комп’ютерної мережі, що дозволяє керувати ними віддалено через Інтернет. Така система повинна вміти розпізнавати конкретні ситуації, що відбуваються в будинку, і відповідним чином на них реагувати.

Семпл

відрізок аудіоінформації, вирізаний або записаний з якого-небудь наявного джерела. Наприклад, звук акустичного музичного інструменту, звук техногенного чи природного походження, звук, вирізаний з наявної аудіокомпозиції чи відеофільму тощо.

Семплер

електронний або цифровий музичний інструмент, який використовує звукові записи (семпли) реальних звуків інструментів (наприклад, фортепіано, скрипки чи труби), уривки із записаних пісень (наприклад, п’ятисекундний риф бас-гітари з фанк-пісні) або знайдені звуки (наприклад, океанські хвилі).

Середовище програмування (IDE)

інтегроване середовище розробки, яке містить редактор початкового коду, компілятор чи інтерпретатор, засоби автоматизації збірки та засоби для спрощення розробки графічного інтерфейсу користувача.

Синтезатори

електронні пристрої, які синтезують звук за допомогою одного чи кількох електричних генераторів коливань.

Словесна бульбашка, хмаринка думок (англ. Speech Balloon)

графічний засіб, виноска, що використовується в основному в коміксах і манґах (комікси, що створені в Японії) для ілюстрації слів чи думок персонажа. Найбільш поширеними формами виносок є «бульбашка», яка вказує на слова, і «хмаринка», яка вказує на думки.

Стилус

інструмент у вигляді ручки для вводу команд на сенсорний екран планшетного комп’ютера чи іншого мобільного пристрою. Являє собою невелику металеву або пластикову паличку зі спеціальним силіконовим наконечником, якою торкаються сенсорної поверхні екрана - або для управління пристроєм (як замінник людського пальця) або для писання, креслення чи малювання.

Сцена 3D (3D-сцена)

віртуальний простір, який складається з тривимірних об’єктів, світла, матеріалів, камер та інших елементів, які дозволяють відтворити візуальні ефекти, такі як перспектива, освітлення, тіні та рельєф. 3D-сцена може бути статичною (фотографія або зображення) або динамічною (відео або інтерактивний застосунок), і створена за допомогою спеціалізованого програмного забезпечення або бібліотек для рендерингу тривимірних об’єктів.

Тривимірна графіка (3D-графіка)

графічний процес візуалізації об’єктів та 3D-сцен в тривимірному просторі, що дозволяє створювати реалістичні зображення або анімації.

Файл OBJ

відкритий формат файлу для 3D-моделей, який зберігає геометричні дані, а також деякі дані про матеріали та текстури.

Файл STL

формат файлів для 3D-моделей. Він зберігає лише інформацію про геометрію.

Формат CSV (англ. comma-separated values - значення, розділені комою)

текстовий формат для зберігання табличних даних, де кожен рядок файлу зазвичай представляє один запис даних, поля якого відокремлюються символом коми та переходу на новий рядок. Формат CSV використовується для обміну даними між базами даних та застосунками для роботи з електронними таблицями.

Формат JSON (англ. JavaScript Object Notation, вимовляється джейсон)

незалежний від мови формат відкритих даних, який використовує зрозумілий для людини текст для опису об’єктів даних, що складаються з пар ключ: значення. Попри те, що дані JSON спочатку були похідними від мови JavaScript, їх можна генерувати та аналізувати за допомогою широкого спектра мов програмування, включаючи JavaScript, PHP, Python, Ruby та Java. Формат застосовується для зберігання та передавання структурованої інформації. Розробив і популяризував формат Дуглас Крокфорд.

Фрактальне мистецтво

форма цифрового мистецтва, створена шляхом обчислення фрактальних об’єктів (в широкому розумінні фрактал означає фігуру, малі частини якої в довільному збільшенні є подібними до неї самої) і представляє результати обчислень як нерухомі зображення, анімацію та автоматично створювані мультимедійні дані. Фрактальне мистецтво зародилося в середині 1980-х років.

Цифровий живопис

новий вид мистецтва, в якому традиційні техніки живопису, такі як акварель, олія та інші, імітуються за допомогою комп’ютера, графічного планшета, стилуса та програмного забезпечення.

Частота (англ. frequency)

фізична величина, що дорівнює кількості однакових подій за одиницю часу (рухів, коливань тощо) і є однією з основних характеристик періодичних процесів. Частота показує, скільки періодів процесу відбувається за одиницю часу. Розрізняють лінійну частоту (ν) - кількість періодичних процесів за секунду й циклічну (кутову) частоту (ω) - кількість коливань за секунд. Циклічна частота використовується в формулах для того, щоб не писати множник , але числові значення приводяться для лінійної частоти. Тому, коли говорять, що тактова частота комп’ютера 3 ГГц, або, що людина чує звуки частотою від 16 Гц до 20000 Гц, йдеться про лінійну частота ν. Наприклад, частота звукової хвилі сприймається людським вухом як тон, частота електромагнітної хвилі світлового діапазону сприймається людським оком як колір. Одиницею вимірювання частоти в Міжнародній системі одиниць є герц (Гц).

Черепашача графіка

термін, що застосовується до зображень, створених за допомогою інструкцій, які виконує виконавець (Черепашка). Черепашкова графіка з’явилася як складова оригінальної мови програмування Logo, розробленої Воллі Фойрцайґом, Сеймуром Пейпертом і Синтією Соломон у 1967 році в освітніх цілях для навчання дітей дошкільного й молодшого шкільного віку основним концепціям програмування. Використання у мові Logo середовища для програмування Черепашки мало на меті допомогти дітям вчитися програмувати, оскільки вони могли б розуміти програму, уявляючи як би вони виконували команди, бувши на місці Черепашки. Популярним прикладом такого середовища є класична бібліотека turtle у мові програмування Python.

Штучний інтелект (Artificial intelligence, AI)

напрямок в інформатиці та інформаційних технологіях, завданням якого є відтворення за допомогою обчислювальних систем та інших штучних пристроїв розумних міркувань і дій.

Додаток A: Редактори коду

У таблиці наведений короткий огляд рекомендованих середовищ програмування і редакторів коду для створення й виконання застосунків за допомогою бібліотеки p5.js.

Таблиця "Редактори коду"
Назва Встановлення Онлайн Опис

Processing IDE

Windows, Linux, macOS

-

безплатне інтегроване середовище розробки з офіційного сайту Processing

p5.js Web Editor

-

+

онлайн-редактор коду з підтримкою HTML, CSS, JavaScript і p5.js (безплатна реєстрація для зберігання й обміну ескізами)

OpenProcessing

-

+

онлайн-середовище розробки з підтримкою HTML, CSS, JavaScript і p5.js (безплатна реєстрація для зберігання й обміну ескізами, розширені можливості доступні за платною підпискою)

CodePen

-

+

онлайн-редактор коду з підтримкою HTML, CSS, JavaScript і p5.js (підтримка мобільних пристроїв, для приєднання p5.js: перейти у вкладку JS, натиснути на , у розділі Add External Scripts/Pens у рядку пошуку знайти та обрати p5.js, натиснути кнопку Save & Close)

Replit

-

+

безплатне інтегроване онлайн-середовище розробки (при створенні нового Repl необхідно обрати шаблон p5.js)

Visual Studio Code

Windows, Linux, macOS

-

безплатне інтегроване середовище розробки (для належної роботи застосунків варто встановити розширення локального вебсервера Live Server )

p5.js Widget Editor

-

+

безплатний онлайн-редактор коду, створений на основі p5.js-widget - інструменту для вбудовування на сторінки сайтів редактора для запуску і редагування ескізів p5.js (проста підтримка мобільних пристроїв)

Додаток B: DOM

У цьому розділі ви дізнаєтесь про те, як керувати вебсторінкою ескізу за допомогою засобів бібліотеки p5.js і мови JavaScript.

Сьогодні JavaScript разом із HTML (мова розмітки гіпертексту, що використовується для створення HTML-документів) і CSS (спеціальна мова для опису зовнішнього вигляду HTML-документів) є основними технологіями Всесвітньої павутини .

Одним з аспектів використання мови JavaScript є керування елементами HTML-документів. Отож, розглянемо основні можливості, які надають JavaScript і бібліотека p5.js для зміни вмісту вебсторінки, на якій виконуються ескізи.

Для наших цілей будемо використовувати онлайн-редактор p5.js Web Editor . За бажанням, ви можете обрати інший редактор із рекомендованих для підручника або ваш улюблений редактор коду. Заразом не забувайте, що для належної роботи застосунків, які запускаються локально, необхідно використовувати локальний вебсервер .

При переході в онлайн-редактор у вікні вебпереглядача відкривається файл ескізу sketch.js, а якщо натиснути на кнопку > під кнопкою запуску, перед нами розгорнеться список усіх файлів нашого проєкту: index.html, sketch.js і style.css.

Список файлів проєкту в онлайн-редакторі p5.js Web Editor
Онлайн-редактор p5.js Web Editor: список файлів проєкту

Файли проєкту вже містять певний код за стандартним налаштуванням. Файл ескізу sketch.js, в якому записують початковий код ескізу за допомогою мови JavaScript і засобів p5.js, має наступний вміст:

sketch.js
function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}

Файл index.html містить код для приєднання файлів бібліотеки p5.js та ескізу sketch.js чи у разі потреби інших бібліотек:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />

  </head>
  <body>
    <main>
    </main>
    <script src="sketch.js"></script>
  </body>
</html>
Приєднання JavaScript-файлів у HTML-документ index.html виконується за допомогою атрибуту src тегу <script>. Атрибут src містить значення URL-адреси файлів, що приєднуються. У редакторі p5.js Web Editor для бібліотек цей шлях є абсолютним, а для файлу ескізу - відносним щодо файлу index.html.
Якщо ви використовуєте локальну розробку і власний редактор коду, слідкуйте за правильністю прописаних шляхів до файлів проєкту.

У файлі style.css записаний початковий набір стилів:

style.css
html, body {
  margin: 0;
  padding: 0;
}
canvas {
  display: block;
}

Додамо в тіло документа index.html певний вміст

index.html
<!DOCTYPE html>
<html lang="uk">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <meta charset="utf-8" />
    <title>JavaScript, p5.js і DOM</title>
  </head>
  <body>
    <main id="content">
      <h1>JavaScript, p5.js і DOM</h1>
      <div>
        <p>p5.js - бібліотека JavaScript для креативного кодування.</p>
        <p>Безплатна, з відкритим початковим кодом.</p>
        <p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
      </div>
      <div class="sample">
        <p>Малюнок кола - зразок простого застосунку.</p>
      </div>
      <div>
        <p>Створено за допомогою <a href="https://editor.p5js.org/">p5.js Web Editor</a></p>
      </div>
    </main>
    <script src="sketch.js"></script>
  </body>
</html>

і змінимо код у файлі ескізу sketch.js для створення простого застосунку

sketch.js
function setup() {
  createCanvas(200, 200);
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

Після запуску застосунку вебпереглядач, використовуючи HTML-розмітку у файлі index.html і розмітку, яка була створена динамічно за допомогою коду із файлу ескізу sketch.js, сформує DOM для вебсторінки index.html.

B.1. Що таке DOM?

Об’єктна модель документа (англ. Document Object Model, DOM) - це міжплатформний інтерфейс програмування застосунків (англ. Application Programming Interface, API) для маніпулювання HTML-документами. DOM дозволяє застосункам отримувати доступ до вмісту HTML-документів, змінювати вміст, структуру та оформлення таких документів. HTML і JavaScript взаємодіють між собою за допомогою DOM.

Коли вебпереглядач завантажує певну вебсторінку, він будує DOM - дерево об’єктів, які представляють HTML-розмітку.

Переглянути DOM-дерево можна у вкладці Елементи на панелі розробника у вебпереглядачі за допомогою сполучення клавіш Ctrl+Shift+C.

Створені об’єкти зберігаються як вузли об’єктної моделі документа і є JavaScript-об’єктами.

Наприклад, поглянемо як виглядає вузол тега <head> у вигляді об’єкта. Для цього у консолі вебпереглядача (вкладка Консоль на панелі розробника у вебпереглядачі) введемо команду console.dir(document.head);.

Тег <head> як об’єкт (фрагмент)
Тег <head> як об’єкт (фрагмент)

Вищенаведена команда друкує JSON-подання переданого об’єкта, що зручно для аналізу його властивостей.

У прикладах коду для друку повідомлень в консолі вебпереглядача ми будемо використовувати ще одну консольну команду: console.log("log");.
З детальним описом команд консолі вебпереглядача (Chrome) можна ознайомитися в довіднику API вебпереглядача .

Кожен вузол у DOM належить до певного вбудованого класу JavaScript. Є базовий клас для всіх вузлів - клас Node, а від нього успадковуються інші типи вузлів:

  • теги створюють вузли-елементи (клас Element);

  • текст усередині тегів створює текстові вузли (клас Text);

  • атрибути тегів створюють вузли-атрибути (клас Attr);

  • коментарі у розмітці створюють вузли-коментарі (клас Comment).

DOM подібний на HTML-код, але не є ним, а лише формується з нього. У прикладі вище, за допомогою JavaScript і бібліотеки p5.js ми додали нові вузли й DOM став іншим, ніж HTML для документа index.html. Інакше, HTML представляє початковий вміст вебсторінки, а DOM представляє поточний вміст сторінки.
DOM вебсторінки index.html можна також переглянути за допомогою інструмента Live DOM Viewer . Оскільки JavaScript взаємодіє, як правило, з тегами, а для зміни вмісту тегів використовує методи та властивості вузлів-елементів, для кращого візуального огляду схеми DOM, що складається лише з вузлів-елементів, зручно використовувати інструмент DOM Visualizer .

Отож, DOM представляє HTML-документ у вигляді перевернутого дерева вузлів, в якому корінь дерева - вгорі, а листки-вузли - розгалужуються донизу. Гілки цієї деревоподібної ієрархії визначають зв’язки між вузлами-елементами.

Якщо використовувати термінологію відносин у сім’ї, вузли-елементи можуть бути батьками, дітьми, рідними братами й сестрами та нащадками.

Діти (дочірні елементи) - елементи, які містяться безпосередньо всередині батьківського елемента. Наприклад, всередині батьківського елемента <html> знаходяться його дочірні елементи <head> і <body>. Рідні брати й сестри - елементи, у яких один і той самий батько, наприклад, <head> і <body> - є рідними братами й сестрами, оскільки мають одного батька - <html>. Нащадки - усі елементи разом з їхніми дітьми, дітьми їхніх дітей і так далі, які лежать всередині даного батьківського елемента.

На верхньому рівні DOM-дерева завжди знаходиться об’єкт document - кореневий батьківський вузол, який представляє HTML-документ, що відображається у вікні переглядача.

Об’єкт document використовується в JavaScript для доступу до всього дерева DOM і має один дочірній вузол, який є елементом <html>.

Враховуючи кореневий вузол document, схема DOM набуває вигляду:

Схема DOM HTML-документа
Схема DOM HTML-документа

У нашій схемі батько-тег <main> має чотирьох дітей - один тег <h1> і три теги <div> (плюс ще кілька порожнів текстових вузлів). <h1> і три теги <div> - це рідні брати й сестри, оскільки вони мають спільного батька.

Перший <div> (рахуємо зліва направо) є батьком для трьох дочірніх елементів <p>, другий і третій <div> - мають по одній дитині <p>. Третій <div> має нащадка <a>.

Нащадками <main> є його діти - один <h1> і три <div>, усі елементи <p> і елемент <a>.

Отже, коли JavaScript взаємодіє з вебсторінкою і намагається отримати вміст, наприклад, елемента <h1>, використовується DOM для входу у дерево вузлів, щоб отримати покликання на вузол-елемент <h1>.

Отримавши таке покликання на <h1>, можна використовувати різні методи та властивості для цього покликання, щоб змінити вміст елемента-вузла, його стиль тощо.

Загалом, отримавши доступ до елементів та їх вміст через DOM, JavaScript може виконувати будь-які маніпуляції над ними, зокрема:

  • додавати, змінювати, видаляти вміст на вебсторінці;

  • змінювати оформлення вебсторінки;

  • реагувати на події у вікні вебпереглядача (натискання кнопок, рух вказівника миші тощо).

Будь-яка робота з DOM повинна починатися лише після повного завантаження вебсторінки, оскільки це гарантує, що DOM-дерево утворено і до нього додані всі елементи вебсторінки. Найпростіший спосіб це забезпечити - розмістити тег <script> з вашим кодом перед тегом </body>.

B.2. Пошук елементів

Щоб виконувати дії (змінювати вміст, властивості, обробляти події тощо) над елементами HTML-документів, спочатку їх шукають в DOM-дереві.

Алгоритм пошуку будь-якого елемента або фрагмента HTML-документа за допомогою JavaScript можна описати так:

  1. За допомогою зарезервованого слова const (або let) визначити ім’я, яке буде містити покликання на шуканий елемент або частину HTML-документа.

  2. Звернутися до кореневого вузла HTML-документа - document.

  3. За допомогою крапкової нотації застосувати на об’єкті document метод querySelector() у разі пошуку якогось одного елемента чи метод querySelectorAll(), якщо потрібний список усіх елементів.

  4. У методах querySelector() і querySelectorAll() як аргумент використати рядок - значення селектора, який описує те, що шукаємо.

Цікавимось

CSS-селектори
CSS-cелектори (англ. select - вибирати) - це складові мови CSS , яка використовується для опису зовнішнього вигляду HTML-документів.

CSS-селектор - це формальний опис елементів або групи елементів. Такий формальний опис визначає, до яких елементів буде застосоване те чи інше CSS-правило.

СSS-правило
СSS-правило

Наприклад, вищенаведене CSS-правило встановлює червоний колір для тексту в усіх абзацах на вебсторінці.

Основні види селекторів:

  • * - універсальний селектор - вибирає будь-які елементи;

  • p - селектор за типом елемента - вибирає усі елементи <p>;

  • #id - селектор за ідентифікатором - вибирає елемент із вказаним id;

  • .class - селектор за класом - вибирає усі елементи із вказаним CSS-класом;

  • elem[name="value"] - селектор за атрибутом - вибирає усі елементи elem, які мають атрибут name зі значенням value.

Селектори можна комбінувати. Наприклад:

  • div p - вибрати усі елементи <p>, які знаходяться в елементі <div>;

  • div > p - вибрати усі дочірні елементи <p>, у яких батьком є <div>;

  • p.sample - вибрати усі елементи <p>, які мають CSS-клас .sample.

Детальніше із селекторами можна ознайомитися на ресурсі для веброзробників MDN Web Docs .

Скористаємось зразком проєкту, щоб проілюструвати різні способи пошуку елементів.

Наприклад, перевіримо HTML-документ на наявність в ньому елемента <p>. Перетворимо вищезгаданий алгоритм у код і запишемо його у файл ескізу у функцію setup():

sketch.js
function setup() {
  createCanvas(200, 200);
  const result = document.querySelector("p"); (1)
  console.log(result); (2)
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}
1 Виконуємо запит на пошук елемента <p> в DOM-дереві документа index.html і результат запиту зберігаємо з назвою result. Тут використовується метод querySelector(), оскільки шукаємо один елемент <p>.
2 Друкуємо значення результату в консолі вебпереглядача за допомогою команди console.log().

З надрукованого в консолі значення result дізнаємось, що елемент <p> успішно знайдений:

<p>p5.js - бібліотека JavaScript для креативного кодування.</p>
Пошук елементів відбувається згори донизу сторінки.

Якщо поглянути на код файлу index.html, в якому відбувався пошук, то можна нарахувати п’ять елементів <p>

index.html
...
<div>
  <p>p5.js - бібліотека JavaScript для креативного кодування.</p>
  <p>Безплатна, з відкритим початковим кодом.</p>
  <p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
</div>
<div class="sample">
  <p>Малюнок кола - зразок простого застосунку.</p>
</div>
<div>
  <p>Створено за допомогою <a href="https://editor.p5js.org/">p5.js Web Editor</a></p>
</div>
...

а в результаті пошуку ми отримали лише перший з абзаців.

querySelector() - метод, який повертає перший елемент від початку документа, який відповідає зазначеному селектору або групі CSS-селекторів. Якщо збігу не знайдено, повертає значення null.

Розглянемо ще кілька прикладів використання методу querySelector().

Селектор за ідентифікатором #content

const result = document.querySelector("#content");
console.log(result);

вибирає елемент <main> із його вмістом

<main id="content">
  <h1>JavaScript, p5.js і DOM</h1>
  <div>
    <p>p5.js - бібліотека JavaScript для креативного кодування.</p>
    <p>Безплатна, з відкритим початковим кодом.</p>
    <p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
  </div>
  <div class="sample">
    <p>Малюнок кола - зразок простого застосунку.</p>
  </div>
  <div>
    <p>Створено за допомогою <a href="https://editor.p5js.org/">p5.js Web Editor</a></p>
  </div>
  <canvas id="defaultCanvas0" class="p5Canvas" width="200" height="200" style="width: 200px; height: 200px;"></canvas>
</main>

Селектор за класом .sample

const result = document.querySelector(".sample");
console.log(result);

вибирає елемент <div>, для якого вказаний CSS-клас .sample

<div class="sample">
  <p>Малюнок кола - зразок простого застосунку.</p>
</div>

Комбінований селектор div.sample p

const result = document.querySelector("div.sample p");
console.log(result);

вибирає елемент <p>, який міститься в елементі <div>, який зі свого боку має CSS-клас .sample

<p>Малюнок кола - зразок простого застосунку.</p>

Комбінований селектор за атрибутом div[class='sample'] > p

const result = document.querySelector("div[class='sample'] > p");
console.log(result);

повертає елемент <p>, який є дочірнім елементом для елемента <div>, який зі свого боку має атрибут class зі значенням sample

<p>Малюнок кола - зразок простого застосунку.</p>

Тепер застосуємо вищезгаданий алгоритм для пошуку усіх елементів <p>. У цьому разі використаємо метод querySelectorAll():

const resultNodeList = document.querySelectorAll("p");
console.log(resultNodeList);

Список знайдених елементів буде міститися в колекції вузлів з назвою NodeList:

NodeList {0: HTMLParagraphElement, 1: HTMLParagraphElement, 2: HTMLParagraphElement, 3: HTMLParagraphElement, 4: HTMLParagraphElement…}
0: <p>p5.js - бібліотека JavaScript для креативного кодування.</p>
1: <p>Безплатна, з відкритим початковим кодом.</p>
2: <p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
3: <p>Малюнок кола - зразок простого застосунку.</p>
4: <p>…</p>
entries: ƒ entries() {}
keys: ƒ keys() {}
values: ƒ values() {}
forEach: ƒ forEach() {}
length: 5
item: ƒ item() {}
<constructor>: "NodeList"
querySelectorALL() - метод, який повертає список усіх елементів документа, що відповідають зазначеному селектору або групі CSS-селекторів. Список знайдених елементів - це колекція NodeList . Якщо збігів не знайдено, властивість length об’єкта NodeList дорівнюватиме нулю.

Об’єкт NodeList має властивість length і може індексуватися подібно масиву (колекції схожі на масиви, для проходу по колекціях можна використовувати цикл for) і є ітерабельним - повертає наступний елемент з колекції (для проходу по колекціях можна використовувати цикл for..of).

Якщо для проходу по елементах отриманої колекції NodeList використати цикл for

const paragraphs = document.querySelectorAll("p");
for (let i = 0; i < paragraphs.length; i++) {
  console.log(paragraphs[i]);
}

або цикл for..of

const paragraphs = document.querySelectorAll("p");
for (const paragraph of paragraphs) {
  console.log(paragraph);
}

або метод forEach

const paragraphs = document.querySelectorAll("p");
paragraphs.forEach((paragraph) => {
  console.log(paragraph);
});

в усіх випадках отримаємо однаковий результат - список усіх елементів <p>:

<p>p5.js - бібліотека JavaScript для креативного кодування.</p>
<p>Безплатна, з відкритим початковим кодом.</p>
<p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
<p>Малюнок кола - зразок простого застосунку.</p>
<p>
  Створено за допомогою
  <a href="https://editor.p5js.org/">p5.js Web Editor</a>
</p>

До певного елемента колекції можна звернутися за індексом (нумерація індексів починається з нуля)

const paragraphs = document.querySelectorAll("p");
console.log(paragraphs[2]);

і отримати цей елемент

<p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>

Для роботи з колекціями визначені спеціальні методи: entries(), keys(), values(). Наприклад, використання методу keys() і циклу for..of

const paragraphs = document.querySelectorAll("p");
for(const key of paragraphs.keys()) {
  console.log(key);
}

дає змогу отримати список індексів елементів колекції

0
1
2
3
4

А метод entries() і цикл for..of

const paragraphs = document.querySelectorAll("p");
for(const p of paragraphs.entries()) {
  console.log(p[0], p[1]);
}

дозволяє отримати як індекси, так і значення елементів колекції:

0 <p>p5.js - бібліотека JavaScript для креативного кодування.</p>
1 <p>Безплатна, з відкритим початковим кодом.</p>
2 <p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
3 <p>Малюнок кола - зразок простого застосунку.</p>
4 <p>Створено за допомогою <a href="https://editor.p5js.org/">p5.js Web Editor</a></p>

Цікавимось

Альтернативні способи пошуку елементів

В DOM описані альтернативні методи вибору елементів, які зараз вважаються застарілими. Нижче наведена порівняльна таблиця нових і застарілих методів пошуку елементів за допомогою JavaScript.

Таблиця "Методи вибору елементів"
Тип пошуку Нові методи Застарілі методи

за ідентифікатором id="content"

let content = document.querySelector("#content");

let content = document.getElementById("content");

усі елементи <h1>

let headings = document.querySelectorAll("h1");

let headings = document.getElementsByTagName("h1");

усі елементи <p> всередині елемента з id="content"

let paragraphs = document.querySelectorAll("#content p");

let paragraphs = content.getElementsByTagName("p");

усі елементи, які мають CSS-клас .sample

let samples = document.querySelectorAll(".sample");

let samples = document.getElementsByClassName("sample");

усі елементи, які лежать всередині елемента з id="content" і мають CSS-клас .sample

let samples = document.querySelectorAll("#content .sample");

let samples = content.getElementsByClassName("sample");

Усі застарілі методи getElementsBy* повертають колекцію вузлів-елементів HTMLCollection .

Бібліотека p5.js для пошуку елементів має власні засоби - функції select() і selectAll() .

Функція select() шукає на сторінці перший елемент, який відповідає заданому рядку CSS-селектора, який може бути ідентифікатором, CSS-класом, тегом або комбінованим варіантом і повертає об’єкт p5.Element.

p5.Element є базовим класом для усіх об’єктів, доданих до ескізу, включаючи HTML-елементи, і містить методи, які можна застосувати до цих об’єктів.

Виконаємо простий код для вибору на вебсторінці першого елемента <p> за допомогою функції select():

function setup() {
  createCanvas(200, 200);
  const paragraph = select("p");
  console.log(paragraph);
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

У консолі отримаємо об’єкт абзацу з його властивостями та методами:

{elt: HTMLParagraphElement, _pixelsState: p5, _pInst: p5, _events: Object, width: 946…}

Відповідно для пошуку усіх елементів <p> документа використаємо функцію selectAll():

const paragraphs = selectAll("p");
console.log(paragraphs);

Функція selectAll() шукає на сторінці усі елементи, що відповідають заданому рядку CSS-селектора, який може бути ідентифікатором, CSS-класом, тегом або комбінованим варіантом, і повертає масив об’єктів p5.Element, інакше - порожній масив, якщо жодного елемента не знайдено.

У підсумку, в консолі отримуємо масив із п’яти об’єктів:

(5) [Object, Object, Object, Object, Object]

B.3. Властивості елементів

Коли пошук елемента на вебсторінці виявився успішним, далі над цим елементом можна виконати наступні дії:

  • отримати вміст (текст або HTML-розмітку) елемента і змінити його;

  • отримати та змінити атрибути елемента.

Розглянемо властивості та методи вузлів-елементів за допомогою яких можна отримати характеристики елемента та змінити їх у разі потреби. Щоб проілюструвати наші дії, скористаємось зразком проєкту.

Отже, отримаємо текстовий вміст для першого елемента <p> на сторінці index.html.

Для цього використаємо властивість textContent , яка дозволяє встановлювати або отримувати текстовий вміст елемента та його нащадків.

Розглянемо такий код:

function setup() {
  createCanvas(200, 200);
  const firstParagraph = document.querySelector("p"); (1)
  let textFirstParagraph = firstParagraph.textContent; (2)
  console.log(textFirstParagraph); (3)
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}
1 Шукаємо елемент <p> за допомогою методу querySelector() і зберігаємо результат з ім’ям firstParagraph.
2 Отримуємо за допомогою крапкової нотації значення властивості textContent знайденого елемента і зберігаємо результат з ім’ям textFirstParagraph.
3 Друкуємо значення textFirstParagraph в консолі.

В консолі отримуємо текст, що міститься в першому елементі <p>:

p5.js - бібліотека JavaScript для креативного кодування.

За допомогою властивості textContent також можна замінити текст в елементі на інший.

Внесемо деякі зміни в попередній код

const firstParagraph = document.querySelector("p"); (1)
let textFirstParagraph = firstParagraph.textContent; (2)
console.log(textFirstParagraph); (3)
firstParagraph.textContent = "Перший абзац зазнав змін."; (4)
let textFirstParagraphChange = firstParagraph.textContent; (5)
console.log(textFirstParagraphChange); (6)

і отримаємо в консолі наступні результати:

p5.js - бібліотека JavaScript для креативного кодування.
Перший абзац зазнав змін.

Проаналізуємо результати:

1 Шукаємо на вебсторінці перший елемент <p> і зберігаємо результат пошуку з ім’ям firstParagraph.
2 Отримуємо текст першого елемента <p> із firstParagraph і зберігаємо з ім’ям textFirstParagraph.
3 Друкуємо в консолі значення тексту з textFirstParagraph.
4 Замінюємо за допомогою операції присвоєння поточне значення властивості textContent для першого елемента <p> на рядок "Перший абзац зазнав змін.".
5 Отримуємо новий текст першого елемента <p> із firstParagraph і зберігаємо з ім’ям textFirstParagraphChange.
6 Друкуємо в консолі значення тексту з textFirstParagraphChange і переглядаємо оновлений текст в першому абзаці у вебпереглядачі на вебсторінці застосунку.

Окрім повної заміни текстового вмісту, текст елемента можна доповнювати. Виконаємо доповнення першого абзацу іншим текстом за допомогою операції додавання з присвоєнням +=

const firstParagraph = document.querySelector("p");
firstParagraph.textContent += " Вона робить кодування доступним для будь-кого!";
let textFirstParagraph = firstParagraph.textContent;
console.log(textFirstParagraph);

і отримаємо в консолі результат конкатенації (об’єднання) тексту:

p5.js - бібліотека JavaScript для креативного кодування. Вона робить кодування доступним для будь-кого!

Оновлення тексту у першому абзаці також відбудеться й у вебпереглядачі на вебсторінці index.html застосунку.

Якщо необхідно замінити чи доповнити текст для декількох елементів, то використовують цикли for чи for..of або метод forEach. Скористаємось варіантом з forEach і вказівкою розгалуження

const paragraphs = document.querySelectorAll("p");
paragraphs.forEach((paragraph) => {
  if (paragraph.textContent.trim() != "Створено за допомогою p5.js Web Editor") {
    paragraph.textContent += "👍";
  } else {
    paragraph.textContent += "❤️";
  }
  console.log(paragraph.textContent);
});

щоб отримати в консолі результат конкатенації тексту кожного абзацу вебсторінки й смайлика Emoji:

p5.js - бібліотека JavaScript для креативного кодування.👍
Безплатна, з відкритим початковим кодом.👍
Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.👍
Малюнок кола - зразок простого застосунку.👍
Створено за допомогою p5.js Web Editor ❤️
У коді вище ми використали JavaScript-метод trim() , який видаляє символи пропусків (пропуск, табуляція, символи завершення рядка та інші) на початку та наприкінці текстового рядка.
У разі використання операції присвоєння =, вміст елемента повністю перезаписується. У разі додавання з присвоєнням += - до поточного вмісту елемента додається новий.

Інша властивість - innerHTML - використовується для встановлення або отримання HTML-розмітки всередині елемента.

Додамо трішки HTML-розмітки у наш застосунок за допомогою наступного коду JavaScript:

const div = document.querySelector(".sample"); (1)
div.innerHTML += "<p>Створи власний застосунок!</p>"; (2)
console.log(div); (3)

Отож, згідно із наведеним кодом виконуються такі кроки:

1 Знаходимо елемент <div>, який має клас .sample, і зберігаємо результат з ім’ям div.
2 За допомогою крапкової нотації звертаємось до властивості innerHTML, яку має div, і додаємо за допомогою += розмітку з абзацом тексту.
3 Друкуємо результат - оновлену розмітку елемента <div>, який має клас .sample.

Завдяки операції += у вікні вебпереглядача і в консолі, в елемент <div> був доданий новий абзац тексту, який розмістився після вмісту, що вже був присутнім в елементі <div>:

<div class="sample">
  <p>Малюнок кола - зразок простого застосунку.</p>
  <p>Створи власний застосунок!</p>
</div>

Інакше, у разі використання операції присвоєння =, увесь вміст елемента <div>, а саме елемент <p>Малюнок кола - зразок простого застосунку.</p>, був би видалений. В елементі <div> містився б лише доданий новий елемент <p>Створи власний застосунок!</p> і результат в консолі (також у вебпереглядачі) мав би наступний вигляд:

<div class="sample">
  <p>Створи власний застосунок!</p>
</div>

Розглянемо ще один приклад використання властивості innerHTML в контексті додавання HTML-розмітки за допомогою шаблонних літералів - рядків-шаблонів, які для запису використовують зворотні лапки ` ` і дозволяють використовувати вирази.

Наведений нижче код динамічно додає на вебсторінку ескізу три нових елементи абзацу, текстові значення яких зберігаються у масиві langs:

let langs = ["HTML", "CSS", "JavaScript"];

let div = document.querySelector(".sample");
div.innerHTML += "<p>Технології, які були використані:</p>";

for (const lang of langs) {
  div.innerHTML += `<p>${lang}</p>`;
}
console.log(div);

Це реалізовується за допомогою циклу for..of і шаблонного рядка, у який на кожній ітерації масиву підставляється значення lang із масиву у форматі ${lang}, а властивість innerHTML додає сформований елемент абзацу на вебсторінку:

<div class="sample">
  <p>Малюнок кола - зразок простого застосунку.</p>
  <p>Технології, які були використані:</p>
  <p>HTML</p>
  <p>CSS</p>
  <p>JavaScript</p>
</div>

Окрім зміни текстового і HTML-вмісту, ми можемо змінювати й атрибути елементів.

Атрибути - це спеціальні слова, які використовуються всередині вступного тегу для контролю поведінки HTML-елемента. Атрибут має вигляд name="value" (назва атрибута name і пов’язане з ним текстове значення value), наприклад, для <main id="content"> - id - атрибут, а content - значення атрибута.

Коли вебпереглядач завантажує вебсторінку, він аналізує HTML і генерує з нього DOM-вузли. Водночас більшість стандартних HTML-атрибутів автоматично стають властивостями DOM-елементів.

Щоб уникнути плутанини: атрибути - те, що написано у HTML, а властивості - те, що знаходиться в DOM-елементах.

Є кілька способів отримати та змінити властивості у DOM-елементів. Для стандартних HTML-атрибутів крапкова нотація - найпростіший спосіб зробити це.

Наприклад, отримаємо зі сторінки застосунку значення атрибутів: href елемента <a> і class елемента <div> із CSS-класом .sample:

const a = document.querySelector("a");
console.log(a.href);

const div = document.querySelector(".sample");
console.log(div.className);

У цьому разі атрибут class елемента <div> не перетворюється автоматично у DOM-властивість із такою ж назвою. Тому тут використовуємо властивість з назвою className.

Використовуючи властивість className, можна швидко замінити всі CSS-класи елемента. Наприклад, так: div.className = "sample other";, де .sample, .other - це CSS-класи.

Значення властивостей - це покликання на сторінку онлайн-редактора і назва класу відповідно:

https://editor.p5js.org/
sample
При аналізі HTML вебпереглядач не завжди перетворює атрибути тегів на текстові DOM-властивості, хоча більшість властивостей - це текстові рядки.

Інший спосіб доступу до атрибутів - використання методів:

Розглянемо приклади використання цих методів. Для початку скористаємось методом hasAttribute() для перевірки існування деяких атрибутів, наприклад, в елементі <a>:

const a = document.querySelector("a");
console.log(a.hasAttribute("type"));
console.log(a.hasAttribute("href"));
console.log(a.hasAttribute("target"));

Поглянувши на результат виконання застосунку

false
true
false

робимо висновок, що елемент <a> має зараз лише один атрибут href із поданого переліку. В цьому можна переконатися, зазирнувши в HTML-код сторінки index.html застосунку.

Отримаємо значення атрибута href для елемента <a>:

const a = document.querySelector("a");
console.log(a.getAttribute("href"));

В консолі знову побачимо URL-адресу редактора коду, як у разі використання крапкової нотації вище:

https://editor.p5js.org/

Якщо у вебпереглядачі натиснути на покликання на редактор коду, він відкриється безпосередньо в області перегляду застосунку.

Додамо атрибут target зі значенням _blank до елемента <a>, щоб змінити таку поведінку:

const a = document.querySelector("a");
a.setAttribute("target", "_blank");

Якщо поглянути на HTML-код сторінки запущеного застосунку (Ctrl+Shift+C), можна помітити, що для нашого покликання динамічно був доданий атрибут target зі значенням _blank.

Чудово! Тепер вікно редактора відкривається у новій вкладці вебпереглядача.

Для видалення атрибутів використовують метод removeAttribute(). Застосуємо його для видалення атрибута id тега <main>:

const main = document.querySelector("main");
main.removeAttribute("id");

Тепер поглянемо на можливості p5.js для зміни вмісту, властивостей і атрибутів елементів.

Отже, отримаємо усі елементи <p> на вебсторінці застосунку за допомогою функції selectAll() і пройдемо по отриманому масиві об’єктів за допомогою циклу for:

const paragraphs = selectAll("p");
for (let i = 0; i < paragraphs.length; i++) {
  console.log(paragraphs[i].elt);
}

В коді ми використали властивість elt , за допомогою якої можна отримати доступ до вузла DOM кожного об’єкта масиву

<p>p5.js - бібліотека JavaScript для креативного кодування.</p>
<p>Безплатна, з відкритим початковим кодом.</p>
<p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
<p>Малюнок кола - зразок простого застосунку.</p>
<p>
  Створено за допомогою
  <a href="https://editor.p5js.org/">p5.js Web Editor</a>
</p>

Особливості використання властивості elt можна проілюструвати на наступному прикладі:

let one = document.querySelector(".sample");
let two = select(".sample");
console.log(one);
console.log(one.innerHTML);
console.log(one.textContent);

console.log("(U・ᴥ・U)"); // розділювач у вигляді песика Фідо

console.log(two);
console.log(two.elt);
console.log(two.elt.innerHTML);
console.log(two.elt.textContent);

У консолі отримаємо такі результати (не враховуючи пропуски в HTML-розмітці):

<div class="sample">…</div>
<p>Малюнок кола - зразок простого застосунку.</p>
Малюнок кола - зразок простого застосунку.
(U・ᴥ・U)
{elt: HTMLDivElement, _pixelsState: p5, _pInst: p5, _events: Object, width: 1206…}
<div class="sample">…</div>
<p>Малюнок кола - зразок простого застосунку.</p>
Малюнок кола - зразок простого застосунку.
Властивість elt надає доступ до HTML-елемента і дозволяє змінити його властивості, наприклад, HTML-вміст, текстовий вміст, стиль тощо.

Для доступу до елементів і зміни їхнього вмісту також можна використати метод html() .

Подивимось, як це працює на наступному прикладі:

const h1 = select("h1"); (1)
h1.html(" <i>та інші технології</i>", true); (2)
let heading = h1.html(); (3)
console.log(heading); (4)
1 Шукаємо елемент <h1> і зберігаємо результат з ім’ям h1.
2 Додаємо за допомогою метода html() новий HTML-вміст для h1.
3 Отримаємо за допомогою метода html() вміст h1 і зберігаємо з ім’ям heading.
4 Друкуємо в консолі значення heading.

Після запуску застосунку у вебпереглядачі заголовок зміниться на JavaScript, p5.js і DOM та інші технології, а у консолі буде надрукований вміст заголовка, який ми отримали за допомогою метода html():

JavaScript, p5.js і DOM <i>та інші технології</i>

Метод html() додав до наявного тексту у заголовка першого рівня HTML-вміст, який передавався у першому аргументі, а доповнення відбулось завдяки другому (необов’язковому) аргументу - логічному значенню true.

Якщо у методі html() не вказувати другий аргумент, то вміст заголовку буде перезаписаний, про що ви можете переконатися самостійно.

Бібліотека p5.js надає засоби також для зміни атрибутів елементів.

За допомогою метода attribute() додамо атрибут style до заголовку <h1>:

const h1 = select("h1");
h1.attribute("style", "text-decoration: underline dotted red;");

Отже, заголовок отримає вбудований стиль за допомогою атрибута style - підкреслення штриховою лінією червоного кольору.

За допомогою метода attribute() можна отримати значення атрибутів елемента. Наприклад, значення ширини полотна застосунку можна отримати так:

const cnv = select("canvas");
let widthCanvas = cnv.attribute("width");
console.log(widthCanvas);

У підсумку, в консолі буде надруковано значення ширина елемента <canvas> у пікселях: 200.

Для встановлення атрибута id бібліотека p5.js має окремий метод id() , який встановлює ідентифікатор для елемента.

Якщо аргумент в id() не передається, цей метод повертає поточний ідентифікатор елемента, наприклад:

let cnv = createCanvas(200, 200);
console.log(cnv.id());

У консолі отримаємо значення id для полотна: defaultCanvas0.

На вебсторінці лише один елемент повинен мати певний ідентифікатор.
Для видалення атрибутів елементів бібліотека p5.js використовує метод removeAttribute() .

Цікавим методом p5.js є метод position() , який встановлює положення елементів. Застосуємо цей метод для елемента <canvas>.

Створивши полотно за допомогою методу createCanvas(), збережемо покликання на нього з ім’ям cnv та змінимо положення полотна на вебсторінці застосунку:

function setup() {
  let cnv = createCanvas(200, 200);
  cnv.position(100, 100);
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

Застосувавши метод position(100, 100) для cnv, позиція лівого верхнього кута полотна зміниться на 100 пікселів праворуч від лівої межі та на 100 пікселів униз від верхньої межі області перегляду, в чому можна переконатися у вікні вебпереглядача після запуску застосунку.

Якщо аргументи для методу position() не вказані, метод повертає об’єкт, що містить координати x та y позиції елемента.

Продемонструємо це для елемента <canvas>:

function setup() {
  let cnv = createCanvas(200, 200);
  cnv.position(10, 10); (1)
  let coords = cnv.position(); (2)
  console.log(coords); (3)
  console.log("x:", coords.x); (4)
  console.log("y:", coords.y);
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}
1 Розміщуємо полотно в точці (10, 10). Координати позиції елемента вказуються відносно точки (0, 0) вікна перегляду.
2 Отримуємо координати полотна (метод position() використовуємо без аргументів) і зберігаємо результат з ім’ям coords.
3 Друкуємо в консолі об’єкт coords.
4 Отримуємо доступ до координат x та y об’єкта coords, використовуючи крапкову нотацію.

В консолі результат матиме вигляд:

{x: 10, y: 10}
x: 10
y: 10

B.4. Створення і видалення елементів

У разі статичної вебсторінки вебпереглядач повністю формує DOM-дерево. Але частіше вебпереглядач має справу з відображенням даних, які прийшли з вебсервера як відповідь на запит користувача.

У такому разі виникає необхідність динамічно створювати чи видаляти елементи, змінювати їхні характеристики та встановлювати зв’язки між ними, інакше змінювати DOM.

Скористаємось зразком проєкту, щоб продемонструвати способи створення і видалення елементів в DOM.

Для видалення елемента з DOM-дерева використовується JavaScript-метод remove() .

Наприклад:

const h1 = document.querySelector("h1");
h1.remove();

Якщо виконати застосунок, у вікні перегляду вебпереглядача елемент <h1> буде відсутнім, тобто видалений з DOM. Якщо відкрити файл index.html - заголовок буде на своєму місці. Цей приклад ще раз демонструє відмінність між HTML і DOM вебсторінки.

Для створення елемента використовують createElement() - метод, який створює новий елемент із заданим тегом.

Наприклад, створимо елемент абзацу <p> з текстовим вмістом "Чудовий редактор!". Для цього метод createElement() необхідно застосувати для об’єкта document:

const p = document.createElement("p");
p.textContent = "Чудовий редактор!";
console.log(p);

У вебпереглядачі абзац з текстом відображатися не буде, але з консолі дізнаємось, що він існує:

<p>Чудовий редактор!</p>
Метод createElement() створює новий елемент, який існує сам по собі й не прив’язаний до жодного DOM-вузла, тому елемент не відображатиметься на вебсторінці. Однак, для нього можна встановлювати властивості, наповнювати його вмістом тощо.

Щоб створений DOM-вузол зайняв своє місце у DOM-дереві та відобразився на вебсторінці, використовують методи, які вставляють вузли-елементи чи текстові вузли в DOM-дерево:

  • append() - наприкінці списку дочірніх вузлів для батьківського вузла;

  • prepend() - перед першим дочірнім елементом.

Створимо кілька вузлів і додамо їх в DOM-дерево:

const div = document.createElement("div"); (1)
div.innerHTML = "<p>Гарна робота!</p>";

const p = document.createElement('p'); (2)
p.textContent = "Чудовий редактор!";
div.prepend(p);

const main = document.querySelector('main'); (3)
main.append(div, "😉");

console.log(main);

Проаналізуємо наш код:

1 Створюємо вузол-елемент <div> з ім’ям div і використовуємо властивість innerHTML для додавання HTML-розмітки.
2 Створюємо вузол-елемент <p> з ім’ям p і використовуємо властивість textContent для додавання текстового вмісту. Вставляємо <p> перед усіма дочірніми елементами <div>.
3 Вибираємо елемент <main> і приєднуємо вузол-елемент <div> та смайлик як текстовий вузол наприкінці усіх дочірніх елементів <main>. При додаванні кількох елементів, записуємо їх через кому.

У консолі (також у вебпереглядачі) отримаємо такий результат:

<main id="content">
  <h1>JavaScript, p5.js і DOM</h1>
  <div>…</div>
  <div class="sample">…</div>
  <div>…</div>
  <canvas id="defaultCanvas0" class="p5Canvas" width="200" height="200" style="width: 200px; height: 200px;"></canvas>
  <div>
    <p>Чудовий редактор!</p>
    <p>Гарна робота!</p>
  </div>
  😉
</main>

Бібліотека p5.js містить власні інструменти для створення конкретних HTML-елементів.

Наприклад, використовуючи функція createP() можна створити елемент абзацу. Як аргумент, метод отримає рядок, що може містити як текст, так і HTML:

function setup() {
  createCanvas(200, 200);
  createP("Вебсторінку, на якій виконується <i>застосунок</i>, можна розглядати як <b>ескіз</b>.");
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

Після запуску застосунку новий елемент абзацу із вказаним вмістом з’явиться під полотном застосунку - перед тегом </body> вебсторінки index.html.

Якщо необхідно створити на вебсторінці елемент зображення, застосовують функцію createImg() , яка створює елемент <img> у DOM з атрибутами src та alt.

Наприклад:

let img = createImg("https://res.cloudinary.com/gtstack/image/upload/v1655927356/p5js/p5jslogo_wls3fj.png", "p5.js"); (1)
img.size(60, 60); (2)
console.log(img.size()); (3)
1 Створюємо елемент зображення <img> з URL-адресою зображення для атрибута src й текстом "p5.js" для атрибута alt.
2 Використовуємо метод size() для встановлення розмірів елемента - ширини й висоти зображення на вебсторінці.
3 Якщо аргументи для size() не вказані, метод повертає ширину та висоту елемента. У цьому разі метод без аргументів друкує в консолі об’єкт зі значеннями ширини й висоти зображення, доступ до яких можна отримати через крапкову нотацію.

Отже, отримуємо в консолі об’єкт зі значеннями розмірів зображення у пікселях:

{width: 60, height: 60}
Для елементів, які потрібно завантажувати, зокрема зображень, рекомендується викликати size() після того, як завантаження елемента завершиться.
У довідці по функціях p5.js, у розділі DOM представлені багато інших функцій для створення окремих елементів.

У бібліотеці p5.js існує універсальна функція для створення будь-яких HTML-елементів - createElement() .

Використаємо цю функцію, щоб створити елемент <small> і збережемо покликання на нього з ім’ям small:

let small = createElement('small', 'за підтримки <b>Processing Foundation</b>'); (1)
console.log(small.elt); (2)
1 Для функції createElement() вказуємо два аргументи: назву тега і його вміст.
2 За допомогою властивості elt отримуємо доступ до створеного елемента <small>, який зберігається у small.

У вебпереглядачі під полотном застосунку буде виведений дрібний текст за допомогою тега <small>, а у консолі надрукується значення вузла-елемента DOM:

<small>
за підтримки
<b>Processing Foundation</b>
</small>
Видалення елементів з DOM можна виконати за допомогою метода remove() бібліотеки p5.js.

B.5. Стилізація елементів

Використовуючи JavaScript можна керувати не лише вмістом, але й зовнішнім виглядом елементів.

Одним зі способів надання стильового оформлення елементам - приєднання стилів за допомогою атрибута style. Наприклад, встановимо колір Azure і курсивне накреслення для тексту абзацу, використовуючи атрибут style:

const sample = document.querySelector(".sample p");
sample.setAttribute("style", "color: #3a86ff; font-style: italic;");
console.log(sample);

В консолі отримаємо елемент абзацу із доданим атрибутом style

<p style="color: #3a86ff; font-style: italic;">Малюнок кола - зразок простого застосунку.</p>

а у вебпереглядачі - колірне оформлення абзацу із курсивним накресленням тексту відповідно.

Такий спосіб стилізації елементів має суттєвий недолік: якщо застосувати інший стиль до елемента абзацу за допомогою атрибуту style, попереднє значення стилю буде перезаписане новим.

Цікавимось

Способи приєднання CSS-стилів

Зазвичай CSS-правила записується в окремому файлі з розширенням .css або всередині тегу <style>, який міститься в <head>.

Використання атрибута style в HTML-тегах - це ще один спосіб написання CSS-правил, який називається вбудованим стилем.

Вбудовані стилі безпосередньо впливають на тег, у який вони записані.

Загалом використання вбудованих стилів не вважається найкращою практикою, однак бувають випадки, коли вбудовані стилі є правильним (або єдиним) вибором.

Отже, використаємо інший спосіб стильового оформлення елементів - властивість style, яка відкриває доступ до вбудованих стилів DOM-елемента:

const p = document.querySelector("p");
console.log(p.style);

В консолі ми отримаємо об’єкт CSSStyleDeclaration, який містить набір CSS-властивостей для елемента <p>:

CSSStyleDeclaration {accentColor: "", additiveSymbols: "", alignContent: "", alignItems: "", alignSelf: ""…}
Атрибут style - це текстовий рядок у HTML-розмітці, а властивість style - це об’єкт.

Щоб отримати доступ до значення конкретної властивості елемента, використовують крапкову нотацію і назву цієї властивості:

const p = document.querySelector("p");
console.log(p.style.color);

Оскільки для елемента <p> не призначено жодних стилів, в результаті звернення до властивості color ми отримуємо порожній рядок "". Зараз усі властивості об’єкта style для елемента <p> мають значення порожніх рядків.

Додамо до першого абзацу декілька стилів, використовуючи властивість style. Стилі встановлюватимуть колір color і внутрішні відступи padding:

const p = document.querySelector("p");
p.style.color = "#3a86ff";
p.style.padding = "15px";
console.log(p.style);

Результат можна переглянути у вебпереглядачі на сторінці застосунку, а в консолі переконатися, що властивості color і padding об’єкта style отримали значення:

...
color: "rgb(58, 134, 255)"
...
padding: "15px"
...

Кілька стилів для елемента <p> можна записати одним рядком за допомогою властивості cssText:

const p = document.querySelector("p");
p.style.cssText = "color: #3a86ff; padding: 15px";
console.log(p.style);

Цікавимось

Способи написання назв CSS-властивостей

Назви CSS-властивостей, які складаються з кількох слів, записуються в так званому kebab-case стилі - через дефіс.

Наприклад, font-size, background-color, margin-top, list-style-type тощо.

Щоб використовувати такі назви як властивості JavaScript-об’єктів, kebab-case замінюють на спосіб написання camelCase.

Наприклад, background-color перетворюється на backgroundColor, font-size - на fontSize, border-top-right-radius - на borderTopRightRadius і т. д.

Загалом для написання імен в коді, окрім вищезгаданих, існують й інші стилі написання: PascalCase і snake_case. За виглядом цих стилів легко здогадатися, які правила для написання імен використовує кожен з них.

Приклад доступу до властивості, назва якої складається з кількох слів:

const p = document.querySelector("p");
p.style.fontSize = "42px";
console.log(p.style);
Для видалення властивості елемента, значення для цієї властивості встановлюється рівним порожньому рядку.

Більш оптимальний підхід у роботі зі стилями - опис заздалегідь усіх потрібних стилів у CSS-класах і зберігання їх в окремому файлі з розширенням .css, а потім додавання, зміна чи видалення CSS-класів для будь-яких елементів, використовувати JavaScript.

Завдяки редактору p5.js Web Editor наш зразок проєкту вже містить створений файл стилів з назвою style.css.

А втім, можна створити власний CSS-файл і приєднати його до сторінки index.html, за аналогією з файлом style.css.

Створення файлу в p5.js Web Editor
Онлайн-редактор p5.js Web Editor: створення файлу

У файлі style.css вже описаний стандартний набір стилів для основних елементів <html>, <body і <canvas> вебсторінки застосунку.

html, body {
  margin: 0;
  padding: 0;
}
canvas {
  display: block;
}

Додамо у правило для елемента <canvas>, який утворює полотно застосунку, опис стилю background-color: #fefae0; для встановлення кольору Cornsilk для тла полотна:

canvas {
  display: block;
  background-color: #fefae0; // Cornsilk
}

Щоб колір тла застосунку змінився із сірого на Cornsilk, необхідно також у файлі sketch.js закоментувати рядок з функцією background(220);, яка постійно замальовує новий колір тла:

function draw() {
  // background(220);
  ...
}

Зберігши зміни у файлах, у вебпереглядачі отримаємо бажаний результат.

Додамо у файл style.css опис ще двох CSS-класів:

.features {
  color: #ef476f; // Paradise Pink
}
.sample {
  color: #06d6a0; // Caribbean Green
}

Для роботи з CSS-класами DOM-елементів використовується властивість classList, яка повертає об’єкт - псевдомасив DOMTokenList, що містить усі класи вузла-елемента.

Отримаємо список CSS-класів для кожного з <div>-елементів вебсторінки ескізу:

const divs = document.querySelectorAll("div");
for(const div of divs) {
  console.log(div.classList);
}

Отримаємо три об’єкти DOMTokenList для кожного <div> відповідно.

Оскільки CSS-клас .sample вже застосований до другого <div> вебсторінки ескізу index.html, інформація про цей CSS-клас міститься в другому DOMTokenList:

DOMTokenList {0: "sample", entries: ƒ entries(), keys: ƒ keys(), values: ƒ values(), forEach: ƒ forEach()…}
0: "sample"
entries: ƒ entries() {}
keys: ƒ keys() {}
values: ƒ values() {}
forEach: ƒ forEach() {}
length: 1
value: "sample"
add: ƒ add() {}
contains: ƒ contains() {}
item: ƒ item() {}
remove: ƒ remove() {}
replace: ƒ replace() {}
supports: ƒ supports() {}
toggle: ƒ toggle() {}
toString: ƒ toString() {}
<constructor>: "DOMTokenList"

Об’єкт classList має низку методів по роботі з CSS-класами.

Використаємо метод add() для додавання CSS-класу .features до інших елементів <div> вебсторінки ескізу і метод contains(), який перевіряє, чи є в елемента певний CSS-клас:

const divs = document.querySelectorAll("div"); (1)
for(const div of divs) { (2)
  if (!div.classList.contains("sample")) { (3)
    div.classList.add("features"); (4)
  }
  console.log(div);
}

Отже, поданий код можна пояснити так:

1 Шукаємо усі елементи <div> на вебсторінці застосунку.
2 Проходимо в циклі for..of по кожному елементу <div>.
3 Перевіряємо, чи поточний <div> не містить CSS-класу .sample.
4 Якщо відповідь так, не містить, додаємо до елемента CSS-клас .features.

У підсумку, в консолі надрукується три елементи <div> з CSS-класами

<div class="features">…</div>
<div class="sample">…</div>
<div class="features">…</div>

а у вебпереглядачі будемо спостерігати оформлення різними кольорами текстового вмісту елементів <div> залежно від застосованих CSS-класів.

Для видалення в елемента CSS-класів використовують метод remove() властивості classList.

Ще один метод об’єкта classList - toggle() - додає CSS-клас для елемента, якщо CSS-клас відсутній для цього елемента, інакше - видаляє CSS-клас.

Подивимось, як це відбувається на практиці, використавши код:

const divs = document.querySelectorAll("div");
for(const div of divs) {
  div.classList.toggle("sample");
  console.log(div);
}

Як бачимо, для елементів <div>, в яких не було CSS-класу .sample, він був доданий, а для елементів, де був встановлений CSS-клас .sample - видалений:

<div class="sample">…</div>
<div class="">…</div>
<div class="sample">…</div>

Чи можна додати/видалити більше одного CSS-класу? Так.

Наприклад:

const divs = document.querySelectorAll("div");
for(const div of divs) {
  div.classList.add("features", "sample");
  console.log(div);
}

При додаванні кількох CSS-класів до елемента <div>, якщо CSS-клас вже присутній в елементі, він не буде доданий повторно:

<div class="features sample">…</div>
<div class="sample features">…</div>
<div class="features sample">…</div>

Бібліотека p5.js також надає інструменти для стильового оформлення елементів. Використовуючи їх, можна легко змінити зовнішній вигляд вебсторінки, на якій виконується ескіз.

Отже, створимо кнопку, встановимо колір Beige для тла кнопки й змінимо позицію кнопки у вікні перегляду:

let button = createButton("Надіслати відгук");
button.style("background-color", "#efebce");
button.position(0, 460);

У коді ми використали:

  • createButton() - функція для створення кнопки;

  • style() - метод для встановлення CSS-правила (кольору тла) для кнопки;

  • position() - метод для розміщення кнопки у вікні перегляду.

Метод style() у цьому разі можна використати інакше:

let button = createButton("Надіслати відгук");
button.style("background-color: #efebce; color: red;"); (1)
console.log(button.style("height")); (2)
button.position(0, 460);
1 Використовуємо в методі style() один аргумент як рядок CSS-правил.
2 Коли метод style() приймає один аргумент, у цьому разі назву CSS-властивості height, отримуємо значення цієї CSS-властивості для кнопки.

Результат можна переглянути у вікні вебпереглядача, а у консолі прочитати значення висоти кнопки у пікселях: 21px.

Для роботи з CSS-класами бібліотека p5.js має у своєму арсеналі кілька методів:

  • class() - додає вказаний клас до елемента, а якщо аргумент - назва CSS-класу - не передається, метод повертає рядок, що містить поточний CSS-клас(и) елемента;

  • addClass() - додає вказаний CSS-клас до елемента;

  • removeClass() - видаляє вказаний CSS-клас з елемента;

  • hasClass() - перевіряє, чи вказаний CSS-клас уже встановлено для елемента;

  • toggleClass() - перемикає CSS-клас елемента.

Для створення CSS-класів метод class() варто застосовувати для елементів, у яких відсутні CSS-класи, оскільки він перезаписує в елементах наявні.

Поєднаємо методи для роботи з CSS-класами в одному коді, щоб проаналізувати як вони працюють:

function setup() {
  let cnv = createCanvas(200, 200);
  let showCSS = "show"; (1)
  // cnv.addClass(showCSS);
  if (cnv.hasClass(showCSS)) { (2)
     cnv.removeClass(showCSS);
     console.log(`CSS-клас ${showCSS} був видалений із полотна.`);
  } else { (3)
     cnv.addClass(showCSS);
     console.log(`CSS-клас ${showCSS} був доданий до полотна.`);
  }
  console.log("CSS-класи полотна:", cnv.class()); (4)
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}
1 Зберігаємо назву CSS-класу show з ім’я showCSS.
2 За допомогою метода hasClass() перевіряємо, чи має полотно CSS-клас, що зберігається в showCSS. Якщо так - видаляємо з полотна CSS-клас, що зберігається в showCSS, за допомогою метода removeClass() і друкуємо про це повідомлення в консолі.
3 Інакше - додаємо до полотна CSS-клас, що зберігається в showCSS, за допомогою метода addClass() і друкуємо про це повідомлення в консолі.
4 Друкуємо в консолі інформацію про поточні класи, які має полотно, використовуючи метод class().

Результати виконання застосунку можна переглянути в консолі:

CSS-клас show був доданий до полотна.
CSS-класи полотна: p5Canvas show

На початку полотно має єдиний CSS-клас: p5Canvas. Якщо розкоментувати рядок у коді вище, до елемента полотна за допомогою метода addClass() буде додано CSS-клас, що зберігається в showCSS і в консолі буде надрукована інформація про протилежну дію:

CSS-клас show був видалений із полотна.
CSS-класи полотна: p5Canvas

Результат вище, але з меншою кількістю рядків коду і без використання розгалуження, можна досягти за допомогою методу toggleClass():

function setup() {
  let cnv = createCanvas(200, 200);
  let showCSS = "show";
  // cnv.addClass(showCSS);
  cnv.toggleClass(showCSS);
  console.log("CSS-класи полотна:", cnv.class());
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

Цей метод працює як перемикач CSS-класів: якщо в елемента CSS-клас відсутній, метод його додає і навпаки.

B.6. Навігація по елементах

Методи пошуку - не єдиний спосіб отримати потрібні елементи чи колекції елементів. Дістатися до елемента можна за допомогою родинних зв’язків.

Використовуючи родинні зв’язки між вузлами-елементами можна отримати доступ до членів родини DOM-дерева.

Для доступу до дітей певного вузла у DOM-дереві використовують властивість children , яка повертає лише дочірні елементи цього вузла у вигляді колекції HTMLCollection.

Наприклад:

const div = document.querySelector("div");
console.log(div.children);

У результаті отримаємо дітей першого <div>-елемента:

HTMLCollection {0: HTMLParagraphElement, 1: HTMLParagraphElement, 2: HTMLParagraphElement, length: 3, item: ƒ item()…}
0: <p>p5.js - бібліотека JavaScript для креативного кодування.</p>
1: <p>Безплатна, з відкритим початковим кодом.</p>
2: <p>Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.</p>
length: 3
item: ƒ item() {}
namedItem: ƒ namedItem() {}
<constructor>: "HTMLCollection"
childNodes - аналогічна children властивість, яка повертає колекцію типу NodeList, що містить дочірні вузли-елементи, текстові вузли та вузли-коментарі.

Щоб отримати текстові значення дочірніх елементів, можна скористатися циклом for..of

const div = document.querySelector("div");
for(const child of div.children) {
  console.log(child.textContent);
}

і властивістю textContent:

p5.js - бібліотека JavaScript для креативного кодування.
Безплатна, з відкритим початковим кодом.
Використовуючи метафору ескізу, p5.js має повний набір функцій малювання.

Для навігації в DOM-дереві можна використати також parentElement і parentNode - властивості, які повертають батька вузла-елемента або null, якщо вузол немає батька.

Наприклад:

const heading = document.querySelector("h1");
console.log(heading.parentElement);
console.log(heading.parentElement.parentElement);

У результаті отримаємо <main> - батька <h1>-елемента і <body> - батька <main>-елемента:

<main id="content">…</main>
<body>…</body>
Існують й інші властивості, які допомагають здійснювати навігацію по вузлам-елементам DOM-дерева: firstElementChild , lastElementChild , previousElementSibling , lastElementChild , previousSibling , nextSibling .

Ідею родинних зв’язків бібліотека p5.js реалізує через власні вбудовані методи: parent() і child() .

Щоб подивитись на батьків чи дітей певного вузла-елемента, вищезгадані методи використовують без аргументів.

Наприклад, визначимо родинні зв’язки для елемента <main>, який має id зі значенням content:

const main = select("#content");
console.log(main.parent());
console.log(main.child());

У цьому разі вузол-елемент main має батька <body> і колекцію NodeList дочірніх вузлів: <h1>, три <div>, <canvas> і текстові вузли.

У попередніх прикладах, використовуючи метод position(), ми встановлювали положення для будь-яких елементів на вебсторінці ескізу. Використовуючи методи child() і parent(), можна приєднувати елемент до вказаного батьківського елемента.

Проілюструємо родинні зв’язки на прикладі використання метода child() (це можна зробити й за допомогою parent()):

let div = createDiv("це батько");
let p = createP("це дитина");
let span = createSpan("це нащадок");
p.child(span); (1)
div.child(p); (2)
console.log(div.elt);
1 За допомогою метода child() елемент <span> розміщуємо в елементі <p>. У цьому разі <p> - батьківський елемент для дочірнього елемента <span>.
2 За допомогою метода child() елемент <p> розміщуємо в елементі <div>. У цьому разі <div> - батьківський елемент для дочірнього елемента <p>.

Окрім того, елемент <span> є нащадком для <div>, у чому можна переконатися в консолі:

<div>
  це батько
  <p>
    це дитина
    <span>це нащадок</span>
  </p>
</div>

У підсумку, наведемо ще один приклад приєднання елементів:

function setup() {
  let cnv = createCanvas(200, 200);
  let div = select("div");
  div.child(cnv);
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

У цьому разі елемент полотна <canvas> був приєднаний до першого елемента <div> вебсторінки ескізу, в чому можна переконатися у вікні вебпереглядача.

B.7. Події

Вебзастосунки проєктуються у такий спосіб, що вони завжди очікують взаємодії з користувачем, тобто чекають настання певних подій, як-от натискання кнопки, виділення тексту на вебсторінці й т. д., а потім реагують на них.

Кожна така подія має свій тип (ім’я), наприклад, mousemove - користувач перемістив вказівник миші, click - користувач натиснув один раз ліву кнопку миші, keydown/keyup - користувач натиснув/відпустив клавішу на клавіатурі тощо.

Щоб реагувати на різні типи подій у вебпереглядачі, використовують функції, які називаються обробниками подій.

Отже, як і раніше, скористаємось зразком проєкту, щоб продемонструвати подійно-орієнтований підхід у взаємодії користувача з вебсторінкою.

Розглянемо ситуацію, в якій вебпереглядач друкує в консолі сповіщення про те, що відбулася подія - натискання лівої кнопки миші на певному елементі.

Для цього використаємо наступний JavaScript-код

const h1 = document.querySelector("h1"); (1)
h1.addEventListener("click", function() { (2)
  console.log("Ви натиснули на h1!");
});

і проаналізуємо кроки його виконання:

1 Шукаємо на вебсторінці перший елемент заголовка <h1> і зберігаємо покликання на нього з ім’ям h1.
2 За допомогою метода addEventListener() реєструємо у вебпереглядачі для h1 функцію обробника події типу click (натискання лівої кнопки миші), яка викликається, коли подія вказаного типу настає.
Метод addEventListener() ще називають слухачем подій.

Функцію обробника події можна також записати у формі стрілочної функції:

h1.addEventListener("click", () => {
  console.log("Ви натиснули на h1!");
});

У підсумку, в консолі буде друкуватися повідомлення

Ви натиснули на h1!

разом зі значенням разів натискань лівої кнопки миші на заголовку <h1>.

Розглянемо приклад використання іншого типу події - copy:

const h1 = document.querySelector("h1");
h1.addEventListener("copy", function() {
  console.log("Ви скопіювали заголовок h1!");
});

Коли у вебпереглядачі скопіювати текст заголовка JavaScript, p5.js і DOM, настане подія copy і слухач подій addEventListener(), який зареєстрований у вебпереглядачі для h1, викличе функцію обробника події copy:

Ви скопіювали заголовок h1!
Більше детально про інші типи подій читайте у довідкових матеріалах сайту MDN Web Docs.

Тепер розглянемо приклад, коли необхідно відстежувати події на кількох елементах:

const paragraphs = document.querySelectorAll("p"); (1)
paragraphs.forEach((paragraph) => { (2)
  paragraph.addEventListener("click", function() { (3)
    console.log(paragraph.textContent); (4)
  });
});

Проаналізуємо наведений код:

1 Шукаємо на вебсторінці усі елементи абзаців <p> і зберігаємо покликання на колекцію знайдених елементів з ім’ям paragraphs.
2 Проходимо по колекції за допомогою методу forEach.
3 Для кожного абзацу із колекції реєструємо функцію обробника події click.
4 При події click на кожному абзаців, функція обробника події друкує в консолі текстовий вміст поточного абзацу.

Якщо після запуску застосунку натискати лівою кнопкою миші на рядки абзаців у вебпереглядачі, в консолі будуть з’являтись текстові значення цих абзаців.

Коли відбувається подія натискання абзацу, вебпереглядач створює об’єкт події, в який записує деталі події та передає його як аргумент функції обробника події.

Змінимо наш попередній код, врахувавши об’єкт події, для якого оберемо ім’я event (англ. event - подія), хоча можна обрати й іншу назву:

const paragraphs = document.querySelectorAll("p");
paragraphs.forEach((paragraph) => {
  paragraph.addEventListener("click", function(event) {
    console.log(event);
  });
});

Тепер, коли запустити застосунок і натиснути, наприклад, на абзаці Малюнок кола - зразок простого застосунку., в консолі отримаємо об’єкт події PointerEvent, збережений з ім’ям event.

Якщо звернутися до властивості target об’єкта event, можна отримати ціль події - об’єкт, для якого ця подія настала:

...
console.log(event.target);
...
Найглибший елемент, який викликає подію, називається цільовим елементом і він доступний через event.target.

У підсумку, застосуємо стильове оформлення кольором Tiffany Blue до тих абзаців, які натискаємо:

const paragraphs = document.querySelectorAll("p");
paragraphs.forEach((paragraph) => {
  paragraph.addEventListener("click", function(event) {
    event.target.style.backgroundColor = "#2ec4b6";
  });
});

Тепер, натискаючи на абзацах тексту, вони виокремлюються кольором, а елементи <p> в HTML-розмітці динамічно отримують вбудовані стилі у вигляді style="background-color: rgb(46, 196, 182);".

Розглянемо ще один приклад, який ілюструє спосіб роботи з DOM, що має назву делегування подій.

Для першого елемента <div> на вебсторінці за допомогою метода addEventListener() зареєструємо функцію обробника події click, яка у вебпереглядачі навколо цього елемента <div>, в разі натискання по ньому, створить межу у вигляді червоної штрихової лінії:

const divFirst = document.querySelector("div");
divFirst.addEventListener("click", function(event) {
  event.target.style.border = "1px dashed red";
  console.log(event.target);
});

Виконавши застосунок, і натискаючи в області першого елемента <div>, у вебпереглядачі та в консолі з’явиться наступний результат:

<p style="border: 1px dashed red;">p5.js - бібліотека JavaScript для креативного кодування.</p>
<div style="border: 1px dashed red;">…</div>

Як бачимо, обробник події, зареєстрований для <div>, викликається, якщо навіть натискати на будь-якому з елементів <p>, які лежать у <div>.

У цьому разі використовують делегування подій, алгоритм якого можна описати так:

  1. Зареєструвати функцію обробника події для батьківського елемента.

  2. У функції обробнику події перевірити наявність цільового елемента за допомогою event.target.

  3. Якщо подія виникла на потрібному елементі, то використовувати його.

У коді цей алгоритм може бути записаний так:

const main = document.querySelector("main");
main.addEventListener("click", function(event) { (1)
  if (event.target.tagName == "DIV") { (2)
    event.target.style.border = "1px dashed red"; (3)
    console.log("target", event.target, "- елемент, який ініціював подію");
  } else {
    console.log("currentTarget", event.currentTarget, "- елемент, до якого приєднано обробник події"); (4)
  }
});
1 Реєструємо функцію обробника події click для батьківського елемента <main>.
2 Перевіряємо за допомогою event.target.tagName чи цільовий елемент, на якому натискаємо, має тег <div>.
3 Якщо це <div>, встановлюємо для нього стильове оформлення у вигляді межі та друкуємо елемент, який викликав подію, тобто <div>.
4 Інакше - друкуємо значення властивості event.currentTarget.

В залежності від місця натискань у вебпереглядачі, в консолі отримуємо:

currentTarget <main id="content">…</main> - елемент, до якого приєднано обробник події
target <div style="border: 1px dashed red;">…</div> - елемент, який ініціював подію

Пояснимо отримані результати. Якщо натискати лівою кнопкою миші на вебсторінці застосунку в області першого елемента <div>, в консолі буде друкуватись значення властивості target - елемент <div>, який ініціював подію.

Натискання лівої кнопки миші в інших місцях області перегляду застосунку у вебпереглядачі, друкує значення властивості currentTarget - елемент <main>, до якого приєднано обробник події click.

Ідея підходу делегування події в тому, що якщо ми маємо багато елементів, події на яких потрібно обробляти подібно, то замість того, щоб призначати обробник індивідуально кожному елементу, ми ставимо один обробник на їхнього спільного батька.

У цьому разі можна отримати цільовий елемент event.target, зрозуміти на якому саме нащадку сталася подія та обробити її.

Бібліотека p5.js також має низку функцій, які можна використовувати для приєднання обробників подій до елементів. Такі функції бібліотеки є слухачами різних подій, за аналогією з методом addEventListener() у JavaScript.

Одна із таких функцій - mousePressed() - викликається один раз після кожного натискання будь-якої кнопки миші на елементі.

Щоб продемонструвати роботу цієї функції, змінимо код у файлі sketch.js:

let d; (1)
function setup() {
  createCanvas(200, 200);
  d = 75; (2)
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, d); (4)
}

function mousePressed() { (3)
  d = d + 5;
}
1 Описуємо глобальну змінну d, яка буде вказувати на значення діаметра кола.
2 Присвоюємо початкове значення 75 пікселів для d.
3 Описуємо функцію mousePressed(), яка щоразу при натисканні будь-якою кнопкою у вікні перегляду застосунку буде запускатися і збільшувати значення діаметра кола на 5 пікселів.
4 Малюємо коло із поточним значенням діаметра d.

Після запуску застосунку, натискання будь-якою кнопкою миші у будь-якому місці вікна перегляду буде збільшувати діаметр кола.

У цьому разі функція mousePressed() виконує ролі як слухача події (натискання кнопок миші), так і обробника події (у тілі функції виконується збільшення значення діаметра кола).

Оголосимо ще дві глобальні змінні: cnv - міститиме покликання на полотно застосунку і g - міститиме покликання на значення кольору для полотна:

let d, g, cnv;
function setup() {
  cnv = createCanvas(200, 200);
  cnv.mousePressed(changeColor); (3)
  d = 75;
  g = 220; (1)
}

function draw() {
  background(g); (4)
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, d);
}

function mousePressed() {
  d = d + 5;
}

function changeColor() { (2)
  g = random(0, 255);
}
1 Присвоюємо початкове значення кольору для полотна з ім’ям g.
2 Оголошуємо функцію з назвою changeColor(), яка за допомогою вбудованої функції random() буде надавати випадкове значення кольору і зберігати його з ім’ям g.
3 Функція mousePressed(), яка є слухачем події натискання кнопки миші на елементі полотна cnv, отримує як аргумент функцію changeColor(), яка є обробником цієї події.
4 Колір полотна змінюється відповідно значення g.

Коли запустити застосунок, щоразове натискання кнопки миші у будь-якому місці вікна перегляду буде збільшувати розмір зафарбованого кола, а при натисканні на полотні - ще й змінювати колір полотна.

Розглянемо ще кілька методів, які можна приєднати як слухачі події переміщення вказівника миші:

  • mouseOver() - викликається один раз після кожного переміщення вказівника миші до елемента.

  • mouseOut() - викликається один раз після кожного переміщення вказівника миші від елемента.

Щоб подивитись на ці методи в дії, створимо застосунок для вивчення назв об’єктів англійською.

Ідея наступна: при наведенні вказівника миші на певне зображення буде з’являтися його назва і водночас зображення будe збільшуватись у своїх розмірах.

Спочатку у застосунку буде один об’єкт - одне зображення. Отже, напишемо код, щоб реалізувати нашу ідею:

function setup() {
  createCanvas(200, 200);
  let img = createImg( (1)
    "https://res.cloudinary.com/gtstack/image/upload/v1656015129/p5js/amanita_rxsrqo.png",
    "amanita"
  );
  img.size(50, 50); (2)
  img.position(random(150), random(250, 400)); (3)

  let amanita = select("img"); (4)
  amanita.mouseOver(selectObj); (5)
  amanita.mouseOut(unselectObj); (6)
}

function draw() {
  background("#5C374C"); // Dark Byzantium
}

function selectObj() { (7)
  this.size(60, 60);
  let altText = this.attribute("alt");
  this.attribute("title", altText);
}

function unselectObj() { (8)
  this.size(50, 50);
}
1 Створюємо за допомогою функції createImg() елемент зображення <img> з двома атрибутами src (URL-адреса файлу, що містить зображення) і alt (альтернативний текст для зображення) і зберігаємо з ім’ям img.
2 За допомогою метода size() встановлюємо для img розміри, тобто надаємо значення 50 пікселів атрибутам width і height.
3 За допомогою метода position() розміщуємо зображення img у точці з випадковими значеннями координат, які одержуються з функції random(). Аргументи для функції random() обрані таким способом, щоб випадкова поява зображення була над полотном застосунку.
4 Вибираємо перший і єдиний елемент зображення <img> на вебсторінці застосунку за допомогою функції select() і зберігаємо з ім’ям amanita.
5 Приєднуємо до amanita функцію mouseOver() - слухач події наведення вказівника миші на зображення, яка отримує як аргумент користувацьку функцію selectObj(), що є обробником вказаної події.
6 Приєднуємо до amanita функцію mouseOut() - слухач події відведення вказівника миші від зображення, яка отримує як аргумент користувацьку функцію unselectObj(), що є обробником вказаної події.
7 Описуємо користувацьку функцію selectObj(), яка буде обробником події наведення вказівника миші на зображення. У тілі функції використовуємо зарезервоване слово this, яке вказує на поточний об’єкт - amanita, для якого викликається користувацька функція. Далі, за допомогою метода size() збільшуємо розміри зображення. Потім, використовуючи метод attribute(), отримуємо значення атрибута alt і зберігаємо з ім’ям altText та використовуємо altText як значення для нового атрибута title для amanita. Завдяки title над зображенням буде з’являтись назва.
8 Описуємо користувацьку функцію unselectObj(), яка буде обробником події відведення вказівника миші від зображення. У тілі функції повертаємо розміри зображення до значень, встановлених у пункті 2.

Який результат отримаємо при запуску застосунку?

У випадковому місці над тлом полотна кольору Dark Byzantium з’являється наше зображення.

Події наведення/відведення вказівника миші для одного об’єкта
Події наведення/відведення вказівника миші для одного об’єкта

При наведенні вказівника миші на зображення - зображення збільшується у розмірах і над ним виникає текст з назвою, що спливає. Коли вказівник із зображення відводимо, розміри зображення повертаються до попередніх значень і текстовий напис зникає.

Ускладнимо наш застосунок у разі, коли є кілька зображень і при наведенні на одному із зображень це зображення зникає, а інші зображення наслідують поведінку як у попередньому прикладі.

Розглянемо код, що реалізує наш задум:

const fruitsImages = { (1)
  apple:
    "https://res.cloudinary.com/gtstack/image/upload/v1656012839/p5js/apple_xwllwv.png",
  banana:
    "https://res.cloudinary.com/gtstack/image/upload/v1656012839/p5js/banana_gf06eh.png",
  pear:
    "https://res.cloudinary.com/gtstack/image/upload/v1656014918/p5js/pear_yy7wsf.png",
  amanita:
    "https://res.cloudinary.com/gtstack/image/upload/v1656015129/p5js/amanita_rxsrqo.png",
};

function setup() {
  createCanvas(200, 200);
  for (const fruit in fruitsImages) { (2)
    let img = createImg(fruitsImages[fruit], fruit); (3)
    img.size(50, 50);
    img.position(random(150), random(250, 400));
  }

  let images = selectAll("img"); (4)
  for (let i = 0; i < images.length; i++) { (5)
    images[i].mouseOver(selectObj);
    images[i].mouseOut(unselectObj);
  }
}

function draw() {
  background("#5C374C"); // Dark Byzantium
}

function selectObj() {
  this.size(60, 60);
  let altText = this.attribute("alt");
  this.attribute("title", altText);
  if (altText == "amanita") { (6)
    this.hide();
  }
}

function unselectObj() {
  this.size(50, 50);
}
1 Оголошуємо об’єкт fruitsImages, який міститиме пари ключ:значення, в яких ключами будуть назви, а значеннями - URL-адреси файлів зображень. Якщо треба, зображення можна завантажити в певний каталог проєкту і вказати відносні шляхи до файлів зображень.
2 За допомогою циклу for..in, який використовується для проходу через властивості об’єкта, створюємо елементи зображення <img> з атрибутом src, що містить URL-адреси файлів зображень, які отримуємо з об’єкта fruitsImages за допомогою fruitsImages[fruit], і атрибутом alt - значення назви беремо з fruit. Задаємо розмір зображення і випадкову позицію в межах полотна.
3 Шукаємо усі елементи <img> на вебсторінці застосунку і зберігаємо у масив з ім’ям images.
4 Використовуємо цикл for для проходження по кожному елементу масиву images, додаючи до елементів зображень слухачі подій mouseOver() і mouseOut(), аргументами для яких є обробники подій selectObj() і unselectObj() відповідно.
5 Якщо для елемента зображення встановлений атрибут альтернативного тексту alt зі значенням amanita, використовуємо зарезервоване слово this, що вказує на поточний об’єкт, і метод hide() , що приховує поточний елемент.
Події наведення/відведення вказівника миші для кількох об’єктів
Події наведення/відведення вказівника миші для кількох об’єктів

У підсумку, у нас вийшов простий інтерактивний застосунок для вивчення не лише назв об’єктів англійською, але й властивостей цих об’єктів.

Розглянемо ще два методи, які можна приєднати як слухачі події перетягування файлів за допомогою вказівника миші:

  • dragOver() - викликається один раз після кожного перетягування файлу над елементом.

  • dragLeave() - викликається один раз щоразу, коли файл, що перетягується, залишає область елемента.

Використовуючи вищезгадані методи, реалізуємо простий застосунок для завантаження файлу на вебсторінку застосунку та отримання інформації про файл: ім’я, тип тощо.

Розпочнемо із такого коду:

function setup() {
  cnv = createCanvas(200, 200);
  let upload = createP("відвантажити файл"); (1)
  upload.style("background-color", "#fefae0"); // Cornsilk
  upload.style("text-align", "center");
  upload.style("padding", "15px");
  upload.style("border", "2px dashed pink"); // Pink
  upload.size(140);

  upload.dragOver(over); (2)
  upload.dragLeave(outside); (3)
}

function draw() {
  background(220);
  fill("#2a9d8f");
  noStroke();
  circle(100, 100, 75);
}

function over() { (4)
  this.style("background-color", "#ffe5d9"); // Unbleached Silk
}

function outside() { (4)
  this.style("background-color", "#fefae0"); // Cornsilk
}
1 Створюємо елемент <p> з ім’ям upload і додаємо стильове оформлення: колір тла, вирівнювання тексту, внутрішні відступи, стиль межі та ширину в пікселях для елемента.
2 Приєднуємо до upload два методи dragOver() і dragLeave(), які є слухачами подій перетягування файлів на область елемента чи поза нею відповідно. Аргументами методів є користувацькі функції over() і outside() - обробники цих подій відповідно.
3 Описуємо функцію over() - обробник події перетягування файлу на область елемента. У тілі функції застосовується стиль для області елемента, як тільки функція буде викликана.
4 Описуємо функцію outside() - обробник події перетягування файлу від області елемента. У тілі функції застосовується стиль для області елемента, як тільки функція буде викликана.

Після запуску застосунку, якщо захопити вказівником миші будь-який файл і перетягнути на елемент <p>, оформлення елемента зміниться відповідно наших налаштувань, описаних в коді.

Коли відпустити вказівник миші у вікні вебпереглядача - файл буде відкритий у вкладці вебпереглядача або вебпереглядач запропонує зберегти його. Це типова поведінка вебпереглядача при перетягуванні файлів у його вікно.

Для нас цікава зовсім інша поведінка - відображення файлу на вебсторінці застосунку з інформацією про сам файл.

Використаємо для наших цілей метод drop() - слухач події завантаження файлу перетягуванням й відпускання на елементі.

function setup() {
  ...
  upload.dragOver(over);
  upload.dragLeave(outside);
  upload.drop(gotFile); (1)
}

function draw() {
  ...
}

function over() {
  ...
}

function outside() {
  ...
}

function gotFile(file) { (2)
  createP("Ім'я: " + file.name);
  createP("Обсяг у байтах: " + file.size);
  createP("Тип: " + file.subtype);
  createImg(file.data, file.name.slice(0, -4));
}
1 Для елемента з ім’ям upload приєднуємо метод drop() - слухач події завантаження файлу на елементі, який як аргумент отримує користувацьку функцію gotFile() - обробник цієї події.
2 Описуємо користувацьку функцію gotFile(), яка отримує об’єкт file класу p5.File , до властивостей якого ми звертаємось через крапкову нотацію, а самі значення використовуємо як аргументи для функцій створення елементів абзацу і зображення.
У нашому коді було використано JavaScript-метод slice(), який повертає частину рядка у вигляді нового рядка без зміни оригінального рядка. Наприклад, якщо до оригінального рядка "apple.png" застосувати slice(0, -4), то утвориться ще один новий рядок зі значенням "apple".

Тепер, якщо перетягувати файли на елемент upload і відпускати їх над ним, на вебсторінці застосунку один під одним будуть створюватись зображення із цих файлів й відображатися інформація про кожен файл.

Об’єкт file класу p5.File має низку властивостей:

  • type - зображення, текст тощо;

  • subtype - розширення файлу;

  • name - повне ім’я файлу;

  • size - обсяг файлу у байтах;

  • data - рядок URL-адреси, що містить дані зображення, текстовий вміст файлу або об’єкт, якщо файл має формат JSON.

Для відвантаження файлів на вебсторінку застосунку також можна використовувати функцію createFileInput() , яка створює елемент <input> у DOM типу file і дозволяє вибирати локальні файли для використання в ескізі.
Решту функцій, які використовуються для приєднання обробників подій для елементів, можна знайти на сторінці базового класу p5.Element , на основі якого створюються усі об’єкти ескізу.

Цікавимось

Кодування Base64

У нашому прикладі значення data, яке ми використовували як значення атрибута src для елемента <img> - це дані зображення, закодовані у кодуванні Base64.

Base64 - алгоритм кодування, який перетворює будь-які символи, двійкові дані та зображення чи звукові файли в рядок, який можна читати, зберігати або передавати мережею без втрати даних.

Закодовані дані зображення після перетворення у Base64 виглядають приблизно так:

"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAUFBQUFBQUGBgUICAcICAsKCQkKCxE..."

Зображення у кодуванні Base64 в основному використовуються для вбудовування даних зображення в інші формати, такі як HTML, CSS або JSON.

Включаючи дані зображення в HTML-документ, вебпереглядачу не потрібно робити додатковий вебзапит для отримання файлу, оскільки зображення вже вбудовано в HTML-документ.

Варто пам’ятати, що для великих за обсягом зображень подання зображення в Base64 утворює дуже довгий рядок.

Перегляньте результати перетворення ваших зображень у формат Base64 за допомогою Base64 Image Encoder .

B.8. Порівняльна таблиця методів/функцій JavaScript і p5.js

У довідці по функціях p5.js міститься окремий розділ , присвячений роботі з DOM.
Таблиця "Основні методи/функції JavaScript і p5.js для керування вебсторінкою ескізу"

Дія для elem

JavaScript

p5.js

Знайти перший

document.querySelector("elem");

select("elem");

Знайти усі

document.querySelectorAll("elem");

selectAll("elem");

Додати атрибут

elem.setAttribute("attr", "value");

elem.attribute("attr", "value");

Видалити атрибут

elem.removeAttribute("attr");

Отримати вміст

elem.innerHTML;

elem.html();

Перезаписати вміст

elem.innerHTML = "content";

elem.html("content");

Доповнити вміст

elem.innerHTML += "content";

elem.html("content", true);

Очистити вміст

elem.innerHTML = "";

elem.html("");

Створити із вмістом і вставити у документ перед </body>

let elem = document.createElement("elem"); elem.innerHTML = "content"; body.append(elem);

createElement("element", "content");

Стати дитиною для other

other.append(elem);

elem.parent(other);

other.child(elem);

Стати батьком для other

elem.append(other);

other.parent(elem);

elem.child(other);

Видалити

elem.remove();

Додати вбудований стиль

elem.setAttribute("style", "value");

elem.style("property: value;");

elem.style.property = "value";

elem.style("property", "value");

Отримати класи

elem.classList.value;

elem.class();

Перевірити клас

elem.classList.contains("class");

elem.hasClass("class");

Додати клас

elem.classList.add("class");

elem.addClass("class");

Видалити клас

elem.classList.remove("class");

elem.removeClass("class");

Перемкнути клас

elem.classList.toggle("class");

elem.toggleClass("class");

Додаток C: Черепашача графіка

У цьому розділі представлений огляд середовища JavaScript Turtle Graphics Library, яке використовує можливості p5.js і JavaScript для створення Черепашачої графіки (Turtle Graphics).

C.1. Передмова

JavaScript Turtle Graphics Library - це середовище програмування, до складу якого входять:

  • JavaScript-бібліотека TurtleGL.js, яка містить базовий набір інструментів для програмування Черепашачої 2D-графіки (Turtle Graphics) у вебпереглядачі, використовуючи об’єктоорієнтований підхід;

  • застосунок Інтерактивна мапа, який є зручним інструментом для створення прототипів зображень.

Мета створення середовища JavaScript Turtle Graphics Library - навчання основам програмування на прикладі роботи з Черепашачою 2D-графікою у вебпереглядачі, використовуючи мову програмування JavaScript і можливості бібліотеки p5.js.

Особливості використання середовища JavaScript Turtle Graphics Library у формі інтерактивного покрокового підручника .

C.2. Приєднання бібліотеки TurtleGL.js

Щоб використовувати бібліотеку TurtleGL.js, її спочатку необхідно приєднати у свій проєкт. Для цього оберіть один зі способів:

  1. В онлайн-редакторі p5.js Web Editor :

    1. відвантажити у каталог проєкту файл бібліотеки TurtleGL.js;

    2. приєднати файл бібліотеки TurtleGL.js у файлі index.html перед файлом ескізу sketch.js.

  2. У разі локальної розробки необхідно завантажити зразок проєкту p5.js, у який приєднати файл бібліотеки TurtleGL.js у файлі index.html перед файлом ескізу sketch.js.

  3. В онлайн-редакторі CodePen :

    1. перейти у вкладку JS;

    2. натиснути на зображення шестерні;

    3. у розділі Add External Scripts/Pens у рядку пошуку знайти й обрати p5.js;

    4. у розділі Add External Scripts/Pens натиснути на кнопку +add another resource і додати посилання на файл бібліотеки TurtleGL.js;

    5. зберегти зміни, натиснувши кнопку Save & Close (для зареєстрованих користувачів), інакше - кнопку Close.

  4. В онлайн-середовищі Replit :

    1. створити новий Repl на основі шаблону p5.js;

    2. відвантажити файл бібліотеки TurtleGL.js у створений Repl;

    3. приєднати файл бібліотеки у файлі index.html перед файлом ескізу script.js.

  5. В онлайн-середовищі OpenProcessing :

    1. у розділі FILES ескізу відвантажити у проєкт файл бібліотеки TurtleGL.js;

    2. перейти у розділ SKETCH ескізу та обрати режим (MODE) HTML/CSS/JS;

    3. приєднати відвантажений файл бібліотеки TurtleGL.js у файлі index.html перед файлом ескізу mySketch.js.

  6. Використати p5.js Widget Editor - простий онлайн-редактор, створений на основі p5.js-widget - інструменту для вбудовування на сторінки сайтів редактора для запуску і редагування ескізів p5.js. У цей редактор бібліотека TurtleGL.js вже інтегрована і готова до використання.

Для належної роботи застосунків, які запускаються локально, необхідно використовувати локальний вебсервер . Вебсервер запускається із каталогу, у який був розпакований завантажений архів зі зразком проєкту. У цьому разі, щоб переглянути свої ескізи, необхідно перейти у вебпереглядачі за адресою http://localhost:port/empty-example/index.html, де port - номер порту.

Бібліотека TurtleGL.js реалізована у вигляді єдиного файлу, який можна завантажити у розділі Початковий код бібліотеки TurtleGL.js.

Бібліотеку TurtleGL.js також можна використовувати у форматі ескізу (за потреби зробити FORK ескізу). У цьому разі приєднувати бібліотеку не потрібно.

C.3. Застосунок "Інтерактивна мапа"

Для зручності роботи із бібліотекою TurtleGL.js створений інтерактивний застосунок, за допомогою якого можна:

  • відстежувати, фіксувати на полотні та зберігати координати Черепашки у текстовий файл;

  • зберігати у файл полотно з малюнком у форматі .png;

  • змінювати властивості Черепашки/олівця (колір, форму, розмір, кутову орієнтацію Черепашки, товщину і колір олівця);

  • відвантажувати на полотно зображення у форматах PNG та JPG й регулювати прозорість зображень.

Застосунок Інтерактивна мапа опублікований:

C.4. Використання бібліотеки TurtleGL.js

C.4.1. Початкові налаштування

У блоці setup() записуємо функцію createCanvas(windowWidth, windowHeight) для створення полотна, де windowWidth і windowHeight - це системні змінні, що зберігають значення ширини і висоти внутрішнього вікна (тобто вікна перегляду, у якому вебпереглядач «малює» вебсторінку) відповідно, і викликаємо myCode() - це назва блоку, в якому записані інструкції для Черепашки.

sketch.js
let t;

function setup() {
  createCanvas(windowWidth, windowHeight);
  myCode();
}

Значення розмірів полотна, які за стандартним налаштуванням використовує функція createCanvas() (коли викликається без аргументів) - 200x200 пікселів. За потреби можна створити полотно будь-якого розміру, зазначивши у createCanvas() відповідні значення розмірів ширини й висоти.

Також на початку у коді оголошуємо ім’я для майбутньої Черепашки - t.

Ім’я для Черепашки можна обрати на свій задум.

Блок myCode() має таку структуру:

sketch.js
function myCode(){
  // інструкції для Черепашки
}
Усі інструкції для керування Черепашкою записуємо у блоці myCode(). Назву цього блоку можна змінити на свій задум.

Завжди першою інструкцією у блоці myCode() є інструкція зі створення об’єкта Черепашки з певним ім’ям (наприклад, t), оголошеним раніше:

sketch.js
function myCode(){
  t = new Turtle();
}

Отож, Черепашка отримала своє ім’я t. Усі наступні інструкції необхідно записувати у форматі t.інструкція.

C.4.2. Координатна сітка

Якщо необхідно створити координатну сітку на полотні, у блоці myCode() до Черепашки з ім’ям t застосовуємо інструкцію grid():

sketch.js
function myCode(){
  t = new Turtle();
  t.grid();
}

Інструкція grid() використовує три параметри із такими значеннями за стандартним налаштуванням:

  • крок сітки - 50 пікселів;

  • колір сітки - Platinum ;

  • колір осей - Cerulean .

Значення кольору записується як рядок в одному із форматів: "red" (назва), "#fdfd90" (шістнадцяткове значення), "rgb(11, 156, 78)" (значення червоної, зеленої, синьої складових), "rgba(45, 145, 67, 0.5)" (значення червоної, зеленої, синьої складових і прозорості).

Значення за стандартним налаштуванням використовуються тоді, коли grid() викликається без аргументів, як у прикладі вище. За потреби grid() можна викликати із користувацькими значеннями, зазначивши їх у дужках у вказаному порядку. Наприклад:

sketch.js
function myCode(){
  t = new Turtle();
  t.grid(30, "rgba(43, 41, 70, 0.5)", "#ff9800"); // Space cadet, Orange peel
}

Використовуючи інструкцію setStepGrid(step) перед малюванням сітки можна окремо встановити крок сітки step, а за допомогою інструкції getStepGrid() можна отримати поточне значення кроку сітки.

C.4.3. Відображення Черепашки, інформаційна панель та компас

За стандартним налаштуванням на полотні відображається інформаційна панель з даними про:

  • стан Черепашки (кутову орієнтацію, поточні координати);

  • стан олівця (на полотні чи піднятий);

  • координати вказівника миші.

А у правому нижньому куті полотна увімкнений компас, який вказує напрямок і значення кутової орієнтації Черепашки.

За відображення вищезгаданих елементів інтерфейсу відповідають інструкції, які записуються у блоці draw()

sketch.js
function draw() {
  t.place();
  t.compass();
  t.dashboard();
}

і використовуються для таких цілей:

  • place() - показати Черепашку на полотні у її поточних координатах (цю інструкцію рекомендується завжди використовувати);

  • compass() - показати компас;

  • dashboard() - показати інформаційну панель.

Також на екрані можна відобразити ім’я розробника: у блоці draw() розмістити інструкцію t.creator().

C.4.4. Черепашка і p5.js

На полотні поруч з Черепашкою можна створювати зображення, використовуючи інструменти бібліотеки p5.js. Виклики функцій p5.js для цих цілей записуються у тілі функцій draw() чи setup(). Зверніть увагу, що за стандартним налаштуванням початок координат (0, 0) розташований у лівому верхньому куті полотна.

Початок координат (0, 0) для Черепашки міститься в центрі полотна. За таких умов, одночасно відстежувати координати Черепашки та координати для побудови фігур складно.

У цьому разі код для побудови фігур за допомогою інструментів бібліотеки p5.js можна записувати у блоці myDraw() (за потреби назву блоку можна змінити на іншу) між коментарями // початок коду для фігур і // кінець коду для фігур, а виклик myDraw() помістити у блоці draw():

sketch.js
function draw() {
  myDraw();
  t.place();
  t.compass();
  t.dashboard();
}

function myDraw(){
  push();
  translate(width / 2, height / 2);
  scale(1, -1);

  // початок коду для фігур
  let [x, y] = t.getPosition();
  stroke(208, 85, 163); // Mulberry
  fill(255, 0);
  circle(int(x), int(y), 100);
  // кінець коду для фігур

  pop();
}

За допомогою виразу let [x, y] = t.getPosition(); можна отримати поточні координати Черепашки з ім’ям t в координатній сітці, початок координат якої міститься в центрі полотна, і, за потреби, використати для побудови зображень фігур та створення анімаційних ефектів за допомогою інструментів бібліотеки p5.js.

У разі використання функції background() із бібліотеки p5.js, варто правильно зазначити місце її виклику у коді, щоб уникнути небажаного зафарбовування всього полотна. Таким місцем розташування виклику функції background() може бути тіло функції myDraw().

Між коментарями // початок коду для сітки і тла полотна і // кінець коду для сітки і тла полотна також можна розмістити виклик інструкції для малювання сітки на полотні, коли необхідно одночасно використовувати й кольорове полотно, і координатну сітку.

sketch.js
function myDraw(){
  // початок коду для сітки і тла полотна
  background(70, 77, 119); // YInMn Blue
  t.grid(30, "rgba(43, 41, 70, 0.5)", "#ff9800"); // Space cadet, Orange peel
  // кінець коду для сітки і тла полотна

  push();
  translate(width / 2, height / 2);
  scale(1, -1);

  // початок коду для фігур
  let [x, y] = t.getPosition();
  stroke(208, 85, 163); // Mulberry
  fill(255, 0);
  circle(int(x), int(y), 100);
  // кінець коду для фігур

  pop();
}

Завдяки тому, що виклик функції background() у тілі функції myDraw() розміщений найпершим, зафарбовування полотна не буде впливати на результати викликів інших функцій для малювання фігур.

C.4.5. Багато Черепашок

Для створення двох (або більше) Черепашок, необхідно оголосити їхні імена та створити їх із цими іменами:

sketch.js
let t1, t2;

function setup() {
  createCanvas(windowWidth, windowHeight);
  myCode();
}

function myCode(){
  t1 = new Turtle();
  t2 = new Turtle();
}

Відображення на полотні створених Черепашок відбувається за допомогою виклику інструкцій place() для кожної із них у блоці draw():

sketch.js
function draw() {
  t1.place();
  t2.place();
}

Відповідно інструкції для різних Черепашок записуються у форматі t1.інструкція, t2.інструкція і т. д.

У режимі кількох Черепашок інформаційна панель і компас показують дані для одного екземпляра Черепашки, для якого вони викликаються.

C.4.6. Зміна розмірів вікна полотна/вебпереглядача

При зміні розмірів вікна полотна/вебпереглядача усі елементи інтерфейсу залишаються на своїх місцях і з’являються смуги прокручування. Щоб ці елементи налаштувалася відповідно до нових розмірів, необхідно оновити вебсторінку. За потреби, перед цим збережіть свій код.

Якщо ви працюєте з інтерактивною мапою, спочатку встановіть розміри вікна вебпереглядача, а потім оновіть вебсторінку, щоб елементи інтерфейсу налаштувались відповідно до нових розмірів.

C.5. Інструкції

C.5.1. Рух

Рух Черепашки відбувається без анімації, тобто виконуються усі записані інструкції і Черепашка відразу розташовується на полотні у точці з кінцевими координатами.

Після запуску Черепашка з’являється на полотні:

  • у точці з координатами (0, 0) (центр полотна);

  • дивиться праворуч;

  • має початкову кутову орієнтацію, що вимірюється у градусах, .

Таблиця "Рух Черепашки"

Інструкція

Опис

forward(distance)

Перемістити Черепашку вперед на відстань distance, у напрямку, куди дивиться Черепашка.

back(distance)

Перемістити Черепашку назад на відстань distance, у протилежний бік напрямку її руху.

goto(x, y)

Перемістити Черепашку в точку з координатами (x, y).

home()

Перемістити Черепашку в точку з координатами (0, 0) (центр полотна) і встановити початкову кутову орієнтацію.

left(angle)

Повернути Черепашку ліворуч на кут angle, де angle - число (ціле чи з рухомою крапкою). Якщо значення кута angle додатне, то Черепашка повертається на це значення кута проти годинникової стрілки (кутова орієнтація стає angle), якщо від’ємне - Черепашка повертається за годинниковою стрілкою на це значення кута (кутова орієнтація стає 360 - abs(angle), де abs(angle) - значення angle по модулю).

right(angle)

Повернути Черепашку праворуч на кут angle, де angle - число (ціле чи з рухомою крапкою). Якщо значення кута angle від’ємне, то Черепашка повертається на це значення кута проти годинникової стрілки (кутова орієнтація стає angle), якщо додатне - Черепашка повертається за годинниковою стрілкою на це значення кута (кутова орієнтація стає 360 - abs(angle), де abs(angle) - значення angle по модулю).

setHeading(angle)

Встановити кутову орієнтацію Черепашки на кут angle, де angle - число (ціле чи з рухомою крапкою). Якщо значення кута angle додатне, то Черепашка повертається на це значення кута проти годинникової стрілки (кутова орієнтація стає angle), якщо від’ємне - Черепашка повертається за годинниковою стрілкою на це значення кута (кутова орієнтація стає 360 - abs(angle), де abs(angle) - значення angle по модулю).

C.5.2. Черепашка

Таблиця "Стан Черепашки"

Інструкція

Опис

getPosition()

Отримати поточні координати Черепашки у вигляді списку [x, y].

getHeading()

Отримати поточну кутову орієнтацію Черепашки.

setShape(shape)

Встановити форму shape для Черепашки. shape може набувати значень: "blank" (невидимість), "circle", "square", "triangle". За стандартним налаштуванням форма Черепашки "triangle".

setShapeSize(s)

Встановити розмір s для форми Черепашки. За стандартним налаштуванням мінімальний розмір - 1, максимальне значення - 20.

showTurtle()

Зробити Черепашку видимою.

hideTurtle()

Зробити Черепашку невидимою.

clr()

Очистити полотно. Черепашка залишається у поточній точці, її кутова орієнтація зберігається.

C.5.3. Олівець

Якщо олівець на полотні, при переміщенні Черепашка малюватиме лінію.
Таблиця "Стан олівця"

Інструкція

Опис

penUp()

Підняти олівець.

penDown()

Опустити олівець на полотно. За стандартним налаштуванням олівець на полотні.

setPenSize(s)

Встановити у пікселях товщину лінії олівця на s. За стандартним налаштуванням товщина лінії олівця 1 піксель.

C.5.4. Колір

Значення кольору записується як рядок в одному із форматів: "red" (назва), "#fdfd90" (шістнадцяткове значення), "rgb(11, 156, 78)" (значення червоної, зеленої, синьої складових), "rgba(45, 145, 67, 0.5)" (значення червоної, зеленої, синьої складових і прозорості).
Таблиця "Використання кольору"

Інструкція

Опис

setColor(c, f)

Встановити колір c для олівця та колір заливки f для зафарбовування фігур. За стандартним налаштуванням колір олівця - чорний, колір заливки - прозорий. Для встановлення лише кольору олівця інструкція використовується з одним параметром: setColor(c) .

setBgColor(c)

Встановити колір c для тла полотна. За стандартним налаштуванням - білий.

setFillColor(f)

Встановити колір заливки f для зафарбовування фігур. За стандартним налаштуванням заливка прозора.

beginFill()

Увімкнути зафарбовування фігури поточним кольором заливки.

endFill()

Вимкнути зафарбовування фігури поточним кольором заливки.

C.5.5. Фігури

Таблиця "Побудова фігур"

Інструкція

Опис

oval(r, e)

Намалювати коло чи еліпс. Якщо параметри r і e набувають однакових значень, то Черепашка малює коло зі значенням радіуса, що дорівнює r. У разі різних значень r і e - отримуємо еліпс.

polygon([
[x1, y1],
[x2, y2],
[x3, y3],
…​])

Намалювати багатокутник за координатами вершин x1, y1, x2, y2, x3, y3, …​ (за годинниковою стрілкою).

C.5.6. Текст

Таблиця "Створення текстових написів"

Інструкція

Опис

write(
"txt",
{horizontal: A, vertical: B},
{font: C, size: D, style: E}
)

Написати текст "txt" у поточній позиції Черепашки.

Об’єкт {horizontal: A, vertical: B} використовується для вирівнювання тексту, де A може набувати значень LEFT, CENTER або RIGHT, а B - TOP, BOTTOM, CENTER або BASELINE.

Об’єкт {font: C, size: D, style: E} визначає шрифт, розмір тексту і стиль відповідно, а саме: C може набувати значень Arial, Times, Verdana тощо, D - ціле число, E може набувати значень NORMAL, ITALIC, BOLD, BOLDITALIC.

За стандартним налаштуванням текст є порожнім рядком і має такі параметри: вирівнювання {horizontal: CENTER, vertical: CENTER}, шрифту {font: "sans-serif", size: 12, style: NORMAL}.

C.6. Розробка

Для розробки середовища JavaScript Turtle Graphics Library використовувались мова програмування JavaScript та інструменти бібліотеки p5.js.

Розроблено з ❤️. Автор: Олександр Мізюк.

C.7. Початковий код бібліотеки TurtleGL.js

Початковий код бібліотеки TurtleGL.js зберігається в єдиному файлі, який можна завантажити тут .

C.8. Ліцензія

Використання бібліотеки TurtleGL.js визначається умовами ліцензії GNU General Public License (GPL) version 3.

Код бібліотеки TurtleGL.js можна вільно і безплатно копіювати, розповсюджувати й змінювати на свій задум.