├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------