Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
20.05.2023
Приховування обʼєктів в Three.js за допомогою Raycaster
Продовжую роботу над своєю картою - з MVP майже закінчив.
Додав до треку мітки зі світлинами. Як я писав раніше, самі мітки позиціюються завдяки CSS2DRenderer та працює це чудово Але, окрім того, треба приховувати мітки, закриті іншими предметами, інакше втрачається просторове уявлення.
Щоб визначити, чи є мітка видимою, чи її перекриває якась гора, використовується клас Raycaster. Він дуже просто працює: спочатку треба задати розташування камери та напрямок на мітку, після чого Raycaster вміє знайти точки перетину з обʼєктами, які йому передаєш. В моєму випадку такий обʼєкт тільки один — сама карта. Якщо точка перетину з картою ближче, ніж мітка — мітку не видно. От і все.
Перевірка робиться на render. До речі, ще я прибрав render з requestAnimationFrame та викликаю його в scene.onUpdate. Так суттєво знизилося завантаження процесора — а перемальовувати без оновлення сцени мені все одно не потрібно.
Все, що робить перевірка — це вішає на мітку CSS відповідний клас. Далі — робота звичайної CSS анімації. Єдиний нюанс, що той елемент, яким керує СSS2DRenderer, краще розглядати суто як контейнер та всі стилі додавати вже всередині — інакше можна натрапити на несподівані конфлікти.
19.05.2023
Blurhash для статичного сайту (та чому це погана ідея)
Blurhash - то класний механізм швидкого відображення попереднього перегляду для світлин. Він кодує світлину у стислий (декілька десятків символів) рядок, з якого потім малює розмите наближення. Ось, як приклад, мій аватар: UrLq3Nxv?w%N?JogIoWBRkWCRjjboMayoMoM.
Мені захотілося впровадити його на своєму сайті. Але є нюанс: для відображення такого хешу потрібний JavaScript та елемент <canvas> - або ж взагалі повноцінний додаток. Бо Blurhash не є загально прийнятним форматом файлів — це просто вхідний рядок для алгоритму
А я хотів повністю статичне рішення. Якщо розібратись, що може з себе ставити таке рішення, то це має бути або інше зображення, або деякий CSS, який можна створити з даного blurhash. Навіть знайшов бібліотеку blurhash-to-css, яка нібито саме те й робить.
Навіть написав код, який генерує blurhash зі світлини, а потім перетворює його на CSS. До речі, знайшов непогану бібліотеку Sharp для перетворення зображень на Node.js. Мені вона знадобилась, щоб дістати з файлу JPEG “сирі пікселі” - а також додати до них альфа-канал. Сайт бібліотеки навіть обіцяє, що вона швидша за ImageMagick, тож варто мати на увазі.
От тільки все це марнування часу. Виявилось, що все, що фактично робить blurhash-to-css - це генерує піксельну версію зображення з низькою роздільною здатністю, а потім її розмиває. Причому ця піксельна версія збирається з CSS linear-gradient, тому, на перший погляд, може здатись, що робиться щось розумніше. Але ні. До того ж згенерований CSS займає цілих 2.5 КБ! В такий розмір тривіально влізе проста маленька версія зображення в data URL. Якщо її ще й розмити, то, мабуть, краще вийде.
Це, нарешті, підводить мене до висновку, що Blurhash в HTML/CSS не потрібен. Головна перевага Blurhash - що це ASCII рядок, який легко включити в документ JSON. Але натомість потрібний особливий рушій, щоб його показати. На вебі так само можна WEBP закодувати в data URL, буде компактний ASCII рядок. Проте, як я розумію, Blurhash в першу чергу потрібний саме поза межами HTML/CSS, в додатках — де, може, не так легко показати зображення з рядка?
Одним словом, наступним разом поекспериментую з data URL.
PS: підхід попереднього завантаження компактної версії світлин називається LQIP.
18.05.2023
Реальна ймовірність збігу UUID
Вчора я трохи завтикав зі ймовірністю. А саме: вирішив, що ймовірність збігу за годину у 3600 разів більша за ймовірність збігу за секунду. Зрозуміти, що це неправильно можна, якщо спростити приклад до підкидання монети. Ймовірність “орла” - 1/2. Якщо підкинути монету двічі, то ймовірність аж ніяк не зросте удвічі та не становитимете 1!
Тож з’ясуймо, яка все ж таки ймовірність того збігу UUID.
-
Знаємо, що відбиток часу має 10 мільйонів значень на секунду. Але насправді нас цікавить не секунда, а проміжок часу між генераціями; припускаючи, що UUID може отримати будь-який таймстемп з проміжку. Наприклад, якщо ми генеруємо 1000 UUID на секунду, то число можливих значень таймстемпу - 10000. (До речі, ще одне припущення — що годинники всіх машин засинхронізовані.)
-
Припускаємо, що кожна машина генерує тільки унікальні таймстемпи. Тоді ймовірність збігу залежить від кількості машин. Це варіант парадокса днів народження, тільки замість дітей в нас “народжуються” таймстемпи, а замість року — множина можливих значень.
-
Тоді за узагальненою формулою можна підрахувати ймовірність збігу на одну генерацію:
(1 - Math.exp(-machineCount*(machineCount-1)/(2*valueCount)). -
Тепер щодо ймовірності на більшому проміжку часу. Не знайшов гарного джерела, тому розбирався сам. Легко обчислити ймовірність того, що подія відбудеться щоразу: для цього ймовірність береться в ступінь за кількістю повторень: 1/2, 1/4, 1/8 і так далі:
p^n. Так само можна обчислити ймовірність того, що подія ніколи не відбудеться:(1-p)^n. А нас цікавить ймовірність зворотного:1-(1-p)^n. Або, в нашому прикладі зі збігами:1 - Math.pow(1-singleCollisionProbability, repeatCount). -
Всю цю математику опублікував в gist. Якщо я не помиляюсь знову, то 2 машини, що генерують 1 UUID на секунду, досягнуть 50% ймовірності збігу за 3 місяці. А якщо вони генерують 1000 UUID на секунду — то вже за хвилину ймовірність досягне 99.7%. Отакої.
…Теорія ймовірності — найпарадоксальніша з галузей математики. Вона, безумовно, має найбільший вплив на наше життя — оцінки ймовірності визначають наші найважливіші життєві рішення. Тільки при цьому немає науки, де легше зробити помилку та бути 100% впевненим у своїй правоті. Дозвольте познайомити вас з парадоксом Монті Голла.
17.05.2023
Залежність UUID від MAC-адреси
Виявив істерично смішну дивину з UUID на AWS ECS. Дивився на UUID в базі та помітив, що всі вони мають однакове закінчення. Що трапилось? Розібрався.
-
UUID версії 1 складається з відбитка часу та імені машини. За специфікацією, імʼя машини — це її MAC адреса. Має бути зрозумілим, що в глобально унікальному ідентифікаторі саме імʼя машини прибирає можливість збігу; бо MAC адреса є унікальною (принаймні в теорії), та в межах машини досить легко не видавати один UUID двічі.
-
Власне, на Linux для того є сервіс uuidd. Він централізовано роздає UUID всім споживачам. Ось вам код, де він читає MAC адресу.
-
Але навіть якщо хтось генерує UUID сам, а не лізе в
uuidd, то теж зазвичай бере MAC адресу, наприклад, дуже популярний Go-пакет github.com/google/uuid. -
Памʼятаєте, як я казав, що MAC адреса теоретично унікальна? Ну, не знаю, як там у фізичних пристроях (на моєму Odroid U3 за замовчуванням вона призначається випадково при старті.) Але я точно знаю, що в Docker вам так не повезе. Docker просто будує MAC адресу з IP адреси. Ой… Адреси віртуальних мереж збігаються, та досить часто.
-
Так що якщо ви генеруєте UUID версії 1 з Docker, то шанси бути реально унікальним ID в них стрімко падають. Щоб цього уникнути, простіше за все не використати в якості Node ID MAC адресу, а взяти щось інше — бажано з зарезервованого діапазону MAC адрес. Та, також, краще не користуватись
uuiddв Докері зовсім — якщо є такий вибір. -
Але це ще не найсмішніше. Бо наша MAC адреса не збігається з докерівською. Покопав далі, знайшов, що всі контейнери AWS ECS Fargate отримують однакову MAC адресу. Однакову! Можете самі запустити
ifconfigв будь-якому контейнері та підтвердити. Міняти MAC адресу в Fargate поки не можна. Це значить, що всі UUID, згенеровані на AWS Fargate стандартним шляхом, ділять один простір відбитків часу. Та дійсно, такі UUID легко знайти у вільному Інтернеті.
Наскільки це погано? Таймстемп в UUID має роздільну здатність 100 наносекунд. За формулою збігу величин, для 1% ймовірності збігу потрібно 450 паралельних генераторів. Але це 1% шанс щосекунди! Для 1% шансу збігу щогодини достатньо 8 машин. З двома машинами можна очікувати збігу кожні 14 годин. Так що, достатньо погано, щоб негайно припинити вживати “стандартні” UUID v1.
16.05.2023
Текст у Three.js
Як додати до сцени текстові мітки? Я очікував, що є якась складна специфічна технологія. Але виявилось все набагато простіше. В Three.js є спеціальний рендерер CSS2DRenderer, який замість малювання 3D обʼєктів позиціює елементи HTML. Тобто до сцени додаються спеціальні “маркери” - CSS2DObject - до кожного з яких приєднаний елемент DOM.
Це працює в поєднанні зі базовим рендерером; в документі <canvas> для обʼєктів та <div> для міток є сусідами та ділять один та той самий простір (що, звісно, необхідно, щоб мітки поєднувались з іншими 3D обʼєктами.)
Так до сцени легко додати не тільки текст, а й будь-який зміст HTML - наприклад, світлину. Зручно. Єдине, чого не вистачає — це відтинання міток, які закриті за іншими предметами. Для того треба вручну робити raycasting, тобто перевіряти, чи не перетинається відрізок від камери до мітки з чимось іншим. Ну, принаймні, напіввручну, бо клас Raycaster бере на себе складну частину.
Поки шукав, як ховати текст “під обʼєкти”, знайшов таку скажену річ, як React для three.js @react-three/fiber. Сенс в тому, що не треба вручну та процедурно оновлювати зміст сцени — можна задати її декларативно. Тобто так само як і для HTML. А ще інтеграція зі звичайним HTML там вбудована, що уможливлює круті штуки. Проте мені зараз React не потрібний, бо для анімації за сценарієм від нього більше перешкод, ніж користі.
15.05.2023
Враження від Greensock
…Вчорашнє розуміння про Greensock доповнилось сьогодні тим, що в onUpdate краще зовсім нічого не перераховувати — саме тому, що ця функція може бути викликана в неочікуваний час та в невизначеному порядку. Як тільки переніс всі обчислення з onUpdate в render, відразу зникли баги. А то було таке, що в прямому порядку анімація проходить нормально, а у зворотному — бардак.
Тож тепер підхід такий: Greensock собі анімує все, що потрібно, а потім я в функції render оновлюю сцену відповідно до поточної мітки, переношу значення в потрібні місця, та, власне, потім роблю малювання. Тепер все передбачувано.
Після цього відчувається користь від бібліотеки: рівень абстракції підвищився. Більше не потрібно думати про стикування частин та про перенос глобальної позиції в локальну до етапу. Можна легко додавати до анімації нові етапи та експериментувати (краще наближати камеру та повертати одночасно? або послідовно? )
Наступним кроком, знайшов ресурс Natural Earth, на якому містяться, окрім іншого, бази даних по різних географічних обʼєктах. Мені цікаві кордони держав та міста. Все це можна завантажити в невідомому мені досі форматі shapefile, до якого, на щастя, є парсери як для JavaScript, та і для Go.
PS: а знаєте, що офісне крісло може вимикати монітор? В цьому винний електростатичний розряд, який створює механізм підйому. Звучить нереально, але в мене таке дійсно відбувається час від часу — встаю з крісла та монітор вибиває. Якби не натрапив випадково на пост, то це б залишалось таємницею. А так, спробую замінити кабель до монітора.
14.05.2023
Бібліотека анімації Greensock
Відкриття дня - бібліотека Greensock. Вона надає зручну структуру для анімації. Я вже мало не написав свою, але випадково побачив в прикладах Greensock та, на щастя, це саме те, що мені було потрібно.
В чому моя задача? Анімація з декількох етапів потребує відстеження як поточного етапу, так і положення в ньому. Greensock дозволяє задавати ці етапи декларативно, та сама бібліотека піклується про їх впорядкування. До речі, вона підтримує й анімацію прокруткою, якою я займався вчора. І помʼякшення анімації вбудоване.
Як воно працює з Three.JS? Взагалі вся анімація являє собою інтерполяцію значень. В базових прикладах по Greensock значення містяться в СSS, але насправді вона вміє інтерполювати будь-які числові атрибути довільного обʼєкту. В тому числі, й векторів Three.JS - або просто звичайних обʼєктів, якщо це зручно. Коли над значеннями, що інтерполюються, потрібно проводити додаткові дії, то їх можна описати в функції onUpdate.
Що мені не дуже подобається: інтерполювати можна тільки по одному обʼєкту, а з Three.JS зазвичай треба комбінувати декілька (або навіть просто position та rotation). В такому випадку створюю окремий обʼєкт-ціль інтерполяції, а з нього вже в onUpdate розкидую значення куди треба.
Ще довго шукав, як же ж робити переходи від сцени до сцени, тобто наприклад з глобуса на карту. Це ускладнюється ще й тім, що з анімацією за прокруткою цей перехід може трапитись в обидва боки. А ще, як виявилось, функція onUpdate може викликатись не тільки для поточного етапу, тому там не можна робити логіку визначення сцени. Потім знайшов систему міток та функцію currentLabel, яка саме відстежує поточний етап анімації. Тут ідея така: під час render перевіряю поточну мітку, та готую сцену для неї, якщо мітка помінялась.
3D-графіка - приємна розвага після системного програмування, поки не примушує згадувати про всякі векторні добутки та кватерніони, про які я вже років 15 не чув.
13.05.2023
Анімація за прокруткою
Черговий прогрес по тревел-постам — реалізував анімацію карти за прокруткою. Подивився оцю нещодавню статтю з NYTimes та зрозумів, що саме цього не вистачало для режисованого, але все ж контрольованого користувачем відображення. (Анімація, яку я маю на увазі, починається після перших параграфів статті, до неї треба ще докрутити.)
Як я це робив. Щоб реєструвати прокрутку, роблю елемент необхідної висоти — саме його фізичний розмір буде визначати тривалість анімації. Всередину розташовую власне полотно. Полотно за розміром збігається з вікном. Його позиціювання залежить від положення прокрутки — це або абсолютне позиціювання нагорі чи внизу контейнера, або — повноекранне fixed, при якому й відбувається анімація.
Щоб стежити за прокруткою, слухаю звичайну подію scroll. Щоб заощадити ресурси, це робиться тільки тоді, коли елемент з картою видимий; для цього існує цікавий клас IntersectionObserver.
Далі позиція прокрутки перекладається на відсоткову, і починається цікава частина. До цього часу моя анімація була повністю заскриптована, та просувалась за допомогою requestAnimationFrame. Натомість для анімації за прокруткою необхідно задати функцію стану сцени від часу. Я майже все так і робив, от тільки з обертанням карти вийшли деякі складнощі. Окремо треба описати переходи від сцени до сцени, як в прямому, так і у зворотному напрямку — додавання та видалення обʼєктів.
12.05.2023
Команда apt-get satisfy та інші цікаві пакетні менеджери
Дізнався на днях про команду apt-get satisfy. Нею можна вказати версію пакета приблизно, як я це звик робити в Rubygems або NPM. Наприклад: apt-get satisfy 'knot-resolver (>=5.6.0)'. Мені це було потрібно, щоб переконатись, що правильна версія пакета буде встановлена в докерфайлі. Взагалі лінтер hadolint радить вказувати версію явно. Але при такому підході докерфайл доведеться міняти, коли версія пакета в репозиторії оновиться — чого я не хочу робити без потреби.
Команда ця чомусь мало відома, навіть довелось відповісти про неї на Ask Ubuntu річної давнини. Зʼявилася вона десь у 2019 році.
Якщо вже зайшло про пакетні менеджери, то на Windows необхідно знати про chocolatey. Завдяки цій програмі можна як встановити, так і оновити багато популярних додатків автоматично. Особливо це корисно для налаштування компʼютерів родичам. Замість того, щоб лазити по системі та оновлювати все підряд, достатньо один раз запустити choco upgrade. Так само однією командою можна встановити пачку необхідних програм.
А на macOS є Homebrew Cask. Я їм не користуюсь, але тільки що перевірив та там є чимало додатків — більше, ніж я думав — наприклад, ось Setapp.
Чим я на Маці користуюсь, то це додатком MacUpdater. В нього зворотний підхід — він сам знаходить в системі всі встановлені додатки, та вміє їх оновлювати. Це набагато ефективніше, ніж погоджувати оновлення кожного додатку окремо. Також не так давно зʼявилася можливість завантажувати та встановлювати додатки. Але все це MacUpdater робить в напівавтоматичному режимі, тому розгорнути пачку додатків однією командою не вийде.
…А ще MacUpdater теж є на Homebrew Cask. We need to go deeper!
11.05.2023
На що гарний Golang у вебдодатках?
Я вже писав статтю про те, на що гарний Ruby on Rails, та я все ще вважаю Rails правильним вибором для вебдодатка. Тобто, якщо додаток написаний на Rails, то скоріше за все не доведеться його переписувати на щось інше.
Але уявимо, що вам треба переконати мене, що додаток треба писати на Go, а не на рельсі. Ой, важко це буде зробити. Бо Go не підготований для створення вебдодатків. Починаючи з того, що мова ця не така гнучка (див. статтю). Але також, на Go не вистачає відразу декількох інструментів, які є в Ruby on Rails. Це й міграції для бази. Й шаблонізатор. Й інтеграційні тести. І це тільки те, чого мені найбільш не вистачає. Писати вебдодаток на Go - технічно можливо, але безглуздо.
Тому пропоную дивитися на роль Go інакше, та переписувати на Go свою базу даних. Бо сильна сторона Go - це утримання складного та великого стану.
В Rails ми делегуємо утримання стану на базу даних. На такому розділенні праці побудований весь сучасний інтернет — тож можна сказати, що воно надзвичайно ефективне. Втім, зустрічаються випадки, коли база не справляється з потребами додатка. Може, бізнес-логіка надто складна, може, обмеження швидкодії не влаштовують. В таких випадках можна пошукати іншу базу, та я б це й радив робити. Можна почати програмувати всередині бази, це теж працює.
Але програмування в базах даних, порівняно зі звичайним, суцільний жах. Тестів немає, деплою немає, і так далі. Тому простіше було б реалізувати логіку стану безпосередньо в додатку. Ось тут вже можна впевнено брати Go. В Go чудова модель рівночасності, яка дуже корисна для керування складними структурами стану. Зробити власну базу легше, ніж здається, бо універсальна база нам не потрібна — тільки те, що використовує додаток. Власне, так NoSQL бази й зʼявляються.
(Звісно, це не єдине застосування Go. Я міркую з боку вебдодатків.)

