PerfectVector
著者: Irene Kim

Konvaのヒット判定でSVGの透明領域を無視する

KonvaでSVG画像の透明領域がクリックを受け取らないようにします。キャッシュによる判定を再現し、穴と背景を試し、元画像変更後に判定を作り直します。

5 分で読める
目次

Konva.Image の透明領域がクリックを無視するようにするには、元画像のデコードを待ち、imageNode.cache()、続いて imageNode.drawHitFromCache(0) を呼び出し、レイヤーを再描画します。すると、キャッシュされたシーンのアルファ、つまりピクセルの不透明度によって、画像のどのピクセルをヒット判定に使うかが決まります。

通常の画像ノードは、SVGの空いた穴も含めて、長方形全体でイベントを受け取れます。KonvaのImage Eventsチュートリアルは、アルファに基づく別の方法を示しています。この記事では、別にクリックできる背景の上で、独自の穴あきSVGをテストします。そのため、穴が画像のクリックを受け取らなくなると、その下の実際の対象がクリックを受け取ります。

これが変更するのはキャンバスの操作領域です。SVGから編集可能な Konva.Path ノードを作ったり、DOMのSVGの pointer-events 規則を変えたりするものではありません。

画像とヒット領域を分ける

ヒット領域は、Konvaがイベントの対象を選ぶために使う範囲です。その形は表示するデザインと異なる場合があります。広い長方形の対象を意図するアプリもあれば、透明な穴を通して下のオブジェクトを選びたいアプリもあります。

意図する操作出発点
長方形内のどこからでも画像全体を選ぶImageの既定のヒット動作
画像の透明ピクセルを通して下のノードに届かせる画像をキャッシュし、アルファからヒット領域を作る
個別のベクター領域を編集する、または独立した操作を割り当てるKonvaのパスを別々にする方法を選び、動作を定義する
インラインDOM SVG内部のポインター対象を制御する別のSVG pointer-eventsガイドを使う

現在のShape APIは、0から255の整数 alphaThreshold を受け取り、既定値は0です。ここにはコールバックではなく数値を渡します。これは見た目を変えるThresholdフィルターとは異なります。この例は、完全に不透明な内部と完全に透明な穴で0を使います。柔らかい影やアンチエイリアスのある縁に適した閾値を定めるものではありません。

1. 2つの完全なSVG入力を保存する

空のフォルダーを作ります。この独自の穴あきエンブレムを hollow.svg として保存します。

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="180" viewBox="0 0 240 180">
  <path fill="#315d6f" fill-rule="evenodd"
        d="M20 30 L180 20 L215 60 L195 150 L35 160 L10 110 Z
           M80 65 L145 55 L160 110 L95 125 Z"/>
</svg>

内側のサブパスは、奇偶の塗り規則によって穴になります。同じ外形から内側のサブパスだけを取り除き、filled.svg として保存します。

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="180" viewBox="0 0 240 180">
  <path fill="#315d6f" d="M20 30 L180 20 L215 60 L195 150 L35 160 L10 110 Z"/>
</svg>

2つ目のファイルは、元画像変更の対照例です。以前は空だった場所を塗ります。どちらにも白い背景の長方形や埋め込みビットマップはありません。

2. 読み込み・キャッシュ・実際の対象の記録を行う

この例はKonva 10.9.0を使います。両方のSVGの隣に、次の3ファイルを保存します。HTMLは版を固定したブラウザー用ビルドを読み込みます。管理下のプロジェクトでは、代わりに同じビルドをローカル配信できます。

reader.html を保存します。

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Konva transparent image hit comparison</title>
<link rel="stylesheet" href="reader.css">
<main>
<h1>Transparent artwork: which node gets the click?</h1>
<p class="banner"><strong id="version">Loading Konva…</strong>. Original same-origin SVGs, actual Konva.Image nodes, and a separately clickable background. These are canvas hit pixels, not editable SVG path nodes. Desktop click test only; no mobile or accessibility audit.</p>
<div class="grid">
<section class="panel"><h2>1. Default rectangular hit</h2><div class="stage" id="default"></div><p>The hollow image still catches its transparent center and corner.</p><pre class="log" id="default-log"></pre></section>
<section class="panel"><h2>2. Cached alpha hit region</h2><div class="stage" id="alpha"></div><p>cache() then drawHitFromCache(0): the center and corner reach the background.</p><pre class="log" id="alpha-log"></pre></section>
<section class="panel"><h2>3. Opaque image fill</h2><div class="stage" id="fill"></div><p>A white fill behind this image is part of its cached scene. Its formerly empty pixels receive image hits.</p><pre class="log" id="fill-log"></pre></section>
<section class="panel"><h2>4. Replaced source, rebuilt cache</h2><div class="stage" id="updated"></div><p>The original hole is painted in filled.svg. Clear, replace, cache, and rebuild the hit region.</p><pre class="log" id="updated-log"></pre></section>
</div>
<p>Click these stage coordinates in each panel: solid (125,110), hole (195,130), transparent corner (305,55), and outside the image (20,20). The corner is inside the image viewport but outside its painted shape. Logs report actual click targets.</p>
 
</main>
<script src="https://unpkg.com/konva@10.9.0/konva.min.js"></script>
<script type="module" src="reader.js"></script>
</html>

reader.css を保存します。

body { margin: 32px; font: 16px/1.55 Arial, sans-serif; color: #183a42; background: #edf2ef; }
main { max-width: 1080px; margin: auto; }
h1 { font-size: 32px; margin-bottom: 12px; }
h2 { margin: 0 0 12px; font-size: 22px; }
p { margin: 10px 0; }
.banner { background: #dce8e4; padding: 18px; border-left: 5px solid #315d6f; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(460px, 1fr)); gap: 20px; margin: 24px 0; }
.panel { background: white; padding: 20px; border: 1px solid #d1dbd5; border-radius: 12px; }
.stage { width: 420px; height: 260px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; background: #f8faf7; padding: 12px; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border-bottom: 1px solid #c9d5ce; padding: 10px; text-align: left; }
.log { min-height: 88px; margin-bottom: 0; font-size: 13px; }

reader.js を保存します。

const source = new Image();
source.src = './hollow.svg';
await source.decode();
const replacement = new Image();
replacement.src = './filled.svg';
await replacement.decode();
 
const clicks = [];
const scenes = [];
function makeScene(id, mode) {
  const stage = new Konva.Stage({ container: id, width: 420, height: 260 });
  const layer = new Konva.Layer();
  stage.add(layer);
  const background = new Konva.Rect({
    id: `${id}-background`, width: 420, height: 260, fill: '#eadfcd'
  });
  const artwork = new Konva.Image({
    id: `${id}-image`, image: source, x: 80, y: 40, width: 240, height: 180
  });
  if (mode === 'fill') artwork.fill('#ffffff');
  layer.add(background, artwork);
  if (mode !== 'default') {
    artwork.cache();
    artwork.drawHitFromCache(0);
  }
  layer.draw();
  const beforeUpdate = stage.getIntersection({ x: 195, y: 130 }).id();
  if (mode === 'update') {
    artwork.clearCache();
    artwork.image(replacement);
    artwork.cache();
    artwork.drawHitFromCache(0);
    layer.draw();
  }
  stage.on('click', (event) => {
    const point = stage.getPointerPosition();
    const record = {
      scene: id, x: point.x, y: point.y,
      target: event.target.id(), trusted: event.evt.isTrusted
    };
    clicks.push(record);
    document.getElementById(`${id}-log`).textContent +=
      `(${Math.round(point.x)}, ${Math.round(point.y)}) → ${record.target}\n`;
    document.dispatchEvent(new CustomEvent('fixture-click-recorded'));
  });
  const scene = { id, stage, artwork, beforeUpdate };
  scenes.push(scene);
  return scene;
}
makeScene('default', 'default');
makeScene('alpha', 'alpha');
makeScene('fill', 'fill');
makeScene('updated', 'update');
document.getElementById('version').textContent = `Konva ${Konva.version}`;

HTMLをローカルファイルとして開かず、このフォルダーをHTTPで配信します。

python -m http.server 8000

http://localhost:8000/reader.html を開き、4つのステージが表示されるのを待ちます。240 × 180のSVGは、各420 × 260のステージ内のステージ座標(80,40)に配置されます。背景はそれぞれ、画像の下にある、イベントを受け取る独立した Konva.Rect です。ステージのクリックリスナーは event.target.id() を報告し、単なるキャンバスのDOM要素ではなく、選ばれたKonvaノードを特定します。

await source.decode() はキャッシュ作成より前に実行されます。画像の読み込み中ではなく、実際の元画像が準備できてからキャッシュします。KonvaのNode APIは、キャッシュとキャッシュ削除のメソッドを説明しています。

3. 塗り部分・穴・隅・背景をクリックする

パネルの下に示した4か所を試します。ブラウザーウィンドウから測った座標ではなく、各ステージ内の座標です。

位置ステージ座標検証するもの
塗りのあるデザイン125,110エンブレムの十分内側にある不透明な点
元の穴195,130hollow.svgの透明な内部
透明な隅305,55画像の長方形内で、塗ったシルエットの外側
画像の外20,20別にクリックできる背景

以下の表は、確認した対象の種類を記録したものです。ログには alpha-background のように、各パネルの接頭辞が含まれます。

設定塗り部分元の穴透明な隅画像の外
既定の画像画像画像画像背景
キャッシュしたアルファのヒット領域画像背景背景背景
不透明な白い塗り付きのキャッシュ画像画像画像画像背景
元画像を差し替えてキャッシュを再作成画像画像背景背景
Konvaの実際のブラウザー比較。既定の長方形判定、キャッシュしたアルファ判定、画像の不透明な塗り、元画像の差し替えと、記録したクリック対象IDを示す
独自のSVGデザインとデスクトップのクリック対象を記録した、実際のKonva 10.9.0のステージです。アルファに基づく穴あき画像では、穴と隅のクリックが別の背景に届きます。画像の不透明な塗りや、穴を塗った差し替え元画像によって結果が変わります。

ブラウザーのテストでは、全16か所で実際のポインタークリックを使い、ネイティブイベントの信頼状態、ステージ座標、対象IDを記録しました。また、元ファイルのハッシュ、画像のデコード、ステージと画像の寸法、公開APIの交差判定結果、各画像ノードを描画したキャンバスのネイティブなアルファサンプルも確認しました。

元の穴では、穴あき画像ノードのアルファは0でした。不透明な塗りと差し替え元画像の対照例は、その場所のアルファが255でした。別のページ背景は、穴あき画像自体を不透明にはしません。背景はその下にある別のKonvaノードに属するからです。これらはアンチエイリアスされた境界から離れた内部のサンプルです。

これは単純な独自のSVGと変形していないステージを使った、1つのデスクトップブラウザーでのテストです。モバイルやタッチ、キーボード操作、複数ブラウザーでの対応、性能向上、すべての柔らかい縁の画像に合う閾値を確立するものではありません。自分のアプリの実際の表示サイズと変換で、クリックのテストを繰り返してください。

4. 元画像が変わったらキャッシュを作り直す

4つ目のパネルは、穴あきの元画像とそのアルファのヒット領域から始めます。その後、キャッシュを削除し、デコード済みの差し替え画像を設定して、再度キャッシュし、ヒット領域を作り直します。この順序の後、元の穴の位置の対象は背景から画像へ変わります。

キャッシュは描画済みのスナップショットです。描画ピクセルに影響する変更の後は、キャッシュされたシーンと、そのアルファに基づくヒット領域を一緒に更新します。別の画像を割り当てるだけで既存のキャッシュのヒット領域が更新されたとは考えないでください。非同期で読み込む差し替えでは、例の手順に進む前に、その画像のデコードを待ちます。

3つ目のパネルは別の原因を切り分けます。artwork.fill('#ffffff') は、同じ画像ノードの一部として、画像の背後に不透明な長方形を描きます。そのキャッシュシーンからヒット領域を作ると、塗った部分も含まれます。不要な画像ノードの塗りを外し、キャッシュとヒット領域を作り直します。元ファイルの白く見える部分が、本当に不透明な塗りの場合もあります。ヒット判定を疑う前に透明性を確認してください。

ピクセル読み出しと対象選択の問題を分けて診断する

症状最初に確認すること
穴のログがまだ画像になるデコード後にキャッシュしたか、ヒット領域を作り直したか、不透明な塗りや元画像の背景が穴を覆っていないか確認する
デザインは変わったのにクリックが古い形に従う元画像の変更後、シーンキャッシュとアルファのヒット領域を削除・再作成する
ピクセル読み出しや汚染キャンバスのエラーが出るヒット判定を解釈する前に、画像のオリジンとレスポンスヘッダーを確認する
画像全体がイベントを受け取らなくなるlistening、可視状態、レイヤー順、上に別ノードがないか確認する
穴が下のオブジェクトではなくステージに届くその点の下に、意図したイベント受付オブジェクトが実際にあるか確認する
キャンバスのクリックは動くがキーボード操作は動かないキーボード操作と意味付けは別に設計する。アルファのヒットピクセルはそれらを提供しない

この例は、両方のSVGをページと同じオリジンから配信します。別オリジンの画像では、ピクセルを読み出すためにオリジンが汚染されていないキャンバスが必要です。KonvaのTainted Canvasガイドは、CORSリクエストを許可するサーバーと crossOrigin を併用する方法を説明しています。サーバーが許可しない場合、crossOrigin = 'anonymous' を設定するだけではアクセスできません。この記事では別オリジンの失敗例は実行していません。

外側のキャンバスでCSSの pointer-events を変えても、Konva.Image のどの内部ピクセルがノードにヒットするかは決まりません。キャンバスの対象選択とDOM SVGのポインター対象を分けて考えます。SVGマスターも残します。キャッシュしたヒットマップはピクセルベースの操作データであり、ベクター文書の代わりではありません。

デザインの取り出しとイベント実装を分ける

画像の配置とアルファに基づくヒットだけが必要なアプリなら、適切な透明ラスター画像で十分かもしれません。PNGやJPGしか残っておらず、アプリに編集可能なパス形状も必要なら、PerfectVectorの画像からベクターへの変換手順でSVG候補を取り出せます。シルエットと穴を確認し、Konvaでの表現を選んでイベントを実装します。この例は独自のネイティブSVGを使い、PerfectVectorでの変換は行っていません。

ベクター化は、リスナーの設置、アクセシブルな操作部品の作成、クリック対象の選択、キャッシュの同期を行いません。SVGマスターがすでにあるなら、操作コードを直接修正します。予想外に長方形の画像全体を含むファイルでは、内容が別々の編集可能なパスだと考える前に、SVG内の埋め込みラスター確認を使ってください。

よくある質問

Konva画像の透明ピクセルがクリックを受け取るのはなぜですか? 既定のImageノードは、長方形の範囲をヒット判定に使えます。元画像の準備後にキャッシュし、適切な数値のアルファ閾値でdrawHitFromCacheを使ってから、再描画して実際の対象を確認します。

drawHitFromCacheはどの引数を受け取りますか? 現在のAPIは、省略可能な0から255の整数のアルファ閾値を受け取り、既定値は0です。検証例は0を渡します。コールバックを渡したり、見た目のThresholdフィルターを使ったりはしません。

白い塗りを付けるとSVGの穴がまたクリック対象になるのはなぜですか? Imageノードの不透明な塗りが、キャッシュシーンの一部になるためです。キャッシュのアルファにはその塗った領域も含まれます。不要な塗りを除き、シーンキャッシュとヒット領域を作り直してください。

アルファのヒット領域でSVGパスを個別編集できるようになりますか? いいえ。デザインは1つのImageノードのままです。個別のベクター領域を制御する必要があるアプリでは、適した別々のPathノードと明示的なシーンモデルを使います。

出典

  1. Konva:Image Events — 既定の画像ヒット動作と、キャッシュされたデザインからのヒット領域生成。
  2. Konva:Shape API — 現行の数値パラメーターalphaThresholdと範囲。
  3. Konva:Node API — キャッシュ作成・削除、イベント対象、公開ノードメソッド。
  4. Konva:Tainted Canvasガイド — オリジン制限、CORS、ピクセル読み出しの失敗。
  5. Konva:SVG on canvas — 画像全体の描画と、別に作るベクターパスの違い。

編集可能な形状がなければ、PerfectVectorでSVG候補を準備し、穴を調べ、意図するKonvaのクリック対象とキャッシュ更新を別にテストしてください。

ブログのその他の記事

PerfectVector

編集しやすい
よりきれいなSVGから始めましょう

クレジットカード不要