|
| 1 | +// @config DESCRIPTION <div style="color:black">Renders live HTML content directly as a WebGL texture via the <b>HTML-in-Canvas</b> API (<code>texElementImage2D</code>).<br>Includes animated CSS gradients, text glow, and a pulsing circle — all driven by standard CSS.</div> |
| 2 | +// |
| 3 | +// This example demonstrates the HTML-in-Canvas API: a styled HTML element with |
| 4 | +// CSS animations is appended to a canvas marked with the "layoutsubtree" |
| 5 | +// attribute, then captured into a WebGL texture via texElementImage2D. |
| 6 | +// |
| 7 | +// Fallback: when device.supportsHtmlTextures is false, a static 2D canvas with |
| 8 | +// hand-drawn placeholder graphics is used as the texture source instead. |
| 9 | +// |
| 10 | +import { deviceType } from 'examples/utils'; |
| 11 | +import * as pc from 'playcanvas'; |
| 12 | + |
| 13 | +const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas')); |
| 14 | + |
| 15 | +// Enable layoutsubtree for HTML-in-Canvas support |
| 16 | +canvas.setAttribute('layoutsubtree', 'true'); |
| 17 | + |
| 18 | +window.focus(); |
| 19 | + |
| 20 | +const gfxOptions = { |
| 21 | + deviceTypes: [deviceType] |
| 22 | +}; |
| 23 | + |
| 24 | +const device = await pc.createGraphicsDevice(canvas, gfxOptions); |
| 25 | +device.maxPixelRatio = Math.min(window.devicePixelRatio, 2); |
| 26 | + |
| 27 | +const createOptions = new pc.AppOptions(); |
| 28 | +createOptions.graphicsDevice = device; |
| 29 | + |
| 30 | +createOptions.componentSystems = [pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem]; |
| 31 | +createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler]; |
| 32 | + |
| 33 | +const app = new pc.AppBase(canvas); |
| 34 | +app.init(createOptions); |
| 35 | +app.start(); |
| 36 | + |
| 37 | +app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); |
| 38 | +app.setCanvasResolution(pc.RESOLUTION_AUTO); |
| 39 | + |
| 40 | +const resize = () => app.resizeCanvas(); |
| 41 | +window.addEventListener('resize', resize); |
| 42 | + |
| 43 | +// Create an HTML element to use as texture source. |
| 44 | +// Per the HTML-in-Canvas proposal, the element must be a direct child of the canvas. |
| 45 | +// The 'inert' attribute prevents hit testing on the element. |
| 46 | +const htmlElement = document.createElement('div'); |
| 47 | +htmlElement.setAttribute('inert', ''); |
| 48 | +htmlElement.style.width = '512px'; |
| 49 | +htmlElement.style.height = '512px'; |
| 50 | +htmlElement.style.padding = '10px'; |
| 51 | +htmlElement.style.background = 'linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #f9ca24, #ff6b6b, #4ecdc4, #45b7d1, #f9ca24)'; |
| 52 | +htmlElement.style.backgroundSize = '400% 400%'; |
| 53 | +htmlElement.style.animation = 'gradient-shift 4s ease infinite'; |
| 54 | +htmlElement.style.borderRadius = '0'; |
| 55 | +htmlElement.style.fontFamily = 'Arial, sans-serif'; |
| 56 | +htmlElement.style.fontSize = '24px'; |
| 57 | +htmlElement.style.color = 'white'; |
| 58 | +htmlElement.style.textAlign = 'center'; |
| 59 | +htmlElement.style.display = 'flex'; |
| 60 | +htmlElement.style.flexDirection = 'column'; |
| 61 | +htmlElement.style.justifyContent = 'center'; |
| 62 | +htmlElement.style.alignItems = 'center'; |
| 63 | +htmlElement.innerHTML = ` |
| 64 | + <h1 style="margin: 0 0 20px 0; animation: glow 3s ease-in-out infinite;">HTML in Canvas!</h1> |
| 65 | + <p style="margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);">This texture is rendered from HTML using texElementImage2D</p> |
| 66 | + <div style="margin-top: 20px; width: 50px; height: 50px; border-radius: 50%; animation: pulse 2s infinite;"></div> |
| 67 | +`; |
| 68 | + |
| 69 | +const style = document.createElement('style'); |
| 70 | +style.textContent = ` |
| 71 | + @keyframes glow { |
| 72 | + 0%, 100% { color: white; text-shadow: 0 0 10px rgba(0,0,0,0.8), 0 0 20px rgba(0,0,0,0.4); font-size: 42px; } |
| 73 | + 50% { color: #f9ca24; text-shadow: 0 0 15px rgba(0,0,0,0.8), 0 0 30px #f9ca24, 0 0 60px #f9ca24, 0 0 90px rgba(249,202,36,0.4); font-size: 48px; } |
| 74 | + } |
| 75 | + @keyframes gradient-shift { |
| 76 | + 0% { background-position: 0% 50%; } |
| 77 | + 50% { background-position: 100% 50%; } |
| 78 | + 100% { background-position: 0% 50%; } |
| 79 | + } |
| 80 | + @keyframes pulse { |
| 81 | + 0% { transform: scale(1); background: #ff6b6b; } |
| 82 | + 25% { transform: scale(1.2); background: #f9ca24; } |
| 83 | + 50% { transform: scale(1); background: #4ecdc4; } |
| 84 | + 75% { transform: scale(1.2); background: #45b7d1; } |
| 85 | + 100% { transform: scale(1); background: #ff6b6b; } |
| 86 | + } |
| 87 | +`; |
| 88 | +document.head.appendChild(style); |
| 89 | + |
| 90 | +canvas.appendChild(htmlElement); |
| 91 | + |
| 92 | +// Create texture |
| 93 | +const htmlTexture = new pc.Texture(device, { |
| 94 | + width: 512, |
| 95 | + height: 512, |
| 96 | + format: pc.PIXELFORMAT_RGBA8, |
| 97 | + name: 'htmlTexture' |
| 98 | +}); |
| 99 | + |
| 100 | +// Fallback canvas texture for browsers without texElementImage2D support |
| 101 | +const createFallbackTexture = () => { |
| 102 | + const fallbackCanvas = document.createElement('canvas'); |
| 103 | + fallbackCanvas.width = 512; |
| 104 | + fallbackCanvas.height = 512; |
| 105 | + const ctx = fallbackCanvas.getContext('2d'); |
| 106 | + if (!ctx) return null; |
| 107 | + |
| 108 | + const gradient = ctx.createLinearGradient(0, 0, 512, 512); |
| 109 | + gradient.addColorStop(0, '#ff6b6b'); |
| 110 | + gradient.addColorStop(0.33, '#4ecdc4'); |
| 111 | + gradient.addColorStop(0.66, '#45b7d1'); |
| 112 | + gradient.addColorStop(1, '#f9ca24'); |
| 113 | + ctx.fillStyle = gradient; |
| 114 | + ctx.fillRect(0, 0, 512, 512); |
| 115 | + |
| 116 | + ctx.fillStyle = 'white'; |
| 117 | + ctx.font = 'bold 36px Arial'; |
| 118 | + ctx.textAlign = 'center'; |
| 119 | + ctx.shadowColor = 'rgba(0,0,0,0.5)'; |
| 120 | + ctx.shadowBlur = 4; |
| 121 | + ctx.shadowOffsetX = 2; |
| 122 | + ctx.shadowOffsetY = 2; |
| 123 | + ctx.fillText('HTML in Canvas!', 256, 180); |
| 124 | + |
| 125 | + ctx.font = '20px Arial'; |
| 126 | + ctx.fillText('(Canvas Fallback)', 256, 220); |
| 127 | + ctx.fillText('texElementImage2D not available', 256, 260); |
| 128 | + |
| 129 | + ctx.beginPath(); |
| 130 | + ctx.arc(256, 320, 25, 0, 2 * Math.PI); |
| 131 | + ctx.fillStyle = 'white'; |
| 132 | + ctx.fill(); |
| 133 | + |
| 134 | + return fallbackCanvas; |
| 135 | +}; |
| 136 | + |
| 137 | +// Start with fallback texture, then switch to HTML source once the paint record is ready |
| 138 | +const fallbackCanvas = createFallbackTexture(); |
| 139 | +if (fallbackCanvas) { |
| 140 | + htmlTexture.setSource(fallbackCanvas); |
| 141 | +} |
| 142 | + |
| 143 | +const onPaintUpload = () => { |
| 144 | + if (!app.graphicsDevice) return; |
| 145 | + htmlTexture.upload(); |
| 146 | +}; |
| 147 | + |
| 148 | +app.on('destroy', () => { |
| 149 | + window.removeEventListener('resize', resize); |
| 150 | + canvas.removeEventListener('paint', onPaintUpload); |
| 151 | + if (htmlElement.parentNode) htmlElement.parentNode.removeChild(htmlElement); |
| 152 | + if (style.parentNode) style.parentNode.removeChild(style); |
| 153 | +}); |
| 154 | + |
| 155 | +if (device.supportsHtmlTextures) { |
| 156 | + // The browser must paint the HTML element before texElementImage2D can use it. |
| 157 | + // Wait for the 'paint' event, then set the HTML element as the texture source. |
| 158 | + canvas.addEventListener('paint', () => { |
| 159 | + htmlTexture.setSource(/** @type {any} */ (htmlElement)); |
| 160 | + }, { once: true }); |
| 161 | + canvas.requestPaint(); |
| 162 | + |
| 163 | + // Re-upload the texture whenever the browser repaints the HTML children |
| 164 | + canvas.addEventListener('paint', onPaintUpload); |
| 165 | +} else { |
| 166 | + console.warn('HTML textures are not supported - using canvas fallback'); |
| 167 | +} |
| 168 | + |
| 169 | +// Create material with the HTML texture |
| 170 | +const material = new pc.StandardMaterial(); |
| 171 | +material.diffuseMap = htmlTexture; |
| 172 | +material.update(); |
| 173 | + |
| 174 | +const box = new pc.Entity('cube'); |
| 175 | +box.addComponent('render', { |
| 176 | + type: 'box', |
| 177 | + material: material |
| 178 | +}); |
| 179 | +app.root.addChild(box); |
| 180 | + |
| 181 | +const camera = new pc.Entity('camera'); |
| 182 | +camera.addComponent('camera', { |
| 183 | + clearColor: new pc.Color(1, 1, 1) |
| 184 | +}); |
| 185 | +app.root.addChild(camera); |
| 186 | +camera.setPosition(0, 0, 3); |
| 187 | + |
| 188 | +app.scene.ambientLight = new pc.Color(0.3, 0.3, 0.3); |
| 189 | + |
| 190 | +const light = new pc.Entity('light'); |
| 191 | +light.addComponent('light'); |
| 192 | +app.root.addChild(light); |
| 193 | +light.setEulerAngles(45, 0, 0); |
| 194 | + |
| 195 | +app.on('update', (/** @type {number} */ dt) => { |
| 196 | + box.rotate(3 * dt, 5 * dt, 6 * dt); |
| 197 | +}); |
| 198 | + |
| 199 | +export { app }; |
0 commit comments