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

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

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

Browser Support

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: not supported.

Source

พื้นหลัง: พิกเซล CSS, พิกเซล Canvas และพิกเซลจริง

แม้ว่าเรามักจะทำงานกับหน่วยความยาวเชิงนามธรรม เช่น em, % หรือ vh แต่ทั้งหมดนี้ก็ขึ้นอยู่กับพิกเซล เมื่อใดก็ตามที่เรากำหนดขนาดหรือตำแหน่งขององค์ประกอบใน CSS เครื่องมือเลย์เอาต์ของเบราว์เซอร์จะแปลงค่าดังกล่าวเป็นพิกเซล (px) ในที่สุด ซึ่งก็คือ "พิกเซล CSS" ที่มีประวัติความเป็นมามากมายและมีความสัมพันธ์เพียงเล็กน้อยกับพิกเซลที่คุณเห็นบนหน้าจอ

เป็นเวลานานมาแล้วที่การประมาณความหนาแน่นของพิกเซลหน้าจอของทุกคนด้วย 96DPI ("จุดต่อนิ้ว") ค่อนข้างสมเหตุสมผล ซึ่งหมายความว่าจอภาพใดก็ตามจะมีพิกเซลประมาณ 38 พิกเซลต่อเซนติเมตร เมื่อเวลาผ่านไป จอภาพมีขนาดใหญ่ขึ้นและ/หรือเล็กลง หรือเริ่มมีพิกเซลมากขึ้นในพื้นที่ผิวเท่าเดิม เมื่อรวมกับข้อเท็จจริงที่ว่าเนื้อหาจำนวนมากบนเว็บกำหนดขนาดของตนเอง รวมถึงขนาดแบบอักษรใน px เราจึงได้ข้อความที่อ่านไม่ออกบนหน้าจอความหนาแน่นสูง ("HiDPI") เหล่านี้ เบราว์เซอร์จึงซ่อนความหนาแน่นของพิกเซลจริงของจอภาพและแสร้งว่าผู้ใช้มีจอแสดงผล 96 DPI px หน่วยใน CSS แสดงถึงขนาดของ 1 พิกเซลในจอแสดงผล 96 DPI เสมือนนี้ จึงเป็นที่มาของชื่อ "พิกเซล CSS" หน่วยนี้ใช้สำหรับการวัดและการวางตำแหน่งเท่านั้น ก่อนที่จะมีการแสดงผลจริง ระบบจะแปลงเป็นพิกเซลจริง

เราจะเปลี่ยนจากจอแสดงผลเสมือนนี้ไปเป็นจอแสดงผลจริงของผู้ใช้ได้อย่างไร ป้อนdevicePixelRatio ค่าส่วนกลางนี้จะบอกจำนวนพิกเซลจริงที่คุณต้องใช้เพื่อสร้างพิกเซล CSS เดียว หาก devicePixelRatio (dPR) เป็น 1 แสดงว่าคุณกำลังทำงานบนจอภาพที่มี DPI ประมาณ 96 หากมีหน้าจอ Retina แสดงว่า dPR น่าจะเป็น 2 ในโทรศัพท์ คุณอาจเห็นค่า dPR ที่สูงขึ้น (และแปลกขึ้น) เช่น 2, 3 หรือแม้แต่ 2.65 โปรดทราบว่าค่านี้เป็นค่าที่แน่นอน แต่ไม่ได้ช่วยให้คุณทราบค่า DPI ที่แท้จริงของจอภาพ dPR ที่ 2 หมายความว่า 1 พิกเซล CSS จะแมปกับพิกเซลจริง 2 พิกเซล

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

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

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

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

มาเพิ่มองค์ประกอบ <canvas> กัน คุณระบุจำนวนพิกเซลที่ต้องการให้ Canvas มีได้โดยใช้แอตทริบิวต์ width และ height ดังนั้น <canvas width=40 height=30> จะเป็น Canvas ที่มีขนาด 40x30 พิกเซล แต่ไม่ได้หมายความว่าระบบจะแสดงที่ 40 x 30 พิกเซล โดยค่าเริ่มต้น Canvas จะใช้แอตทริบิวต์ width และ height เพื่อกำหนดขนาดที่แท้จริง แต่คุณสามารถปรับขนาด Canvas ได้ตามต้องการโดยใช้พร็อพเพอร์ตี้ CSS ทั้งหมดที่คุณรู้จักและชื่นชอบ จากทุกสิ่งที่เราได้เรียนรู้มาจนถึงตอนนี้ คุณอาจคิดว่าวิธีนี้อาจไม่เหมาะกับทุกสถานการณ์ พิกเซลหนึ่งพิกเซลบน Canvas อาจครอบคลุมพิกเซลจริงหลายพิกเซล หรืออาจครอบคลุมเพียงเศษเสี้ยวของพิกเซลจริง ซึ่งอาจทำให้เกิดอาร์ติแฟกต์ภาพที่ไม่พึงประสงค์

สรุปได้ว่า องค์ประกอบ Canvas มีขนาดที่กำหนดเพื่อกำหนดพื้นที่ที่คุณวาดได้ จำนวนพิกเซลของ Canvas จะไม่ขึ้นอยู่กับขนาดการแสดงผลของ Canvas ซึ่งระบุเป็นพิกเซล CSS จำนวนพิกเซล CSS ไม่เท่ากับจำนวนพิกเซลจริง

ความสมบูรณ์แบบของ Pixel

ในบางสถานการณ์ คุณอาจต้องการให้มีการแมปจากพิกเซล Canvas ไปยังพิกเซลจริงอย่างแม่นยำ หากการแมปนี้สำเร็จ จะเรียกว่า "สมบูรณ์แบบระดับพิกเซล" การแสดงผลที่สมบูรณ์แบบเป็นสิ่งสำคัญสำหรับการแสดงผลข้อความที่อ่านได้ โดยเฉพาะเมื่อใช้การแสดงผลระดับย่อยของพิกเซล หรือเมื่อแสดงกราฟิกที่มีเส้นที่จัดเรียงอย่างแม่นยำซึ่งมีความสว่างสลับกัน

หากต้องการสร้างผืนผ้าใบที่ใกล้เคียงกับผืนผ้าใบที่สมบูรณ์แบบระดับพิกเซลบนเว็บให้มากที่สุด วิธีที่ใช้กันโดยทั่วไปมีดังนี้

<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 พิกเซลโดยใช้การปรับขนาดแบบไบลิเนียร์ รูปแบบที่เกิดขึ้นนี้เรียกว่าเอฟเฟกต์มัวเร
(คุณอาจต้องเปิดรูปภาพนี้ในแท็บใหม่เพื่อดูโดยไม่มีการปรับขนาด)

devicePixelContentBox

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

ดังที่กล่าวไว้ใน ResizeObserver: เหมือนกับ document.onresize สำหรับองค์ประกอบ ฟังก์ชัน Callback ของ ResizeObserver จะเรียกใช้ก่อนการวาดและหลังเลย์เอาต์ ซึ่งหมายความว่าพารามิเตอร์ entries ในการเรียกกลับจะมีขนาดขององค์ประกอบที่สังเกตได้ทั้งหมดก่อนที่จะมีการวาด ในบริบทของปัญหาเกี่ยวกับ Canvas ที่ระบุไว้ข้างต้น เราสามารถใช้โอกาสนี้เพื่อปรับจำนวนพิกเซลใน 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> Chrome 84 ขึ้นไปรองรับ devicePixelContentBox