캔버스(htmlCanvasElement)에 이미지 업로드 후, 특정영역에 클릭이벤트를 주는 기능을 구현하고자 한다.
openLayers를 통해서만 활용해 봤을 뿐, 실제로 <canvas /> 태그를 직접 사용해 본 경험이 없어 하나씩 작업해 볼 예정이다.
file upload 를 통해 이미지업로드 후, 이미지정보를 기반으로 캔버스에 비율에 맞춰 도식화한다.
#0. 실행환경
- next 13 프로젝트 생성
- typescript
- tailwind (css)
- zustand
- dexie
#1. 프로젝트 디렉토리 구조
...
∨ src/
∨ app/
> ...
> canvas/ :
> _components:/ : "use client" 컴포넌트
> _store/ : zustand store
> utils/ : 기타 함수들 (포맷변환 등 )
> page.tsx : /canvas 페이지
#2. 이미지 파일 업로드
드래그 앤 드롭 및 클릭으로 이미지파일 업로드 기능 작업
// app/canvas/_components/canvas.tsx
"use client";
import {
ChangeEvent,
DragEvent,
FunctionComponent,
useEffect,
useRef,
useState,
} from "react";
export const Upload: FunctionComponent = () => {
const ref = useRef<HTMLInputElement>(null);
const [imageData, setImageData] = useState<File | undefined>();
/**
* 드래그앤드롭 핸들러
* @param e
* @returns
*/
const dropHandler = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
const dt = e.dataTransfer;
const files = dt.files;
if (files) {
for (let i = 0; i < files.length; i++) {
if (!files[i].type.match("image.*")) {
return;
}
setImageData(files[i]);
}
}
};
/**
* 파일 업로드 핸들러
* @param e
*/
const uploadHandler = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
if (e.target.files?.length) {
setImageData(e.target.files[0]);
}
};
return (
<div
className="border-dashed border-gray-500 border-2 p-2 h-28 flex align-middle justify-center cursor-pointer hover:opacity-80"
onDrop={dropHandler}
onDragOver={(e: DragEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
target.classList.add("opacity-80");
e.preventDefault();
}}
onDragLeave={(e: DragEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
target.classList.remove("opacity-80");
}}
onClick={() => {
ref.current?.click();
}}
>
<input
ref={ref}
type="file"
hidden
accept="image/*"
multiple={false}
onChange={uploadHandler}
/>
클릭 또는 파일 드래그
</div>
);
};
- onDrop: 파일 드롭 핸들러
- onDrop 이벤트를 잘 호출하려면 onDragOver 이벤트에 e.preventDefault()를 꼭 선언해줘야 함
참고: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event) - e.dataTransfer를 통해 드롭된 파일데이터 정보를 활용할 수 있음
- onDrop 이벤트를 잘 호출하려면 onDragOver 이벤트에 e.preventDefault()를 꼭 선언해줘야 함
- onDragOver: 타깃 내 드래그 행위 체크 핸들러
- 파일 드롭 핸들러 제어 및 디자인 컨트롤
- onDragLeave: 아켓 내 드래그 아웃 체크 핸들러
- onClick : 드래그 앤 드롭 외 클릭 파일 가져오기 핸들러
- <input type="file" hidden onChange={uploadHandler} />로 파일 가져오기 이벤트 로직 작업 후, div의 onClick 이벤트에서 해당 input.onChange() 호출
#3. 캔버스에 이미지 추가
#1. 에서 업로드된 이미지파일(imageData : File)을 읽어 메타정보 추출 및 캔버스에 해상도 조절하여 그리기
1. 캔버스 태그 선언
// app/canvas/_components/canvas.tsx
"use client";
import { FunctionComponent, useEffect, useRef, useState } from "react";
export const Canvas: FunctionComponent = () => {
const ref = useRef<HTMLCanvasElement>(null);
return (
<div className="relative w-full h-full">
<canvas
id="canvas"
width="1000"
height="1000"
className="absolute w-full h-full"
ref={ref}
tabIndex={1}
/>
</div>
);
};
2. 이미지 그리기
업로드한 이미지(File)를 읽어 Image 메서드를 이용하여 메타정보와 blob을 추출
추출된 정보들을 기반으로 캔버스의 drawImage() 함수를 통하여 캔버스 중심에 위치하도록 이미지를 그림
// app/canvas/_component/upload.tsx ~ useEffect()
const reader = new FileReader(); // 업로드된 파일 읽기
const canvas = document.querySelector("#canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d");
/**
* 캔버스 해상도 조절
*/
const dpr = window.devicePixelRatio; // 사용 모니터의 단위 해상도 (px)
const rect = canvas.getBoundingClientRect(); // canvas 영역 정보 엇기
canvas.width = rect.width * dpr; // 캔버스 영역 사이즈 x 모니터 단위 해상도
canvas.height = rect.height * dpr; // 캔버스 영역 사이즈 x 모니터 단위 해상도
ctx?.scale(dpr, dpr); // 캔버스 context 영역 스케일 조정
canvas.style.width = `${rect.width}px`; // canvas 높이 고정 - tmp
canvas.style.height = `${rect.height}px`; // canvas 높이 고정 - tmp
reader.addEventListener("load", (e) => { // 이미지파일 처리 프로세스
// 파일 로드
const img = new Image(); // 이미지 처리
if (e.target?.result) {
img.addEventListener("load", () => {
const w = img.naturalWidth; // 실 사이즈
const h = img.naturalHeight; // 실 사이즈
/**
* 비율 구하기
*/
const wr = canvas.width / w;
const hr = canvas.height / h;
const scale = Math.min(wr, hr); // view 안에 담기도록
/**
* 캔버스 중간에 이미지 중심 올 수 있도록 버퍼값 구하기
* d = canvas.center - view-image.center (실제 이미지 사이즈가 아니라, canvas에 그려질 비율조정된 이미지 사이즈)
*/
const dx = canvas.width / 2 - (w * scale) / 2; // 캔버스 중심에 이미지 중심오도록 버퍼값
const dy = canvas.height / 2 - (h * scale) / 2;
ctx?.reset(); // 기존 이미지 지우기
ctx?.drawImage(
img,
0,
0,
img.width,
img.height,
dx,
dy,
w * scale, // dw
h * scale // dy
);
});
img.src = e.target.result as string;
}
});
reader.readAsDataURL(imageData);
- context.drawImage(imgSrc: CanvasImageSource, x: number, y: number, w?: number, h?: number, dx?: number, dy?: number, dw?: number, dh?: number)
- imgSrc: canvas Context에 추가할 이미지 소스
- x, y: 캔버스에 도식화할 이미지 영역의 시작포인트
- w, h: 캔버스에 도식화 할 이미지 영역의 종료 포인트까지의 거리 (시작점으로부터의 거리)
- 이미지 전체 그리기 : x, y. w, h -> 0, 0, img.width, img.height
- 이미지의 특정영역(a1, b1, a2, b2)만 그리기: x, y, w, h -> a1, b1, a2-a1, b2-b1
- dx, dy: 캔버스 콘텍스트에서 이미지 영역의 시작 포인트
- dw, dh: 캔버스 콘테스트에서 이미지 영역의 종료 포인트까지의 거리
- 이미지 비율 구하기
$$ scale_{size} = \frac {size_{canvas}}{size_{image}} $$- 넓이 비율 = 캔버스 너비 / 이미지 너비
- 각 가로, 세로 비율을 구한 후 최솟값을 스케일값으로 지정 (이미지 가로, 세로 중 더 큰 쪽을 기준으로 스케일)
- 각 이미지 사이즈에, 해당 스케일값을 곱하면 캔버스 콘텍스트 상에서의 dx, dy 값을 구할 수 있음
- 이미지가 캔버스 중앙에 오도록 위치값 구하기
$$ d_{size} = \frac {size_{canvas}}{2} - \frac {size_{image} * scale_{size}}{2} $$- 캔버스 중심값 - 이미지 중심값
- 캔버스 중심값에서 스케일 적용한 이미지 중심값을 빼면, 이미지의 시작위치를 계산할 수 있음
- 종료위치 또한 이미지 사이즈에 스케일을 적용하면 구할 수 있음
후기
익숙하게 다루지 않던 부분이라 단순한 알고리즘중심으로 작업했다. 계속 조금씩 작업하다 보면 로직들이 좀 더 깔끔해지지 않을까 싶은 생각이 들었지만, 기능들을 좀 더 활용해보기 위해 우선 진행하기로 했다.
추 후, 캔버스 내 이미지상의 위치값(좌표값), 가이드라인, 확대보기 등 추가적으로 기술할 예정이다.
'Dev-FE' 카테고리의 다른 글
[WEB] #5. 캔버스에 스파이뷰 추가하기 (0) | 2024.03.26 |
---|---|
[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 |