HTML-in-Canvas API ช่วยให้คุณแสดงผลซับทรี DOM แบบอินเทอร์แอกทีฟที่เข้าถึงได้โดยตรงในบริบท Canvas 2D, WebGL หรือ WebGPU
คู่มือนี้จะอธิบายแนวคิดทางเทคนิคของ HTML-in-Canvas ที่ใช้กันโดยทั่วไปในบริบท Canvas ทั้งหมด ดังนี้
- เตรียม Canvas เพื่อแสดงผล HTML
- ทำความเข้าใจโครงสร้างย่อย
drawable - ทำความเข้าใจเหตุการณ์
paint - อัปเดตเรขาคณิตขององค์ประกอบ
- ทำความเข้าใจการช่วยเหลือพิเศษของ HTML-in-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 ที่ดำเนินการภายในเหตุการณ์ 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)
เพื่อให้แสดงผลได้อย่างคมชัด ตารางกริดของ Canvas ต้องตรงกับจำนวนพิกเซลของอุปกรณ์ที่ Canvas ครอบคลุม ซึ่งก็คือ 540 × 270 ในตัวอย่างก่อนหน้านี้
ปรับขนาดตารางกริดของ 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 คุณสามารถเลือกทางเลือกใดทางเลือกหนึ่งต่อไปนี้ได้ โดยขึ้นอยู่กับเป้าหมายการใช้งาน
- UI ทางเลือก
- โฆษณาซ้อนทับ HTML แบบลอย
- ไลบรารี three-html-render
- html2canvas
- dom-to-image
ขั้นตอนถัดไป
- ดูวิธีแสดงผล HTML ในบริบท 2 มิติ
- ดูวิธีแสดงผล HTML ใน Canvas ของ WebGL
- ดูวิธีแสดงผล HTML ใน Canvas ของ WebGPU
- ดูข้อมูลเกี่ยวกับกลยุทธ์สำรอง
- แจ้งปัญหาใน GitHub เพื่อแชร์ความคิดเห็น
- ดูคอลเล็กชันเดโม HTML-in-Canvas โดยชุมชน