[Processing] Watch Face

Yean·2023년 11월 8일
1
post-thumbnail

모바일앱개발 수업을 들으며
Processing을 이용하여 WatchFace를 디자인하고 구현하는
프로젝트를 하게 되었다.

1. 기획

먼저, 어떤 Watch Face 디자인을 할 것인지에 앞서
시간에 대해 정의할 필요가 있다.

시간의 시각화

시간
1. 어떤 시각에서 어떤 시각과의 사이
2. 어떤 행동을 할 틈

무슨 뜻인지 언뜻 이해는 되지만 너무 추상적이다.

그래서 나는 시간의 정의 그 자체보다
시간의 특성에 초점을 맞추기로 했다.

내가 주목한 시간의 특성은
시간은 한 방향으로만 흐르고
한 번 흘러간 시간은 되돌릴 수 없다
시간의 불가역성 이다.


한 방향으로만 흐르는 시간의 특성은
마치 한 방향으로만 흐르는 시간 위를 달리는 이미지로
시각화할 수 있겠다고 생각했다

또한 시간은 되돌릴 수 없기에
지금 이 순간은 인생에 단 한 번뿐이다

이러한 아이디어를 바탕으로
워치페이스에 이러한 시간의 특성을 접목시켜
사용자가 같은 화면을 다시 보게 될 확률을 줄이면서
시간 위를 달리는 듯한 모습으로
시간을 시각화하는 것을 기획하였다.

2. 디자인

앞서 생각한 기획의도에 적합하다고 떠올린 것은
모두 한 번쯤은 해봤을 크롬의 다이노 게임이다.

게임의 원리는 간단하다

공룡은 장애물을 피해 오직 앞으로만 이동하고
이동한만큼의 거리가 실시간으로 우측 상단의 점수로 갱신된다.

한 방향으로만 이동하는 공룡은
한 방향으로만 흐르는 시간의 특성과 매칭된다.

그렇다면
사용자가 워치페이스를 차면서
같은 화면을 보게 될 확률은 어떻게 줄일 수 있을까?

3. 기능

ArrayList<Line> lines = new ArrayList<Line>();
int lineCount = 7;
void setup(){
 for (int i = 0; i < lineCount; i++) {
    float x1 = random(0, 750); // 원 안에서 랜덤한 위치에서 시작
    float x2 = x1 + random(50, 180); // 선의 길이 
    float y = random(70, 320); // 선의 y 위치
    float speed = random(7, 12); // 랜덤한 이동 속도
    Line line = new Line(x1, x2, y, speed);
    lines.add(line);
  }
}

void moveLines() {
  for (Line line : lines) {
    line.x1 -= line.speed; // 선의 시작 위치
    line.x2 -= line.speed;
    if (line.x1 + line.length < 30) {
      line.x1 = 800; // 선이 오른쪽에에서 다시 시작하게 함
      line.x2 = line.x1 + line.length; // 선의 끝 위치
      line.speed = random(7, 10); // 랜덤한 속도로 재설정
    }
  }
}

class Line {
  float x1;
  float x2;
  float y;
  float speed;
  float length;

  Line(float x1, float x2, float y, float speed) {
    this.x1 = x1;
    this.x2 = x2;
    this.y = y;
    this.speed = speed;
    this.length = x2 - x1;
  }

void drawLine() {
  line(x1, y, x2, y);
  }
}

시간의 불가역성의 시각적 재해석

랜덤한 위치, 길이, 속도로 선을 생성하여 이동시키면
사용자가 워치페이스에서 같은 화면을 다시 보게 될 확률은 현저히 줄어든다

또한 한 방향으로 흐르는 선들은
(한 방향으로만 흐르는) 시간의 흐름을 나타내며
하염없이 한 방향을 향해 가는 공룡 애니메이션은
마치 한 방향으로 흐르는 시간 위를
달리는 (실제로 달리지는 않지만) 이미지를 연상케 한다.

void fakeTiming2() {
  // 하루를 분으로 환산
  fsec+=100;
  if(fsec > 59){
    fmin++;
    fsec = 0;
  }
}

  fakeTiming2();
  String time2 = nf(fhour * 60 + fmin, 4);

'분'으로 환산

또한 공룡 게임과 같이
우측 상단에
지금까지 흐른 시간이 분으로 환산되어
매 분마다 갱신된다.
이를 통해 사용자는 하루 1,440분 중
지금까지 흘러 간 시간과
앞으로 남은 시간을 직관적으로 확인 할 수 있다.

배터리 잔량 확인

이 외에도 사용자는 좌측 상단의
하트와 그 바로 옆의 숫자를 통해
배터리 잔량을 확인할 수 있다.

배터리가 30% 이하로 떨어질 시
공룡이 앉아서 숨을 고르는 듯한
애니메이션이 연출되어
사용자로 하여금 약간의 재미와
공룡에게 친밀감을 느낄 수 있도록 한다.

 //배터리
 
  boolean lowBattery = false; 
  fakeBattery();
  textSize(35);
    
   if (battery >= 70){
     fill(20);
     text(battery, 525, 250);
        image(onehundred, 510, 240, imageSize * 0.1, imageSize * 0.1);
        image(img[i], 210, 500 - img[i].height/2,
      img[i].width * scaleFactor * 1.3, img[i].height * scaleFactor * 1.3); // 공룡 위치, 크기
      i += dir;
      if(i == 0 || i == img.length - 1)
      dir *= -1; 
   }else if (battery >30 && battery < 70) {
     fill(10);
     text(battery, 525, 250); 
     image(img[i], 210, 500 - img[i].height/2,
      img[i].width * scaleFactor * 1.3, img[i].height * scaleFactor * 1.3); // 공룡 위치, 크기
      i += dir;
      if(i == 0 || i == img.length - 1)
      dir *= -1; 
      image(sixty, 510, 240, imageSize * 0.1, imageSize * 0.1);
  }else if (lowBattery) {// 배터리 잔량이 20% 이하인 경우 앉아서 쉬는 공룡 이미지 표시
    fill(10);
    text(battery, 525, 250);
    image(sleeping[j], 210, 500 - sleeping[j].height/2,
      sleeping[j].width * scaleFactor * 1.3, sleeping[j].height * scaleFactor * 1.3); // 공룡 위치, 크기
    j += desc;
    if(j == 0 || j == sleeping.length - 1)
      desc *= -1; 
    image(twenty, 510, 240, imageSize * 0.1, imageSize * 0.1);
    if (battery == 0){
      image(zero, 510, 240, imageSize * 0.1, imageSize * 0.1); 
    }
  }

공룡 모션 구현

기본적으로 공룡 이미지는
구글링하여 가져온 이미지의
누끼를 따서 사용하였고,
이 외에 공룡의 자연스러운 걸음 걸이, 앉아서 숨을 고르는 듯한
애니메이션 효과를 주기 위해
필요한 프레임은 직접 포토샵을 이용하여
공룡 이미지를 그려 넣었다.

String[] imgs = {"dino.png", "dino2.png",  "dino3.png", "dino4.png", "dino5.png"};
PImage[] img = new PImage[imgs.length];
void setup(){
  i = 0;
  dir = 1;

  for(int j = 0; j < 5; j++)
    img[j] = loadImage(imgs[j]);
}

void draw() {
	if (battery > 70){
      img[i].width * scaleFactor * 1.3, img[i].height * scaleFactor * 1.3); // 공룡 위치, 크기
      i += dir;
      if(i == 0 || i == img.length - 1)
     `dir *= -1; 
    }
}

PImage[] img = new PImage[imgs.length];를 통해 imgs 배열의 길이와 같은 크기로 초기화한 후
for문을 통해 imgs 배열에 저장된 이미지 파일들을 불러와 img 배열에 저장한다.

그 후 draw() 함수를 통해 이미지 애니메이션을 제어하는데,
if(i == 0 || i == img.length - 1)
i가 이미지 배열의 시작 또는 끝에 도달했을 때
dir *= -1; 부호를 반전시켜
i가 이미지 배열의 시작 또는 끝에 도달하면
방향을 반대로 바꾸어 애니메이션이 반복되도록 하는 로직을 구현하였다.

배터리가 30% 이하로 떨어졌을 때의 애니메이션 역시
동일한 로직을 통해 구현하였다.

시간의 변화에 따른 배경 변화


시간의 변화에 따라 해, 달, 별등의 이미지나
배경 색과 그에 맞는 폰트 색의 변화를 주었다.

void setup(){
 for (int i = 0; i < starCount; i++) {
    float x = random(width);
    float y = random(70, 320);
    float speed = random(7, 12);
    Star star = new Star(x, y, speed);
    stars.add(star);
  }
}


void draw() {
//일출 및 일몰 시간에 태양(반원) 그리기
  if (fhour >=5 && fhour < 6 || fhour >= 17 && fhour < 18 ) { // 일출 | 일몰 시간
   image(sunset, 400, 523, imageSize, imageSize);
  }if ((fhour >= 5 && fhour < 18) == false) { // 밤에 달 그리기
   image(moon, 290, 255, imageSize * 0.3, imageSize * 0.3);
  }
  
    // 폰트 색깔
  if ((fhour >= 5 && fhour < 18) == false ){ // 밤
    textSize(85);
    fill(110);
    text(time, 280, 635);
    textSize(35); // 분으로 환산
    fill(110);
    text(time2, 590, 250);
  }else if (fhour >= 5 && fhour < 6 ||fhour >= 17 && fhour < 18){ // 일출 | 일몰
    textSize(85);
    fill(237, 227, 179);
    text(time, 280, 635);
    textSize(35); // 분으로 환산
    fill(237, 227, 179);
    text(time2, 590, 250);
  } else { // 낮
    textSize(85);
    fill(70);
    text(time, 280, 635);
    textSize(35); // 분으로 환산
    fill(70);
    text(time2, 590, 250);
  }
  
  void WatchFace(){
  //시간에 따른 배경색 변화
  if (fhour >= 5 && fhour < 6 || fhour >= 17 && fhour < 18){ // 일출 | 일몰
    fill(197, 74, 33); // 주황색
    stroke(197, 74, 33);
  }else if (fhour >= 6  && fhour < 17 ){ // 낮
    fill(225); // 흰색
    stroke(225);
  }else{ // 밤
    fill(14, 42, 79);
    stroke(14, 42, 79);
  
  }
  
  void moveStars() {
  for (Star star : stars) {
    star.x -= star.speed;
    if (star.x < 0) {
      star.x = width;
      star.y = random(70, 320);
      star.speed = random(7, 12);
    }
  }
}

void drawStars() {
  noStroke();
  fill(255, 210,128);
  for (Star star : stars) {
    ellipse(star.x, star.y, 5, 5);
  }
}

class Star {
  float x;
  float y;
  float speed;

  Star(float x, float y, float speed) {
    this.x = x;
    this.y = y;
    this.speed = speed;
  }
}

}

날씨의 변화에 따른 효과

날씨 이모티콘과 그에 맞는 옷차림을 입혀
귀여운 애니메이션을 연출하였다.

switch (state) {
   case 1: //해
      image(sun, 190, 320, imageSize * 0.3, imageSize * 0.3);
      image(sunglass, 238, 420, imageSize * 0.2, imageSize * 0.05);
      break;
   case 2: //비
      image(umbrella, 250, 415, imageSize * 0.4, imageSize * 0.6);
      image(rainy_cloud, 190, 320, imageSize * 0.4, imageSize * 0.4);
      break;
    case 3: //눈
      image(muffler, 220, 470, imageSize * 0.1, imageSize * 0.2);
      image(snow, 190, 320, imageSize * 0.4, imageSize * 0.4);
      break;
    // case 0: // 0인 경우에는 아무 이미지도 표시하지 않음
  
}
} 

void mouseClicked() {
  // 클릭할 때마다 케이스 변경
  state = (state + 1) % 4; 
}

날씨 case에 따라 아이콘과 옷차림에 변화를 주기 위해
switch-case문을 사용하였다.

날씨 데이터는 fake data를 사용하였기에
마우스 클릭을 통해 효과를 줄 수 있도록 구현하였다.

Fake Data

void fakeTiming() {
  // 시간을 가짜 데이터로 초기화
  fsec+=100;
  if(fsec > 59){
    fmin++;
    fsec = 0;
  }
  if(fmin > 59){
    fhour++;
    fmin = 0;
}
  if(fhour > 23){
    fhour = 0;
  }
}

void fakeBattery() {
  // 배터리를을 가짜 데이터로 초기화
  if(battery > 30 ){
    battery -= 0.1;
    lowBattery = false;
  }else { // 배터리 잔량 30% 이하일 때
     battery -= 0.1;
    lowBattery = true; 
    if(battery == 0){
      battery = 100;
   }
 }
}

Processing 자체적으로 시간 데이터를 가져올 수 있는 함수가 있긴 하지만
배터리나 날씨 등의 real data를 가져오기 위해
해당 데이터에 맞는 API를 이용할 수 있는 기능은 없다.
따라서 관련 데이터들은 자체적으로 fake data를 만들어 사용하였다.

0개의 댓글