Matter.js

Matter.js는 웹 기반 2D 물리 엔진으로, 웹 페이지나 웹 애플리케이션에서 간단한 물리 시뮬레이션을 구현하는 데 사용된다.이 엔진은 HTML5 Canvas를 기반으로 하며, 자바스크립트로 작성되어 있다. Matter.js를 사용하면 물리적인 특성을 가진 객체들을 생성하고, 그들 간의 상호작용을 구현하여 현실적인 움직임 및 충돌 효과를 만들 수 있다.

 

물리엔진? 그게 뭔데 ㅠㅠ

 

정말 처음보는 거라 먼소린지 모르겠고,.. 공식 문서도 참 불친절하구,,,,,,,,,,,,

기본 효과같은 건 구글링이나 공식문서를 보면 충분히 활용 가능하지만

저 ... path지정방법과 스크롤 이펙트가 어렵다.

 

그래서 오늘은 예제 코드를 뜯어보면서 사용법을 익혀볼 생각이다.(코드펜 예제를 바탕으로 주석 설명을 달았다.)

 

Matter.js 공식 문서

https://brm.io/matter-js/

 

Matter.js

Matter.js is 2D rigid body physics engine for the web, using JavaScript and HTML5

brm.io

 

코드펜 참조

https://codepen.io/Marco-Ahlers/pen/PoxrEzm

 

Animation

...

codepen.io

완성된 사이트는 요기!

https://jjul-gsap-test-01.netlify.app/test03

 

Physics-based Falling Animation

 

jjul-gsap-test-01.netlify.app

 

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

728x90
반응형
다쭐◠‿◠