最近在做一个互动绘本类的 H5(封面 + 5 道题 + 结果页,每页一张插画带按钮/选项),遇到一个老问题:
这类产品就是大海报 + 特定区域可点击。
但按钮的位置与形状都是很奇特的,如果用固定的 left/top 来定位按钮在不同设备上容易发生错位。误触率会很高。
最终的做法是。 美术交付的素材是整张透明 PNG,每张图都是 与背景同尺寸,按钮/选项只是画在透明背景的某个位置上。 这样做的好处是图层叠起来天然对齐, 用 canvas 读图片的 alpha 通道,扫出非透明像素的最小包围盒,运行时自动生成热区。 美术只要保证”整张画布尺寸出图、透明背景”,代码这边换素材零改动。
利用png alpha,js动态计算热区按钮位置:

美术交付的素材(背景图, 与背景同尺寸的按钮透明背景图):

核心代码:alpha-scan
function detectBox(img) {
return new Promise((resolve) => {
const run = () => {
const w = img.naturalWidth, h = img.naturalHeight;
if (!w || !h) return resolve(null);
// 1) 降采样到最长边 320px,扫起来快 27 倍
const SCALE = 320 / Math.max(w, h);
const cw = Math.max(1, Math.round(w * SCALE));
const ch = Math.max(1, Math.round(h * SCALE));
const c = document.createElement('canvas');
c.width = cw; c.height = ch;
const ctx = c.getContext('2d', { willReadFrequently: true });
ctx.drawImage(img, 0, 0, cw, ch);
let data;
try { data = ctx.getImageData(0, 0, cw, ch).data; }
catch (e) {
// file:// 协议下 CORS 会拦截 getImageData,需要兜底
console.warn('[alpha-scan blocked]', img.src);
return resolve(null);
}
// 2) 扫所有不透明像素,求最小包围盒
let x1 = cw, y1 = ch, x2 = -1, y2 = -1;
for (let y = 0; y < ch; y++) {
for (let x = 0; x < cw; x++) {
// alpha > 24 才算"有内容",过滤抗锯齿羽化边
if (data[(y * cw + x) * 4 + 3] > 24) {
if (x < x1) x1 = x;
if (y < y1) y1 = y;
if (x > x2) x2 = x;
if (y > y2) y2 = y;
}
}
}
if (x2 < 0) return resolve(null);
// 3) 上下左右各扩 1%,点击更友好
const padX = cw * 0.01, padY = ch * 0.01;
x1 = Math.max(0, x1 - padX);
y1 = Math.max(0, y1 - padY);
x2 = Math.min(cw - 1, x2 + padX);
y2 = Math.min(ch - 1, y2 + padY);
// 4) 关键:返回百分比而非像素,天然响应式
resolve({
left: (x1 / cw) * 100,
top: (y1 / ch) * 100,
width: ((x2 - x1) / cw) * 100,
height: ((y2 - y1) / ch) * 100,
});
};
if (img.complete && img.naturalWidth) run();
else img.addEventListener('load', run, { once: true });
});
}
几个关键设计
1. 降采样加速
941×1672 的图直接扫像素是 157 万次循环,缩到最长边 320 之后只剩 5.7 万次,快了将近 30 倍。肉眼看不出精度损失——按钮位置精确到 ±3 像素对点击体验毫无影响。
2. alpha 阈值 > 24,不是 > 0
PNG 边缘有抗锯齿羽化,外圈会有一圈 alpha 很低的半透明像素。如果用 > 0 判定,包围盒会被这些”几乎看不见的边”撑大,热区比视觉边界大一圈。阈值设到 24(约 10%)能干净地过滤掉这些羽化边。
3. 返回百分比,不是像素
这是真正让方案变成”响应式”的一步。计算结果脱离了 canvas 的具体尺寸,stage 怎么缩放(从手机 375px 到 iPad 1024px),热区都能跟着按钮一起缩放。配合 aspect-ratio: 941/1672 锁定的 stage,整页缩放永远不会错位。
4. 视觉层和交互层解耦
最终生成的 DOM 结构是这样的:
<section class="screen">
<img class="layer bg" src="1-bg.png"> <!-- 背景 -->
<img class="layer option" src="1-1.png"> <!-- 选项视觉 -->
<img class="layer option" src="1-2.png">
<img class="layer btn" src="1-btn.png"> <!-- 按钮视觉 -->
<button class="hit" style="left:23%;top:67%;width:54%;height:8%"></button>
<!-- 透明的真热区,盖在按钮上 -->
</section>
<img> 负责画面,<button class="hit"> 是透明的真点击区域,叠在 alpha 扫描出的包围盒上。这样的好处:
- 热区可以加 padding(前面那个扩 1%),不影响视觉
- 视觉层全部
pointer-events: none,不会拦截事件 - 按钮按下效果给
<img>加transform: scale(.96)即可,热区不动
适用场景
这个套路的核心约束是:美术按整张画布尺寸出图、透明背景。只要满足这个交付规范,下面这些场景都能用:
- 运营活动 H5/ 营销页:双 11、618 那种”一张大长图配几个按钮”,省掉量坐标的麻烦
- 互动绘本 / 儿童教育:每页一张插画,点哪个动物做反馈
- 找不同、找物、密室逃脱:物品藏在画面里,热区都是不规则的
- 抽奖转盘、刮刮卡:扇形/不规则区域的点击判定
- 不规则形状的 CTA 按钮:心形、云朵、撕纸边的按钮,矩形 hit 区会误触
- 海报/请柬的可交互版:婚礼请柬、年会邀请等整张设计图加几个按钮
同一原理的延伸
抽象一层,这个套路的本质是 “运行时从位图反推几何信息”。同原理的玩法有一大堆:
- 自动裁边 / trim:导出 PNG 时把透明边裁掉,TexturePacker 这类 sprite 打包工具就这么干
- Sprite sheet 自动切分:一张图里几十个小图标,扫连通的不透明区域自动分割
- 像素级碰撞检测:2D 游戏里不规则 sprite 的碰撞,bounding box 粗筛 + alpha 精判
- CSS
shape-outside: url():浏览器原生用图片 alpha 让文字环绕不规则形状 - 位图转矢量(potrace):扫边界然后拟合贝塞尔,SVG 化的第一步也是 alpha 扫描
- OCR 预处理:找文字块、表格线,先做连通域分析
- 抠图工具的边界细化:AI 出粗 mask 后用 alpha 扫描修整边缘 + 找最小包围盒做 crop
- canvas 手写签名:“是否签了字” + 自动裁切空白,本质都是扫 alpha
- PDF 印章/签名定位:在大白底里找有颜色的连通块
设计哲学
这套思路其实是 “约定优于配置”在视觉资产上的体现:
- 不让美术写坐标 JSON、也不让前端量像素,让资产文件本身承载位置信息
- 代价是一次性扫描计算(缩图加速 + 缓存到 Map),收益是改图零代码改动
- 适用前提是”整张画布尺寸 + 透明背景”,得在交付规范里写死
再往深一步,这是 “运行时自省” 在前端的常见模式——别假设、别硬编码,让数据/浏览器自己告诉你答案。同类思路还有:getComputedStyle 反推样式、IntersectionObserver 反推可见性、ResizeObserver 反推尺寸。共同点都是把”配置时的猜测”换成”运行时的观测”。
回到最初的问题:为什么这个方法巧妙?因为它把”美术 + 前端”的协作契约压缩成了一条——整张画布尺寸出图,透明背景。剩下的全交给 alpha 扫描,连响应式都顺手解决了。换素材时美术不用通知我、我也不用改一行代码。这种”少即是多”的协议设计,比写一百行精确坐标都香。