cat
재미

뛰는 고양이를 코드로 구현해보자

2023. 7. 29. 16:50
목차
  1. Canvas 초기 세팅
  2. 움직이는 언덕 만들기
  3. 고양이 생성

고양이를 너무 좋아하는 나는 이번 주말에 Canvas로 언덕을 뛰어다니는 고양이를 구현해보려고 한다.

Canvas 초기 세팅

index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat's on the hill</title>

    <link rel="stylesheet" type="text/css" href="./style.css" >
</head>
<body>
    <script type="module" src="./app.js"></script>
</body>
</html>

style.css

* {
    outline: 0;
    margin: 0;
    padding: 0;
}

html {
    width: 100%;
    height: 100%;
}

body {
    width: 100%;
    height: 100%;
    background-color: #C2DEDC; // 내가 좋아하는 색상
}

canvas {
    width: 100%;
    height: 100%;
}

app.js

class App {
  constructor() {
    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext("2d");
    document.body.appendChild(this.canvas);

    window.addEventListener("resize", this.resize.bind(this), false);
    this.resize();

    requestAnimationFrame(this.animate.bind(this));
  }

// 스크린의 넓이, 높이를 가져오기 위해 resize()추가
  resize() {
    this.stageWidth = document.body.clientWidth;
    this.stageHeight = document.body.clientHeight;

    this.canvas.width = this.stageWidth * 2;
    this.canvas.height = this.stageHeight * 2;
    this.ctx.scale(2, 2);

    this.catController.resize(this.stageWidth, this.stageHeight);
  }

  animate(t) {
    requestAnimationFrame(this.animate.bind(this));

    // 최초에 canvas를 깨끗이 지워줌
    this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);
  }
}

window.onload = () => {
  new App();
};

움직이는 언덕 만들기

이렇게 기초 세팅을 끝내놓고 언덕을 그리러 가보자

hill.js

export class Hill {
  // 생성자 함수
  // color : Hill 색상
  // speed : Hill 움직임의 속도
  // total : 포인트의 총 개수
  constructor(color, speed, total) {
    this.color = color;
    this.speed = speed;
    this.total = total;
  }

  // 크기 조정 함수
  resize(stageWidth, stageHeight) {
    // 화면의 너비와 높이를 설정
    this.stageWidth = stageWidth;
    this.stageHeight = stageHeight;

    // 포인트 배열 초기화
    this.points = [];

    // 각 포인트 사이의 간격을 설정. 전체 너비를 포인트 개수만큼 나누어 간격을 설정
    this.gap = Math.ceil(this.stageWidth / (this.total - 2));

    // 각 포인트의 x, y 좌표를 설정하며 포인트 배열에 추가
    for (let i = 0; i < this.total; i++) {
      this.points[i] = {
        x: i * this.gap,
        y: this.getY(),
      };
    }
  }

  // 그리기 함수
  draw(ctx) {
    // 캔버스 색상을 Hill 색상으로 설정
    ctx.fillStyle = this.color;
    ctx.beginPath();

    let cur = this.points[0];
    let prev = cur;

    // dots 배열을 이용해서 곡선의 컨트롤 포인트를 저장
    let dots = [];
    cur.x += this.speed;

    // 화면을 벗어나면 포인트 배열에서 제거하고 새로운 포인트를 추가
    if (cur.x > -this.gap) {
      this.points.unshift({
        x: -(this.gap * 2),
        y: this.getY(),
      });
    } else if (cur.x > this.stageWidth + this.gap) {
      this.points.splice(-1);
    }

    ctx.moveTo(cur.x, cur.y);

    let prevCx = cur.x;
    let prevCy = cur.y;

    // 각 포인트를 순회하며 이전 포인트와 현재 포인트를 이용해 곡선을 그림
    for (let i = 1; i < this.points.length; i++) {
      cur = this.points[i];
      cur.x += this.speed;
      const cx = (prev.x + cur.x) / 2;
      const cy = (prev.y + cur.y) / 2;
      ctx.quadraticCurveTo(prev.x, prev.y, cx, cy);

      dots.push({
        x1: prevCx,
        y1: prevCy,
        x2: prev.x,
        y2: prev.y,
        x3: cx,
        y3: cy,
      });

      prev = cur;
      prevCx = cx;
      prevCy = cy;
    }
    // 곡선 그리기를 완료하고 캔버스 아래로 라인을 그림
    ctx.lineTo(prev.x, prev.y);
    ctx.lineTo(this.stageWidth, this.stageHeight);
    ctx.lineTo(this.points[0].x, this.stageHeight);
    ctx.fill();

    // 곡선의 컨트롤 포인트를 반환
    return dots;
  }

  // 랜덤한 높이를 계산하는 함수
  getY() {
    const min = this.stageHeight / 8;
    const max = this.stageHeight - min;
    // min과 max 사이의 랜덤 값을 반환
    return min + Math.random() * max;
  }
}

app.js에 언덕을 3개 추가해 줬다.

그리고 각각 언덕에 대해서 속도를 가까운 곳부터 먼 곳 순서로 느리게 움직이게 해서 원근감을 표현했다

app.js

import { Hill } from "./hill.js";

class App {
  constructor() {
    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext("2d");
    document.body.appendChild(this.canvas);

    // Hill 객체들을 배열로 관리
    this.hills = [
      new Hill("#ECE5C7", 0.2, 12),
      new Hill("#CDC2AE", 0.5, 8),
      new Hill("#116A7B", 1.4, 6),
    ];

    window.addEventListener("resize", this.resize.bind(this), false);
    this.resize();

    requestAnimationFrame(this.animate.bind(this));
  }

  resize() {
    this.stageWidth = document.body.clientWidth;
    this.stageHeight = document.body.clientHeight;

    this.canvas.width = this.stageWidth * 2;
    this.canvas.height = this.stageHeight * 2;
    this.ctx.scale(2, 2);

    // 각 Hill 객체의 크기를 조정
    for (let i = 0; i < this.hills.length; i++) {
      this.hills[i].resize(this.stageWidth, this.stageHeight);
    }
  }

  animate(t) {
    requestAnimationFrame(this.animate.bind(this));

    this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);

    let dots;

    // 각 Hill 객체를 그림
    for (let i = 0; i < this.hills.length; i++) {
      dots = this.hills[i].draw(this.ctx);
    }

  }
}

// 윈도우 로드 이벤트가 발생하면 App 객체를 생성
window.onload = () => {
  new App();
};
움직이는 언덕

고양이 생성

고양이 파일 10개를 준비해 놨지만 온라인에서 제공하는 시퀀스 파일은 배경이미지를 흰색으로 넣어버려 가지고 파이썬 파일로 직접 구현했다.

from PIL import Image
import os

# 이미지 파일들이 위치한 디렉토리
img_dir = "/Users/blanc/Documents/Project/cats-on-hill/cats"

# 해당 디렉토리 내의 모든 PNG 파일을 가져옵니다.
imgs = [i for i in os.listdir(img_dir) if i.endswith(".png")]

# 파일 이름을 숫자로 정렬합니다. (예: "1.png", "2.png", ..., "10.png")
imgs = sorted(imgs, key=lambda x: int(os.path.splitext(x)[0]))

# 각각의 이미지를 연다.
img_list = [Image.open(os.path.join(img_dir, img)).convert("RGBA") for img in imgs]

# 각각의 이미지의 너비와 높이를 가져온다.
img_widths, img_heights = zip(*(i.size for i in img_list))

# 전체 이미지의 너비는 각각의 이미지의 너비의 합이고, 높이는 가장 큰 높이와 같다.
total_width = sum(img_widths)
max_height = max(img_heights)

# 새 이미지를 생성한다. (RGBA 모드로 생성하여 배경이 없는 이미지를 만든다)
new_img = Image.new('RGBA', (total_width, max_height))

# 각각의 이미지를 새 이미지에 붙인다.
x_offset = 0
for img in img_list:
    new_img.paste(img, (x_offset,0))
    x_offset += img.width

# 결과 이미지를 저장한다.
new_img.save('combined.png')

이 코드를 통해서 1장의 파일로 만들었다.

파일을 첨부해 놨으니 다운로드하고 싶은 사람은 언제든지 받아도 좋다.

고양이 파일.png

고양이 파일은 다른 파일들과 마찬가지로 같은 폴더에 넣어두자 catController.js에서 쓰인다.

이제 고양이의 이미지는 준비되었으니까 고양이를 정의해 보자

cat.js

export class Cat {
  constructor(img, stageWidth) {
    // 생성자 함수에서 필요한 변수들을 초기화
    this.img = img;

    // 총 프레임과 현재 프레임
    this.totalframe = 8;
    this.curFrame = 0;

    // 이미지의 너비와 높이
    this.imgWidth = 112;
    this.imgHeight = 72;

    // 고양이의 너비와 높이
    this.catWidth = 56;
    this.catHeight = 36;

    // 고양이 이미지의 너비의 절반
    this.catWidthHalf = this.catWidth / 2;
    // 초기 위치와 속도 설정
    this.x = stageWidth + this.catWidth;
    this.y = 0;
    this.speed = Math.random() * 2 + 1;

    // 1초에 5번 움직여서 한 동작으로 묶었다
    this.fps = 5;
    this.fpsTime = 1000 / this.fps;
  }

  // 그리기 함수
  draw(ctx, t, dots) {
    // 이전에 그렸던 시간과 현재 시간을 비교
    if (!this.time) {
      this.time = t;
    }
    const now = t - this.time;
    if (now > this.fpsTime) {
      this.time = t;
      this.curFrame += 1;
      // 모든 프레임을 그렸으면 첫 프레임으로 돌아감
      if (this.curFrame == this.totalframe) {
        this.curFrame = 0;
      }
    }
    this.animate(ctx, dots);
  }

  // 애니메이션 함수
  animate(ctx, dots) {
    this.x -= this.speed * 0.4;
    // 고양이의 y 위치를 구함
    const closest = this.getY(this.x, dots);
    this.y = closest.y;

    // Canvas 상태 저장
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.fillStyle = "#000000";
    // 고양이의 회전 각도를 설정
    ctx.rotate(closest.rotation);
    // 고양이 이미지를 그림
    ctx.drawImage(
      this.img,
      this.imgWidth * this.curFrame,
      0,
      this.imgWidth,
      this.imgHeight,
      -this.catWidthHalf,
      -this.catHeight + 2,
      this.catWidth,
      this.catHeight
    );
    // Canvas 상태 복원
    ctx.restore();
  }

  // 고양이의 y 위치와 회전 각도를 구하는 함수
  getY(x, dots) {
    for (let i = 1; i < dots.length; i++) {
      if (x >= dots[i].x1 && x <= dots[i].x3) {
        return this.getY2(x, dots[i]);
      }
    }
    return {
      y: 0,
      rotation: 0,
    };
  }

  // getY 함수의 보조 함수
  getY2(x, dot) {
    const total = 200;
    let pt = this.getPointOnQuad(
      dot.x1,
      dot.y1,
      dot.x2,
      dot.y2,
      dot.x3,
      dot.y3,
      0
    );
    let prevX = pt.x;
    for (let i = 1; i < total; i++) {
      const t = i / total;
      pt = this.getPointOnQuad(
        dot.x1,
        dot.y1,
        dot.x2,
        dot.y2,
        dot.x3,
        dot.y3,
        t
      );
      if (x >= prevX && x <= pt.x) {
        return pt;
      }
      prevX = pt.x;
    }
    return pt;
  }

  // 언덕의 가파르기와 높낮이를 찾기 위해서 해당 함수를 정의한다. (stack overflow에 잘 나와있다)
  getQuadValue(p0, p1, p2, t) {
    return (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
  }

  // 언덕의 점을 구하는 함수
  getPointOnQuad(x1, y1, x2, y2, x3, y3, t) {
    const tx = this.quadTangent(x1, x2, x3, t);
    const ty = this.quadTangent(y1, y2, y3, t);
    const rotation = -Math.atan2(tx, ty) + (90 * Math.PI) / 180;
    return {
      x: this.getQuadValue(x1, x2, x3, t),
      y: this.getQuadValue(y1, y2, y3, t),
      rotation,
    };
  }

  // 접선을 구하는 함수
  quadTangent(a, b, c, t) {
    return 2 * (1 - t) * (b - a) + 2 * (c - b) * t;
  }
}

그리고 고양이를 소환하고 어떻게 움직이게 할지 정의해주는 catController도 만들자

catController.js

import { Cat } from "./cat.js";

export class CatController {
  constructor() {
    // 새 이미지 객체 생성
    this.img = new Image();
    // 이미지 로딩이 완료되면 loaded 함수 호출
    this.img.onload = () => {
      this.loaded();
    };
    // 이미지 경로 설정
    this.img.src = "cats.png";

    // Cat 객체를 담을 배열
    this.items = [];

    this.cur = 0;
    this.isLoaded = false;
  }

  // 무대의 크기를 받아와 저장하는 함수
  resize(stageWidth, stageHeight) {
    this.stageWidth = stageWidth;
    this.stageHeight = stageHeight;
  }

  // 이미지 로딩이 완료된 후 호출되는 함수
  loaded() {
    this.isLoaded = true;
    this.addCat();
  }

  // Cat 객체를 생성하여 items 배열에 추가하는 함수
  addCat() {
    this.items.push(new Cat(this.img, this.stageWidth));
  }

  // Cat 객체들을 그리는 함수
  draw(ctx, t, dots) {
    // 이미지가 로딩된 경우에만 그리기 수행
    if (this.isLoaded) {
      this.cur += 1;
      if (this.cur > 200) {
        this.cur = 0;
        this.addCat();
      }

      // 배열에 있는 Cat 객체들을 그리고 화면 밖으로 나간 객체는 제거
      for (let i = this.items.length - 1; i >= 0; i--) {
        const item = this.items[i];
        if (item.x < -item.width) {
          this.items.splice(i, 1);
        } else {
          item.draw(ctx, t, dots);
        }
      }
    }
  }
}

그리고 나서 app.js로 돌아와서 방금 정의한 스크립트들을 import 해주고 고양이를 생성하면,

app.js

import { Hill } from "./hill.js";
import { CatController } from "./cat-controller.js";

class App {
  constructor() {
    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext("2d");
    document.body.appendChild(this.canvas);

    this.hills = [
      new Hill("#ECE5C7", 0.2, 12),
      new Hill("#CDC2AE", 0.5, 8),
      new Hill("#116A7B", 1.4, 6),
    ];

    this.catController = new CatController();

    window.addEventListener("resize", this.resize.bind(this), false);
    this.resize();

    requestAnimationFrame(this.animate.bind(this));
  }

  resize() {
    this.stageWidth = document.body.clientWidth;
    this.stageHeight = document.body.clientHeight;

    this.canvas.width = this.stageWidth * 2;
    this.canvas.height = this.stageHeight * 2;
    this.ctx.scale(2, 2);

    for (let i = 0; i < this.hills.length; i++) {
      this.hills[i].resize(this.stageWidth, this.stageHeight);
    }

    this.catController.resize(this.stageWidth, this.stageHeight);
  }

  animate(t) {
    requestAnimationFrame(this.animate.bind(this));

    this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);

    let dots;

    for (let i = 0; i < this.hills.length; i++) {
      dots = this.hills[i].draw(this.ctx);
    }

    this.catController.draw(this.ctx, t, dots);
  }
}
window.onload = () => {
  new App();
};

완성했다.

이제 html파일을 실행하면 된다..!

 

 

달려라 달려

 

 

 

 

 

 


 

 

사실 티스토리 블로그 커버이미지에 저 위의 코드를 삽입하려고 시작했었는데

막상 넣어보니 CPU소모가 커서 렉이 너무 심하게 걸렸다.. 어쩔 수 없이 뺐다.. ㄲㅂ

 

 

 

 

 

끝

저작자표시

'재미' 카테고리의 다른 글

크롬 익스텐션으로 사내툴 개선  (1) 2023.11.02
ChatGPT를 이용한 자동화 블로그  (2) 2023.04.29
  1. Canvas 초기 세팅
  2. 움직이는 언덕 만들기
  3. 고양이 생성
'재미' 카테고리의 다른 글
  • 크롬 익스텐션으로 사내툴 개선
  • ChatGPT를 이용한 자동화 블로그
여행 가고싶다
여행 가고싶다
blanc여행 가고싶다 님의 블로그입니다.
여행 가고싶다
blanc
여행 가고싶다
전체
오늘
어제
  • 분류 전체보기 (43)
    • 토이프로젝트 (13)
      • 프론트엔드 인턴쉽 (5)
      • ProjectSassy (4)
      • LinkArchive (4)
    • FrontEnd (11)
      • React (6)
      • JavaScript (1)
      • TypeScript (1)
      • CSS (0)
      • Test (2)
    • CS (0)
      • Network (4)
      • 기타 (8)
    • 재미 (3)
    • AWS (2)
    • Git (1)
    • 클린코드 (1)

인기 글

최근 글

최근 댓글

hELLO · Designed By 정상우.
여행 가고싶다
뛰는 고양이를 코드로 구현해보자
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.