\ Fractals, những hình ảnh bí ẩn tồn tại khắp mọi nơi nhưng không thể nhìn thấy bằng mắt thường. Hôm nay chúng ta sẽ vẽ một trong những Fractals nổi tiếng nhất, chỉ sử dụng Vanilla JS và HTML5 Canvas API. Hãy bắt đầu code!
Để định nghĩa Fractal Tree, trước tiên chúng ta phải biết định nghĩa của Fractal.
Fractals là những mẫu hình vô tận được tạo ra bằng cách lặp lại các phương trình toán học, mà ở bất kỳ tỷ lệ nào, ở bất kỳ mức độ phóng to nào, đều trông gần giống nhau. Nói cách khác, một đối tượng hình học có cấu trúc cơ bản, thô hoặc phân mảnh, lặp lại chính nó ở các tỷ lệ khác nhau.
Vì vậy, nếu chúng ta chia nhỏ một Fractal, chúng ta sẽ thấy một bản sao thu nhỏ của toàn bộ.
Benoit Mandelbrot, người đã đặt ra thuật ngữ Fractal vào năm 1975, đã nói:
\
\ Khá rõ ràng, phải không?
Đây là một số ví dụ:

\ 
Vậy, Fractal Tree là gì?
Hãy tưởng tượng một nhánh cây, và các nhánh mọc ra từ nó, và sau đó hai nhánh mọc ra từ mỗi nhánh, và cứ tiếp tục như vậy... đó chính là hình dạng của Fractal Tree.
Hình dạng của nó bắt nguồn từ tam giác Sierpinski (hoặc miếng đệm Sierpinski).
Như bạn có thể thấy, một hình dạng trở thành hình dạng khác khi thay đổi góc giữa các nhánh:

Hôm nay, chúng ta sẽ tạo ra một hình tương tự như hình dạng cuối cùng của GIF đó.
Trước tiên, đây là sản phẩm cuối cùng (bạn có thể điều chỉnh nó trong quá trình thực hiện):

Bây giờ hãy vẽ nó, từng bước một.
Đầu tiên, chúng ta khởi tạo file index.html với một canvas có kích thước hợp lý và một thẻ script nơi tất cả code JS của chúng ta sẽ được viết.
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <canvas id="my_canvas" width="1000" height="800"></canvas> <script></script> </body> </html>
Sau đó, chúng ta bắt đầu viết JavaScript.
Chúng ta khởi tạo phần tử canvas trong JS, bằng cách truy cập nó thông qua biến myCanvas và tạo ngữ cảnh vẽ 2D với biến ctx (context).
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <canvas id="my_canvas" width="1000" height="800"></canvas> <script> var myCanvas = document.getElementById("my_canvas"); var ctx = myCanvas.getContext("2d"); </script> </body> </html>
Vâng, phương thức getContext thêm các thuộc tính và phương thức cho phép bạn vẽ, trong trường hợp này, là vẽ 2D.
Bây giờ đến lúc suy nghĩ. Làm thế nào chúng ta có thể định nghĩa thuật toán để vẽ một cây Fractal? Hm... 🤔
Hãy xem, chúng ta biết rằng các nhánh cây sẽ ngày càng nhỏ hơn. Và mỗi nhánh kết thúc với hai nhánh mọc ra từ nó, một bên trái và một bên phải.
Nói cách khác, khi một nhánh đủ dài, gắn hai nhánh nhỏ hơn vào nó. Lặp lại.
Nghe có vẻ như chúng ta nên sử dụng một câu lệnh đệ quy ở đâu đó, phải không?
Quay lại code, bây giờ chúng ta định nghĩa hàm fractalTree của mình, hàm này cần ít nhất bốn đối số: tọa độ X và Y nơi nhánh bắt đầu, chiều dài của nhánh và góc của nó.
Bên trong hàm của chúng ta, chúng ta bắt đầu vẽ với phương thức beginPath(), và sau đó lưu trạng thái của canvas với phương thức save().
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <canvas id="my_canvas" width="1000" height="800"></canvas> <script> var myCanvas = document.getElementById("my_canvas"); var ctx = myCanvas.getContext("2d"); function draw(startX, startY, len, angle) { ctx.beginPath(); ctx.save(); } </script> </body> </html>
Phương thức beginPath thường được sử dụng khi bạn bắt đầu một đường thẳng hoặc hình mới có kiểu cố định, như cùng một màu dọc theo toàn bộ đường thẳng, hoặc cùng một độ rộng. Phương thức save chỉ lưu toàn bộ trạng thái của canvas bằng cách đẩy trạng thái hiện tại vào một ngăn xếp.
Bây giờ chúng ta sẽ vẽ Fractal Tree bằng cách vẽ một đường thẳng (nhánh), xoay canvas, vẽ nhánh tiếp theo, và cứ tiếp tục như vậy. Nó diễn ra như thế này (Tôi sẽ giải thích từng phương thức bên dưới mẫu code):
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <canvas id="my_canvas" width="1000" height="800"></canvas> <script> var myCanvas = document.getElementById("my_canvas"); var ctx = myCanvas.getContext("2d"); function draw(startX, startY, len, angle) { ctx.beginPath(); ctx.save(); ctx.translate(startX, startY); ctx.rotate(angle * Math.PI/180); ctx.moveTo(0, 0); ctx.lineTo(0, -len); ctx.stroke(); if(len < 10) { ctx.restore(); return; } draw(0, -len, len*0.8, -15); draw(0, -len, len*0.8, +15); ctx.restore(); } draw(400, 600, 120, 0) </script> </body> </html>
Vì vậy, đầu tiên chúng ta thêm ba phương thức, translate, rotate và moveTo, để "di chuyển" canvas, gốc của nó và "bút chì" của chúng ta để có thể vẽ nhánh theo góc mong muốn. Giống như chúng ta đang vẽ một nhánh, sau đó căn giữa nhánh này (bằng cách di chuyển toàn bộ canvas), và sau đó vẽ một nhánh mới từ cuối nhánh trước đó.
Hai phương thức cuối cùng trước câu lệnh if là lineTo và stroke; phương thức đầu tiên thêm một đường thẳng vào đường dẫn hiện tại, và phương thức thứ hai hiển thị nó. Bạn có thể hiểu nó như thế này: lineTo đưa ra lệnh, và stroke thực hiện nó.
Bây giờ chúng ta có một câu lệnh if cho biết khi nào dừng đệ quy, khi nào dừng vẽ. Phương thức restore, như đã nêu trong Tài liệu MDN, "khôi phục trạng thái canvas đã lưu gần đây nhất bằng cách lấy mục nhập trên cùng trong ngăn xếp trạng thái vẽ".
Sau câu lệnh if, chúng ta có lệnh gọi đệ quy và một lệnh gọi khác đến phương thức restore. Và sau đó là một lệnh gọi đến hàm mà chúng ta vừa hoàn thành.
Bây giờ chạy code trong trình duyệt của bạn. Cuối cùng, bạn sẽ thấy một Fractal Tree!

Tuyệt vời, phải không? Bây giờ hãy làm cho nó tốt hơn nữa.
Chúng ta sẽ thêm một tham số mới vào hàm draw của mình, branchWidth, để làm cho Fractal Tree của chúng ta trông thực tế hơn.
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <canvas id="my_canvas" width="1000" height="800"></canvas> <script> var myCanvas = document.getElementById("my_canvas"); var ctx = myCanvas.getContext("2d"); function draw(startX, startY, len, angle, branchWidth) { ctx.lineWidth = branchWidth; ctx.beginPath(); ctx.save(); ctx.translate(startX, startY); ctx.rotate(angle * Math.PI/180); ctx.moveTo(0, 0); ctx.lineTo(0, -len); ctx.stroke(); if(len < 10) { ctx.restore(); return; } draw(0, -len, len*0.8, angle-15, branchWidth*0.8); draw(0, -len, len*0.8, angle+15, branchWidth*0.8); ctx.restore(); } draw(400, 600, 120, 0, 10) </script> </body> </html>
Vì vậy, trong mỗi lần lặp, chúng ta làm cho mỗi nhánh mỏng hơn. Tôi cũng đã thay đổi tham số góc trong lệnh gọi đệ quy để tạo ra một cây "mở" hơn.
Bây giờ, hãy thêm một chút màu sắc! Và cả bóng đổ, tại sao không.


