Matter.js
Matter.js는 웹 기반 2D 물리 엔진으로, 웹 페이지나 웹 애플리케이션에서 간단한 물리 시뮬레이션을 구현하는 데 사용된다.이 엔진은 HTML5 Canvas를 기반으로 하며, 자바스크립트로 작성되어 있다. Matter.js를 사용하면 물리적인 특성을 가진 객체들을 생성하고, 그들 간의 상호작용을 구현하여 현실적인 움직임 및 충돌 효과를 만들 수 있다.
물리엔진? 그게 뭔데 ㅠㅠ
정말 처음보는 거라 먼소린지 모르겠고,.. 공식 문서도 참 불친절하구,,,,,,,,,,,,
기본 효과같은 건 구글링이나 공식문서를 보면 충분히 활용 가능하지만
저 ... path지정방법과 스크롤 이펙트가 어렵다.
그래서 오늘은 예제 코드를 뜯어보면서 사용법을 익혀볼 생각이다.(코드펜 예제를 바탕으로 주석 설명을 달았다.)
Matter.js 공식 문서
코드펜 참조
https://codepen.io/Marco-Ahlers/pen/PoxrEzm
완성된 사이트는 요기!
https://jjul-gsap-test-01.netlify.app/test03
html 구조
<div id="container"></div>
안믿기겠지만 이게 끝.ㅋㅋㅋㅋ
body 안에 container라는 id를 가진 div요소를 넣어준다.
css
body {
margin: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #eee7e0;
width: 100vw;
height: 100vh;
}
#container {
width: 100vw;
height: 100vh;
}
svg {
display: none;
}
기본적인 body와 container 스타일링이다.
cdn 주소
<!-- path cdn -->
<script src="https://cdn.jsdelivr.net/npm/pathseg@1.2.1/pathseg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.3.0/build/decomp.min.js"></script>
<!-- matter.js cdn -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.js"></script>
matter-js 주소와 나머진 path 관련된 cdn같다.
참고로 matter.js의 최신 버전은 0.19.0이다. (예제에선 더 낮은 버전을 사용.)
스크립트: 초기설정
// 📕--- 초기 세팅 ---
const Engine = Matter.Engine,
World = Matter.World,
Bodies = Matter.Bodies;
Body = Matter.Body;
Composites = Matter.Composites;
Common = Matter.Common;
Vertices = Matter.Vertices;
Svg = Matter.Svg;
Composite = Matter.Composite;
// --- 초기 세팅 ---
Matter.js를 쓰려면 먼저 초기 설정을 해주어야한다. 공식 문서에서 내가 원하는 효과를 찾아 github 리드미 파일에 코드가 다 나와있다.
요런식으로
원하는 효과가 있을 땐 공식 문서를 잘 살펴보고 적용해보자.
내가 봤을 때 이 예제는 concave라는 효과를 사용한 거 같다
https://github.com/liabru/matter-js/blob/master/examples/concave.js
물리엔진 생성 후 렌더링
// 📕Engine.create() 함수를 사용하여 Matter.js 엔진을 생성한다.
const engine = Engine.create();
// 📕렌더링할 컨테이너 역할 부분의 id를 가져온다.(getElementById)
const container = document.getElementById("container");
// 📕Matter.Render.create() 함수를 사용하여 시각적인 렌더링을 담당하는 객체를 생성한다.
const renderer = Matter.Render.create({
element: container, // 렌더러가 그릴 HTML요소를 지정해준다.
engine: engine, // 렌더러에 사용될 Matter.js 엔진을 지정해준다.
options: {
// 커스텀 옵션들을 설정해준다.
width: container.offsetWidth,
height: container.offsetHeight,
wireframes: false, //wireframe 여부
background: "#eee7e0",
},
});
벽 설정
// 📕경계 벽 생성하는 부분 (순서대로 왼쪽, 오른쪽, 아래이다. isStatic은 바디를 정적으로 만들어 물리 시뮬레이션에 영향을 주지 않도록 한다.)
// 🧐 여기선 왜 위쪽 벽을 설정하지 않았을까? -> 떨어지는 효과를 나타내야하기 때문이다.
const walls = [
Bodies.rectangle(0, 0, 1, container.offsetHeight * 2, {
isStatic: true,
}),
Bodies.rectangle(
container.offsetWidth,
0,
1,
container.offsetHeight * 2,
{ isStatic: true }
),
Bodies.rectangle(
0,
container.offsetHeight,
container.offsetWidth * 2,
1,
{ isStatic: true }
),
];
물리 설정
// 📕물리적 특성을 설정하는 객체인 physics를 정의한다.
const physics = {
restitution: 0.7, // 복원력 또는 탄성력 (0~1 사이에 지정해줌. 1에 가까워질 수록 충돌 후 빠르게 원래 형태로 돌아간다.)
friction: 0.4, // 마찰 계수 (0~1 사이에 지정해줌. 1에 가까워질 수록 강한 마찰이 발생한다.)
};
svg 모양 설정
// 도넛모양 생성하는 부분인 듯!
const createCircle = (width, color, x, y) => {
const outerCircle = Bodies.circle(x, y, width, {
...physics,
render: {
fillStyle: color,
},
});
const innerCircle = Bodies.circle(x, y, width / 2.5, {
...physics,
render: {
fillStyle: "#eee7e0",
},
});
return Body.create({
parts: [outerCircle, innerCircle],
});
};
// 소세지모양 생성하는 부분인듯!
const createSvgElement = (path, color, scale, x, y, angle) => {
pathElement = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
pathElement.setAttribute("d", path);
const vertices = Svg.pathToVertices(pathElement, 3);
vertices.forEach((vertical) => {
const i = vertices.findIndex(
(v) => v.x === vertical.x && v.y === vertical.y
);
if (i !== -1) {
vertices.splice(i, 1);
}
});
const body = Bodies.fromVertices(
x,
y,
vertices,
{
...physics,
render: {
fillStyle: color,
strokeStyle: color,
lineWidth: "1px",
},
},
true
);
Body.scale(body, 1, 1);
Body.setAngle(body, angle);
return body;
};
// 모양 생성하는 함수를 바탕으로 요소들을 생성해주는 부분인듯
const elements = [
createSvgElement(curved, "#583d91", 1, 250, -400, 30),
createSvgElement(reactangle, "#eb5c48", 1, 450, -400, 50),
createCircle(115, "#9a3d90", 500, -400),
createSvgElement(curved, "#583d91", 1, 700, -400, 50),
createCircle(135, "#eb5c48", 1150, -200),
createSvgElement(curved, "#583d91", 1, 1460, -400, 200),
createSvgElement(curved, "#583d91", 1, 1300, -700, 300),
createSvgElement(reactangle, "#fab71a", 1, 1600, -700, 100),
createCircle(115, "#fab71a", 100, -900),
createSvgElement(curved, "#eb5c48", 1, 250, -900, 250),
createCircle(135, "#fab71a", 600, -900),
createSvgElement(curved, "#eb5c48", 1, 1500, -900, 100),
];
마우스 감지 이벤트
// 📕MouseConstraint은 마우스 입력을 기반으로 물리 시뮬레이션에 영향을 주는 제약(Constraint)을 제공한다.
const mouseConstraint = Matter.MouseConstraint.create(engine, {
element: container,
constraint: {
stiffness: 0.2, // 제어의 강도 또는 강성(stiffness)을 나타내는 값 (낮을수록 유연)
render: {
visible: false, // 제어 요소(마우스)의 가시성 여부
},
},
});
물리엔진 world에 추가 후 렌더링하기
// 📕World.add()로 물리 엔진 세계에 다양한 물체들을 추가(배열 안에 설정해놓은 변수들을 펼침연산자로 추가한다.)
World.add(engine.world, [...walls, mouseConstraint, ...elements]);
// 📕Engine.run()로 Matter.js 엔진을 실행
Engine.run(engine);
// 📕Render.run()호출하여 렌더러 실행
Matter.Render.run(renderer);
거의 웬만한 설명은 주석에 달아놓았으므로 궁금한 부분은 gpt 찬스 쓰시면 될 듯 싶슴당. (귀찮아서 그런거 아님)
전체 코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Physics-based Falling Animation</title>
<style>
body {
margin: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #eee7e0;
width: 100vw;
height: 100vh;
}
#container {
width: 100vw;
height: 100vh;
}
svg {
display: none;
}
</style>
</head>
<body>
<div id="container"></div>
<!-- path cdn -->
<script src="https://cdn.jsdelivr.net/npm/pathseg@1.2.1/pathseg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.3.0/build/decomp.min.js"></script>
<!-- matter.js cdn -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.js"></script>
<script>
// 📕보통 matter.js에서 패스를 지정할 때 쓴다.
const reactangle =
"m.26,274.72c-1.79-20.16,5.77-40.86,22.2-55.17L256.07,16.09c27.25-23.73,68.58-20.88,92.31,6.37,23.73,27.25,20.88,68.58-6.37,92.31l-233.61,203.46c-27.25,23.73-68.58,20.88-92.31-6.37-9.42-10.82-14.66-23.86-15.83-37.14Z";
const curved =
"m341.46,31.89c14.51,28.65,3.05,63.65-25.6,78.16-3.42,1.73-34.78,17.16-83.49,26.15-28.57,5.27-57.68,7.34-86.51,6.14-36.13-1.5-71.89-8.14-106.28-19.74C9.15,112.33-7.2,79.34,3.07,48.9,13.33,18.47,46.33,2.12,76.77,12.39c101.89,34.37,185.72-5.68,186.54-6.1,28.65-14.51,63.65-3.05,78.16,25.6Z";
// 📕--- 초기 세팅 ---
const Engine = Matter.Engine,
World = Matter.World,
Bodies = Matter.Bodies;
Body = Matter.Body;
Composites = Matter.Composites;
Common = Matter.Common;
Vertices = Matter.Vertices;
Svg = Matter.Svg;
Composite = Matter.Composite;
// --- 초기 세팅 ---
// 📕Engine.create() 함수를 사용하여 Matter.js 엔진을 생성한다.
const engine = Engine.create();
// 📕렌더링할 컨테이너 역할 부분의 id를 가져온다.(getElementById)
const container = document.getElementById("container");
// 📕Matter.Render.create() 함수를 사용하여 시각적인 렌더링을 담당하는 객체를 생성한다.
const renderer = Matter.Render.create({
element: container, // 렌더러가 그릴 HTML요소를 지정해준다.
engine: engine, // 렌더러에 사용될 Matter.js 엔진을 지정해준다.
options: {
// 커스텀 옵션들을 설정해준다.
width: container.offsetWidth,
height: container.offsetHeight,
wireframes: false, //wireframe 여부
background: "#eee7e0",
},
});
// 📕경계 벽 생성하는 부분 (순서대로 왼쪽, 오른쪽, 아래이다. isStatic은 바디를 정적으로 만들어 물리 시뮬레이션에 영향을 주지 않도록 한다.)
// 🧐 여기선 왜 위쪽 벽을 설정하지 않았을까? -> 떨어지는 효과를 나타내야하기 때문이다.
const walls = [
Bodies.rectangle(0, 0, 1, container.offsetHeight * 2, {
isStatic: true,
}),
Bodies.rectangle(
container.offsetWidth,
0,
1,
container.offsetHeight * 2,
{ isStatic: true }
),
Bodies.rectangle(
0,
container.offsetHeight,
container.offsetWidth * 2,
1,
{ isStatic: true }
),
];
// 기존 코드를 살펴보면 벗어나는 부분을 계산해서 적용시켰다.
// const walls = [
// Bodies.rectangle(0 - 50, 0 - 50, 1, container.offsetHeight * 2 - 50, {
// isStatic: true,
// }),
// Bodies.rectangle(
// container.offsetWidth + 50,
// 0 - 50,
// 1,
// container.offsetHeight * 2 + 50,
// { isStatic: true }
// ),
// Bodies.rectangle(
// 0 - 50,
// container.offsetHeight + 50,
// container.offsetWidth * 2 + 50,
// 1,
// { isStatic: true }
// ),
// ];
// 📕물리적 특성을 설정하는 객체인 physics를 정의한다.
const physics = {
restitution: 0.7, // 복원력 또는 탄성력 (0~1 사이에 지정해줌. 1에 가까워질 수록 충돌 후 빠르게 원래 형태로 돌아간다.)
friction: 0.4, // 마찰 계수 (0~1 사이에 지정해줌. 1에 가까워질 수록 강한 마찰이 발생한다.)
};
// 도넛모양 생성하는 부분인 듯!
const createCircle = (width, color, x, y) => {
const outerCircle = Bodies.circle(x, y, width, {
...physics,
render: {
fillStyle: color,
},
});
const innerCircle = Bodies.circle(x, y, width / 2.5, {
...physics,
render: {
fillStyle: "#eee7e0",
},
});
return Body.create({
parts: [outerCircle, innerCircle],
});
};
// 소세지모양 생성하는 부분인듯!
const createSvgElement = (path, color, scale, x, y, angle) => {
pathElement = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
pathElement.setAttribute("d", path);
const vertices = Svg.pathToVertices(pathElement, 3);
vertices.forEach((vertical) => {
const i = vertices.findIndex(
(v) => v.x === vertical.x && v.y === vertical.y
);
if (i !== -1) {
vertices.splice(i, 1);
}
});
const body = Bodies.fromVertices(
x,
y,
vertices,
{
...physics,
render: {
fillStyle: color,
strokeStyle: color,
lineWidth: "1px",
},
},
true
);
Body.scale(body, 1, 1);
Body.setAngle(body, angle);
return body;
};
// 모양 생성하는 함수를 바탕으로 요소들을 생성해주는 부분인듯
const elements = [
createSvgElement(curved, "#583d91", 1, 250, -400, 30),
createSvgElement(reactangle, "#eb5c48", 1, 450, -400, 50),
createCircle(115, "#9a3d90", 500, -400),
createSvgElement(curved, "#583d91", 1, 700, -400, 50),
createCircle(135, "#eb5c48", 1150, -200),
createSvgElement(curved, "#583d91", 1, 1460, -400, 200),
createSvgElement(curved, "#583d91", 1, 1300, -700, 300),
createSvgElement(reactangle, "#fab71a", 1, 1600, -700, 100),
createCircle(115, "#fab71a", 100, -900),
createSvgElement(curved, "#eb5c48", 1, 250, -900, 250),
createCircle(135, "#fab71a", 600, -900),
createSvgElement(curved, "#eb5c48", 1, 1500, -900, 100),
];
// 📕MouseConstraint은 마우스 입력을 기반으로 물리 시뮬레이션에 영향을 주는 제약(Constraint)을 제공한다.
const mouseConstraint = Matter.MouseConstraint.create(engine, {
element: container,
constraint: {
stiffness: 0.2, // 제어의 강도 또는 강성(stiffness)을 나타내는 값 (낮을수록 유연)
render: {
visible: false, // 제어 요소(마우스)의 가시성 여부
},
},
});
// 📕World.add()로 물리 엔진 세계에 다양한 물체들을 추가(배열 안에 설정해놓은 변수들을 펼침연산자로 추가한다.)
World.add(engine.world, [...walls, mouseConstraint, ...elements]);
// 📕Engine.run()로 Matter.js 엔진을 실행
Engine.run(engine);
// 📕Render.run()호출하여 렌더러 실행
Matter.Render.run(renderer);
</script>
</body>
</html>
완성된 페이지
https://github.com/YeoDaSeul4355/gsap_study/blob/main/test03.html