两个 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 预览画布,它不进文件。