Có thật sự bao nhiêu pixel trong một canvas?
Kể từ Chrome 84, ResizeObserver hỗ trợ một phép đo hộp mới có tên là devicePixelContentBox
. Phép đo này đo lường kích thước của phần tử bằng pixel vật lý. Điều này cho phép kết xuất đồ hoạ hoàn hảo đến từng pixel, đặc biệt là trong bối cảnh màn hình có mật độ cao.
Thông tin cơ bả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 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, công cụ bố cục của trình duyệt cuối cùng sẽ chuyển đổi giá trị đó thành pixel (px
). Đây là "Pixel CSS", có nhiều lịch sử và chỉ có mối quan hệ lỏng lẻo với các pixel mà 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 96 DPI ("số điểm trên mỗi inch") là khá hợp lý, tức là bất kỳ màn hình nào cũng sẽ có khoảng 38 pixel trên mỗi cm. Theo thời gian, màn hình lớn hơn và/hoặc nhỏ hơn 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 điều đó với thực tế là nhiều nội dung trên web xác định kích thước của chúng, bao gồm cả cỡ chữ, trong px
và chúng ta sẽ có văn bản khó đọc trên những màn hình có mật độ cao ("HiDPI") này. Để đối phó, các trình duyệt sẽ ẩn mật độ điểm ảnh thực tế của màn hình và thay vào đó 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 96 DPI ảo này, do đó có tên là "CSS Pixel". Đơn vị này chỉ được dùng để đo lường và định vị. Trước khi quá trình kết xuất thực tế diễn ra, một lượt 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 biết số lượng pixel vật lý cần thiết để tạo thành một pixel CSS duy nhất. Nếu devicePixelRatio
(dPR) là 1
, thì bạn đang làm việc trên một màn hình có khoảng 96 DPI. Nếu bạn có màn hình retina, thì dPR của bạn có thể là 2
. Trên điện thoại, không hiếm khi gặp phải 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 suy ra giá trị DPI thực tế của màn hình. dPR bằng 2
có nghĩa là 1 pixel CSS sẽ ánh xạ chính xác 2 pixel thực.
1
...Màn hình này có chiều rộng 3440 pixel và vùng hiển thị rộng 79 cm.
Điều này 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 khiến <div style="width: 1cm; height: 1cm">
không đo chính xác 1 cm về kích thước trên hầu hết các màn hình.
Cuối cùng, dPR cũng có thể bị ảnh hưởng bởi 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 Công cụ cho nhà phát triển trong khi thu phóng, bạn có thể thấy các giá trị phân số xuất hiện.

devicePixelRatio
phân số do thu phóng.Hãy thêm phần tử <canvas>
vào hỗn hợp. Bạn có thể chỉ định số lượng pixel mà bạn muốn canvas có bằng cách sử dụng các thuộc tính width
và height
. Vậy <canvas width=40 height=30>
sẽ là một canvas có kích thước 40 x 30 pixel. Tuy nhiên, điều này không có nghĩa là hình ảnh sẽ được hiển thị ở kích thước 40 x 30 pixel. Theo mặc định, canvas sẽ sử dụng thuộc tính width
và height
để xác định kích thước vốn có của canvas, 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 cho đến nay, có thể bạn sẽ nhận thấy rằng điều này sẽ không 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 đẹp mắt.
Tóm lại: Các phần tử canvas có một kích thước nhất định để xác định vùng mà bạn có thể vẽ. Số lượng pixel trên 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ố lượng pixel CSS không giống với số lượng pixel thực.
Độ hoàn hảo của Pixel
Trong một số trường hợp, bạn nên có một mối liên kết chính xác từ các pixel trên canvas đến các pixel thực. Nếu đạt được mối liên kết này, thì đó được gọi là "pixel-perfect" (độ phân giải hoàn hảo). Việc hiển thị chính xác đến từng pixel là rất quan trọng để hiển thị văn bản một cách dễ đọc, đặc biệt là khi sử dụng phương pháp hiển thị dưới pixel hoặc khi hiển thị đồ hoạ có các đường thẳng được căn chỉnh chặt chẽ với độ sáng thay đổi.
Để đạt được một khung hình gần như hoàn hảo đến từng pixel trên web, đây là phương pháp được sử dụng nhiều nhất:
<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>
Độc giả tinh ý có thể thắc mắc điều gì sẽ xảy ra khi dPR không phải là giá trị nguyên. Đó là một câu hỏi hay và cũng 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ó thể các giá trị này sẽ phân giải thành các giá trị pixel CSS phân số. 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:

getBoundingClientRect()
tạo ra.Pixel CSS hoàn toàn là ảo, vì vậy, về lý thuyết, việc có các phân số của một pixel là điều bình thường, nhưng làm cách nào để trình duyệt tìm ra mối liên kết với các pixel thực? Vì không có pixel vật lý phân số.
Chế độ căn chỉnh theo pixel
Phần của quy trình chuyển đổi đơn vị chịu trách nhiệm căn chỉnh các phần tử với pixel vật lý được gọi là "pixel snapping" (căn chỉnh pixel), và nó thực hiện đúng như tên gọi: Căn chỉnh các giá trị pixel phân số thành các giá trị pixel vật lý, số nguyên. Cách thức chính xác của quá trình này sẽ khác nhau tuỳ theo 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ột màn hình có dPR là 1, thì một trình duyệt có thể kết xuất phần tử đó ở 792px
pixel thực, trong khi một trình duyệt khác có thể kết xuất phần tử đó ở 791px
. Chỉ lệch một pixel, nhưng một pixel có thể gây bất lợi cho những bản kết xuất cần phải hoàn hảo về mặt điểm ảnh. Điều này có thể dẫn đến hiện tượng mờ hoặc thậm chí là các hiện tượng giả tạo dễ thấy hơn như hiệu ứng Moiré.

(Bạn có thể phải mở hình ảnh này trong một thẻ mới để xem mà không có bất kỳ tỷ lệ nào được áp dụng cho hình ảnh.)
devicePixelContentBox
devicePixelContentBox
cung cấp cho bạn hộp nội dung của một phần tử theo đơn vị pixel của 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 chính 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 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 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 chúng được vẽ. Trong bối cảnh vấn đề về canvas mà chúng ta đã trình bày ở trên, chúng ta có thể tận dụng cơ hội này để điều chỉnh số lượng pixel trên canvas, đảm bảo rằng chúng ta có được một mối quan hệ ánh xạ chính xác giữa pixel canvas và pixel vật lý.
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 options 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 có bất kỳ chỉ số nào về hộp được quan sát thay đổi.
Với thuộc tính mới này, chúng ta thậm chí có thể tạo hiệu ứ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 số) mà không thấy bất kỳ hiệu ứng Moiré nào trên quá trình kết xuất. Nếu bạn muốn xem hiệu ứng Moiré khi sử dụng getBoundingClientRect()
và cách thuộc tính ResizeObserver
mới giú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 đối tượ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 mọi phần tử 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 một cách đáng ngạc nhiên trên web và cho đến nay, bạn không có cách nào biết được số lượng chính xác pixel thực 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 hoàn hảo đến từng pixel bằng <canvas>
. devicePixelContentBox
được hỗ trợ trong Chrome phiên bản 84 trở lên.