Скрол-історія

Сцена на всю висоту екрана, плівки, що передають одна одній хід, і ШАРИ, які приїжджають поверх них, тримаються і йдуть: спершу титульна картка сторінки, далі твердження по одному на екран. Плівок шість, і разом вони одна дія: порожня лава - коробка наповнюється - кожна банка отримує чисту картку - стулки закриваються - стрічка і наліпка - руки забирають коробку. Позиція скролу обирає кадр, тож ролики не ПРОГРАЮТЬСЯ, а ПЕРЕМОТУЮТЬСЯ: десять секунд знятого покривають секцію будь-якої висоти, а той, хто спинив скрол, спиняє коробку на півдорозі, а не дивиться, як вона тікає.

12.21 переписав дві речі. Перша: плівка стала ПЕРШИМ екраном, і герой сторінки живе на ній титульною карткою, а не блоком над нею. Друга: усі шари поїхали на годиннику скролу. До того плівка перемотувалась щокадру, а твердження перемикав IntersectionObserver - булеве спрацювання, яке віддає CSS 330 мс переходу гратись самому. Плівка пливла, текст клацав, і жодна крива це не лікує, бо два годинники міряли різне.

Живий приклад

Це справжня секція, а не знімок: ті самі дві плівки і ті самі п'ять тверджень, що на лендингу тренера. Від 860 сцена займає весь кадр продукту і липне, твердження лягають на неї чорнильними плитками і зникають, коли кадр іде далі; вужче - сцена стає звичайною картинкою 16:9, а твердження звичайними боксами, і жоден лист не запитується з мережі.

Порожня коробка на лаві в залі.
Тренерам · залам · командам

Спортивне харчування оптом для тренерів

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

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

  1. 💰 Гуртові цінина кожному товарі в сесії, а не на підсумку
  2. 👥 Одна сесія - усі клієнтидодавайте кожному за його ціллю, не виходячи з кошика
  3. ⏱️ Історія по кожному клієнтуповтор попереднього замовлення в один тап
  4. 🚚 Одна доставка на тренераодне відправлення, розподіл за клієнтами всередині
  5. 🛡️ Надійний склад і постачаннясклад, походження і сертифікат на кожен товар
Коли вживати

Коли твердження треба не перелічити, а вивести одне з одного. Сітка відповідає на питання «що», плівка відповідає на «звідки це взялось»: коробка наповнюється, БО тренер додає ще одного клієнта, і кожне твердження підіймається поруч із тим кадром, який його заробив. Це і є робота «зв'язок» з motion.md.

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

Обмеження

Плівка приходить ОДНИМ зображенням. 41 кадр, викладені 6x7, кожна комірка 800x450; drawImage копіює одну комірку на кадр. Зміряно на справжньому ролику: лист 1006 KB проти 2190 KB тих самих 41 кадру як mp4 і 1280 KB як 41 окремий jpg. Тобто найлегший із трьох, ОДИН запит замість сорока одного, і жодного відеодекодера - саме там перемотуваний <video> і затинається.

Плівок стало шість, і саме тому з'явилось відкладене завантаження. Дві плівки на шість екранів це 50-70 пікселів скролу на кадр - коробка наповнюється ривками, бо між двома сусідніми кадрами півекрана прокрутки. Шість плівок, по одній на екран, дають 21-24 пікселі на кадр, і це вже перемотка, а не слайдшоу. Ціна - 5.8 MB листів замість 1.95, і платити її одразу не можна: лист запитується за один екран до того, як він потрібен, і запас це відношення, а не число - висота сцени, поділена на хід секції, тобто рівно один екран скролу, скільки б шарів не було в історії. Зміряно на живій сторінці при 1440x900: на першій фарбі запитано два листи з шести, третій приходить на чверті ходу, четвертий на третині, п'ятий на 58%, шостий на трьох чвертях.

Без JavaScript сторінка ЗАВЕРШЕНА, а не зіпсована. Сцена несе справжній <img> першого кадру; канва лежить зверху з нульовою прозорістю і проявляється лише тоді, коли на ній справді намальовано кадр. Стан спокою шарів написаний не на них, а під .story.is-live - клас, який ставить тільки скрипт. Написане навпаки, воно лишило б сторінку з упалим скриптом із власним H1 на нульовій прозорості назавжди, і жоден прилад цього б не побачив.

Один .story на сторінку. Дві липкі сцени підряд конкурують за один екран, і друга починає рухатись раніше, ніж перша доїхала. Плівок усередині сцени може бути скільки завгодно - кожна оголошує свій відрізок data-from / data-to, і відрізки НАКЛАДАЮТЬСЯ: у перекритті обидві малюють, а наступна проявляється поверх попередньої. Різ в одній точці читається як збій, накладення читається як один дубль. На лендингу тренера відрізків шість, по одному на екран, і кожне перекриття це 0.04 ходу, тобто близько 180 пікселів скролу.

Кожна плівка починається СПРАВЖНІМ останнім кадром попередньої, а не намальованим наближенням до нього. Kling 2.5 на 720p вимагає стартовий кадр і забороняє кінцевий, тож ланцюг тут вимушений і послідовний: зняли сцену, витягли її останній кадр, віддали наступній. Ціна - сцени не можна генерувати паралельно. Виграш - лава, світло, підлога і сама коробка збігаються на стику до пікселя, і перекриття ховає рух, а не розбіжність.

Стани

Клас лишився один. .story.is-live - скрипт узяв секцію: тільки з нею шари мають стан спокою. .is-on і .is-in, які тут стояли до 12.21, ВИДАЛЕНІ, і це не прибирання, а наслідок: обидва були булевими, а стан кожного шару тепер неперервний, і назвати класом його неможливо.

Замість них скрипт пише три числа, і всі три без одиниць:

ЧислоДеДіапазонЩо з нього робить css
--story-tшар0..1приїзд. Для твердження воно росте і ЛИШАЄТЬСЯ на 1; для титульної картки - падає, бо герой іде разом зі своїм екраном. Прозорість це воно, а частини шару читають його з РІЗНИМИ порогами - у цьому й каскад
--story-uшар0..1скільки приїзду ще лишилось. Хід це воно, тож шар, згаслий наполовину, наполовину і проїхав - одне число, ніколи не два годинники. Доїхавши, воно нуль і нулем лишається: повертатись нікуди, тому ніщо не відскакує
--story-qканва0..1як далеко плівка всередині СВОГО відрізка. Прояв у перекритті це воно, обрізане clamp-ом по першій восьмій

Жодне з трьох ніколи не потрапляє в transition, і це ремонт, а не стиль: у першій збірці на плівці стояв transition: opacity, скрип переписував значення щокадру, кожен кадр перезапускав 330 мс, і значення не доїжджало ніколи. Зміряно на передачі ходу: обидві плівки на 0.008, обидві малюють правильні кадри. Плівка, яка працює бездоганно і якої не видно. Рампа між двома рухомими кінцями не рампа.

Жодного :hover на самій історії: тут нема на що натискати. Виняток - кнопки в титульній картці, і вони отримали грань замість тла, бо тла для темної плитки в системі немає, а вигадувати токен під ховер не можна.

Рух

Робота: ЗВ'ЯЗОК. Рухаються рівно дві властивості - opacity і transform, бо це ті дві, які браузер міняє не чіпаючи розкладку. Ні пружини, ні відскоку, ні часток, ні лічильника - принцип 4 продукту каже «спокійно і впевнено», і плівка тут аргумент, а не феєрверк.

Тривалості немає. І це не пропуск: рух тут не має власного годинника, бо ним керує скрол. --dur-* і --ease-* у файлі не читаються ЖОДНОГО разу, а криву дає не токен, а smoothstep на плечі обгортки - те, що знімає з приїзду кут. Дистанція одна, --move-lg, і вона нова: --move-md це 10px, хід елемента ВСЕРЕДИНІ сторінки, а 10px на плитці, яка володіє цілим екраном, це не рух, а тремтіння. --move-lg це 40px, і 40 тут відношення, а не здогад - шар проходить свій власний відступ, --space-40.

Хореографія - це одна формула на індексі, а не список порогів. До 12.22 тут стояли чотири вручну набрані числа - окреме для кікера, для заголовка, для ліда, для кнопок - і п'ятий рядок у картці не мав би де стати в чергу. Тепер скрипт пише --i (місце дитини всередині плитки) і --w (місце слова всередині заголовка), крок написаний ОДИН раз, а --story-s це частка дитини в обгортці її шару. Додали рядок - він бере свою чергу; забрали - решта зімкнулась.

Твердження ПАРКУЮТЬСЯ, а не проходять повз - 12.23. До того кожне володіло своїм екраном і віддавало його назад: піднялось, потрималось, згасло, наступне приїхало. Тепер кожне приїжджає у свою точку скролу і ЛИШАЄТЬСЯ - значення впирається в 1, і гілки назад немає, - а до кінця історії всі п'ять стоять на дошці разом. Це те, чого сторінка, яка продає, хоче саме в ту мить, коли коробку забирають.

Дошка це один прибитий екран, і твердження лежать на ній двома колонками до країв - непарні ліворуч, парні праворуч, - тож плівці лишається середина. Стос росте ЗНИЗУ вгору, подалі від шапки: сама сцена прибита на top: 0 навмисно, бо фотографія під плаваючою смугою це саме те, як виглядає повноекранний герой, а от текст під тією ж смугою це просто текст, якого не прочитати.

Висота секції перестала бути побічним ефектом. Поки кожне твердження мало свій рядок на 100svh, ці рядки і БУЛИ довжиною скролу; запарковані на одній дошці вони всі заввишки в один екран, і секція згорнулась би до одного екрана, а плівці не було б що перемотувати. Тепер довжина написана тим відношенням, яким вона завжди була - один екран на шар, - а число шарів дає скрипт, бо він єдиний їх бачить. Сторінка з упалим скриптом падає на 1: секція рівно заввишки зі свою сцену, без плівки і без прибивання.

Заголовок приїжджає СЛОВО ЗА СЛОВОМ, і це єдине місце, де історії дозволено показувати себе. Сам заголовок перестає згасати - його несуть слова, - а крок між словами КОРОТШИЙ за крок між блоками: речення читається на одному диханні, і довга пауза між словами читається як затинання, а не як фраза.

І одна річ, яка МАЛЮЄТЬСЯ, а не згасає. Тонка помаранчева риска росте від того краю, до якого притиснута плитка - непарні ліворуч, парні праворуч. Шість однакових приїздів на шість екранів це знову список; риска дає інший рід руху за ті самі дві властивості, бо scaleX це transform.

Бібліотеки скрол-таймлайну тут немає, і це рішення, а не пропуск. GSAP ScrollTrigger зі scrub робить рівно те, що робить арифметика вище: чіпляє прозорість і хід до позиції скролу замість власного годинника. Те, чого від нього хочуть насправді - каскад, розбивка заголовка на слова, риска, - це ХОРЕОГРАФІЯ, тобто авторська робота, і вона однакова з бібліотекою і без неї. Різниця лише в ціні: 70 KB з CDN у пакеті, який зобов'язаний підніматись без збірки й без мережі (тест чистого клону), плюс залежність без власника в системі, де їх нуль.

При prefers-reduced-motion перемотки немає взагалі: лишається постер, а шари просто показані. --move-lg складається сам через перевизначення в tokens.css, але обгортку він не дістає - прозорість це СТАН, а не тривалість, і блок у кінці файлу підіймає кожен шар до непрозорого рукою. Елемент, що перестав з'являтись, був би гіршим дефектом за будь-яку анімацію.

Ширина

360 - це не маленький десктоп. Нижче --bp-shell-wide немає ні липкості, ні канви, ні листів: жоден із двох не запитується з мережі, і це зміряно на живій сторінці, а не обіцяно. Сцена - звичайна картинка 16:9, твердження - та сама сітка боксів 2 / 3, що стояла тут до історії. Прибитий на весь екран ролик це те місце, де такі сторінки вмирають на телефоні, і найдешевший спосіб не вмерти - там не бути.

На всю ширину - це ширина КАДРУ ПРОДУКТУ, і вона зміряна, а не обчислена. Перша збірка виходила лише за власні 16px відступу сторінки, і поки колонка сторінки дорівнює кадру, цього досить: на 1280 кадр 1064 і колонка 1064, дивитись нема на що. Але колонка впирається в --container-page, тож на 1440 обабіч плівки відкрились дві білі смуги по 12px, на 1600 по 92px, а на 1920 - по 252px. Білі стовпи вздовж повноекранної фотографії, і вони росли разом з екраном. 100vw тут відповідь неправильна: він рахує класичний скролбар, якого в кадрі немає, і плівка вийшла б ширшою за сторінку, тобто горизонтальний скрол на кожному екрані. Тому ширину скрипт ЧИТАЄ з .wf-canvas і публікує як --story-bleed - те саме, що uivStickyHeader() робить із --shell-header-h. Запасне значення 100%: сторінка з упалим скриптом виглядає рівно так, як виглядала.

Точки: 620 - твердження стають трьома в ряд. 860 робить п'ять речей одразу: сцена виходить на всю ширину кадру продукту і липне на 100svh, титульна картка підтягується ПОВЕРХ неї на цілу сцену (тому перший екран сторінки і перший екран історії це один екран), твердження лягають рядами по екрану і стають чорнильними плитками, непарні ліворуч і парні праворуч, і вмикається скрипт.

Нижче 860 порядок ІНШИЙ, ніж у розмітці. Сцена мусить стояти першою у розмітці, щоб від 860 бути липким предком усього, що по ній їде; на телефоні ж сторінка, яка продає, відкривається власним H1, а не картинкою. Одне order розводить два читання, і жодне не програє.

Токени

Прочитані з scrollstory.css: 11 семантичних ролей і 25 примітивів. Список виводить node tools/roles.mjs scrollstory з самого файла, тож він не може розійтися з ним.

Семантичні ролі (11)Примітиви (25)
--bg-action --bg-inverse --bg-page --bg-sunken --line-hair --line-oninverse-soft --text-muted --text-oninverse --text-oninverse-muted --text-primary --text-secondary--container-text --font-display --fs-12 --fs-14 --fs-16 --fs-24 --fs-34 --fw-black --fw-bold --fw-semibold --lh-airy --lh-snug --ls-caps --ls-display --move-lg --move-md --radius-12 --radius-16 --space-12 --space-16 --space-2 --space-24 --space-32 --space-40 --space-8