ขอแนะนำช่วงทดลองใช้ HTML-in-Canvas API จากต้นทาง

Thomas Nattestad
Thomas Nattestad

เป็นเวลาหลายปีแล้วที่นักพัฒนาเว็บต้องตัดสินใจเลือกสถาปัตยกรรมที่ยากลำบากเมื่อสร้างแอปพลิเคชันภาพที่ซับซ้อนและมีการโต้ตอบสูงบนเว็บ/ คุณควรใช้ DOM สำหรับฟีเจอร์ความหมายที่หลากหลายหรือแสดงผลโดยตรงไปยังองค์ประกอบ <canvas> เพื่อประสิทธิภาพกราฟิกในระดับต่ำ

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

DOM กับ Canvas

หากต้องการทำความเข้าใจประสิทธิภาพของ API ใหม่นี้ คุณควรดูจุดแข็งที่เกี่ยวข้องของทั้ง DOM และ Canvas

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

ในทางกลับกัน Canvas (และ WebGL/WebGPU) ช่วยให้เข้าถึงระดับต่ำเพื่อขับเคลื่อนตารางกริดของพิกเซลสำหรับกราฟิก 2 มิติและ 3 มิติขั้นสูง เกมและเว็บแอปที่ซับซ้อน (เช่น Google เอกสารหรือ Figma) จำเป็นต้องมีการเข้าถึงระดับต่ำที่มีประสิทธิภาพนี้ เนื่องจาก Canvas เป็นตารางกริดของพิกเซลโดยพื้นฐาน การรองรับฟีเจอร์ต่างๆ เช่น ข้อความที่ปรับเปลี่ยนตามอุปกรณ์ได้จึงต้องใช้ตรรกะ UI ที่กำหนดเองที่ซับซ้อน ซึ่งจะเพิ่มขนาดแพ็กเกจอย่างมาก ที่สำคัญคือฟีเจอร์เบราว์เซอร์ที่มีประสิทธิภาพทั้งหมดที่ผสานรวมอยู่ใน DOM จะใช้งานไม่ได้เลยเมื่อ UI ติดอยู่ในตารางกริดพิกเซล Canvas แบบคงที่

ข้อดีของการนำ DOM ไปใช้ใน Canvas

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

สิ่งที่คุณจะได้รับจากการให้ DOM จัดการ UI ภายในองค์ประกอบ <canvas> มีดังนี้

  • การจัดวางและจัดรูปแบบข้อความ: การจัดวางและจัดรูปแบบข้อความที่ง่ายขึ้น รวมถึงข้อความหลายบรรทัดหรือข้อความสองทิศทางที่มีการใช้สไตล์ CSS
  • การควบคุมแบบฟอร์ม: การควบคุมแบบฟอร์มที่แสดงออกได้และใช้งานง่ายขึ้นพร้อมตัวเลือกการปรับแต่งที่หลากหลาย
  • การเลือกข้อความ การคัดลอก/วาง และการคลิกขวา: ผู้ใช้สามารถไฮไลต์ข้อความภายในฉาก 3 มิติ หรือคลิกขวาเมนูบริบทได้โดยตรง
  • การช่วยเหลือพิเศษ: เนื้อหาที่แสดงผลภายใน Canvas จะแสดงในแผนผังการช่วยเหลือพิเศษ ระบบการช่วยเหลือพิเศษสามารถแยกวิเคราะห์ UI ได้เช่นเดียวกับ HTML ปกติ และแสดง UI ให้ระบบต่างๆ เช่น โปรแกรมอ่านหน้าจอ
  • ค้นหาในหน้าเว็บ: ผู้ใช้สามารถใช้ฟีเจอร์ค้นหาในหน้าเว็บ (Ctrl/Cmd+F) เพื่อค้นหาข้อความ และเบราว์เซอร์จะไฮไลต์ข้อความนั้นโดยตรงภายในพื้นผิว WebGL
  • การจัดทำดัชนีและอินเทอร์เฟซที่ตัวแทน AI สามารถโต้ตอบได้: Web Crawler และตัวแทน AI สามารถจัดทำดัชนีและอ่านข้อความที่แสดงผลในฉาก 2 มิติและ 3 มิติได้อย่างราบรื่น
  • การผสานรวมส่วนขยาย: ส่วนขยายของเบราว์เซอร์จะทำงานได้โดยตรง ตัวอย่างเช่น ส่วนขยายการแทนที่ข้อความจะอัปเดตข้อความที่แสดงผลในตาข่าย 3 มิติโดยอัตโนมัติ
  • การผสานรวมเครื่องมือสำหรับนักพัฒนาเว็บ: คุณสามารถตรวจสอบเนื้อหา Canvas รวมถึงองค์ประกอบ UI ของ WebGL/WebGPU ได้โดยตรงในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome ปรับแต่งสไตล์ CSS ในเครื่องมือตรวจสอบ แล้วดูการอัปเดตในพื้นผิว 3 มิติได้ทันที

กรณีการใช้งานระดับสูง

API นี้ปลดล็อกศักยภาพอันน่าทึ่งในหลายโดเมน ดังนี้

  • แอปพลิเคชันขนาดใหญ่ที่ใช้ Canvas: เว็บแอปขนาดใหญ่ เช่น Google เอกสาร, Miro หรือ Figma สามารถแสดงผลคอมโพเนนต์ UI ของแอปพลิเคชันที่ซับซ้อนลงในพื้นที่ทำงานที่ขับเคลื่อนด้วย Canvas ได้โดยตรง ซึ่งจะช่วยปรับปรุงการช่วยเหลือพิเศษและลดขนาดแพ็กเกจ
  • ฉากและเกม 3 มิติ: ตอนนี้เว็บไซต์การตลาด ประสบการณ์การใช้งาน WebXR ที่สมจริง และเกมบนเว็บสามารถวาง UI ของเว็บที่โต้ตอบได้อย่างเต็มที่ลงในฉาก 3 มิติ เช่น หนังสือ 3 มิติที่ใช้ข้อความ DOM จริง หรือเทอร์มินัลในเกมที่รองรับการคัดลอกและวางโดยตรง

วิธีใช้ API

การใช้ API จะเกิดขึ้นใน 3 ระยะ ได้แก่ การตั้งค่า Canvas การแสดงผลลงใน Canvas และการอัปเดตการแปลง 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

ตรวจสอบว่าได้ปรับขนาดตารางกริด 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: การแสดงผล

สำหรับบริบท 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

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

สำหรับกรณีบริบท 2 มิติ ให้ใช้การแปลงที่แสดงผลโดยการเรียกการแสดงผลกับ .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 อย่างไรก็ตาม หากโปรแกรม Shader ใช้การฉายภาพมุมมองโมเดลทั่วไปเพื่อวาดพื้นผิว คุณสามารถใช้ฟังก์ชันความสะดวกใหม่ element.getElementTransform() เพื่อคำนวณการแปลงที่ใช้ได้ในลักษณะเดียวกับค่าที่แสดงผลจาก drawElementImage() คุณต้องทำดังนี้เพื่อให้การดำเนินการนี้เป็นไปได้

  • แปลงเมทริกซ์ WebGL MVP เป็น เมทริกซ์ DOM
  • ทำให้องค์ประกอบ HTML เป็นปกติ องค์ประกอบ HTML จะมีขนาดเป็นพิกเซล (เช่น กว้าง 200 พิกเซล) อย่างไรก็ตาม WebGL มักจะถือว่าออบเจ็กต์เป็น "สี่เหลี่ยมจัตุรัสหน่วย" เช่น ตั้งแต่ 0 ถึง 1 หากไม่ทำให้เป็นปกติ ปุ่ม 200 พิกเซลจะดูใหญ่ขึ้น 200 เท่า
  • แมปกับวิวพอร์ต Canvas ขั้นตอนนี้เป็นระยะ "การปรับขนาด" ซึ่งจะขยายการคำนวณพื้นที่หน่วยกลับเพื่อให้ตรงกับขนาดพิกเซลจริงขององค์ประกอบ <canvas> บนหน้าจอ นอกจากนี้ยังพลิกแกน Y ด้วย เนื่องจากใน WebGL ค่าขึ้นเป็นค่าบวก แต่ใน CSS ค่าลงเป็นค่าบวก
  • คำนวณการแปลงสุดท้าย คูณเมทริกซ์ตามลำดับต่อไปนี้: Viewport * MVP * Normalization. การรวมเมทริกซ์เหล่านี้เป็นการแปลงสุดท้ายจะสร้าง "แผนที่" ที่บอกเบราว์เซอร์ได้อย่างชัดเจนว่าเลเยอร์องค์ประกอบ HTML นั้นควรอยู่ที่ใดเพื่อให้สอดคล้องกับการวาด 3 มิติ
  • ใช้การแปลงกับองค์ประกอบ 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();
  }
}

การรองรับไลบรารีและเฟรมเวิร์ก

ไลบรารียอดนิยมบางรายการได้เปิดตัวการรองรับฟีเจอร์ HTML-in-Canvas แล้ว

Three.js

การอัปเดตเมทริกซ์ด้วยตนเองอาจเป็นเรื่องน่าเบื่อ ซึ่งเป็นเหตุผลที่เฟรมเวิร์กต่างๆ เริ่มให้การรองรับแล้ว Three.js มีการรองรับในระยะทดลองใช้โดยใช้ experimental support โดยใช้ 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 โดยใช้ Texture 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 เราได้เห็นโซลูชันที่สร้างสรรค์จากชุมชนแล้ว ตั้งแต่หนังสือ 3 มิติที่แปลได้ไปจนถึงองค์ประกอบ UI ที่หักเหผ่าน Shader แก้ว ดังนี้

  • หนังสือ 3 มิติ: หนังสือ 3 มิติที่แสดงผลด้วย WebGL ซึ่งใช้เลย์เอาต์ HTML สำหรับหน้าต่างๆ ผู้ใช้สามารถเปลี่ยนแบบอักษรด้วย CSS ได้ เนื่องจากใช้ DOM การแปลในตัวจึงทำงานได้ทันที และตัวแทน AI สามารถแยกข้อความได้ง่ายขึ้น
  • UI 3 มิติแบบอินเทอร์แอกทีฟ: แถบเลื่อนเจลลี WebGPU ที่หักเหแสงตามโมเดล 3 มิติพื้นฐาน ขณะเดียวกันก็ยังคงตอบสนองต่อแอตทริบิวต์ <input type="range"> ของ HTML มาตรฐาน
  • พื้นผิวแบบเคลื่อนไหว: ป้ายโฆษณา 3 มิติแบบไดนามิกที่แสดงผลดินสอ SVG แบบเคลื่อนไหวโดยใช้ DOM ลงในพื้นผิว WebGL โดยตรงโดยไม่จำเป็นต้องมีลูปภาพเคลื่อนไหวที่กำหนดเอง
  • การวางซ้อนแบบหักเห: เลเยอร์การพิมพ์แบบอินเทอร์แอกทีฟที่บิดเบี้ยวโดยเคอร์เซอร์ 3 มิติที่เคลื่อนไหว แต่ยังคงเลือกและค้นหาได้ทั้งหมดโดยใช้ฟีเจอร์ค้นหาในหน้าเว็บ

ดูคอลเล็กชันการสาธิตที่สร้างโดยชุมชน หากต้องการให้การสาธิต HTML-in-Canvas ของคุณแสดงในคอลเล็กชันนี้ ให้สร้าง Pull Requestเพื่อเพิ่มการสาธิต

ข้อจำกัด

แม้ว่าจะมีประสิทธิภาพ แต่ API ก็มีข้อจำกัดบางประการ ดังนี้

  • เนื้อหาข้ามต้นทาง: API จะไม่ทำงานกับเนื้อหา iframe ข้ามต้นทางด้วยเหตุผลด้านความปลอดภัยและความเป็นส่วนตัว
  • การเลื่อนในเธรดหลัก: HTML-in-Canvas จะแสดงผลด้วย JavaScript ซึ่งหมายความว่าการเลื่อนและภาพเคลื่อนไหวจะอัปเดตแยกจาก JavaScript ไม่ได้เหมือนกับที่ทำได้นอก Canvas นักพัฒนาแอปควรพิจารณาลักษณะประสิทธิภาพของการใส่เนื้อหาที่เลื่อนได้ไว้ใน Canvas เทียบกับการเลื่อน Canvas ทั้งหมดอย่างรอบคอบ

ความคิดเห็น

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

แหล่งข้อมูล