Animation workshop : นาฬิกาติดผนัง (ตอนที่ 2)

1. วิเคราะห์การหมุนของเข็มนาฬิกา


การหมุนของเข็มนาฬิกาเป็นงานในส่วนของ animation เพราะการวาดเข็มจะเปลี่ยนไปเมื่อเวลาเปลี่ยน การหมุนของเข็มนาวินาที,นาทีและชั่วโมงใช้หลักการเดียวกัน ต่างกันที่ความเร็วเเชิงมุม การวิเคราะห์นี้จะช่วยทำให้เราทราบว่าในแต่ละหน่วยเวลาจะต้องทำการวาดเข็มนาฬิกาอย่างไร


รูปที่ 1

การหมุนของเข็มวินาที

การหมุน 1 รอบหมายถึงเวลาเดินไป 60 วินาทีหรือ 1 นาที คิดเป็นค่าของมุมคือ \( 2\pi \) ดังนั้นเวลา 1 วินาทีคิดเป็นค่าของมุมเป็น

\[ \text{angle of one second } = \frac{2\pi}{60} = \frac{\pi}{30} \tag{1.0} \]

การหมุนของเข็มนาที

เหมือนกับการหมุนของเข็มวินาที แต่เปลี่ยนหน่วยเวลา 1 รอบหมายถึงเวลาเดินไป 60 นาทีหรือ 1 ชั่วโมง

\[ \text{angle of one minute } = \frac{2\pi}{60} = \frac{\pi}{30} \tag{1.1} \]

การหมุนของเข็มชั่วโมง

การหมุน 1 รอบหมายถึงเวลาเดินไป 12 ชั่วโมง คิดเป็นค่าของมุมคือ \( 2\pi \) ดังนั้นเวลา 1 ชั่วโมงคิดเป็นค่าของมุมเป็น

\[ \text{angle of one hour } = \frac{2\pi}{12} = \frac{\pi}{6} \tag{1.2} \]

2. การแปลงค่าของมุมไปสู่ coordinates


ขั้นตอนนี้เป็นการหา coordinates ที่จะนำไปใช้เป็นจุดปลายในการวาดเข็มนาฬิกาทั้ง 3 เส้น (ดูรายละเอียดจากตอนที่ 1) จาก (1.0),(1.1) และ (1.2) บอกถึงการได้มาซึ่งมุมของแต่ละเข็มที่จะกวดไปเมื่อเวลาผ่านไป 1 หน่วยเวลา จะนำค่าเหล่านี้ไปใช้งานได้ต้องทำการเปลี่ยนให้เป็น coordinates ที่ตรงตามชุดคำสั่งต้องการก่อน โดยอาศัยการแปลงจากระบบ polar coordinates ไปสู่่ cartesian coordinates

ถ้า \( r \) แทนความยาวของเข็มนาฬิกาที่ต้องการคำนวณ และ \(\theta \) ดังนี้

\[ \begin{align*} x &= rcos(\theta) \\ y &= rsin(\theta) \\ \end{align*} \]

หรือ ถ้าให้ t คือเวลาที่ผ่านไป เช่น 5 วินาที, 10 นาที จะได้

\[ \begin{align*} x_t &= rcos(t\theta) \tag{1.3} \\ y_t &= rsin(t\theta) \tag{1.4}\\ \end{align*} \]

แต่ raylib การเริ่มนับมุมที่ 0 radian เริ่มจากเส้นแนวนอน (X+ axis) แล้ววัดมุมตามเข็มนาฬิกา ในขณะที่การนับเวลาจะเริ่มนับจากแนวตั้ง (Y+ axis) ดังนั้นค่ามุมที่คำนวณได้ ก่อนนำไปใช้คำนวณต่อต้องนำเอา \( \frac{\pi}{2}\) ไปลบออกเสียก่อน

ดังนั้น (1.3),(1.4) จะต้องเชียนใหม่เป็น

\[ \begin{align*} x_t &= rcos(t\theta - \frac{\pi}{2}) \tag{1.5} \\ y_t &= rsin(t\theta - \frac{\pi}{2}) \tag{1.6}\\ \end{align*} \]
รูปที่ 2

เมื่อนำเอาค่ามุมของ 1 หน่วยเวลาของวินาที,นาทีและชั่วโมงมาแทนค่า สามารถสรุปเป็นสูตรคำนวณสำหรับตำแหน่งของเข็มทั้งสามดังนี้

  » coordinate ของปลายเข็มวินาที

\[ \begin{align*} x_s &= rcos(s \cdot \frac{\pi}{30} - \frac{\pi}{2}) \tag{1.7} \\ y_s &= rsin(s \cdot \frac{\pi}{30} - \frac{\pi}{2}) \tag{1.8}\\ \end{align*} \]

  » coordinate ของปลายเข็มนาที

\[ \begin{align*} x_m &= rcos(m \cdot \frac{\pi}{30} - \frac{\pi}{2}) \tag{1.9} \\ y_m &= rsin(m \cdot \frac{\pi}{30} - \frac{\pi}{2}) \tag{1.10}\\ \end{align*} \]

  » coordinate ของปลายเข็มชั่วโมง

\[ \begin{align*} x_h &= rcos(h \cdot \frac{\pi}{6} - \frac{\pi}{2}) \tag{1.11} \\ y_h &= rsin(h \cdot \frac{\pi}{6} - \frac{\pi}{2}) \tag{1.12}\\ \end{align*} \]

(1.7) ถึง (1.12) จะยังไม่สามารถนำไปใช้งานได้ ยังขาดขั้นตอนการย้ายตำแหน่ง (translation) เพราะจุดกำเนิดของนาฬิกาไม่ได้อยู่ที่ (0,0) แต่ย้ายไปที่ (cx,cy) ดังนั้นค่า coordinates ที่คำนวณได้ต้องย้ายตำแหน่งตามไปด้วย โดยการนำค่าของ cx และ cy รวมเข้าไปด้วย ดังนี้


\[ \begin{align*} x_s &= rcos(s \cdot \frac{\pi}{30} - \frac{\pi}{2}) + cx \tag{1.13} \\ y_s &= rsin(s \cdot \frac{\pi}{30} - \frac{\pi}{2}) + cy \tag{1.14}\\\\ x_m &= rcos(m \cdot \frac{\pi}{30} - \frac{\pi}{2}) + cx\tag{1.15} \\ y_m &= rsin(m \cdot \frac{\pi}{30} - \frac{\pi}{2}) + cy \tag{1.16}\\\\ x_h &= rcos(h \cdot \frac{\pi}{6} - \frac{\pi}{2}) + cx\tag{1.17} \\ y_h &= rsin(h \cdot \frac{\pi}{6} - \frac{\pi}{2}) + cy\tag{1.18}\\ \end{align*} \]

3. การหาค่าของเวลา และการแปลงค่าไปสู่ coordinates


Raylib ใช้ชุดคำสั่ง SetTargetFPS(fps) เพื่อใช้กำหนดอัตราเร็วในการวาดใน 1 วินาที หมายความว่าสามารถคำนวณเวลาได้อัตราเร็วนี้

\[ \begin{align*} \text{frames rate} &= \frac{\text{number of frames}}{time} \\\\ \therefore time &= \frac{\text{number of frames}}{\text{frames rate}} \tag{2.0}\\ \end{align*} \]

เช่น ถ้ากำหนดให้ fps = 20 และจำนวน frames ที่วาดไปแล้วคือ 1200 frames จะได้เวลาที่ใช้ไปคือ

\[ \begin{align*} \text{seconds} &= \frac{1200}{20} = 60 \\\\ \text{minutes} &= \frac{1}{60} \times \frac{1200}{20} = 10 \\\\ \text{hours} &= \frac{1}{360} \times \frac{1200}{20} = \frac{1}{60} \\\\ \end{align*} \]

ค่าที่ได้จากการคำนวณนี้จะนำไปใช้แทนค่าให้กับตัวแปร s,m,และ h ใน (1.13) - (1.18) ถ้ากำหนดให้ cx = 100, cy = 100 และ r = 10 จะได้

\[ \begin{align*} x_s &= 10 \cdot cos(60 \cdot \frac{\pi}{30} - \frac{\pi}{2}) + 100 \\ y_s &= 10 \cdot sin(60 \cdot \frac{\pi}{30} - \frac{\pi}{2}) + 100 \\\\ x_m &= 10 \cdot cos(10 \cdot \frac{\pi}{30} - \frac{\pi}{2}) + 100 \\ y_m &= 10 \cdot sin(10 \cdot \frac{\pi}{30} - \frac{\pi}{2}) + 100 \\\\ x_h &= 10 \cdot cos(\frac{1}{60} \cdot \frac{\pi}{6} - \frac{\pi}{2}) + 100 \\ y_h &= 10 \cdot sin(\frac{1}{60} \cdot \frac{\pi}{6} - \frac{\pi}{2}) + 100 \\\\ x_s &= -10.7 + 100 = 89.3 \\ y_s &= -10.0 + 100 = 90.0\\\\ x_m &= 8.7 + 100 = 108.7\\ y_m &= -5.0 + 100 = 95.0 \\\\ x_h &= 0.087 + 100 = 100.087 \\ y_h &= -10.0 + 100 = 90.0\\\\ \end{align*} \]

นั่นคือการวาดภาพ frame ที่ 1200 หมายถึงเป็นเวลา HH:MM:SS = 00:10:60 หรือ 00:11:00 และจุดปลายของเข็มวินาที,นาทีและชั่วโมงจะเป็น (89,90),(109,95),(100,90) ตามลำดับ


เขียน Code เพื่อทำ animation


เนื้อหาในตอนนี้จะมีผลต่อการปรับแต่ง code จากตอนที่ 1 ใน 3 functions คือ draw_sec_arm(),draw_min_arm() และ draw_hrs_arm() โดยต้องส่งค่า parameter ที่บอกเวลาให้กับทั้ง 3 function เพื่อนำไปคำนวณ coordinates ใหม่



void draw_sec_arm(int tick){
	float thickness = 3.0f;
    float ang = (tick * M_PI/30 - M_PI/2);
    Vector2 start = {(float)cx,(float)cy};
    Vector2 end;
    end.x = (float)(cx + SEC_ARM_LEN * cos(ang));    
    end.y = (float)(cy + SEC_ARM_LEN * sin(ang));
    DrawLineEx(start,end,thickness,RED);
}

void draw_min_arm(int tick){
	float thickness = 7.0f;
    float ang = (tick * M_PI/30 - M_PI/2);
    Vector2 start = {(float)cx,(float)cy};
    Vector2 end;
    end.x = (float)(cx + SEC_MIN_LEN * cos(ang));
    end.y = (float)(cy + SEC_MIN_LEN * sin(ang));
    DrawLineEx(start,end,thickness,BLUE);
}

void draw_hrs_arm(int tick){
	float thickness = 11.0f;
    float ang = (tick * M_PI/6 - M_PI/2);
    Vector2 start = {(float)cx,(float)cy};
    Vector2 end;
    end.x = (float)(cx + SEC_HRS_LEN * cos(ang));
    end.y = (float)(cy + SEC_HRS_LEN * sin(ang));
    DrawLineEx(start,end,thickness,DARKPURPLE);
}
  

cos(),sin() เป็น function ทาง trigonometry และ M_PI เป็นค่าคงที่ของ \( \pi \) ที่ถูกประกาศไว้ใน math.h


4. การแปลงค่าลำดับของ frame ไปเป็นหน่วยของเวลา


ใน workshop เราใช้หน่วยเวลา 3 หน่วยคือ วินาที, นาที และชั่วโมง ดังนั้นค่าลำดับของ frame ที่ได้จะต้องถูกแปลงไปเป็นหน่วยของเวลาทั้ง 3 หน่วย

จาก (2.0) ค่าของตัวแปร time จะมีหน่วยเป็น วินาที เนื่องจากหน่วยของ frame rate คือ frames per second จะได้ว่า

\[ \begin{align*} \text{seconds} &= \frac{\text{number of frames}}{\text{frames rate}} \\\\ \text{minutes} &= \frac{1}{60} \times \text{seconds} \\\\ \text{hours} &= \frac{1}{60} \times \text{minutes}\\\\ \end{align*} \]

จะเห็นได้ว่าความสัมพันธ์ระหว่างค่าของเวลาทั้ง 3 หน่วยมีลักษณะเดียวกับการทำงานของเฟืองทด


รูปที่ 3

  เมื่อจำนวน frame ที่วาดไปแล้วมีจำนวนเท่ากับ fps ก็จะทำให้ค่าของ second เพิ่มขึ้น 1

  เมื่อจำนวน second มีจำนวนเท่ากับ 60 ก็จะทำให้ค่าของ minute เพิ่มขึ้น 1

  เมื่อจำนวน minute มีจำนวนเท่ากับ 60 ก็จะทำให้ค่าของ hour เพิ่มขึ้น 1


เขียน Code เพื่อคำนวณเวลา



const int FPS = 20;

int second = 0;
int minute = 0;
int hour = 0;
int frame_count = 0;

while(!WindowShouldClose()){
    if(frame_count < FPS){
        frame_count+=1; 
    }else{        
        second+=1;//increase one second
        if(second > 59){
            minute+=1; // increase one minute
            if(minute > 59){            	
                hour+=1; // increase one hour
            }
        }
    }
    ...
}
  

ข้อจำกัดจาก code ข้างบนคือตัวแปร frame_count จะเพิ่มจำนวนไปเรื่อยๆ ตราบที่โปรแกรมยังทำงาน ในทางปฏิบัติแล้วไม่ควรทำ เพราะหน่วนความจำของคอมพิวเตอร์มีจำกัดไม่สามารถเพิ่มค่าได้อย่างไม่มีจำกัด ดังนั้นต้องมีเงื่อนไขในการล้างค่าที่ใช้ไปแล้ว ซึ่งก็คือการเดินทางครบรอบของเข็มนาฬิกา เมื่อเข็มเดินทางครบ 1 รอบก็ทำการล้างค่าที่มีอยู่แล้วเริ่มรอบการทำงานใหม่ code ที่ได้ควรจะเป็น


const int FPS = 20;

int second = 0;
int minute = 0;
int hour = 0;
int frame_count = 0;

while(!WindowShouldClose()){
    if(frame_count < FPS){
        frame_count+=1; 
    }else{
        frame_count = 0; // clean up                
        second+=1;//increase one second
        if(second > 60){
            second =0; //clean up
            minute+=1; //increase one minute
            if(minute > 60){
                minute = 0; //clean up            	
                hour+=1; // increase one hour
                if(hour > 12){
                    hour = 0; //clean up
                }
            }
        }
    }
    ...
}
  

Complete code



#include "math.h"  
#include "raylib.h"

/ constants :

//clock  
const int WIN_WIDTH=400;
const int WIN_HEIGHT=400;  
const int cx = (int)(WIN_WIDTH / 2);
const int cy = (int)(WIN_HEIGHT / 2);

// arms
const int SEC_ARM_LEN = 70;
const int MIN_ARM_LEN = 60;
const int HRS_ARM_LEN = 50;

const int FPS = 20;
int second = 0;
int minute = 0;
int hour = 0;
int frame_count = 0;

// functions
void draw_clock();
void draw_sec_arm(int tick);
void draw_min_arm(int tick);
void draw_hrs_arm(int tick);

int main(void){	
	InitWindow(SCR_WIDTH,SCR_HEIGHT,"Clock");	
	SetTargetFPS(FPS);	
	while(!WindowShouldClose()){
    	if(frame_count < FPS){
        	frame_count+=1; 
    	}else{
        	frame_count = 0; // clean up                
        	second+=1;//increase one second
        	if(second > 60){
            	second =0; //clean up
            	minute+=1; //increase one minute
            	if(minute > 60){
                	minute = 0; //clean up            	
                	hour+=1; // increase one hour
                	if(hour > 12){
                    	hour = 0; //clean up
                	}
            	}
        	}
    	}
    
         ClearBackground(WHITE);
           draw_hrs_arm(hour);				
		   draw_min_arm(minute);	
		   draw_sec_arm(second);		
		   draw_clock();	
		EndDrawing();
    }
    CloseWindow();
    return 0;
}    

void draw_clock(){
    float thickness = 5.0f; // thickness of the ring
    float inrad = 100.0f; // inner radius
    float ourad = inrad + thickness; // outter radius
    float stang = 0.0f; // start angle 
    float enang = 360.0f; // end angle
    float seg = 360.0f; // segments
    
    Vector2 cnt = {(int)cx,(int)cy}; // cast cx,cy to Vector2 
    DrawRing(cnt,inrad,ourad,stang,enang,seg,BLUE); // draw blue clock
    DrawCircle(cx,cy,10,BLACK); // draw black circle with radius 10 pixels on the middle of the clock
}

void draw_sec_arm(int tick){
	float thickness = 3.0f;
    float ang = (tick * M_PI/30 - M_PI/2);
    Vector2 start = {(float)cx,(float)cy};
    Vector2 end;
    end.x = (float)(cx + SEC_ARM_LEN * cos(ang));    
    end.y = (float)(cy + SEC_ARM_LEN * sin(ang));
    DrawLineEx(start,end,thickness,RED);
}

void draw_min_arm(int tick){
	float thickness = 7.0f;
    float ang = (tick * M_PI/30 - M_PI/2);
    Vector2 start = {(float)cx,(float)cy};
    Vector2 end;
    end.x = (float)(cx + MIN_ARM_LEN * cos(ang));
    end.y = (float)(cy + MIN_ARM_LEN * sin(ang));
    DrawLineEx(start,end,thickness,BLUE);
}

void draw_hrs_arm(int tick){
	float thickness = 11.0f;
    float ang = (tick * M_PI/6 - M_PI/2);
    Vector2 start = {(float)cx,(float)cy};
    Vector2 end;
    end.x = (float)(cx + HRS_ARM_LEN * cos(ang));
    end.y = (float)(cy + HRS_ARM_LEN * sin(ang));
    DrawLineEx(start,end,thickness,DARKPURPLE);
}

  



Home work

ค้นคว้าหาวิธีนำเอาค่าของเวลาจากระบบปฏิบัติการมาใช้เป็นค่าเริ่มต้นสำหรับ เพื่อให้นาฬิกานี้สามารถใช้บอกเวลาได้จริง


ความคิดเห็น