본문 바로가기

Dev-FE

[WEB] #5. 캔버스에 스파이뷰 추가하기

지도기반의 위치정보를 표출하는 웹서비스를 오랫동안 개발하면서 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