pages/play-puzzle/
|
└──index.tsx
copmonents/play-puzzle
|
└──index.tsx
└──/puzzle-canvas
└──index.tsx
└──create-puzzle.tsx
└──move-puzzle.tsx
└──audio-effect.tsx
└──complete-animation.tsx
└──config-init.tsx
└──find-change.tsx
PuzzleCanvas 컴포넌트를 화면에 출력한다.setConfig, createTilessocket.emit("groupIndex"), socket.emit("getPuzzleConfig"), Puzzle.setting()Puzzle.move()initConfig이 방의 첫 번째 유저라면
import { createTiles } from "@components/play-puzzle/puzzle-canvas/puzzle/create-puzzle";
if (isFirstClient) {
setConfig(puzzleImg, level, Paper);
createTiles();
config = Puzzle.exportConfig();
Puzzle.move(isFirstClient, socket, roomID);
socket.emit("setPuzzleConfig", { roomID: roomID, config: config });
}
이 방의 첫 번째 유저가 아니라면
else {
socket.emit("groupIndex", { roomID: roomID, groupTileIndex: 200 });
socket.on("groupIndex", ({ groupIndex }: { groupIndex: number }) => {
if (!isNaN(groupIndex)) {
Puzzle.groupFirstUpdate(groupIndex);
}
});
socket.on("getPuzzleConfig", (res: Config) => {
Puzzle.setting(getConfig(res, Paper));
Puzzle.move(isFirstClient, socket, roomID);
});
socket.emit("getPuzzleConfig", { roomID: roomID });
}
Puzzle Config
return {
originHeight : img.current.height; //실제 사진의 높이
originWidth: img.current.width; //실제 사진의 너비
imgWidth: originHeight >= originWidth
? Math.round((levelSize[level] * originWidth) / originHeight / 100) * 100
: levelSize[level]; // canas에 나타날 이미지의 너비
imgHeight: originHeight >= originWidth
? levelSize[level]
: Math.round((levelSize[level] * originHeight) / originWidth / 100) * 100; // canvas에 아나탈 이미지의 높이
tilesPerRow: Math.floor(imgWidth / tileWidth); // 한 줄에 타일 개수
tilesPerColumn: Math.floor(imgHeight / tileWidth); // 한 열에 타일 개수
tileWidth: 100; // 한 타일의 길이(타일은 정사각형)
tileMarginWidth: number; // 타일이 잘 맞기 위한 사이값
level: number; // 난이도
imgName: String; // img 태그의 id값
groupTiles: any[]; // 그룹화된 타일들 배열 ([타일, 해당 타일의 그룹(없을 땐 undefined)])
shapes: any[]; // 랜덤으로 생성된 각 타일들의 shape 정보
tiles: any[]; // 타일들 배열
complete: boolean; // 퍼즐 성공 여부
groupTileIndex: number | null; // 그룹화된 타일들 배열의 인덱스
project: any; // html의 캔버스를 감싼 paper 변수. paper 객체를 조작할 떄 필요
puzzleImage: any; // paper.Raster 객체
tileIndexes: any[]; // 타일들 배열의 인덱스
};

getMask에서 위에서 랜덤으로 생성한 모양값 따라 퍼즐 곡선을 만든 paper.path()를 반환받는다.for (let y = 0; y < config.tilesPerColumn; y++) {
for (let x = 0; x < config.tilesPerRow; x++) {
const shape = config.shapes[y * config.tilesPerRow + x];
const mask = getMask(...);
//mask.opacity, mask.strokeColor 퍼즐 투명도와 퍼즐색 설정
//퍼즐의 모양을 복사하여 테두리용 path 생성
const img = getTileRaster(
config.puzzleImage.clone(),
new Size(config.tileWidth, config.tileWidth),
new Point(config.tileWidth * x, config.tileWidth * y),
Math.max(
config.imgWidth / config.originWidth,
config.imgHeight / config.originHeight
)
);
const border = mask.clone();
border.strokeColor = new config.project.Color("#ddd");
// 퍼즐 모양, 퍼즐 배경 이미지, 퍼즐 테두리를 group으로 묶어주면 하나의 타일이 됨
const tile = new config.project.Group([mask, img, border]);
config.tile.push(tile);
config.groupTiles.push([tile, undefined]);
config.groupArr.push(undefined);
config.tileIndexes.push(config.tileIndexes.length);
}
}
Puzzle.setting({...config,});을 호출해 puzzleConfig를 업데이트한다.//_parent와 index는 paperjs의 path의 Project Hierarchy이다.
select_idx = gtile[0].index //해당타일 index;
gtile[0]._parent.addChild(gtile[0]); //해당타일
if (gtile[1] === undefined) {
//그룹화안된 타일을 움직이고 있다면 바뀐 위치를 바로 넣어준다.
gtile[0].position = new Point(newPosition.x, newPosition.y);
} else {
//그룹화된 타일을 움직이고 있다면 같은 그룹인 타일의 위치도 바꿔준다.
config.groupTiles.forEach((gtile_now) => {
if (gtile[1] === gtile_now[1]) {
gtile_now[0].position = new Point(
gtile_now[0].position._x + newPosition.x - originalPosition.x,
gtile_now[0].position._y + newPosition.y - originalPosition.y
);
}
});
}
마우스 클릭을 손에서 떼었을 때, 해당 타일의 그룹화 여부 확인 후 그룹화를 하고 위치보정을 한다. 변화된 위치를 socket.emit("tilePosition")으로 서버의 퍼즐 정보에 저장한다. 선점을 해제한다.
fitTiles() 함수를 호출한다.fitTiles는 mouseUp한 타일과 근처 타일(상하좌우 타일 중 1)이 합쳐져야 하는지 판단하고, preTile과 nowTile의 위치를 보정해준다.(근처에 대충 맞춰도 연결된 타일들로 딱 맞춰준다) 때문에 위치보정만을 위해서도 호출된다.(groupFit) 위치보정을 위한 호출이 아닐 때, 두 타일이 합쳐져야 한다면 uniteFlag의 값을 true로 바꾼다. (위치보정을 위해서 FindChange 컴포넌트의 함수들을 사용한다.)uniteTiles(nowTile, preTile)함수를 호출한다.uniteTiles(nowTile, preTile)는 nowTile의 그룹의 유무, preTiles의 그룹의 유무를 고려하며 두 타일을 합친다. 두 타일 모두 그룹이 있다면 preTile의 그룹이 우선순위가 높다.(nowTile의 타일 또는 그룹의 타입들은 preTile의 그룹에 들어간다.)타일의그룹에 preTile이나 nowTile의 그룹인덱스를 넣으면 그룹화가 되었다고 간주한다.uniteTiles에서 nowTile과 preTile 모두가 그룹화되어 있지 않다면 (※groupIndex 질문하기)uniteTiles에서 dismantling을 하고 그룹과 그룹간의 퍼즐을 맞게 하기 위해 groupFit을 호출한다. (※dismantling 리팩토링할까?) (dismantling은 groupTiles에서 nowTile의 타일의그룹 정보를 undefined하는 작업을 말함)groupFit은 퍼즐 그룹과 퍼즐 그룹을 합칠 때 위치 보정을 위한 함수이다.
groupFit은 이 문제를 해결하는 함수이다.fitTiles를 호출한다.config.groupTiles.forEach((gtile, idx) => {
if (gtile[0] === tile) {
//이번에 이동한 타일이 그룹화가 되지 않았을 때
if (gtile[1] === undefined) {
socket.emit("tilePosition", {
roomID: roomID,
tileIndex: idx,
tilePosition: gtile[0].position,
tileGroup: gtile[1],
changedData: gtile[0],
});
} else {
//이번 이동한 타일이 그룹화가 되었다면
//groupTiles의 모든 원소들의 0번째엔 해당 타일이 없다.
config.groupTiles.forEach((tileNow, index) => {
if (gtile[1] === tileNow[1]) {
//이번 이동한 타일의 그룹과 같은 그룹에 있는 타일들
socket.emit("tilePosition", {
roomID: roomID,
tileIndex: index,
tilePosition: tileNow[0].position,
tileGroup: tileNow[1],
changedData: tileNow[0],
});
}
});
}
}
});
fitTiles는 mouseUp한 타일과 근처 타일(상하좌우 타일 중 1)이 합쳐져야 하는지 판단하고, preTile과 nowTile의 위치를 보정해준다.(근처에 대충 맞춰도 연결된 타일들로 딱 맞춰준다) 때문에 두 타일을 합칠 필요가 없어도 위치보정만을 위해서도 groupFit에서 호출된다.
위치 보정이란 무엇인가

위의 경우 상-하 퍼즐을 맞출 때, y축 이동과 x축 이동을 고려해야 퍼즐이 육안으로 보기에 정확히 맞춰진다. 위의 퍼즐을 기준으로 보았을 때 아래 퍼즐의 y가 오른쪽으로 이동해야 한다. 상하 퍼즐끼리 맞출때 y축, x축 이동을 구현한 함수를 yUp, xUp이라 했다.
반대로 좌-우 퍼즐끼리 맞출 때 y축, x축 이동을 구현한 함수를 yChnage, xChange라고 했다.
groupTiles의 모든 타일들의 그룹이 undefined이며 같을 때 퍼즐이 완성되었다 간주한다.