ผืนผ้าใบมีพิกเซลกี่พิกเซลจริงๆ
ตั้งแต่ Chrome 84 เป็นต้นมา ResizeObserver รองรับการวัดกล่องรูปแบบใหม่ชื่อ devicePixelContentBox
ซึ่งจะวัดมิติข้อมูลขององค์ประกอบในพิกเซลจริง ซึ่งช่วยให้แสดงผลกราฟิกที่ยอดเยี่ยมแบบพิกเซลได้ โดยเฉพาะในบริบทของหน้าจอที่มีความหนาแน่นสูง
การสนับสนุนเบราว์เซอร์
- 84
- 84
- 93
- x
พื้นหลัง: พิกเซล CSS, พิกเซล Canvas และพิกเซลจริง
แม้ว่าเราจะมักจะใช้หน่วยวัดความยาวอย่าง em
, %
หรือ vh
แต่หน่วยย่อยต่างๆ จะแสดงเป็นพิกเซล เมื่อใดก็ตามที่เราระบุขนาดหรือตำแหน่งขององค์ประกอบใน CSS เครื่องมือเลย์เอาต์ของเบราว์เซอร์จะแปลงค่านั้นเป็นพิกเซล (px
) ในท้ายที่สุด ค่าเหล่านี้คือ "พิกเซล CSS" ซึ่งมีประวัติมากมายและมีความสัมพันธ์แบบคร่าวๆ กับพิกเซลที่คุณมีบนหน้าจอเท่านั้น
เป็นเวลานานแล้วที่การประมาณความหนาแน่นพิกเซลหน้าจอด้วย 96 DPI ("จุดต่อนิ้ว") นั้นค่อนข้างสมเหตุสมผล ซึ่งหมายความว่าจอภาพจะมีขนาดประมาณ 38 พิกเซลต่อซม. เมื่อเวลาผ่านไป จอภาพมีขนาดเพิ่มขึ้นและ/หรือหดลงหรือเริ่มมีพิกเซลมากขึ้นบนพื้นที่ผิวเดียวกัน รวมกับข้อเท็จจริงที่ว่าเนื้อหาจำนวนมากบนเว็บเป็นตัวกำหนดขนาดรวมถึงขนาดแบบอักษรในpx
แล้วเราก็ได้ข้อความที่อ่านไม่ออกบนหน้าจอความหนาแน่นสูง ("HiDPI") เหล่านี้ ดังนั้น เบราว์เซอร์จะซ่อนความหนาแน่นพิกเซลจริงของหน้าจอและแอบอ้างว่าผู้ใช้มีหน้าจอแสดงผล 96 DPI แทน หน่วย px
ใน CSS แสดงถึงขนาด 1 พิกเซลบนจอแสดงผลเสมือน 96 DPI นี้ เราจึงตั้งชื่อว่า "พิกเซล CSS" หน่วยนี้ใช้สำหรับการวัดและการกำหนดตำแหน่งเท่านั้น ก่อนที่การแสดงผลจริงจะเกิดขึ้น จะมีการแปลงเป็นพิกเซลจริงก่อน
เราเปลี่ยนจากจอแสดงผลเสมือนนี้ไปยังจอแสดงผลจริงของผู้ใช้ได้อย่างไร ป้อนdevicePixelRatio
ค่าส่วนกลางนี้จะบอกจำนวนพิกเซลจริงที่ต้องใช้เพื่อสร้างพิกเซล CSS 1 รายการ หาก devicePixelRatio
(dPR) เป็น 1
หมายความว่าคุณกำลังทำงานในจอภาพที่ประมาณ 96 DPI หากคุณมีหน้าจอเรตินา dPR อาจอยู่ที่ 2
ในโทรศัพท์ ค่า dPR ที่สูง (และแปลกๆ) เช่น 2
, 3
หรือแม้แต่ 2.65
ก็ไม่ใช่เรื่องแปลก คุณต้องทราบว่าค่านี้คือตรงตามค่า แต่ไม่อนุญาตให้รับค่า DPI จริงของจอภาพ dPR ของ 2
หมายความว่าพิกเซล CSS 1 พิกเซลจะแมปกับพิกเซลจริง 2 พิกเซลทุกประการ
1
ตามข้อมูลของ Chrome...หน้าจอกว้าง 3440 พิกเซล และพื้นที่แสดงผลกว้าง 79 ซม.
เพื่อให้ได้ความละเอียดที่ 110 DPI เกือบ 96 แล้ว แต่ยังไม่ใช่
นี่ยังเป็นเหตุผลที่ <div style="width: 1cm; height: 1cm">
จะไม่วัดขนาด 1 ซม. ในจอแสดงผลส่วนใหญ่พอดี
สุดท้าย คุณลักษณะการซูมของเบราว์เซอร์อาจส่งผลต่อ dPR ด้วย หากคุณซูมเข้า เบราว์เซอร์จะเพิ่ม dPR ที่รายงาน ทำให้ทุกอย่างแสดงผลใหญ่ขึ้น หากคุณเลือก devicePixelRatio
ในคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บขณะซูม คุณจะเห็นค่าที่เป็นเศษส่วนปรากฏขึ้น
เพิ่มองค์ประกอบ <canvas>
ลงในมิกซ์กัน คุณระบุจำนวนพิกเซลที่ต้องการให้แคนวาสมีได้โดยใช้แอตทริบิวต์ width
และ height
ดังนั้น <canvas width=40 height=30>
จะเป็นภาพพิมพ์แคนวาสขนาด 40 x 30 พิกเซล แต่ไม่ได้หมายความว่าจะแสดงที่ขนาด 40 x 30 พิกเซล โดยค่าเริ่มต้น Canvas จะใช้แอตทริบิวต์ width
และ height
เพื่อกำหนดขนาดที่แท้จริง แต่คุณสามารถปรับขนาดผืนผ้าใบได้ตามต้องการโดยใช้คุณสมบัติ CSS ทั้งหมดที่คุณรู้จักและชื่นชอบ จากทุกอย่างที่เราได้เรียนรู้มาจนถึงตอนนี้ คุณอาจพบว่าการทำเช่นนี้อาจไม่ใช่วิธีที่ดีที่สุดในทุกสถานการณ์ พิกเซล 1 พิกเซลบนผืนผ้าใบอาจบดบังพิกเซลจริงหลายพิกเซลหรือเพียงเศษเสี้ยวของพิกเซลจริง ซึ่งอาจทำให้มีการสร้างผลงานที่เป็นภาพที่ไม่น่าพอใจ
สรุป: องค์ประกอบของ Canvas มีขนาดที่กำหนดเพื่อกำหนดพื้นที่ที่จะวาดได้ จำนวนพิกเซลของ Canvas จะไม่ขึ้นอยู่กับขนาดการแสดงผลของ Canvas ที่ระบุไว้ในพิกเซล CSS อย่างสิ้นเชิง จำนวนพิกเซล CSS ไม่เท่ากับจำนวนพิกเซลจริง
ความสมบูรณ์แบบพิกเซล
ในบางสถานการณ์ คุณควรมีการแมปที่แน่นอนจากพิกเซล Canvas ไปยังพิกเซลจริง หากการแมปนี้ทำสำเร็จ จะเรียกว่า "pixel-perfect" การแสดงผลที่สมบูรณ์แบบแบบพิกเซลนั้นสำคัญต่อการแสดงภาพข้อความที่อ่านได้ชัดเจน โดยเฉพาะอย่างยิ่งเมื่อใช้การแสดงผลพิกเซลย่อยหรือเมื่อแสดงกราฟิกที่มีความสว่างสลับชิดกันแบบชิดกัน
เพื่อให้ได้ผลลัพธ์ที่ใกล้เคียงที่สุดในแคนวาสที่มีพิกเซลสมบูรณ์แบบที่สุดบนเว็บ วิธีการต่อไปนี้มักไม่มากก็น้อย
<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>
ผู้อ่านที่เก่งกาจอาจสงสัยว่าจะเกิดอะไรขึ้นเมื่อ dPR ไม่ใช่ค่าจำนวนเต็ม นี่เป็นคำถามที่ดีและสาเหตุของปัญหานี้หายไปไหน นอกจากนี้ หากคุณระบุตำแหน่งหรือขนาดขององค์ประกอบโดยใช้เปอร์เซ็นต์, vh
หรือค่าทางอ้อมอื่นๆ องค์ประกอบเหล่านั้นอาจเป็นค่าพิกเซล CSS แบบเศษส่วน องค์ประกอบที่มี margin-left: 33%
อาจลงท้ายด้วยสี่เหลี่ยมผืนผ้าดังนี้
พิกเซล CSS มีลักษณะเสมือนจริงเท่านั้น ดังนั้นการมีเศษส่วนพิกเซลก็ไม่เป็นไรในทางทฤษฎี แต่เบราว์เซอร์จะหาการแมปไปยังพิกเซลจริงได้อย่างไร เนื่องจากพิกเซลจริงเป็นเศษส่วนไม่ใช่สิ่งที่
การสแนป Pixel
ขั้นตอนหนึ่งของกระบวนการแปลงหน่วยที่ดูแลการจัดองค์ประกอบด้วยพิกเซลจริงเรียกว่า "การสแนปพิกเซล" ซึ่งทำหน้าที่ดังข้อความบนกระป๋อง จะสแนปค่าพิกเซลเศษส่วนกับค่าพิกเซลจริงที่เป็นจำนวนเต็ม ความแตกต่างที่เกิดขึ้นในแต่ละเบราว์เซอร์ หากเรามีองค์ประกอบที่มีความกว้าง 791.984px
ในจอแสดงผลที่ dPR เท่ากับ 1 เบราว์เซอร์หนึ่งอาจแสดงผลองค์ประกอบที่ 792px
พิกเซลจริง ขณะที่เบราว์เซอร์อื่นอาจแสดงผลที่ 791px
แม้จะเป็นพิกเซลเดียว แต่พิกเซลเดียวอาจส่งผลเสียต่อการแสดงภาพที่ต้องสมบูรณ์แบบพิกเซลได้ ซึ่งอาจทำให้เบลอภาพหรือสิ่งประดิษฐ์ที่เห็นได้ชัดเจนขึ้น เช่น เอฟเฟกต์มัวเร
devicePixelContentBox
devicePixelContentBox
มอบกล่องเนื้อหาขององค์ประกอบในหน่วยพิกเซลของอุปกรณ์ (กล่าวคือ พิกเซลจริง) ซึ่งเป็นส่วนหนึ่งของ ResizeObserver
แม้ว่าตอนนี้เบราว์เซอร์หลักทั้งหมดรองรับ การหาการปรับขนาดObserver ตั้งแต่ Safari 13.1 แล้ว แต่ในตอนนี้พร็อพเพอร์ตี้ devicePixelContentBox
สามารถใช้ได้เฉพาะใน Chrome 84 ขึ้นไปเท่านั้น
อย่างที่กล่าวไว้ใน ResizeObserver
ซึ่งก็เหมือนกับ document.onresize
สำหรับองค์ประกอบ ฟังก์ชันเรียกกลับของ ResizeObserver
จะถูกเรียกก่อนการแสดงผลและหลังเลย์เอาต์ ซึ่งหมายความว่าพารามิเตอร์ entries
ของโค้ดเรียกกลับจะมีขนาดขององค์ประกอบที่สังเกตได้ทั้งหมดก่อนที่จะลงสี ในบริบทของปัญหา Canvas ที่ระบุไว้ข้างต้น เราสามารถใช้โอกาสนี้เพื่อปรับจำนวนพิกเซลบนผืนผ้าใบ เพื่อให้แน่ใจว่าเราได้จับคู่แบบหนึ่งต่อหนึ่งระหว่างพิกเซล Canvas กับพิกเซลจริงได้
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']});
พร็อพเพอร์ตี้ box
ในออบเจ็กต์ตัวเลือกสำหรับ observer.observe()
ให้คุณกำหนดขนาดที่ต้องการสังเกต ดังนั้น แม้ว่า ResizeObserverEntry
แต่ละรายการจะระบุ borderBoxSize
, contentBoxSize
และ devicePixelContentBoxSize
เสมอ (หากเบราว์เซอร์รองรับ) ระบบจะเรียกใช้โค้ดเรียกกลับก็ต่อเมื่อเมตริกของช่องที่พบมีการเปลี่ยนแปลงเท่านั้น
คุณสมบัติใหม่นี้ช่วยให้เราทำให้ขนาดและตำแหน่งของ Canvas เคลื่อนไหวได้ (รับประกันค่าพิกเซลแบบเศษส่วนได้อย่างมีประสิทธิภาพ) และจะไม่เห็นเอฟเฟกต์มัวเรในการแสดงภาพ หากต้องการดูผลกระทบของมัวเรในวิธีการที่ใช้ getBoundingClientRect()
และวิธีที่พร็อพเพอร์ตี้ ResizeObserver
ใหม่ช่วยหลีกเลี่ยงปัญหานี้ โปรดดูการสาธิตใน Chrome 84 ขึ้นไป
การตรวจหาฟีเจอร์
หากต้องการดูว่าเบราว์เซอร์ของผู้ใช้รองรับ devicePixelContentBox
หรือไม่ เราจะสังเกตองค์ประกอบใดก็ได้และดูว่าพร็อพเพอร์ตี้ดังกล่าวอยู่ใน ResizeObserverEntry
หรือไม่
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
}
บทสรุป
พิกเซลคือหัวข้อที่ซับซ้อนอย่างไม่น่าเชื่อบนเว็บและจนถึงตอนนี้ก็ยังไม่มีวิธีที่จะทราบจำนวนพิกเซลจริงที่องค์ประกอบหนึ่งๆ ใช้บนหน้าจอของผู้ใช้ พร็อพเพอร์ตี้ devicePixelContentBox
ใหม่ใน ResizeObserverEntry
จะให้ข้อมูลดังกล่าวแก่คุณและทำให้คุณเรนเดอร์พิกเซลที่สมบูรณ์แบบด้วย <canvas>
ได้ devicePixelContentBox
ใช้ได้ใน Chrome 84 ขึ้นไป