Thực sự có bao nhiêu pixel trong một canvas?
Kể từ Chrome 84, ResizeObserver hỗ trợ một phương thức đo lường hộp mới có tên là devicePixelContentBox
. Phương thức này đo lường kích thước của phần tử theo pixel thực tế. Điều này cho phép kết xuất đồ hoạ hoàn hảo theo pixel, đặc biệt là trong bối cảnh màn hình có mật độ điểm ảnh cao.
Nền: pixel CSS, pixel canvas và pixel thực
Mặc dù chúng ta thường làm việc với các đơn vị chiều dài trừu tượng như em
, %
hoặc vh
, nhưng tất cả đều được quy về pixel. Bất cứ khi nào chúng ta chỉ định kích thước hoặc vị trí của một phần tử trong CSS, cuối cùng công cụ bố cục của trình duyệt sẽ chuyển đổi giá trị đó thành pixel (px
). Đây là "Pixel CSS", có nhiều giá trị trước đó và chỉ có mối quan hệ lỏng lẻo với các pixel bạn có trên màn hình.
Trong một thời gian dài, việc ước tính mật độ pixel trên màn hình của bất kỳ ai bằng 96DPI ("điểm trên mỗi inch") là khá hợp lý, nghĩa là bất kỳ màn hình nào cũng có khoảng 38 pixel trên mỗi cm. Theo thời gian, màn hình đã tăng và/hoặc giảm kích thước hoặc bắt đầu có nhiều pixel hơn trên cùng một diện tích bề mặt. Kết hợp với thực tế là nhiều nội dung trên web xác định kích thước, bao gồm cả cỡ chữ, trong px
, chúng ta sẽ có văn bản không đọc được trên các màn hình có mật độ điểm ảnh cao ("HiDPI") này. Để đối phó, trình duyệt sẽ ẩn mật độ điểm ảnh thực tế của màn hình và giả vờ rằng người dùng có màn hình 96 DPI. Đơn vị px
trong CSS biểu thị kích thước của một pixel trên màn hình ảo 96 DPI này, do đó có tên là "Pixel CSS". Đơn vị này chỉ dùng để đo lường và định vị. Trước khi quá trình kết xuất thực tế diễn ra, quá trình chuyển đổi sang pixel thực sẽ diễn ra.
Làm cách nào để chuyển từ màn hình ảo này sang màn hình thực của người dùng? Nhập devicePixelRatio
. Giá trị chung này cho bạn biết số pixel thực cần thiết để tạo một pixel CSS. Nếu devicePixelRatio
(dPR) là 1
, thì bạn đang làm việc trên một màn hình có độ phân giải khoảng 96DPI. Nếu bạn có màn hình retina, dPR có thể là 2
. Trên điện thoại, không hiếm khi gặp các giá trị dPR cao hơn (và kỳ lạ hơn) như 2
, 3
hoặc thậm chí là 2.65
. Điều quan trọng cần lưu ý là giá trị này là chính xác, nhưng không cho phép bạn lấy giá trị DPI thực tế của màn hình. dPR là 2
có nghĩa là 1 pixel CSS sẽ ánh xạ đến chính xác 2 pixel thực.
1
…Màn hình này có chiều rộng 3440 pixel và diện tích hiển thị rộng 79 cm.
Điều đó dẫn đến độ phân giải 110 DPI. Gần 96 nhưng chưa chính xác.
Đó cũng là lý do tại sao <div style="width: 1cm; height: 1cm">
sẽ không đo chính xác kích thước 1cm trên hầu hết màn hình.
Cuối cùng, dPR cũng có thể chịu ảnh hưởng của tính năng thu phóng của trình duyệt. Nếu bạn phóng to, trình duyệt sẽ tăng dPR được báo cáo, khiến mọi thứ hiển thị lớn hơn. Nếu kiểm tra devicePixelRatio
trong Bảng điều khiển DevTools trong khi thu phóng, bạn có thể thấy các giá trị phân đoạn xuất hiện.
Hãy thêm phần tử <canvas>
vào hỗn hợp. Bạn có thể chỉ định số pixel mà bạn muốn canvas có bằng cách sử dụng thuộc tính width
và height
. Vì vậy, <canvas width=40 height=30>
sẽ là một canvas có kích thước 40x30 pixel. Tuy nhiên, điều này không có nghĩa là hình ảnh sẽ hiển thị ở kích thước 40x30 pixel. Theo mặc định, canvas sẽ sử dụng thuộc tính width
và height
để xác định kích thước nội tại, nhưng bạn có thể tuỳ ý đổi kích thước canvas bằng tất cả các thuộc tính CSS mà bạn biết và yêu thích. Với tất cả những gì chúng ta đã tìm hiểu được cho đến nay, có thể bạn sẽ nhận thấy rằng cách này không phải là cách lý tưởng trong mọi trường hợp. Một pixel trên canvas có thể bao phủ nhiều pixel thực hoặc chỉ một phần của pixel thực. Điều này có thể dẫn đến các thành phần hình ảnh không mong muốn.
Tóm lại: Các phần tử Canvas có kích thước nhất định để xác định khu vực mà bạn có thể vẽ. Số pixel của canvas hoàn toàn độc lập với kích thước hiển thị của canvas, được chỉ định bằng pixel CSS. Số pixel CSS không giống với số pixel thực.
Pixel hoàn hảo
Trong một số trường hợp, bạn nên có mối liên kết chính xác từ pixel canvas đến pixel thực. Nếu đạt được việc liên kết này, thì đó được gọi là "pixel-perfect" (chính xác đến từng pixel). Việc kết xuất chính xác từng pixel là rất quan trọng để hiển thị văn bản rõ ràng, đặc biệt là khi sử dụng tính năng kết xuất điểm ảnh phụ hoặc khi hiển thị đồ hoạ có các đường được căn chỉnh chặt chẽ với độ sáng luân phiên.
Để đạt được một kết quả gần với canvas hoàn hảo nhất có thể trên web, bạn có thể sử dụng phương pháp sau:
<style>
/* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
const cvs = document.querySelector('#myCanvas');
// Get the canvas' size in CSS pixels
const rectangle = cvs.getBoundingClientRect();
// Convert it to real pixels. Ish.
cvs.width = rectangle.width * devicePixelRatio;
cvs.height = rectangle.height * devicePixelRatio;
// Start drawing…
</script>
Người đọc tinh ý có thể thắc mắc điều gì sẽ xảy ra khi dPR không phải là giá trị số nguyên. Đó là một câu hỏi hay và chính là điểm mấu chốt của toàn bộ vấn đề này. Ngoài ra, nếu bạn chỉ định vị trí hoặc kích thước của một phần tử bằng cách sử dụng tỷ lệ phần trăm, vh
hoặc các giá trị gián tiếp khác, thì các giá trị đó có thể phân giải thành giá trị pixel CSS theo tỷ lệ. Một phần tử có margin-left: 33%
có thể kết thúc bằng một hình chữ nhật như sau:
Pixel CSS hoàn toàn là ảo, vì vậy, về lý thuyết, việc có các phân số pixel là không sao, nhưng trình duyệt sẽ xác định mối liên kết với pixel thực như thế nào? Vì pixel thực không có giá trị phân đoạn.
Chụp nhanh pixel
Phần của quá trình chuyển đổi đơn vị giúp căn chỉnh các phần tử với pixel thực tế được gọi là "pixel snapping" (chuyển đổi pixel). Quá trình này thực hiện đúng như tên gọi: chuyển đổi các giá trị pixel thập phân thành giá trị pixel thực tế, số nguyên. Cách thức diễn ra chính xác của việc này còn tuỳ thuộc vào trình duyệt. Nếu chúng ta có một phần tử có chiều rộng là 791.984px
trên màn hình có dPR là 1, thì một trình duyệt có thể hiển thị phần tử đó ở 792px
pixel thực tế, trong khi một trình duyệt khác có thể hiển thị phần tử đó ở 791px
. Đó chỉ là một pixel bị lệch, nhưng một pixel có thể gây bất lợi cho các kết xuất cần phải hoàn hảo về mặt pixel. Điều này có thể dẫn đến tình trạng mờ hoặc thậm chí là các hiện tượng rõ ràng hơn như hiệu ứng Moiré.
devicePixelContentBox
devicePixelContentBox
cung cấp cho bạn hộp nội dung của một phần tử theo đơn vị pixel thiết bị (tức là pixel thực). Đây là một phần của ResizeObserver
. Mặc dù ResizeObserver hiện được hỗ trợ trong tất cả các trình duyệt lớn kể từ Safari 13.1, nhưng thuộc tính devicePixelContentBox
hiện chỉ có trong Chrome 84 trở lên.
Như đã đề cập trong phần ResizeObserver
: giống như document.onresize
cho các phần tử, hàm gọi lại của ResizeObserver
sẽ được gọi trước khi vẽ và sau khi bố cục. Điều đó có nghĩa là tham số entries
cho lệnh gọi lại sẽ chứa kích thước của tất cả các phần tử được quan sát ngay trước khi các phần tử đó được vẽ. Trong bối cảnh vấn đề về canvas nêu trên, chúng ta có thể tận dụng cơ hội này để điều chỉnh số pixel trên canvas, đảm bảo rằng chúng ta có được mối liên kết một với một chính xác giữa pixel canvas và pixel thực.
const observer = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === canvas);
canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
/* … render to canvas … */
});
observer.observe(canvas, {box: ['device-pixel-content-box']});
Thuộc tính box
trong đối tượng tuỳ chọn cho observer.observe()
cho phép bạn xác định kích thước mà bạn muốn quan sát. Vì vậy, mặc dù mỗi ResizeObserverEntry
sẽ luôn cung cấp borderBoxSize
, contentBoxSize
và devicePixelContentBoxSize
(miễn là trình duyệt hỗ trợ), nhưng lệnh gọi lại sẽ chỉ được gọi nếu bất kỳ chỉ số hộp đã quan sát nào thay đổi.
Với thuộc tính mới này, chúng ta thậm chí có thể tạo ảnh động cho kích thước và vị trí của canvas (đảm bảo hiệu quả các giá trị pixel phân đoạn) và không thấy hiệu ứng Moiré nào trên kết xuất. Nếu bạn muốn xem hiệu ứng Moiré trên phương pháp sử dụng getBoundingClientRect()
và cách thuộc tính ResizeObserver
mới cho phép bạn tránh hiệu ứng này, hãy xem bản minh hoạ trong Chrome 84 trở lên!
Phát hiện tính năng
Để kiểm tra xem trình duyệt của người dùng có hỗ trợ devicePixelContentBox
hay không, chúng ta có thể quan sát bất kỳ phần tử nào và kiểm tra xem thuộc tính có xuất hiện trên ResizeObserverEntry
hay không:
function hasDevicePixelContentBox() {
return new Promise((resolve) => {
const ro = new ResizeObserver((entries) => {
resolve(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
ro.disconnect();
});
ro.observe(document.body, {box: ['device-pixel-content-box']});
}).catch(() => false);
}
if (!(await hasDevicePixelContentBox())) {
// The browser does NOT support devicePixelContentBox
}
Kết luận
Pixel là một chủ đề phức tạp đáng ngạc nhiên trên web và cho đến nay, bạn không có cách nào để biết chính xác số pixel thực tế mà một phần tử chiếm trên màn hình của người dùng. Thuộc tính devicePixelContentBox
mới trên ResizeObserverEntry
cung cấp cho bạn thông tin đó và cho phép bạn kết xuất hình ảnh hoàn hảo theo pixel bằng <canvas>
. devicePixelContentBox
được hỗ trợ trong Chrome phiên bản 84 trở lên.