astro bun Deno logo
Skip to content
Go back

用 Alpha 通道扫描自动定位按钮热区

Edit page

最近在做一个互动绘本类的 H5(封面 + 5 道题 + 结果页,每页一张插画带按钮/选项),遇到一个老问题:

这类产品就是大海报 + 特定区域可点击。

但按钮的位置与形状都是很奇特的,如果用固定的 left/top 来定位按钮在不同设备上容易发生错位。误触率会很高。

最终的做法是。 美术交付的素材是整张透明 PNG,每张图都是 与背景同尺寸,按钮/选项只是画在透明背景的某个位置上。 这样做的好处是图层叠起来天然对齐, 用 canvas 读图片的 alpha 通道,扫出非透明像素的最小包围盒,运行时自动生成热区。 美术只要保证”整张画布尺寸出图、透明背景”,代码这边换素材零改动。

利用png alpha,js动态计算热区按钮位置: 运行时 alpha 扫描生成的点击热区

美术交付的素材(背景图, 与背景同尺寸的按钮透明背景图): 整张画布尺寸的透明 PNG 资产

核心代码: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 扫描出的包围盒上。这样的好处:

适用场景

这个套路的核心约束是:美术按整张画布尺寸出图、透明背景。只要满足这个交付规范,下面这些场景都能用:

同一原理的延伸

抽象一层,这个套路的本质是 “运行时从位图反推几何信息”。同原理的玩法有一大堆:

  1. 自动裁边 / trim:导出 PNG 时把透明边裁掉,TexturePacker 这类 sprite 打包工具就这么干
  2. Sprite sheet 自动切分:一张图里几十个小图标,扫连通的不透明区域自动分割
  3. 像素级碰撞检测:2D 游戏里不规则 sprite 的碰撞,bounding box 粗筛 + alpha 精判
  4. CSS shape-outside: url():浏览器原生用图片 alpha 让文字环绕不规则形状
  5. 位图转矢量(potrace):扫边界然后拟合贝塞尔,SVG 化的第一步也是 alpha 扫描
  6. OCR 预处理:找文字块、表格线,先做连通域分析
  7. 抠图工具的边界细化:AI 出粗 mask 后用 alpha 扫描修整边缘 + 找最小包围盒做 crop
  8. canvas 手写签名:“是否签了字” + 自动裁切空白,本质都是扫 alpha
  9. PDF 印章/签名定位:在大白底里找有颜色的连通块

设计哲学

这套思路其实是 “约定优于配置”在视觉资产上的体现

再往深一步,这是 “运行时自省” 在前端的常见模式——别假设、别硬编码,让数据/浏览器自己告诉你答案。同类思路还有:getComputedStyle 反推样式、IntersectionObserver 反推可见性、ResizeObserver 反推尺寸。共同点都是把”配置时的猜测”换成”运行时的观测”。


回到最初的问题:为什么这个方法巧妙?因为它把”美术 + 前端”的协作契约压缩成了一条——整张画布尺寸出图,透明背景。剩下的全交给 alpha 扫描,连响应式都顺手解决了。换素材时美术不用通知我、我也不用改一行代码。这种”少即是多”的协议设计,比写一百行精确坐标都香。


Edit page
Share this post on:

Next Post
实现一个免费的 RAG 聊天助手
Code_You