PerfectVector
작성: Irene Kim

Konva 히트 감지에서 SVG 투명 영역 제외하기

Konva에서 투명 SVG 이미지 영역이 클릭을 받지 않게 합니다. 캐시 기반 히트 감지를 재현하고 구멍·배경과 원본 변경 후 갱신을 확인하세요.

11분 읽기
목차

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. 완전한 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. 불러오고 캐시한 뒤 실제 대상 기록하기

이 예제는 Konva 10.9.0을 사용합니다. 다음 세 파일을 두 SVG 옆에 저장하세요. 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을 열고 스테이지 네 개가 나타날 때까지 기다립니다. 240 × 180 SVG는 각 420 × 260 스테이지의 좌표 (80,40)에 배치됩니다. 각 배경은 이미지 아래에 있는, 이벤트를 받는 별도의 Konva.Rect입니다. 스테이지의 클릭 리스너는 event.target.id()를 보고하며, 이는 단순한 캔버스 DOM 요소가 아니라 선택된 Konva 노드를 식별합니다.

await source.decode()는 캐시 생성 전에 실행됩니다. 이미지가 로딩 중일 때가 아니라 실제 원본이 준비된 뒤 캐시하세요. Konva의 Node API는 캐시와 캐시 해제 메서드를 설명합니다.

3. 불투명 부분, 구멍, 모서리, 배경 클릭하기

패널 아래 나열된 네 위치를 시험하세요. 브라우저 창에서 측정한 좌표가 아니라 각 스테이지 안의 좌표입니다.

위치스테이지 좌표시험하는 내용
불투명 아트워크125,110엠블럼 내부에 충분히 들어온 불투명 점
원래 구멍195,130hollow.svg의 투명한 내부
투명 모서리305,55이미지 직사각형 안이지만 칠해진 실루엣 밖
이미지 바깥20,20별도로 클릭 가능한 배경

다음 표는 시험한 대상 유형을 기록합니다. 로그에는 alpha-background처럼 각 패널의 접두사가 포함됩니다.

구성불투명 부분원래 구멍투명 모서리이미지 바깥
기본 이미지이미지이미지이미지배경
캐시된 알파 히트 영역이미지배경배경배경
불투명 흰색 채우기를 적용한 캐시 이미지이미지이미지이미지배경
원본 교체와 캐시 재구성이미지이미지배경배경
기본 직사각형 히트, 캐시된 알파 히트, 불투명 이미지 채우기, 교체된 원본과 기록된 클릭 대상 ID를 보여 주는 실제 Konva 브라우저 비교
직접 만든 SVG 아트워크와 기록된 데스크톱 클릭 대상을 사용하는 실제 Konva 10.9.0 스테이지입니다. 알파 기반의 속이 빈 이미지는 구멍과 모서리 클릭을 별도 배경으로 전달합니다. 불투명 이미지 채우기와 칠해진 교체 원본은 결과를 바꿉니다.

브라우저 시험은 16개 위치 모두에서 실제 포인터 클릭을 사용하고, 기본 이벤트의 신뢰 여부, 스테이지 좌표, 대상 ID를 기록했습니다. 원본 파일 해시, 이미지 디코딩, 스테이지·이미지 치수, 공개 교차 검사 결과, 각 이미지 노드의 렌더링된 캔버스에서 가져온 기본 알파 샘플도 확인했습니다.

원래 구멍에서 속이 빈 이미지 노드의 알파는 0이었습니다. 불투명 채우기와 교체 원본 비교군의 같은 위치 알파는 255였습니다. 별도 페이지 배경은 속이 빈 이미지 자체를 불투명하게 만들지 않았습니다. 아래에 있는 다른 Konva 노드였기 때문입니다. 이는 안티앨리어싱 경계에서 떨어진 내부 샘플입니다.

단순한 원본 SVG와 변환하지 않은 스테이지로 수행한 단일 데스크톱 브라우저 시험입니다. 모바일·터치 동작, 키보드 상호작용, 여러 브라우저 조합, 성능 향상, 모든 부드러운 가장자리 이미지에 맞는 임계값을 입증하지 않습니다. 앱의 실제 표시 크기와 변환에서 클릭 시험을 반복하세요.

4. 원본이 바뀌면 캐시 다시 구성하기

네 번째 패널은 속이 빈 원본과 알파 히트 영역으로 시작합니다. 그 뒤 캐시를 지우고, 이미 디코딩된 교체 이미지를 설정하고, 다시 캐시한 다음 히트 영역을 재구성합니다. 이 순서 후에는 원래 구멍 위치의 대상이 배경에서 이미지로 바뀝니다.

캐시는 렌더링된 스냅샷입니다. 렌더링 픽셀에 영향을 주는 변경 후에는 캐시 장면과 알파 기반 히트 영역을 함께 갱신하세요. 다른 이미지를 할당했다고 기존 캐시 히트 영역이 갱신되었다고 가정하지 마세요. 교체 이미지를 비동기로 불러온다면 예제의 순서를 실행하기 전에 해당 이미지의 디코딩을 기다립니다.

세 번째 패널은 다른 원인을 분리합니다. artwork.fill('#ffffff')는 같은 이미지 노드의 일부로 이미지 뒤에 불투명 직사각형을 칠합니다. 그 캐시 장면으로 히트 영역을 만들면 칠해진 면적도 포함됩니다. 불필요한 이미지 노드 채우기를 제거하고 캐시와 히트 영역을 다시 구성하세요. 원본 파일에서 흰색으로 보이는 부분도 실제 불투명 페인트일 수 있습니다. 히트 감지를 원인으로 판단하기 전에 투명도를 검사합니다.

픽셀 읽기와 대상 지정 실패를 별도로 진단하기

증상먼저 확인할 사항
구멍에서도 이미지가 로그에 기록됨디코딩 후 캐시했는지, 히트 영역을 재구성했는지, 불투명 채우기나 원본 배경이 구멍을 덮지 않는지 확인
아트워크는 바뀌었지만 클릭은 예전 모양을 따름원본 변경 후 장면 캐시와 알파 히트 영역을 지우고 재구성
픽셀 읽기 또는 오염된 캔버스 오류 발생히트 시험 결과를 해석하기 전에 이미지 출처와 응답 헤더 확인
전체 이미지가 이벤트를 받지 않음listening, 표시 여부, 레이어 순서, 다른 노드가 위에 있는지 확인
구멍 클릭이 아래 객체 대신 스테이지에 도달함해당 점 아래에 의도한 이벤트 수신 객체가 실제로 있는지 확인
캔버스 클릭은 작동하지만 키보드 조작은 안 됨키보드 제어와 의미 구조를 별도로 설계합니다. 알파 히트 픽셀은 이를 제공하지 않습니다.

이 시험은 두 SVG를 페이지와 같은 출처에서 제공합니다. 교차 출처 이미지는 픽셀을 다시 읽으려면 출처가 깨끗한 캔버스가 필요합니다. Konva의 오염된 캔버스 가이드는 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 경로를 개별 편집할 수 있나요? 아니요. 아트워크는 여전히 하나의 Image 노드입니다. 앱에 개별 벡터 영역 제어가 필요하다면 적합한 별도 Path 노드와 명시적인 장면 모델을 사용하세요.

출처

  1. Konva: Image Events — 기본 이미지 히트 동작과 캐시 아트워크에서 히트 영역을 생성하는 방법입니다.
  2. Konva: Shape API — 현재 숫자 alphaThreshold 매개변수와 범위입니다.
  3. Konva: Node API — 캐시 생성·해제, 이벤트 대상, 공개 노드 메서드입니다.
  4. Konva: 오염된 캔버스 가이드 — 출처 제한, CORS, 픽셀 읽기 실패를 설명합니다.
  5. Konva: 캔버스 위의 SVG — 전체 이미지 렌더링과 개별 구성한 벡터 경로를 구분합니다.

편집 가능한 도형이 없다면 PerfectVector로 SVG 후보를 준비하고 구멍을 검사한 다음, 의도한 Konva 클릭 대상과 캐시 갱신을 별도로 시험하세요.

블로그의 다른 글

PerfectVector

편집하기 쉬운
더 깔끔한 SVG로 다시 시작하세요

신용카드 불필요