Hiểu về view port và view box trong SVG

Hiểu về view port và view box trong SVG
Photo by Colin Walsh from Unsplash

Gần đây mình có làm lại giao diện của blog, trong lúc đó có rảnh rỗi nên chuyển icon từ font awesome sang SVG (của HeroiconsBootstrap Icons). Trong lúc căn chỉnh các icon này cho ngay ngắn, mình có để ý đến một thuộc tính viewBox và thử sửa vài giá trị thì thấy nó hoạt động (thật là vi diệu 🤣). Vì rất tò mò về thuộc tính này nên mình đã tìm hiểu xem sao.

View port và view box

Khái quát

Chắc hẳn mọi người đều đã từng dùng SVG cả rồi. Nhưng chắc nhiều bạn cũng như mình, có thế nào thì dùng thế đó chứ không customize thuộc tính này, thuộc tính kia bao giờ. Tuy nhiên lần này, mình dùng icon dạng SVG và khi gắn nó vào trang thì rất nhiều icon bị lệch dòng. Do đó bắt buộc mình phải chỉnh sửa cho nó ngay ngắn. Do đó mình mới có cơ hội customize lại thuộc tính viewBox của SVG.

Thực ra lúc đầu mình thử chỉnh sửa, tăng chỗ này, giảm chỗ kia thì thấy nó hoạt động, căn mãi thì cũng thành công. Thế nhưng cứ làm mò như vậy mãi cũng không được, vừa mất thời gian, vừa kém hiệu quả. Thế nên mình quyết tâm tìm hiểu thuộc tính này để việc căn chỉnh dễ dàng hơn.

Trước hết, viewBox là một thuộc tính của thẻ svg, chỉ định giá trị về kích thước và khu vực hiển thị (view box) của SVG đó. Khi chỉ định viewBox thì cần theo cú pháp như sau:

viewBox="x y width height"

Trong đó, x, y là toạ độ (góc trên bên trái), width, height là kích thước của view box. Bốn giá trị này ngăn cách bằng dấu phẩy hoặc dấu cách đều được.

Ngoài ra, thẻ svg còn có một vùng hiển thị (gọi là view port), được xác định thông qua các thuộc tính width, height của thẻ svg (hoặc width, height của CSS). View port là vùng mà SVG được hiển thị trên trình duyệt, còn view box là khung để căn chỉnh nội dung bên trong view port.

Giải thích thì hơi khó hiểu, nhưng xem qua một ví dụ sau là các bạn sẽ hiểu ngay 😁

Ví dụ minh hoạ

Để giải thích về view box và view port, mình lấy một ví dụ đơn giản như sau. Một thẻ SVG đơn giản với nền xám và có một vòng tròn ở giữa.

<svg
    width="200"
    height="200"
    viewBox="0 0 200 200"
    style="margin: auto; background: #eee">
    <circle cx="25" cy="25" r="25" fill="blue"/>
</svg>

Kết quả của đoạn code trên là một hình ảnh đơn giản thế này:

Để ý thẻ svg thì nó sẽ được hiển thị trên trình duyệt ở một khung kích thước 200px x 200px. Đây chính là view port của SVG đó. Trong phần này thì view port sẽ luôn giữ nguyên, để chúng ta có thể nhìn rõ ràng hơn ý nghĩa của view box.

Với ví dụ đầu tiên này, viewBox="0 0 200 200", tức là x, y đều là 0, width, height đúng bằng kích thước của view port. Từ là với ví dụ này view box đang trùng với view port. Điều đó được minh hoạ trong ảnh dưới đây:

view port & view box

Vì view box và view port trùng nhau, nên việc hiển thị diễn ra bình thường. Ví dụ này rất dễ hiểu rồi. Tiếp theo, mình sẽ điều chỉnh lại view box một chút. Trong hình ảnh dưới đây, view box được điều chỉnh lại để width = 100, tức là độ rộng chỉ bằng 1/2 view port, chiều cao giữ nguyên, hãy xem có gì thay đổi so với ảnh trước:

So với ảnh đầu tiên, thì vị trí của hình tròn đã có sự thay đổi. Nguyên nhân là do view box đã thay đổi dẫn đến vị trí hiển thị của hình tròn đã khác đi. Để hình dung dễ hơn, mời các bạn xem hình ảnh sau:

view box

Lúc này view box và view port đã không cùng kích thước, và khác cả tỉ lệ giữa chiều dài/chiều rộng. Vì thẻ svg không chỉ định thuộc tính preserveAspectRatio, nên mặc định tỉ lệ dài/rộng của view box sẽ được giữ nguyên (vì view box và view port cùng chiều cao nên view box sẽ giữ nguyên kích thước), đồng thời view box sẽ được căn giữa trong view port. Kết quả là chúng ta có hình ảnh như trên.

Tiếp tục điều chỉnh thêm chút nữa, viewBox="0 0 100 100", tức là view box giờ đây có kích thước mỗi chiều chỉ bằng 1/2 view port. Hãy xem SVG được hiển thị thế nào trên trình duyệt:

So với ảnh đầu tiên, thì hình trong bên trong SVG đã lớn hơn khá nhiều (chính xác là gấp đôi về đường kính). Nguyên nhân là do lúc này, tỉ lệ chiều dài/chiều rộng của view box và view port đang tương đương nhau, và do đó, view box sẽ được giãn ra để vừa với view port (cụ thể là sẽ giãn gấp đôi để phủ hết view port). Kết quả là hình tròn bên trong cũng được giãn theo

view box zoom

Như vậy, chúng ta có thể hiểu thông số về kích thước của view box. Nó là kích thước của phần hiển thị bên trong view port (khung hiển thị trình duyệt dành cho SVG). Và nếu kích thước này khác với kích thước view port nó sẽ được “zoom” cho phù hợp. Còn việc zoom diễn ra như thế nào chúng ta sẽ tìm hiểu kỹ hơn ở phần sau.

Tiếp theo, chúng ta sẽ tìm hiểu kỹ về các thông số x, y. Trong hình ảnh dưới đây, thông số kích thước view box được giữ nguyên so với view port, chỉ thay đổi giá trị x = 25. Kết quả là chúng ta chỉ còn nhìn thấy một nửa hình tròn.

Hình ảnh trên là kết quả của việc thay đổi vị trí của view box mà kích thước được giữ nguyên. Hình ảnh dưới đây mô tả lại rõ hơn về sự thay đổi này. Để ý view box màu đỏ có kích thước trùng với view port, nhưng vị trí thì lệch đi (25px theo chiều ngang). Kết quả là hình tròn bị cắt 1 nửa, sau đó view box được dịch chuyển để khớp với view port (kích thước bằng nhau nên sẽ dịch chuyển để trùng nhau luôn). Nếu trong trường hợp view box và view port không cùng kích thước, thì ngoài dịch chuyển về vị trí, thì view box còn được zoom để phù hợp với view port nữa.

slice

Cuối cùng là thay đổi cả x và y của view box. Đến lúc này thì chắc các bạn cũng đoán ra được kết quả rồi đúng không. Dưới đây là hình ảnh của viewBox="25, 25, 200, 200".

Thêm một ví dụ nữa, với trường hợp giá trị x, y âm, viewBox="-25 -25 200 200". Đã hiểu về view box rồi thì mình nghĩ các bạn sẽ hiểu ngay tại sao kết quả lại như dưới đây:

preserveAspectRatio

Trong phần trước, chúng ta đã biết, nếu view port và view box có kích thước khác nhau, thì view box sẽ được zoom cho phù hợp với view port. Tuy nhiên, khi tỉ lệ chiều dài/rộng của view port và view box khác nhau, thì việc zoom diễn ra như thế nào? Để điều chỉnh việc đó, chúng ta có thể sử dụng thuộc tính preserveAspectRatio của thẻ svg. Thuộc tính preserveAspectRatio nhận 2 giá trị ngăn cách bằng dấu cách (nhất định phải là dấu cách).

Lưu ý là preserveAspectRatio chỉ có tác dụng khi thẻ svg có thuộc tính viewBox mà thôi.

preserveAspectRatio="<align> [<meetOrSlice>]"

Giá trị đầu tiên là cách view box được căn chỉnh thế nào trong view port. Bản thân giá trị này bao gồm 2 thành phần nhỏ hơn (viết liền). Nửa đầu tiên là giá trị căn chỉnh theo trục x (chiều ngang) và nửa còn lại là căn chỉnh theo trục y (chiều dọc). Các giá trị của phần này như sau:

xMincăn view box ra ngoài cùng bên trái theo chiều ngang
xMidcăn view box chính giữa theo chiều ngang
xMaxcăn view box ra ngoài cùng bên trái theo chiều ngang
YMincăn view box lên trên cùng theo chiều dọc
YMidcăn view box chính giữa theo chiều dọc
YMaxcăn view box xuống dưới cùng theo chiều dọc

Khi sử dụng, chúng ta kết hợp giá trị căn chỉnh theo trục x và trục y, viết liền thành một giá trị, ví dụ:

xMaxYMax (căn view box vào góc dưới bên phải view port)
xMidYMid (căn view box vào chính giữa view port theo cả hai chiều)

Giá trị thứ hai của preserveAspectRatio nhận một trong 3 giá trị sau:

meetGiữ tỉ lệ dài/rộng, zoom view box nhỏ nhất, loạt vào trong view port
sliceGiữ tỉ lệ dài/rộng, zoom view box lớn nhất, phần thừa ra ngoài view port sẽ bị ẩn
noneKhông giữ tỉ lệ dài/rộng, view box sẽ được zoom để tràn hết view port

Giá trị thứ 2 này kết hợp với giá trị thứ nhất (ngăn cách bằng dấu cách) sẽ định nghĩa cách mà view box được zoom và căn chỉnh trong view port (riêng giá trị none luôn đứng 1 mình), ví dụ như sau.

preserveAspectRatio="xMidYMid meet" (mặc định)
preserveAspectRatio="xMinYMin slice"

meetOrSlice

Trước hết, chúng ta sẽ xem xét kỹ hơn về giá trị thứ 2 của thuộc tính preserveAspectRatio. Trong các ví dụ sau, các thẻ svg giống hệt nhau, chỉ khác thuộc tính preserveAspectRatio. View port được thiết lập rộng 200px, cao 75px, còn view box có giá trị là 0 0 50 50 (kích thước bằng đúng kích thước hình tròn bên trong).

Lúc này tỉ lệ chiều dài/chiều rộng của view port và view box đã khác nhau, khi zoom view box cho phù hợp với view port thì chúng ta sẽ nhìn rõ ràng ảnh hưởng của preserveAspectRatio. Trong phần này, giá trị đầu tiên sẽ luôn là xMinYMin để dễ so sánh.

Dưới đây là ví dụ đầu tiên preserveAspectRatio="xMinYMin meet", nghĩa là tỉ lệ chiều dài/chiều rộng sẽ được giữ nguyên, đồng thời view box sẽ được zoom để lọt trong lòng view port. Trong trường hợp này, view box sẽ được zoom để khớp về chiều cao, còn chiều rộng sẽ được zoom với tỉ lệ tương ứng. Đồng thời với giá trị đầu tiên là xMinYMin thì view box sẽ được căn ở góc trên bên trái view port.

Trong ví dụ tiếp theo, chúng ta thay đổi preserveAspectRatio="xMinYMin slice". Với giá trị này, tỉ lệ chiều dài/chiều rộng sẽ được giữ nguyên nhưng view box sẽ được zoom theo chiều lớn hơn. Cụ thể trong trường hợp này view box sẽ được zoom để tràn hết chiều rộng, chiều cao bị thừa ra ngoài view port sẽ bị cắt (be sliced) nên kết quả là chúng ta chỉ nhìn thấy 1 nửa hình tròn.

Trong ví dụ cuối cùng, preserveAspectRatio="none", có nghĩa là view box sẽ được zoom để tràn hết view port mà không cần giữ tỉ lệ chiều dài/chiều rộng. Do tỉ lệ chiều dài/chiều rộng không được giữ nên hình tròn cũng bị giãn theo (và không còn tròn nữa):

Một điều khá thú vị là none chỉ có thể đứng một mình. Nếu kết hợp 2 giá trị như meet hay slice thì sẽ bị coi là không hợp lệ và giá trị mặc định sẽ được sử dụng.

align

Trong phần trước, để xem xét kỹ hơn về meet, slicenone, giá trị đầu tiên của preserveAspectRatio luôn luôn được thiết lập là xMinYMin. Trong phần này, chúng ta sẽ giữ nguyên giá trị thứ 2 là meet xem xét kỹ hơn về giá trị align này.

Các ví dụ dưới đây, thẻ svg được thiết lập view port rộng 200px, cao 100px, view box là 0 0 50 50 (bằng đúng hình tròn bên trong). Vì giá trị thứ 2 là meet nên nó sẽ luôn được zoom để đạt độ cao 100px (chiều rộng cũng 100px) và sẽ lọt trong lòng view port. Lúc này, việc view box được căn chỉnh thế nào trong view port phụ thuộc vào giá trị thứ nhất của preserveAspectRatio.

Ba hình ảnh SVG dưới đây được thiết lập giá trị preserveAspectRatio lần lượt là xMinYMin meet, xMidYmin meetxMaxYmin meet (thực ra vì chiều cao của view box và view port giống nhau nên căn theo chiều y thế nào cũng như nhau, ở đây chỉ quan tâm chiều x). Hãy xem view box được căn chỉnh thế nào trong view port với từng trường hợp.

Tương tự như vậy với các hình ảnh có tỉ lệ chiều dài/chiều rộng lớn hơn. Như trong các ví dụ dưới đây, view port được thiết lập cao 200px rộng 100px. Như vậy, view box sẽ được zoom để có động rộng bằng 100px (cao cũng 100px), lọt vào trong lòng view port và lúc này việc căn chỉnh hoàn toàn theo trục y.

Một điểm khá thú vị là các phương thức căn chỉnh trong SVG mãi gần đây mới xuất hiện trong các thẻ HTML thông thường (nhờ sự xuất hiện của flexbox). Và trước khi flexbox xuất hiện, mình đã từng thấy người ta sử dụng svg để hiển thị trên màn hình các thành phần cần căn chỉnh theo cả chiều ngang và chiều dọc, điều vốn rất khó thực hiện chỉ với CSS bình thường. Tuy nhiên, cách làm cụ thể thế nào thì không rõ 😅

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.