미새문지
퍼즐 만들기 본문
그저께 정보처리기사 필기를 보고왔다. 이제는 아예 컴퓨터로 체크하고 답안 제출을 하다보니 시험지도 없어 문제 추출도 못 하는 것 같더라.
다 풀고 점수 확인하니 72점 안정권으로 들어왔다. 합격자 발표는 3월 12일인가 했는데 아마 크게 변동은 없을거라 붙었다 생각하고 실기도 천천히 준비하려고 한다.
퍼즐
const PUZZLE_ROWS = 5;
const PUZZLE_COLS = 5;
퍼즐의 열과 행 개수를 5개로 초기화했다. 처음엔 10개씩 해서 100개의 퍼즐로 해볼까했는데 이미지가 어려우면 못 풀겠더라. 그래서 무난하게 5x5로 했다.
퍼즐에 사용할 이미지는 어디서 가져온지 모르는 흑정령 그림. 쓸만한 이미지를 찾던 중에 사진 폴더에 있길래 가져왔다.
지피티한테 물어본 결과 퍼즐을 만드는 방식은 css를 사용해 이미지의 배경을 나눠서 쪼개는 방식, canvas를 사용해 이미지를 쪼개고 관리할 수 있는 방식, 두 가지가 있다.
css의 방식은 단순히 퍼즐을 쪼개고 위치 지정해주는 방식이라 기본적으로 쪼개진 퍼즐을 맞출 수 있는 용도로만 사용된다고 하고, 반대로 canvas는 이미지 데이터를 직접 다룰 수 있다고 하여 퍼즐 맞추기 뿐만 아니라 셔플, 드래그같은 방식을 구현하기 좋다고 한다.
type PuzzlePiece = {
id: number;
src: string;
originalIndex: number;
};
퍼즐의 타입 목록은 3개로 나뉘어진다. id는 인덱스를 통해 퍼즐을 구분할 수 있는 값, src는 쪼개진 조각의 이미지 부분을 기억할 수 있는 값, originalIndex는 현재 조각의 원래 인덱스 값으로 퍼즐의 자리가 올바른지 확인하는 값이다.
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [pieces, setPieces] = useState<PuzzlePiece[]>([]);
const [draggingPiece, setDraggingPiece] = useState<PuzzlePiece | null>(null);
const [changePiece, setChangePiece] = useState<number | null>(null);
필요한 상수들을 초기화해준다. 퍼즐 목록을 받아줄 pieces, 드래그하는 조각이 뭔지 저장하는 draggingPiece, 드래그한 조각을 놓은 위치를 저장하는 changePiece
const shuffleArray = (array: any[]) => {
return array.sort(() => Math.random() - 0.5);
};
퍼즐을 무작위로 섞기 위해 Math.random()을 사용한 함수를 만들어준다.
math.random()의 경우 0에서 1사이의 값을 반환하는데, 음수는 배열을 섞지 않고 양수는 배열을 섞기 때문에, -0.5를 해줌으로써 음수와 양수를 왔다갔다 하며 난수를 뽑아낼 수 있다.
const handleDraggingPiece = (piece: PuzzlePiece) => {
setDraggingPiece(piece);
};
const handleChangePiece = (event: React.DragEvent<HTMLDivElement>, index: number) => {
event.preventDefault();
setChangePiece(index);
};
위에서 초기화한 드래그하는 피스 값 적용과 피스를 놓았을 때의 위치 변경을 해줄 함수를 만들어준다.
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
if (draggingPiece === null || changePiece === null) return;
const newPieces = [...pieces];
const draggedIndex = pieces.findIndex(p => p.id === draggingPiece.id);
[newPieces[draggedIndex], newPieces[changePiece]] = [newPieces[changePiece], newPieces[draggedIndex]];
setPieces(newPieces);
setDraggingPiece(null);
setChangePiece(null);
};
피스를 떨궜을 때 변경된 곳로 피스를 변경해주는 함수이다. changePiece는 떨군 피스의 위치값만 바꿔준다면, 이 함수는 바뀐 위치로 해당 피스 값을 이동시키는 역할을 한다.
현재 드래그 중인 피스나 위치 변경한 피스가 없으면 즉시 반환해준다.
findIndex 함수를 통해 현재 드래그중인 피스와 같은 id값의 피스를 찾아 원래 위치와 변경한 위치를 변경해준다.
useEffect(() => {
const image = new window.Image();
image.src = "/img/blackSpirit.jpg";
image.onload = () => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = image.width;
canvas.height = image.height;
const pieceWidth = image.width / PUZZLE_COLS;
const pieceHeight = image.height / PUZZLE_ROWS;
let tempPieces: PuzzlePiece[] = [];
let id = 0;
for (let y = 0; y < PUZZLE_ROWS; y++) {
for (let x = 0; x < PUZZLE_COLS; x++) {
const pieceCanvas = document.createElement("canvas");
pieceCanvas.width = pieceWidth;
pieceCanvas.height = pieceHeight;
const pieceCtx = pieceCanvas.getContext("2d");
if (!pieceCtx) continue;
pieceCtx.drawImage(
image,
x*pieceWidth, y*pieceHeight,
pieceWidth, pieceHeight,
0, 0,
pieceWidth, pieceHeight
);
tempPieces.push({
id: id++,
src: pieceCanvas.toDataURL(),
originalIndex: y * PUZZLE_COLS + x,
});
}
}
setPieces(shuffleArray(tempPieces));
};
}, []);
캔버스에 이미지를 불러올 때 img 태그보다 new Image()가 좋다고 한다. 이미지를 조각내려면 캔버스에 이미지를 그린 후 조각으로 나눠야 하는데, 이 과정에서 이미지가 로드 된 후에만 정상 작동을 한다고 하여 자바스크립트에서 직접 이미지 객체를 생성하는 것이다.
getContext("2d")는 캔버스 요소에서 2d 그림을 그릴 수 있도록 도와주는 객체를 반환한다.
이후 퍼즐 크기와 피스 개수에 맞춰 크기를 지정해주고 각 피스마다 해당 크기에 맞는 이미지를 입힌다.
피스를 만드는 과정에서 toDataURL()은 캔버스 이미지를 base64 문자열로 변환하는 함수라는데, 이렇게 데이터로 저장하게 되면 이후에 이미지 파일을 다시 로드할 필요 없이 조각을 따로 넣을 수 있어서 효율이 좋다고 한다.
피스에 이미지를 다 입혔으면 셔플함수를 통해 퍼즐 위치를 랜덤으로 변경시킨다.
퍼즐이 개수별로 나뉘어서 섞인걸 확인할 수 있다.
올바르게 맞춰주면 끝
퍼즐을 캔버스에 입히는 과정이 이해가 안되서 계속 지피티를 돌려봤었다. three.js을 찍먹했을 때도 캔버스와 카메라 부분을 이해하기 어려웠는데, 나중에 꼭 쓰일 것 같아 캔버스는 학습해봐야겠다.
다음은 이미지를 직접 삽입할 수 있게 하고 퍼즐 이동 횟수 등 여러가지 수치를 넣을 예정
'개발 TIL' 카테고리의 다른 글
Promise.all() (0) | 2025.02.12 |
---|---|
디바운스(Debounce), 쓰로틀(Throttle) (0) | 2025.01.31 |
이벤트 전파(event propagation), 웹 성능 최적화 방법 (0) | 2025.01.31 |
CommonJS와 ES Module의 차이점 (0) | 2025.01.29 |
async와 defer의 차이점 (0) | 2025.01.28 |