web motion design

TLDR;

动态设计原则上是为了利于用户交互,不是为了cool;好的动设能让用户直观的接收到信息。 动设的技术方案:
  • 嵌入视频
  • 过渡(transition)
  • 动画(animation)

video background

<video autoplay muted loop>
<source src="path/video.mp4" type="video/mp4"><!-- better compatibility --!>
<source src="path/video.webm" type="video/webm"><!-- smaller size --!>
</video>

transitions

Simple (state A to B), but NOT handle complex motions.
transition:
<transition-property>
<transition-duration>
<transition-function>
<transition-delay:0>,
color .4s ease-in-out 1s;
/* transition-property: all; NOT good for performance */
对于 transition-property 不支持的属性,如 background,可采用其他方式 Get Around;
// 常用属性
opacity: 0; // to 1
transform: translate3d(<x>, <y>, <z>);
color

object-*

image/video 元素中 src 对象的 layout
object-position // 位置
object-fit:
fill |
contain |
cover |
none |
scale-down

transform

transform-box:
content-box |
border-box |
fill-box | svg: act as content-box for CSS layout elements
stroke-box | svg: act as border-box for CSS layout elements
view-box // svg default, The nearest 
SVG viewport is used as the reference box transform-origin: // 变形原点 transform-style: // 设置元素的子元素是位于 3D 空间中还是平面中 flat| preserve

animations

animation:
<duration> <easing-function> <delay> <iteration-count> <direction> <fill-mode> <play-state> <name>

animation-fill-mode: <[none|forwards|backwards|both]>; // 动画执行之前/之后如何将样式应用于其目标
animation-play-state: <[paused|running]>;
animation-direction: <[normal|reverse|alternate?-reverse]>

@keyframes <name> {
<timing> {}
}
与 transition 类似,可以改变元素位置、大小等; 特别一点的,可以制作动图; 复杂的,可以借助第三方库如GSAP,编写 JS;这种方式是比较吃性能的,慎用! sample: Mac Dock栏的鼠标划过 stick 效果
.list {
transform-style: preserve-3d;
transform: perspective(1000px);
}
.list .item {
transition: .5s;
filter: brightness(0);
}
.list .item:hover {
filter: brightness(1);
transform: translateZ(200px);
}
.list .item:hover + * { // hover 后1元素
filter: brightness(0.6);
transform: translateZ(150px) rotateY(40deg);
}
.list .item:hover + * + * { // hover 后2元素
filter: brightness(0.4);
transform: translateZ(70px) rotateY(20deg);
}
.list .item:has(+ *:hover) { // hover 前1元素
filter: brightness(0.6);
transform: translateZ(150px) rotateY(40deg);
}
.list .item:has(+ * + *:hover) { // hover 前2元素
filter: brightness(0.4);
transform: translateZ(70px) rotateY(20deg);
}

sample: svg text loader

svg path {
animation: animation-stroke 42 ease-in-out 1 forwards;
}
// 动画的核心原理是利用 dash:【——  ——  】 -> 【  ——  ——】
@keyframes animate-stroke {
0% {
fill: transparent;
stroke: some-color;
stroke-width: 3;
stroke-dashoffset: 25%; // dash 从哪开始
stroke-dasharray: 0 32%; // dash and gap
}

50% {
fill: transparent;
stroke: some-color;
stroke-width: 3;
}

80%, 100% {
fill: some-color;
stroke: transparent;
stroke-width: 0;
stroke-dashoffset: -25%; // dash 从哪开始
stroke-dasharray: 32% 0; // dash and gap
}
}
sample: loading dots
.dot-container {
display: flex;
gap: .25rem;
}
.dot-container .dot {
width: .8rem;
height: .8rem;
background-color: white;
border-radius: 50%;
animation: 1s ease-in-out infinite scaleUp;
}

.dot-container .dot:nth-child(2) {
animation-delay: .5s;
}
.dot-container .dot:nth-child(3) {
animation-delay: 1s;
}

@keyframes scaleUp {
0%, 80%, 100% {
transform: scale(0);
}

40% {
transform: scale(1);
}
}
sample: animate illustrations
[id^="star-"] {
animation: 6s ease-in-out infinite alternate pulse;
transform-origin: center;
}
@keyframes pulse {
0%, 100% {
transform: scale(1.2);
opacity: 1;
}
50% {
transform: scale(0);
opacity: 0;
}
}
@keyframes bounce {
// copy from animate.css
}
sample: animate avatar
.avatar {
background-image: url("avavar-of-multi-frames");
background-repeat: none;
background-size: cover;
animation: 2s steps(<frames-count - 1>) infinite walking;
}

@keyframes walking {
to {
background-position: 100%;
}
}
<script src="path/to/gsap.js"></script>
<script>
gsap.set("#anchor", {
scale: 0,
transformOrigin: "50% 50%"
});
let tl = gsap.timeline({
repeat: -1, // same as infinite
repeatDelay: 1.5,
yoyo: true // same as alternate
});
tl.to("#anchor", {
scale: 1,
rotation: 360,
duration: 1.2,
ease: "elastic.out"
});
tl.fromTo("#left", {
drawSVG: "100% 100%"
},
{
drawSVG: "0 100%", // stroke start, end
duration: 1.2,
ease: "power4.inOut"
});
</script>

sample: typewriter effect

let mainTl = gsap.timeline({ repeat: -1 });
let tl = gsap.timeline({
repeat: 1,
yoyo: true,
repeatDelay: 6
});

words.foreach(word => {
tl.to("#typewriter", {
text: word, // core code here
duration: 1
});

mainTl.add()
})

// 光标
#cursor {
animation: 1s infinite steps(1) blink;
}

@keyframes blink {
0%, 100% { opacity: 0 }
50% { opacity: 1 }
}
advanced effect samples
  • 鼠标跟随 effect:需要用到 getBoundClientReact()获取光标的位置信息,计算光标的相对位置
gsap.to(magneto, {
duration: 1,
x: computedX, // core code here
y: computedY // core code here
})
  • 滚动触发 effect:需要gsap 集成 scrollJS插件
  • Parallax animation:将一张图分解成多 layer,将 layer 动画

prefers-reduced-motion

@media (prefers-reduced-motion: reduce) {
*, ::before, ::after {
animation-duration: 0.001s !important;
animation-iteration-repeat: 1 !important;
transition-duration: 0.001s !important;
}
}
GSAP[1] 观测 animation 过程中属性值的变化[2] core code:
  • 事件监听:click / animationstart / animationend
  • Loop:window.requestAnimationFrame
/*
To experiment with a different CSS property:
1. Update the lines marked with "start" and "end" in the CSS.
2. Update `animatedPropName` in the JS below.
3. Run the experiment!

Note: the layout CSS of this codepen itself can bork some animations. For example, animating `float` does nothing because the subject div is positioned via `grid`.
*/

const animatedPropName = 'height';

/* you probably don't need to change anything below */

const start = document.getElementById('start');
const subject = document.getElementById('subject');
const output = document.getElementById('output');
const propName = document.getElementById('prop-name');

start.addEventListener('click', startExperiment);
subject.addEventListener('animationstart', startAnimation);
subject.addEventListener('animationend', endAnimation);

propName.innerText = animatedPropName;

let isAnimating = false;
let startTime;

function startExperiment() {
// change button
start.disabled = true;
start.innerText = 'Animating...';

// clear previous experiment
subject.classList.remove('animate');
output.innerText = '';

// begin animation
addSnapshot('start');
subject.classList.add('animate');
}

function startAnimation() {
isAnimating = true;
startTime = Date.now();
window.requestAnimationFrame(update);
}

function endAnimation() {
// wrap things up
isAnimating = false;
addSnapshot('end');

// change button
start.disabled = false;
start.innerText = 'Restart Animation';
}

function update() {
if (isAnimating) {
const elapsedTime = Date.now() - startTime;
addSnapshot(elapsedTime + 'ms');
window.requestAnimationFrame(update);
}
}

function addSnapshot(time) {
const styles = window.getComputedStyle(subject);
const propValue = styles.getPropertyValue(animatedPropName);
output.innerText += time.padStart(5) + ': ' + propValue + '\n';

// scroll output to bottom
output.scrollTop = output.scrollHeight;
}
todo:根据链接中第二个 z-index demo,创建一个轮盘展示组建;

Date:
Words:
1283
Time to read:
6 mins