├── .gitignore ├── .vscode └── settings.json ├── README.md ├── package.json ├── rollup.config.js ├── aframe-to-md.mjs ├── src ├── aframe-html.js └── HTMLMesh.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "esversion", 4 | "iife", 5 | "selectend" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AFrame-HTML 2 | 3 | ```html 4 | 5 | 6 | ``` 7 | 8 | ![image](https://user-images.githubusercontent.com/4225330/167301172-50270499-ac85-4b14-a25e-f82454b19cb0.png) 9 | 10 | 11 | ### html component 12 | 13 | | Property | Type | Description | Default | 14 | | :------- | :------- | :-------------------------------------------------------- | :------ | 15 | | html | selector | HTML element to use. | | 16 | | cursor | selector | Visual indicator for where the user is currently pointing | | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-htmlmesh", 3 | "version": "2.6.0", 4 | "description": "Show HTML elements in AFrame", 5 | "repository": "https://github.com/AdaRoseCannon/aframe-htmlmesh", 6 | "scripts": { 7 | "docs": "awk '\n BEGIN {p=1}\n /^/ {print;system(\"node ./aframe-to-md.mjs ./build/aframe-html.js\");p=0}\n /^/ {p=1}\n p' README.md > ~README.md && mv ~README.md README.md", 8 | "build": "rollup -c && find build -maxdepth 2 -iname \"*.js\" -not -type d -exec du -h {} \\;;npm run docs", 9 | "develop": "rollup -w -c" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^21.0.2", 15 | "@rollup/plugin-node-resolve": "^13.1.3", 16 | "@rollup/plugin-strip": "^2.1.0", 17 | "rollup": "^2.67.2", 18 | "rollup-plugin-terser": "^7.0.2", 19 | "tablemark": "^3.0.0" 20 | }, 21 | "dependencies": { 22 | "three": "^0.180.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion:11 */ 2 | import { terser } from "rollup-plugin-terser"; 3 | import strip from '@rollup/plugin-strip'; 4 | import resolve from "@rollup/plugin-node-resolve"; 5 | import commonjs from "@rollup/plugin-commonjs"; 6 | // import {copyFileSync} from 'fs'; 7 | // const path = 'node_modules/three/examples/jsm/interactive/HTMLMesh.js'; 8 | // copyFileSync(path, 'src/HTMLMesh.js'); 9 | 10 | export default [ 11 | { 12 | input: "src/aframe-html.js", 13 | external: ['three'], 14 | output: { 15 | format: "iife", 16 | sourcemap: true, 17 | file: 'build/aframe-html.js', 18 | globals: { 19 | three: 'THREE' 20 | } 21 | }, 22 | plugins: [ 23 | resolve(), 24 | commonjs({ 25 | include: ["node_modules/**"], 26 | }), 27 | // strip({labels: ['documentation']}), 28 | ] 29 | }, 30 | { 31 | input: "src/aframe-html.js", 32 | external: ['three'], 33 | output: { 34 | format: "iife", 35 | sourcemap: true, 36 | file: 'build/aframe-html.min.js', 37 | globals: { 38 | three: 'THREE' 39 | } 40 | }, 41 | plugins: [ 42 | resolve(), 43 | commonjs({ 44 | include: ["node_modules/**"], 45 | }), 46 | strip({labels: ['documentation']}), 47 | terser() 48 | ] 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /aframe-to-md.mjs: -------------------------------------------------------------------------------- 1 | import tablemark from "tablemark"; 2 | const myArgs = process.argv.slice(2); 3 | 4 | const handler = { 5 | get(target, prop, receiver) { 6 | return target[prop] || function () { 7 | // console.log(prop); 8 | }; 9 | } 10 | }; 11 | 12 | function processSchema(obj, property) { 13 | const out = {}; 14 | if (property) out.property = property; 15 | out.type = obj.type || typeof obj.default; 16 | if (obj.oneOf) { 17 | out.description = `${obj.description || ""}. One of ${obj.oneOf.join(', ')}`; 18 | } else { 19 | out.description = `${obj.description || ""}`; 20 | } 21 | if (typeof obj.default === 'object' || typeof obj.default === 'string') { 22 | out.default = JSON.stringify(obj.default); 23 | } else { 24 | out.default = obj.default; 25 | } 26 | return out; 27 | } 28 | 29 | global.THREE = new Proxy({ 30 | MathUtils: { 31 | generateUUID: ()=>({replace:function(){}}) 32 | } 33 | }, handler); 34 | global.AFRAME= { 35 | registerComponent: function (name, details) { 36 | const table = []; 37 | if (details.schema.description) { 38 | const out = processSchema(details.schema); 39 | table.push(out); 40 | } else { 41 | for (const [property, obj] of Object.entries(details.schema)) { 42 | const out = processSchema(obj, property); 43 | table.push(out); 44 | } 45 | } 46 | console.log(`### ${name} component` + '\n'); 47 | if (details.description) { 48 | console.log(details.description + '\n'); 49 | } 50 | if (table.length) { 51 | console.log(tablemark(table)); 52 | } else { 53 | console.log("No configuration required"); 54 | } 55 | }, 56 | registerShader: function (name, details) { 57 | this.registerComponent(name, details); 58 | }, 59 | registerPrimitive: function (name, details) { 60 | const table = []; 61 | console.log(`### <${name}>` + '\n'); 62 | if (details.description) console.log(details.description + '\n'); 63 | if (details.defaultComponents) { 64 | const table = []; 65 | for (const [defaultComponent, settings] of Object.entries(details.defaultComponents)) { 66 | const out = {defaultComponent, settings: JSON.stringify(settings)}; 67 | table.push(out); 68 | } 69 | if (table.length) { 70 | console.log(`**Default Components:**` + '\n'); 71 | console.log(tablemark(table)); 72 | } 73 | } 74 | if (details.mappings) { 75 | const table = []; 76 | for (const [property, mapping] of Object.entries(details.mappings)) { 77 | const out = {property, mapping:JSON.stringify(mapping)}; 78 | table.push(out); 79 | } 80 | if (table.length) { 81 | console.log(`**Entity Attribute Mappings:**` + '\n'); 82 | console.log(tablemark(table)); 83 | } 84 | } 85 | } 86 | } 87 | import(myArgs[0]); 88 | -------------------------------------------------------------------------------- /src/aframe-html.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 9, -W097 */ 2 | /* For dealing with spline curves */ 3 | /* global THREE, AFRAME, setTimeout, console */ 4 | 'use strict'; 5 | 6 | import { HTMLMesh } from './HTMLMesh.js'; 7 | 8 | const schemaHTML = { 9 | html: { 10 | type: 'selector', 11 | }, 12 | cursor: { 13 | type: 'selector', 14 | } 15 | }; 16 | 17 | documentation: 18 | { 19 | schemaHTML.html.description = `HTML element to use.`; 20 | schemaHTML.cursor.description = `Visual indicator for where the user is currently pointing`; 21 | } 22 | 23 | const _pointer = new THREE.Vector2(); 24 | const _event = { type: '', data: _pointer }; 25 | AFRAME.registerComponent('html', { 26 | schema: schemaHTML, 27 | init() { 28 | this.rerender = this.rerender.bind(this); 29 | this.handle = this.handle.bind(this); 30 | this.onClick = e => this.handle('click', e); 31 | this.onMouseLeave = e => this.handle('mouseleave', e); 32 | this.onMouseEnter = e => this.handle('mouseenter', e); 33 | this.onMouseUp = e => this.handle('mouseup', e); 34 | this.onMouseDown = e => this.handle('mousedown', e); 35 | this.mouseMoveDetail = { 36 | detail: { 37 | cursorEl: null, 38 | intersection: null 39 | } 40 | }; 41 | }, 42 | play() { 43 | this.el.addEventListener('click', this.onClick); 44 | this.el.addEventListener('mouseleave', this.onMouseLeave); 45 | this.el.addEventListener('mouseenter', this.onMouseEnter); 46 | this.el.addEventListener('mouseup', this.onMouseUp); 47 | this.el.addEventListener('mousedown', this.onMouseDown); 48 | }, 49 | pause() { 50 | this.el.removeEventListener('click', this.onClick); 51 | this.el.removeEventListener('mouseleave', this.onMouseLeave); 52 | this.el.removeEventListener('mouseenter', this.onMouseEnter); 53 | this.el.removeEventListener('mouseup', this.onMouseUp); 54 | this.el.removeEventListener('mousedown', this.onMouseDown); 55 | }, 56 | update() { 57 | this.remove(); 58 | if (!this.data.html) return; 59 | const mesh = new HTMLMesh(this.data.html); 60 | this.el.setObject3D('html', mesh); 61 | this.data.html.addEventListener('input', this.rerender); 62 | this.data.html.addEventListener('change', this.rerender); 63 | this.cursor = this.data.cursor ? this.data.cursor.object3D : null; 64 | }, 65 | tick() { 66 | if (this.activeRaycaster) { 67 | const intersection = this.activeRaycaster.components.raycaster.getIntersection(this.el); 68 | this.mouseMoveDetail.detail.cursorEl = this.activeRaycaster; 69 | this.mouseMoveDetail.detail.intersection = intersection; 70 | this.handle('mousemove', this.mouseMoveDetail); 71 | } 72 | }, 73 | handle(type, evt) { 74 | const intersection = evt.detail.intersection; 75 | const raycaster = evt.detail.cursorEl; 76 | if (type === 'mouseenter') { 77 | this.activeRaycaster = raycaster; 78 | } 79 | if (type === 'mouseleave' && this.activeRaycaster === raycaster) { 80 | this.activeRaycaster = null; 81 | } 82 | if (this.cursor) this.cursor.visible = false; 83 | if (intersection) { 84 | const mesh = this.el.getObject3D('html'); 85 | const uv = intersection.uv; 86 | _event.type = type; 87 | _event.data.set( uv.x, 1 - uv.y ); 88 | mesh.dispatchEvent( _event ); 89 | 90 | if (this.cursor) { 91 | this.cursor.visible = true; 92 | this.cursor.parent.worldToLocal(this.cursor.position.copy(intersection.point)); 93 | } 94 | } 95 | }, 96 | rerender() { 97 | const mesh = this.el.getObject3D('html'); 98 | if (mesh && !mesh.material.map.scheduleUpdate) { 99 | mesh.material.map.scheduleUpdate = setTimeout( () => mesh.material.map.update(), 16 ); 100 | } 101 | }, 102 | remove() { 103 | const mesh = this.el.getObject3D('html'); 104 | if (mesh) { 105 | this.el.removeObject3D('html'); 106 | this.data.html.removeEventListener('input', this.rerender); 107 | this.data.html.removeEventListener('change', this.rerender); 108 | mesh.dispose(); 109 | } 110 | this.activeRaycaster = null; 111 | this.mouseMoveDetail.detail.cursorEl = null; 112 | this.mouseMoveDetail.detail.intersection = null; 113 | this.cursor = null; 114 | }, 115 | }); 116 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic Example — AFrame HTML 6 | 10 | 11 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 71 | 72 | 74 | 75 | 76 | 77 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
109 |
110 |

111 | My Metaverse Site 112 |

113 |
114 |
115 |

Change Color

116 | 117 |
118 | Color 119 | 120 | 121 |
122 |
123 | Material: 124 | 125 | 126 |
127 |
128 | Size: 129 | 130 |
131 |
132 | Text: 133 | 134 |
135 |
136 | Number: 137 | 138 |
139 | 140 |
141 |
142 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /src/HTMLMesh.js: -------------------------------------------------------------------------------- 1 | // This is a copy of https://github.com/mrdoob/three.js/blob/dd4a1378a06c826e19ae0ed1b2b609a76cdb930a/examples/jsm/interactive/HTMLMesh.js 2 | // with the following changes: 3 | // - Keep compatibility with three r147 aframe 1.4.2, still using "this.encoding = sRGBEncoding", otherwise using "this.colorSpace = SRGBColorSpace;" 4 | // - window.dispatchEvent line commented, see the TODO below. 5 | // Look at https://github.com/mrdoob/three.js/commits/dev/examples/jsm/interactive/HTMLMesh.js 6 | // to see if there are other changes that can be retrieved here. 7 | import { 8 | CanvasTexture, 9 | LinearFilter, 10 | Mesh, 11 | MeshBasicMaterial, 12 | PlaneGeometry, 13 | SRGBColorSpace, 14 | sRGBEncoding, 15 | Color 16 | } from 'three'; 17 | 18 | /** 19 | * This class can be used to render a DOM element onto a canvas and use it as a texture 20 | * for a plane mesh. 21 | * 22 | * A typical use case for this class is to render the GUI of `lil-gui` as a texture so it 23 | * is compatible for VR. 24 | * 25 | * ```js 26 | * const gui = new GUI( { width: 300 } ); // create lil-gui instance 27 | * 28 | * const mesh = new HTMLMesh( gui.domElement ); 29 | * scene.add( mesh ); 30 | * ``` 31 | * 32 | * @augments Mesh 33 | * @three_import import { HTMLMesh } from 'three/addons/interactive/HTMLMesh.js'; 34 | */ 35 | class HTMLMesh extends Mesh { 36 | 37 | /** 38 | * Constructs a new HTML mesh. 39 | * 40 | * @param {HTMLElement} dom - The DOM element to display as a plane mesh. 41 | */ 42 | constructor( dom ) { 43 | 44 | const texture = new HTMLTexture( dom ); 45 | 46 | const geometry = new PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 ); 47 | const material = new MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } ); 48 | 49 | super( geometry, material ); 50 | 51 | function onEvent( event ) { 52 | 53 | material.map.dispatchDOMEvent( event ); 54 | 55 | } 56 | 57 | this.addEventListener( 'mousedown', onEvent ); 58 | this.addEventListener( 'mousemove', onEvent ); 59 | this.addEventListener( 'mouseup', onEvent ); 60 | this.addEventListener( 'click', onEvent ); 61 | 62 | /** 63 | * Frees the GPU-related resources allocated by this instance and removes all event listeners. 64 | * Call this method whenever this instance is no longer used in your app. 65 | */ 66 | this.dispose = function () { 67 | 68 | geometry.dispose(); 69 | material.dispose(); 70 | 71 | material.map.dispose(); 72 | 73 | canvases.delete( dom ); 74 | 75 | this.removeEventListener( 'mousedown', onEvent ); 76 | this.removeEventListener( 'mousemove', onEvent ); 77 | this.removeEventListener( 'mouseup', onEvent ); 78 | this.removeEventListener( 'click', onEvent ); 79 | 80 | }; 81 | 82 | } 83 | 84 | } 85 | 86 | class HTMLTexture extends CanvasTexture { 87 | 88 | constructor( dom ) { 89 | 90 | super( html2canvas( dom ) ); 91 | 92 | this.dom = dom; 93 | 94 | this.anisotropy = 16; 95 | if ( THREE.REVISION === '147' ) { // Keep compatibility with aframe 1.4.2 96 | 97 | this.encoding = sRGBEncoding; 98 | 99 | } else { 100 | 101 | this.colorSpace = SRGBColorSpace; 102 | 103 | } 104 | 105 | this.minFilter = LinearFilter; 106 | this.magFilter = LinearFilter; 107 | this.generateMipmaps = false; 108 | 109 | // Create an observer on the DOM, and run html2canvas update in the next loop 110 | const observer = new MutationObserver( () => { 111 | 112 | if ( ! this.scheduleUpdate ) { 113 | 114 | // ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer 115 | this.scheduleUpdate = setTimeout( () => this.update(), 16 ); 116 | 117 | } 118 | 119 | } ); 120 | 121 | const config = { attributes: true, childList: true, subtree: true, characterData: true }; 122 | observer.observe( dom, config ); 123 | 124 | this.observer = observer; 125 | 126 | } 127 | 128 | dispatchDOMEvent( event ) { 129 | 130 | if ( event.data ) { 131 | 132 | htmlevent( this.dom, event.type, event.data.x, event.data.y ); 133 | 134 | } 135 | 136 | } 137 | 138 | update() { 139 | 140 | this.image = html2canvas( this.dom ); 141 | this.needsUpdate = true; 142 | 143 | this.scheduleUpdate = null; 144 | 145 | } 146 | 147 | dispose() { 148 | 149 | if ( this.observer ) { 150 | 151 | this.observer.disconnect(); 152 | 153 | } 154 | 155 | this.scheduleUpdate = clearTimeout( this.scheduleUpdate ); 156 | 157 | super.dispose(); 158 | 159 | } 160 | 161 | } 162 | 163 | 164 | // 165 | 166 | const canvases = new WeakMap(); 167 | 168 | function html2canvas( element ) { 169 | 170 | const range = document.createRange(); 171 | const color = new Color(); 172 | 173 | function Clipper( context ) { 174 | 175 | const clips = []; 176 | let isClipping = false; 177 | 178 | function doClip() { 179 | 180 | if ( isClipping ) { 181 | 182 | isClipping = false; 183 | context.restore(); 184 | 185 | } 186 | 187 | if ( clips.length === 0 ) return; 188 | 189 | let minX = - Infinity, minY = - Infinity; 190 | let maxX = Infinity, maxY = Infinity; 191 | 192 | for ( let i = 0; i < clips.length; i ++ ) { 193 | 194 | const clip = clips[ i ]; 195 | 196 | minX = Math.max( minX, clip.x ); 197 | minY = Math.max( minY, clip.y ); 198 | maxX = Math.min( maxX, clip.x + clip.width ); 199 | maxY = Math.min( maxY, clip.y + clip.height ); 200 | 201 | } 202 | 203 | context.save(); 204 | context.beginPath(); 205 | context.rect( minX, minY, maxX - minX, maxY - minY ); 206 | context.clip(); 207 | 208 | isClipping = true; 209 | 210 | } 211 | 212 | return { 213 | 214 | add: function ( clip ) { 215 | 216 | clips.push( clip ); 217 | doClip(); 218 | 219 | }, 220 | 221 | remove: function () { 222 | 223 | clips.pop(); 224 | doClip(); 225 | 226 | } 227 | 228 | }; 229 | 230 | } 231 | 232 | function drawText( style, x, y, string ) { 233 | 234 | if ( string !== '' ) { 235 | 236 | if ( style.textTransform === 'uppercase' ) { 237 | 238 | string = string.toUpperCase(); 239 | 240 | } 241 | 242 | context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily; 243 | context.textBaseline = 'top'; 244 | context.fillStyle = style.color; 245 | context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 ); 246 | 247 | } 248 | 249 | } 250 | 251 | function buildRectPath( x, y, w, h, r ) { 252 | 253 | if ( w < 2 * r ) r = w / 2; 254 | if ( h < 2 * r ) r = h / 2; 255 | 256 | context.beginPath(); 257 | context.moveTo( x + r, y ); 258 | context.arcTo( x + w, y, x + w, y + h, r ); 259 | context.arcTo( x + w, y + h, x, y + h, r ); 260 | context.arcTo( x, y + h, x, y, r ); 261 | context.arcTo( x, y, x + w, y, r ); 262 | context.closePath(); 263 | 264 | } 265 | 266 | function drawBorder( style, which, x, y, width, height ) { 267 | 268 | const borderWidth = style[ which + 'Width' ]; 269 | const borderStyle = style[ which + 'Style' ]; 270 | const borderColor = style[ which + 'Color' ]; 271 | 272 | if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) { 273 | 274 | context.strokeStyle = borderColor; 275 | context.lineWidth = parseFloat( borderWidth ); 276 | context.beginPath(); 277 | context.moveTo( x, y ); 278 | context.lineTo( x + width, y + height ); 279 | context.stroke(); 280 | 281 | } 282 | 283 | } 284 | 285 | function drawElement( element, style ) { 286 | 287 | // Do not render invisible elements, comments and scripts. 288 | if ( element.nodeType === Node.COMMENT_NODE || element.nodeName === 'SCRIPT' || ( element.style && element.style.display === 'none' ) ) { 289 | 290 | return; 291 | 292 | } 293 | 294 | let x = 0, y = 0, width = 0, height = 0; 295 | 296 | if ( element.nodeType === Node.TEXT_NODE ) { 297 | 298 | // text 299 | 300 | range.selectNode( element ); 301 | 302 | const rect = range.getBoundingClientRect(); 303 | 304 | x = rect.left - offset.left - 0.5; 305 | y = rect.top - offset.top - 0.5; 306 | width = rect.width; 307 | height = rect.height; 308 | 309 | drawText( style, x, y, element.nodeValue.trim() ); 310 | 311 | } else if ( element instanceof HTMLCanvasElement ) { 312 | 313 | // Canvas element 314 | const rect = element.getBoundingClientRect(); 315 | x = rect.left - offset.left - 0.5; 316 | y = rect.top - offset.top - 0.5; 317 | const width = rect.width; 318 | const height = rect.height; 319 | context.drawImage( element, x, y, width, height ); 320 | 321 | } else if ( element instanceof HTMLImageElement ) { 322 | 323 | const rect = element.getBoundingClientRect(); 324 | 325 | x = rect.left - offset.left - 0.5; 326 | y = rect.top - offset.top - 0.5; 327 | width = rect.width; 328 | height = rect.height; 329 | 330 | context.drawImage( element, x, y, width, height ); 331 | 332 | } else { 333 | 334 | const rect = element.getBoundingClientRect(); 335 | 336 | x = rect.left - offset.left - 0.5; 337 | y = rect.top - offset.top - 0.5; 338 | width = rect.width; 339 | height = rect.height; 340 | 341 | style = window.getComputedStyle( element ); 342 | 343 | // Get the border of the element used for fill and border 344 | 345 | buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) ); 346 | 347 | const backgroundColor = style.backgroundColor; 348 | 349 | if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) { 350 | 351 | context.fillStyle = backgroundColor; 352 | context.fill(); 353 | 354 | } 355 | 356 | // If all the borders match then stroke the round rectangle 357 | 358 | const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ]; 359 | 360 | let match = true; 361 | let prevBorder = null; 362 | 363 | for ( const border of borders ) { 364 | 365 | if ( prevBorder !== null ) { 366 | 367 | match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) && 368 | ( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) && 369 | ( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] ); 370 | 371 | } 372 | 373 | if ( match === false ) break; 374 | 375 | prevBorder = border; 376 | 377 | } 378 | 379 | if ( match === true ) { 380 | 381 | // They all match so stroke the rectangle from before allows for border-radius 382 | 383 | const width = parseFloat( style.borderTopWidth ); 384 | 385 | if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) { 386 | 387 | context.strokeStyle = style.borderTopColor; 388 | context.lineWidth = width; 389 | context.stroke(); 390 | 391 | } 392 | 393 | } else { 394 | 395 | // Otherwise draw individual borders 396 | 397 | drawBorder( style, 'borderTop', x, y, width, 0 ); 398 | drawBorder( style, 'borderLeft', x, y, 0, height ); 399 | drawBorder( style, 'borderBottom', x, y + height, width, 0 ); 400 | drawBorder( style, 'borderRight', x + width, y, 0, height ); 401 | 402 | } 403 | 404 | if ( element instanceof HTMLInputElement ) { 405 | 406 | let accentColor = style.accentColor; 407 | 408 | if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color; 409 | 410 | color.set( accentColor ); 411 | 412 | const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) ); 413 | const accentTextColor = luminance < 0.5 ? 'white' : '#111111'; 414 | 415 | if ( element.type === 'radio' ) { 416 | 417 | buildRectPath( x, y, width, height, height ); 418 | 419 | context.fillStyle = 'white'; 420 | context.strokeStyle = accentColor; 421 | context.lineWidth = 1; 422 | context.fill(); 423 | context.stroke(); 424 | 425 | if ( element.checked ) { 426 | 427 | buildRectPath( x + 2, y + 2, width - 4, height - 4, height ); 428 | 429 | context.fillStyle = accentColor; 430 | context.strokeStyle = accentTextColor; 431 | context.lineWidth = 2; 432 | context.fill(); 433 | context.stroke(); 434 | 435 | } 436 | 437 | } 438 | 439 | if ( element.type === 'checkbox' ) { 440 | 441 | buildRectPath( x, y, width, height, 2 ); 442 | 443 | context.fillStyle = element.checked ? accentColor : 'white'; 444 | context.strokeStyle = element.checked ? accentTextColor : accentColor; 445 | context.lineWidth = 1; 446 | context.stroke(); 447 | context.fill(); 448 | 449 | if ( element.checked ) { 450 | 451 | const currentTextAlign = context.textAlign; 452 | 453 | context.textAlign = 'center'; 454 | 455 | const properties = { 456 | color: accentTextColor, 457 | fontFamily: style.fontFamily, 458 | fontSize: height + 'px', 459 | fontWeight: 'bold' 460 | }; 461 | 462 | drawText( properties, x + ( width / 2 ), y, '✔' ); 463 | 464 | context.textAlign = currentTextAlign; 465 | 466 | } 467 | 468 | } 469 | 470 | if ( element.type === 'range' ) { 471 | 472 | const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) ); 473 | const position = ( ( value - min ) / ( max - min ) ) * ( width - height ); 474 | 475 | buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 ); 476 | context.fillStyle = accentTextColor; 477 | context.strokeStyle = accentColor; 478 | context.lineWidth = 1; 479 | context.fill(); 480 | context.stroke(); 481 | 482 | buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 ); 483 | context.fillStyle = accentColor; 484 | context.fill(); 485 | 486 | buildRectPath( x + position, y, height, height, height / 2 ); 487 | context.fillStyle = accentColor; 488 | context.fill(); 489 | 490 | } 491 | 492 | if ( element.type === 'color' || element.type === 'text' || element.type === 'number' || element.type === 'email' || element.type === 'password' ) { 493 | 494 | clipper.add( { x: x, y: y, width: width, height: height } ); 495 | 496 | const displayValue = element.type === 'password' ? '*'.repeat( element.value.length ) : element.value; 497 | 498 | drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), displayValue ); 499 | 500 | clipper.remove(); 501 | 502 | } 503 | 504 | } 505 | 506 | } 507 | 508 | /* 509 | // debug 510 | context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 ); 511 | context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 ); 512 | */ 513 | 514 | const isClipping = style.overflow === 'auto' || style.overflow === 'hidden'; 515 | 516 | if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } ); 517 | 518 | for ( let i = 0; i < element.childNodes.length; i ++ ) { 519 | 520 | drawElement( element.childNodes[ i ], style ); 521 | 522 | } 523 | 524 | if ( isClipping ) clipper.remove(); 525 | 526 | } 527 | 528 | const offset = element.getBoundingClientRect(); 529 | 530 | let canvas = canvases.get( element ); 531 | 532 | if ( canvas === undefined ) { 533 | 534 | canvas = document.createElement( 'canvas' ); 535 | canvases.set( element, canvas ); 536 | 537 | } 538 | 539 | canvas.width = offset.width; 540 | canvas.height = offset.height; 541 | 542 | const context = canvas.getContext( '2d'/*, { alpha: false }*/ ); 543 | 544 | const clipper = new Clipper( context ); 545 | 546 | // console.time( 'drawElement' ); 547 | 548 | context.clearRect( 0, 0, canvas.width, canvas.height ); 549 | 550 | drawElement( element ); 551 | 552 | // console.timeEnd( 'drawElement' ); 553 | 554 | return canvas; 555 | 556 | } 557 | 558 | function htmlevent( element, event, x, y ) { 559 | 560 | const mouseEventInit = { 561 | clientX: ( x * element.offsetWidth ) + element.offsetLeft, 562 | clientY: ( y * element.offsetHeight ) + element.offsetTop, 563 | view: element.ownerDocument.defaultView 564 | }; 565 | 566 | // TODO: Find out why this is added. Keep commented out when this file is updated 567 | // window.dispatchEvent( new MouseEvent( event, mouseEventInit ) ); 568 | 569 | const rect = element.getBoundingClientRect(); 570 | 571 | x = x * rect.width + rect.left; 572 | y = y * rect.height + rect.top; 573 | 574 | function traverse( element ) { 575 | 576 | if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) { 577 | 578 | const rect = element.getBoundingClientRect(); 579 | 580 | if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) { 581 | 582 | element.dispatchEvent( new MouseEvent( event, mouseEventInit ) ); 583 | 584 | if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) { 585 | 586 | const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) ); 587 | 588 | const width = rect.width; 589 | const offsetX = x - rect.x; 590 | const proportion = offsetX / width; 591 | element.value = min + ( max - min ) * proportion; 592 | element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) ); 593 | 594 | } 595 | 596 | if ( element instanceof HTMLInputElement && ( element.type === 'text' || element.type === 'number' || element.type === 'email' || element.type === 'password' ) && ( event === 'mousedown' || event === 'click' ) ) { 597 | 598 | element.focus(); 599 | 600 | } 601 | 602 | } 603 | 604 | for ( let i = 0; i < element.childNodes.length; i ++ ) { 605 | 606 | traverse( element.childNodes[ i ] ); 607 | 608 | } 609 | 610 | } 611 | 612 | } 613 | 614 | traverse( element ); 615 | 616 | } 617 | 618 | export { HTMLMesh }; 619 | --------------------------------------------------------------------------------