Loading...
Loading...
Compare original and translation side by side
requestAnimationFramerequestAnimationFrame"When we align the interface to the way we think and move, something magical happens — it stops feeling like a computer and starts feeling like a seamless extension of us."
“当我们让界面贴合人类的思考与行动方式时,神奇的事情就会发生——它不再像计算机,而是成为我们无缝的延伸。”
click/* Feedback lives on the press, and it's instant */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}click/* 反馈在按下时触发,且即时生效 */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}"Touch and content should move together."
setPointerCapturepointermoveel.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // respect where they grabbed
// ...track position + timestamp history for velocity
});“触摸操作与内容应同步移动。”
setPointerCapturepointermoveel.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // 尊重用户抓取的位置
// ...记录位置+时间戳历史以计算速度
});"The thought and the gesture happen in parallel."
@keyframes“思考与手势是并行发生的。”
@keyframes"Think of animation as a conversation between you and the object, not something prescribed by the interface."
1.0< 1.01.00.8| Interaction | Damping | Response |
|---|---|---|
| Move / reposition (e.g. PiP) | | |
| Rotation | | |
| Drawer / sheet | | |
bouncedurationdamping: 1.0import { animate } from 'motion';
// Critically damped default (no overshoot)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// Momentum interaction — a little bounce, only because a flick preceded it
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });“将动画视为你与对象之间的对话,而非界面预设的内容。”
1.0< 1.01.00.8| 交互类型 | 阻尼 | 响应时间 |
|---|---|---|
| 移动/重新定位(如画中画) | | |
| 旋转 | | |
| 抽屉/面板 | | |
bouncedurationdamping: 1.0import { animate } from 'motion';
// 临界阻尼默认值(无过冲)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// 动量交互——添加少量弹跳,仅因为之前有轻拂动作
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });relativeVelocity = gestureVelocity / (targetValue − currentValue)y=50y=15050 / 100 = 0.5velocityrelativeVelocity = gestureVelocity / (targetValue − currentValue)y=50y=15050 / 100 = 0.5velocity"Take a small input and make a big output."
// decelerationRate ≈ 0.998 for normal scroll feel; 0.99 for snappier
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // choose target from the projection
animateSpringTo(target, { velocity: releaseVelocity }); // then hand off velocity (§5)v²/(2·decel)“将小输入转化为大输出。”
// decelerationRate ≈ 0.998为普通滚动手感;0.99为更灵敏的滚动
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // 根据预测结果选择目标
animateSpringTo(target, { velocity: releaseVelocity }); // 然后传递速度(第5节)v²/(2·decel)"If something disappears one way, we expect it to emerge from where it came."
transform-origin“如果元素从某个方向消失,我们期望它从同一方向出现。”
transform-origin// The further past the bound, the less the element follows — real things slow before they stop
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}// 超出边界越多,元素跟随的幅度越小——真实物体在停止前会减速
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}swipeleftswipeleftrequestAnimationFrameCADisplayLinktransformopacitywill-changerequestAnimationFrameCADisplayLinktransformopacitywill-changebackdrop-filterbackdrop-filter: blur().toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* bright top edge = light catching the material */
}backdrop-filterbackdrop-filter: blur().toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* 亮顶边模拟光线照射材质 */
}prefers-reduced-motion: reduceprefers-reduced-transparency: reduceprefers-contrast: more@media (prefers-reduced-motion: reduce) {
.sheet { transition: opacity 200ms ease; transform: none !important; }
}
@media (prefers-reduced-transparency: reduce) {
.toolbar { background: white; backdrop-filter: none; }
}prefers-reduced-motion: reduceprefers-reduced-transparency: reduceprefers-contrast: more@media (prefers-reduced-motion: reduce) {
.sheet { transition: opacity 200ms ease; transform: none !important; }
}
@media (prefers-reduced-transparency: reduce) {
.toolbar { background: white; backdrop-filter: none; }
}letter-spacing0remem:root { font: 100%/1.5 system-ui, sans-serif; } /* body: system font, comfortable leading */
.display {
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.05; /* tight leading for large text */
letter-spacing: -0.02em; /* negative tracking as it grows */
font-optical-sizing: auto;
}letter-spacing0remem:root { font: 100%/1.5 system-ui, sans-serif; } /* 正文:系统字体,舒适行高 */
.display {
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.05; /* 大文本使用紧凑行高 */
letter-spacing: -0.02em; /* 文本变大时使用负字距 */
font-optical-sizing: auto;
}| Need | Technique | Concrete value |
|---|---|---|
| Default UI spring | Critically damped, no overshoot | |
| Momentum / flick spring | Under-damped, slight bounce | |
| Gesture → spring velocity | Hand off release velocity | |
| Flick landing point | Project momentum | |
| Interrupt cleanly | Start from presentation (live) value | read the on-screen transform |
| Avoid reversal "brick wall" | Carry velocity through re-target | spring that blends velocity |
| Reversible transition | Mirror the easing curve | inverse cubic-bézier |
| Decide reverse vs. commit | Use velocity sign, not position | at release |
| 1:1 drag | Pointer Events + capture | respect the grab offset |
| Feedback | On pointer-down, continuous | never only at the end |
| Boundary | Rubber-band, don't hard-stop | progressive resistance |
| Translucent chrome | | content scrolls under |
| Type tracking | Size-specific, never fixed | tighten large text ( |
| Reduced motion | Cross-fade, not slide/spring | |
| 需求 | 技巧 | 具体值 |
|---|---|---|
| 默认UI弹簧动画 | 临界阻尼,无过冲 | |
| 动量/轻拂弹簧动画 | 欠阻尼,轻微弹跳 | |
| 手势→弹簧速度传递 | 传递释放时的速度 | 若需归一化则使用 |
| 轻拂落点预测 | 动量预测 | |
| 平滑中断 | 从当前显示值开始 | 读取屏幕上的实时变换值 |
| 避免反向“砖墙”效果 | 重定位时延续速度 | 支持融合速度的弹簧动画 |
| 可逆过渡效果 | 镜像缓动曲线 | 反向三次贝塞尔曲线 |
| 判断反向还是确认 | 使用速度符号,而非位置 | 在释放时判断 |
| 1:1拖拽 | Pointer Events + 捕获 | 尊重抓取偏移量 |
| 反馈 | 指针按下时触发,持续提供 | 绝不仅在结束时提供 |
| 边界处理 | 橡皮筋效果,而非硬停止 | 渐进式阻力 |
| 半透明界面 | | 内容在下方滚动 |
| 排版字距 | 随尺寸变化,非固定值 | 大文本收紧( |
| 减少动效 | 交叉淡入淡出,而非滑动/弹簧 | |