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

ผืนผ้าใบมีพิกเซลกี่พิกเซลจริงๆ

ตั้งแต่ 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 พิกเซลทุกประการ

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

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

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

เครื่องมือสำหรับนักพัฒนาเว็บที่แสดง 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% อาจลงท้ายด้วยสี่เหลี่ยมผืนผ้าดังนี้

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

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

การสแนป Pixel

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

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

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 ขึ้นไป