效果預覽
將兩張相同的圖片 position:absolute
定位在一起,透過 clip-path:polygon()
這個 CSS 屬性將元素裁剪。第一張圖片保留上半部分,第二張圖片保留下半部分,將上半部分向上移動,下半部分向下移動,形成視覺的切割效果。
front 放置第一張圖片,back 放置第二張圖片,canvas 用來繪製切割線。三個元素定位在相同的位置,保持 canvas 顯示在最上面。
<div class="box">
<div class="block back"></div>
<div class="block front"></div>
<canvas width="400" height="600" class="block canvas"></canvas>
</div>
<style>
html,
body {
margin: 0;
padding: 0;
}
.box {
width: 100vw;
height: 100vh;
position: relative;
}
.block {
width: 400px;
height: 600px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.back {
background-color: #f00;
}
.front {
background-color: #f00;
}
.canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
// 外層盒子
const box = document.querySelector(".box");
// canvas 用來繪製切割線
const canvas = document.querySelector(".canvas");
// 第一個元素
const front = document.querySelector(".front");
// 第二個元素
const back = document.querySelector(".back");
// canvas 上下文對象
const ctx = canvas.getContext("2d");
// 是否開始繪製
let startDraw = false;
// 切割的起始點坐標
let clipPoint1 = [0, 0];
// 切割終點坐標
let clipPoint2 = [0, 0];
對 box 監聽滑鼠按下事件,如果是在 canvas 中按下的則不處理。滑鼠移動進入 canvas 時記錄切割的起始坐標,同時開始繪製切割線。滑鼠在 canvas 中移動,實時繪製切割線。滑鼠移出 canvas 時記錄切割的終點坐標,同時停止繪製切割線。
// 監聽滑鼠按下事件
box.addEventListener("mousedown", (e) => {
// 如果滑鼠不是在 canvas 外按下的則不處理
if (e.target == canvas) {
return;
}
// 開始繪製切割線
startDraw = true;
// 初始化坐標
clipPoint1 = [0, 0];
clipPoint2 = [0, 0];
// 監聽滑鼠進入 canvas 事件,獲取切割起始坐標,開始繪製切割線
canvas.addEventListener("mouseenter", (e) => {
clipPoint1 = [e.offsetX, e.offsetY];
ctx.beginPath();
});
// 滑鼠在canvas中移動,繪製切割線
canvas.addEventListener("mousemove", (e) => {
if (startDraw) {
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
});
// 滑鼠離開canvas,獲取切割終點坐標,停止繪製切割線
canvas.addEventListener("mouseleave", (e) => {
clipPoint2 = [e.offsetX, e.offsetY];
startDraw = false;
});
});
在滑鼠彈起時,根據起始和終點坐標對 front 和 back 設置 clip-path 切割元素。然後獲取元素的 top 值,設置動畫,front 向上移動,back 向下移動。
// 監聽滑鼠彈起事件
box.addEventListener("mouseup", (e) => {
// 清除切割線
ctx.clearRect(0, 0, 600, 800);
// 停止繪製切割線
startDraw = false;
// 如果滑鼠水平滑動的距離不超過390,則不會切割元素
// 元素寬度400,取390保持冗餘
if (Math.abs(clipPoint2[0] - clipPoint1[0]) < 390) {
return;
}
// 切割點Y坐標
let point1Y;
let point2Y;
// 無論是從左往右切割還是從右往左,保持 point1Y為左邊Y坐標,point2Y右邊Y坐標
if (clipPoint2[0] - clipPoint1[0] < 0) {
point1Y = clipPoint2[1];
point2Y = clipPoint1[1];
} else {
point1Y = clipPoint1[1];
point2Y = clipPoint2[1];
}
// 設置裁剪路徑
clipPath1 = `polygon(0 0, 400px 0, 400px ${point2Y}px, 0 ${point1Y}px)`;
clipPath2 = `polygon(0 ${point1Y}px, 400px ${point2Y}px, 400px 600px, 0 600px)`;
// 對元素裁剪
front.style.clipPath = clipPath1;
back.style.clipPath = clipPath2;
// 獲取元素的 top 屬性值
let frontTop, backTop;
frontTop = backTop = parseFloat(getComputedStyle(front).top);
// 移動的距離
let num = 0;
// 動畫id
let requestId = null;
// 切割後移動動畫
function move() {
num += 1;
frontTop -= num;
backTop += num;
// 第一張向上移動
front.style.top = frontTop + "px";
// 第二張向下移動
back.style.top = backTop + "px";
requestId = requestAnimationFrame(move);
// 移動距離大於5則結束移動
if (num >= 5) {
cancelAnimationFrame(requestId);
}
}
requestId = requestAnimationFrame(move);
});
<!DOCTYPE html>
<html lang="zh_CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>滑動切割</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
.box {
width: 100vw;
height: 100vh;
position: relative;
}
.block {
width: 400px;
height: 600px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.back {
background-color: #f00;
}
.front {
background-color: #f00;
}
.canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div class="box">
<div class="block back"></div>
<div class="block front"></div>
<canvas width="400" height="600" class="block canvas"></canvas>
</div>
<script>
// 外層盒子
const box = document.querySelector(".box");
// canvas 用來繪製切割線
const canvas = document.querySelector(".canvas");
// 第一個元素
const front = document.querySelector(".front");
// 第二個元素
const back = document.querySelector(".back");
// canvas 上下文對象
const ctx = canvas.getContext("2d");
// 是否開始繪製
let startDraw = false;
// 切割的起始點坐標
let clipPoint1 = [0, 0];
// 切割終點坐標
let clipPoint2 = [0, 0];
// 監聽滑鼠按下事件
box.addEventListener("mousedown", (e) => {
// 如果滑鼠不是在 canvas 外按下的則不處理
if (e.target == canvas) {
return;
}
// 開始繪製切割線
startDraw = true;
// 初始化坐標
clipPoint1 = [0, 0];
clipPoint2 = [0, 0];
// 監聽滑鼠進入 canvas 事件,獲取切割起始坐標,開始繪製切割線
canvas.addEventListener("mouseenter", (e) => {
clipPoint1 = [e.offsetX, e.offsetY];
ctx.beginPath();
});
// 滑鼠在canvas中移動,繪製切割線
canvas.addEventListener("mousemove", (e) => {
if (startDraw) {
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
});
// 滑鼠離開canvas,獲取切割終點坐標,停止繪製切割線
canvas.addEventListener("mouseleave", (e) => {
clipPoint2 = [e.offsetX, e.offsetY];
startDraw = false;
});
});
// 監聽滑鼠彈起事件
box.addEventListener("mouseup", (e) => {
// 清除切割線
ctx.clearRect(0, 0, 600, 800);
// 停止繪製切割線
startDraw = false;
// 如果滑鼠水平滑動的距離不超過390,則不會切割元素
// 元素寬度400,取390保持冗餘
if (Math.abs(clipPoint2[0] - clipPoint1[0]) < 390) {
return;
}
// 切割點Y坐標
let point1Y;
let point2Y;
// 無論是從左往右切割還是從右往左,保持 point1Y為左邊Y坐標,point2Y右邊Y坐標
if (clipPoint2[0] - clipPoint1[0] < 0) {
point1Y = clipPoint2[1];
point2Y = clipPoint1[1];
} else {
point1Y = clipPoint1[1];
point2Y = clipPoint2[1];
}
// 設置裁剪路徑
clipPath1 = `polygon(0 0, 400px 0, 400px ${point2Y}px, 0 ${point1Y}px)`;
clipPath2 = `polygon(0 ${point1Y}px, 400px ${point2Y}px, 400px 600px, 0 600px)`;
// 對元素裁剪
front.style.clipPath = clipPath1;
back.style.clipPath = clipPath2;
// 獲取元素的 top 屬性值
let frontTop, backTop;
frontTop = backTop = parseFloat(getComputedStyle(front).top);
// 移動的距離
let num = 0;
// 動畫id
let requestId = null;
// 切割後移動動畫
function move() {
num += 1;
frontTop -= num;
backTop += num;
// 第一張向上移動
front.style.top = frontTop + "px";
// 第二張向下移動
back.style.top = backTop + "px";
requestId = requestAnimationFrame(move);
// 移動距離大於5則結束移動
if (num >= 5) {
cancelAnimationFrame(requestId);
}
}
requestId = requestAnimationFrame(move);
});
</script>
</body>
</html>
---
原文出處:https://juejin.cn/post/7555077475740581940