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 CustomPaint
và CustomPainter
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 paint
và shouldRepaint
trả về kiểu bool
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <span class="token keyword">class</span> <span class="token class-name">MyCustomPainter</span> <span class="token keyword">extends</span> <span class="token class-name">CustomPainter</span> <span class="token punctuation">{</span> <span class="token function">MyCustomPainter</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>banKinh<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// bán kính đường tròn</span> <span class="token keyword">final</span> double banKinh<span class="token punctuation">;</span> <span class="token metadata symbol">@override</span> <span class="token keyword">void</span> <span class="token function">paint</span><span class="token punctuation">(</span>Canvas canvas<span class="token punctuation">,</span> Size size<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// tọa độ tâm đường tròn cũng chính là tâm của Widget (size.width / 2, size.height / 2)</span> <span class="token keyword">final</span> toaDoTamDuongTron <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span>size<span class="token punctuation">.</span>width <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">,</span> size<span class="token punctuation">.</span>height <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// paint: giống như chọn style cho cái hình mình vẽ: nó màu gì,...</span> <span class="token keyword">final</span> paint <span class="token operator">=</span> <span class="token function">Paint</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token punctuation">.</span>color <span class="token operator">=</span> Colors<span class="token punctuation">.</span>pink <span class="token comment">// vẽ màu hồng</span> <span class="token punctuation">.</span><span class="token punctuation">.</span>style <span class="token operator">=</span> PaintingStyle<span class="token punctuation">.</span>stroke <span class="token comment">// chỉ vẽ viền</span> <span class="token punctuation">.</span><span class="token punctuation">.</span>strokeWidth <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span> <span class="token comment">// độ dày viền bằng 2</span> <span class="token comment">// vẽ đường tròn</span> canvas<span class="token punctuation">.</span><span class="token function">drawCircle</span><span class="token punctuation">(</span>toaDoTamDuongTron<span class="token punctuation">,</span> banKinh<span class="token punctuation">,</span> paint<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token metadata symbol">@override</span> bool <span class="token function">shouldRepaint</span><span class="token punctuation">(</span>MyCustomPainter oldDelegate<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> banKinh <span class="token operator">!=</span> oldDelegate<span class="token punctuation">.</span>banKinh<span class="token punctuation">;</span> <span class="token comment">// nếu bán kính truyền vào bị thay đổi thì mới cho vẽ lại.</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Canvas
là cái bảng vẽ trắng tinh để chúng ta vẽ đủ thứ trên đời vào đó. ClassCanvas
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àmdrawCircle
của nó để vẽ đường tròn đó.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.0
vàheight = 200.0
.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- 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àmpaint
. Nếu returntrue
thì nó sẽ vẽ lại, returnfalse
thì không vẽ lại Offset
truyền vào hoành độ x, tung độ y. Một objectOffset
đạ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ộtvector
trong Toán.- 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) và 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.
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">class</span> <span class="token class-name">MyCircleWidget</span> <span class="token keyword">extends</span> <span class="token class-name">StatelessWidget</span> <span class="token punctuation">{</span> <span class="token metadata symbol">@override</span> Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">CustomPaint</span><span class="token punctuation">(</span> painter<span class="token punctuation">:</span> <span class="token function">MyCustomPainter</span><span class="token punctuation">(</span>banKinh<span class="token punctuation">:</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">,</span> size<span class="token punctuation">:</span> <span class="token function">Size</span><span class="token punctuation">(</span><span class="token number">300</span><span class="token punctuation">,</span> <span class="token number">300</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Widget CustomPaint
có 3 property quan trọng là:
painter
: truyền vào mộtCustomPainter
như đã tạo ở trênchild
: truyền vào một Widget con của WidgetCustomPaint
nàysize
: Khi truyền objectSize
vào paramsize
thì objectMyCustomPainter
sẽ nhận được trong hàmvoid paint(Canvas canvas, Size size)
. Chú ý là nếu WidgetCustomPaint
này cóchild
thì cái objectSize
này trở nên vô nghĩa vì Flutter sẽ sử dụng size của Widgetchild
thay cho objectSize
được chúng ta truyền vào biếnsize
.
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 CustomPaint
và CustomPainter
, 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
, height
và r
(bán kính) trong hàm paint
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token keyword">class</span> <span class="token class-name">VietNamFlagPainter</span> <span class="token keyword">extends</span> <span class="token class-name">CustomPainter</span> <span class="token punctuation">{</span> <span class="token metadata symbol">@override</span> <span class="token keyword">void</span> <span class="token function">paint</span><span class="token punctuation">(</span>Canvas canvas<span class="token punctuation">,</span> Size size<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> double width <span class="token operator">=</span> size<span class="token punctuation">.</span>width<span class="token punctuation">;</span> <span class="token comment">// chiều rộng của lá cờ</span> <span class="token keyword">final</span> double height <span class="token operator">=</span> <span class="token number">2</span> <span class="token operator">*</span> width <span class="token operator">/</span> <span class="token number">3</span><span class="token punctuation">;</span> <span class="token comment">// chiều cao của lá cờ bằng 2 / 3 chiều rộng</span> <span class="token keyword">final</span> double r <span class="token operator">=</span> width <span class="token operator">/</span> <span class="token number">5</span><span class="token punctuation">;</span> <span class="token comment">// bán kính đường tròn ngoại tiếp ngôi sao</span> <span class="token punctuation">}</span> <span class="token metadata symbol">@override</span> bool <span class="token function">shouldRepaint</span><span class="token punctuation">(</span>CustomPainter oldDelegate<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 7 | <span class="token keyword">final</span> Paint redPaint <span class="token operator">=</span> <span class="token function">Paint</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// tạo paint màu đỏ để vẽ cờ đỏ</span> <span class="token punctuation">.</span><span class="token punctuation">.</span>color <span class="token operator">=</span> Colors<span class="token punctuation">.</span>red <span class="token punctuation">.</span><span class="token punctuation">.</span>style <span class="token operator">=</span> PaintingStyle<span class="token punctuation">.</span>fill<span class="token punctuation">;</span> <span class="token comment">// tô màu hết cả shape</span> <span class="token comment">// vẽ hình chữ nhật full cái Widget CustomPaint luôn</span> canvas<span class="token punctuation">.</span><span class="token function">drawRect</span><span class="token punctuation">(</span>Rect<span class="token punctuation">.</span><span class="token function">fromLTRB</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> width<span class="token punctuation">,</span> height<span class="token punctuation">)</span><span class="token punctuation">,</span> redPaint<span class="token punctuation">)</span><span class="token punctuation">;</span> |
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(r∗sin(72°),r∗cos(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(r∗sin(36°),−r∗cos(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.
1 2 3 4 5 6 7 8 | <span class="token keyword">import</span> <span class="token string">'dart:math'</span><span class="token punctuation">;</span> <span class="token comment">// cần phải import thư viện dart:math</span> extension NumberUtil on num <span class="token punctuation">{</span> num <span class="token function">toRadian</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">this</span> <span class="token operator">*</span> pi <span class="token operator">/</span> <span class="token number">180</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
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=xCalculate và yFlutter=−yCalculatey Flutter = -y CalculateyFlutter=−yCalculate trong đó xCalculatexCalculatexCalculate và yCalculateyCalculateyCalculate là tọa độ mình vừa tính toán tìm được lúc nảy còn xFlutterxFlutterxFlutter và yFlutteryFlutteryFlutter là tọa độ mình viết code trong Flutter.
1 2 3 4 5 6 7 8 | <span class="token keyword">final</span> pointA <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token operator">-</span>r<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// vì lúc nảy mình tính được tọa độ A (0, r)</span> <span class="token comment">// Tọa độ B tính toán được là B (r * sin(72°), r * cos(72°)) nên trong code là</span> <span class="token keyword">final</span> pointB <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span>r <span class="token operator">*</span> <span class="token function">sin</span><span class="token punctuation">(</span><span class="token number">72.</span><span class="token function">toRadian</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token operator">-</span>r <span class="token operator">*</span> <span class="token function">cos</span><span class="token punctuation">(</span><span class="token number">72.</span><span class="token function">toRadian</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Tọa độ C tính toán được là C (r * sin(36°), -r * cos(36°)) nên trong code là</span> <span class="token keyword">final</span> pointC <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span>r <span class="token operator">*</span> <span class="token function">sin</span><span class="token punctuation">(</span><span class="token number">36.</span><span class="token function">toRadian</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> r <span class="token operator">*</span> <span class="token function">cos</span><span class="token punctuation">(</span><span class="token number">36.</span><span class="token function">toRadian</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">final</span> pointD <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span><span class="token operator">-</span>pointC<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointC<span class="token punctuation">.</span>dy<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// đối xứng với C qua trục tung</span> <span class="token keyword">final</span> pointE <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span><span class="token operator">-</span>pointB<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointB<span class="token punctuation">.</span>dy<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// đối xứng với B qua trục tung</span> |
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 đó.
1 2 3 4 5 6 7 8 | <span class="token keyword">final</span> Path path <span class="token operator">=</span> <span class="token function">Path</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token function">moveTo</span><span class="token punctuation">(</span>pointA<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointA<span class="token punctuation">.</span>dy<span class="token punctuation">)</span> <span class="token comment">// đưa ngòi bút đến điểm A</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token function">lineTo</span><span class="token punctuation">(</span>pointC<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointC<span class="token punctuation">.</span>dy<span class="token punctuation">)</span> <span class="token comment">// vẽ 1 line từ A đến C</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token function">lineTo</span><span class="token punctuation">(</span>pointE<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointE<span class="token punctuation">.</span>dy<span class="token punctuation">)</span> <span class="token comment">// vẽ 1 line từ C đến E</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token function">lineTo</span><span class="token punctuation">(</span>pointB<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointB<span class="token punctuation">.</span>dy<span class="token punctuation">)</span> <span class="token comment">// vẽ 1 line từ E đến B</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token function">lineTo</span><span class="token punctuation">(</span>pointD<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> pointD<span class="token punctuation">.</span>dy<span class="token punctuation">)</span> <span class="token comment">// vẽ 1 line từ B đến D</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// vẽ 1 line từ D đến A tạo thành 1 hình khép kín</span> |
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.
1 2 3 4 5 6 | <span class="token keyword">final</span> Paint yellowPaint <span class="token operator">=</span> <span class="token function">Paint</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// tạo paint màu vàng để vẽ sao vàng</span> <span class="token punctuation">.</span><span class="token punctuation">.</span>color <span class="token operator">=</span> Colors<span class="token punctuation">.</span>yellow <span class="token punctuation">.</span><span class="token punctuation">.</span>style <span class="token operator">=</span> PaintingStyle<span class="token punctuation">.</span>fill<span class="token punctuation">;</span> canvas<span class="token punctuation">.</span><span class="token function">drawPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> yellowPaint<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// vẽ ngôi sao vàng theo path</span> |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token comment">// toạ độ tính toán của tâm ngôi sao</span> <span class="token keyword">final</span> pointO <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// toạ độ thật của tâm ngôi sao</span> <span class="token keyword">final</span> pointI <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span>width <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">,</span> height <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// vector tịnh tiến OI</span> <span class="token keyword">final</span> translationVector <span class="token operator">=</span> pointI <span class="token operator">-</span> pointO<span class="token punctuation">;</span> <span class="token comment">// thực hiện phép tịnh tiến cả Path bằng hàm shift</span> <span class="token keyword">final</span> realPath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">shift</span><span class="token punctuation">(</span>translationVector<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// vẽ realPath</span> canvas<span class="token punctuation">.</span><span class="token function">drawPath</span><span class="token punctuation">(</span>realPath<span class="token punctuation">,</span> yellowPaint<span class="token punctuation">)</span><span class="token punctuation">;</span> |
Đâ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