HTML-in-Canvas: แนวคิดทางเทคนิค

HTML-in-Canvas API ช่วยให้คุณแสดงผลซับทรี DOM แบบอินเทอร์แอกทีฟที่เข้าถึงได้โดยตรงในบริบท Canvas 2D, WebGL หรือ WebGPU

คู่มือนี้จะอธิบายแนวคิดทางเทคนิคของ HTML-in-Canvas ที่ใช้กันโดยทั่วไปในบริบท Canvas ทั้งหมด ดังนี้

เตรียม Canvas เพื่อแสดงผล HTML

หากต้องการแสดงผลองค์ประกอบ HTML ภายใน <canvas> ให้กำหนดค่าแอตทริบิวต์ 2 รายการต่อไปนี้

  • ตั้งค่า content="drawable" ใน <canvas>: การดำเนินการนี้จะเลือกองค์ประกอบ Canvas เพื่อจัดวางโหนด DOM ที่สืบทอดมาแทนที่จะถือว่าเป็นเนื้อหาที่ซ่อนอยู่เท่านั้น
  • ตั้งค่า drawable ในองค์ประกอบเป้าหมาย: วิธีนี้จะทำเครื่องหมายองค์ประกอบเป็นรูทการวาดอิสระที่มีสิทธิ์แสดงผล Canvas
<canvas id="mainCanvas" width="600" height="400" content="drawable">
  <div id="parentElement" class="card" drawable>
    <h2>Container Title</h2>
    <p>Descriptive text inside the parent element.</p>
    <!-- Nested drawable: excluded from parentElement and drawn independently -->
    <button id="childElement" class="btn" drawable>Action Button</button>
  </div>
</canvas>

ทำความเข้าใจแผนผังย่อยที่ถอนได้

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

ซับทรีที่วาดได้ประกอบด้วยองค์ประกอบที่มีแอตทริบิวต์ drawable และองค์ประกอบสืบทอดทั้งหมด ยกเว้นองค์ประกอบสืบทอดที่ฝังอยู่ซึ่งมี drawable ด้วย เช่น

<canvas content="drawable">
  <!-- Subtree A (Renders parent container and text) -->
  <div id="parentElement" drawable>
    <h2>Parent Card Heading</h2>
    <p>Card body text content.</p>
    <!-- Subtree B (Excluded from Subtree A and drawn separately) -->
    <button id="nestedChildElement" drawable>
      Clickable Button
    </button>
  </div>
</canvas>
องค์ประกอบ ขอบเขตการแสดงผล พฤติกรรม
#parentElement (Subtree A) แผงพื้นหลัง ส่วนหัว และย่อหน้า แสดงผลเมื่อเรียกใช้ drawElementImage(parentElement, ...)
#nestedChildElement (Subtree B) พื้นผิวและข้อความบนปุ่ม ยกเว้นจากแผนผังย่อย #parentElement แสดงผลในการเรียก drawElementImage(nestedChildElement, ...) แยกต่างหาก
ปุ่ม drawable ซ้อนอยู่ในการ์ด drawable และแต่ละปุ่มจะวาดโดยการเรียกใช้ drawElementImage() ของตัวเอง

การฝังองค์ประกอบที่วาดได้ช่วยให้คุณรักษาความสัมพันธ์ของ HTML ที่สื่อความหมายได้ ขณะเดียวกันก็ให้อิสระในการจัดเลเยอร์ จัดตำแหน่ง หรือสร้างภาพเคลื่อนไหวให้กับองค์ประกอบย่อยอย่างอิสระเหนือฉาก Canvas

ทําความเข้าใจเหตุการณ์ paint

HTML-in-Canvas API จะเปิดตัวเหตุการณ์ paint เบราว์เซอร์จะส่งเหตุการณ์ paint ใน <canvas> โดยอัตโนมัติเมื่อใดก็ตามที่เลย์เอาต์ ข้อความ หรือสไตล์ CSS เปลี่ยนแปลงในองค์ประกอบสืบทอด drawable

canvas.addEventListener("paint", (event) => {
  // Clear the viewport
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Redraw elements individually
  ctx.drawElementImage(parentElement, 20, 20);
  ctx.drawElementImage(childElement, 40, 90);
});

เหตุการณ์ paint จะส่งอินสแตนซ์ของ CanvasPaintEvent ซึ่งขยาย DOM Event มาตรฐาน และมีพร็อพเพอร์ตี้ changedElements ที่มีเฉพาะองค์ประกอบสืบทอด drawable ที่เปลี่ยนแปลงไปในเชิงภาพ (เช่น การเปลี่ยนแปลงสไตล์ การเปลี่ยนเลย์เอาต์ การแก้ไขข้อความ หรือการเปลี่ยนแปลงสถานะโฟกัส) นับตั้งแต่การวาดครั้งล่าสุด

canvas.addEventListener("paint", (event) => {
  // Array of changed elements
  for (const el of event.changedElements) {
    console.log(`Element #${el.id} visually changed and needs repainting`);
  }
});
เบราว์เซอร์จะเริ่มวาดบน Canvas เมื่อใดก็ตามที่การแสดงผลขององค์ประกอบสืบทอดที่วาดได้มีการเปลี่ยนแปลง

วงจรของเฟรม

คำสั่งการวาด Canvas ที่ดำเนินการภายในเหตุการณ์ paint จะปรากฏในการอัปเดตการแสดงผลปัจจุบัน แต่การแก้ไข DOM ที่ทำภายในเหตุการณ์ paint จะไม่แสดงจนกว่าจะถึงเฟรมถัดไป

canvas.addEventListener("paint", () => {
  // Renders in the CURRENT rendering update:
  ctx.drawElementImage(parentElement, 20, 20);

  // Modifying DOM styles inside paint schedules a new layout cycle;
  // changes will appear in the NEXT frame:
  parentElement.style.backgroundColor = "#e0f2fe";
});

Canvas ที่ซ้อนกัน

หากหน้าเว็บมีองค์ประกอบ <canvas content="drawable"> ที่ซ้อนกัน เบราว์เซอร์จะส่งเหตุการณ์ paint ตามลำดับแผนผัง DOM แบบย้อนกลับ (องค์ประกอบย่อยจะเริ่มทำงานก่อนองค์ประกอบระดับบน) วิธีนี้ช่วยให้มั่นใจว่าองค์ประกอบ Canvas ย่อยจะแสดงผลก่อนที่องค์ประกอบ Canvas หลักจะรวมองค์ประกอบเหล่านั้นเข้าด้วยกัน

สมมติว่าในตัวอย่างต่อไปนี้ องค์ประกอบ nestedElement ด้านในต้องเปลี่ยนไปในเชิงภาพ (เช่น สีพื้นหลัง CSS เปลี่ยนไปเมื่อผู้ใช้เลื่อนเคอร์เซอร์ไปวางเหนือองค์ประกอบ) อันดับแรก เหตุการณ์ paint จะเริ่มทำงานใน Canvas nestedCanvas Listener จะล้างบัฟเฟอร์ย่อยและเรียกใช้ ctx.drawElementImage(nestedElement, ...) เพื่อแสดงผลสถานะที่อัปเดต จากนั้นเหตุการณ์ paint จะเริ่มทำงานใน parentCanvas Listener จะวาด wrapperElement (ซึ่งมี Canvas nestedCanvas ที่อัปเดตแล้ว) ลงในฉาก

<!-- Outer parent canvas -->
<canvas id="parentCanvas" width="800" height="600" content="drawable">
  <!-- Wrapper container for nested content -->
  <div id="wrapperElement" drawable>

    <!-- Nested child canvas -->
    <canvas id="nestedCanvas" width="300" height="200" content="drawable">
      <div id="nestedElement" class="badge" drawable>
        Inner Child Content
      </div>
    </canvas>

  </div>
</canvas>

<script>
const parentCanvas = document.getElementById("parentCanvas");
const parentCtx = parentCanvas.getContext("2d");

const nestedCanvas = document.getElementById("nestedCanvas");
const nestedCtx = nestedCanvas.getContext("2d");

const nestedElement = document.getElementById("nestedElement");
const wrapperElement = document.getElementById("wrapperElement");

// ============================================================================
// 1. Child Canvas Listener (Fires FIRST)
// ============================================================================
nestedCanvas.addEventListener("paint", (event) => {
  // Clear and redraw the child canvas with the updated inner element
  nestedCtx.clearRect(0, 0, nestedCanvas.width, nestedCanvas.height);
  nestedCtx.drawElementImage(nestedElement, 10, 10);

  console.log("1. Child nested canvas rendered fresh pixels.");
});

// ============================================================================
// 2. Parent Canvas Listener (Fires SECOND)
// ============================================================================
parentCanvas.addEventListener("paint", (event) => {
  // Clear and redraw the parent canvas scene
  parentCtx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);

  // Draws the wrapper container (including the freshly painted nestedCanvas)
  parentCtx.drawElementImage(wrapperElement, 50, 50);

  console.log("2. Parent canvas composited the complete scene.");
});
</script>
เมื่อองค์ประกอบ <canvas content="drawable"> ซ้อนกัน เหตุการณ์การวาดจะเริ่มทำงานตามลำดับต้นไม้ย้อนกลับ โดย Canvas ที่ซ้อนกันจะวาดก่อน เพื่อให้ Canvas ระดับบนสุดสามารถวาดพิกเซลที่อัปเดตแล้วได้

ทริกเกอร์เหตุการณ์ paint

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

let animatedX = 0;

canvas.addEventListener("paint", () => {
  animatedX += 1.5; // Move the element horizontally
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawElementImage(staticElement, 20, 20);
  ctx.drawElementImage(movingElement, animatedX, 200);
  // Request canvas paint on each rendering update for smooth animation
  canvas.requestPaint();
});

canvas.requestPaint();

อัปเดตเรขาคณิตขององค์ประกอบ

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

  • ส่งเหตุการณ์การป้อนข้อมูล เช่น การคลิก การแตะ และการเคลื่อนไหวของเคอร์เซอร์ไปยังองค์ประกอบที่ถูกต้อง ซึ่งเรียกว่าการทดสอบตัวชี้
  • แสดงผลลัพธ์ที่ถูกต้องจากคำค้นหาเลย์เอาต์ เช่น getBoundingClientRect()
  • รายงานขอบเขตขององค์ประกอบไปยังเทคโนโลยีความช่วยเหลือพิเศษ เช่น แว่นขยายหน้าจอ

ในแบบ 2 มิติ drawElementImage() จะอัปเดตเรขาคณิตโดยอัตโนมัติตามค่าเริ่มต้น อาร์กิวเมนต์ที่คุณส่งไปยังเมธอดนี้ พร้อมกับรูปแบบที่คำนวณขององค์ประกอบและการเปลี่ยนรูปแบบปัจจุบันของบริบทนั้นเพียงพอที่จะระบุตำแหน่งที่องค์ประกอบจะปรากฏ

ในบริบท WebGL และ WebGPU เบราว์เซอร์จะระบุเรขาคณิตไม่ได้ ระบบจะคัดลอกองค์ประกอบ HTML ลงในพื้นผิว จากนั้นโปรแกรมตัวปรับเฉดสีจะแมปพื้นผิวนั้นลงในฉากได้ตามต้องการ หากต้องการบอกเบราว์เซอร์ว่าองค์ประกอบปรากฏที่ใด ให้ส่งการเปลี่ยนรูปแบบไปยัง updateElementGeometry()

เมื่อโค้ดหยุดเรียก updateElementGeometry() เบราว์เซอร์จะไม่ทราบอีกต่อไปว่าองค์ประกอบ HTML ในการ์ด 3 มิติอยู่ที่ใด การ์ดจะหมุนไปเรื่อยๆ แต่การคลิกจะยังคงนำไปยังตำแหน่งเดิม

หากต้องการอัปเดตเรขาคณิต ให้ส่งการเปลี่ยนรูปแบบเชิงพื้นที่ไปยังเมธอด ดูวิธีสร้างการเปลี่ยนรูปแบบสำหรับบริบทต่างๆ ดังนี้

จัดการลำดับการซ้อนกันของการทดสอบตัวชี้

โดยค่าเริ่มต้น ทุกครั้งที่มีการอัปเดตเรขาคณิตขององค์ประกอบ เบราว์เซอร์จะนำองค์ประกอบนั้นไปไว้ที่ด้านบนสุดของสแต็กการโต้ตอบ (วาดล่าสุด = เป้าหมายการคลิกบนสุด) คุณควบคุมลักษณะการทำงานนี้ได้โดยใช้ preserveHitTestOrder ดังนี้

// Updates the backdrop without stealing clicks from foreground elements
canvas.updateElementGeometry(backgroundElement, {
  canvasTransform: customTransform,
  preserveHitTestOrder: true
});
ตัวเลือก พฤติกรรม กรณีการใช้งาน
preserveHitTestOrder: false (ค่าเริ่มต้น) ย้ายองค์ประกอบไปที่ด้านบนของสแต็กการทดสอบตัวชี้ องค์ประกอบที่อยู่เบื้องหน้าที่ควรได้รับการคลิกก่อน
preserveHitTestOrder: true ปล่อยองค์ประกอบไว้ที่ลำดับความสำคัญของสแต็กปัจจุบัน องค์ประกอบพื้นหลังที่ต้องอัปเดตหลังจากองค์ประกอบที่อยู่เบื้องหน้า

รีเซ็ตเรขาคณิต

หากต้องการยกเลิกการวาดองค์ประกอบโดยไม่นำออกจาก DOM ให้เรียกใช้ canvas.clearElementGeometry() ซึ่งจะรีเซ็ตการเปลี่ยนรูปแบบ Canvas ขององค์ประกอบและทำเครื่องหมายเป็น "นอกหน้าจอ" ในแผนผังการช่วยเหลือพิเศษ

// Remove from canvas hit-testing and mark as offscreen
canvas.clearElementGeometry(childElement);

ขนาดและหน่วย

เมื่อคุณวาด HTML ลงใน Canvas เบราว์เซอร์จะทำงานในระบบพิกัด 3 ระบบพร้อมกัน ได้แก่ พิกเซล CSS, พิกเซลอุปกรณ์ และพิกเซลตารางกริด Canvas

พิกเซล CSS ใช้สำหรับวัดและจัดตำแหน่งเนื้อหาระหว่างการจัดเลย์เอาต์ จากนั้นเบราว์เซอร์จะปรับขนาดตาม devicePixelRatio เพื่อแรสเตอร์หน้าเว็บเป็นพิกเซลของอุปกรณ์จริง เนื่องจากพิกเซล CSS เป็นหน่วยเชิงตรรกะ ตำแหน่งและขนาดจึงมักเป็นเศษส่วนของพิกเซลอุปกรณ์ (เช่น 10.5 CSS px × DPR 1.5 = 15.75 พิกเซลอุปกรณ์) หน้าจอจะแสดงสีได้เฉพาะพิกเซลทั้งหมด ดังนั้นเบราว์เซอร์จึงต้องแก้ไขสิ่งที่อยู่ระหว่างพิกเซลเหล่านั้น โดยทำได้ 2 วิธีดังนี้

  • การสแนป: ขอบเขตกล่อง เส้นขอบ และออฟเซ็ตเลเยอร์จะปัดเศษเป็นพิกเซลอุปกรณ์จำนวนเต็มที่ใกล้เคียงที่สุดเพื่อให้ภาพคมชัดอยู่เสมอ
  • การลดรอยหยัก: สำหรับอักขระข้อความ เส้นโค้ง และขอบที่เปลี่ยนรูป ระบบจะผสมผสานการครอบคลุมบางส่วนเข้ากับพิกเซลข้างเคียงเพื่อให้ยังคงดูเรียบเนียนและอยู่ในตำแหน่งที่ถูกต้อง

<canvas> มีตารางกริดพิกเซลของตัวเอง ซึ่งกำหนดโดยแอตทริบิวต์ width และ height ซึ่งแยกจากทั้งพิกเซล CSS และพิกเซลอุปกรณ์ ในการแสดงผล เบราว์เซอร์จะสุ่มตัวอย่างบิตแมปของ Canvas ใหม่ลงในตารางกริดพิกเซลจริง โดยผสมผสานพิกเซล Canvas ที่อยู่ใกล้เคียงเพื่อเติมพิกเซลของอุปกรณ์แต่ละพิกเซล หากต้องการดูวิธีที่เบราว์เซอร์แมประหว่าง CSS, Canvas และพิกเซลของอุปกรณ์ โปรดดูการแสดงผลที่สมบูรณ์แบบระดับพิกเซลด้วย devicePixelContentBox

องค์ประกอบ HTML drawable จะจัดวางในพิกเซล CSS เนื่องจากเนื้อหาที่วาดลงใน Canvas สามารถผ่านการแปลง 2 ครั้ง จากพิกเซล CSS ไปยังตารางกริดของ Canvas แล้วจากตารางกริดของ Canvas ไปยังพิกเซลจริง แต่ละขั้นตอนที่ไม่สอดคล้องกันจะเพิ่มการผสมอีกรอบ และผลลัพธ์อาจดูเบลอ เพื่อหลีกเลี่ยงปัญหานี้ ให้สร้างตารางกริดของ Canvas ให้ตรงกับตารางกริดของพิกเซลจริง ซึ่งมีวัตถุประสงค์ 2 ประการดังนี้

  • การแสดงผลที่สอดคล้องกัน: หาก Canvas แมปแบบ 1:1 กับพิกเซลจริง การแปลงพิกเซล CSS เป็นพิกเซล Canvas จะให้ผลลัพธ์เหมือนกับการแปลงพิกเซล CSS เป็นพิกเซลจริงของเบราว์เซอร์สำหรับ HTML ปกติ
  • ไม่มีการผสมสีเพิ่มเติม: หาก Canvas แมป 1:1 กับพิกเซลจริง การแปลงเป็นพิกเซลของอุปกรณ์จะไม่เพิ่มความเบลอ

รูปภาพต่อไปนี้แสดงการแปลงพิกเซลที่ส่งผลให้เนื้อหาเบลอ ตารางกริดทั้ง 3 ไม่ตรงกัน โดยองค์ประกอบ Canvas มีขนาด 360 × 180 พิกเซล CSS และขนาด Canvas คือ 300 × 150 พิกเซลตารางกริด Canvas ที่อัตราส่วนพิกเซลของอุปกรณ์ 1.5 แคนวาสจะครอบคลุมพิกเซลของอุปกรณ์ 540 × 270 พิกเซล พิกเซล CSS 1 พิกเซลครอบคลุมพิกเซลของอุปกรณ์ 1.5 พิกเซล และพิกเซล Canvas 1 พิกเซลครอบคลุมพิกเซล CSS 1.2 พิกเซล หรือพิกเซลของอุปกรณ์ 1.8 พิกเซล (แผง 1) เมื่อวาดวงกลม ตำแหน่งและรัศมีจะอยู่ในพิกเซลของ Canvas ไม่ใช่พิกเซล CSS ดังนั้นเบราว์เซอร์จึงวาดวงกลมลงในบิตแมป Canvas ขนาด 300 × 150 และลบรอยหยักที่ขอบ (แผง 2) จากนั้นเบราว์เซอร์จะขยายบิตแมปนี้ให้ครอบคลุมพิกเซลของอุปกรณ์ขนาด 540 × 270 เพื่อแสดง Canvas ดังนั้นเบราว์เซอร์จึงต้องทำการประมาณค่าในช่วง โดยพิกเซลของอุปกรณ์แต่ละพิกเซลจะใช้การผสมผสานแบบถ่วงน้ำหนักของพิกเซล Canvas ที่ใกล้ที่สุด ซึ่งมีขอบที่ลบรอยหยักอยู่แล้ว ขอบจะถูกผสมเป็นครั้งที่ 2 และกระจายไปทั่วพิกเซลของอุปกรณ์หลายพิกเซล วงกลมจึงดูเบลอ (แผงที่ 3) ปัญหานี้หลีกเลี่ยงได้หากตารางกริดของ Canvas ตรงกับพิกเซลของอุปกรณ์แบบ 1:1 จากนั้นเบราว์เซอร์จะลบรอยหยักที่ขอบเพียงครั้งเดียวบนตารางกริดของอุปกรณ์โดยตรง และแสดงบิตแมปโดยไม่ต้องสุ่มตัวอย่างใหม่ เพื่อให้ขอบยังคงคมชัด (แผงที่ 4)

แผนภาพ 4 แผงที่เปรียบเทียบตารางพิกเซลสำหรับขอบโค้ง: 1. ตารางกริด CSS (360 x 180), Canvas (300 x 150) และอุปกรณ์ (540 x 270) ที่ไม่สอดคล้องกัน 2. เส้นโค้งที่ปรับให้เรียบเนียนบนตารางกริด Canvas ขนาด 300 x 150 ที่หยาบ 3. บิตแมปนั้นจะถูกประมาณค่าในช่วงลงในตารางกริดของอุปกรณ์ขนาด 540 x 270 โดยจะลบรอยหยักเป็นครั้งที่ 2 และเบลอขอบ 4. Canvas ขนาด 540 x 270 ที่ตรงกับตารางกริดของอุปกรณ์แบบ 1 ต่อ 1 โดยมีการลบรอยหยัก 1 ครั้งเพื่อให้ขอบคมชัด
วิธีที่ CSS, Canvas และตารางกริดพิกเซลของอุปกรณ์ที่ไม่สอดคล้องกันทำให้เกิดการลดรอยหยักซ้ำ (แผง 1-3) เมื่อเทียบกับตารางกริด Canvas ต่ออุปกรณ์แบบ 1:1 (แผง 4)

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

การเปรียบเทียบข้อมูลคู่กันของการ์ด HTML ที่มีข้อความ &quot;Hello from the DOM! การ์ดนี้เป็น HTML ที่วาดลงใน Canvas' ซ้าย: วาดบนตารางกริด Canvas ขนาด 300 x 150 เริ่มต้น ซึ่งดูเบลอ ขวา: วาดบนตารางกริด Canvas ขนาด 800 x 400 ที่ปรับขนาดด้วย ResizeObserver ซึ่งดูคมชัด
Canvas เดียวกันขนาด 400 x 200 พิกเซล CSS บนหน้าจอที่มี DPR เท่ากับ 1.5 ซ้าย: ตารางกริดของ Canvas ขนาด 400 x 200 ขวา: ปรับตาม DPR เป็นตารางกริดขนาด 600 x 300

ปรับขนาดตารางกริดของ Canvas ด้วย ResizeObserver

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

หากต้องการให้ตารางกริดของ Canvas ซิงค์กับ Canvas บนหน้าจอ ให้ปรับขนาดด้วย ResizeObserver และปรับตารางกริดของ Canvas เป็น devicePixelContentBoxSize เสมอ

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);
  canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

แปลงพิกเซล CSS เป็นพิกัดตารางกริดของ Canvas

เครื่องจัดการ paint มักจะรวมการวัด DOM เข้ากับคำสั่งประมวลผล เช่น เพื่อจัดตำแหน่งองค์ประกอบตามขนาด ทั้ง 2 รายการใช้หน่วยที่แตกต่างกัน ดังนี้

  • พิกเซลของ Canvas จะอธิบายตารางกริดของ Canvas ซึ่งมีขนาดตาม canvas.width และ canvas.height เช่น พารามิเตอร์ dx, dy, dwidth และ dheight ของเมธอดการวาด drawElementImage() หรือพารามิเตอร์ API ของ Canvas อื่นๆ เช่น fillRect() และ translate()
  • พิกเซล CSS อธิบายหน้าเว็บ การวัด DOM เช่น offsetWidth และ clientWidth รวมถึงพิกัดเหตุการณ์ตัวชี้ เช่น offsetX จะอยู่ในพิกเซล CSS

ภาพรวมของหน่วยที่ใช้โดยพารามิเตอร์ API ต่างๆ มีดังนี้

ค่า หน่วย
canvas.width และ canvas.height พิกเซลตารางกริดของ Canvas
พารามิเตอร์การวาดภาพ 2 มิติ ซึ่งรวมถึงdx dy dwidth และdheightของdrawElementImage() พิกเซลตารางกริดของ Canvas
width และ height ส่งคืนโดย canvas.getElementImageDefaultSize() พิกเซลตารางกริดของ Canvas
สี่เหลี่ยมผืนผ้าต้นทาง: sx, sy, swidth และ sheight ของ drawElementImage() และ texElementSubImage2D(); sourceX, sourceY, sourceWidth และ sourceHeight ของ drawElementImageToTexture() พิกเซล CSS
ขนาดพื้นผิว: width และ height ตัวเลือกจาก texElementSubImage2D(); size จาก drawElementImageToTexture() พิกเซลพื้นผิว โดยค่าเริ่มต้น ขนาดขององค์ประกอบในหน่วยพิกเซลของตารางกริดของ Canvas จะปัดเศษขึ้น
เรขาคณิตขององค์ประกอบ: getElementTransform() และตัวเลือก canvasTransform และ canvasClip ของ updateElementGeometry() พิกเซล CSS
การวัด DOM เช่น offsetWidth, clientWidth, getBoundingClientRect(), พิกัดเหตุการณ์ตัวชี้ และ contentBoxSize ของรายการ ResizeObserver พิกเซล CSS
devicePixelContentBoxSize ของรายการ ResizeObserver พิกเซลของอุปกรณ์

หากใช้หน่วยผิด ขนาดของเนื้อหา HTML ที่วาดลงใน Canvas อาจไม่เป็นไปตามที่คุณคาดหวัง คุณต้องแปลงค่าพิกเซล CSS เป็นพิกเซลตารางกริดของ Canvas เมื่อวาด HTML ลงใน Canvas โดยคำนวณอัตราส่วนระหว่างขนาดตารางกริดของ Canvas กับขนาด CSS ของ Canvas ซึ่งก็คือ canvas.width / cssContentWidth ในแนวนอน และ canvas.height / cssContentHeight ในแนวตั้ง Resize observer เป็นตำแหน่งที่สะดวกในการคำนวณอัตราส่วนดังนี้

// The number of canvas grid pixels per CSS pixel
let scaleX = 1;
let scaleY = 1;

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  const cssContentWidth = entry.contentRect.width;
  const cssContentHeight = entry.contentRect.height;

  // Best practice: make the canvas grid match the physical pixel grid
  if (dpc) {
    canvas.width = dpc[0].inlineSize;
    canvas.height = dpc[0].blockSize;
  } else {
    canvas.width = Math.round(cssContentWidth * window.devicePixelRatio);
    canvas.height = Math.round(cssContentHeight * window.devicePixelRatio);
  }

  scaleX = canvas.width / cssContentWidth;
  scaleY = canvas.height / cssContentHeight;
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

จากนั้นคูณค่าพิกเซล CSS ด้วย scaleX และ scaleY ก่อนส่งไปยังเมธอดการวาด เช่น หากต้องการจัดองค์ประกอบที่วาดได้ให้อยู่กึ่งกลางใน Canvas ให้ทำดังนี้

const ctx = canvas.getContext('2d');
const card = document.getElementById('card');

canvas.onpaint = () => {
  ctx.reset();

  // offsetWidth and offsetHeight are in CSS pixels.
  // Convert them to canvas grid coordinates.
  const width = card.offsetWidth * scaleX;
  const height = card.offsetHeight * scaleY;
  const x = (canvas.width - width) / 2;
  const y = (canvas.height - height) / 2;

  // drawElementImage() takes canvas grid coordinates.
  ctx.drawElementImage(card, x, y);
};

สุดท้าย เนื่องจาก drawElementImage() วาดกล่องเส้นขอบขององค์ประกอบ ให้ใช้ box-sizing: border-box เพื่อรวมระยะห่างจากขอบและเส้นขอบใน width และ height ของการ์ด เพื่อให้ระบบวาดการ์ดตามขนาดที่คุณตั้งไว้ ดังนี้

#card {
  /* Make width and height include padding and border */
  box-sizing: border-box;
  width: 200px;
  padding: 16px;
}

ทำความเข้าใจการช่วยเหลือพิเศษของ HTML-in-Canvas

โดยทั่วไป เนื้อหา HTML ของ <canvas content="drawable"> จะแสดงต่อการช่วยเหลือพิเศษในลักษณะเดียวกับ DOM ปกติ ซึ่งหมายความว่าจะแสดงในแผนผังการช่วยเหลือพิเศษ เว้นแต่จะซ่อนไว้อย่างชัดเจนโดยใช้แอตทริบิวต์ เช่น aria-hidden="true" หรือ inert อย่างไรก็ตาม มีข้อยกเว้นที่สำคัญอย่างหนึ่งคือองค์ประกอบ drawable ที่ยังไม่ได้สร้างหรืออัปเดตเรขาคณิตที่แสดงผล

องค์ประกอบ drawable ภายใน <canvas content="drawable"> ไม่มีเรขาคณิตที่แสดงผลในกรณีต่อไปนี้

  • แต่ยังไม่ได้แสดงผลด้วย drawElementImage() ซึ่งจะอัปเดตเรขาคณิตโดยอัตโนมัติตามค่าเริ่มต้น
  • เรขาคณิตขององค์ประกอบยังไม่ได้รับการอัปเดตอย่างชัดเจนด้วย updateElementGeometry()

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

ตัวอย่างต่อไปนี้จะคงความหมายของรูปภาพ รูป และคำอธิบายภาพไว้ขณะวาดรูปภาพและรูปแยกกัน

<canvas id="canvas" width="400" height="300" content="drawable">
  <figure id="figure">
    <img drawable id="image" src="..." alt="image alt text" />
    <figcaption drawable id="caption">A caption</figcaption>
  </figure>
</canvas>
<script>
  const ctx = canvas.getContext('2d');

  canvas.onpaint = () => {
    ctx.reset();
    // Note: Because `preserveElementGeometry` defaults to false, the
    // `drawElementImage` call automatically updates geometry, ensuring
    // the image and caption have geometry information for accessibility.
    ctx.drawElementImage(image, 0, 0);
    ctx.drawElementImage(caption, 0, 200);
  };
</script>

กลยุทธ์สำรอง

HTML-in-Canvas API มีให้บริการใน Chrome 157 และเบราว์เซอร์บางรุ่นอาจไม่รองรับ โปรดตรวจสอบว่าเบราว์เซอร์รองรับ API หรือไม่ และเปลี่ยนไปใช้โซลูชันอื่นหากจำเป็น เพื่อให้โซลูชันทำงานได้อย่างราบรื่นสำหรับทุกคน

export function supportsHtmlInCanvas() {
  if (typeof document === 'undefined') return false;
  const canvas = document.createElement('canvas');
  canvas.setAttribute('content', 'drawable');
  return canvas.content === 'drawable';
}

หากระบบไม่รองรับ API คุณสามารถเลือกทางเลือกใดทางเลือกหนึ่งต่อไปนี้ได้ โดยขึ้นอยู่กับเป้าหมายการใช้งาน

ขั้นตอนถัดไป