Никита Дубко, «Веб-стандарты»
Никита Дубко, «Веб-стандарты»
gzip_size https://static.tildacdn.com/js/tilda-animation-2.0.min.js
Plain: 34298
Gzipped: 6749
const observer = new IntersectionObserver(
callback,
{
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: [1.0],
}
);
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (entry.intersectionRatio >= 0.75) {
// TODO
}
}
});
};
animation: hadouken 2s linear infinite;
animation-name: hadouken;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
function easeInOut(t) {
if (t <= 0.5) {
return 2 * t * t;
}
t -= 0.5;
return 2 * t * (1 - t) + 0.5;
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.target {
transform-origin: 0 50%;
animation: grow-progress auto linear;
animation-timeline: scroll();
}
animation-timeline: scroll(<scroller> <axis>)
@keyframes animate-it { … }
.scroller {
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: inline;
/* scroll-timeline: --my-scroller inline; */
}
.target {
animation: animate-it linear;
animation-timeline: --my-scroller;
}
const $progressbar = document.querySelector('#progress');
$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
{
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
fill: 'forwards',
timeline: new ScrollTimeline({
source: document.documentElement,
}),
}
);
animation-timeline: view(<axis> <view-timeline-inset>)
.revealing-image {
view-timeline-name: --revealing-image;
view-timeline-axis: block;
animation: auto linear reveal both;
animation-timeline: --revealing-image;
animation-range: entry 25% cover 50%;
}
@keyframes reveal {
from { opacity: 0; }
to { opacity: 1; }
}
animation-range: entry 0% entry 100%;
$el.animate(
{
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
timeline: new ViewTimeline({
subject: document.getElementById('subject'),
}),
rangeStart: 'entry 25%',
rangeEnd: 'cover 50%',
}
);
.icon {
animation: icon-scale, icon-scale;
animation-direction: normal, reverse;
animation-timeline: view(inline);
animation-range: entry, exit;
}
@keyframes icon-scale { 0% { scale: 0; } }
<meta http-equiv="Page-Enter" content="blendTrans(Duration=1.0)">
<meta http-equiv="Page-Exit" content="blendTrans(Duration=1.0)">
<meta http-equiv="Site-Enter" content="blendTrans(Duration=1.0)">
<meta http-equiv="Site-Exit" content="blendTrans(Duration=1.0)">
Microsoft: FrontPage FAQ
document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// 1. Делаем скриншот
document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// 1. Делаем скриншот
document.startViewTransition(() => {
// 2. Состояние DOM сохранено
updateTheDOMSomehow(data);
});
// 1. Делаем скриншот
document.startViewTransition(() => {
// 2. Состояние DOM сохранено
updateTheDOMSomehow(data);
// 3. Запускаем магию
});
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition-old(root)
opacity: 1 ➞ 0
::view-transition-new(root)
opacity: 0 ➞ 1
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
.card {
view-transition-name: card;
}
::view-transition
└─ ::view-transition-group(card)
└─ ::view-transition-image-pair(card)
├─ ::view-transition-old(card)
└─ ::view-transition-new(card)
::view-transition
└─ ::view-transition-group(sidebar)
└─ ::view-transition-image-pair(sidebar)
└─ ::view-transition-new(sidebar)
/* Появление */
::view-transition-new(sidebar):only-child {
animation: 300ms linear both slide-from-right;
}
/* Пропадание */
::view-transition-old(sidebar):only-child {
animation: 300ms linear both slide-to-right;
}
const wait = ms => new Promise(r => setTimeout(r, ms));
document.startViewTransition(async () => {
updateTheDOMSomehow();
await Promise.race([document.fonts.ready, wait(100)]);
});
export async function getPageContent(url) {
// Не используйте в продакшене!
const response = await fetch(url);
const text = await response.text();
return /<body[^>]*>([\w\W]*)<\/body>/.exec(text)[1];
}
Demo
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
const transition = document.startViewTransition(...);
console.log(transition);
function startViewTransition(callback) {
if (!document.startViewTransition) {
callback();
return;
}
document.startViewTransition(callback);
}
<link rel="prefetch" href="/another-page">
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["page-1.html", "page-2.html"]
}
]
}
</script>
@keyframes header {
from {
filter: hue-rotate(0turn);
}
to {
filter: hue-rotate(1turn);
}
}
header::before {
animation: header auto linear;
animation-timeline: scroll();
}
.card {
transform-origin: 50% 0;
animation: card ease both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes card {
0% { translate: -100%; filter: blur(10px); opacity: 0; }
100% { translate: 0; filter: blur(0); opacity: 1; }
}
onLinkNavigate(async ({ fromPath, toPath }) => {
const content = await getPageContent(toPath);
const avatar = getLink(toPath)
.closest(".card")
.querySelector("img");
avatar.style.viewTransitionName = "avatar-img";
transitionHelper({
updateDOM() {
document.body.innerHTML = content;
},
});
});
.talk img {
view-transition-name: avatar-img;
}
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}