横刷动画学习
知识点梳理
css clip-path
用于裁剪元素的可见区域——被裁掉的部分完全不可见、不可交互,但元素本身仍占据原来的布局空间。
inset() 函数
inset(top right bottom left) 表示从四条边向内收缩裁剪:
top
┌─────────────────┐
│ ┌───────────┐ │
left│ │ 可见区域 │ │right
│ └───────────┘ │
└─────────────────┘
bottom
只露出左侧 30% 的图片内容
clipPath: inset(0 70% 0 0)
/* ↑ ↑ ↑ ↑
上 右 下 左 */
top = 0:上边不裁
right = 70%:从右边裁掉 70%
bottom = 0:下边不裁
left = 0:左边不裁
clipPath还支持其他形状函数,具体自查文档看
为什么用 clipPath 而不是其他方案

clipPath 在现代浏览器中属于 合成层属性(compositor-friendly),配合 requestAnimationFrame 逐帧更新百分比,动画非常流畅。
performance.now()
高精度时间戳,返回从页面打开(navigationStart)到调用时刻的毫秒数,精度可达微秒级(小数点后多位)。
requestAnimationFrame 的回调参数 now 本身就是 performance.now() 的值
源码展示
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="referrer" content="no-referrer">
<title>初始横刷演示</title>
<style>
:root {
--page-bg-start: #f6f2ea;
--page-bg-end: #f0e5d6;
--viewer-bg: #ece7e0;
--viewer-radius: 16px;
--viewer-shadow: 0 20px 50px rgba(60, 43, 20, 0.12);
--handle-width: 2px;
--handle-color: rgba(255, 255, 255, 0.58);
--handle-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
min-height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", sans-serif;
background: linear-gradient(135deg, var(--page-bg-start), var(--page-bg-end));
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.demo-shell {
width: min(100%, 600px);
}
.image-viewer {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
background: var(--viewer-bg);
overflow: hidden;
border-radius: var(--viewer-radius);
box-shadow: var(--viewer-shadow);
}
.viewer-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center center;
pointer-events: none;
user-select: none;
filter: blur(20px);
transform: scale(1.1);
transition: filter 0.6s ease-out, transform 0.6s ease-out;
will-change: clip-path, filter, transform;
}
.viewer-image.loaded {
filter: blur(0);
transform: scale(1);
}
.image-before {
z-index: 2;
}
.image-after {
z-index: 10;
clip-path: inset(0 0 0 0);
}
.compare-handle {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: var(--handle-width);
background: var(--handle-color);
z-index: 12;
transform: translateX(-50%);
pointer-events: none;
box-shadow: var(--handle-shadow);
opacity: 0;
}
@media (max-width: 640px) {
body {
padding: 16px;
}
.image-viewer {
border-radius: 14px;
}
}
</style>
</head>
<body>
<div class="demo-shell">
<div class="image-viewer" id="imageViewer" aria-label="初始状态横刷动画演示">
<img src="" alt="Before" class="viewer-image image-before" id="imgBefore">
<img src="" alt="After" class="viewer-image image-after" id="imgAfter">
<div class="compare-handle" id="compareHandle"></div>
</div>
</div>
<script>
const DEMO_IMAGES = [
{
id: 'template-1',
src: 'https://zh.gtimg.com/m-1/bll-apollo/common/upload/67ee0017687b2ed4cdc092fb/fc51a4e594336a71bd6f0b91d76f5b98.png',
position: 'center center'
},
{
id: 'template-9',
src: 'https://zh.gtimg.com/m-1/bll-apollo/common/upload/67ee0017687b2ed4cdc092fb/1687a052ed4db21c7112ef8f82939d12.png',
position: 'center center'
},
{
id: 'template-lion-dance',
src: 'https://search.imtt.qq.com/ai-card-assets/peel-off-card/horse-year/lion-dance-new.jpg',
position: 'center center'
},
{
id: 'template-shishi-ruyi',
src: 'https://search.imtt.qq.com/ai-card-assets/peel-off-card/horse-year/shishi-ruyi-v2.webp',
position: 'center center'
},
{
id: 'template-gonghe-xinxi',
src: 'https://search.imtt.qq.com/ai-card-assets/peel-off-card/horse-year/gonghe-xinxi-v2.webp',
position: 'center center'
}
];
const imgBefore = document.getElementById('imgBefore');
const imgAfter = document.getElementById('imgAfter');
const compareHandle = document.getElementById('compareHandle');
const SWEEP_DURATION = 2500;
const SWEEP_PAUSE = 1000;
const INITIAL_DELAY = 300;
let isRunning = false;
let currentIndex = 0;
let direction = 1;
let animationFrameId = null;
let pauseTimer = null;
let startTimer = null;
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
function updateImagePosition(position) {
imgBefore.style.objectPosition = position;
imgAfter.style.objectPosition = position;
}
function loadImageInto(imgElement, src) {
imgElement.classList.remove('loaded');
const tempImg = new Image();
tempImg.onload = () => {
imgElement.src = src;
imgElement.classList.add('loaded');
};
tempImg.src = src;
imgElement.src = src;
}
function renderSweep(percent) {
compareHandle.style.left = `${percent}%`;
imgAfter.style.clipPath = `inset(0 0 0 ${percent}%)`;
}
function stopSweepDemo() {
isRunning = false;
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (pauseTimer) {
clearTimeout(pauseTimer);
pauseTimer = null;
}
if (startTimer) {
clearTimeout(startTimer);
startTimer = null;
}
}
function animateSweep(fromPercent, toPercent, onComplete) {
const startTime = performance.now();
function step(now) {
if (!isRunning) {
return;
}
const elapsed = now - startTime;
const progress = Math.min(elapsed / SWEEP_DURATION, 1);
const eased = easeInOutQuad(progress);
const currentPercent = fromPercent + (toPercent - fromPercent) * eased;
renderSweep(currentPercent);
if (progress < 1) {
animationFrameId = requestAnimationFrame(step);
return;
}
animationFrameId = null;
onComplete();
}
animationFrameId = requestAnimationFrame(step);
}
function runSweepLoop() {
if (!isRunning) {
return;
}
const fromPercent = parseFloat(compareHandle.style.left || '0');
const targetPercent = direction === 0 ? 0 : 100;
animateSweep(fromPercent, targetPercent, () => {
if (!isRunning) {
return;
}
pauseTimer = setTimeout(() => {
if (!isRunning) {
return;
}
currentIndex = (currentIndex + 1) % DEMO_IMAGES.length;
const nextItem = DEMO_IMAGES[(currentIndex + 1) % DEMO_IMAGES.length];
if (direction === 0) {
loadImageInto(imgBefore, nextItem.src);
} else {
loadImageInto(imgAfter, nextItem.src);
updateImagePosition(nextItem.position);
}
direction = 1 - direction;
runSweepLoop();
}, SWEEP_PAUSE);
});
}
function startSweepDemo() {
stopSweepDemo();
isRunning = true;
currentIndex = 0;
direction = 1;
compareHandle.style.opacity = '1';
loadImageInto(imgAfter, DEMO_IMAGES[0].src);
loadImageInto(imgBefore, DEMO_IMAGES[1].src);
updateImagePosition(DEMO_IMAGES[0].position);
renderSweep(0);
startTimer = setTimeout(() => {
runSweepLoop();
}, INITIAL_DELAY);
}
startSweepDemo();
window.addEventListener('beforeunload', stopSweepDemo);
</script>
</body>
</html>

浙公网安备 33010602011771号