diff --git a/examples/src/examples/misc/html-texture.example.mjs b/examples/src/examples/misc/html-texture.example.mjs
new file mode 100644
index 00000000000..1ac6160bb3f
--- /dev/null
+++ b/examples/src/examples/misc/html-texture.example.mjs
@@ -0,0 +1,202 @@
+// @config DESCRIPTION This example demonstrates the HTML-in-Canvas proposal using texElement2D to render HTML content directly as a WebGL texture. Features graceful fallback to canvas rendering when not supported.
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+
+// Enable layoutsubtree for HTML-in-Canvas support
+canvas.setAttribute('layoutsubtree', '');
+// Alternative attribute names that might be used in different implementations
+canvas.setAttribute('data-layoutsubtree', '');
+canvas.style.contain = 'layout style paint';
+
+window.focus();
+
+const gfxOptions = {
+ deviceTypes: [deviceType],
+ glslangUrl: `${rootPath}/static/lib/glslang/glslang.js`,
+ twgslUrl: `${rootPath}/static/lib/twgsl/twgsl.js`
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+
+createOptions.componentSystems = [pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem];
+createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler];
+
+const app = new pc.AppBase(canvas);
+app.init(createOptions);
+app.start();
+
+// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
+app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
+app.setCanvasResolution(pc.RESOLUTION_AUTO);
+
+// Ensure canvas is resized when window changes size
+const resize = () => app.resizeCanvas();
+window.addEventListener('resize', resize);
+app.on('destroy', () => {
+ window.removeEventListener('resize', resize);
+});
+
+// Create an HTML element to use as texture source
+// According to the HTML-in-Canvas proposal, the element must be a direct child of the canvas
+const htmlElement = document.createElement('div');
+htmlElement.style.width = '512px';
+htmlElement.style.height = '512px';
+htmlElement.style.position = 'absolute';
+htmlElement.style.top = '0';
+htmlElement.style.left = '0';
+htmlElement.style.pointerEvents = 'none'; // Prevent interaction
+htmlElement.style.zIndex = '-1'; // Place behind canvas content
+htmlElement.style.backgroundColor = '#ff6b6b';
+htmlElement.style.background = 'linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #f9ca24)';
+htmlElement.style.borderRadius = '20px';
+htmlElement.style.padding = '20px';
+htmlElement.style.fontFamily = 'Arial, sans-serif';
+htmlElement.style.fontSize = '24px';
+htmlElement.style.color = 'white';
+htmlElement.style.textAlign = 'center';
+htmlElement.style.display = 'flex';
+htmlElement.style.flexDirection = 'column';
+htmlElement.style.justifyContent = 'center';
+htmlElement.style.alignItems = 'center';
+htmlElement.innerHTML = `
+
HTML in Canvas!
+ This texture is rendered from HTML using texElement2D
+
+
+`;
+
+// Add CSS animation
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes pulse {
+ 0% { transform: scale(1); opacity: 1; }
+ 50% { transform: scale(1.2); opacity: 0.7; }
+ 100% { transform: scale(1); opacity: 1; }
+ }
+`;
+document.head.appendChild(style);
+
+// Add the HTML element as a direct child of the canvas
+canvas.appendChild(htmlElement);
+
+// Create texture from HTML element
+const htmlTexture = new pc.Texture(device, {
+ width: 512,
+ height: 512,
+ format: pc.PIXELFORMAT_RGBA8,
+ name: 'htmlTexture'
+});
+
+// Helper function to create fallback canvas texture
+const createFallbackTexture = () => {
+ const fallbackCanvas = document.createElement('canvas');
+ fallbackCanvas.width = 512;
+ fallbackCanvas.height = 512;
+ const ctx = fallbackCanvas.getContext('2d');
+
+ if (!ctx) {
+ console.error('Failed to get 2D context');
+ return null;
+ }
+
+ const gradient = ctx.createLinearGradient(0, 0, 512, 512);
+ gradient.addColorStop(0, '#ff6b6b');
+ gradient.addColorStop(0.33, '#4ecdc4');
+ gradient.addColorStop(0.66, '#45b7d1');
+ gradient.addColorStop(1, '#f9ca24');
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, 512, 512);
+
+ ctx.fillStyle = 'white';
+ ctx.font = 'bold 36px Arial';
+ ctx.textAlign = 'center';
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
+ ctx.shadowBlur = 4;
+ ctx.shadowOffsetX = 2;
+ ctx.shadowOffsetY = 2;
+ ctx.fillText('HTML in Canvas!', 256, 180);
+
+ ctx.font = '20px Arial';
+ ctx.fillText('(Canvas Fallback)', 256, 220);
+ ctx.fillText('texElement2D not available', 256, 260);
+
+ ctx.beginPath();
+ ctx.arc(256, 320, 25, 0, 2 * Math.PI);
+ ctx.fillStyle = 'white';
+ ctx.fill();
+
+ return fallbackCanvas;
+};
+
+// Set the HTML element as the texture source
+if (device.supportsTexElement2D) {
+ console.log('texElement2D is supported - attempting to use HTML element as texture');
+ try {
+ htmlTexture.setSource(/** @type {any} */ (htmlElement));
+ console.log('Successfully set HTML element as texture source');
+ } catch (error) {
+ console.warn('Failed to use texElement2D:', error.message);
+ console.log('Falling back to canvas rendering');
+
+ const fallbackCanvas = createFallbackTexture();
+ if (fallbackCanvas) {
+ htmlTexture.setSource(fallbackCanvas);
+ }
+ }
+} else {
+ console.warn('texElement2D is not supported - falling back to canvas rendering');
+ const fallbackCanvas = createFallbackTexture();
+ if (fallbackCanvas) {
+ htmlTexture.setSource(fallbackCanvas);
+ }
+}
+
+// Create material with the HTML texture
+const material = new pc.StandardMaterial();
+material.diffuseMap = htmlTexture;
+material.update();
+
+// create box entity
+const box = new pc.Entity('cube');
+box.addComponent('render', {
+ type: 'box',
+ material: material
+});
+app.root.addChild(box);
+
+// create camera entity
+const camera = new pc.Entity('camera');
+camera.addComponent('camera', {
+ clearColor: new pc.Color(0.1, 0.1, 0.1)
+});
+app.root.addChild(camera);
+camera.setPosition(0, 0, 3);
+
+// create directional light entity
+const light = new pc.Entity('light');
+light.addComponent('light');
+app.root.addChild(light);
+light.setEulerAngles(45, 0, 0);
+
+// Update the HTML texture periodically to capture animations
+let updateCounter = 0;
+app.on('update', (/** @type {number} */ dt) => {
+ box.rotate(10 * dt, 20 * dt, 30 * dt);
+
+ // Update texture every few frames to capture HTML animations
+ updateCounter += dt;
+ if (updateCounter > 0.1) { // Update 10 times per second
+ updateCounter = 0;
+ if (device.supportsTexElement2D) {
+ htmlTexture.upload();
+ }
+ }
+});
+
+export { app };
diff --git a/examples/thumbnails/misc_html-texture_large.webp b/examples/thumbnails/misc_html-texture_large.webp
new file mode 100644
index 00000000000..a9fdce95b6d
Binary files /dev/null and b/examples/thumbnails/misc_html-texture_large.webp differ
diff --git a/examples/thumbnails/misc_html-texture_small.webp b/examples/thumbnails/misc_html-texture_small.webp
new file mode 100644
index 00000000000..b35a9456631
Binary files /dev/null and b/examples/thumbnails/misc_html-texture_small.webp differ
diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js
index d2bd94e75e1..efc8421df3b 100644
--- a/src/platform/graphics/graphics-device.js
+++ b/src/platform/graphics/graphics-device.js
@@ -840,17 +840,18 @@ class GraphicsDevice extends EventHandler {
}
/**
- * Reports whether a texture source is a canvas, image, video or ImageBitmap.
+ * Reports whether a texture source is a canvas, image, video, ImageBitmap, or HTML element.
*
* @param {*} texture - Texture source data.
- * @returns {boolean} True if the texture is a canvas, image, video or ImageBitmap and false
+ * @returns {boolean} True if the texture is a canvas, image, video, ImageBitmap, or HTML element and false
* otherwise.
* @ignore
*/
_isBrowserInterface(texture) {
return this._isImageBrowserInterface(texture) ||
this._isImageCanvasInterface(texture) ||
- this._isImageVideoInterface(texture);
+ this._isImageVideoInterface(texture) ||
+ this._isHTMLElementInterface(texture);
}
_isImageBrowserInterface(texture) {
@@ -866,6 +867,13 @@ class GraphicsDevice extends EventHandler {
return (typeof HTMLVideoElement !== 'undefined' && texture instanceof HTMLVideoElement);
}
+ _isHTMLElementInterface(texture) {
+ return (typeof HTMLElement !== 'undefined' && texture instanceof HTMLElement &&
+ !(texture instanceof HTMLImageElement) &&
+ !(texture instanceof HTMLCanvasElement) &&
+ !(texture instanceof HTMLVideoElement));
+ }
+
/**
* Sets the width and height of the canvas, then fires the `resizecanvas` event. Note that the
* specified width and height values will be multiplied by the value of
diff --git a/src/platform/graphics/texture.js b/src/platform/graphics/texture.js
index e8b7035a38c..1fe30d02c74 100644
--- a/src/platform/graphics/texture.js
+++ b/src/platform/graphics/texture.js
@@ -1008,11 +1008,11 @@ class Texture {
}
/**
- * Set the pixel data of the texture from a canvas, image, video DOM element. If the texture is
- * a cubemap, the supplied source must be an array of 6 canvases, images or videos.
+ * Set the pixel data of the texture from a canvas, image, video, or HTML DOM element. If the texture is
+ * a cubemap, the supplied source must be an array of 6 canvases, images, videos, or HTML elements.
*
- * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]} source - A
- * canvas, image or video element, or an array of 6 canvas, image or video elements.
+ * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement|HTMLElement|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|HTMLElement[]} source - A
+ * canvas, image, video, or HTML element, or an array of 6 canvas, image, video, or HTML elements.
* @param {number} [mipLevel] - A non-negative integer specifying the image level of detail.
* Defaults to 0, which represents the base image source. A level value of N, that is greater
* than 0, represents the image source for the Nth mipmap reduction level.
@@ -1066,7 +1066,13 @@ class Texture {
if (source instanceof HTMLVideoElement) {
width = source.videoWidth;
height = source.videoHeight;
+ } else if (this.device._isHTMLElementInterface(source)) {
+ // For HTML elements, use getBoundingClientRect for dimensions
+ const rect = source.getBoundingClientRect();
+ width = Math.floor(rect.width) || 1;
+ height = Math.floor(rect.height) || 1;
} else {
+ // For canvas and image elements
width = source.width;
height = source.height;
}
diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js
index ec76a5c90dc..19b9ba225e9 100644
--- a/src/platform/graphics/webgl/webgl-graphics-device.js
+++ b/src/platform/graphics/webgl/webgl-graphics-device.js
@@ -797,6 +797,10 @@ class WebglGraphicsDevice extends GraphicsDevice {
this.extCompressedTextureATC = this.getExtension('WEBGL_compressed_texture_atc');
this.extCompressedTextureASTC = this.getExtension('WEBGL_compressed_texture_astc');
this.extTextureCompressionBPTC = this.getExtension('EXT_texture_compression_bptc');
+
+ // Check for HTML-in-Canvas support (texElement2D)
+ // This is a proposed API that may not be available yet
+ this.supportsTexElement2D = typeof gl.texElement2D === 'function';
}
/**
diff --git a/src/platform/graphics/webgl/webgl-texture.js b/src/platform/graphics/webgl/webgl-texture.js
index d05421f56aa..15c719f4bee 100644
--- a/src/platform/graphics/webgl/webgl-texture.js
+++ b/src/platform/graphics/webgl/webgl-texture.js
@@ -659,37 +659,20 @@ class WebglTexture {
} else {
// ----- 2D -----
if (device._isBrowserInterface(mipObject)) {
- // Downsize images that are too large to be used as textures
- if (device._isImageBrowserInterface(mipObject)) {
- if (mipObject.width > device.maxTextureSize || mipObject.height > device.maxTextureSize) {
- mipObject = downsampleImage(mipObject, device.maxTextureSize);
- if (mipLevel === 0) {
- texture._width = mipObject.width;
- texture._height = mipObject.height;
- }
- }
- }
-
- const w = mipObject.width || mipObject.videoWidth;
- const h = mipObject.height || mipObject.videoHeight;
+ // Handle HTML elements using texElement2D if supported
+ if (device._isHTMLElementInterface(mipObject) && device.supportsTexElement2D) {
+ // Use texElement2D for HTML elements
+ device.setUnpackFlipY(texture._flipY);
+ device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
- // Upload the image, canvas or video
- device.setUnpackFlipY(texture._flipY);
- device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
+ // Get dimensions from the HTML element
+ const rect = mipObject.getBoundingClientRect();
+ const w = Math.floor(rect.width) || texture._width;
+ const h = Math.floor(rect.height) || texture._height;
- // TEMP: disable fast path for video updates until
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1511207 is resolved
- if (this._glCreated && texture._width === w && texture._height === h && !device._isImageVideoInterface(mipObject)) {
- gl.texSubImage2D(
- gl.TEXTURE_2D,
- mipLevel,
- 0, 0,
- this._glFormat,
- this._glPixelType,
- mipObject
- );
- } else {
- gl.texImage2D(
+ // texElement2D has a different signature than texImage2D
+ // According to the proposal: texElement2D(target, level, internalformat, format, type, element)
+ gl.texElement2D(
gl.TEXTURE_2D,
mipLevel,
this._glInternalFormat,
@@ -702,6 +685,51 @@ class WebglTexture {
texture._width = w;
texture._height = h;
}
+ } else {
+ // Downsize images that are too large to be used as textures
+ if (device._isImageBrowserInterface(mipObject)) {
+ if (mipObject.width > device.maxTextureSize || mipObject.height > device.maxTextureSize) {
+ mipObject = downsampleImage(mipObject, device.maxTextureSize);
+ if (mipLevel === 0) {
+ texture._width = mipObject.width;
+ texture._height = mipObject.height;
+ }
+ }
+ }
+
+ const w = mipObject.width || mipObject.videoWidth;
+ const h = mipObject.height || mipObject.videoHeight;
+
+ // Upload the image, canvas or video
+ device.setUnpackFlipY(texture._flipY);
+ device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
+
+ // TEMP: disable fast path for video updates until
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1511207 is resolved
+ if (this._glCreated && texture._width === w && texture._height === h && !device._isImageVideoInterface(mipObject)) {
+ gl.texSubImage2D(
+ gl.TEXTURE_2D,
+ mipLevel,
+ 0, 0,
+ this._glFormat,
+ this._glPixelType,
+ mipObject
+ );
+ } else {
+ gl.texImage2D(
+ gl.TEXTURE_2D,
+ mipLevel,
+ this._glInternalFormat,
+ this._glFormat,
+ this._glPixelType,
+ mipObject
+ );
+
+ if (mipLevel === 0) {
+ texture._width = w;
+ texture._height = h;
+ }
+ }
}
} else {
// Upload the byte array