ภาพพิมพ์แคนวาสมีจำนวนพิกเซลจริงๆ เท่าใด
ตั้งแต่ Chrome 84 เป็นต้นไป ResizeObserver รองรับการวัดกล่องแบบใหม่ที่เรียกว่า devicePixelContentBox
ซึ่งจะวัดขนาดขององค์ประกอบเป็นพิกเซลจริง ซึ่งช่วยให้แสดงผลกราฟิกได้อย่างสมบูรณ์แบบ โดยเฉพาะในบริบทของหน้าจอที่มีความละเอียดสูง
พื้นหลัง: พิกเซล CSS, พิกเซลแคนวาส และพิกเซลจริง
แม้ว่าเราจะใช้หน่วยความยาวนามธรรม เช่น em
, %
หรือ vh
อยู่บ่อยครั้ง แต่ทั้งหมดนี้ล้วนแล้วแต่เป็นพิกเซล เมื่อใดก็ตามที่เราระบุขนาดหรือตําแหน่งขององค์ประกอบใน CSS ท้ายที่สุดแล้วเครื่องมือวางเลย์เอาต์ของเบราว์เซอร์จะแปลงค่านั้นให้เป็นพิกเซล (px
) ซึ่งก็คือ "พิกเซล CSS" ที่มีประวัติมากมายและมีความเกี่ยวข้องเพียงเล็กน้อยกับพิกเซลที่คุณเห็นบนหน้าจอ
เป็นเวลานานแล้วที่การประมาณความหนาแน่นของพิกเซลหน้าจอของทุกคนด้วย 96DPI ("จุดต่อนิ้ว") นั้นค่อนข้างสมเหตุสมผล ซึ่งหมายความว่าจอภาพหนึ่งๆ จะมีพิกเซลประมาณ 38 พิกเซลต่อ 1 ซม. เมื่อเวลาผ่านไป จอภาพก็ขยายและ/หรือหดขนาดลง หรือเริ่มมีจำนวนพิกเซลมากขึ้นในพื้นที่ผิวเดียวกัน เมื่อรวมกับข้อเท็จจริงที่ว่าเนื้อหาจำนวนมากบนเว็บกำหนดมิติข้อมูล รวมถึงขนาดแบบอักษรเป็น px
ผลลัพธ์ที่ได้คือข้อความที่อ่านไม่ออกบนหน้าจอที่มีความหนาแน่นสูง ("HiDPI") เบราว์เซอร์จึงใช้มาตรการตอบโต้ด้วยการซ่อนความหนาแน่นของพิกเซลจริงของจอภาพและจําเป็นว่าผู้ใช้มีจอแสดงผล 96 DPI หน่วย px
ใน CSS แสดงขนาดของพิกเซล 1 พิกเซลบนจอแสดงผล 96 DPI เสมือนนี้ จึงมีชื่อเป็น "พิกเซล CSS" หน่วยนี้ใช้สำหรับการวัดและการวางตำแหน่งเท่านั้น ก่อนที่จะมีการเรนเดอร์จริง ระบบจะแปลงเป็นพิกเซลจริง
เราจะเปลี่ยนจากจอแสดงผลเสมือนจริงนี้ไปเป็นจอแสดงผลจริงของผู้ใช้ได้อย่างไร ป้อนdevicePixelRatio
ค่าส่วนกลางนี้บอกจํานวนพิกเซลจริงที่จําเป็นในการสร้างพิกเซล CSS 1 พิกเซล หาก devicePixelRatio
(dPR) เป็น 1
แสดงว่าคุณกําลังทํางานบนจอภาพที่มี DPI ประมาณ 96 หากมีหน้าจอ Retina ค่า 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
ในคอนโซล DevTools ขณะซูม คุณจะเห็นค่าเศษส่วนปรากฏขึ้น
มาเพิ่มองค์ประกอบ <canvas>
ลงในส่วนผสมกัน คุณสามารถระบุจำนวนพิกเซลที่ต้องการให้ภาพพิมพ์แคนวาสมีได้โดยใช้แอตทริบิวต์ width
และ height
ดังนั้น <canvas width=40 height=30>
จะเป็นภาพพิมพ์แคนวาสขนาด 40 x 30 พิกเซล แต่ไม่ได้หมายความว่ารูปภาพจะแสดงที่ขนาด 40 x 30 พิกเซล โดยค่าเริ่มต้น แคนวาสจะใช้แอตทริบิวต์ width
และ height
เพื่อกำหนดขนาดโดยประมาณ แต่คุณปรับขนาดแคนวาสได้ตามต้องการโดยใช้พร็อพเพอร์ตี้ CSS ทั้งหมดที่คุณรู้จักและชื่นชอบ จากทุกสิ่งที่เราได้เรียนรู้มาจนถึงตอนนี้ คุณอาจคิดว่าวิธีนี้อาจไม่เหมาะสําหรับบางสถานการณ์ พิกเซล 1 พิกเซลบนผืนผ้าใบอาจครอบคลุมพิกเซลจริงหลายพิกเซล หรือเพียงเศษเสี้ยวของพิกเซลจริง ซึ่งอาจทำให้เกิดข้อบกพร่องที่ไม่น่าพอใจ
โดยสรุปคือ องค์ประกอบ Canvas จะมีขนาดที่กำหนดไว้เพื่อกำหนดพื้นที่ที่คุณวาดได้ จำนวนพิกเซลของภาพพิมพ์แคนวาสไม่เกี่ยวข้องกับขนาดการแสดงผลของภาพพิมพ์แคนวาสที่ระบุเป็นพิกเซล CSS แต่อย่างใด จำนวนพิกเซล CSS ไม่เหมือนกับจำนวนพิกเซลจริง
ภาพที่คมชัดทุกพิกเซล
ในบางสถานการณ์ คุณควรมีการแมปที่แน่นอนจากพิกเซลของภาพพิมพ์แคนวาสไปยังพิกเซลจริง หากการแมปนี้สำเร็จ ผลลัพธ์ที่ได้จะเรียกว่า "ตรงทุกพิกเซล" การแสดงผลที่สมบูรณ์แบบระดับพิกเซลเป็นสิ่งสําคัญอย่างยิ่งสําหรับการแสดงผลข้อความที่อ่านง่าย โดยเฉพาะเมื่อใช้การแสดงผลระดับพิกเซลย่อยหรือเมื่อแสดงกราฟิกที่มีเส้นที่เรียงกันอย่างแน่นหนาซึ่งมีความสว่างสลับกัน
หากต้องการสร้างผลงานที่ใกล้เคียงกับภาพพิมพ์แคนวาสแบบพิกเซลเป๊ะที่สุดบนเว็บ แนวทางที่แนะนำคือ
<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 เป็นพิกเซลเสมือนจริงล้วนๆ ดังนั้นการมีเศษของพิกเซลจึงไม่ใช่เรื่องผิดหลักวิชา แต่เบราว์เซอร์จะจับคู่กับพิกเซลจริงได้อย่างไร เนื่องจากพิกเซลจริงแบบเศษทศนิยมนั้นไม่มีอยู่จริง
การจับคู่พิกเซล
ส่วนหนึ่งของกระบวนการแปลงหน่วยที่ดูแลการจัดวางองค์ประกอบกับพิกเซลจริงเรียกว่า "การจับคู่พิกเซล" ซึ่งจะทําตามชื่อที่บอกไว้ คือจับคู่ค่าพิกเซลที่เป็นทศนิยมกับค่าพิกเซลจริงที่เป็นจำนวนเต็ม การดำเนินการนี้แตกต่างกันไปในแต่ละเบราว์เซอร์ หากเรามีองค์ประกอบที่มีขนาดกว้าง 791.984px
ในจอแสดงผลที่ dPR เท่ากับ 1 เบราว์เซอร์หนึ่งอาจแสดงผลองค์ประกอบที่ 792px
พิกเซลจริง ขณะที่เบราว์เซอร์อื่นอาจแสดงผลที่ 791px
นั่นคือการเลื่อนเพียง 1 พิกเซล แต่ 1 พิกเซลอาจส่งผลเสียต่อการแสดงผลที่ต้องมีความแม่นยำระดับพิกเซล ซึ่งอาจทําให้ภาพเบลอหรือมีสิ่งรบกวนปรากฏให้เห็นมากขึ้น เช่น เอฟเฟกต์ Moiré
devicePixelContentBox
devicePixelContentBox
แสดงกล่องเนื้อหาขององค์ประกอบเป็นหน่วยพิกเซลของอุปกรณ์ (เช่น พิกเซลจริง) เป็นส่วนหนึ่งของ ResizeObserver
แม้ว่าเบราว์เซอร์หลักทั้งหมดจะรองรับ ResizeObserver แล้วตั้งแต่ Safari 13.1 แต่ตอนนี้พร็อพเพอร์ตี้ devicePixelContentBox
มีให้บริการใน Chrome 84 ขึ้นไปเท่านั้น
ดังที่กล่าวไว้ใน ResizeObserver
: เหมือนกับ document.onresize
สำหรับองค์ประกอบ ระบบจะเรียกใช้ฟังก์ชัน Callback ของ ResizeObserver
ก่อนการวาดภาพและหลังการจัดวาง ซึ่งหมายความว่าพารามิเตอร์ entries
ของคอลแบ็กจะมีขนาดขององค์ประกอบที่สังเกตทั้งหมดก่อนที่องค์ประกอบเหล่านั้นจะแสดง ในบริบทของปัญหาภาพพิมพ์แคนวาสที่ระบุไว้ข้างต้น เราสามารถใช้โอกาสนี้เพื่อปรับจำนวนพิกเซลในภาพพิมพ์แคนวาส เพื่อให้ได้การแมปแบบ 1:1 ที่แน่นอนระหว่างพิกเซลของภาพพิมพ์แคนวาสกับพิกเซลจริง
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
เสมอ (หากเบราว์เซอร์รองรับ) ระบบจะเรียกใช้การเรียกกลับก็ต่อเมื่อเมตริกกล่องที่สังเกตได้มีการเปลี่ยนแปลงเท่านั้น
พร็อพเพอร์ตี้ใหม่นี้ช่วยให้เราสร้างภาพเคลื่อนไหวขนาดและตำแหน่งของผืนผ้าใบได้ (รับประกันค่าพิกเซลเศษส่วนได้อย่างมีประสิทธิภาพ) และไม่เห็นเอฟเฟกต์ภาพซ้อนทับในการเรนเดอร์ หากต้องการดูเอฟเฟกต์ภาพซ้อนในวิธีการใช้ 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>
ได้ Chrome 84 ขึ้นไปรองรับ devicePixelContentBox