Vẽ Quốc kỳ Việt Nam bằng Flutter

Tram Ho

Vấn đề

“Mình học Toán để làm gì vậy anh. Anh làm IT lâu rồi có bao giờ cần sử dụng đến sin cos tan không?”. Thằng em mình đang học lớp 12 hỏi. Biết trả lời nó sao bây giờ. Thú thật, mình cũng ngu toán lắm, suy nghĩ mãi mới nghĩ ra cái đề bài dễ dễ một tí mà mình đã học trong môn đồ họa máy tính để demo cho nó xem. Đề bài cực kỳ quen thuộc trong môn đồ họa máy tính đó là: Vẽ Quốc kỳ Việt Nam bằng Flutter.

Bao giờ cũng thế, để có thể vẽ được một hình phức tạp, chúng ta nên bắt đầu từ những việc nhỏ nhất, vẽ một hình đơn giản nhất.

1. Vẽ một hình đơn giản trong Flutter

Để vẽ một Đường Tròn đơn giản trong Flutter. Ta sử dụng Widget CustomPaintCustomPainter rất đơn giản chỉ với 2 bước:

Bước 1: Tạo một CustomPainter

Tạo 1 class đặt tên là MyCustomPainter kế thừa class CustomPainter, nó sẽ bắt bạn override lại 2 hàm paintshouldRepaint trả về kiểu bool

  1. Canvas là cái bảng vẽ trắng tinh để chúng ta vẽ đủ thứ trên đời vào đó. Class Canvas này cung cấp cho chúng ta các hàm để vẽ. Như ở ví dụ trên mình sử dụng hàm drawCircle của nó để vẽ đường tròn đó.
  2. Size là kích thước của Widget mình định vẽ ra. Ví dụ: Size(300.0, 200.0) thì Widget có width = 300.0height = 200.0.
  3. Paint như một stylist, tạo nên style cho bản vẽ như vẽ màu gì, độ dày nét vẽ, bla bla
  4. Hàm shouldRepaint giúp chúng ta quyết định có nên vẽ lại cái CustomView này hay không. Khi nó vẽ lại nó sẽ chạy lại hàm paint. Nếu return true thì nó sẽ vẽ lại, return false thì không vẽ lại
  5. Offset truyền vào hoành độ x, tung độ y. Một object Offset đại diện cho một điểm trong hệ tọa độ Descartes (Đề – Các) mà các bạn đã học trong Toán ấy. Ngoài ra, nó còn có thể đại diện cho tọa độ của một vector trong Toán.
  6. Khác với hệ tọa độ Đề – Các mà chúng ta được học khi xưa, hệ tọa độ trong Flutter nó quy ước chiều dương của trục tung là hướng xuống và gốc tọa độ mình đặt tên là điểm A (0, 0) sẽ được quy ước ở vị trí đó. Từ điểm A (0, 0) đó, ta dễ dàng suy ra 3 điểm còn lại là B (size.width, 0), C (size.width, size.height)D (0, size.height). Đây là 4 điểm cực kỳ quan trọng giúp chúng ta xác định được tọa độ của các điểm, các hình cần vẽ trong canvas

Bước 2: Tạo ra widget CustomPaint truyền MyCustomPainter vừa tạo vào.

Widget CustomPaint có 3 property quan trọng là:

  1. painter: truyền vào một CustomPainter như đã tạo ở trên
  2. child: truyền vào một Widget con của Widget CustomPaint này
  3. size: Khi truyền object Size vào param size thì object MyCustomPainter sẽ nhận được trong hàm void paint(Canvas canvas, Size size). Chú ý là nếu Widget CustomPaint này có child thì cái object Size này trở nên vô nghĩa vì Flutter sẽ sử dụng size của Widget child thay cho object Size được chúng ta truyền vào biến size.

Full source code: https://dartpad.dev/0605bed20c6410a25f5d91120c865d1c

Bonus thêm cái hình, cái hình này đủ giải thích được đống lý thuyết trên

Chỉ với 2 class CustomPaintCustomPainter, chúng ta đã vẽ được một custom shape. Thật dễ dàng đúng không nào. Nếu tém tém lại thì triệu lời nói ở trên chỉ gói gọn trong bức cái này mà thôi. Nhớ được cái này là cân được tất

Để vẽ một hình phức tạp hơn, ta cần sử dụng đến Path. Bây giờ ta sẽ đi tìm hiểu Path qua bài toán chính của chúng ta hôm nay: Vẽ Quốc kỳ Việt Nam

2. Vẽ Quốc kỳ Việt Nam

Kiếm trên Wiki, được cái ảnh tỷ lệ chuẩn của lá cờ Việt Nam

Okay, vậy đề bài là: chiều dài lá cờ là 3a, chiều rộng là 2a, bán kính đường tròn ngoại tiếp của ngôi sao vàng là 3a / 5 (tức là bằng 1 / 5 chiều dài). Bắt đầu thôi

Việc

Việc cần làm trước tiên là tạo một CustomPainter và khai báo các biến như width, heightr (bán kính) trong hàm paint

Tiếp theo, ta sẽ cần vẽ cái lá cờ đỏ. Tạo một redPaint và dùng hàm drawRect để vẽ hình chữ nhật màu đỏ. Hàm drawRect cần truyền vào một object Rect (hình chữ nhật). Để tạo một object Rect ta sử dụng hàm Rect.fromLTRB(double left, double top, double right, double bottom). Trong đó left, top, right, bottom là khoảng cách tính từ trục hoành hay trục tung như thế này:

Tiếp theo, để vẽ được ngôi sao, ta cần tìm tọa độ của 5 đỉnh A, B, C, D, E. Tí nữa sẽ biết vì sao cần tìm tọa độ

Mặc dù hệ tọa độ trong Flutter nó khác hệ tọa độ Đề – các chút nhưng mình cứ gắn hệ trục tọa độ Đề – các quen thuộc, tức là gốc tọa độ là điểm O (0, 0) và chiều dương của trục tung hướng lên để nó quen thuộc với Toán mình từng học hơn, giải nó nhanh và dễ hơn. Tí nữa mình sẽ sử dụng các phép dời hình để đưa về hệ tọa độ Flutter sau.

Ta có: OA = OB = OC = OD = OE = r nên ta tìm được ngay tọa độ điểm A(0,r)A (0, r)A(0,r)

5 điểm A, B, C, D, E tạo ra 5 dây cung bằng nhau nên tạo ra 5 góc ở tâm bằng nhau. Vì vậy ta có thể tính được AOB^=360°/5=72°widehat{AOB} = 360° / 5 = 72°AOB=360°/5=72°. Từ đó ta tính được ∣xB∣|x_B|xB = FB = OB * sin(72°) và ∣yB∣|y_B|yB = OF = OB * cos(72°). Vì điểm B nằm ở góc phần tư thứ nhất (chiều dương Ox và chiều dương Oy) nên tọa độ điểm B(r∗sin(72°),r∗cos(72°))B (r * sin(72°), r * cos(72°))B(rsin(72°),rcos(72°))

Vì điểm E đối xứng với điểm B qua Oy nên ta tìm được luôn tọa độ điểm E(−xB,yB)E (-x_B, y_B)E(xB,yB)

Chỉ với 1 vài tính toán đơn giản ta đã tìm được tọa độ của 3 điểm A, B và E. Bây giờ ta tiếp tục đi tìm tọa độ của điểm C và D:

Kẻ OG vuông góc với CD => OG vừa là đường cao vừa là tia phân giác của COD^widehat{COD}COD => COG^widehat{COG}COG = 72° / 2 = 36°. Từ đó ta tính được ∣xC∣|x_C|xC = GC = OC * sin(36°) và ∣yC∣|y_C|yC = OG = OC * cos(36°). Vì điểm C nằm ở góc phần tư thứ IV (chiều dương Ox, chiều âm Oy) nên tọa độ điểm C(r∗sin(36°),−r∗cos(36°))C (r * sin(36°), -r * cos(36°))C(rsin(36°),rcos(36°))

Vì điểm D đối xứng với điểm C qua Oy nên ta tìm được tọa độ điểm D(−xC,yC)D (-x_C, y_C)D(xC,yC)

Well, vậy là đã tìm được tọa độ của 5 điểm. Giờ đưa chúng vào code thôi. Vì hàm sin, cos trong thư viện dart:math của Dart sử dụng đơn vị radian nên trước tiên ta cần viết một extension function cho phép chuyển đổi đơn vị độ sang đơn vị radian.

Giờ thì tạo ra 5 object Offset đại diện cho 5 điểm A, B, C, D, E thôi. Chú ý là lúc nảy ta tính toán sử dụng hệ tọa độ có chiều dương của trục tung hướng lên còn hệ tọa độ trong Flutter có chiều dương trục tung hướng xuống nên trong code ta cần lấy các điểm đối xứng với A, B, C, D, E qua trục Ox theo công thức: xFlutter=xCalculatexFlutter = xCalculatexFlutter=xCalculateyFlutter=−yCalculatey Flutter = -y CalculateyFlutter=yCalculate trong đó xCalculatexCalculatexCalculateyCalculateyCalculateyCalculate là tọa độ mình vừa tính toán tìm được lúc nảy còn xFlutterxFlutterxFlutteryFlutteryFlutteryFlutter là tọa độ mình viết code trong Flutter.

Chúng ta thường vẽ ngôi sao bằng cách nối từ A → C, từ C → E, từ E → B, từ B → D và D nối về lại A đúng không nào

Path sẽ giúp chúng ta làm việc đó.

Ban đầu Path sẽ bắt đầu ở điểm M (0, 0) như trong hình dưới. Hàm moveTo(xA, yA), lineTo(xC, yC) đều truyền vào hoành độ và tung độ của một điểm nhưng khác nhau ở chỗ hàm moveTo chỉ di chuyển cây bút từ điểm hiện tại đến điểm được truyền vào (là điểm A) chứ không nối 2 điểm đó lại với nhau còn lineTo là thực hiện nối 2 điểm đó với nhau. Như trong hình lineTo(xC, yC) sẽ nối điểm hiện tại của cây bút là A với điểm C.

Canvas có hàm drawPath để vẽ một Path bất kỳ, miễn ta tìm được tọa độ các điểm trên Path nó đều vẽ được. Vẽ thôi.

Yeah, giờ run app lên ta sẽ nhận được hình ảnh thế này =))

Quên mất, lúc nảy ta tính là giả định tâm ngôi sao trùng với gốc tọa độ O (0, 0), còn thực tế tâm của ngôi sao phải là điểm I (width / 2, height / 2). Ta cần thực hiện phép tịnh tiến theo vectơ OI→overrightarrow{OI}OI bằng hàm shift:

Đây là thành quả:

Full source code: https://dartpad.dev/9f75935302a6e4fb20073cbc3ae95507

Kết luận

Well, lý thuyết của bộ môn “Draw custom shape” trong Flutter chỉ có nhiêu đó thôi. Để có thể vẽ được các hình phức tạp theo yêu cầu của dự án thì quan trọng nhất vẫn là tư duy của chúng ta vận dụng vào các dòng code bên trong hàm paint(Canvas canvas, Size size) của CustomPainter. Bài tiếp theo có thể mình sẽ nâng level vẽ một hình khó hơn, thiết thực, gần gũi với dự án thực tế hơn để lập trình Flutter trở nên bớt nhàm chán. Hẹn các bạn vào bài tiếp theo nhé. Nếu thấy những chia sẻ của mình có giá trị thì click up vote ủng hộ mình nhé. Cảm ơn các bạn

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo