.button {
background: hsl(77, 86%, 81%);
transition: background-color .25s,
transform .15s;
}
.button:hover {
background: hsl(77, 86%, 91%);
transform: scale(1.05);
}
button.onclick = () => {
// パネルを作る
const panel = document.createElement('div');
panel.classList.add('panel');
panel.textContent = 'ハロー!';
parent.appendChild(panel);
// アニメーションさせる
panel.style.transform = 'scale(0)';
panel.style.transition = 'transform .5s';
panel.style.transform = 'scale(1)';
};
+
transition-delay
>
0s…
button.onclick = () => {
const panel = document.createElement('div');
...
panel.style.transform = 'scale(0)';
panel.style.transition = 'transform .5s';
panel.style.transform = 'scale(1)';
};
button.onclick = () => {
// パネルを作る
const panel = document.createElement('div');
...
// アニメーションさせる
panel.style.transform = 'scale(0)';
panel.style.transition = 'transform .5s';
requestAnimationFrame(() => {
panel.style.transform = 'scale(1)';
});
};
button.onclick = () => {
const panel = document.createElement('div');
...
panel.style.transform = 'scale(0)';
requestAnimationFrame(() => {
panel.style.transform = 'scale(1)';
});
};
button.onclick = () => {
// パネルを作る
const panel = document.createElement('div');
...
// アニメーションさせる
panel.style.transform = 'scale(0)';
panel.style.transition = 'transform .5s';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
panel.style.transform = 'scale(1)';
});
});
};
button.onclick = () => {
...
panel.style.transform = 'scale(0)';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
panel.style.transform = 'scale(1)';
});
});
};
button.onclick = () => {
// パネルを作る
const panel = document.createElement('div');
...
// アニメーションさせる
panel.style.transform = 'scale(0)';
getComputedStyle(panel).transform;
panel.style.transition = 'transform .5s';
panel.style.transform = 'scale(1)';
};
button.onclick = () => {
const panel = document.createElement('div');
...
panel.style.transform = 'scale(0)';
getComputedStyle(panel).transform;
panel.style.transition = 'transform .5s';
panel.style.transform = 'scale(1)';
};
createElement
の直後
display: none
getComputedStyle(elem)
だけだと、スタイルの算出は走らない!❌
getComputedStyle(elem).<property>
👍
getComputedStyle
は重い
const PopupButton = () => {
const [isShowing, setIsShowing] = React.useState(false);
const panelRef = React.useRef(null);
React.useLayoutEffect(() => {
panelRef.current.style.transform = 'scale(1)';
}, [isShowing, panelRef.current]);
const buttonClick = React.useCallback(() => {
setIsShowing(!isShowing);
}, [isShowing]);
return (<>
<button onClick={buttonClick} />
{isShowing ? (
<div ref={panelRef}
style={{ transition: 'transform .5s', transform: 'scale(0)' }}>
ハロー!
</div>
) : null}
</>);
};
const PopupButton = () => {
const [isShowing, setIsShowing] = React.useState(false);
const panelRef = React.useRef(null);
React.useLayoutEffect(() => {
getComputedStyle(panelRef.current).transform;
panelRef.current.style.transform = 'scale(1)';
}, [isShowing, panelRef.current]);
const buttonClick = React.useCallback(() => {
setIsShowing(!isShowing);
}, [isShowing]);
return (<>
<button onClick={buttonClick} />
{isShowing ? (
<div ref={panelRef}
style={{ transition: 'transform .5s', transform: 'scale(0)' }}>
ハロー!
</div>
) : null}
</>);
};
button.onclick = () => {
// … パネルが表示されていれば、削除する。
panel.remove();
};
button.onclick = () => {
// … パネルが表示されていれば、削除する。
panel.style.transform = 'scale(0)';
panel.addEventListener('transitionend', () => {
panel.remove();
});
};
display:none
になったら?
transition-property
の計算値が変わったら?
transitionはキャンセルされて
transitionend
は発火されない!
//...
const buttonClick = React.useCallback(() => {
if (panelState === 'showing') {
setPanelState('hiding');
panelRef.current.addEventListener('transitionend', () => {
setPanelState('hidden');
}, { once: true });
} else {
setPanelState('showing');
}
}, [panelState]);
// ...
transitionrun
→ transitionが生成された 🆕
→ transitionend
を待ってもオッケー 🙆♂️
transitioncancel
→ 要素が消えた🗑️
(削除されたり、再生成されたり、
display:none
になったり)
→ transitionend
を待たない方が良い ❌
animationcancel
(transitioncancel
と同様)
transitioncancel
たちanimationcancel
animationcancel
button.onclick = () => {
// パネルを作る
const panel = document.createElement('div');
panel.classList.add('panel');
panel.textContent = 'ハロー!';
parent.appendChild(panel);
// アニメーションさせる
panel.style.transform = 'scale(0)';
getComputedStyle(panel).transform;
panel.style.transition = 'transform .5s';
panel.style.transform = 'scale(1)';
panel.animate(
{ transform: ['scale(0)', 'scale(1)'] },
{ duration: 500, easing: 'ease' }
);};
Element.animate
を対応していないようです。
React.useLayoutEffect(() => {
let animation;
if (panelState === 'showing') {
animation = panelRef.current.animate(
{ transform: ['scale(0)', 'scale(1)'] },
{ duration: 500, easing: 'ease' }
);
} else if (panelState === 'hiding') {
animation = panelRef.current.animate(
{ transform: ['scale(1)', 'scale(0)'] },
{ duration: 500, easing: 'ease' }
);
// animationがキャンセルされたらfinishedがrejectされる
animation.finished.then(() => {
setPanelState('hidden');
});
}
return () => {
if (animation) { animation.cancel() }
};
}, [panelState, panelRef.current]);
Animation.finished
を対応していないようです。
// transitionend
button.onclick = () => {
panel.style.transform = 'scale(0)';
panel.addEventListener('transitionend', () => {
panel.remove();
});
};
// Web Animationsのfinished Promise
button.onclick = () => {
panel.style.transform = 'scale(0)';
const transition = panel.getAnimations()[0];
transition.finished.then(() => {
panel.remove();
});
});
CSSTransition.setKeyframes()
document.addEventListener('transitionrun', evt => {
if (evt.propertyName !== 'fill') {
return;
}
const transition = evt.target
.getAnimations()
.find(animation => animation.transitionProperty === 'fill');
const keyframes = transition.effect.getKeyframes();
const hslKeyframes = generateHslKeyframes(
keyframes[0].fill,
keyframes[1].fill
);
transition.effect.setKeyframes(hslKeyframes);
});
getAnimations
もしくはCSSTransition.setKeyframes
を対応していないようです。
Element.animate() | 48+ | 36+ | 3月? |
getAnimations() | Nightly, 75+ | Experimental Web Platform features, 4月? | 3月? |
react.render(
<Router>
<Route exact path="/" component={Photos} />
<Route path="/polls" component={Polls} />
<Route path="/finder" component={Finder} />
</Router>
);
<Router>
<Route render={({ location }) => (
<TransitionGroup>
<CSSTransition key={location.pathname}
timeout={400} classNames="slide">
<Switch location={location}>
<Route exact path="/" component={Photos} />
<Route path="/polls" component={Polls} />
<Route path="/finder" component={Finder} />
</Switch>
</CSSTransition>
</TransitionGroup>
)}/>
</Router>
<CSSTransition>
の場合は何度もスタイル算出およびレイアウトの処理をさせる)
<Router>
<Route render={({ location }) => (
<Photos active={location.pathname === '/'}/>
<Polls active={location.pathname === '/stories'}/>
<Finder active={location.pathname === '/notes'}/>
)}/>
</Router>
<CSSTransition>
の場合は何度もスタイル算出およびレイアウトの処理をさせている)
requestAnimationFrame
,
getComputedStyle
, CSS animations,
Element.animate
transitioncancel
,
transitionrun
, Animation.finished
Element.animate
,
getAnimations
offset-path
prefers-reduced-motion
steps(5,
jump-none)
translate
,
scale
,
rotate
2019年のプレゼン