<div id="root"></div>
const { useState, useRef, useCallback, useEffect, useReducer } = React;
const { createGlobalStyle, css } = styled;
const SLIDE_PUZZLE_SIZE = {
height: "60vmin",
width: "60vmin"
};
const BG =
"https://images.ctfassets.net/w6gireivdm0c/61UEuQEwHoSoz3GIpNkF9e/1d18ada4da6c6de4912efa71f57d44aa/NEKO9V9A9131_TP_V4.jpg";
const SHUFFLE_SPEED = 60;
const MAX_SHUFFLE_COUNT = 100;
const LEVEL = {
easy: 9,
normal: 16,
hard: 25
};
const BOARD_COLOR = "#997f5d";
const fitImage = async (url, h, w) => {
const image = await loadImage(url).then((r) => r);
const width = image.width;
const height = image.height;
const ratio = height / width;
const fittedImageSize = {
height: 0,
width: 0
};
if (ratio >= 1) {
fittedImageSize.height = height * (w / width);
fittedImageSize.width = w;
} else {
fittedImageSize.height = h;
fittedImageSize.width = width * (h / height);
}
return fittedImageSize;
};
const clamp = (value, min, max) => {
if (value < min) return min;
else if (value > max) return max;
return value;
};
const loadImage = (url) => {
return new Promise((resolve) => {
const img = new Image();
img.src = url;
img.crossOrigin = "Anonymous";
img.addEventListener("load", (e) => {
resolve(e.target);
});
});
};
async function getSquareImage(url, sideLength) {
const image = await loadImage(url).then((r) => r);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const ratio =
image.height / image.width >= 1
? sideLength / image.width
: sideLength / image.height;
canvas.width = sideLength; //image.width
canvas.height = sideLength; //image.width
ctx.drawImage(
image,
sideLength / 2 - (image.width * ratio) / 2,
sideLength / 2 - (image.height * ratio) / 2,
image.width * ratio,
image.height * ratio
);
const result = canvas.toDataURL("image/jpeg");
//console.log(result)
return result;
}
async function divideImage(url, rows, cols) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// const squareImageUrl = await getSquareImage(url).then(r => r)
const image = await loadImage(url).then((r) => r);
//console.log(image)
canvas.width = image.width / rows;
canvas.height = image.height / cols;
const dividedImages = [];
let count = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
ctx.drawImage(
image,
(j * image.width) / rows,
(i * image.height) / cols,
image.width / rows,
image.height / cols,
0,
0,
canvas.width,
canvas.height
);
const clipedImage = canvas.toDataURL("image/jpeg");
dividedImages[count] = clipedImage;
count++;
}
}
//console.log(dividedImages)
return dividedImages;
}
const initialState = {
isLock: true,
isShuffle: false,
isComplete: false,
bg: null,
pieces: [],
pieceLength: 0,
width: 0,
height: 0,
trouble: 0
};
const slidePuzzleReducer = (state, action) => {
switch (action.type) {
case "RESIZE_SLIDE_PUZZLE": {
if (!state.pieces.length) {
return {
...state
};
}
const pieces = [...state.pieces];
const pieceLength = state.pieceLength;
const width = action.width;
const height = action.height;
const rows = Math.sqrt(pieceLength);
const cols = rows;
let count = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const pieceWidth = width / rows;
const pieceHeight = height / cols;
pieces[count].x *= pieceWidth / pieces[count].width;
pieces[count].y *= pieceHeight / pieces[count].height;
pieces[count].basePointX *= pieceWidth / pieces[count].width;
pieces[count].basePointY *= pieceHeight / pieces[count].height;
pieces[count].height = pieceHeight;
pieces[count].width = pieceWidth;
count++;
}
}
return {
...state,
pieces,
height,
width
};
}
case "RESET_SLIDE_PUZZLE": {
return {
...state,
pieces: [],
pieceLength: 0,
bg: null,
isLock: true,
isComplete: false,
isShuffle: false,
width: 0,
height: 0,
trouble: 0
};
}
case "INITIALIZE_PIECES": {
const pieces = [...state.pieces];
const pieceLength = state.pieceLength;
const height = action.height;
const width = action.width;
const rows = Math.sqrt(pieceLength);
const cols = rows;
const pieceImages = action.pieceImages;
let count = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const pieceHeight = height / cols;
const pieceWidth = width / rows;
const x = j * pieceHeight;
const y = i * pieceHeight;
pieces[count] = {
...pieces[count],
id: count,
baseId: count,
image: pieceImages[count],
x,
y,
basePointX: x,
basePointY: y,
blank: count === pieceLength - 1 ? true : false,
isDrag: false,
isShuffle: false,
height: pieceHeight,
width: pieceWidth,
offsetX: 0,
offsetY: 0
};
count++;
}
}
return {
...state,
pieces
};
}
case "SHUFFLE_PIECES": {
const pieces = [...state.pieces];
pieces.map((piece) => ({
...piece,
isShuffle: false
}));
const pieceLength = pieces.length;
const blank = pieces.find((piece) => piece.blank);
const piecesNextToBlank = pieces.filter((piece, i) => {
return (
(piece.id === blank.id - 1 && blank.y === piece.y) || ///Math.sqrt(pieceLength) * piece.width - piece.width) ||
(piece.id === blank.id + 1 && blank.y === piece.y) || // && piece.x !== 0を追加して空白の次が一行ずれていたらだめ
(piece.id === blank.id - Math.sqrt(pieceLength) && blank.x === piece.x) ||
(piece.id === blank.id + Math.sqrt(pieceLength) && blank.x === piece.x)
);
});
console.log(piecesNextToBlank);
const r = Math.floor(Math.random() * piecesNextToBlank.length);
console.log(r);
const movingPiece = piecesNextToBlank[r];
console.log(movingPiece);
if (!movingPiece) {
return {
...state,
pieces
};
}
if (movingPiece.isShuffle) {
return {
...state,
pieces: pieces.map((piece) => ({
...piece,
isShuffle: false
}))
};
}
return {
...state,
pieces: pieces.map((piece) =>
piece === movingPiece
? {
...piece,
id: blank.id,
x: blank.x,
y: blank.y,
basePointX: blank.x,
basePointY: blank.y,
isShuffle: true
}
: piece === blank
? {
...piece,
id: movingPiece.id,
x: movingPiece.x,
y: movingPiece.y,
basePointX: movingPiece.x,
basePointY: movingPiece.y,
isShuffle: false
}
: piece
)
};
}
case "SORT_PIECES": {
const pieces = [...state.pieces].sort((a, b) => a.id - b.id);
return {
...state,
pieces
};
}
case "DRAG_START_PIECE": {
const pieces = [...state.pieces].map((piece) =>
piece.baseId === action.num
? {
...piece,
isDrag: true
}
: piece
);
return {
...state,
pieces
};
}
case "LIMIT_MOVEMENT_PIECE": {
const pieces = [...state.pieces];
const width = state.width;
const height = state.height;
pieces.forEach((piece) => {
if (piece.isDrag) {
if (piece.x <= 0) {
piece.offsetX = 0;
}
if (piece.x + piece.width >= width) {
piece.offsetX = 0;
}
if (piece.y <= 0) {
piece.offsetY = 0;
}
if (piece.y + piece.height >= height) {
piece.offsetY = 0;
}
}
});
return {
...state,
pieces
};
}
case "DRAG_MOVE_PIECE": {
const pieces = [...state.pieces];
const pieceLength = pieces.length;
const elementOffsetX = action.elementOffsetX;
const elementOffsetY = action.elementOffsetY;
pieces.forEach((piece) => {
if (piece.isDrag) {
if (
pieces[piece.id - 1] &&
pieces[piece.id - 1].blank &&
pieces[piece.id - 1].y === piece.y
) {
console.log("is-left");
pieces[piece.id] = {
...piece,
offsetX: clamp(elementOffsetX, -piece.width, 0)
};
} else if (
pieces[piece.id + 1] &&
pieces[piece.id + 1].blank &&
pieces[piece.id + 1].y === piece.y //同じ行であれば。例えば2, 3は番号でいえば隣り合っているけどピースは1行ずれているからダメ
) {
console.log("is-right");
pieces[piece.id] = {
...piece,
offsetX: clamp(elementOffsetX, 0, piece.width)
};
} else if (
pieces[piece.id - Math.sqrt(pieceLength)] &&
pieces[piece.id - Math.sqrt(pieceLength)].blank &&
pieces[piece.id - Math.sqrt(pieceLength)].x === piece.x
) {
console.log("is-top");
pieces[piece.id] = {
...piece,
offsetY: clamp(elementOffsetY, -piece.height, 0)
};
} else if (
pieces[piece.id + Math.sqrt(pieceLength)] &&
pieces[piece.id + Math.sqrt(pieceLength)].blank &&
pieces[piece.id + Math.sqrt(pieceLength)].x === piece.x
) {
console.log("is-bottom");
pieces[piece.id] = {
...piece,
offsetY: clamp(elementOffsetY, 0, piece.height)
};
}
}
});
return {
...state,
pieces
};
}
case "DRAG_END_PIECE": {
const pieces = [...state.pieces];
const pieceLength = pieces.length;
const replacePiece = (targetPiece, blank) => {
pieces[targetPiece.id] = {
...pieces[targetPiece.id],
id: blank.id,
x: blank.basePointX,
y: blank.basePointY,
basePointX: blank.basePointX,
basePointY: blank.basePointY,
offsetX: 0,
offsetY: 0
};
pieces[blank.id] = {
...pieces[blank.id],
id: targetPiece.id,
x: targetPiece.basePointX,
y: targetPiece.basePointY,
basePointX: targetPiece.basePointX,
basePointY: targetPiece.basePointY,
offsetX: 0,
offsetY: 0
};
};
let trouble = 0;
pieces.forEach((piece) => {
if (piece.isDrag) {
if (
Math.abs(piece.offsetX) > piece.width / 2 ||
Math.abs(piece.offsetY) > piece.height / 2
) {
if (piece.offsetX < 0) {
const blank = pieces[piece.id - 1];
replacePiece(piece, blank);
} else if (piece.offsetX > 0) {
const blank = pieces[piece.id + 1];
replacePiece(piece, blank);
} else if (piece.offsetY < 0) {
const blank = pieces[piece.id - Math.sqrt(pieceLength)];
replacePiece(piece, blank);
} else if (piece.offsetY > 0) {
const blank = pieces[piece.id + Math.sqrt(pieceLength)];
replacePiece(piece, blank);
}
trouble = state.trouble + 1;
} else {
piece.offsetX = 0;
piece.offsetY = 0;
trouble = state.trouble;
}
}
});
return {
...state,
pieces: pieces.map((piece) =>
piece.isDrag
? {
...piece,
isDrag: false
}
: piece
),
trouble
};
}
case "CHANGE_SLIDE_PUZZLE_BG": {
return {
...state,
bg: action.url
};
}
case "CHANGE_PIECE_LENGTH": {
return {
...state,
pieceLength: action.num
};
}
case "INITIALIZE_SLIDE_PUZZLE": {
const pieces = [...state.pieces];
const pieceLength = action.pieceLength;
const height = action.height;
const width = action.width;
const pieceImages = action.pieceImages;
const rows = Math.sqrt(pieceLength);
const cols = rows;
let count = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const pieceHeight = height / cols;
const pieceWidth = width / rows;
const x = j * pieceHeight;
const y = i * pieceHeight;
pieces[count] = {
...pieces[count],
id: count,
baseId: count,
image: pieceImages[count],
x,
y,
basePointX: x,
basePointY: y,
blank: count === pieceLength - 1 ? true : false,
isDrag: false,
isShuffle: false,
height: pieceHeight,
width: pieceWidth,
offsetX: 0,
offsetY: 0
};
count++;
}
}
return {
...state,
pieces,
pieceLength
};
}
case "START_SHUFFLE_PIECES": {
return {
...state,
isShuffle: true
};
}
case "STOP_SHUFFLE_PIECES": {
const pieces = [...state.pieces].map((piece) => ({
...piece,
isShuffle: false
}));
return {
...state,
pieces,
isShuffle: false
};
}
case "LOCK_SLIDE_PUZZLE": {
return {
...state,
isLock: true
};
}
case "UNLOCK_SLIDE_PUZZLE": {
return {
...state,
isLock: false
};
}
case "COMPLETE_SLIDE_PUZZLE": {
return {
...state,
isComplete: true
};
}
case "INCOMPLETE_SLIDE_PUZZLE": {
return {
...state,
isComplete: false
};
}
case "RESET_TROUBLE": {
return {
...state,
trouble: 0
};
}
default:
throw new Error();
}
};
const useDragAndDrop = () => {
const [elementPosition, setElementPosition] = useState({ top: 0, left: 0 });
const [elementOffset, setElementOffset] = useState({ x: 0, y: 0 });
const pointerStartPosition = useRef({ x: null, y: null });
const pointerMovePosition = useRef({ x: null, y: null });
const currentDragElement = useRef(null);
const prevElementOffset = useRef({ x: 0, y: 0 });
const prevElementOffsetX = useRef(0);
const prevElementOffsetY = useRef(0);
const getCurrentPosition = (elem) => {
const { top, left } = elem.getBoundingClientRect();
return { top, left };
};
const moveDistance = (distance) =>
setElementOffset({
x: prevElementOffset.current.x + distance.x,
y: prevElementOffset.current.y + distance.y
});
const resetElementOffset = () => {
setElementOffset({
x: 0,
y: 0
});
prevElementOffset.current = {
x: 0,
y: 0
};
};
const resetPointerStartPosition = () => {
if (
pointerStartPosition.current.x === null ||
pointerStartPosition.current.y === null
)
return;
pointerStartPosition.current.x = null;
pointerStartPosition.current.y = null;
};
const handleMouseDown = (e) => {
e.preventDefault();
pointerStartPosition.current.x = e.clientX;
pointerStartPosition.current.y = e.clientY;
currentDragElement.current = e.target;
const elementCurrentPosition = getCurrentPosition(currentDragElement.current);
setElementPosition({
top: elementCurrentPosition.top,
left: elementCurrentPosition.left
});
};
const handleMouseMove = (e) => {
e.preventDefault();
if (!currentDragElement.current) return;
console.log("suga move: ", currentDragElement.current);
if (
pointerStartPosition.current.x === null ||
pointerStartPosition.current.y === null
)
return;
pointerMovePosition.current.x = e.clientX;
pointerMovePosition.current.y = e.clientY;
const pointerMoveDistance = {
x: pointerMovePosition.current.x - pointerStartPosition.current.x,
y: pointerMovePosition.current.y - pointerStartPosition.current.y
};
moveDistance(pointerMoveDistance);
};
const handleMouseUp = (e) => {
e.preventDefault();
if (!currentDragElement.current) return;
console.log("suga up: ", currentDragElement.current);
resetPointerStartPosition();
const elementCurrentPosition = getCurrentPosition(currentDragElement.current);
setElementPosition({
top: elementCurrentPosition.top,
left: elementCurrentPosition.left
});
currentDragElement.current = null;
};
useEffect(() => {
document.body.addEventListener("mousemove", handleMouseMove);
document.body.addEventListener("mouseup", handleMouseUp);
document.body.addEventListener("mouseleave", handleMouseUp);
return () => {
document.body.removeEventListener("mousemove", handleMouseMove);
document.body.removeEventListener("mouseup", handleMouseUp);
document.body.removeEventListener("mouseleave", handleMouseUp);
};
}, []);
useEffect(() => {
prevElementOffset.current = {
x: elementOffset.x,
y: elementOffset.y
};
}, [elementPosition.left, elementPosition.top]);
return [
{
currentDragElement,
elementPosition,
elementOffset
},
{
pointerStartPosition,
pointerMovePosition
},
handleMouseDown,
setElementOffset,
resetElementOffset,
setElementPosition
];
};
const useResizeObserver = (elements, callback) => {
useEffect(() => {
console.log("mari");
const resizeObserver = new ResizeObserver((entries) => callback(entries));
for (const elem of elements) {
//console.log(elem);
elem.current && resizeObserver.observe(elem.current);
}
return () => resizeObserver.disconnect();
}, []);
};
const useMutationObserver = (elements, callback, config) => {
useEffect(() => {
const mutationObserver = new MutationObserver((mutations) => {
mutationObserver.disconnect();
callback(mutations);
for (const elem of elements) {
elem.current && mutationObserver.observe(elem.current, config);
}
});
for (const elem of elements) {
elem.current && mutationObserver.observe(elem.current, config);
}
return () => mutationObserver.disconnect();
}, []);
};
const useSlidePuzzle = (bg, pieceLength) => {
const [
element,
pointer,
handleMouseDown,
setElementOffset,
resetElementOffset,
setElementPosition
] = useDragAndDrop();
const { currentDragElement, elementPosition, elementOffset } = element;
const { pointerStartPosition, pointerMovePosition } = pointer;
const [slidePuzzle, dispatch] = useReducer(slidePuzzleReducer, initialState);
const slidePuzzleNode = useRef(null);
const intervalId = useRef(null);
const initializeSlidePuzzle = async () => {
// https://shanabrian.com/web/javascript/element-client-width-height.php
const width = slidePuzzleNode.current.clientWidth;
const height = slidePuzzleNode.current.clientHeight;
const rows = Math.sqrt(pieceLength);
const cols = rows;
const squareImageUrl = await getSquareImage(bg, width).then((r) => r);
const pieceImages = divideImage(squareImageUrl, rows, cols);
pieceImages.then((images) =>
dispatch({
type: "INITIALIZE_SLIDE_PUZZLE",
pieceLength,
pieceImages: images,
height,
width
})
);
};
/*const initializePieces = useCallback(() => {
const rect = slidePuzzleNode.current;
const pieceLength = slidePuzzle.pieceLength;
const bg = slidePuzzle.bg;
// https://shanabrian.com/web/javascript/element-client-width-height.php
const width = rect.clientWidth; //width//puzzle.width
const height = rect.clientHeight; //puzzle.height
const rows = Math.sqrt(pieceLength);
const cols = rows;
const pieceImages = divideImage(bg, rows, cols);
pieceImages.then((images) =>
dispatch({
type: "INITIALIZE_PIECES",
//pieceLength,
pieceImages: images,
height,
width
})
);
}, [
slidePuzzle.bg,
slidePuzzle.pieceLength,
slidePuzzle.width,
slidePuzzle.height,
slidePuzzle.pieceImages
]);*/
const changeSlidePuzzleBg = useCallback((url) =>
dispatch({
type: "CHANGE_SLIDE_PUZZLE_BG",
url
})
);
const changePieceLength = useCallback((num) =>
dispatch({
type: "CHANGE_PIECE_LENGTH",
num
})
);
const startShufflePieces = useCallback(() =>
dispatch({
type: "START_SHUFFLE_PIECES"
})
);
const stopShufflePieces = useCallback(() =>
//clearInterval(intervalId.current)
dispatch({
type: "STOP_SHUFFLE_PIECES"
})
);
const lockSlidePuzzle = useCallback(() =>
dispatch({
type: "LOCK_SLIDE_PUZZLE"
})
);
const unlockSlidePuzzle = useCallback(() =>
dispatch({
type: "UNLOCK_SLIDE_PUZZLE"
})
);
const completeSlidePuzzle = useCallback(() =>
dispatch({
type: "COMPLETE_SLIDE_PUZZLE"
})
);
const incompleteSlidePuzzle = useCallback(() =>
dispatch({
type: "INCOMPLETE_SLIDE_PUZZLE"
})
);
const resetSlidePuzzle = useCallback(() =>
dispatch({
type: "RESET_SLIDE_PUZZLE"
})
);
const resetTrouble = useCallback(() =>
dispatch({
type: "RESET_TROUBLE"
})
);
useResizeObserver([slidePuzzleNode], (entries) => {
const height = entries[0].contentRect.height;
const width = entries[0].contentRect.width;
if (height === 0 || width === 0) return;
dispatch({
type: "RESIZE_SLIDE_PUZZLE",
width,
height
});
});
useEffect(() => {
if (!slidePuzzle.isShuffle) return;
let count = 0;
intervalId.current = setInterval(() => {
count++;
dispatch({
type: "SHUFFLE_PIECES"
});
if (count > MAX_SHUFFLE_COUNT) {
clearInterval(intervalId.current);
dispatch({
type: "SORT_PIECES"
});
dispatch({
type: "STOP_SHUFFLE_PIECES"
});
dispatch({
type: "UNLOCK_SLIDE_PUZZLE"
});
}
}, SHUFFLE_SPEED);
}, [slidePuzzle.isShuffle]);
/*useEffect(() => {
if (slidePuzzle.isShuffle) return;
clearInterval(intervalId.current);
}, [slidePuzzle.isShuffle]);*/
// mousedown
useEffect(() => {
if (slidePuzzle.isLock) return;
if (!currentDragElement.current) return;
const num = parseFloat(
currentDragElement.current.className.replace(/[^0-9]/g, "")
);
dispatch({
type: "DRAG_START_PIECE",
num
});
}, [elementPosition]);
// mousemove
useEffect(() => {
if (slidePuzzle.isLock) return;
if (!currentDragElement.current) return;
dispatch({
type: "LIMIT_MOVEMENT_PIECE"
});
dispatch({
type: "DRAG_MOVE_PIECE",
elementOffsetX: elementOffset.x,
elementOffsetY: elementOffset.y
});
}, [elementOffset.x, elementOffset.y]);
//const [pieces, setPieces] = useState([])
/*useMutationObserver(
[slidePuzzleNode],
(mutations) => {
setPieces(pieces => {
pieces.forEach(piece => {
if(piece.isDrag){
if(
pieces[piece.id - 1] &&
pieces[piece.id - 1].blank &&
pieces[piece.id - 1].y === piece.y
) {
console.log('is-left')
pieces[piece.id] = {
...piece,
offsetX: clamp(
elementOffsetX,
-piece.width,//pieces[piece.id - 1].x,
0//pieces[piece.id - 1].x + piece.width
)
}
}
else if(
pieces[piece.id + 1] &&
pieces[piece.id + 1].blank &&
pieces[piece.id + 1].y === piece.y//同じ行であれば。例えば2, 3は番号でいえば隣り合っているけどピースは1行ずれているからダメ
) {
console.log('is-right')
pieces[piece.id] = {
...piece,
offsetX: clamp(
elementOffsetX,
0,//pieces[piece.id + 1].x - piece.width,
piece.width//pieces[piece.id + 1].x
)
}
}
else if(
pieces[piece.id - Math.sqrt(pieceLength)] &&
pieces[piece.id - Math.sqrt(pieceLength)].blank &&
pieces[piece.id - Math.sqrt(pieceLength)].x === piece.x
) {
console.log('is-top')
pieces[piece.id] = {
...piece,
offsetY: clamp(
elementOffsetY,
-piece.height,//pieces[piece.id - Math.sqrt(pieceLength)].y,
0//pieces[piece.id - Math.sqrt(pieceLength)].y + piece.height
)
}
}
else if(
pieces[piece.id + Math.sqrt(pieceLength)] &&
pieces[piece.id + Math.sqrt(pieceLength)].blank &&
pieces[piece.id + Math.sqrt(pieceLength)].x === piece.x
) {
console.log('is-bottom')
pieces[piece.id] = {
...piece,
offsetY: clamp(
elementOffsetY,
0,//pieces[piece.id + Math.sqrt(pieceLength)].y - piece.height,
piece.height//pieces[piece.id + Math.sqrt(pieceLength)].y
)
}
}
}
})
})
},
{
attributes: true,
subtree: false,
childList: false,
attributeFilter: ["class"]
}
)*/
// mouseup
useEffect(() => {
if (slidePuzzle.isLock) return;
if (currentDragElement.current) return;
dispatch({
type: "DRAG_END_PIECE"
});
dispatch({
type: "SORT_PIECES"
});
resetElementOffset();
}, [elementPosition]);
//素早いmousemoveからのmouseup後にelementOffsetXが更新されてしまうため、ここでもリセット
/*useEffect(() => {
if (slidePuzzle.isLock) return;
if (currentDragElement.current) return;
console.log("oua: ", elementOffset.x);
resetElementOffset();
}, [elementOffset.x, elementOffset.y]);*/
// mouseup後にpuzzleが変更されたら
useEffect(() => {
if (slidePuzzle.isLock) return;
console.log(currentDragElement.current);
if (currentDragElement.current) return;
const matchPieces = slidePuzzle.pieces.filter(
(piece) => piece.id === piece.baseId
);
//console.log(matchPieces)
console.log(slidePuzzle);
console.log(matchPieces.length);
console.log(slidePuzzle.pieceLength);
if (matchPieces.length === slidePuzzle.pieceLength) {
console.log("clear");
dispatch({
type: "COMPLETE_SLIDE_PUZZLE"
});
} else {
console.log("mada");
}
}, [slidePuzzle.pieces]);
return [
{
pieces: slidePuzzle.pieces,
slidePuzzleNode,
handleMouseDown
},
initializeSlidePuzzle,
//initializePieces,
changeSlidePuzzleBg,
changePieceLength,
startShufflePieces,
stopShufflePieces,
lockSlidePuzzle,
unlockSlidePuzzle,
slidePuzzle.isComplete,
completeSlidePuzzle,
incompleteSlidePuzzle,
resetSlidePuzzle,
slidePuzzle.trouble,
resetTrouble
];
};
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
:root {
--base-color: burlywood;
--accent-color: darkorange;
--text-color: rgb(40, 40, 40);
}
* {
font-family: 'Press Start 2P', cursive;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
background-color: var(--base-color);
color: var(--text-color);
margin: 0;
}
`;
const Spacer = ({ size, horizontal }) => {
return (
<div
style={
horizontal
? { width: size, height: "auto", display: "inline-block", flexShrink: 0 }
: { width: "auto", height: size, flexShrink: 0 }
}
/>
);
};
const StyledSlidePuzzle = styled.div`
background-color: ${({ boardColor }) => boardColor};
border: 1vmin solid ${({ boardColor }) => boardColor};
border-radius: 1vmin;
height: ${({ sideLength }) => sideLength};
position: relative;
width: ${({ sideLength }) => sideLength};
z-index: 0;
`;
const Piece = styled.div`
//background-color: ${({ boardColor }) => boardColor};//#997f5d;//#333;
box-sizing: border-box;
//border: .3vmin solid ${({ boardColor }) => boardColor};//#997f5d;//#333;
height: ${({ piece }) => piece.height}px;
padding: 0.3vmin;
position: absolute;
left: ${({ piece }) => piece.x}px;
top: ${({ piece }) => piece.y}px;
transition: ${({ piece }) => (piece.isShuffle ? `all linear 100ms` : "")};
transform: translate3d(
${({ piece }) => piece.offsetX}px,
${({ piece }) => piece.offsetY}px,
0
);
user-select: none;
width: ${({ piece }) => piece.width}px;
z-index: ${({ piece }) => (piece.blank ? 1 : 3)};
`;
const Img = styled.img`
display: block
height: auto;
pointer-events: none;
width: 100%;
`;
const Num = styled.span`
color: white;
font-size: 1vmin;
position: absolute;
top: 1.2vmin; //.6vmin;
left: 1.2vmin;
`;
const SlidePuzzleWrapper = styled.div`
//height: 60vmin;
//width: 60vmin;
`;
const SlidePuzzle = ({ className, slidePuzzle, boardColor, sideLength }) => (
<StyledSlidePuzzle
className={className}
ref={slidePuzzle.slidePuzzleNode}
boardColor={boardColor}
sideLength={sideLength}
>
{slidePuzzle.pieces.map((piece, i) => (
<Piece
className={`piece piece-${piece.baseId}`}
piece={piece}
onMouseDown={slidePuzzle.handleMouseDown}
key={piece.baseId}
>
{!piece.blank && (
<React.Fragment>
<Num className="piece-num">{piece.baseId}</Num>
<Img className="piece-image" src={piece.image}></Img>
</React.Fragment>
)}
</Piece>
))}
</StyledSlidePuzzle>
);
const JustifyContentSpaceBetween = styled.div`
display: flex;
justify-content: space-between;
`;
const JustifyContentCenter = styled.div`
display: flex;
justify-content: center;
`;
const StyledGameStatus = styled.div`
color: ${({ color }) => (color ? color : "#333333")};
font-size: ${({ size }) => (size ? size : "1em")};
`;
const GameStatus = ({ className, title, text, color, size }) => (
<StyledGameStatus
className={className}
color={color}
size={size}
>{`${title}: ${text}`}</StyledGameStatus>
);
const Button = styled.button`
background-color: ${({ colors }) => (colors && colors.bg) || "lightgray"};
border: none;
border-radius: 0.6vmin;
cursor: ${({ isDisabled }) => (isDisabled ? "cursor" : "pointer")};
color: ${({ colors }) => (colors && colors.text) || "#333333"};
font-size: ${({ sizes }) => (sizes && sizes.font) || "1em"};
height: ${({ sizes }) => (sizes && sizes.height) || "auto"};
outline: none;
opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
padding: ${({ sizes }) => (sizes && sizes.padding) || "1em 2em"};
pointer-events: ${({ isDisabled }) => (isDisabled ? "none" : "auto")};
user-select: none;
width: ${({ sizes }) => (sizes && sizes.width) || "auto"};
`;
const StyledModalWindow = styled.div`
z-index: 1000;
& .content-wrapper {
}
`;
const Overlay = styled.div`
background-color: rgba(0, 0, 0, 0.6);
position: fixed;
top: -100vh;
left: -100vw;
bottom: -100vh;
right: -100vw;
z-index: 1;
`;
const ContentWrapper = styled.div`
background-color: white;
box-sizing: border-box;
border-radius: 2vmin;
color: #333;
display: flex;
align-items: center;
justify-content: center;
min-height: 60vmin;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 80vmin;
z-index: 2;
> button {
position: absolute;
top: 4vmin;
right: 4vmin;
}
`;
const Content = styled.div`
max-height: 80vmin;
padding: 4vmin;
`;
const Title = styled.h1`
font-size: 4vmin;
margin: 0 auto 4vmin;
`;
const Text = styled.p`
color: #777;
font-size: 2vmin;
margin: 0 0 4vmin;
`;
const Buttons = styled.div`
display: flex;
justify-content: center;
> button {
font-size: 2vmin;
margin: 0 0.6vmin;
}
`;
const ModalWindow = ({
className,
title,
text,
isShow,
isShowCloseButton,
buttons
}) => {
const [isClose, setIsClose] = useState(false);
const closeButtonSizes = {
padding: "0"
};
return (
<>
{isShow ? (
<StyledModalWindow className={className}>
<Overlay />
<ContentWrapper>
{isShowCloseButton && (
<Button
colors={{
bg: "transparent"
}}
sizes={closeButtonSizes}
onClick={() => setIsClose(true)}
>
✖
</Button>
)}
<Content>
<Title>{title}</Title>
<Text>{text}</Text>
<Buttons>
{buttons.map((b, i) => (
<Button onClick={b.callback} colors={b.colors} sizes={b.sizes}>
{b.label}
</Button>
))}
</Buttons>
</Content>
</ContentWrapper>
</StyledModalWindow>
) : (
<></>
)}
</>
);
};
const StyledApp = styled.div`
background-color: burlywood;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100%;
`;
const App = () => {
const [isStart, setIsStart] = useState(false);
const [level, setLevel] = useState(LEVEL.easy);
const [selectBg, setSelectBg] = useState(BG);
const [
slidePuzzle,
initializeSlidePuzzle,
//initializePieces,
changeSlidePuzzleBg,
changePieceLength,
startShufflePieces,
stopShufflePieces,
lockSlidePuzzle,
unlockSlidePuzzle,
isComplete,
completeSlidePuzzle,
incompleteSlidePuzzle,
resetSlidePuzzle,
trouble,
resetTrouble
] = useSlidePuzzle(selectBg, level);
useEffect(() => {
initializeSlidePuzzle();
}, []);
const initializeGame = useCallback(() => {
setIsStart(false);
}, [])
const startGame = useCallback(() => {
setIsStart(true);
//lockSlidePuzzle();
startShufflePieces();
}, []);
useEffect(() => {
if (!isComplete) return;
lockSlidePuzzle(); //resetSlidePuzzle()
// alert('Game clear!')
}, [isComplete]);
const resetGame = useCallback(() => {
setIsStart(false);
resetSlidePuzzle();
//stopShufflePieces()
initializeSlidePuzzle();
}, []);
const buttonSizes = {
font: "1vmin",
padding: "2vmin 4vmin"
};
const startBtn = {
label: "start",
colors: {
bg: `var(--accent-color)`,
text: "white"
},
callback: startGame
};
const endBtn = {
label: "end",
colors: {
bg: "#333",
text: "white"
}
//callback: initializeGame
};
const replayBtn = {
label: "replay",
colors: {
bg: "green",
text: "white"
}
//callback: () => {
// initializeGame();
// startGame();
// }
};
return (
<>
<GlobalStyle />
<StyledApp>
<ModalWindow
className="Game clear!"
title="Time's up"
text={`your trouble is ${trouble}.`}
isShow={isComplete}
isShowCloseButton={false}
buttons={[endBtn, replayBtn]}
></ModalWindow>
<SlidePuzzleWrapper>
<JustifyContentSpaceBetween>
<GameStatus
className="troble"
title="Trouble"
text={trouble}
size="2vmin"
/>
<GameStatus
className="level"
title="Level"
text={
level === LEVEL.easy
? "easy"
: level === LEVEL.normal
? "normal"
: level === LEVEL.hard
? "hard"
: "easy"
}
size="2vmin"
/>
</JustifyContentSpaceBetween>
<Spacer size="1.4vmin" />
<SlidePuzzle
className="slide-puzzle"
slidePuzzle={slidePuzzle}
boardColor={BOARD_COLOR}
sideLength="60vmin"
/>
<Spacer size="3vmin" />
<JustifyContentCenter>
<Button
colors={{
bg: "var(--accent-color)",
text: "#fff"
}}
sizes={buttonSizes}
onClick={startGame}
isDisabled={isStart ? true : false}
>
Start
</Button>
<Spacer size="1vmin" horizontal={true} />
<Button
sizes={buttonSizes}
onClick={resetGame}
isDisabled={isStart ? false : true}
>
Reset
</Button>
</JustifyContentCenter>
</SlidePuzzleWrapper>
</StyledApp>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
This Pen doesn't use any external CSS resources.