`#include “Example.h”
struct MyExample : tvgexam::Example { tvg::Shape* hourHand = nullptr; tvg::Shape* minHand = nullptr; tvg::Shape* secHand = nullptr; tvg::Scene* pendulum = nullptr;
static constexpr float PI = 3.141592f;
bool content(tvg::Canvas* canvas, uint32_t w, uint32_t h) override
{
//background
auto bg = tvg::Shape::gen();
bg->appendRect(0, 0, w, h);
bg->fill(250, 248, 240);
canvas->add(bg);
float clockCx = float(w) * 0.5f;
float clockCy = float(h) * 0.28f;
float clockR = (w < h ? float(w) : float(h)) * 0.22f;
//clock case
auto face = tvg::Shape::gen();
face->appendCircle(clockCx, clockCy, clockR, clockR);
face->fill(255, 255, 255);
face->strokeFill(60, 40, 20);
face->strokeWidth(6);
canvas->add(face);
//hour tick marks
for (int i = 0; i < 12; i++) {
float angle = float(i) * 30.0f * (PI / 180.0f);
float x1 = clockCx + sinf(angle) * (clockR - 8);
float y1 = clockCy - cosf(angle) * (clockR - 8);
float x2 = clockCx + sinf(angle) * (clockR - 20);
float y2 = clockCy - cosf(angle) * (clockR - 20);
auto tick = tvg::Shape::gen();
tick->moveTo(x1, y1);
tick->lineTo(x2, y2);
tick->strokeFill(60, 40, 20);
tick->strokeWidth(3);
canvas->add(tick);
}
//hour
hourHand = tvg::Shape::gen();
hourHand->moveTo(0, 0);
hourHand->lineTo(0, -clockR * 0.45f);
hourHand->strokeFill(30, 30, 30);
hourHand->strokeWidth(6);
hourHand->strokeCap(tvg::StrokeCap::Round);
hourHand->translate(clockCx, clockCy);
canvas->add(hourHand);
//minute
minHand = tvg::Shape::gen();
minHand->moveTo(0, 0);
minHand->lineTo(0, -clockR * 0.7f);
minHand->strokeFill(30, 30, 30);
minHand->strokeWidth(4);
minHand->strokeCap(tvg::StrokeCap::Round);
minHand->translate(clockCx, clockCy);
canvas->add(minHand);
//second
secHand = tvg::Shape::gen();
secHand->moveTo(0, 0);
secHand->lineTo(0, -clockR * 0.85f);
secHand->strokeFill(200, 30, 30);
secHand->strokeWidth(2);
secHand->strokeCap(tvg::StrokeCap::Round);
secHand->translate(clockCx, clockCy);
canvas->add(secHand);
//center pin
auto pin = tvg::Shape::gen();
pin->appendCircle(clockCx, clockCy, 6, 6);
pin->fill(30, 30, 30);
canvas->add(pin);
float pivotX = clockCx;
float pivotY = clockCy + clockR + 10;
float pendLen = clockR * 1.6f;
pendulum = tvg::Scene::gen();
auto rod = tvg::Shape::gen();
rod->moveTo(0, 0);
rod->lineTo(0, pendLen);
rod->strokeFill(120, 120, 120);
rod->strokeWidth(4);
pendulum->add(rod);
auto bob = tvg::Shape::gen();
bob->appendCircle(0, pendLen, 20, 20);
bob->fill(200, 160, 40);
bob->strokeFill(120, 90, 20);
bob->strokeWidth(2);
pendulum->add(bob);
pendulum->translate(pivotX, pivotY);
canvas->add(pendulum);
return true;
}
bool update(tvg::Canvas* canvas, uint32_t elapsed) override
{
float t = float(elapsed) * 0.001f;
pendulum->rotate(20.0f * sinf(t * 2.0f));
secHand->rotate(fmodf(t * 60.0f, 360.0f)); //1 turn / 6 sec
minHand->rotate(fmodf(t * 6.0f, 360.0f)); //1 turn / 60 sec
hourHand->rotate(fmodf(t * 0.5f, 360.0f)); //1 turn / 720 sec
canvas->update();
return true;
}
};
int main(int argc, char **argv) { return tvgexam::main(new MyExample, argc, argv, true, 400, 600); }`
content()에서 도형을 한 번만 생성해 캔버스에 등록하고, update()에서 매 프레임 각도만 갱신하는 tvgexam::Example의 애니메이션 패턴을 사용했습니다.
Paint::rotate()는 항상 도형 로컬 좌표의 (0,0)을 축으로 회전한다는 점을 이용해, 시침/분침/초침을 원점에서 위로 뻗은 모양으로 그린 뒤 translate()로 시계 중심 위치로 옮겼습니다. 이렇게 하면 rotate() 호출만으로 시계 중심을 축으로 도는 바늘을 만들 수 있습니다.
추(진자)는 막대와 원(bob) 두 도형을 하나의 Scene으로 묶어, 같은 축을 기준으로 함께 회전하도록 했습니다.
추의 왕복 운동은 20.0f * sinf(t * 2.0f)로 구현했습니다. sin 함수가 -11 사이를 부드럽게 오가는 성질을 이용해 -20도+20도 사이를 계속 왕복시켜, 실제 진자의 조화진동에 가까운 움직임을 만들었습니다.
시침/분침/초침은 실제 시:분:초 비율(60배속 관계)은 유지하되, 데모로 빠르게 보이도록 속도를 임의로 올려서 애니메이션했습니다. 도형 속성을 바꾼 뒤에는 반드시 canvas->update()를 호출해야 변경 사항이 화면에 반영됩니다.
처음에는 회전축을 Picture 전용 API인 origin()으로 잡으려다 컴파일 에러(has no member named ‘origin’)가 나서, API 문서를 다시 확인해 translate() + 로컬 좌표 방식으로 수정했습니다.
https://github.com/user-attachments/assets/db0fcbec-041c-4c50-957f-63de4afb99df https://github.com/mnchn-prog/thorvg.example
댓글
Discussion 원문