兩個 300×300,邊緣差半格
同一張照片裁兩次,產物都正好 300×300:一次手動框、尺寸框裡填 300 和 300,一次點比例檔讓工具從整圖居中收攏。兩張丟進純色底按像素放大,第二條左邊留白比右邊多 1 像素、上邊比下邊多 1 像素,人朝右下挪了半格——純色底會把這 1 像素直接顯出來。
兩個嫌疑都不成立。匯出不是重採樣糊的:它走 9 參 drawImage(T292),源矩形寬高等於目標寬高,座標編碼圖實測 dest(0,0) 解碼回源 (0,0),零插值。選區也沒有半像素:全模板只有一個取整函式 clampInt(T191),x、y、寬、高全過它。真相是座標過了好幾層取整、各層方向不一致——圖片裁剪這條線上有居中除法、指標座標除以顯示縮放、drawImage 拿小數時的處理。
取整方向:半數一律朝 +∞
Math.max(lo, Math.min(hi, Math.round(v) || lo))
clampInt 先 Math.round、再把結果夾進區間。方向逐條列出:round(0.5)=1、round(2.5)=3、round(-0.5)=0——半數永遠朝正無窮進一位,不是 floor、不是 ceil,也不是逢半取偶的銀行家捨入。填 10.7 先 round 成 11、再被下界 MIN=12 抬起(T102)。帶 .5 的中間量就多佔右下那一格。
居中的那次除法,餘數落在哪邊
半格誕生在比例檔的居中邏輯裡:先按比例算塊高,再把 (oldW - w) / 2 也交給 clampInt(T177 與 T178)。只要 oldW - w 是奇數,這一除必帶 .5,而 .5 永遠向上進。401×299 取 0.75 得 224×299,餘 177,177 / 2 = 88.5,clampInt(88.5, 0, 177) 給 89,於是左邊距 89、右邊距 88:整塊比幾何中心偏右下 0.5 像素,落到檔案裡就是某一條邊多 1 像素。同一張源取 1:1 就沒事,299×299 餘數 102 是偶數,(401 - 299) / 2 = 51,左右各 51。
圖 1|同一張 401×299,偶數餘數與奇數餘數的兩種居中落點。
塊越大越不起眼:1080×1350 取 1.777778 得 1080×607、(1350 - 607) / 2 = 371.5 → y=372,上留 372、下剩 371;720×541 取 1.333333 得 720×540@(0,1),源少 1 行、下移 1 格。
螢幕上那層像素不是檔案裡那層
顯示是另一套座標:scale=Math.min(1, Math.min(520, innerWidth-80)/natW)(T152-153),本次 innerWidth=764,640 寬的圖源 scale=0.8125,預覽畫布 520×325(T196)由 5 參 drawImage(T201)重採樣而來。選區框位置另有 4 處獨立 Math.round(T204-207):sel=(17,23,300,300) → left=14px、top=19px、寬 244px,肉眼判斷居中的精度就是一格,螢幕上那個 244×244 的框和檔案裡的 300×300 本來就是兩套像素。
小數是從換算那步漏進來的:指標換回原圖座標是 toNat=(clientX-r.left)/scale(T221),clientX=100 算出 123.0769,到 pointerup 才被 clampInt(T249)取成 123。
差一格的定位實驗
同一個 300×300 走兩條路徑各畫一次,讀 dest(0,0) 解碼回源圖哪一格:
- 原點 (0.5,0.5):直接畫回 (0,0),瀏覽器自己向下取整;模板路徑 clampInt(0.5, 0, 340)=1 回 (1,1),差一格、方向右下。
- 原點 (10.7,20.3):直接畫回 (10,20),模板給 (11,20),x 右一格、y 停在 20。
- 原點 (1,1) 與 (0,0):兩條路徑完全一致,整數座標零漂移。
漂移只來自小數原點被 Math.round 改寫:一個朝 +∞、一個朝 -∞,中間那個半數就是頭像偏掉的半格。
尺寸框寫著 min=1,也裁不出 1 像素
#cp-w 與 #cp-h 寫著 min="1"(T27/T29),JS 下界是 MIN=12(T102),上界 natW,外加 if (w)(T183)的假值短路:
| 填 | sel.w | 依據 |
|---|---|---|
| 1 | 12 | 下界是 MIN=12 |
| 100.6 | 101 | 小數朝 +∞ |
| 0 或留空 | 200 | 假值被短路忽略 |
| 1000 | 640 | 上界=natW |
雙軸各自 clamp 還會靜默打破等比例:1.333333 檔輸入 w=1001,寬夾到 640、高夾到 400,w/h=1.600000(T185 不重算)。越界補邊只在源比 12 還小時真發生:8×6 輸入 w=30 → sel=12×200、右下角 RGBA=0,0,0,0、png=308B,正常路徑被 clamp 擋回內(T186/T245/T275)。
偏掉的格子可能正好是透明像素
PNG 逐像素保 alpha:源梯度 floor(255*x/639) 裁 200×100@(100,50) 後左中右仍是 39、79、119;只有 mime 是 image/jpeg 才先鋪底(T288-291),alpha 39 的 (200,100,50) 混黑底實測 31,15,8。
方向也歸這套座標管:photo_exif6.jpg 的 SOF 記 1200×900、瀏覽器解碼成 900×1200,模板只讀 naturalWidth(T151),座標都在轉正後的空間裡。「保持原格式」下 image/gif 與 image/webp 與 image/bmp 全退化成 image/png(T287),逐幀處理得先GIF 拆圖。
模板一步縮放都不做:crop 300×300 再 resize 150×150、與先 resize 到 320×200 再 crop 150×150 同為 png=1346B,而兩者的 dest(1,1) 都是源 (2,2),縮放那步已把採樣柵格挪一格(圖片尺寸調整)。
Q:同樣點比例檔,有時偏半格有時不偏,看什麼?
看 oldW 與 w 之差是奇是偶:401×299 取 0.75 餘 177 → 88.5 → 89,左 89 右 88;取 1:1 餘 102 → 51,左右各 51。
Q:尺寸框填小數能把選區挪半格嗎?
不能,x、y、寬、高全過 clampInt,半數一律朝 +∞:填 100.6 得 101、0.5 得 1、10.7 得 12;介面那句「選區 300×300(起點 17,23)」裡也沒有小數。
Q:偏這半格時,頭像會糊嗎?
不會。匯出是 9 參 drawImage、源矩形等於目標矩形,實測 dest(0,0) 精確取到源 (0,0);會糊的只有編輯區那張 520×325 預覽畫布,它不進檔案。