Автор блогу Master.dev поділився спостереженням, яке підштовхнуло його до експериментів із CSS: маленький краб, що безцільно повзає по верхній межі елемента. Саме цей приклад нагадав йому про одну з улюблених технік — комбінацію властивостей offset-path та offset-distance.

offset-path: border-box;
offset-distance: 20%; /* будь-яке значення */

Зазвичай offset-path сприймають як спосіб задати конкретний шлях за допомогою path() чи rect(), вздовж якого потім переміщується елемент через анімацію значення offset-distance. Але виявляється, що шляхом може слугувати просто межа батьківського елемента — це суттєво спрощує розмітку.

Оскільки Safari вже підтримує функцію random(), а в Chrome вона доступна під прапорцем Experimental Web Features, автор вирішив перевірити, чи можна змусити того самого краба переміщатися у випадкові точки, обійшовшись виключно CSS.

Якщо переглядати наведені демонстрації у браузері без підтримки цієї функції, руху просто не буде видно — значення offset-distance, що обчислюється через random(), буде проігнороване. Для продакшн-використання можна додати запасний варіант через @supports із заздалегідь заданими або згенерованими через JavaScript значеннями.

Створення базового блока

Спершу потрібен звичайний контейнер:

.box {
  width: 200px;
  height: 200px;
  border: 2px solid black;
  position: relative;
}

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

Розміщення елемента на межі блока

Далі коло можна прив'язати до контуру блока. Здавалося би, доведеться описувати шлях тими самими розмірами, що й сам блок, наприклад через offset-path: rect(0 200px 200px 0);, але насправді достатньо просто вказати, що шляхом слугує межа елемента.

.circle {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background-color: red;
  offset-path: border-box;
  offset-distance: 20%;
}

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

Анімація руху

Наступний крок — змусити коло рухатися вздовж межі, адже саме це є головною ціллю експерименту:

.circle {
  ...
  animation: moveCircle 25s infinite alternate ease-in-out;
}

Логічно було би просто прописати випадкові позиції в ключових кадрах:

@keyframes moveCircle {
  0% { offset-distance: random(0%, 100%); }
  25% { offset-distance: random(0%, 100%); }
  50% { offset-distance: random(0%, 100%); }
  75% { offset-distance: random(0%, 100%); }
  100% { offset-distance: random(0%, 100%); }
}

Автор зазначає, що не може достеменно пояснити, чому такий підхід не працює, але факт залишається фактом. Можливо, справа в особливостях роботи функції random() у межах одного блока @keyframes: усі значення обчислюються однаковими, хоча явного ключового слова для спільного обчислення не вказано — імовірно, у цьому контексті воно застосовується неявно.

Проблема вирішується, якщо кожному викову функції передати унікальне зерно (seed). Автор також розширив діапазон до 200% для більшої амплітуди руху:

@keyframes moveCircle {
  0% { offset-distance: random(--k1, 0%, 200%); }
  25% { offset-distance: random(--k2, 0%, 200%); }
  50% { offset-distance: random(--k3, 0%, 200%); }
  75% { offset-distance: random(--k4, 0%, 200%); }
  100% { offset-distance: random(--k5, 0%, 200%); }
}

Якщо потрібен рух лише вздовж верхньої межі, діапазон випадковості слід обмежити до 25%.

Сяюча куля та тінь

Автора настільки захопило те, що механізм спрацював, що він відволікся на створення додаткових прикладів. Наприклад, коло можна розмістити у своєрідній «трасі», побудувавши межі за допомогою outline та box-shadow замість звичайної рамки, а саме коло перетворити на неймовірно сяючу кулю.

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

Елемент, що рухається, звісно, не обов'язково має бути колом — це може бути будь-що. Автор спробував замінити його на велику тінь і подивитися, як виглядатиме її рух по контуру.

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

А що з крабом?

Повторити краба не вийде — цей трюк уже показали, і він втратив новизну.

Тож автор вирішив зробити песика замість краба.

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

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

Створення повноцінного «двору для песика» (dog-yard) автор залишив як вправу для читачів.

Джерело: Master.dev Blog