Pixi.js Viewport 예제코드

|

Pixi.js Viewport 예제코드

PixiView.vue

<template>
  <div id="container">
    <canvas id="pixi-canvas"></canvas>
  </div>
</template>

<script>
import "./js/pixi-app";
import { PixiApp } from "./js/pixi-app";
export default {
  mounted() {
    this.createPixiApp();
  },
  methods: {
    createPixiApp() {
      const app = new PixiApp(800, 600);
      const canvas = document.getElementById("pixi-canvas");
      canvas.appendChild(app.view);
      console.log(`window size(${window.innerWidth}, ${window.innerHeight})`);
      console.log(`app size(${app.view.width}, ${app.view.height})`);
    },
  },
};
</script>

<style>
#container {
  display: block;
  width: 100vw;
  height: 100vh;
  background: whitesmoke;
}
#pixi-canvas {
  display: block;
  margin: 5%;
}
</style>

js/pixi-app.js

import * as PIXI from "pixi.js-legacy";
import { Viewport } from "pixi-viewport";

class PixiApp extends PIXI.Application {
  constructor(width, height) {
    super({
      width: width,
      height: height,
      backgroundColor: 0xffebee,
      antialias: true,
    });

    const screenWidth = 800;
    const screenHeight = 600;
    const worldWidth = screenWidth * 2;
    const worldHeight = screenHeight * 2;

    const viewport = new Viewport({
      screenWidth: screenWidth,
      screenHeight: screenHeight,
      worldWidth: worldWidth,
      worldHeight: worldHeight,

      interaction: this.renderer.plugins.interaction,
    });

    this.stage.addChild(viewport);

    const grid = this.#createGrid();
    viewport.addChild(grid);

    const boundary = this.#createBoundary(worldWidth, worldHeight);
    viewport.addChild(boundary);

    this.#registerViewportEventHandler(viewport);

    console.log(viewport);
    console.log(`Screen width: ${viewport.screenWidth}`);
    console.log(`World width: ${viewport.worldWidth}`); // World height, in pixels
    console.log(
      `Screen width in World pixels width(${viewport.screenWidthInWorldPixels})`
    ); // Get how many world pixels fit in screen's width
    console.log(`Screen World width(${viewport.screenWorldWidth})`); // World width in screen coordinates
    console.log(`World Screen width(${viewport.worldScreenWidth})`); // Screen width in world coordinates
    console.log(`Viewport Corner`, viewport.corner); // Screen width in world coordinates
    console.log(`Viewport Center`, viewport.center); // Screen width in world coordinates
  }

  #registerViewportEventHandler(viewport) {
    const screenWidth = 800;
    const screenHeight = 600;

    viewport
      .drag()
      .pinch()
      .wheel()
      .decelerate()
      .clamp({
        direction: "all",
        underflow: "center",
      })
      .clampZoom({
        minWidth: screenWidth,
        minHeight: screenHeight,
        maxWidth: screenWidth * 3,
        maxHeight: screenHeight * 3,
      })
      .clampZoom({
        minScale: 0.5,
        maxScale: 2,
      })
      .fit();

    viewport.on("pointerup", (e) => {
      const x = e.data.global.x;
      const y = e.data.global.y;
      console.log(`point UP (${x}, ${y})`); // Viewport 좌표
      console.log(`toScreen: `, viewport.toScreen(x, y));
      console.log(`toWorld: `, viewport.toWorld(x, y));
      console.log(`visibleBounds()`, viewport.getVisibleBounds());
    });
  }

  #createBoundary(width, height) {
    const container = new PIXI.Container();
    const boundary = new PIXI.Graphics();
    boundary.lineStyle(8, 0xff0000);
    boundary.drawRect(0, 0, width, height);
    container.addChild(boundary);

    return container;
  }

  #createGrid() {
    const container = new PIXI.Container();
    const origin = this.#createCrossPoint({
      x: 0,
      y: 0,
      color: 0xff0000,
      size: 50,
      fontsize: 14,
    });
    container.addChild(origin);

    for (let row = 0; row <= 20; row++) {
      for (let col = 0; col <= 20; col++) {
        if (row == 0 && col == 0) continue;

        const p = this.#createCrossPoint({
          x: col * 100,
          y: row * 100,
        });
        container.addChild(p);
      }
    }

    return container;
  }

  #createCrossPoint({
    x,
    y,
    color = 0x000000,
    size = 5,
    thickness = 1,
    fontsize = 8,
  }) {
    const container = new PIXI.Container();

    const grid = new PIXI.Graphics();
    grid.lineStyle(thickness, color);
    grid.moveTo(x - size, y);
    grid.lineTo(x + size, y);
    grid.moveTo(x, y - size);
    grid.lineTo(x, y + size);
    container.addChild(grid);

    const text = new PIXI.Text(`(${x},${y})`, {
      fontFamily: "Arial",
      fontSize: fontsize,
      fill: color,
    });
    text.position.set(x + 3, y + 3);
    container.addChild(text);

    return container;
  }
}

export { PixiApp };

실행화면

화면의 각 위치를 터치해보고, 화면을 panning으로 이동 후 각 위치를 터치, Zoom 등으로 화면을 확대/축소하면서 각 위치를 터치하면서 좌표계를 확인해볼 수 있습니다.

image

각 이벤트를 거쳐 전달되는 좌표값은 Viewport 상의 좌표이며, 이를 World 좌표로 변환하면 각 오브젝트가 그려진 Canvas 상의 좌표라고 볼 수 있습니다.

Pixi.js Viewport 상에서의 Event

|

Pixi.js Viewport 상에서의 Event

pixi-viewport라는 유틸리티를 이용해서 구현한 예제입니다.

package.json

아래와 같이 모듈을 추가해주면 됩니다.

...

"dependencies": {
  "core-js": "^3.8.3",
  "pixi-viewport": "^4.34.4",
  "pixi.js-legacy": "^6.3.0",
  "vue": "^3.2.13"
},

...

PixiView.vue

<template>
  <div id="container">
    <div id="pixi-canvas"></div>
  </div>
</template>

<script>
import "./js/pixi-app";
import { PixiApp } from "./js/pixi-app";
export default {
  mounted() {
    this.createPixiApp();
  },
  methods: {
    createPixiApp() {
      const app = new PixiApp(800, 800);
      const canvas = document.getElementById("pixi-canvas");
      canvas.appendChild(app.view);
      console.log(`window size(${window.innerWidth}, ${window.innerHeight})`);
      console.log(`app size(${app.view.width}, ${app.view.height})`);
    },
  },
};
</script>

<style>
#container {
  display: block;
  width: 100vw;
  height: 100vh;
  background: whitesmoke;
}
#pixi-canvas {
  display: block;
  margin: 20px;
}
</style>

pixp-app.js

import * as PIXI from "pixi.js-legacy";
import { Viewport } from "pixi-viewport";

class PixiApp extends PIXI.Application {
  constructor(width, height) {
    super({
      width: width,
      height: height,
      backgroundColor: 0xffebee,
      antialias: true,
    });

    const viewport = new Viewport({
      screenWidth: width,
      screenHeight: height,
      worldWidth: width * 2,
      worldHeight: height * 2,

      interaction: this.renderer.plugins.interaction, // the interaction module is important for wheel to work properly when renderer.view is placed or scaled
    });

    this.stage.addChild(viewport);

    const rect = this.#createRect(100, 100);
    viewport.addChild(rect);
    rect.interactive = true;
    rect.on("pointerdown", (e) => {
      console.log(`rect DOWN (${e.data.global.x}, ${e.data.global.y})`); // Viewport 좌표
    });

    const grid = this.#createGrid();
    viewport.addChild(grid);

    viewport.drag().pinch().wheel().decelerate();

    viewport.on("pointerdown", (e) => {
      console.log(`point DOWN (${e.data.global.x}, ${e.data.global.y})`); // Viewport 좌표
    });
  }

  #createRect(centerX, centerY, width = 100, height = 100) {
    const rect = new PIXI.Graphics();
    rect.beginFill(0xff8080);
    rect.drawRect(0, 0, width, height);
    rect.pivot.set(width / 2, height / 2);
    rect.position.set(centerX, centerY);
    rect.endFill();

    return rect;
  }

  #createGrid() {
    const grid = new PIXI.Graphics();
    grid.lineStyle(2, 0xf06292);
    grid.moveTo(-2000, 0);
    grid.lineTo(2000, 0);
    grid.moveTo(0, -2000);
    grid.lineTo(0, 2000);
    grid.lineStyle(1, 0x000000);
    for (let i = 1; i < 5; i++) {
      grid.moveTo(i * 400, -2000);
      grid.lineTo(i * 400, 2000);
      grid.moveTo(-2000, i * 400);
      grid.lineTo(2000, i * 400);
    }

    return grid;
  }
}

export { PixiApp };

실행화면

image

Pixi.js Event 등록하기

|

Pixi 캔버스 상의 오브젝트에 이벤트 등록하기

아래와 같은 코드로 작성하면 다음과 같은 이벤트들을 사용할 수 있습니다.

  • pointerdown
  • pointermove
  • pointerup
  • click
  • pointerover
  • pointerout

참고로, 이벤트들은 Pixi.js에서 등록된 이벤트가 아니라 HTML5에 정의되어 있는 이벤트들입니다.

<template>
  <div id="container">
    <canvas id="pixi-canvas"></canvas>
  </div>
</template>

<script>
import * as PIXI from "pixi.js";
import { onMounted } from "@vue/runtime-core";

export default {
  setup() {
    onMounted(() => {
      const app = createPixiApp();

      createInteractionExample(app);
    });

    const createPixiApp = () => {
      var canvas = document.getElementById("pixi-canvas");

      const app = new PIXI.Application({
        view: canvas,
      });

      return app;
    };

    const createInteractionExample = (app) => {
      const container = new PIXI.Container();
      const c1 = new PIXI.Graphics();
      c1.lineStyle(0);
      c1.beginFill(0x008080);
      c1.drawCircle(400, 100, 50);
      c1.endFill();
      container.addChild(c1);

      app.stage.addChild(container);

      c1.interactive = true;
      c1.on("pointerdown", (e) => {
        console.log("pointerdown");
        console.log(e);
      });
      // c1.on("pointermove", (e) => {
      //   console.log("pointermove");
      //   console.log(e);
      // });
      c1.on("pointerup", (e) => {
        console.log("pointerup");
        console.log(e);
      });
      c1.on("click", (e) => {
        console.log("click");
        console.log(e);
      });
      c1.on("pointerover", (e) => {
        console.log("pointerover");
        console.log(e);
      });
      c1.on("pointerout", (e) => {
        console.log("pointerout");
        console.log(e);
      });
    };
  },
};
</script>

<style>
#container {
  display: block;
  background: white;
  padding: 20px;
}

#pixi-canvas {
  width: 600px;
  height: 600px;
}
</style>

이벤트

개인적으로 Pixi이 Event들은 만족스럽지 않습니다. 제 프로젝트의 문제인지 일시적 버그인지 모르겠지만, 위의 예제에서 오브젝트에 설정한 pointermove 이벤트가 오브젝트가 아닌 캔버스 전체에 적용되는 버그도 있으며, pointdown, pointup, click 이벤트들이 일반적인 형태로 동작하지 않는 것 같았습니다. 보통 pointdown 후 좌표를 크게 이동한 후 pointup을 할 경우 click 이벤트는 호출되지 않는데, Pxii에서는 항상 호출되네요.

그리고 아래와 같이 로그에서도 모든 이벤트의 typemousemove로 전달되어 오는 등 아쉬운 점이 많네요.

image

git 컨닝 페이퍼

|

기본 명령어

명령어 | 설명 — | — git init | 현재 위치를 git 저장소로 초기화 git add [파일] | 해당 파일을 git에 등록하거나 수정된 파일을 추가 git commit | 변경된 내용을 커밋 git status | 현재 상태를 출력

응용

  • git add . : 모든 파일을 git에 추가함 git commit –amend | 같은 브랜치 상의 최종 커밋을 취소하고 새로운 내용을 추가한 커밋을 할 수 있음

브랜치 관련 명령어

명령어 | 설명 — | — git branch [이름] | 해당 이름의 브랜치를 생성 git checkout [브랜치이름] | 해당 이름의 브랜치로 변경 git merge [브랜치이름] | 현재 브랜치에 [브랜치이름]의 브랜치 내용을 정합

응용

  • git branch -D [이름] 해당 이름의 브랜치를 삭제
  • git checkout -B [브랜치이름] 해당 이름의 브랜치를 생성하면서 변경

원격 명령어

명령어 | 설명 — | — git clone | 원격 저장소의 모든 내용을 로컬에 저장 git remote | 로컬 저장소를 원격 저장소에 연결 git push | 로컬 저장소의 내용을 원격 저장소에 전달 git fetch | 로컬 저장소와 원격 저장소의 변경 사항이 다를 때 이를 대조하고 최신 데이터를 반영함 git pull | 원격 저장소의 최신 내용을 로컬 저장소에 업데이트

고급 명령어

명령어 | 설명 — | — git tag | 커밋을 참조하기 쉽도록 태그를 붙임 git revert | 이전에 작성한 커밋을 지우지만 지운 내역을 남김 git reset | 커밋을 버리고 이전의 특정 버전으로 되돌아감. git revert와의 차이는 지운 커밋 내역을 남기지 않는다는 점 git rebase | 브랜치 이력을 확인하면서 정합

응용

  • git checkout – [파일] 커밋하지 않은 변경 내역을 취소
  • git revase -i 커밋 내용을 합치면서 정합

Pixi.js 기본 도형 그린 후 회전하기

|

화면에 도형 그룹 그린 후 회전하는 코드

<template>
  <div id="container">
    <canvas id="pixi-canvas"></canvas>
  </div>
</template>

<script>
import * as PIXI from "pixi.js";
import { onMounted } from "@vue/runtime-core";

export default {
  setup() {
    onMounted(() => {
      const app = createPixiApp();

      const container = createContainerWithDrawingObjects();
      app.stage.addChild(container);

      container.pivot.x = container.width / 2;
      container.pivot.y = container.height / 2;
      container.x = container.width / 2;
      container.y = container.height / 2;

      let elapsed = 0.0;
      app.ticker.add((delta) => {
        elapsed += delta;

        if (elapsed < 1) return;
        elapsed = 0;

        container.rotation = container.rotation + 0.01;
      });
    });

    const createPixiApp = () => {
      var canvas = document.getElementById("pixi-canvas");

      const app = new PIXI.Application({
        width: 800,
        height: 800,
        antialias: true,
        backgroundAlpha: true,
        view: canvas,
      });

      return app;
    };

    const createContainerWithDrawingObjects = () => {
      const container = new PIXI.Container();

      const rect = new PIXI.Graphics();
      rect.beginFill(0xff0000);
      rect.drawRect(50, 50, 200, 200);
      rect.endFill();
      container.addChild(rect);

      const c1 = new PIXI.Graphics();
      c1.lineStyle(0);
      c1.beginFill(0x008080);
      c1.drawCircle(400, 100, 50);
      c1.endFill();
      container.addChild(c1);

      const c2 = new PIXI.Graphics();
      c2.lineStyle(10, 0xfeeb77, 1);
      c2.beginFill(0x650a5a, 1);
      c2.drawCircle(400, 250, 50);
      c2.endFill();
      container.addChild(c2);

      const polyline = new PIXI.Graphics();
      polyline.lineStyle(10, 0x00ffff);
      polyline.moveTo(50, 350);
      polyline.lineTo(200, 350);
      polyline.lineTo(280, 400);
      polyline.lineTo(100, 450);
      container.addChild(polyline);

      const polygon = new PIXI.Graphics();
      polygon.lineStyle(0);
      polygon.beginFill(0x3500fa, 1);
      polygon.moveTo(550, 70);
      polygon.lineTo(650, 160);
      polygon.lineTo(730, 120);
      polygon.lineTo(680, 270);
      polygon.lineTo(540, 220);
      polygon.closePath();
      polygon.endFill();
      container.addChild(polygon);

      return container;
    };
  },
};
</script>

<style>
#container {
  display: block;
  background: white;
  padding: 20px;
}

#pixi-canvas {
  width: 600px;
  height: 600px;
}
</style>

Container를 회전하는 코드

여기서 중요한 부분은 아래 코드 부분입니다. container.pivot는 회전할 때의 중심 좌표를 의미합니다. 기본값은 0 이며, 이 경우 회전할 때 Container의 왼쪽상단 부분의 꼭지점을 기준으로 회전합니다.

그리고 pivot를 Container의 중심으로 정하면, 화면에 좌표를 기준도 Container의 중심으로 변경됩니다. 따라서 기존과 같은 좌표에 도형을 그리기 위해서는 Container의 (x, y) 좌표를 변경할 필요가 있습니다.

container.pivot.x = container.width / 2;
container.pivot.y = container.height / 2;
container.x = container.width / 2;
container.y = container.height / 2;

let elapsed = 0.0;
app.ticker.add((delta) => {
  elapsed += delta;

  if (elapsed < 1) return;
  elapsed = 0;

  container.rotation = container.rotation + 0.01;
});

위에서

container.pivot.x = container.width / 2;
container.pivot.y = container.height / 2;
container.x = container.width / 2;
container.y = container.height / 2;

코드는

container.pivot.set(container.width / 2, container.height / 2);
container.position.set(container.width / 2, container.height / 2);

로도 작성 가능합니다.