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

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

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

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

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

แหล่งที่มา

แม้ว่าเราจะใช้หน่วยความยาวนามธรรม เช่น 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 พิกเซลทุกประการ

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

devicePixelContentBox

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

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

พร็อพเพอร์ตี้ใหม่นี้ช่วยให้เราสร้างภาพเคลื่อนไหวขนาดและตําแหน่งของภาพพิมพ์แคนวาสได้ (รับประกันค่าพิกเซลเศษส่วนได้อย่างมีประสิทธิภาพ) และไม่เห็นเอฟเฟกต์ภาพซ้อนทับในการเรนเดอร์ หากต้องการดูผลกระทบของมัวร์สำหรับแนวทางที่ใช้ 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