חדש: גרסת המקור לניסיון של HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

במשך שנים, מפתחי אתרים נאלצו לבחור בין שתי אפשרויות קשות כשבנו אפליקציות ויזואליות מורכבות ואינטראקטיביות מאוד באינטרנט: להסתמך על DOM בגלל התכונות הסמנטיות העשירות שלו, או לבצע רינדור ישירות לאלמנט <canvas> כדי להשיג ביצועים גרפיים ברמה נמוכה.

עם HTML-in-Canvas API הניסיוני החדש – שזמין עכשיו בגרסת מקור לניסיון – אתם לא צריכים לבחור. באמצעות ה-API הזה אפשר לצייר תוכן DOM ישירות על בד ציור דו-ממדי או על טקסטורה של WebGL/WebGPU, תוך שמירה על ממשק משתמש אינטראקטיבי ונגיש שמחובר לתכונות המועדפות בדפדפן. שילוב של HTML עם עיבוד גרפי ברמה נמוכה מאפשר ליצור חוויות שלא היו אפשריות בעבר.

ההבדל בין DOM ל-Canvas

כדי להבין את היכולות של ה-API החדש הזה, כדאי לבחון את היתרונות היחסיים של DOM ו-Canvas.

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

לעומת זאת, Canvas (ו-WebGL/WebGPU) מאפשר גישה ברמה נמוכה כדי להפעיל רשת של פיקסלים לגרפיקה דו-ממדית ותלת-ממדית מתקדמת מאוד. משחקים ואפליקציות אינטרנט מורכבות (כמו Google Docs או Figma) דורשים גישה ברמה נמוכה עם ביצועים טובים. מכיוון שהקנבס הוא בעצם רשת של פיקסלים, כדי לתמוך בתכונות כמו טקסט רספונסיבי היה צריך לוגיקה מורכבת של ממשק משתמש מותאם אישית, מה שהגדיל באופן משמעותי את גודל החבילה. חשוב לציין שכל התכונות העוצמתיות של הדפדפן שמשולבות ב-DOM נשברות לחלוטין כשממשק המשתמש נלכד בתוך רשת פיקסלים סטטית של Canvas.

היתרונות של העברת ה-DOM אל Canvas

ה-API של HTML ב-Canvas הוא הגשר שמאפשר לכם ליהנות מהמיטב שבשני העולמות. אם מציבים HTML בתוך רכיב <canvas> ומסנכרנים את הטרנספורמציה שלו, התוכן נשאר אינטראקטיבי לחלוטין וכל השילובים עם הדפדפן פועלים באופן אוטומטי.

אלה היתרונות של שימוש ב-DOM לטיפול בממשק המשתמש בתוך רכיב <canvas>:

  • פריסת טקסט ועיצוב: פריסת טקסט ועיצוב פשוטים, כולל טקסט רב-שורה או דו-כיווני עם סגנונות CSS.
  • אמצעי בקרה של טפסים: אמצעי בקרה של טפסים שקל יותר להשתמש בהם, עם אפשרויות רבות להתאמה אישית.
  • בחירת טקסט, העתקה/הדבקה ולחיצה ימנית: משתמשים יכולים להדגיש טקסט בתוך סצנות תלת-ממד או ללחוץ לחיצה ימנית על תפריטי הקשר באופן טבעי.
  • בחירת טקסט, העתקה/הדבקה ולחיצה ימנית: משתמשים יכולים להדגיש טקסט בתוך סצנות תלת-ממד או ללחוץ לחיצה ימנית על תפריטי הקשר באופן טבעי.
  • נגישות: התוכן שמעובד בתוך אזור הציור נחשף לעץ הנגישות. מערכות נגישות יכולות לנתח את ממשק המשתמש כמו HTML רגיל, ולחשוף אותו למערכות כמו קוראי מסך.
  • Find-in-page: המשתמשים יכולים להשתמש בחיפוש בדף (Ctrl/Cmd+F) כדי לחפש טקסט, והדפדפן יסמן אותו ישירות בטקסטורות של WebGL.
  • Find-in-page: המשתמשים יכולים להשתמש בחיפוש בדף (Ctrl/Cmd+F) כדי לחפש טקסט, והדפדפן יסמן אותו ישירות בטקסטורות של WebGL.
  • זמינות להוספה לאינדקס ויצירת ממשק לסוכן AI: סורקי אינטרנט וסוכני AI יכולים לאנדקס ולקרוא את הטקסט שמוצג בסצנות הדו-ממד והתלת-ממד שלכם בצורה חלקה.
  • שילוב תוספים: תוספים לדפדפן פועלים באופן מקורי. לדוגמה, תוסף להחלפת טקסט יעודכן אוטומטית בטקסט שמוצג ברשתות התלת-ממד.
  • שילוב של כלי הפיתוח: אתם יכולים לבדוק את התוכן של רכיבי Canvas, כולל רכיבי ממשק משתמש של WebGL/WebGPU, ישירות בכלי הפיתוח ל-Chrome. אפשר לשנות סגנון CSS בכלי לבדיקת רכיבים ולראות את העדכון שלו באופן מיידי במרקם התלת-ממדי.

תרחישים כלליים לדוגמה

ה-API הזה פותח פוטנציאל מדהים בכמה תחומים:

  • אפליקציות מבוססות-בד ציור גדולות: אפליקציות אינטרנט כבדות כמו Google Docs,‏ Miro או Figma יכולות עכשיו לעבד רכיבי ממשק משתמש מורכבים של אפליקציות באופן מקורי בסביבות העבודה מבוססות-בד הציור שלהן, וכך לשפר את הנגישות ולהקטין את משקל החבילה.
  • סצנות ומשחקים בתלת-ממד: אתרים שיווקיים, חוויות WebXR סוחפות ומשחקי אינטרנט יכולים עכשיו להציב ממשק משתמש אינטראקטיבי לחלוטין בסצנות תלת-ממדיות – כמו ספר תלת-ממדי שמשתמש בטקסט DOM אמיתי, או מסוף במשחק שתומך באופן מקורי בהעתקה ובהדבקה.

איך משתמשים ב-API?

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

דרישות מוקדמות

‫HTML-in-Canvas API נמצא בגרסת מקור לניסיון ב-Chrome מגרסה 148 עד גרסה 150. כדי לבדוק את התכונה באתר שלכם, צריך להשתמש ב-Chrome Canary מגרסה 149 ואילך עם הפעלת האפשרות chrome://flags/#canvas-draw-element. כדי להפעיל את ה-API למשתמשים אחרים, צריך להירשם לתקופת הניסיון של Origin.

שלב 1: הגדרה בסיסית של Canvas

קודם מוסיפים את מאפיין layoutsubtree לתג <canvas>. כך הדפדפן מודע לתוכן שמוטמע בתוך רכיב ה-Canvas, והוא מוכן להצגה בתוך רכיב ה-Canvas ולחשיפה לעצי נגישות.

<canvas id="canvas" style="width: 200px; height: 200px;" layoutsubtree>
  <div id="form_element">
    <label for="name">Name:</label> <input id="name" type="text">
  </div>
</canvas>

שינוי הגודל של רשת הקנבס

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

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);

שלב 2: עיבוד

בהקשר דו-ממדי, משתמשים בשיטה drawElementImage. צריך לעשות את זה בתוך האירוע paint, שמופעל בכל פעם שהרכיב מצייר מחדש – לדוגמה, במהלך הדגשת טקסט או קלט של משתמשים. חשוב לעדכן את טרנספורמציית ה-CSS של הרכיב עם ערך ההחזרה כדי שהאינטראקטיביות תמשיך לפעול.

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

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

  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Use the transform returned later on...
};

עיבוד באמצעות WebGL

ב-WebGL, משתמשים ב-texElementImage2D. הפונקציה פועלת באופן דומה ל-texImage2D, אבל מקבלת את רכיב ה-DOM כמקור.

canvas.onpaint = () => {
  if (gl.texElementImage2D) {
    gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
  }
};

עיבוד באמצעות WebGPU

‫WebGPU משתמש בשיטה copyElementImageToTexture בתור המכשיר, בדומה ל-copyExternalImageToTexture:

canvas.onpaint = () => {
  root.device.queue.copyElementImageToTexture(
    valueElement,
    { texture: targetTexture }
  );
};

שלב 3: מעדכנים את הטרנספורמציה של ה-CSS

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

במקרה של הקשר דו-ממדי, מחילים את הטרנספורמציה שמוחזרת על ידי קריאת הרינדור אל .style.transform property:

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

canvas.onpaint = () => {
  ctx.reset();
  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Sync the DOM location with the drawn location
  form_element.style.transform = transform.toString();
};

ב-WebGL או ב-WebGPU, המיקום של רכיב במסך תלוי באופן שבו קוד ה-Shader משתמש בטקסטורה של הפלט, ואי אפשר להסיק אותו מהקשר העיבוד של Canvas. עם זאת, אם תוכנית ההצללה שלכם משתמשת בהטלה של תצוגת מודל טיפוסית כדי לצייר את המרקם, תוכלו להשתמש בפונקציה החדשה והנוחה element.getElementTransform() כדי לחשב טרנספורמציה שאפשר להשתמש בה באותו אופן כמו ערך ההחזרה מ-drawElementImage(). כדי לעשות זאת, צריך לבצע את הפעולות הבאות:

  • המרת מטריצת MVP של WebGL למטריצת DOM.
  • מנרמלים את רכיב ה-HTML. הגודל של רכיבי HTML מוגדר בפיקסלים (לדוגמה, רוחב של 200px). עם זאת, ב-WebGL, בדרך כלל האובייקטים נחשבים ל'ריבועים יחידתיים', למשל, בטווח שבין 0 ל-1. אם לא תבצעו נורמליזציה, לחצן בגודל 200px ייראה גדול פי 200.
  • מיפוי לאזור התצוגה של בד הציור. השלב הזה הוא שלב 'שינוי קנה המידה': הוא מרחיב את המתמטיקה של יחידת המרחב כדי שתתאים למידות הפיקסלים בפועל של הרכיב <canvas> במסך. היא גם הופכת את ציר ה-Y, כי ב-WebGL, הכיוון למעלה הוא חיובי, אבל ב-CSS, הכיוון למטה הוא חיובי.
  • חישוב הטרנספורמציה הסופית. מכפילים את המטריצות לפי הסדר: Viewport * MVP * Normalization. השילוב שלהן למטריצת טרנספורמציה סופית אחת יוצר 'מיפוי' שמציין לדפדפן בדיוק איפה צריך למקם את שכבת רכיב ה-HTML כדי שתהיה מיושרת עם הציור בתלת-ממד.
  • מחילים את הטרנספורמציה על רכיב ה-HTML. הפעולה הזו מעבירה את שכבת רכיב ה-HTML כך שהיא תמוקם ישירות מעל הפיקסלים המעובדים שלה. כך תוכלו לוודא שכאשר משתמש לוחץ על לחצן או בוחר טקסט, הוא לוחץ על רכיב ה-HTML האמיתי.
if (canvas.getElementTransform) {
  // 1. Convert WebGL MVP Matrix to DOM Matrix
  const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));

  // 2. Normalize the HTML element (pixels -> 1x1 unit square)
  const width = targetHTMLElement.offsetWidth;
  const height = targetHTMLElement.offsetHeight;

  const cssToUnitSpace = new DOMMatrix()
    .scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y
    .translate(-width / 2, -height / 2); // Center the element

  // 3. Map to the canvas viewport
  const clipToCanvasViewport = new DOMMatrix()
    .translate(canvas.width / 2, canvas.height / 2) // Move origin to center
    .scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions

  // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)
  const screenSpaceTransform = clipToCanvasViewport
      .multiply(mvpDOM)
      .multiply(cssToUnitSpace);

  // 5. Apply to the transform
  const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
  if (computedTransform) {
    targetHTMLElement.style.transform = computedTransform.toString();
  }
}

תמיכה בספריות וב-framework

חלק מהספריות הפופולריות כבר כוללות תמיכה בתכונה HTML-in-Canvas.

Three.js

עדכון מטריצות באופן ידני יכול להיות מייגע, ולכן מסגרות כבר מצטרפות לטרנד. ‫Three.js כולל תמיכה ניסיונית באמצעות ה-API החדש THREE.HTMLTexture:

const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

PlayCanvas

‫PlayCanvas תומך גם ב-HTML-in-Canvas באמצעות ה-API של הטקסטורה:

// Wait for the 'paint' event to set the source
canvas.addEventListener('paint', () => {
    htmlTexture.setSource(htmlElement);
}, { once: true });
canvas.requestPaint();

// Keep up to date
canvas.addEventListener('paint', onPaintUpload);

const material = new pc.StandardMaterial();
material.diffuseMap = htmlTexture;
material.update();

הדגמות

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

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

  • הספר בתלת-ממד: ספר בתלת-ממד שעבר עיבוד באמצעות WebGL ומשתמש בפריסת HTML לדפים שלו. המשתמשים יכולים להחליף גופנים באמצעות CSS. התרגום המובנה מבוסס על DOM, ולכן הוא פועל באופן מיידי, וסוכני AI יכולים לחלץ את הטקסט בפחות מורכבות.
  • ממשקי משתמש תלת-ממדיים אינטראקטיביים: פס גלילה של ג'לי ב-WebGPU ששובר את האור על סמך מודל תלת-ממדי בסיסי, ועדיין מגיב למאפייני שלבים רגילים של HTML <input type="range">.
  • טקסטורות מונפשות: שלט חוצות תלת-ממדי דינמי שמציג עיפרון SVG מונפש באמצעות DOM ישירות לטקסטורה של WebGL בלי צורך בלולאת אנימציה מותאמת אישית.
  • שכבות-על שבירות: שכבת טיפוגרפיה אינטראקטיבית שעברה עיוות על ידי סמן תלת-ממדי נע, אבל אפשר לבחור אותה ולחפש בה באמצעות התכונה 'חיפוש בדף'.

כדאי לעיין באוסף ההדגמות שנוצרו על ידי הקהילה. אם רוצים שהדגמה של HTML-in-Canvas תופיע באוסף הזה, אפשר ליצור בקשת משיכה כדי להוסיף אותה.

מגבלות

ממשק ה-API הזה הוא עוצמתי, אבל יש לו כמה מגבלות מכוונות:

  • תוכן ממקורות שונים: מטעמי אבטחה ופרטיות, ה-API לא פועל עם תוכן iframe ממקורות שונים.
  • גלילה ב-Main thread: ציור של HTML ב-Canvas מתבצע באמצעות JavaScript, ולכן גלילה ואנימציות לא יכולות להתעדכן באופן עצמאי מ-JavaScript, כמו שהן יכולות מחוץ ל-Canvas. מפתחים צריכים לשקול בקפידה את מאפייני הביצועים של הצבת תוכן שניתן לגלול בתוך אזור ציור לעומת גלילה של כל אזור הציור.

משוב

אם אתם מנסים את HTML-in-Canvas API, נשמח לשמוע מכם! אתם יכולים להירשם לגרסת מקור לניסיון כדי להפעיל את התכונה באתר שלכם בזמן שהיא בשלב הניסיוני, וכך לעזור לנו לעצב את ה-API. אפשר גם לדווח על בעיה כדי לשלוח משוב.

משאבים