使用clip-path将一张图片,切成不规则大小,并且可以还原

65 阅读2分钟
<!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;

          // create grid of points (rows+1) x (cols+1)
          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;
              // jitter interior points only
              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";
              // clip-path in px coordinates
              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();

        // init with default
        loadImage("https://picsum.photos/1200/800?random=3");
        // auto generate when loaded
        img.onload = () => {
          prepareCanvas();
          generatePieces();
        };
      })();
    </script>
  </body>
</html>