PerfectVector
작성: Irene Kim

Leaflet SVG 마커: iconAnchor로 끝점 맞추기

보이는 끝점을 iconAnchor에 대응시켜 Leaflet의 사용자 지정 SVG 마커를 배치합니다. viewBox 여백, 종횡비 맞춤, 아이콘 크기 변화까지 반영합니다.

8분 읽기
목차

Leaflet의 사용자 지정 SVG 마커는 iconSize를 표시 이미지 크기로, iconAnchor를 그 이미지 안에서 보이는 끝점의 픽셀 위치로 설정합니다. 이미지의 왼쪽 위 모서리부터 측정합니다. 투명한 SVG 여백도 이 크기에 포함되므로, 너비의 절반과 높이 전체를 사용하면 비대칭이거나 여백이 있는 핀의 끝점을 놓칠 수 있습니다.

이 안내는 끝점이 몸체 중심보다 오른쪽에 있는 직접 만든 핀을 사용합니다. 타일 없는 브라우저 지도에서 두 원본 프레임을 두 아이콘 크기로 비교합니다. 이미 깨끗한 SVG 그림이 있다면 그대로 사용합니다. 앵커 배치에는 벡터화가 필요하지 않습니다.

그림, 이미지 상자, 지도 지점을 구분합니다

여기서는 세 좌표계가 만납니다. SVG 경로는 원본 단위로 그림을 설명합니다. iconSize는 픽셀 단위의 표시 이미지 상자를 정합니다. Leaflet은 iconAnchor로 지정한 상자 안의 지점을 마커의 지리적 위치에 놓습니다. Icon 참고 문서는 이 옵션과 왼쪽 위를 기준으로 하는 앵커 원점을 정의합니다.

일반적인 핀은 아래쪽 변의 가운데에 닿을 수 있지만, 그림에 따라 다른 곳에서 끝날 수 있습니다. Leaflet 문서의 기본값은 크기가 지정된 경우 중앙 앵커입니다. 실험의 아래쪽 중앙 값은 이 기본값이 아니라 테스트를 위해 명시적으로 세운 가정입니다.

공식 사용자 지정 아이콘 튜토리얼은 L.icon 인스턴스를 마커에 전달하는 방법을 보여 줍니다. 외부 그림을 가져올 때 추가로 해야 할 일은 이미지 안에서 지도 좌표에 닿아야 하는 지점을 찾는 것입니다.

보이는 끝점과 앵커
여백 있는 캔버스 안의 비대칭 위치 핀 끝점이 십자선에서 어긋난 상태와 맞닿은 상태를 비교하는 개념도
일러스트: 보이는 끝점을 배치할 때 전체 이미지 프레임을 반영합니다. 아래의 측정된 브라우저 실험은 별도로 만든 SVG 파일을 사용합니다.

원본 끝점을 아이콘 픽셀에 대응시킵니다

핀 끝점의 원본 좌표는 (40, 80)입니다. 꼭 맞는 프레임은 viewBox="0 0 60 80"입니다. 여백 버전은 프레임만 viewBox="-20 -10 100 120"으로 바꿉니다. 두 파일은 같은 경로와 투명한 구멍을 유지합니다.

viewBox 참고 문서는 이 원본 사각형을 설명합니다. 그 안의 빈 공간은 그림과 함께 이동합니다. SVG 경계 상자 안내는 캔버스와 칠해진 도형의 경계가 다를 수 있는 이유를 설명합니다.

두 버전 모두 preserveAspectRatio="xMidYMid meet"를 명시적으로 사용합니다. 이 설정은 프레임을 뷰포트 안에 균일하게 맞추고 남는 공간을 중앙 정렬합니다. 해당 설정은 MDN preserveAspectRatio 참고 문서를 참고합니다.

이 맞춤 모드에서 W와 H를 아이콘 크기, (minX, minY, vbW, vbH)를 viewBox, (tipX, tipY)를 원본 끝점으로 둡니다.

s = min(W / vbW, H / vbH)
offsetX = (W - vbW * s) / 2
offsetY = (H - vbH * s) / 2
anchorX = (tipX - minX) * s + offsetX
anchorY = (tipY - minY) * s + offsetY

여백이 있는 60×80 아이콘에서 s는 0.6입니다. 100×120 원본 프레임이 60×72픽셀을 차지하므로 위아래에 각각 4픽셀이 남습니다. 이 수직 오프셋을 포함한 끝점은 [36, 58]이 됩니다. 각 치수에서 비율만 따로 구하면 균일 맞춤 단계를 놓칩니다.

원본 프레임아이콘 크기계산한 끝점 앵커
꼭 맞음, 0 0 60 8060×80[40, 80]
꼭 맞음, 0 0 60 8090×120[60, 120]
여백 있음, -20 -10 100 12060×80[36, 58]
여백 있음, -20 -10 100 12090×120[54, 87]

이 계산은 변환되지 않은 이미지와 명시적인 중앙 meet 설정에 적용됩니다. 다른 정렬, slice, none에는 그에 맞는 좌표 대응이 필요합니다. 종횡비 안내가 이러한 선택을 다룹니다. 마커 이미지에 추가 CSS 변환이 있다면 별도로 검사해야 합니다.

타일 없는 지도에서 배치를 확인합니다

공식 다운로드 페이지에서 안정 릴리스로 명시한 Leaflet 1.9.4를 사용했습니다. 실험은 버전을 고정한 JavaScript와 CSS를 로컬로 불러오며 타일 레이어를 추가하지 않습니다. 각 지도는 좌표 [0, 0]을 파란 십자선에 투영하며, 그 기준점 옆에 SVG 이미지 하나를 표시합니다.

원본 검사에서는 각 SVG에 동일한 복합 경로 하나가 있고 내장 래스터 이미지는 없었습니다. 브라우저 이미지 크기는 요청한 대로 60×80 및 90×120픽셀이었습니다. 각 이미지에서 관찰한 사각형을 사용해 대응된 끝점 위치를 투영된 지리적 지점과 비교했습니다.

원본아이콘 크기아래쪽 중앙 앵커를 가정했을 때 오차계산한 끝점 앵커의 오차
꼭 맞음60×80오른쪽 10px0px
꼭 맞음90×120오른쪽 15px0px
여백 있음60×80오른쪽 6px, 위쪽 22px0px
여백 있음90×120오른쪽 9px, 위쪽 33px0px

보정한 핀 끝점에는 십자선이 시각적으로도 맞닿았습니다. 이는 Chrome 브라우저 하나에서 직접 만든 테스트 예제를 관찰한 결과이며 모든 SVG, 브라우저, 지도 통합에 대한 보장은 아닙니다. 여백이 있는 핀은 앵커를 고친 뒤에도 같은 이미지 상자 안에서 더 작게 남습니다. 배치와 그림 크기는 별개의 결정입니다.

작동하는 마커 시작 코드를 복사합니다

완전한 이 HTML을 marker.html로 저장하고 로컬 웹 서버에서 제공합니다. Leaflet 다운로드 페이지에 연결된 CDN에서 버전을 고정한 Leaflet 파일을 불러옵니다. SVG를 이미지 URL로 인코딩하므로 아이콘 파일이나 타일 서비스는 필요하지 않습니다. 라이브러리 파일 두 개에는 인터넷 접속이 필요합니다.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Leaflet SVG tip anchor</title>
<link rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
  #map { width: 360px; height: 300px; background: #edf3ef; }
  .cross { position: absolute; z-index: 1000; pointer-events: none;
    width: 24px; height: 24px; transform: translate(-50%, -50%); }
  .cross::before, .cross::after { content: ""; position: absolute;
    background: #006cbe; }
  .cross::before { top: 12px; width: 24px; height: 1px; }
  .cross::after { left: 12px; width: 1px; height: 24px; }
</style>
<div id="map"></div>
<pre id="status">Loading marker…</pre>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="60" height="80" viewBox="-20 -10 100 120" preserveAspectRatio="xMidYMid meet"><path fill="#bf593c" fill-rule="evenodd" d="M30 0 C46 0 60 12 60 28 C60 46 46 54 40 80 C30 62 0 46 0 28 C0 12 14 0 30 0 Z M40 26 A10 10 0 1 0 20 26 A10 10 0 1 0 40 26 Z"/></svg>`;
const size = [60, 80];
const viewBox = [-20, -10, 100, 120];
const sourceTip = [40, 80];
const scale = Math.min(size[0] / viewBox[2], size[1] / viewBox[3]);
const anchor = [
  (sourceTip[0] - viewBox[0]) * scale
    + (size[0] - viewBox[2] * scale) / 2,
  (sourceTip[1] - viewBox[1]) * scale
    + (size[1] - viewBox[3] * scale) / 2
];
const map = L.map("map", {
  zoomControl: false, attributionControl: false,
  dragging: false, scrollWheelZoom: false, doubleClickZoom: false,
  boxZoom: false, keyboard: false, touchZoom: false
}).setView([0, 0], 2);
const position = [0, 0];
const icon = L.icon({
  iconUrl: "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg),
  iconSize: size,
  iconAnchor: anchor
});
const marker = L.marker(position, { icon }).addTo(map);
const container = document.querySelector("#map");
const cross = document.createElement("span");
cross.className = "cross";
container.append(cross);
const point = map.latLngToContainerPoint(position);
cross.style.left = point.x + "px";
cross.style.top = point.y + "px";
(async () => {
  const image = marker.getElement();
  await image.decode();
  const ir = image.getBoundingClientRect();
  const mr = container.getBoundingClientRect();
  const displacement = [
    ir.left + anchor[0] - mr.left - point.x,
    ir.top + anchor[1] - mr.top - point.y
  ];
  document.querySelector("#status").textContent = JSON.stringify({
    version: L.version, size, anchor,
    observedImageSize: [ir.width, ir.height], displacement
  }, null, 2);
})();
</script>
</html>

표시된 상태는 anchor: [36, 58], 이미지 크기 [60, 80], 변위 0을 보고해야 합니다. size를 [90, 120]으로 바꾸면 같은 계산에서 [54, 87]이 나옵니다. 대신 꼭 맞는 프레임을 사용하려면 SVG의 viewBox와 JavaScript viewBox 배열을 모두 0 0 60 80으로 바꿉니다. 둘을 일치시킵니다.

경로는 fill-rule="evenodd"가 적용된 원형 내부 윤곽선을 포함하여 실제 구멍을 만듭니다. MDN fill-rule 참고 문서가 이 규칙을 설명합니다. 예제는 지도 컨테이너에 테두리나 변환을 추가하지 않습니다. 이를 추가하면 뷰포트 사각형과 컨테이너 좌표를 비교할 때 주의해야 합니다.

그림을 바꾸기 전에 대상 환경을 검사합니다

마커가 여전히 좌표에서 어긋난다면 다음 순서로 확인합니다.

  1. SVG 이미지가 불러와졌고 마커 요소가 의도한 픽셀 너비와 높이를 갖는지 확인합니다.
  2. SVG 내부에 적용된 변환까지 포함해 실제 원본 끝점을 찾습니다.
  3. 전체 viewBox, 최소 좌표, 맞춤 모드를 확인합니다.
  4. 프레임이나 아이콘 크기를 바꿀 때마다 앵커를 다시 계산합니다.
  5. 테스트 중에는 무관한 CSS 덮어쓰기를 제거하고, 마지막 스타일과 의도한 표시 크기를 다시 확인합니다.

이 확인 중에는 지리적 좌표를 고정합니다. 이미지 여백을 보상하려고 좌표를 옮기면 그림 배치 오류가 가려집니다.

이는 브라우저 마커 작업 흐름입니다. QGIS SVG 마커는 자체 대상 설정과 매개변수화된 칠 구문을 사용합니다. 일반 브라우저 전달을 위해 평범한 SVG 원본을 보관합니다.

아이콘이 PNG나 JPG로만 있고 편집 가능한 윤곽선이 필요하다면 PerfectVector로 그림을 변환하고, 구멍과 끝점을 확인한 다음 깨끗한 SVG 원본을 저장합니다. 그런 다음 최종 원본 프레임에서 Leaflet 앵커를 계산합니다. 벡터화는 그림을 준비하고, 지도 코드는 위치와 배치를 제공합니다. 기존 SVG의 오프셋만 문제라면 프레임이나 앵커를 직접 수정합니다. 이미지 벡터화 안내는 래스터 원본 변환이 유용한 경우를 설명합니다.

자주 묻는 질문

iconAnchor는 위도와 경도인가요? 아닙니다. 표시 아이콘 이미지 안의 픽셀 위치이며 왼쪽 위 모서리부터 측정합니다. 마커의 지리적 위치는 별도로 제공합니다.

앵커를 고쳐도 여백 있는 SVG가 왜 여전히 작나요? 빈 원본 프레임이 여전히 이미지 상자의 일부를 차지합니다. 앵커 보정은 끝점을 배치할 뿐, 빈 공간을 없애거나 칠해진 도형을 키우지 않습니다.

항상 너비의 절반과 높이 전체를 사용해야 하나요? 보이는 부착점이 실제로 그 위치에 있을 때만 해당 값을 사용합니다. 비대칭 핀과 여백 원본에는 둘 다 위치 마커 그림이어도 다른 값이 필요합니다.

PNG를 SVG로 변환하면 마커 위치가 고쳐지나요? 변환은 편집 가능한 그림을 제공할 수 있습니다. 마커 배치는 여전히 최종 원본 프레임, 표시 크기, 앵커, 지도 좌표가 결정합니다.

출처

  1. Leaflet — Icon 참고 문서 — 픽셀 크기, 앵커 원점, 기본 중앙 정렬, 지리적 정렬입니다.
  2. Leaflet — 사용자 지정 아이콘 마커 — 아이콘을 만들고 마커에 할당하는 방법입니다.
  3. Leaflet — 다운로드 — 안정 릴리스와 버전이 고정된 JavaScript 및 CSS 링크입니다.
  4. MDN — viewBox — 두 프레임에 사용하는 원본 좌표 사각형입니다.
  5. MDN — preserveAspectRatio — xMidYMid meet의 균일 맞춤과 남는 공간 중앙 정렬입니다.
  6. MDN — fill-rule — 투명한 구멍에 사용하는 홀짝 규칙입니다.

블로그의 다른 글

PerfectVector

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

신용카드 불필요