← 과제 목록

[과제] 2주차) Basic Programming 이정민

@mungjin01
  • #과제
목차

무엇을 만들까 고민을 하다가 아날로그 시계를 만들어보기로 결정했다.

시계를 만들기 위해

1단계: 시계판 만들기
2단계: 눈금 만들기
3단계: 침 그리기
4단계: 침 움직이게 하기

이러한 순서로 구현을 했다.

1단계 - 시계판 만들기

시계 테두리가 되어줄 원 하나와 그 안의 실제 시간을 보여주는 원 하나 이렇게 원이 총 2개가 필요하다

// 안쪽 시계
auto dial = tvg::Shape::gen();
dial->appendCircle(CX, CY, R, R);
canvas->add(dial);

// 테두리(베젤) 
auto bezel = tvg::Shape::gen();
bezel->appendCircle(CX, CY, R, R);
bezel->strokeWidth(16.0f);
canvas->add(bezel);

테두리 원에는 그라디언트를 주어서 약간 밝음과 어두움이 반복되어 금속 느낌이 나도록 하였다.

stops[0] = {0.0f,  236, 238, 244, 255};   // 밝음
stops[1] = {0.35f, 120, 126, 140, 255};   // 어두움
stops[2] = {0.7f,  208, 212, 222, 255};   // 밝음
stops[3] = {1.0f,   74,  79,  92, 255};   // 어두움


step1

2단계 - 눈금 만들기

원 둘레에 눈금 60개를 6도 간격으로 배치하는 단계이다. 처음에는 appendRect로 눈금 하나를 만든 뒤 rotate()를 적용하면 될 것이라고 생각했지만 실제로는 눈금이 예상한 위치를 벗어나 화면 밖으로 이동했다.

The rotational axis passes through the point on the object with zero coordinates.

rotate()는 항상 원점 (0,0)을 기준으로 회전해서 생기는 문제였다. 그래서 눈금 좌표 자체를 원점 주위에 정의하면 원점 회전이 곧 제자리 회전이 되고, 마지막에 translate(CX, CY)로 전체를 시계 중심으로 옮기면 된다.

tick->appendRect(
    -half,
    -(R - 30.0f),
    half * 2.0f,
    len,
    half,
    half
);

tick->rotate(i * 6.0f);  // 원점을 기준으로 회전
tick->translate(CX, CY); // 회전 중심을 시계 중심으로 이동


step2

3단계 침 그리기

시계침은 약간 뾰족한 느낌이 있어야 할 것 같아서 폴리곤으로 그렸다.

항상 느끼듯 좌표나 이런 것들을 쓰는 것이 가장 어려운 것 같다. 클로드의 도움을 많이 받았다.

  auto hour = tvg::Shape::gen();
        hour->moveTo(-11.0f, 26.0f);
        hour->lineTo(-7.0f, -150.0f);
        hour->lineTo(0.0f, -164.0f);
        hour->lineTo(7.0f, -150.0f);
        hour->lineTo(11.0f, 26.0f);
        hour->close();
        hour->fill(38, 42, 54);
        hour->rotate(305.0f);
        hour->translate(CX, CY);
        canvas->add(hour);

        auto minute = tvg::Shape::gen();
        minute->moveTo(-8.0f, 32.0f);
        minute->lineTo(-5.0f, -218.0f);
        minute->lineTo(0.0f, -234.0f);
        minute->lineTo(5.0f, -218.0f);
        minute->lineTo(8.0f, 32.0f);
        minute->close();
        minute->fill(38, 42, 54);
        minute->rotate(60.0f);
        minute->translate(CX, CY);
        canvas->add(minute);

        auto second = tvg::Shape::gen();
        second->appendCircle(0.0f, 58.0f, 13.0f, 13.0f);
        second->moveTo(0.0f, 70.0f);
        second->lineTo(0.0f, -252.0f);
        second->fill(198, 58, 48);
        second->strokeWidth(4.0f);
        second->strokeCap(tvg::StrokeCap::Round);
        second->strokeFill(198, 58, 48);
        second->rotate(180.0f);
        second->translate(CX, CY);
        canvas->add(second);

        auto hub = tvg::Shape::gen();
        hub->appendCircle(CX, CY, 15.0f, 15.0f);
        hub->fill(38, 42, 54);
        hub->appendCircle(CX, CY, 6.0f, 6.0f);
        hub->fillRule(tvg::FillRule::EvenOdd);
        canvas->add(hub);


step3

4단계 - 침 움직이게 하기

update(canvas, elapsed)는 매 프레임 호출된다. elapsed에는 프로그램이 시작된 뒤 흐른 시간이 밀리초 단위로 들어온다.

float now = origin + elapsed * 0.001f;

origin은 프로그램 시작 시점의 자정 이후 경과 초이고, elapsed * 0.001f는 프로그램 실행 후 흐른 시간을 초로 변환한 값이다.

now에서 시침, 분침, 초침에 필요한 값을 각각 계산했다.

float s = fmodf(now, 60.0f);
float m = fmodf(now / 60.0f, 60.0f);
float h = fmodf(now / 3600.0f, 12.0f);

hour->rotate(h * 30.0f);
minute->rotate(m * 6.0f);
second->rotate(s * 6.0f);

초침과 분침은 한 바퀴가 60칸이므로 한 칸당 6도씩 움직인다. 시침은 한 바퀴가 12칸이므로 한 시간당 30도씩 움직인다.

시간 값을 정수로 자르지 않았기 때문에 초침은 매 프레임 조금씩 움직인다.

float s = fmodf(now, 60.0f);

여기에 floorf()를 적용하면 소수점이 제거되어 초침이 1초마다 6도씩 움직인다.

float s = floorf(fmodf(now, 60.0f));


clock

전체 코드


#include "Example.h"

/************************************************************************/
/* ThorVG Drawing Contents                                              */
/************************************************************************/

struct UserExample : tvgexam::Example
{
    static constexpr float CX = 400.0f;
    static constexpr float CY = 400.0f;
    static constexpr float R = 300.0f;

    tvg::Shape* hour = nullptr;
    tvg::Shape* minute = nullptr;
    tvg::Shape* second = nullptr;

    float origin = 0.0f;

    bool content(tvg::Canvas* canvas, uint32_t w, uint32_t h) override
    {
        auto bg = tvg::Shape::gen();
        bg->appendRect(0, 0, w, h);
        bg->fill(28, 32, 44);
        canvas->add(bg);

        auto dial = tvg::Shape::gen();
        dial->appendCircle(CX, CY, R, R);
        {
            auto fill = tvg::RadialGradient::gen();
            fill->radial(CX, CY, R, CX - 110.0f, CY - 130.0f, 0.0f);

            tvg::Fill::ColorStop stops[3];
            stops[0] = {0.0f, 252, 251, 246, 255};
            stops[1] = {0.65f, 226, 222, 210, 255};
            stops[2] = {1.0f, 178, 172, 158, 255};
            fill->colorStops(stops, 3);

            dial->fill(fill);
        }
        canvas->add(dial);

        auto bezel = tvg::Shape::gen();
        bezel->appendCircle(CX, CY, R, R);
        bezel->strokeWidth(16.0f);
        {
            auto fill = tvg::LinearGradient::gen();
            fill->linear(CX - R, CY - R, CX + R, CY + R);

            tvg::Fill::ColorStop stops[4];
            stops[0] = {0.0f, 236, 238, 244, 255};
            stops[1] = {0.35f, 120, 126, 140, 255};
            stops[2] = {0.7f, 208, 212, 222, 255};
            stops[3] = {1.0f, 74, 79, 92, 255};
            fill->colorStops(stops, 4);

            bezel->strokeFill(fill);
        }
        canvas->add(bezel);

        auto ticks = tvg::Scene::gen();
        for (int i = 0; i < 60; ++i) {
            auto major = (i % 5 == 0);
            auto len = major ? 34.0f : 16.0f;
            auto half = major ? 5.0f : 1.8f;

            auto tick = tvg::Shape::gen();
            tick->appendRect(-half, -(R - 30.0f), half * 2.0f, len, half, half);
            if (major) tick->fill(46, 50, 62);
            else tick->fill(120, 122, 128);

            tick->rotate(i * 6.0f);
            tick->translate(CX, CY);
            ticks->add(tick);
        }
        canvas->add(ticks);

        hour = tvg::Shape::gen();
        hour->moveTo(-11.0f, 26.0f);
        hour->lineTo(-7.0f, -150.0f);
        hour->lineTo(0.0f, -164.0f);
        hour->lineTo(7.0f, -150.0f);
        hour->lineTo(11.0f, 26.0f);
        hour->close();
        hour->fill(38, 42, 54);
        hour->translate(CX, CY);
        canvas->add(hour);

        minute = tvg::Shape::gen();
        minute->moveTo(-8.0f, 32.0f);
        minute->lineTo(-5.0f, -218.0f);
        minute->lineTo(0.0f, -234.0f);
        minute->lineTo(5.0f, -218.0f);
        minute->lineTo(8.0f, 32.0f);
        minute->close();
        minute->fill(38, 42, 54);
        minute->translate(CX, CY);
        canvas->add(minute);

        second = tvg::Shape::gen();
        second->appendCircle(0.0f, 58.0f, 13.0f, 13.0f);
        second->moveTo(0.0f, 70.0f);
        second->lineTo(0.0f, -252.0f);
        second->fill(198, 58, 48);
        second->strokeWidth(4.0f);
        second->strokeCap(tvg::StrokeCap::Round);
        second->strokeFill(198, 58, 48);
        second->translate(CX, CY);
        canvas->add(second);

        auto hub = tvg::Shape::gen();
        hub->appendCircle(CX, CY, 15.0f, 15.0f);
        hub->fill(38, 42, 54);
        hub->appendCircle(CX, CY, 6.0f, 6.0f);
        hub->fillRule(tvg::FillRule::EvenOdd);
        canvas->add(hub);

        auto now = time(nullptr);
        auto lt = localtime(&now);
        origin = lt->tm_hour * 3600.0f + lt->tm_min * 60.0f + lt->tm_sec;

        return update(canvas, 0);
    }

    bool update(tvg::Canvas* canvas, uint32_t elapsed) override
    {
        auto now = origin + elapsed * 0.001f;

        auto s = fmodf(now, 60.0f);
        auto m = fmodf(now / 60.0f, 60.0f);
        auto h = fmodf(now / 3600.0f, 12.0f);

        hour->rotate(h * 30.0f);
        minute->rotate(m * 6.0f);
        second->rotate(s * 6.0f);

        return true;
    }
};

/************************************************************************/
/* Entry Point                                                          */
/************************************************************************/

int main(int argc, char **argv)
{
    return tvgexam::main(new UserExample, argc, argv);
}

과제링크

https://github.com/mungjin01/thorvg.example/tree/example/analog-clock

댓글

Discussion 원문