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

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

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

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

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

  • створення творів цифрового мистецтва за допомогою 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

Мова 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 Конструктор класу отримує кільк