Як я малював картинки за допомогою JavaScript заради лайку від дівчини

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт, мене звати Юра. Я давно займаюсь фронтендом і люблю математику. В якийсь момент вирішив спробувати робити те, чого боюсь, і став малювати картинки за допомогою JavaScript і Canvas. А потім і робити всілякі анімації.

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

Автор гіфки, до речі, легендарна людина Dave Whyte. Якщо вам подобається такий арт, рекомендую підписатися. Він навіть робив титри для серіалу Queen’s Gambit.

Але повернемось до мене. Гіфка виглядала простою — щось збільшується, в кінці вона починається з початку, участь в процесі беруть лише чорно-білі квадрати. Що могло піти не так, та і яка тут може бути математика, окрім збільшення квадратів?

Та що тут робити

Спочатку я подумав: тут все двомірне, в 3D немає сенсу, просто зроблю на Canvas2D. Теоретично можна було і на CSS, але з нього складніше зробити якісне відео. А тут ще і явно був якийсь motion blur.

Якщо вам цікаво, код що намалює шахову дошку, виглядає десь так:

grid.forEach(cell=>{ // масив з шаховою сіткою
      context.save();
      context.translate(x,y);
      context.fillStyle = (cell.odd>0.5)?'#fff':'#000'
      context.fillRect(0,0,size,size);
      context.restore()
})

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

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

let CellSize = lerp(size,3*size,progress)
// lerp(a,b,t) = a*(1-t) +b*t - лінійна інтерполяція двох чисел

До речі, progress тут і далі — це зміна часу від 0 до 1, фактично, таймлайн анімації.
Радісний я записав відео з Canvas. І...

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

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

Тож я не зупинився. Подумав, це сталось через 2D-простір, і «несправжній» зум вирішив переробити в 3D-просторі. Щоб я фізично «летів» назустріч кубикам, а кубики не збільшувалися, а просто наближалися. Мабуть, це з 2D-зумом не працює так, як в реальному світі. А от 3D мене виручить — в цьому просторі я живу, його я точно знаю!

3D

Я використав three.js, бо це стандарт всього 3D на фронтенді зараз. Створив сцену, де камера летіла назустріч кубикам, та в процесі проявляв втричі менший грід. Той самий сетап, але тепер змінювалась лише камера. А кубики залишались на місці і проявлялись.

Звісно, і код був інший. Тепер кожен кубик був Mesh, і на three.js малювання виглядало так:

grid.forEach(cell=>{
	let cube = new Mesh(new BoxGeometry(1),new MeshBasicMaterial({color: g.odd?'white':'black'))
	cube.position.set(x,y,0) // кубики в площині xy, а камера рухається по осі z
	scene.add(cube)
})

Безпосередньо анімація наближення — один рядок з рухом камери. Бо, щоб щось стало в три рази більшим, треба лише скоротити відстань до об’єкта в три рази.

Тож це був мій код:

//  progress змінюється від 0 до 1, відстань до камери -- від 1 до 1/3
camera.position.z = lerp(1,1/3, progress);  

І я отримав результат 🥳

Той самий клятий результат!

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

f(x) = lerp(1,1/3, x) = 1 - x*2/3

Графік виглядав так:

Вона лінійна, і все, що важливе в ній — значення у точці 0 та 1, це відповідно 1 та ⅓.
І тут я подумав як «математик», що, мабуть, лінійний рух камери не описує плавність, яка мені треба. А також потрібні функції вищого ступеня, другого чи третього. І настала фаза відчаю, де я просто вгадував всі функціі, що задовольняли:

f(0) =1
f(1) = ⅓

Відчай

Я подумав про сімейство функцій f = ax^2+bx+c і їх виявилось нескінченно багато:

Безліч a та b, що підійдуть. На графіку це виглядає так:

Тож я спробував декілька варіантів. Зараз дивлюсь в старий код, там навіть був варіант
f(x) = (32/45)*x^2 — (8/5)*x + 1; який взагалі не задовольняє рівняння 😅 Ну як так, Юра з минулого?!

З деякими функціями анімація стала іншою, теж цікавою. Фактично це був easing.

f(x) = ⅔ *x^2 -4/3 *x+1

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

f(x) = ⅓ x^2-x+1

Це виглядає як майже ідеальний луп. І навіть зараз мені важко побачити різницю. Але уявіть, наскільки я був прискіпливим, що побачив, що вона не ідеальна — зум нерівномірний. Плюс я подумав, що ця функція спрацювала для збільшення 1 до 3, але тепер для збільшення 1 до 4 не підійде. Знов треба рівняння і вгадування? Тобто це якесь «несправжнє рішення» задачі.

Прийняття математика в собі

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

Бо швидкість це буквально похідна від функції руху. Функції руху, що я вгадую!
Тобто мені треба дописати ще одне рівняння:

f(0) = 1
f(1) = ⅓
f’(0) =3*f’(1) - найважливіше!

Чому саме 3 — тому, що в кінці мого циклу, в мене фактично відбувалася заміна об’єктів, що збільшилися в три рази, на їх копії з початку анімації. Тобто швидкість мала впасти в три рази, щоб залишитися для глядача сталою!

Бо. наприклад, похідна від початкової функції завжди стала:

f’(x) = (1-⅔ x)’ = -⅔ 

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

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

F(x) = a*e^(bx)

Бо в нього ще й цікаві властивості з похідними.

Тож я кинувся розв’язувати ці рівняння для нової функції.

Розвʼязок швидко знайшовся, і ця функція ідеально лягала в наші обмеження!

f(x) = e^(ln(1/3)*x)

Тут одразу легко побачити, як його масштабувати для будь-якого збільшення в N разів! Просто замінити 3 на N.

Додайте сюди властивість з мемів:

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

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

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

Може здатися, що ця функція тільки для цього і підійшла б. Але ці анімації побудовані на ідентичній функції:



Висновок

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

P.S. Не пам’ятаю, чи вона лайкнула цю гіфку, але я пригадую, що лайкнула інші. В ретроспективі я щасливий, що йшов в цих своїх ідеях, нехай і відчайдушно наївних, до кінця.

👍ПодобаєтьсяСподобалось58
До обраногоВ обраному11
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Вибачте, e^(ln(1/n)*x) це хіба не n^(-x)?

так! або ж (1/n)^x, все вірно, я просто чіпляюсь за експоненційну форму бо в неї ці гарні правила диференціації і я легше уявляю її графік одразу 😅

Пане, та ви ж робите шейдери! Дуже круто!
Спробуйте hlsl — є більш інтуітивні і прості інструменти для програмування відеокарти.

Дякую! Я писав на HLSL також, але ж для веба більше актуальні GLSL та WGSL, а я все ще все в браузері тестую 🥲. Дарую вам гарний маленький шейдер www.shadertoy.com/view/NsdXzf

Чудова стаття, дякую, розрадили!

Вітаю, ви для себе відкрили Generative Art. Рекомендую ознайомитись з такими речами, як p5.js: p5js.org та OpenProcessing: openprocessing.org/browse

дякую! це чудові ресурси! Обожнюю generative art, навіть колись продавав свій на fxhash.xyz, гляньте які форми він здобув там, в комерційному світі! А також тут www.artblocks.io/curated/collections

А де ви Tree.js забули ?

Класна стаття!

Юра, я все зрозумів, окрім природи процесу — чому при лінійному русі воно йде з паузою (як тільки зʼявляються маленькі квадратики), а при експоненциальному ні?

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

Якшо швидкість лінійна, то це наприклад значить швидкість зуму 1 метр в секунду. Це означає шо вона така на початку гіфки і в кінці.І от основний прикол, шо гіфка зациклена, і получається шо в кінці гіфки швидкість теж 1метр але масштаб сцени вже зменшився в 3 рази, і потім вона починається з початку, квадрати шо в кінці гіфки стали 0.33м, раптово знов стають 1метр, і тому візуально швидкість стрибає рівно в 3 рази. якшо формально залишається такою ж. Це трохи складне пояснення, але я не знаю як простіше) основна проблема шо іде підміна старих маленьких квадратів на нові з початку гіфки, і вони в різному масштабі. От =)

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

дякую!
Я тут подумав — може справа в перспективі? Я погуглив — вона начебто експоненційна.

ну начебто візуальний розмір об’єкта лінійно пропорційний відстані до нього, там шось типу SIZE = distance*tan(FOV), але може ти шось інше маєш на увазі.

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

ви праві, я помилився, залежність зворотньо лінійна, SIZE = 1/DISTANCE. Що дає той самий результат пропорцій, якшо відстань збільшується в N разів, так само зменшується і кутовий розмір обєкта. Бо по моїй формулі розмір зростав з відстанню, що на жаль не так =D

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

Моя любить таблички і графіки, побудовані на них 😊

Так а шо, немає ніякого css фреймворка з різними ізінгами? Ви молодець звичайно, але є враження ,що винайшли велосипед.

буду радий якщо підкажете такий CSS ізінг! ☺️

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

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

Chatgpt каже, що є такі плагіни: jquery easing plugin, GSAP, anime.js, cubic bezier generator, velocity.js, lottie

дякую, ось зручна візуальна сторінка де можна пограти з різними ізінгами gsap.com/docs/v3/Eases . Але як знайти серед них такий ізінг шоб його перша похідна зменшувалася рівно в 3 рази? Або в 4? 🤔

Дуже цікаво, прочитав з виглядом, наче я розумію всі ці формули 😁

Якщо не лайкнула — треба відправити демосцену з кубиками преса :)

Респект, класна стаття і стара тема демосцен яка кудись пропала. Ми таким дуже завзято розважались, свого часу на першому і другому курсі універу. Під DOS на BGI. Я потім ще на OpenGL робив модні під CRT монітори скрінсейвери. Нажаль воно десь загубилося.

Підтвержую, мені чоловік (він фронтендер) теж коли ми познайомились в 2018 показував якісь анімації яких він у Артюха навчився 😂 Так шо ваші анімаціі не тільки вам допомагають з дівчатами 😅

круто, вам дорога в демосцену.

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

заради лайку від дівчини

це так романтично

математично романтично

offtop
>>ТОВ АБРИС ПТ
>30.30 Виробництво повітряних і космічних літальних апаратів, супутнього устатковання

Сподіваюсь вони не на джаваскрипті

Телеметрію в консоль виводить? ))

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

спочатку прочитав «заради лайки від дівчини»

Підписатись на коментарі