חדש: גרסת המקור לניסיון של 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

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

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

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

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

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

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

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

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

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

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

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

  • המרת MVP Matrix של WebGL ל-DOM Matrix.
  • מנרמלים את רכיב ה-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();
  }
}

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

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

Three.js

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

משוב

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

משאבים