<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>不规则切图 Demo(clip-path)</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, Segoe UI, Microsoft Yahei, Arial;
margin: 18px;
color: #111;
}
.controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.controls input[type="range"] {
width: 140px;
}
.canvas-wrap {
position: relative;
display: inline-block;
background: #eee;
}
.container {
position: relative;
overflow: visible;
}
.piece {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: 0 0;
background-size: contain;
will-change: transform;
transition: transform 800ms cubic-bezier(0.2, 0.9, 0.2, 1);
}
.btn {
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #bbb;
background: white;
cursor: pointer;
}
.row {
display: flex;
gap: 6px;
align-items: center;
}
label {
font-size: 13px;
color: #333;
}
footer {
margin-top: 14px;
color: #666;
font-size: 13px;
}
</style>
</head>
<body>
<h2>不规则切图并无缝还原 — Demo</h2>
<div class="controls">
<div class="row">
<label>选择图片:</label>
<input id="file" type="file" accept="image/*" />
</div>
<div class="row">
<label>默认图片:</label>
<button id="useDefault" class="btn">加载示例</button>
</div>
<div class="row">
<label>列(cols)</label>
<input id="cols" type="range" min="2" max="12" value="6" />
<span id="colsVal">6</span>
</div>
<div class="row">
<label>行(rows)</label>
<input id="rows" type="range" min="2" max="12" value="4" />
<span id="rowsVal">4</span>
</div>
<div class="row">
<label>抖动幅度(jitter)</label>
<input
id="jitter"
type="range"
min="0"
max="0.5"
step="0.01"
value="0.18"
/>
<span id="jitterVal">0.18</span>
</div>
<div class="row">
<label>分散距离</label>
<input id="scatter" type="range" min="20" max="800" value="240" />
<span id="scatterVal">240</span>
</div>
<div class="row">
<label>块延迟(ms)</label>
<input id="stagger" type="range" min="0" max="200" value="30" />
<span id="staggerVal">30</span>
</div>
<div class="row">
<button id="make" class="btn">生成切片</button>
<button id="scatterBtn" class="btn">分散</button>
<button id="restore" class="btn">还原</button>
<button id="randomize" class="btn">随机分散</button>
</div>
</div>
<div id="canvasWrap" class="canvas-wrap">
<div id="container" class="container"></div>
<div
id="overlay"
style="position: absolute; left: 0; top: 0; pointer-events: none"
></div>
</div>
<footer>
说明:采用规则网格的顶点抖动来生成共享顶点的多边形,保证拼接时无缝隙。点击“分散”会把每块随机平移并旋转,点击“还原”会回到原位并无缝合成。
</footer>
<script>
(() => {
const file = document.getElementById("file");
const useDefault = document.getElementById("useDefault");
const container = document.getElementById("container");
const canvasWrap = document.getElementById("canvasWrap");
const colsInput = document.getElementById("cols");
const rowsInput = document.getElementById("rows");
const jitterInput = document.getElementById("jitter");
const scatterInput = document.getElementById("scatter");
const staggerInput = document.getElementById("stagger");
const colsVal = document.getElementById("colsVal");
const rowsVal = document.getElementById("rowsVal");
const jitterVal = document.getElementById("jitterVal");
const scatterVal = document.getElementById("scatterVal");
const staggerVal = document.getElementById("staggerVal");
const makeBtn = document.getElementById("make");
const scatterBtn = document.getElementById("scatterBtn");
const restoreBtn = document.getElementById("restore");
const randomizeBtn = document.getElementById("randomize");
let img = new Image();
let pieces = [];
let naturalW = 800,
naturalH = 500;
function setTextInputs() {
colsVal.textContent = colsInput.value;
rowsVal.textContent = rowsInput.value;
jitterVal.textContent = jitterInput.value;
scatterVal.textContent = scatterInput.value;
staggerVal.textContent = staggerInput.value;
}
setTextInputs();
colsInput.oninput =
rowsInput.oninput =
jitterInput.oninput =
scatterInput.oninput =
staggerInput.oninput =
setTextInputs;
useDefault.onclick = () => {
loadImage("https://picsum.photos/1200/800?random=1");
};
file.onchange = (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
const url = URL.createObjectURL(f);
loadImage(url, () => URL.revokeObjectURL(url));
};
function loadImage(src, cb) {
img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
naturalW = img.naturalWidth;
naturalH = img.naturalHeight;
prepareCanvas();
if (cb) cb();
};
img.onerror = () => {
alert("图片加载失败");
};
img.src = src;
}
function prepareCanvas() {
container.innerHTML = "";
container.style.width = naturalW + "px";
container.style.height = naturalH + "px";
canvasWrap.style.width = naturalW + "px";
canvasWrap.style.height = naturalH + "px";
pieces = [];
}
function generatePieces() {
container.innerHTML = "";
pieces = [];
const cols = parseInt(colsInput.value, 10);
const rows = parseInt(rowsInput.value, 10);
const jitter = parseFloat(jitterInput.value);
const cellW = naturalW / cols;
const cellH = naturalH / rows;
const points = [];
for (let r = 0; r <= rows; r++) {
points[r] = [];
for (let c = 0; c <= cols; c++) {
const gx = c * cellW;
const gy = r * cellH;
const maxJx = cellW * jitter;
const maxJy = cellH * jitter;
const rx =
c === 0 || c === cols ? 0 : (Math.random() * 2 - 1) * maxJx;
const ry =
r === 0 || r === rows ? 0 : (Math.random() * 2 - 1) * maxJy;
let px = Math.round(Math.max(0, Math.min(naturalW, gx + rx)));
let py = Math.round(Math.max(0, Math.min(naturalH, gy + ry)));
points[r][c] = { x: px, y: py };
}
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const p0 = points[r][c];
const p1 = points[r][c + 1];
const p2 = points[r + 1][c + 1];
const p3 = points[r + 1][c];
const poly = [p0, p1, p2, p3];
const el = document.createElement("div");
el.className = "piece";
el.style.width = naturalW + "px";
el.style.height = naturalH + "px";
el.style.backgroundImage = `url(${img.src})`;
el.style.backgroundSize = naturalW + "px " + naturalH + "px";
const coords = poly.map((p) => `${p.x}px ${p.y}px`).join(",");
el.style.clipPath = `polygon(${coords})`;
const idx = r * cols + c;
el.dataset.index = idx;
el.style.transitionDelay = "0ms";
el.dataset.cx = Math.round((p0.x + p1.x + p2.x + p3.x) / 4);
el.dataset.cy = Math.round((p0.y + p1.y + p2.y + p3.y) / 4);
container.appendChild(el);
pieces.push(el);
}
}
}
function scatter(randomSeed = false) {
const max = parseInt(scatterInput.value, 10);
const step = parseInt(staggerInput.value, 10) || 0;
pieces.forEach((el, i) => {
const angle = (Math.random() * 2 - 1) * 40;
const tx = (Math.random() * 2 - 1) * max;
const ty = (Math.random() * 2 - 1) * max;
el.style.zIndex = 1000 + i;
el.style.transitionDelay = `${i * step}ms`;
el.style.transform = `translate(${tx}px,${ty}px) rotate(${angle}deg)`;
});
}
function restore() {
const step = parseInt(staggerInput.value, 10) || 0;
const total = pieces.length;
pieces.forEach((el, i) => {
const delay = (total - 1 - i) * step;
el.style.transitionDelay = `${delay}ms`;
el.style.transform = "none";
el.style.zIndex = 1;
});
}
makeBtn.onclick = () => {
if (!img.src) loadImage("https://picsum.photos/1200/800?random=2");
setTimeout(generatePieces, 60);
};
scatterBtn.onclick = () => scatter();
randomizeBtn.onclick = () => {
generatePieces();
setTimeout(() => scatter(), 40);
};
restoreBtn.onclick = () => restore();
loadImage("https://picsum.photos/1200/800?random=3");
img.onload = () => {
prepareCanvas();
generatePieces();
};
})();
</script>
</body>
</html>