Nature of code 4장까지 스터디를 진행하면서 주로 물속에서 물체의 움직임과 관련하여 발표를 주로하였다. 4장까지의 내용을 복습할겸 해당 프로젝트를 진행하게 되었다.
이미지출처: 핀터레스트
이 코드는 별이 고래 머리의 범위 내에 있을 때 별의 위치를 조정하여 충돌을 피하고, 그렇지 않을 경우 원래 위치로 돌아가도록 하는 기능을 구현하였다.
updatePosition 함수는 고래가 움직일때마다 별의 위치를 업데이트해주는 함수이다.
let shieldR = 90;
updatePosition(x: number, y: number) {
// 고래와 별 사이의 거리 계산
let dx = x - this.pos.x;
let dy = y - this.pos.y;
// 유클리드
// 두 점 사이의 x 좌표와 y 좌표의 차이를 제곱하여 더한 후, 그 결과에 제곱근
let distance = this.p5.sqrt(dx * dx + dy * dy);
// 반경 50px 내에 별을 그리지 않는다
if (distance < shieldR) {
// 공에서 별로 향하는 벡터의 단위벡터
let unitX = dx / distance;
let unitY = dy / distance;
// 별의 위치를 공과 반대 방향으로 조정
this.pos.sub(unitX * 2, unitY * 2);
} else {
// 원상복구
this.pos.add(
(this.originPos.x - this.pos.x) * 0.05,
(this.originPos.y - this.pos.y) * 0.05
);
}
}
원 모형으로 으로 설명하면 더 이해가 쉽다.
여기서 x, y는 원의 중심점을 의미한다. 이 중심점과 별의 위치 사이의 거리를 계산한다.

this.wPos.add(this.speedX, this.speedY);
위치 = 위치 + 속도
// 벽과의 충돌 체크
if (
this.wPos.x < this.boundary ||
this.wPos.x > window.innerWidth - this.boundary
) {
this.speedX *= -1; // x축 방향 반전
}
if (
this.wPos.y < this.boundary ||
this.wPos.y > window.innerHeight - this.boundary
) {
this.speedY *= -1; // y축 방향 반전
}
this.fins.forEach((f, i) => {
if (i > 0) {
const prevFin = this.fins[i - 1];
f.update(prevFin.pos.x, prevFin.pos.y, i === this.numFin - 1, i);
} else {
this.fins[0].update(this.wPos.x, this.wPos.y);
}
});
작명이 잘못된건 인정.. 지느러미라고 쓰고 몸통이라고 읽겠다.
몸통의 길이와 방향을 계산하여, 움직이는 위치와 방향으로 그려질 수 있도록 해야했다. 몸통이 향해야 할 목표 위치(target)와 현재 위치(this.pos) 간의 벡터를 계산하여 사다리꼴의 모양과 방향을 계산하는것이 어려웠다.
// 현재 움직이는 위치와 fin위치간 거리의 차이를 구한다.
const target = this.p5.createVector(x, y);
const dir = p5.Vector.sub(target, this.pos);
dir.setMag(isTail ? this.w - 30 : this.w);
dir.mult(-1);
// 현재 움직이는 위치가 어디로 향하는지, 각도를 구한다.
this.angle = dir.heading();
this.pos = p5.Vector.add(target, dir);
(1) 삼각함수를 사용한 위치 계산
// 사다리꼴의 기본 형태 정의
let orig = this.p5.createVector(0, 0);
let other = this.p5.createVector(
0,
-this.p5.dist(this.pos.x, this.pos.y, arg2.x, arg2.y) // 아래 방향 기준 사다리꼴의 길이
);
const h = isTail ? 20 : 50;
const arg2 = this.p5.createVector(0, 0);
const dx = h * this.p5.cos(this.angle);
const dy = h * this.p5.sin(this.angle);
arg2.set(this.pos.x + dx, this.pos.y + dy);
cos와 sin 함수를 사용하여 각도(this.angle)에 따른 이동 벡터를 계산한다. 각도에 따라 이동 벡터를 조정하면 지느러미가 실제로 회전하고 움직이는 방향에 맞게 위치를 조정할 수 있다.
이미지출처: nature of code
벡터를 x축과 y축 방향으로 분해할 때, 삼각함수는 다음과 같은 역할을 한다:
const dx = h * this.p5.cos(this.angle);
// 현재 위치에서 몸통의 끝부분이 x축 방향으로 이동해야 하는 거리
// : 각도에 따라 코사인 값은 x축 방향으로의 이동량을 제공
const dy = h * this.p5.sin(this.angle);
// 현재 위치에서 지느러미의 끝부분이 y축 방향으로 이동해야 하는 거리
// 각도에 따라 사인 값은 y축 방향으로의 이동량을 제공
(2) 사다리꼴의 네점 계산
let p1, p2, p3, p4;
p1 = orig.copy(); // 왼쪽 위
p2 = orig.copy(); // 오른쪽 위
p3 = other.copy(); // 오른쪽 아래
p4 = other.copy(); // 왼쪽 아래
p1.add(-widthFront / 2, margin);
p2.add(widthFront / 2, margin);
p3.add(widthBack / 2, -margin);
p4.add(-widthBack / 2, -margin);
// 사다리꼴의 네 점을 배열로 저장
const trapPoints = [p1, p2, p3, p4];
(3) 회전 및 위치 조정
for (let i = 0; i < trapPoints.length; i++) {
let h = this.pos.copy();
// h 벡터는 현재 위치 this.pos와 이동된 위치 arg2 사이의 벡터.두 위치 간의 방향.
h.sub(arg2);
// 사다리꼴의 각 점을 h 벡터의 방향으로 회전
trapPoints[i].rotate(h.heading());
// 사다리꼴을 90도 회전. 사다리꼴이 h 벡터와 수직으로 배치되도록 하기 위함
trapPoints[i].rotate(this.p5.radians(90));
// 회전된 사다리꼴의 각 점을 원래 위치 this.pos로 이동
trapPoints[i].add(this.pos);
}
this.p5.fill(255, 200);
this.p5.noStroke();
(4) 사다리꼴 그리기
this.p5.fill(255, 200);
this.p5.noStroke();
// curveTightness 설정
this.p5.curveTightness(0.5);
// 둥근 사다리꼴 그리기
this.p5.beginShape();
for (let i = 0; i < trapPoints.length; i++) {
this.p5.curveVertex(trapPoints[i].x, trapPoints[i].y);
}
// 마지막 점은 시작점을 위해 반복
this.p5.curveVertex(trapPoints[0].x, trapPoints[0].y);
this.p5.curveVertex(trapPoints[1].x, trapPoints[1].y);
this.p5.endShape(this.p5.CLOSE);
머리를 기준으로 몸통과 꼬리가 sine 운동을 한다.
몸통의 무늬는 머리에서 꼬리로 갈 수록 점(dot)의 밀도가 낮아진다.
양 옆의 지느러미는 움직이는 방향을 기준으로 상하 운동을 한다.
전시하였을때 모두가 고래를 터치하는 행동을 보였다. 고래를 터치하였을 때 꿈틀거리거나 말풍선을 띄워 대화하는것처럼 보이는 효과를 추가해야겠다.
참고자료