การแสดงผลที่สมบูรณ์แบบแบบพิกเซลด้วย devicePixelContentBox

ภาพพิมพ์แคนวาสมีจำนวนพิกเซลจริงๆ เท่าใด

ตั้งแต่ Chrome 84 เป็นต้นไป ResizeObserver รองรับการวัดกล่องแบบใหม่ที่เรียกว่า devicePixelContentBox ซึ่งจะวัดขนาดขององค์ประกอบเป็นพิกเซลจริง ซึ่งช่วยให้แสดงผลกราฟิกได้อย่างสมบูรณ์แบบ โดยเฉพาะในบริบทของหน้าจอที่มีความละเอียดสูง

การรองรับเบราว์เซอร์

  • Chrome: 84
  • Edge: 84
  • Firefox: 93
  • Safari: ไม่รองรับ

แหล่งที่มา

พื้นหลัง: พิกเซล 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 พิกเซลตรงๆ

ตัวอย่าง
จอภาพของฉันมีค่า dPR เท่ากับ 1 ตามข้อมูลของ Chrome…

โดยมีความกว้าง 3440 พิกเซลและพื้นที่แสดงผลกว้าง 79 ซม. ซึ่งจะให้ความละเอียด 110 DPI ใกล้เคียงกับ 96 แต่ไม่ใช่ ด้วยเหตุนี้ <div style="width: 1cm; height: 1cm"> จึงมีขนาดไม่ตรงกับ 1 ซม. บนจอแสดงผลส่วนใหญ่

สุดท้าย dPR ยังอาจได้รับผลกระทบจากฟีเจอร์การซูมของเบราว์เซอร์ด้วย หากคุณซูมเข้า เบราว์เซอร์จะเพิ่ม dPR ที่รายงาน ทำให้ทุกอย่างแสดงผลใหญ่ขึ้น หากเลือก devicePixelRatio ในคอนโซล DevTools ขณะซูม คุณจะเห็นค่าเศษส่วนปรากฏขึ้น

เครื่องมือสำหรับนักพัฒนาเว็บแสดง devicePixelRatio ทศนิยมที่หลากหลายเนื่องจากการซูม

มาเพิ่มองค์ประกอบ <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% อาจกลายเป็นสี่เหลี่ยมผืนผ้าเช่นนี้

เครื่องมือสําหรับนักพัฒนาเว็บแสดงค่าพิกเซลที่เป็นเศษทศนิยมซึ่งเป็นผลจากการเรียกใช้ getBoundingClientRect()

พิกเซล CSS เป็นพิกเซลเสมือนจริงล้วนๆ ดังนั้นการมีเศษของพิกเซลจึงไม่ใช่เรื่องผิดหลักวิชา แต่เบราว์เซอร์จะจับคู่กับพิกเซลจริงได้อย่างไร เนื่องจากพิกเซลจริงแบบเศษทศนิยมนั้นไม่มีอยู่จริง

การจับคู่พิกเซล

ส่วนหนึ่งของกระบวนการแปลงหน่วยที่ดูแลการจัดวางองค์ประกอบกับพิกเซลจริงเรียกว่า "การจับคู่พิกเซล" ซึ่งจะทําตามชื่อที่บอกไว้ คือจับคู่ค่าพิกเซลที่เป็นทศนิยมกับค่าพิกเซลจริงที่เป็นจำนวนเต็ม การดำเนินการนี้แตกต่างกันไปในแต่ละเบราว์เซอร์ หากเรามีองค์ประกอบที่มีขนาดกว้าง 791.984px ในจอแสดงผลที่ dPR เท่ากับ 1 เบราว์เซอร์หนึ่งอาจแสดงผลองค์ประกอบที่ 792px พิกเซลจริง ขณะที่เบราว์เซอร์อื่นอาจแสดงผลที่ 791px นั่นคือการเลื่อนเพียง 1 พิกเซล แต่ 1 พิกเซลอาจส่งผลเสียต่อการแสดงผลที่ต้องมีความแม่นยำระดับพิกเซล ซึ่งอาจทําให้ภาพเบลอหรือมีสิ่งรบกวนปรากฏให้เห็นมากขึ้น เช่น เอฟเฟกต์ Moiré

รูปภาพด้านบนคือแรสเตอร์ของพิกเซลที่มีสีต่างกัน รูปภาพด้านล่างเหมือนกับรูปภาพด้านบน แต่ความกว้างและความสูงลดลง 1 พิกเซลโดยใช้การปรับขนาดแบบเชิงเส้นคู่ รูปแบบที่เกิดขึ้นเรียกว่าเอฟเฟกต์โมiré
(คุณอาจต้องเปิดรูปภาพนี้ในแท็บใหม่เพื่อดูแบบไม่ใช้การปรับขนาด)

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