‫HTML ב-Canvas: מושגים טכניים

‫HTML-in-Canvas API מאפשר להציג עצי משנה של DOM אינטראקטיביים ונגישים ישירות בהקשרים של קנבס ב-2D, ב-WebGL או ב-WebGPU.

המדריך הזה מתאר מושגים טכניים של HTML ב-Canvas שמשותפים לכל ההקשרים של Canvas:

הכנת אזור העריכה לעיבוד HTML

כדי לעבד רכיבי HTML בתוך <canvas>, צריך להגדיר את שני המאפיינים הבאים:

  • מגדירים את 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>

הסבר על עץ המשנה של ה-drawable

כשמכינים 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 (עץ משנה א') חלונית רקע, כותרת ופסקה מוצג כשמתקשרים אל drawElementImage(parentElement, ...)
#nestedChildElement (עץ משנה ב') הרקע והטקסט של הכפתור לא נכלל בעץ המשנה של #parentElement; מוצג בקריאה נפרדת ל-drawElementImage(nestedChildElement, ...)
כפתור drawable מוטמע בתוך כרטיס drawable, וכל אחד מהם משורטט באמצעות קריאת drawElementImage() משלו.

הטמעה של רכיבים שניתן לצייר מאפשרת לשמור על קשרים סמנטיים ב-HTML, וגם נותנת לכם את החופש להוסיף שכבות, למקם או להוסיף אנימציה לרכיבי צאצא באופן עצמאי בסצנת ה-canvas.

הסבר על האירוע paint

ה-API של HTML-in-Canvas מציג את האירוע 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 content="drawable"> בתצוגת עץ, הדפדפן שולח אירועי paint בסדר הפוך של עץ ה-DOM (צאצאים מופעלים לפני אבות קדמונים). כך אפשר לוודא שרכיבי ה-canvas הצאצאים יעברו עיבוד לפני שרכיבי ה-canvas ההורים יבצעו קומפוזיציה שלהם.

נניח שבדוגמה הבאה, הרכיב הפנימי nestedElement צריך להשתנות מבחינה חזותית (לדוגמה, צבע הרקע שלו ב-CSS משתנה כשהמשתמש מציב את הסמן מעליו). קודם כול, האירוע paint מופעל ב-canvas של nestedCanvas. המאזין שלו מנקה את מאגר הנתונים הזמני של הצאצא וקורא ל-ctx.drawElementImage(nestedElement, ...) כדי לעבד את המצב המעודכן. ואז האירוע paint מופעל ב-parentCanvas. המאזין שלו מושך את 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().
  • לדווח על גבולות הרכיב לטכנולוגיות מסייעות, כמו הגדלת המסך.

במצב דו-ממדי, drawElementImage() מעדכן את הגיאומטריה באופן אוטומטי כברירת מחדל. הארגומנטים שאתם מעבירים אליה, יחד עם הסגנון המחושב של הרכיב והטרנספורמציה הנוכחית של ההקשר, מספיקים כדי לקבוע איפה הרכיב יופיע.

בהקשרים של WebGL ו-WebGPU, הדפדפן לא יכול לקבוע את הגיאומטריה. רכיב HTML מועתק לטקסטורה, ותוכנית ההצללה יכולה למפות את הטקסטורה הזו לסצנה בכל דרך שרירותית. כדי להודיע לדפדפן איפה הרכיב מופיע, מעבירים טרנספורמציה אל updateElementGeometry().

כשהקוד מפסיק להפעיל את updateElementGeometry(), הדפדפן לא יודע יותר איפה נמצאים רכיבי ה-HTML בכרטיס התלת-ממדי. הכרטיס ממשיך להסתובב, אבל הקליקים עדיין מובילים למקום שבו הוא היה.

כדי לעדכן את הגיאומטריה, מעבירים טרנספורמציה מרחבית ל-method. כך יוצרים טרנספורמציה בהקשרים שונים:

ניהול סדר הערימה של בדיקת מיקום המצביע

כברירת מחדל, בכל פעם שהגיאומטריה של רכיב מתעדכנת, הדפדפן מעביר אותו לראש סטאק האינטראקציות (הפריט האחרון שצויר = יעד הקליק העליון). אפשר לשלוט בהתנהגות הזו באמצעות 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, הדפדפן פועל בשלוש מערכות קואורדינטות בו-זמנית: פיקסלים של CSS, פיקסלים של המכשיר ופיקסלים של רשת ה-canvas.

פיקסלים של CSS משמשים למדידה ולמיקום של תוכן במהלך הפריסה. לאחר מכן, הדפדפן משנה את קנה המידה שלהם ב-devicePixelRatio כדי לבצע רסטריזציה של הדף לפי הפיקסלים של המכשיר הפיזי. מכיוון שפיקסלים של CSS הם יחידה לוגית, המיקומים והגדלים מופיעים לעיתים קרובות כשברים של פיקסל במכשיר (לדוגמה, ‎10.5 CSS px × DPR של ‎1.5 = 15.75 device px). מסך יכול לצבוע רק פיקסלים שלמים, ולכן הדפדפן צריך לפתור כל מה שנופל ביניהם. הוא עושה את זה בשתי דרכים:

  • הצמדה: הגבולות של התיבות, השכבות והגבולות מעוגלים לפיקסל המכשיר השלם הקרוב ביותר כדי לשמור על חדות.
  • החלקת קצוות (anti-aliasing): בגליפים של טקסט, בעקומות ובקצוות שעברו טרנספורמציה, הכיסוי החלקי משולב בפיקסלים הסמוכים כדי שהם עדיין ייראו חלקים ובמקום הנכון.

ל-<canvas> יש רשת פיקסלים משלו, שמוגדרת על ידי המאפיינים width ו-height, והיא נפרדת גם מפיקסלים של CSS וגם מפיקסלים של המכשיר. כדי להציג אותו, הדפדפן דוגם מחדש את מפת הסיביות של ה-canvas לרשת הפיקסלים הפיזית, וממזג פיקסלים שכנים של ה-canvas כדי למלא כל פיקסל במכשיר. כדי ללמוד איך הדפדפן ממפה בין CSS, לבין canvas ופיקסלים של המכשיר, קראו את המאמר עיבוד מושלם של פיקסלים באמצעות devicePixelContentBox.

כל רכיבי ה-HTML של drawable מוגדרים בפיקסלים לוגיים. מכיוון שתוכן שמשורטט ב-canvas יכול לעבור שתי המרות, מפיקסלים של CSS לרשת של ה-canvas ואז מהרשת של ה-canvas לפיקסלים פיזיים, כל שלב שלא תואם בדיוק מוסיף עוד סבב של מיזוג, והתוצאה יכולה להיראות מטושטשת. כדי למנוע את זה, צריך להתאים את רשת הפיקסלים של אזור העריכה לרשת הפיקסלים הפיזית. יש לכך שתי מטרות:

  • עיבוד עקבי: אם ה-canvas ממפה 1:1 לפיקסלים פיזיים, המרת פיקסלים של CSS לפיקסלים של canvas מניבה את אותה תוצאה כמו ההמרה של הדפדפן מפיקסלים של CSS לפיקסלים פיזיים ל-HTML רגיל.
  • אין מיזוג נוסף: אם ה-canvas ממפה 1:1 לפיקסלים פיזיים, ההמרה שלו לפיקסלים של המכשיר לא מוסיפה טשטוש.

בתמונה הבאה מוצגת המרת פיקסלים שגורמת לתוכן מטושטש. שלוש הרשתות לא מיושרות: רכיב ה-canvas הוא בגודל 360 × 180 פיקסלים ב-CSS, וגודל ה-canvas הוא 300 × 150 פיקסלים ברשת ה-canvas. ביחס פיקסלים של 1.5, אזור הציור מכסה 540 × 270 פיקסלים במכשיר. פיקסל CSS אחד משתרע על פני 1.5 פיקסלים של המכשיר, ופיקסל אחד של אזור העריכה משתרע על פני 1.2 פיקסלים של CSS, או 1.8 פיקסלים של המכשיר (חלונית 1). כשהעיגול משורטט, המיקום והרדיוס שלו הם בפיקסלים של אזור הציור, ולא בפיקסלים של CSS, כך שהדפדפן משרטט אותו במפת הסיביות של אזור הציור בגודל 300 × 150 ומבצע החלקת קצוות (לוח 2). כדי להציג את אזור הציור, הדפדפן מותח את מפת הסיביות הזו על פני 540 × 270 פיקסלים של המכשיר, כך שהדפדפן צריך לבצע אינטרפולציה: כל פיקסל של המכשיר מקבל שילוב משוקלל של הפיקסלים הקרובים ביותר של אזור הציור, שכבר מכילים את הקצה שעבר החלקת קצוות. הקצה מטושטש בפעם השנייה ומתפרס על פני כמה פיקסלים במכשיר, כך שהעיגול נראה מטושטש (חלונית 3). אפשר למנוע את זה אם רשת ה-canvas תואמת לפיקסלים של המכשיר ביחס של 1:1. לאחר מכן, הדפדפן יבצע החלקת קצוות (anti-aliasing) פעם אחת בלבד, ישירות ברשת המכשיר, ויציג את מפת הסיביות ללא דגימה מחדש, כך שהקצה יישאר חד (חלונית 4):

דיאגרמה עם ארבעה פאנלים שמשווה בין רשתות פיקסלים של קצה מעוקל: 1. רשתות לא מיושרות של CSS (360 על 180), אזור העריכה (300 על 150) והמכשיר (540 על 270). 2. העקומה עברה החלקת קצוות (anti-aliasing) על רשת הקנבס הגסה בגודל 300 על 150. 3. מפת הסיביות הזו עוברת אינטרפולציה לרשת המכשיר בגודל 540 על 270, החלקת קצוות בפעם השנייה וטשטוש הקצה. 4. קנבס בגודל 540 על 270 שתואם לרשת המכשיר ביחס של 1 ל-1, עם החלקת קצוות (anti-aliasing) פעם אחת כדי לקבל קצה חד.
איך חוסר התאמה בין רשתות הפיקסלים של CSS, של רכיב ה-canvas ושל המכשיר גורם להחלקת קצוות כפולה (לוחות 1-3), בהשוואה לרשת של רכיב ה-canvas ושל המכשיר ביחס של 1:1 (לוח 4).

כדי שהעיבוד יהיה חד, רשת ה-canvas צריכה להתאים למספר הפיקסלים במכשיר שה-canvas מכסה: 540 × 270 בדוגמה הקודמת.

השוואה בטבלה של כרטיס HTML שכתוב בו &quot;שלום מה-DOM! הכרטיס הזה הוא HTML שמשורטט ב-canvas.' משמאל: ציור על רשת ברירת המחדל של הקנבס בגודל 300 על 150, שנראה מטושטש. משמאל: ציור על רשת קנבס בגודל 800 על 400, עם שינוי גודל באמצעות ResizeObserver, והוא נראה חד.
אותו אזור ציור, 400x200 פיקסלים של CSS, במסך עם DPR של 1.5. משמאל: רשת הקנבס בגודל 400x200. מימין: מותאם ל-DPR, רשת של 600x300.

שינוי הגודל של רשת הקנבס באמצעות צופה לשינוי גודל

גודל הרשת המתאים תלוי במכשיר, והוא עשוי להשתנות כשהפריסה משתנה, כשהמשתמש משנה את מרחק התצוגה בדף או כשהמשתמש מעביר את החלון לתצוגה אחרת. לכן, במקום להגדיר את גודל הרשת בקוד, תנו לדפדפן להגיד לכם מה הוא צריך להיות.

כדי לשמור על סנכרון בין רשת לוח הציור לבין לוח הציור במסך, תמיד צריך להגדיר את הגודל שלו באמצעות ResizeObserver ולהתאים את רשת לוח הציור ל-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 לקואורדינטות של רשת קנבס

רכיב handler של paint משלב לעיתים קרובות מדידות DOM עם בקשות להזזת פריט גרפי (draw call) – לדוגמה, כדי למקם רכיב על סמך הגודל שלו. הם משתמשים ביחידות שונות:

  • פיקסלים של קנבס מתארים את רשת הקנבס, שהגודל שלה הוא canvas.width ו-canvas.height. לדוגמה, הפרמטרים dx,‏ dy,‏ dwidth ו-dheight של שיטות השרטוט של drawElementImage(), או פרמטרים אחרים של ממשקי API של רכיב canvas, כמו fillRect() ו-translate().
  • פיקסלים של CSS מתארים את הדף. מדידות DOM כמו offsetWidth ו-clientWidth, וקואורדינטות של אירועי מצביע כמו offsetX, הן בפיקסלים של CSS.

סקירה כללית של היחידות שמשמשות את הפרמטרים השונים של ה-API:

ערך יחידות
canvas.width וגם canvas.height פיקסלים של רשת הקנבס
פרמטרים של שרטוט דו-ממדי, כולל dx,‏ dy,‏ dwidth ו-dheight מתוך drawElementImage() פיקסלים של רשת הקנבס
‫width ו-height הוחזרו על ידי canvas.getElementImageDefaultSize() פיקסלים של רשת הקנבס
מלבני מקור: sx, ‏sy, ‏swidth ו-sheight מתוך drawElementImage() ו-texElementSubImage2D(); ‏sourceX, ‏sourceY, ‏sourceWidth ו-sourceHeight מתוך drawElementImageToTexture() פיקסלים של CSS
גודל המרקם: width ו-height אפשרויות של texElementSubImage2D(); size מתוך drawElementImageToTexture() פיקסלים של טקסטורה. כברירת מחדל, גודל הרכיב בפיקסלים של רשת אזור העריכה, מעוגל כלפי מעלה
גיאומטריית הרכיב: getElementTransform(), והאפשרויות canvasTransform ו-canvasClip של updateElementGeometry() פיקסלים של CSS
מדידות DOM, כמו offsetWidth,‏ clientWidth,‏ getBoundingClientRect(), קואורדינטות של אירועי מצביע ו-contentBoxSize של רשומת צופה לשינוי גודל פיקסלים של CSS
‫devicePixelContentBoxSize של רשומה ב-ResizeObserver פיקסלים במכשיר

אם תשתמשו ביחידות הלא נכונות, יכול להיות שגודל תוכן ה-HTML שמופיע ב-canvas לא יהיה כמו שציפיתם. כשמציירים HTML ב-canvas, צריך להמיר את ערך הפיקסל הלוגי לפיקסלים ברשת של ה-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 לפני שמעבירים אותם לשיטת השרטוט. לדוגמה, כדי למרכז רכיב שניתן לשרטוט בדף השקף:

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 ב-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 לא נתמך, בהתאם ליעדי ההטמעה שלכם, אתם יכולים לבחור באחת מהחלופות הבאות:

השלבים הבאים