모바일앱개발 수업을 들으며
Processing
을 이용하여 WatchFace
를 디자인하고 구현하는
프로젝트를 하게 되었다.
먼저, 어떤 Watch Face 디자인을 할 것인지에 앞서
시간에 대해 정의할 필요가 있다.
시간
1. 어떤 시각에서 어떤 시각과의 사이
2. 어떤 행동을 할 틈
무슨 뜻인지 언뜻 이해는 되지만 너무 추상적이다.
그래서 나는 시간의 정의 그 자체보다
시간의 특성에 초점을 맞추기로 했다.
내가 주목한 시간의 특성은
시간은 한 방향
으로만 흐르고
한 번 흘러간 시간은 되돌릴 수 없다
는
시간의 불가역성
이다.
한 방향으로만 흐르는 시간의 특성은
마치 한 방향으로만 흐르는 시간 위를 달리는
이미지로
시각화할 수 있겠다고 생각했다
또한 시간은 되돌릴 수 없기에
지금 이 순간은 인생에 단 한 번뿐
이다
이러한 아이디어를 바탕으로
워치페이스에 이러한 시간의 특성을 접목시켜
사용자가 같은 화면을 다시 보게 될 확률을 줄이면서
시간 위를 달리는
듯한 모습으로
시간을 시각화하는 것을 기획하였다.
앞서 생각한 기획의도에 적합하다고 떠올린 것은
모두 한 번쯤은 해봤을 크롬의 다이노 게임
이다.
게임의 원리는 간단하다
공룡은 장애물을 피해 오직 앞으로만 이동
하고
이동한만큼의 거리가 실시간으로 우측 상단의 점수로 갱신된다.
한 방향
으로만 이동하는 공룡은
한 방향
으로만 흐르는 시간의 특성과 매칭된다.
그렇다면
사용자가 워치페이스를 차면서
같은 화면을 보게 될 확률은 어떻게 줄일 수 있을까?
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를 사용하였기에
마우스 클릭을 통해 효과를 줄 수 있도록 구현하였다.
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
를 만들어 사용하였다.