해당 내용은 판교고 사이드 프로젝트 진행시에 사용할 캔버스에 대한 학습 내용을 정리하였습니다.
타일을 마우스 드래그로 위치 이동
저번 게시물에서는 타일을 배치하는것까지 완료하였습니다. 이번 게시물에서는 배치된 타일을 마우스 드래그로 이동하는 것을 알아보았습니다. 이번에 진행 순서는 아래와 같습니다.
- document에 마우스 이벤트 등록
- 이벤트 좌표를 이용해서 클릭한 타일의 위치 이동
먼저 이벤트를 등록해보겠습니다. 캔버스에 마우스를 클릭했을 때 대상이 존재하는지 여부를 확인하여 존재하는 경우 마우스 드래그를 했을 때 클릭 지점과 마우스 드래그 지점 사이만큼 대상의 위치를 옮겨주면 됩니다.
이번에 추가한 전체 코드는 아래와 같습니다. 하나씩 알아보도록 하겠습니다.
const update = false;
const moveRectangle = (target, downEvent) => {
const [currentX, currentY] = [target.x, target. y];
return (moveEvent) => {
const [movementX, movementY] = [downEvent.x - moveEvent.x, downEvent.y - moveEvent.y];
target.x = currentX - movementX;
target.y = currentY - movementY;
update = true;
}
};
const contains = (rectangle, { x, y }) => {
if (rectangle.x > x || rectangle.y > y) {
return false;
}
if ((rectangle.x + rectangle.width) < x || (rectangle.y + rectangle.height) < y) {
return false;
}
return true;
}
canvas.addEventListener('mousedown', (downEvent) => {
const { pageX, pageY } = downEvent;
const target = rectangleList.find((rectangle) => (
contains(rectangle, { x: pageX, y: pageY })
));
if (!target) return;
const moveHandler = moveRectangle(target, downEvent);
canvas.addEventListener('mousemove', moveHandler);
canvas.addEventListener('mouseup', () => {
canvas.removeEventListener('mousemove', moveHandler);
}, { once: true });
});
먼저 이벤트 등록 부분입니다. 캔버스에 mousedown 이벤트핸들러를 등록 후 핸들러 내부에서 mousemove와 mouseup 이벤트를 등록하고 있습니다. 이는 외부에 따로 등록하는 경우 마우스를 이동할 때마다 mousemove 이벤트가 발생하는 것을 막기 위해서 이러한 방법을 사용하였습니다.
마우스 클릭 후 아래 코드에는 보이지 않지만 외부에 선언된 moveRectangle 함수를 호출한 반환값을 mousemove 이벤트의 핸들러로 등록합니다. 그리고 마우스를 뗏을 때 mosemove 이벤트가 발생하지 않도록 제거합니다.
어떤 방법이 더 좋은 지 알고 계신분이 계시다면 댓글 남겨주시면 감사하겠습니다!
canvas.addEventListener('mousedown', (downEvent) => {
const { pageX, pageY } = downEvent;
// ...
const moveHandler = moveRectangle(target, downEvent);
canvas.addEventListener('mousemove', moveHandler);
canvas.addEventListener('mouseup', () => {
canvas.removeEventListener('mousemove', moveHandler);
}, { once: true });
});
그리고 마우스 클릭 했을 때 클릭한 지점에 타일이 존재하는지 여부를 확인합니다. 네모 타일의 사이즈, 좌표와 클릭한 지점의 좌표를 이용해서 선택여부를 확인합니다.
const contains = (rectangle, { x, y }) => {
if (rectangle.x > x || rectangle.y > y) {
return false;
}
if ((rectangle.x + rectangle.width) < x || (rectangle.y + rectangle.height) < y) {
return false;
}
return true;
}
canvas.addEventListener('mousedown', (downEvent) => {
const { pageX, pageY } = downEvent;
const target = matches((rectangle) => (
rectangle.contains({ x: pageX, y: pageY })
));
if (!target) return;
// ...
});
마지막으로 선택 된 대상의 위치를 마우스가 움직이는 위치에 따라 변경해주면 됩니다. 마우스 클릭한 시점의 대상의 위치를 기억하고 있다가 마우스가 이동한 위치만큼을 클릭한 시점의 위치에서 빼주면 됩니다.
여기까지 작성하게 되면 네모 타일이 이동하는게 아닌 이동 거리마다 새로운 네모 타일이 그려지게 됩니다. 이 문제를 해결하기 위해서는 기존에 캔버스의 모두 제거한 후 변경 된 내용으로 새롭게 작성해주어야 합니다.
이를 위해 update 라는 변수를 추가하겠습니다.
let update = false;
const moveRectangle = (target, downEvent) => {
const [currentX, currentY] = [target.x, target. y];
return (moveEvent) => {
const [movementX, movementY] = [downEvent.x - moveEvent.x, downEvent.y - moveEvent.y];
target.x = currentX - movementX;
target.y = currentY - movementY;
update = true;
}
};
기존에 작성했었던 draw 함수를 수정해보겠습니다. requestAnimationFrame 함수를 사용합니다. requestAnimationFrame은 브라우저의 리플로우 및 리페인트 주기를 고려하여 콜백함수를 호출해주기 때문에 최적화된 애니메이션을 생성할 수 있습니다. setInterval을 사용할때보다 자연스러운 애니메이션을 구현할 수 있습니다.
추가적으로 변경사항이 없는 경우 캔버스를 다시 그리지 않기 위해서 update 변수를 사용하여 값이 true 인 경우에만 캔버스를 다시 그리도록 코드를 추가하였습니다.
const draw = () => {
requestAnimationFrame(draw);
if (!update) return;
ctx.clearRect(0,0, window.innerWidth, window.innerHeight);
ctx.strokeStyle = 'black';
rectangleList.forEach(({ x, y, width, height }) => {
ctx.strokeRect(x, y, width, height);
});
ctx.strokeStyle = 'red';
ctx.strokeRect(
targetRectangle.x,
targetRectangle.y,
targetRectangle.width,
targetRectangle.height,
);
update = false;
};
지금까지 최종 코드는 아래와 같습니다. 다음은 타일 이동 도중 목표 타일이 존재하는 경우 자석기능을 추가해보도록 하겠습니다.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const rectangleList = [];
let targetRectangle = null;
let update = true;
const init = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
const createRectangle = () => {
for (let i = 0; i < 5; i += 1) {
const x = Math.floor(Math.random() * 20) * 20;
const y = Math.floor(Math.random() * 20) * 20;
const width = Math.floor(Math.random() * 20) * 20 + 50;
const height = Math.floor(Math.random() * 20) * 20 + 50;
rectangleList.push({ x, y, width, height });
}
};
const createTargetRectangle = () => {
const x = 800;
const y = 300;
const width = Math.floor(Math.random() * 20) * 20 + 50;
const height = Math.floor(Math.random() * 20) * 20 + 50;
targetRectangle = { x, y, width, height };
// 목표 도형과 동일한 사이즈의 도형을 만들기 위한 코드
rectangleList.push({
x: Math.floor(Math.random() * 20) * 20,
y: Math.floor(Math.random() * 20) * 20,
width,
height
});
};
const draw = () => {
requestAnimationFrame(draw);
if (!update) return;
ctx.clearRect(0,0, window.innerWidth, window.innerHeight);
ctx.strokeStyle = 'black';
rectangleList.forEach(({ x, y, width, height }) => {
ctx.strokeRect(x, y, width, height);
});
ctx.strokeStyle = 'red';
ctx.strokeRect(
targetRectangle.x,
targetRectangle.y,
targetRectangle.width,
targetRectangle.height,
);
update = false;
};
const moveRectangle = (target, downEvent) => {
const [currentX, currentY] = [target.x, target. y];
return (moveEvent) => {
const [movementX, movementY] = [downEvent.x - moveEvent.x, downEvent.y - moveEvent.y];
target.x = currentX - movementX;
target.y = currentY - movementY;
update = true;
}
};
const contains = (rectangle, { x, y }) => {
if (rectangle.x > x || rectangle.y > y) {
return false;
}
if ((rectangle.x + rectangle.width) < x || (rectangle.y + rectangle.height) < y) {
return false;
}
return true;
}
canvas.addEventListener('mousedown', (downEvent) => {
const { pageX, pageY } = downEvent;
const target = rectangleList.find((rectangle) => (
contains(rectangle, { x: pageX, y: pageY })
));
if (!target) return;
const moveHandler = moveRectangle(target, downEvent);
canvas.addEventListener('mousemove', moveHandler);
canvas.addEventListener('mouseup', () => {
canvas.removeEventListener('mousemove', moveHandler);
}, { once: true });
});
document.addEventListener('DOMContentLoaded', () => {
init();
createRectangle();
createTargetRectangle();
draw();
})
'사이드 프로젝트 > 판교고' 카테고리의 다른 글
[판교고 - 기술체크] 1-3. 동일한 타일 여부 확인 (0) | 2023.05.15 |
---|---|
[판교고 - 기술체크] 1-1. 캔버스에 타일 배치 (0) | 2023.04.21 |