# 4×4 貼圖合圖頁面說明（`sheet-4x4.blade.php`）

對應實作檔案：[resources/views/line-sticker/sheet-4x4.blade.php](../resources/views/line-sticker/sheet-4x4.blade.php)

本頁為**單一 Blade 內嵌**的完整 HTML 文件：`<head>` 內含 Google Fonts 與整段 `<style>`，`<body>` 結尾載入 [JSZip 3.10.1](https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js) 與整段 `<script>`。**不經 Laravel API**、圖片僅在瀏覽器內處理。

---

## 1. 整體版面（由上到下）

| 區塊 | 說明 |
|------|------|
| **`.topbar`** | 紫色漸層橫條，說明「限 4×4（16 格）合圖」與去背前提（近似純色背景）。 |
| **`header`** | 左：Logo + 標題「4×4 貼圖合圖」與副標「去背 + ZIP・限 4×4 貼圖」。右：**去背 Toggle**（`#rmbgToggle`）+ **已儲存計數**（`#cnt` / 16）。 |
| **`.app`** | 主工作區，桌機為**左右雙欄** flex。 |

### 1.1 左欄 `.lpanel`（約 58% 寬）

1. **`.phdr`**（`#phdr`）：短說明文字，載入圖後會改為「偵測錯誤可手動畫框裁切」。
2. **`.bg-swatch-wrap`**（`#bgSwatchWrap`，初始 `display:none`）：顯示「偵測背景色」、可點擊的色塊（`#bgSwatch`）、HEX（`#bgHex`）、自動容差（`#bgAutoThresh`）。
3. **`.cwrap`**（`#cwrap`）：相對定位容器，內含兩層互斥 UI：
   - **`#dropZone`**：未選圖時顯示；拖曳／點擊觸發檔案選擇（`#fileInput`，`accept="image/*"`）。
   - **`#edArea`**：選圖後顯示（加 class `on`）；內有 **`#mainCv`**（底圖）與 **`#ovCv`**（選框 overlay，疊在 main 上）。

### 1.2 右欄 `.rpanel`（約 42% 寬）

由上到下：

1. **`.csec`**：裁切框數值 — `X/Y/寬/高`（`#iX` `#iY` `#iW` `#iH`）與即時顯示尺寸 **`#szdsp`**。
2. **`.abtns`**：按鈕網格（多數全寬 `grid-column:1/-1`）：
   - `#scrollHint`：僅在行動裝置顯示，提示往下滑。
   - `#btnAuto`：自動偵測 16 格（載入圖後才 `disabled=false`）。
   - `#btnCrop`：手動裁切並加入（框選足夠大後啟用）。
   - 重設框選、下載 ZIP（`#btnDl`）、清空、全部重來（`location.reload()`）。
3. **`.psec`**：「已裁切預覽」標題（`#pcnt` 顯示 `n / 16`）+ **`#pgrid`** 4 欄網格。
4. **`.ibar`**：檔名（`#iName`）、原圖尺寸（`#iSize`）。

### 1.3 浮層

- **`#lb`**：Lightbox，點預覽縮圖時顯示大圖 `#lbImg`。
- **`#toast`**：底部短訊息（`toast()` 控制 class `on`）。

---

## 2. CSS 排版要點

- **根節點**：`html, body` 為 `height:100%`、`overflow:hidden`、`display:flex; flex-direction:column`，整頁不捲動，由內部 `.pgrid` 等區塊各自捲動。
- **色彩變數**：`:root` 定義 `--bg`、`--surf`、`--pur`（紫）、`--acc`（綠）、`--acc2`（青）等，風格接近 CropAlpha。
- **安全區**：`--sab` / `--sat` 使用 `env(safe-area-inset-*)`，底部 `.ibar`、`.toast`、`#lb` 有加上 `padding-bottom: calc(... + var(--sab))`。
- **桌機**：`.app` 橫向 flex；`.lpanel` 58%、`.rpanel` 42%，中間邊框分隔。
- **窄螢幕**（`max-width:640px`）：`.app` 改 **直向**；左欄固定約 `60dvh`、右欄可捲動；`.pgrid` 仍為 **4 欄**。
- **橫向小高度**（`max-width:900px` and `orientation:landscape` and `max-height:520px`）：再改回左右分欄，避免手機橫拿時版面擠爆。

---

## 3. 作動方式與資料流（使用者視角）

```mermaid
flowchart TD
  A[選擇或拖曳一張圖] --> B[loadFile: FileReader + Image]
  B --> C[detectBgColor: 背景色與 autoThresh]
  B --> D[layoutCanvas: 縮放畫布 + 畫原圖]
  B --> E[resetSel: 預設框選尺寸]
  B --> F[啟用 btnAuto]
  F --> G{使用者操作}
  G -->|自動偵測 16 格| H[autoDetect]
  G -->|手動拖曳框選| I[onDown/onMove/onUp on ovCv]
  I --> J[doCrop: cropToCanvas + items.push]
  H --> J
  J --> K[renderPreview: 4x4 grid]
  K --> L[下載 ZIP: JSZip + blob 下載]
```

### 3.1 載入圖片

```384:405:resources/views/line-sticker/sheet-4x4.blade.php
function loadFile(file) {
  curName = file.name;
  const r = new FileReader();
  r.onload = e => {
    const image = new Image();
    image.onload = () => {
      img = image;
      dropZone.style.display = 'none';
      document.getElementById('edArea').classList.add('on');
      document.getElementById('phdr').textContent = '偵測錯誤可手動畫框裁切';
      // ...
      detectBgColor();
      layoutCanvas();
      resetSel();
      document.getElementById('btnAuto').disabled = false;
    };
    image.src = e.target.result;
  };
  r.readAsDataURL(file);
}
```

- 拖曳：`dropZone` 的 `dragover` / `dragleave` / `drop`（只取第一張 `image/*`）。
- 點擊空白處：`dropZone` click 觸發隱藏 `fileInput`；`change` 時同樣 `loadFile` 第一張。

### 3.2 畫布座標系

- **`sc`**：將原圖 `naturalWidth/Height` 等比縮入 `#cwrap` 的顯示比例（不超過 1，不放大）。
- **`dW`, `dH`**：`mainCv` / `ovCv` 的像素尺寸（與螢幕上 CSS 像素一致）。
- **選框 `cr`**：在 **顯示座標**（scaled）下儲存 `{x,y,w,h}`；換算回原圖像素：`Math.round(cr.x/sc)` 等（見 `doCrop`、`autoDetect` 內 `cropToCanvas`）。
- **`alignOverlay()`**：用 `getBoundingClientRect` 把 `ovCv` 的 `left/top` 對齊到 `mainCv`，避免 flex 置中造成 overlay 偏移。

### 3.3 手動框選

- 事件綁在 **`#ovCv`**：`mousedown` / `mousemove` / `mouseup` / `mouseleave` 與對應 `touch*`（`preventDefault` + `passive:false`）。
- 拖曳中更新 `cr` 並 `syncFields()` 寫回 `iX`～`iH`；放開後若寬高皆大於 4px，啟用「手動裁切並加入」。
- 使用者也可直接改數字欄位：`input` 事件把 `cr` 設為 `原值 * sc` 並重畫 overlay。

### 3.4 自動偵測 16 格

```827:832:resources/views/line-sticker/sheet-4x4.blade.php
// ─── Auto-detect 4×4 grid ────────────────────────────────────────────────────
/*
  Always assumes 4×4 grid. Finds 3 row cuts and 3 col cuts by:
  1. Equal-division baseline (robust even when gaps are tiny or zero)
  2. Snap each cut to nearest local minimum within ±20% of cell size
*/
```

- 在**全解析度** offscreen canvas 上讀取 `ImageData`。
- 用目前背景色 `getBgColor()` 與 **`autoThresh`** 建二值化「前景」遮罩 `fg`（色距平方大於門檻為 1）。
- **列投影** `rowProj[y]`：該列前景比例；`findCuts` 在平滑後的投影上，以 **1/4、2/4、3/4 等分點** 為中心，在 **±20% 格寬** 內找區域最小值作為切割線，得到 4 個列區間。
- 每個列區間內再做 **欄投影** 與同樣的 `findCuts`，得到最多 16 個 `rect`；過濾寬高須大於 10px。
- 依 `y` 再 `x` 排序後，最多加入 **`MAX - items.length`** 個（`MAX === 16`）。
- 成功後 **`drawDetectedBoxes`** 在 overlay 上畫半透明遮罩與編號框。

### 3.5 去背 Toggle 與演算法

- **`rmbgEnabled`**（預設 `true`）：`toggleRmbg()` 切換 track 的 class `on` 與標籤「去背 / 保留背景」。
- **背景色**：`detectBgColor()` 在小尺寸取樣圖上，對四角邊帶不透明像素取平均 RGB；再以直方圖 + 高斯平滑找「背景峰後第一個谷」換算成 **`autoThresh`**（限制在 5～60）。
- **手動背景**：點 `#bgSwatch` 會動態建立 `<input type="color">`，更新 `manualBg` 並呼叫 `detectBgColorForThresh()` 重算門檻。
- **`cropToCanvas(sx,sy,sw,sh)`**：從原圖 `img` 裁到 offscreen canvas；若 `rmbgEnabled` 則呼叫 **`removeBackground`** 再 **`autotrimCanvas`**（依 alpha 外接矩形裁掉透明邊）。

`removeBackground` 重點步驟（同檔約 651～738 行）：

1. 依 RGB 與背景距離 ≤ `thresh²` 建 `isBg`；全透明像素視為背景候選。
2. 從**四邊**對 `isBg` 做 **flood-fill**，只移除「與邊界連通」的背景（避免中間同色洞被誤刪）。
3. 與已移除像素相鄰者，alpha 乘 0.25 做 **1px 柔邊**。
4. 對不透明連通區塊做標記，移除過小區塊（面積門檻約 `max(10, W*H*0.0025)`）。
5. **洞填補**：對「透明但非邊界連通」的洞，把 RGBA 還原成裁切前的 `orig` 並設 alpha=255。

### 3.6 預覽與 ZIP

- **`items`**：`{ dataUrl, name, w, h }[]`；`name` 一律 **`pad2(序號) + '.png'`**（例如 `01.png`…`16.png`）。
- **`removeItem`**：刪除一筆後會 **重新編號** 所有 `name`，確保 ZIP 內檔名連續。
- **`downloadAll`**：`JSZip()` 逐檔加入（binary 來自 `dataUrlToUint8`），`generateAsync({ type:'blob', compression:'STORE' })`，觸發 `<a download>`，檔名 **`4x4_sticker_${items.length}張.zip`**。

### 3.7 行動裝置捲動

```1028:1030:resources/views/line-sticker/sheet-4x4.blade.php
document.body.addEventListener('touchmove', e => {
  if (!e.target.closest('.rpanel')) e.preventDefault();
}, {passive:false});
```

- 目的：避免整頁被手指拖動造成「橡皮筋」捲動；**僅在 `.rpanel` 內**允許垂直捲動（按鈕區與預覽 grid）。

---

## 4. 重要常數與限制

| 項目 | 值 / 行為 |
|------|-----------|
| `MAX` | `16`（[L349](resources/views/line-sticker/sheet-4x4.blade.php#L349)） |
| 自動偵測後矩形數 | 理論上 16；若已有部分手動加入，只補滿到 16 張為止 |
| 後端請求 | 無（僅載入頁面本身 + CDN：JSZip、Google Fonts） |

---

## 5. 維護時可從哪裡改起

| 想改的內容 | 建議閱讀區段 |
|------------|----------------|
| 文案、按鈕順序、DOM 結構 | HTML 約 [L212](resources/views/line-sticker/sheet-4x4.blade.php#L212)～[L327](resources/views/line-sticker/sheet-4x4.blade.php#L327) |
| 顏色、雙欄比例、RWD | `<style>` 約 [L10](resources/views/line-sticker/sheet-4x4.blade.php#L10)～[L207](resources/views/line-sticker/sheet-4x4.blade.php#L207) |
| 背景偵測／門檻 | `detectBgColor` / `detectBgColorForThresh` 約 [L412](resources/views/line-sticker/sheet-4x4.blade.php#L412)～[L523](resources/views/line-sticker/sheet-4x4.blade.php#L523) |
| 4×4 切割線 | `autoDetect` 約 [L833](resources/views/line-sticker/sheet-4x4.blade.php#L833)～[L948](resources/views/line-sticker/sheet-4x4.blade.php#L948) |
| 去背與裁切後修邊 | `removeBackground` / `autotrimCanvas` 約 [L651](resources/views/line-sticker/sheet-4x4.blade.php#L651)～[L756](resources/views/line-sticker/sheet-4x4.blade.php#L756) |
| ZIP 檔名與內容 | `downloadAll` 約 [L986](resources/views/line-sticker/sheet-4x4.blade.php#L986)～[L1004](resources/views/line-sticker/sheet-4x4.blade.php#L1004) |

---

## 6. 參考版型來源

視覺與互動邏輯源自 CropAlpha 去背版：[https://cidercoder.github.io/Crop-tool-rmbg/](https://cidercoder.github.io/Crop-tool-rmbg/)（本專案已改為固定 16 格、移除「上傳下一張」累積、ZIP 檔名與輸出檔名規則等。）
