Media Art ProgrammingWeek 09 · Period 03 Handout

p5.js / Media Art Programming

9주차 3교시 — 학생 주도 실습: 벡터를 활용한 움직임 작품

Vectors and Motion 주제의 학생용 강의 자료입니다.

WEEK 09 PERIOD 03 P5.JS MEDIA ART

9주차 3교시 — 학생 주도 실습: 벡터를 활용한 움직임 작품

강의 아웃라인: week-09-outline 이전 교시: week-09-period2-student (가속도, 삼각함수, 클래스 결합) 다음 주차: week-10-outline (힘과 물리)

항목 내용
대상 p5.js 입문자 (테크아트 전공)
소요 시간 45분
사전 준비 1교시 벡터 기본, 2교시 가속도·삼각함수·Mover 클래스
결과물 벡터·클래스·배열 기반 움직임 작품 1점
실습 목표
  • p5.Vector와 벡터 연산을 사용하여 자연스러운 움직임을 구현합니다
  • 7주차 클래스/배열 패턴을 벡터 기반으로 확장하여 다수의 객체가 움직이는 작품을 제작합니다

실습 환경 설정

p5.js Web Editor(editor.p5js.org)에서 새 스케치를 만듭니다.


스타터 코드

기본 구조:

class Mover {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(random(-2, 2), random(-2, 2));
    this.acc = createVector(0, 0);
    this.r = 15;
  }

  update() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);
  }

  display() {
    fill(127);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.r * 2);
  }

  edges() {
    if (this.pos.x > width) this.pos.x = 0;
    if (this.pos.x < 0) this.pos.x = width;
    if (this.pos.y > height) this.pos.y = 0;
    if (this.pos.y < 0) this.pos.y = height;
  }
}

let movers = [];

function setup() {
  createCanvas(400, 400);
  for (let i = 0; i < 10; i++) {
    movers.push(new Mover(random(width), random(height)));
  }
}

function draw() {
  background(220);
  for (let m of movers) {
    m.update();
    m.edges();
    m.display();
  }
}

기본 동작: 10개의 회색 원이 무작위 방향으로 등속 이동.


Step 1 — Mover 클래스 작성 및 동작 확인

목표: 벡터 기반 Mover 클래스를 작성하고, 1개의 객체가 화면에서 움직이는 것을 확인한다.

방향 A: 벡터 바운싱 월드

5주차 바운싱을 벡터 + 클래스로 구현.

작성할 코드:

class Mover {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(random(-3, 3), random(-3, 3));
    this.acc = createVector(0, 0);
    this.r = random(10, 25);
    this.c = color(random(100, 255), random(100, 255), random(255));
  }

  update() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);
  }

  display() {
    fill(this.c);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.r * 2);
  }

  edges() {
    if (this.pos.x > width - this.r || this.pos.x < this.r) {
      this.vel.x *= -1;
    }
    if (this.pos.y > height - this.r || this.pos.y < this.r) {
      this.vel.y *= -1;
    }
  }
}

7주차 Ball 클래스의 bounce()와 같은 논리를 벡터로 작성한 구조입니다.

방향 B: 마우스 추적 Mover

마우스를 향해 가속하는 Mover입니다.

작성할 코드 (추가할 메서드):

class Mover {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(0, 0);
    this.acc = createVector(0, 0);
    this.r = random(5, 12);
    this.c = color(random(100, 255), random(100, 200), random(200, 255));
  }

  seekMouse() {
    let mouse = createVector(mouseX, mouseY);
    let dir = p5.Vector.sub(mouse, this.pos);
    dir.normalize();
    dir.mult(0.3);
    this.acc = dir;
  }

  update() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.vel.mult(0.98);   // 마찰
    this.acc.mult(0);
  }

  display() {
    fill(this.c);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.r * 2);
  }
}

seekMouse() 패턴: sub()로 방향 → normalize()로 크기 1 → mult(0.3)로 가속 크기 조절.

방향 C: 삼각함수 궤도 Mover

sin/cos로 원운동하는 Mover입니다.

작성할 코드:

class OrbitalMover {
  constructor(cx, cy, r, speed) {
    this.center = createVector(cx, cy);  // 궤도 중심
    this.r = r;                           // 궤도 반지름
    this.angle = random(TWO_PI);          // 시작 각도
    this.speed = speed;                   // 회전 속도
    this.pos = createVector(0, 0);
    this.size = random(5, 15);
    this.c = color(random(150, 255), random(100, 200), random(200, 255));
  }

  update() {
    this.angle += this.speed;
    this.pos.x = cos(this.angle) * this.r + this.center.x;
    this.pos.y = sin(this.angle) * this.r + this.center.y;
  }

  display() {
    fill(this.c);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.size);
  }
}

원운동 공식: cos(angle) * r + cx, sin(angle) * r + cy.

예상 결과: 방향에 따라 1개의 객체가 화면에서 움직이는 것을 확인

힌트
  • createVector()setup() 또는 클래스의 constructor() 안에서 호출.
  • 클래스 안에서는 this.pos, this.vel처럼 this를 붙인다.
  • 배열로 늘리기 전에 객체 1개부터 확인.

Step 2 — 배열로 확장

목표: 5개 이상의 Mover 객체를 배열로 관리하고, 각각 독립적으로 움직이게 한다.

가이드:

let movers = [];

function setup() {
  createCanvas(400, 400);
  for (let i = 0; i < 10; i++) {
    movers.push(new Mover(random(width), random(height)));
  }
}

function draw() {
  background(220);
  for (let m of movers) {
    // 방향 B의 경우:
    // m.seekMouse();
    m.update();
    m.edges();    // 방향 A만 해당
    m.display();
  }
}

7주차에서 했던 push(new Mover(...))for...of 순회와 같은 패턴입니다.

14 multimover array loop

방향 C의 경우 (궤도 Mover):

let orbiters = [];

function setup() {
  createCanvas(400, 400);
  for (let i = 0; i < 8; i++) {
    let r = 50 + i * 20;          // 반지름을 점점 크게
    let speed = 0.02 - i * 0.002; // 바깥일수록 느리게
    orbiters.push(new OrbitalMover(200, 200, r, speed));
  }
}

function draw() {
  background(0, 0, 0, 30);  // 잔상 효과
  for (let o of orbiters) {
    o.update();
    o.display();
  }
}

반지름을 다르게 설정하면 태양계처럼 여러 궤도에서 도는 점들이 생깁니다.

예상 결과: 다수의 객체가 각각 독립적으로 움직이는 장면

힌트
  • let movers = [];setup() 밖에 선언.
  • movers.push(new Mover(...))로 객체 추가.
  • for (let m of movers)로 순회.
  • 방향 C의 createVector()setup() 안이나 constructor() 안에서 호출.

Step 3 — 시각 효과 및 인터랙션 추가

목표: 색상, 크기 변화, 잔상 효과, 인터랙션 중 1개 이상을 추가한다.

15 speed to visual effects

색상 효과

속도에 따른 색상 변화 (HSB 모드):

// setup() 안에 추가
colorMode(HSB, 360, 100, 100, 100);

// display() 메서드 수정
display() {
  let speed = this.vel.mag();  // 속도의 크기
  let hue = map(speed, 0, 5, 180, 360);  // 느리면 파랑, 빠르면 빨강
  fill(hue, 80, 100);
  noStroke();
  ellipse(this.pos.x, this.pos.y, this.r * 2);
}

this.vel.mag()은 1교시에서 배운 벡터의 크기라서, 속도가 빠를수록 다른 색상이 나옵니다.

크기 효과

속도에 비례한 크기 변화:

display() {
  let speed = this.vel.mag();
  let size = map(speed, 0, 5, 5, 30);
  fill(this.c);
  noStroke();
  ellipse(this.pos.x, this.pos.y, size);
}

잔상 효과

function draw() {
  // background(220);  // 이 줄을 아래로 교체
  background(0, 0, 0, 30);  // 반투명 배경 → 잔상

  for (let m of movers) {
    m.update();
    m.edges();
    m.display();
  }
}

5주차에서 배운 잔상 효과입니다. background()의 네 번째 값(alpha)이 작을수록 잔상이 오래 남습니다.

인터랙션 — 클릭으로 객체 추가

function mousePressed() {
  movers.push(new Mover(mouseX, mouseY));
}

4주차, 6주차, 7주차 내용을 조합하면 클릭할 때마다 마우스 위치에 새 Mover가 생깁니다.

인터랙션 — 키보드로 속도에 순간 변화(impulse) 주기

function keyPressed() {
  let impulse = createVector(0, 0);
  if (key === 'w') impulse = createVector(0, -2);
  if (key === 's') impulse = createVector(0, 2);
  if (key === 'a') impulse = createVector(-2, 0);
  if (key === 'd') impulse = createVector(2, 0);

  for (let m of movers) {
    m.vel.add(impulse);  // 속도에 직접 순간 변화를 더함
  }
}

WASD 키를 누르면 모든 공의 vel에 한 번의 속도 변화가 더해집니다.

예상 결과: 시각적으로 풍부하고, 사용자 입력에 반응하는 작품

힌트
  • HSB 색상 모드를 쓰려면 setup() 안에 colorMode(HSB, 360, 100, 100, 100); 추가.
  • 잔상 효과는 background(r, g, b, alpha) — alpha가 작을수록 잔상이 오래 남습니다. RGB 모드에서는 background(0, 30), HSB 모드에서는 background(0, 0, 0, 30).
  • mousePressed() 함수는 클래스 밖, setup()/draw()와 같은 레벨에 작성.

Step 4 — 선택 확장

확장 1: 중력 추가

// update() 메서드에서 또는 draw()에서
// 매 프레임 아래로 가속
update() {
  let gravity = createVector(0, 0.1);
  this.acc.add(gravity);
  this.vel.add(this.acc);
  this.pos.add(this.vel);
  this.acc.mult(0);
}

확장 2: sin으로 크기 진동

display() {
  let oscillatingSize = this.r + sin(frameCount * 0.1) * 5;
  fill(this.c);
  noStroke();
  ellipse(this.pos.x, this.pos.y, oscillatingSize * 2);
}

sin()으로 크기를 주기적으로 변화시킵니다.

확장 3: 속도 제한 (limit())

update() {
  this.vel.add(this.acc);
  this.vel.limit(5);  // 최대 속도 5로 제한
  this.pos.add(this.vel);
  this.acc.mult(0);
}

vel.limit(5)는 최대 속도를 5로 제한합니다.

확장 4: 방향에 따라 회전하는 삼각형

display() {
  push();
  translate(this.pos.x, this.pos.y);
  rotate(this.vel.heading());  // 속도 방향으로 회전
  fill(this.c);
  noStroke();
  triangle(-this.r, -this.r / 2, -this.r, this.r / 2, this.r, 0);
  pop();
}

vel.heading()은 속도 벡터의 각도이며, rotate()와 결합하면 이동 방향을 바라보는 삼각형을 만들 수 있습니다.


완성 예시

방향 A: 벡터 바운싱 월드

class Mover {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(random(-3, 3), random(-3, 3));
    this.acc = createVector(0, 0);
    this.r = random(10, 25);
    this.c = color(random(100, 255), random(100, 255), random(255), 200);
  }

  update() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);
  }

  display() {
    fill(this.c);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.r * 2);
  }

  edges() {
    if (this.pos.x > width - this.r) {
      this.pos.x = width - this.r;
      this.vel.x *= -1;
    }
    if (this.pos.x < this.r) {
      this.pos.x = this.r;
      this.vel.x *= -1;
    }
    if (this.pos.y > height - this.r) {
      this.pos.y = height - this.r;
      this.vel.y *= -1;
    }
    if (this.pos.y < this.r) {
      this.pos.y = this.r;
      this.vel.y *= -1;
    }
  }
}

let movers = [];

function setup() {
  createCanvas(400, 400);
  for (let i = 0; i < 20; i++) {
    movers.push(new Mover(random(width), random(height)));
  }
}

function draw() {
  background(240);
  for (let m of movers) {
    m.update();
    m.edges();
    m.display();
  }
}

function mousePressed() {
  movers.push(new Mover(mouseX, mouseY));
}

실행 결과: - 20개의 다양한 크기/색상의 원이 벽에 튕기며 이동함 - 클릭할 때마다 마우스 위치에 새 원 추가

방향 B: 마우스 추적 파티클

class Mover {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(random(-1, 1), random(-1, 1));
    this.acc = createVector(0, 0);
    this.r = random(3, 10);
    this.c = color(random(150, 255), random(100, 200), random(200, 255));
  }

  seekMouse() {
    let mouse = createVector(mouseX, mouseY);
    let dir = p5.Vector.sub(mouse, this.pos);
    dir.normalize();
    dir.mult(0.3);
    this.acc = dir;
  }

  update() {
    this.vel.add(this.acc);
    this.vel.limit(6);      // 최대 속도 제한
    this.pos.add(this.vel);
    this.vel.mult(0.98);    // 마찰
    this.acc.mult(0);
  }

  display() {
    let speed = this.vel.mag();
    let alpha = map(speed, 0, 6, 50, 255);
    fill(red(this.c), green(this.c), blue(this.c), alpha);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.r * 2);
  }
}

let movers = [];

function setup() {
  createCanvas(400, 400);
  for (let i = 0; i < 30; i++) {
    movers.push(new Mover(random(width), random(height)));
  }
}

function draw() {
  background(0, 0, 0, 30);  // 잔상 효과
  for (let m of movers) {
    m.seekMouse();
    m.update();
    m.display();
  }
}

function mousePressed() {
  for (let i = 0; i < 5; i++) {
    movers.push(new Mover(mouseX + random(-20, 20), mouseY + random(-20, 20)));
  }
}

실행 결과: - 30개의 점이 마우스 방향으로 이동함 - 잔상 효과로 궤적이 보임 - 빠르면 밝고, 느리면 투명 - 클릭할 때마다 5개씩 추가됨

방향 C: 삼각함수 궤도 아트

class OrbitalMover {
  constructor(cx, cy, r, speed) {
    this.center = createVector(cx, cy);
    this.r = r;
    this.angle = random(TWO_PI);
    this.speed = speed;
    this.pos = createVector(0, 0);
    this.size = map(r, 30, 180, 3, 12);
    this.hue = map(r, 30, 180, 0, 300);
  }

  update() {
    this.angle += this.speed;
    this.pos.x = cos(this.angle) * this.r + this.center.x;
    this.pos.y = sin(this.angle) * this.r + this.center.y;
  }

  display() {
    fill(this.hue, 80, 100, 60);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.size);
  }
}

let orbiters = [];

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100, 100);

  for (let i = 0; i < 12; i++) {
    let r = 30 + i * 15;
    let speed = 0.03 - i * 0.002;
    orbiters.push(new OrbitalMover(200, 200, r, speed));
  }
}

function draw() {
  background(0, 0, 10, 8);  // 잔상 효과
  for (let o of orbiters) {
    o.update();
    o.display();
  }
}

function mousePressed() {
  // 클릭하면 마우스 위치를 중심으로 새 궤도 추가
  let r = random(20, 80);
  let speed = random(0.01, 0.05);
  orbiters.push(new OrbitalMover(mouseX, mouseY, r, speed));
}

실행 결과: - 12개의 점이 중앙을 중심으로 각각 다른 반지름/속도로 원운동 - 잔상 효과로 궤적의 시각적 패턴이 형성됨 - 안쪽 궤도는 빠르고 작은 점, 바깥 궤도는 느리고 큰 점 - 클릭하면 마우스 위치에 새 궤도 추가


마무리 체크

개념 핵심 코드
벡터 x, y를 하나로 묶음 createVector(x, y)
움직임 위치 + 속도 pos.add(vel)
가속 속도 + 가속도 vel.add(acc)pos.add(vel)
원운동 sin + cos cos(angle) * r, sin(angle) * r
Mover 클래스 7주차 클래스의 벡터 업그레이드 this.pos, this.vel, this.acc
핵심 한 문장

pos.add(vel) = 움직임, vel.add(acc) = 자연스러운 가속.

작업 후 Ctrl+S 또는 Cmd+S로 저장합니다.