지도기반의 위치정보를 표출하는 웹서비스를 오랫동안 개발하면서 openlayers(ol)를 정말 잘 활용했었다.
openlayers에 layer spy라는 예제가 있었는데, 이 로직을 기반으로 캔버스를 이용하여 스파이뷰를 작업해 보았다.
캔버스에 이미지를 올리고, 이미지에 벡터영역을 그리기 위해 위치를 잘 잡을 수 있도록 가이드라인과 스파이뷰가 필요해졌다.
캔버스에 마우스오버 시 확대된 스파이뷰를 표현할 예정이다.
#0. 실행환경
[WEB] #4. 캔버스 이미지 처리 프로젝트에 덧붙여 진행
- 1차로 새로운 캔버스를 생성하여 작업했으며,
- 가능하다면 ol처럼 레이어 관리를 통해 진행해 볼 예정이다.
#1. 디렉터리 구조
...
∨ canvas/
...
> interaction/
> guideLine.tsx : 가이드라인 컴포넌트
> position.tsx : 마우스 위치좌표 컴포넌트
> spyView.tsx : 스파이뷰 컴포넌트
#2. 스파이뷰
react Component 형식으로 작업하여 호출하는 방식으로 사용
- 소스캔버스(target)에서 마우스위치정보를 이용하여 캔버스의 좌표 취득
- 반경만큼의 이미지 데이터를 추출하여 스파이뷰(spyCanvas)로 이미지 복사
// app/canvas/interaction/spyView.tsx
import { FunctionComponent, useEffect } from "react";
interface SpyViewProps {
target?: string;
}
export const SpyView: FunctionComponent<SpyViewProps> = ({
target = "#canvas",
}) => {
useEffect(() => {
const canvas = document.querySelector(
target || "#canvas"
) as HTMLCanvasElement;
const dpr = window.devicePixelRatio;
if (canvas) {
const moveHandler = (e: MouseEvent) => {
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
const x = e.clientX * dpr;
const y = e.clientY * dpr;
const offset = 100;
const imgData = ctx.getImageData(
x - offset / 2,
y - offset / 2,
offset,
offset
);
const spyCanvas = document.querySelector(
"#spyCanvas"
) as HTMLCanvasElement;
const ctx2 = spyCanvas.getContext("2d") as CanvasRenderingContext2D;
ctx2.reset();
ctx2.beginPath();
ctx2.putImageData(imgData, 0, 0);
ctx2.scale(dpr, dpr);
};
canvas?.addEventListener("mousemove", moveHandler);
return () => {
canvas?.removeEventListener("mousemove", moveHandler);
};
}
}, [target]);
return (
<div
className="absolute w-[100px] h-[100px] rounded-full overflow-hidden shadow-2xl">
<canvas
id="spyCanvas"
width={100}
height={100}
className="w-[100px] h-[100px] bg-slate-300"
/>
</div>
);
};
- 소스캔버스에 mousemove 이벤트 추가
- x = event.clientX * dpr; y = event.clientY * dpr;
- mousemove event.clientX, event.clientY 값으로부터 캔버스에서의 위치좌표 추출
- clientX, clientY값에 window.devicePixelRatio; 값을 곱하면 캔버스에 해당위치값 얻을 수 있음
- ( WEB #4. 에서 캔버스영역에 스케일조정을 dpr로 했기 때문)
- offset: 표출할 사이즈
- context.getImageData(sx: number, sy: number, sw: number, sh: number)
- 소스캔버스(target)의 해당영역 이미지를 추출
- sx, sy = x - offset / 2: 마우스 위치를 스파이뷰 중심으로 오도록 좌표조정
- context.putImagData(src: ImageData, dx: number, dy: number)
- 스파이뷰(spyCanvas)에 이미지데이터 추가
#3. 스파이뷰에 가이드라인 추가
원형 스파이뷰에 중심을 알기 쉽도록 tailwindcss를 이용하여 가이드라인을 표현
- ::before, ::after를 이용하여 십자선 표시
- 기본적으로 마우스 우측하단에 버퍼(100) 간격에 위치시키며, 캔버스의 우측&하단 가장자리에 마우스가 있을 경우 좌상단으로 위치 변경하도록 로직 처리
// app/canvas/interaction/spyView.tsx ... return ()
<div
className="absolute w-[100px] h-[100px] rounded-full overflow-hidden shadow-2xl
before:absolute before:w-[1px] before:h-[100px] before:top-0 before:left-1/2 before:bg-gray-500
after:absolute after:w-[100px] after:h-[1px] after:top-1/2 after:left-0 after:bg-gray-500
"
>
<canvas
id="spyCanvas"
width={100}
height={100}
className="w-[100px] h-[100px] bg-slate-300"
/>
</div>
- spyCanvas를 감싸는 div에 before, after추가
// app/canvas/interaction/spyView.tsx
// ... useEffect
const bounding = canvas.getBoundingClientRect();
// ... moveHandler
const spyCanvas = document.querySelector(
"#spyCanvas"
) as HTMLCanvasElement;
if (bounding.width - 100 > e.offsetX + 10) {
// 우측에 가까운지 체크
(spyCanvas.parentElement as HTMLDivElement).style.left = `${
e.offsetX + 10
}px`;
(spyCanvas.parentElement as HTMLDivElement).style.right = "unset";
} else {
(spyCanvas.parentElement as HTMLDivElement).style.right = `${
bounding.width - e.offsetX + 10
}px`;
(spyCanvas.parentElement as HTMLDivElement).style.left = "unset";
}
if (bounding.height - 100 > e.offsetY + 10) {
// 하단에 가까운지 체크
(spyCanvas.parentElement as HTMLDivElement).style.top = `${
e.offsetY + 10
}px`;
(spyCanvas.parentElement as HTMLDivElement).style.bottom = "unset";
} else {
(spyCanvas.parentElement as HTMLDivElement).style.top = "unset";
(spyCanvas.parentElement as HTMLDivElement).style.bottom = `${
bounding.height - e.offsetY + 10
}px`;
}
- 스파이뷰의 너비, 높이와 각 clientX, clientY 값의 차를 비교하여 좌우, 상하 위치 조정
#4. 전체코드
#2, #3을 합치면 전체코드는 아래와 같음
// app/canvas/interaction/spyView.tsx
import { FunctionComponent, useEffect } from "react";
interface SpyViewProps {
target?: string;
}
export const SpyView: FunctionComponent<SpyViewProps> = ({
target = "#canvas",
}) => {
useEffect(() => {
const canvas = document.querySelector(
target || "#canvas"
) as HTMLCanvasElement;
const bounding = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio;
if (canvas) {
const moveHandler = (e: MouseEvent) => {
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
const x = e.clientX * dpr;
const y = e.clientY * dpr;
const offset = 100;
const imgData = ctx.getImageData(
x - offset / 2,
y - offset / 2,
offset,
offset
);
const spyCanvas = document.querySelector(
"#spyCanvas"
) as HTMLCanvasElement;
if (bounding.width - 100 > e.offsetX + 10) {
// 우측에 가까운지 체크
(spyCanvas.parentElement as HTMLDivElement).style.left = `${
e.offsetX + 10
}px`;
(spyCanvas.parentElement as HTMLDivElement).style.right = "unset";
} else {
(spyCanvas.parentElement as HTMLDivElement).style.right = `${
bounding.width - e.offsetX + 10
}px`;
(spyCanvas.parentElement as HTMLDivElement).style.left = "unset";
}
if (bounding.height - 100 > e.offsetY + 10) {
// 하단에 가까운지 체크
(spyCanvas.parentElement as HTMLDivElement).style.top = `${
e.offsetY + 10
}px`;
(spyCanvas.parentElement as HTMLDivElement).style.bottom = "unset";
} else {
(spyCanvas.parentElement as HTMLDivElement).style.top = "unset";
(spyCanvas.parentElement as HTMLDivElement).style.bottom = `${
bounding.height - e.offsetY + 10
}px`;
}
const ctx2 = spyCanvas.getContext("2d") as CanvasRenderingContext2D;
ctx2.reset();
ctx2.beginPath();
ctx2.putImageData(imgData, 0, 0);
ctx2.scale(dpr, dpr);
};
canvas?.addEventListener("mousemove", moveHandler);
return () => {
canvas?.removeEventListener("mousemove", moveHandler);
};
}
}, [target]);
return (
<div
className="absolute w-[100px] h-[100px] rounded-full overflow-hidden shadow-2xl
before:absolute before:w-[1px] before:h-[100px] before:top-0 before:left-1/2 before:bg-gray-500
after:absolute after:w-[100px] after:h-[1px] after:top-1/2 after:left-0 after:bg-gray-500
"
>
<canvas
id="spyCanvas"
width={100}
height={100}
className="w-[100px] h-[100px] bg-slate-300"
/>
</div>
);
};
후기
새로운 canvas를 이용하여 스파이뷰를 작업했지만 레이어활용을 한다면 하나의 캔버스로도 작업할 수 있을 것으로 보인다.
레이어 관리 기능 개발 후 수정해 볼 예정이다.
참고링크
Layer Spy
Layer rendering can be manipulated in prerender and postrender event listeners. These listeners get an event with a reference to the Canvas rendering context. In this example, the prerender listener sets a clipping mask around the most recent mouse positio
openlayers.org
'Dev-FE' 카테고리의 다른 글
[WEB] #4. 캔버스 이미지 처리 (1) | 2024.03.18 |
---|---|
[JS,TS] 파일 사이즈 단위 변환 (0) | 2024.03.12 |
[React-Antd]#1. upload render 디자인 커스텀하기 (0) | 2024.02.28 |
[React-Native] #2. React Native - navigation 이용한 페이지 이동 (0) | 2022.10.30 |
[React-Native] #1. React Native 프로젝트 첫 시작하기 (0) | 2022.10.19 |