Skip to content

可视化点击区域

812 words
4 min

Live2D 模型内部定义了若干个点击区域(Hit Area),用于响应用户在不同部位的点击交互(如点击头部、身体等)。本教程将介绍如何监听点击事件,以及如何将这些区域可视化地绘制出来——这在调试交互效果时非常实用。

如果你还没有完成基础初始化,请先阅读快速开始

HTML 结构

html
<div style="position: relative; display: inline-block">
  <canvas id="l2d" style="width: 300px; height: 400px"></canvas>
</div>

监听点击事件

通过 tap 事件可以得知用户点击了哪个区域:

ts
l2d.on('tap', areaName => {
  console.log('点击区域:', areaName);
});

areaName 是模型中定义的区域名称(如 HeadBody),空字符串表示点击了没有命名区域的地方。

注意:Hit Area 由模型作者在 Live2D Cubism Editor 中定义,不同模型的区域名称和数量不同,部分模型可能没有定义任何 Hit Area。

获取区域边界

getHitAreaBounds() 方法返回所有 Hit Area 的边界矩形,坐标是相对于 Canvas 的归一化值(范围 0 ~ 1):

ts
await l2d.load({ path: '...' });

const bounds = l2d.getHitAreaBounds();
console.log(bounds);
// [
//   { name: 'Head', x: 0.28, y: 0.08, w: 0.44, h: 0.28 },
//   { name: 'Body', x: 0.22, y: 0.36, w: 0.56, h: 0.48 },
// ]

注意:需要在模型加载完成(load() resolve 之后)才能获取到有效数据,加载前调用返回空数组。

可视化实现

核心思路是创建一个与 Canvas 完全重叠的透明覆盖层 Canvas,然后在动画帧中持续绘制 Hit Area 的边界矩形。

完整代码

ts
import { init } from 'l2d';

const canvas = document.getElementById('l2d') as HTMLCanvasElement;
const l2d = init(canvas);

// 创建覆盖层 canvas,绝对定位覆盖在模型上方,不拦截点击
const overlay = document.createElement('canvas');
overlay.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;z-index:9999;';
document.body.appendChild(overlay);
const ctx = overlay.getContext('2d')!;

let rafId: number | null = null;

// 监听点击事件
l2d.on('tap', areaName => {
  console.log('点击区域:', areaName);
});

l2d.load({
  path: 'https://model.hacxy.cn/shizuku/shizuku.model.json',
  scale: 0.8,
}).then(() => {
  const dpr = window.devicePixelRatio || 1;

  function draw() {
    const rect = canvas.getBoundingClientRect();
    const w = rect.width * dpr;
    const h = rect.height * dpr;

    // 同步覆盖层的位置和尺寸
    if (overlay.width !== w || overlay.height !== h) {
      overlay.width = w;
      overlay.height = h;
    }
    overlay.style.left = `${rect.left}px`;
    overlay.style.top = `${rect.top}px`;
    overlay.style.width = `${rect.width}px`;
    overlay.style.height = `${rect.height}px`;

    ctx.clearRect(0, 0, w, h);

    // 遍历所有 Hit Area,绘制边界矩形和名称标签
    for (const b of l2d.getHitAreaBounds()) {
      const x = b.x * w;
      const y = b.y * h;
      const bw = b.w * w;
      const bh = b.h * h;

      // 绿色边框
      ctx.strokeStyle = 'rgba(0,255,100,0.9)';
      ctx.lineWidth = 2;
      ctx.strokeRect(x, y, bw, bh);

      // 半透明填充
      ctx.fillStyle = 'rgba(0,255,100,0.12)';
      ctx.fillRect(x, y, bw, bh);

      // 区域名称标签
      ctx.fillStyle = 'rgba(0,255,100,1)';
      ctx.font = `bold ${12 * dpr}px monospace`;
      ctx.fillText(b.name, x + 4, y + 14 * dpr);
    }

    rafId = requestAnimationFrame(draw);
  }

  draw();
});

关键点说明

  • pointer-events:none:覆盖层不拦截鼠标事件,点击穿透到下方的 Canvas。
  • devicePixelRatio:乘以 DPR 保证在高分屏上绘制清晰。
  • 每帧重绘:使用 requestAnimationFrame 保持覆盖层与模型同步(Canvas 位置变化时边框也跟随移动)。
  • 归一化坐标转像素b.x * wb.y * h 将 0~1 的归一化坐标还原为实际像素。

交互演示

点击下方「运行」按钮,模型加载完成后会看到绿色边框标注出各个可点击区域,点击区域会在右上角提示区域名称。

ts
l2d.on('tap', areaName => {
  message.info(`点击区域: ${areaName || '(无名区域)'}`);
});

l2d.load({
  path: 'https://model.hacxy.cn/shizuku/shizuku.model.json',
  scale: 0.8,
}).then(() => {
  const canvas = l2d.getCanvas();
  const dpr = window.devicePixelRatio || 1;

  function draw() {
    const rect = canvas.getBoundingClientRect();
    const w = rect.width * dpr;
    const h = rect.height * dpr;

    if (overlay.width !== w || overlay.height !== h) {
      overlay.width = w;
      overlay.height = h;
    }
    overlay.style.left = `${rect.left}px`;
    overlay.style.top = `${rect.top}px`;
    overlay.style.width = `${rect.width}px`;
    overlay.style.height = `${rect.height}px`;

    ctx.clearRect(0, 0, w, h);

    for (const b of l2d.getHitAreaBounds()) {
      const x = b.x * w;
      const y = b.y * h;
      const bw = b.w * w;
      const bh = b.h * h;

      ctx.strokeStyle = 'rgba(0,255,100,0.9)';
      ctx.lineWidth = 2;
      ctx.strokeRect(x, y, bw, bh);

      ctx.fillStyle = 'rgba(0,255,100,0.12)';
      ctx.fillRect(x, y, bw, bh);

      ctx.fillStyle = 'rgba(0,255,100,1)';
      ctx.font = `bold ${12 * dpr}px monospace`;
      ctx.fillText(b.name, x + 4, y + 14 * dpr);
    }

    rafId = requestAnimationFrame(draw);
  }

  draw();
  message.success('加载完成!点击绿色区域试试');
});

进阶:清理覆盖层

如果需要动态销毁模型或切换场景,记得同时清理覆盖层,避免残留:

ts
// 销毁时
cancelAnimationFrame(rafId!);
overlay.remove();
l2d.destroy();

MIT Licensed