├── .gitignore ├── LICENSE ├── README.md ├── dist ├── build.js └── examples │ ├── assets │ ├── aframe.png │ ├── box.png │ ├── floor.png │ ├── github.png │ └── npm.png │ └── index.html ├── examples ├── assets │ ├── aframe.png │ ├── box.png │ ├── floor.png │ ├── github.png │ └── npm.png └── index.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── aframe-htmlembed-component.js └── htmlcanvas.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Paul Brunt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | # HTML Embed Component 3 | 4 | HTML Embed is a component created for [A-Frame](https://aframe.io/). The HTML Embed component allows for arbitrary html to be inserted into your aframe scene. It allows you to update the display within A-Frame simply by manipulating the DOM as you normally would. 5 | 6 | In addition to rendering the html to the A-Frame scene it allows for interaction. Most css pseudo selectors such as hover, active, focus and target should work with interactivity enabled without any modifications to your css. Mouse events can be attached to the html elements as usual. 7 | 8 | ## Limitations 9 | 10 | * All styles and images must be in the same origin or allow access via CORS; this allows the component to embed all of the assets required to render the html properly to the canvas via the foreignObject element. 11 | * transform-style css is limited to flat. This is mainly due to it not being rendered properly to canvas so element bounding for preserve-3d has not yet been implemented. If the rendering is fixed as some point I may go back and get it working as well. 12 | * "a-" tags do not render correctly as XHTML embeded into SVG, so any parent "a-" elements of the embed html will be converted to div tags for rendering. This may mean your css will require modification. 13 | * Elements that require rendering outside of the DOM such as the iframe and canvas element will not work. 14 | * :before and :after pseudo elements can't be accessed via the DOM so they can't be used in the element to determine the object bounds. As such, use them with caution. 15 | * Form elements are not consistently rendered to the canvas element so some basic default styles are included for consistency. 16 | * Currently there is no support for css transitions. 17 | 18 | 19 | ## Properties 20 | | Property | Default | Description | 21 | |----------|---------|-------------| 22 | | ppu | 256 | number of pixels to display per unit of the aframescene. | 23 | 24 | ## Methods 25 | 26 | | Method | Description | 27 | |--------|-------------| 28 | | forceRender | Forces the htmlembed component to be re-render | 29 | 30 | 31 | ## Events 32 | 33 | | Name | Event Type | Description | 34 | |------|-------|-------------| 35 | |focusableenter | [FocusableEvent](#focusablervent) | Dispatched when the cursor is moved over a focusable element. Useful for providing visual/haptic feedback to the user letting them know that the element is clickable. | 36 | | focusableleave | [FocusableEvent](#focusablervent) | Dispatched when the cursor is moved out of a focusable element. | 37 | | inputrequired | [InputrequiredEvent](#inputrequiredevent) | Dispatched when an element that requires keyboard input or a user selection is clicked. Can be used to bring up a custom keyboard. | 38 | | resized | N/A | Dispatched when the embed html content size is changed. | 39 | | rendered | N/A | Dispatched when the embedded HTML content is rendered to the A-Frame Scene. | 40 | 41 | ### FocusableEvent 42 | 43 | | Property | Description | 44 | |----------|-------------| 45 | | target | The target element that the cursor is over. | 46 | 47 | ### InputrequiredEvent 48 | 49 | | Property | Description | 50 | |----------|-------------| 51 | | target | The input element that the user selected. || 52 | 53 | 54 | ## How to Use 55 | 56 | To use the component you just add the component to the A-Frame entity containing the html. For example: 57 | ```html 58 | 59 | 60 |

An Example

61 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

62 | image 63 |
64 |
65 | ``` 66 | 67 | ## Interactivity 68 | 69 | ### Using CSS 70 | 71 | HTML Embed allows you to add interactivity by adding standard css interactions such as hover: 72 | ```css 73 | .button{ 74 | display: inline-block; 75 | border-radius: 5px; 76 | background-color: #dddddd; 77 | color: #000000; 78 | } 79 | .button:hover{ 80 | background-color: #000000; 81 | color: #ffffff; 82 | } 83 | ``` 84 | ```html 85 | 86 | 87 | Home 88 | 89 | 90 | ``` 91 | ### Using Javascript 92 | 93 | You can add javascript interactivity in the standard way either by events on the elements themselves or alternatively by adding event listeners to the DOM. 94 | 95 | ```html 96 | 97 | 98 |
Click Me
99 |
100 |
101 | ``` 102 | ```javascript 103 | document.querySelector('#clickme').addEventListener('click',function(e){ 104 | console.log('do something else'); 105 | }); 106 | ``` 107 | 108 | ## Interactions 109 | 110 | Interactions are achived though the normal cursor and laser-controls components and allow you to interacte with the html as if you where using a mouse. If an element is clicked that requires keyboard input the inputrequired event is dispatched so a keyboard overlay can be invoked. 111 | 112 | ## Installation 113 | 114 | ### Browser 115 | 116 | Install and use by directly including the browser files: 117 | ```html 118 | 119 | My A-Frame Scene 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |

My HTML

128 |
129 |
130 | 131 | 132 | ``` 133 | 134 | ### npm 135 | 136 | Install via npm: 137 | 138 | *npm install aframe-htmlembed-component* 139 | 140 | Then register and use. 141 | ```js 142 | require('aframe'); 143 | require('aframe-htmlembed-component'); 144 | ``` 145 | 146 | ## Building 147 | 148 | - Install [Node.js](https://nodejs.org/). 149 | 150 | - Clone the project to your file system: 151 | 152 | ``` 153 | git clone https://github.com/supereggbert/aframe-htmlembed-component.git 154 | ``` 155 | * enter the aframe-htmlembed-component directory. 156 | 157 | ```cd ./aframe-htmlembed-component``` 158 | 159 | * Install build dependencies 160 | 161 | ```npm install``` 162 | 163 | * Run the build script. 164 | 165 | ```npm run build``` 166 | 167 | The compiled file is at `aframe-htmlembed-component/dist/build.js` 168 | 169 | -------------------------------------------------------------------------------- /dist/build.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function s(i){if(t[i])return t[i].exports;var a=t[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,s),a.l=!0,a.exports}s.m=e,s.c=t,s.d=function(e,t,i){s.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.t=function(e,t){if(1&t&&(e=s(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(s.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)s.d(i,a,function(t){return e[t]}.bind(null,a));return i},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="",s(s.s=0)}([function(e,t,s){s(1)},function(e,t,s){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");const i=s(2);AFRAME.registerComponent("htmlembed",{schema:{ppu:{type:"number",default:256}},init:function(){var e=new i(this.el,()=>{t&&(t.needsUpdate=!0)},(e,t)=>{switch(e){case"resize":this.el.emit("resize");break;case"rendered":this.el.emit("rendered");break;case"focusableenter":this.el.emit("focusableenter",t);break;case"focusableleave":this.el.emit("focusableleave",t);break;case"inputrequired":this.el.emit("inputrequired",t)}});this.htmlcanvas=e;var t=new THREE.CanvasTexture(e.canvas);t.minFilter=THREE.LinearFilter,t.wrapS=THREE.ClampToEdgeWrapping,t.wrapT=THREE.ClampToEdgeWrapping;const s=new THREE.MeshBasicMaterial({map:t,transparent:!0});var a=new THREE.PlaneGeometry,n=new THREE.Mesh(a,s);this.el.setObject3D("screen",n),this.screen=n,this.el.addEventListener("raycaster-intersected",e=>{this.raycaster=e.detail.el}),this.el.addEventListener("raycaster-intersected-cleared",e=>{this.htmlcanvas.clearHover(),this.raycaster=null}),this.el.addEventListener("mousedown",e=>{e instanceof CustomEvent?this.htmlcanvas.mousedown(this.lastX,this.lastY):e.stopPropagation()}),this.el.addEventListener("mouseup",e=>{e instanceof CustomEvent?this.htmlcanvas.mouseup(this.lastX,this.lastY):e.stopPropagation()}),this.resize()},resize(){this.width=this.htmlcanvas.width/this.data.ppu,this.height=this.htmlcanvas.height/this.data.ppu,this.screen.scale.x=Math.max(1e-4,this.width),this.screen.scale.y=Math.max(1e-4,this.height)},update(){this.resize()},forceRender(){this.htmlcanvas.forceRender()},tick:function(){if(this.resize(),this.raycaster){var e=this.raycaster.components.raycaster.getIntersection(this.el);if(e){var t=e.point;this.el.object3D.worldToLocal(t);var s=this.width/2,i=this.height/2,a=Math.round((t.x+s)/this.width*this.htmlcanvas.canvas.width),n=Math.round((1-(t.y+i)/this.height)*this.htmlcanvas.canvas.height);this.lastX==a&&this.lastY==n||this.htmlcanvas.mousemove(a,n),this.lastX=a,this.lastY=n}}},remove:function(){this.el.removeObject3D("screen")}})},function(e,t){!function(){var e=document.createElement("style");e.innerHTML="input, select,textarea{border: 1px solid #000000;margin: 0;background-color: #ffffff;-webkit-appearance: none;}:-webkit-autofill {color: #fff !important;}input[type='checkbox']{width: 20px;height: 20px;display: inline-block;}input[type='radio']{width: 20px;height: 20px;display: inline-block;border-radius: 50%;}input[type='checkbox'][checked],input[type='radio'][checked]{background-color: #555555;}a-entity[htmlembed] img{display:inline-block}a-entity[htmlembed]{display:none}";var t=document.querySelector("head");t.insertBefore(e,t.firstChild)}();e.exports=class{constructor(e,t,s){if(!e)throw"Container Element is Required";var i;this.updateCallback=t,this.eventCallback=s,this.canvas=document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),this.html=e,this.html.style.display="block",this.width=0,this.height=0,this.html.style.display="none",this.html.style.position="absolute",this.html.style.top="0",this.html.style.left="0",this.html.style.overflow="hidden",this.mousemovehtml=e=>{e.stopPropagation()},this.html.addEventListener("mousemove",this.mousemovehtml),this.hashChangeEvent=()=>{this.hashChanged()},window.addEventListener("hashchange",this.hashChangeEvent,!1),this.overElements=[],this.focusElement=null,this.img=new Image,this.img.addEventListener("load",()=>{this.render()}),this.csshack();var a=new MutationObserver((e,t)=>{if(!this.nowatch)for(var s=0;s{this.svgToImg(),i=!1}))}});a.observe(this.html,{attributes:!0,childList:!0,subtree:!0}),this.observer=a,this.cssgenerated=[],this.cssembed=[],this.serializer=new XMLSerializer,this.hashChanged()}forceRender(){Array.from(document.querySelectorAll("*")).map(e=>e.classCache={}),this.svgToImg()}hashChanged(){if(window.clearedHash!=window.location.hash){Array.from(document.querySelectorAll("*")).map(e=>e.classCache={});var e=document.querySelector(".targethack");if(e&&e.classList.remove("targethack"),window.location.hash){var t=document.querySelector(window.location.hash);t&&t.classList.add("targethack")}}window.clearedHash=window.location.hash,this.svgToImg()}cleanUp(){this.observer.disconnect(),window.removeEventListener("hashchange",this.hashChangeEvent),this.html.addEventListener("mousemove",this.mousrmovehtml)}csshack(){for(var e=document.styleSheets,t=0;t-1&&i.push(s[a].cssText.replace(new RegExp(":hover","g"),".hoverhack")),s[a].cssText.indexOf(":active")>-1&&i.push(s[a].cssText.replace(new RegExp(":active","g"),".activehack")),s[a].cssText.indexOf(":focus")>-1&&i.push(s[a].cssText.replace(new RegExp(":focus","g"),".focushack")),s[a].cssText.indexOf(":target")>-1&&i.push(s[a].cssText.replace(new RegExp(":target","g"),".targethack"));var n=i.indexOf(s[a].cssText);n>-1&&i.splice(n,1)}for(a=0;a{var i,a=[];t=(t=(t=(t=t.replace(new RegExp(":hover","g"),".hoverhack")).replace(new RegExp(":active","g"),".activehack")).replace(new RegExp(":focus","g"),".focushack")).replace(new RegExp(":target","g"),".targethack");const n=RegExp(/url\((?!['"]?(?:data):)['"]?([^'"\)]*)['"]?\)/gi);for(;i=n.exec(t);)a.push(this.getDataURL(new URL(i[1],e)).then((e=>s=>{t=t.replace(e[1],s)})(i)));Promise.all(a).then(e=>{s(t)})})}getURL(e){return e=new URL(e,window.location).href,new Promise(t=>{var s=new XMLHttpRequest;s.open("GET",e,!0),s.responseType="arraybuffer",s.onload=()=>{t(s)},s.send()})}generatePageCSS(){for(var e=Array.from(document.querySelectorAll("style, link[type='text/css'],link[rel='stylesheet']")),t=[],s=0;se=>{this.cssembed[t]=e})(0,a))):t.push(this.getURL(i.getAttribute("href")).then((e=>t=>{var s=new TextDecoder("utf-8").decode(t.response);return this.embedCss(window.location,s).then(((e,t)=>e=>{this.cssembed[t]=e})(0,e))})(a)))}}return Promise.all(t)}getDataURL(e){return new Promise(t=>{this.getURL(e).then(s=>{var i=new Uint8Array(s.response),a=s.getResponseHeader("Content-Type").split(";")[0];if("text/css"==a){var n=new TextDecoder("utf-8").decode(i);this.embedCss(e,n).then(e=>{var s=window.btoa(e);s.length>0?t("data:"+a+";base64,"+s):t("")})}else{var r=this.arrayBufferToBase64(i);t("data:"+a+";base64,"+r)}})})}embededSVG(){for(var e=[],t=this.html.querySelectorAll("*"),s=0;st=>{e.removeAttributeNS("http://www.w3.org/1999/xlink","href"),e.setAttribute("href",t)})(t[s]))),"IMG"==t[s].tagName&&"data"!=t[s].src.substr(0,4)&&e.push(this.getDataURL(t[s].src).then((e=>t=>{e.setAttribute("src",t)})(t[s]))),"http://www.w3.org/1999/xhtml"==t[s].namespaceURI&&t[s].hasAttribute("style")){var a=t[s].getAttribute("style");e.push(this.embedCss(window.location,a).then(((e,t)=>s=>{e!=s&&t.setAttribute("style",s)})(a,t[s])))}}var n=this.html.querySelectorAll("style");for(s=0;st=>{e.innerHTML!=t&&(e.innerHTML=t)})(n[s])));return Promise.all(e)}updateFocusBlur(){for(var e=this.html.querySelectorAll("*"),t=0;t-1?(s.hasOwnProperty("focus")||(s.focus=(e=>()=>this.setFocus(e))(s)),s.hasOwnProperty("blur")||(s.blur=(e=>()=>this.focusElement==e&&this.setBlur())(s))):(delete s.focus,delete s.blur)}}getParents(){var e=[],t=[],s=this.html.parentNode;do{var i=s.tagName.toLowerCase();"a-"==i.substr(0,2)&&(i="div");var a="<"+("body"==i?'body xmlns="http://www.w3.org/1999/xhtml"':i)+' style="transform: none;left: 0;top: 0;position:static;display: block" class="'+s.className+'"'+(s.id?' id="'+s.id+'"':"")+">";e.unshift(a);var n="";if(t.push(n),"body"==i)break}while(s=s.parentNode);return[e.join(""),t.join("")]}updateCheckedAttributes(){for(var e=this.html.getElementsByTagName("input"),t=0;t{this.html.style.display="block",this.width==this.html.offsetWidth&&this.height==this.html.offsetHeight||(this.width=this.html.offsetWidth,this.height=this.html.offsetHeight,this.canvas.width=this.width,this.canvas.height=this.height,this.eventCallback&&this.eventCallback("resized"));var e=this.serializer.serializeToString(this.html),t=this.getParents();e=''+t[0]+e+t[1]+"",this.img.src="data:image/svg+xml;utf8,"+encodeURIComponent(e),this.html.style.display="none"})}render(){this.canvas.width=this.width,this.canvas.height=this.height,this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.drawImage(this.img,0,0),this.updateCallback&&this.updateCallback(),this.eventCallback&&this.eventCallback("rendered")}transformPoint(e,t,s,i,a){var n=e.transform;if(0==n.indexOf("matrix(")){var r=new THREE.Matrix4,h=n.substring(7,n.length-1).split(", ").map(parseFloat);r.elements[0]=h[0],r.elements[1]=h[1],r.elements[4]=h[2],r.elements[5]=h[3],r.elements[12]=h[4],r.elements[13]=h[5]}else{if(0!=n.indexOf("matrix3d("))return[t,s,z];r=new THREE.Matrix4,h=n.substring(9,n.length-1).split(", ").map(parseFloat),r.elements=h}var l=e["transform-origin"],o=i+(l=l.replace(new RegExp("px","g"),"").split(" ").map(parseFloat))[0],c=a+l[1],u=0;l[2]&&(u+=l[2]);var d=(new THREE.Matrix4).makeTranslation(-o,-c,-u);if(0!=(r=(new THREE.Matrix4).makeTranslation(o,c,u).multiply(r).multiply(d)).determinant())return[t,s];var m=(new THREE.Matrix4).getInverse(r),v=new THREE.Vector3(t,s,0),f=new THREE.Vector3(t,s,-1);v.applyMatrix4(m),f.applyMatrix4(m);var p=f.sub(v).normalize();if(0==p.z)return!1;var g=p.multiplyScalar(-v.z/p.z).add(v);return[g.x,g.y]}getBorderRadii(e,t){for(var s,i=["border-top-left-radius","border-top-right-radius","border-bottom-right-radius","border-bottom-left-radius"],a=[],n=0;ne.offsetWidth){var b=1/g*e.offsetWidth;m=Math.min(m,b),v=Math.min(v,b)}var w=c[1][1]+c[2][1];w>e.offsetHeight&&(b=1/w*e.offsetHeight,f=Math.min(f,b),v=Math.min(v,b));var E=c[2][0]+c[3][0];E>e.offsetWidth&&(b=1/E*e.offsetWidth,f=Math.min(f,b),p=Math.min(p,b));var y=c[0][1]+c[3][1];return y>e.offsetHeight&&(b=1/y*e.offsetHeight,m=Math.min(m,b),p=Math.min(p,b)),c[0][0]=c[0][0]*m,c[0][1]=c[0][1]*m,c[1][0]=c[1][0]*v,c[1][1]=c[1][1]*v,c[2][0]=c[2][0]*f,c[2][1]=c[2][1]*f,c[3][0]=c[3][0]*p,c[3][1]=c[3][1]*p,c}checkInBorder(e,t,s,i,a,n){if("0px"==t["border-radius"])return!0;var r,h,l=e.offsetWidth,o=e.offsetHeight,c=this.getBorderRadii(e,t);return!(s1)&&(!(s>a+l-c[1][0]&&i1)&&(!(s>a+l-c[2][0]&&i>n+o-c[2][1]&&(r=(s-(a+l-c[2][0]))/c[2][0])*r+(h=(i-(n+o-c[2][1]))/c[2][1])*h>1)&&!(sn+o-c[3][1]&&(r=(c[3][0]+a-s)/c[3][0])*r+(h=(i-(n+o-c[3][1]))/c[3][1])*h>1)))}checkElement(e,t,s,i,a,n,r,h){if(r.offsetParent){var l=window.getComputedStyle(r),o=r.offsetLeft+s,c=r.offsetTop+i,u=r.offsetWidth,d=r.offsetHeight,m=l["z-index"];if("auto"!=m&&(a=0,n=parseInt(m)),"static"!=l.position&&r!=this.html&&"auto"==m&&(a+=1),("block"==l.display||"inline-block"==l.display)&&"none"!=l.transform){var v=this.transformPoint(l,e,t,o,c);if(!v)return;e=v[0],t=v[1],"auto"==m&&(a+=1)}if(e>o&&ec&&t=h.zIndex||n>h.level)&&n>=h.level&&"none"!=l["pointer-events"]&&(h.zIndex=a,h.ele=r,h.level=n);else if("visible"!=l.overflow)return;var f=r.firstChild;if(f)do{1==f.nodeType&&(f.offsetParent==r?this.checkElement(e,t,s+o,i+c,a,n,f,h):this.checkElement(e,t,s,i,a,n,f,h))}while(f=f.nextSibling)}}elementAt(e,t){this.html.style.display="block";var s={zIndex:0,ele:null,level:0};return this.checkElement(e,t,0,0,0,0,this.html,s),this.html.style.display="none",s.ele}moveMouse(){var e=this.moveX,t=this.moveY,s=this.moveButton,i={screenX:e,screenY:t,clientX:e,clientY:t,button:s||0,bubbles:!0,cancelable:!0},a={clientX:e,clientY:t,button:s||0,bubbles:!1},n=this.elementAt(e,t);if(n!=this.lastEle)if(n){n.tabIndex>-1&&this.eventCallback&&this.eventCallback("focusableenter",{target:n}),this.lastEle&&this.lastEle.tabIndex>-1&&this.eventCallback&&this.eventCallback("focusableleave",{target:this.lastEle});var r=[],h=n;this.lastEle&&this.lastEle.dispatchEvent(new MouseEvent("mouseout",i)),n.dispatchEvent(new MouseEvent("mouseover",i));do{if(h==this.html)break;-1==this.overElements.indexOf(h)&&(h.classList&&h.classList.add("hoverhack"),h.dispatchEvent(new MouseEvent("mouseenter",a)),this.overElements.push(h)),r.push(h)}while(h=h.parentNode);for(var l=0;l-1?this.setFocus(a):this.focusElement=null),a==this.mousedownElement&&(a.dispatchEvent(new MouseEvent("click",i)),"INPUT"==a.tagName&&this.updateCheckedAttributes(),"INPUT"!=a.tagName&&"TEXTAREA"!=a.tagName&&"SELECT"!=a.tagName||this.eventCallback&&this.eventCallback("inputrequired",{target:a}))):(this.focusElement&&this.focusElement.dispatchEvent(new FocusEvent("blur")),this.focusElement=null)}}}]); -------------------------------------------------------------------------------- /dist/examples/assets/aframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/dist/examples/assets/aframe.png -------------------------------------------------------------------------------- /dist/examples/assets/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/dist/examples/assets/box.png -------------------------------------------------------------------------------- /dist/examples/assets/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/dist/examples/assets/floor.png -------------------------------------------------------------------------------- /dist/examples/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/dist/examples/assets/github.png -------------------------------------------------------------------------------- /dist/examples/assets/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/dist/examples/assets/npm.png -------------------------------------------------------------------------------- /dist/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 123 | 143 | 144 | 145 |
146 |
147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |

Menu

163 | 169 |
170 | 171 |
172 |

A-Frame HTML Embed Component

173 |

HTML Embed is a component created for A-Frame. The HTML Embed component allows for arbitrary html to be inserted into your aframe scene. It allows you to update the display within A-Frame simply by manipulating the DOM as you normally would. 174 |

175 |

176 | In addition to rendering the html to the A-Frame scene it allows for interaction. Most css pseudo selectors such as hover, active, focus and target should work with interactivity enabled without any modifications to your css. Mouse events can be attached to the html elements as usual.

177 | 178 |
179 |
180 |

Cascading Style Sheets (CSS)

181 |

HTML Embed allows you to add interactivity by adding standard css interactions such as hover: 182 |

183 | .button{ 184 | color: #000000; 185 | } 186 | .button:hover{ 187 | color: #ffffff; 188 | } 189 |
190 |
191 | <a-scene> 192 | <a-entity htmlembed> 193 | <a href="#home" class="button">Home</a> 194 | </a-entity> 195 | </a-scene> 196 |
197 | 198 | 199 |
200 |
201 |

Interactivity

202 |

You can add javascript interactivity in the standard way either by events on the elements themselves or alternatively by adding event listeners to the DOM.

203 |
204 | cubebutton.addEventListener("click",function(){ 205 | if(show){ 206 | box.setAttribute("visible","false"); 207 | cubebutton.innerHTML="Show Box"; 208 | }else{ 209 | box.setAttribute("visible","true"); 210 | cubebutton.innerHTML="Hide Box"; 211 | } 212 | show=!show; 213 | }); 214 |
215 | Show Box 216 | 217 | 218 |
219 |
220 |

Limitations

221 |
    222 |
  • All styles and images must be in the same origin or allow access via CORS; this allows the component to embed all of the assets required to render the html properly to the canvas via the foreignObject element.
  • 223 |
  • transform-style css is limited to flat. This is mainly due to it not being rendered properly to canvas so element bounding for preserve-3d has not yet been implemented. If the rendering is fixed as some point I may go back and get it working as well.
  • 224 |
  • "a-" tags do not render correctly as XHTML embeded into SVG, so any parent "a-" elements of the embed html will be converted to div tags for rendering. This may mean your css will require modification.
  • 225 |
  • Elements that require rendering outside of the DOM such as the iframe and canvas element will not work. 226 |
  • :before and :after pseudo elements can't be accessed via the DOM so they can't be used in the element to determine the object bounds. As such, use them with caution.
  • 227 |
  • Form elements are not consistently rendered to the canvas element so some basic default styles are included for consistency.
  • 228 |
  • Currently there is no support for css transitions.
  • 229 |
230 | 231 |
232 |
233 | 234 |

Links

235 |
A-Frame
236 |
Github
237 |
npm
238 |
239 | 240 |
241 |
242 |
243 |
244 | 245 | 246 | -------------------------------------------------------------------------------- /examples/assets/aframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/examples/assets/aframe.png -------------------------------------------------------------------------------- /examples/assets/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/examples/assets/box.png -------------------------------------------------------------------------------- /examples/assets/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/examples/assets/floor.png -------------------------------------------------------------------------------- /examples/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/examples/assets/github.png -------------------------------------------------------------------------------- /examples/assets/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supereggbert/aframe-htmlembed-component/1c1448e0c7aaff66cf984e7efcb15d893fd05b3b/examples/assets/npm.png -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 123 | 143 | 144 | 145 |
146 |
147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |

Menu

163 | 169 |
170 | 171 |
172 |

A-Frame HTML Embed Component

173 |

HTML Embed is a component created for A-Frame. The HTML Embed component allows for arbitrary html to be inserted into your aframe scene. It allows you to update the display within A-Frame simply by manipulating the DOM as you normally would. 174 |

175 |

176 | In addition to rendering the html to the A-Frame scene it allows for interaction. Most css pseudo selectors such as hover, active, focus and target should work with interactivity enabled without any modifications to your css. Mouse events can be attached to the html elements as usual.

177 | 178 |
179 |
180 |

Cascading Style Sheets (CSS)

181 |

HTML Embed allows you to add interactivity by adding standard css interactions such as hover: 182 |

183 | .button{ 184 | color: #000000; 185 | } 186 | .button:hover{ 187 | color: #ffffff; 188 | } 189 |
190 |
191 | <a-scene> 192 | <a-entity htmlembed> 193 | <a href="#home" class="button">Home</a> 194 | </a-entity> 195 | </a-scene> 196 |
197 | 198 | 199 |
200 |
201 |

Interactivity

202 |

You can add javascript interactivity in the standard way either by events on the elements themselves or alternatively by adding event listeners to the DOM.

203 |
204 | cubebutton.addEventListener("click",function(){ 205 | if(show){ 206 | box.setAttribute("visible","false"); 207 | cubebutton.innerHTML="Show Box"; 208 | }else{ 209 | box.setAttribute("visible","true"); 210 | cubebutton.innerHTML="Hide Box"; 211 | } 212 | show=!show; 213 | }); 214 |
215 | Show Box 216 | 217 | 218 |
219 |
220 |

Limitations

221 |
    222 |
  • All styles and images must be in the same origin or allow access via CORS; this allows the component to embed all of the assets required to render the html properly to the canvas via the foreignObject element.
  • 223 |
  • transform-style css is limited to flat. This is mainly due to it not being rendered properly to canvas so element bounding for preserve-3d has not yet been implemented. If the rendering is fixed as some point I may go back and get it working as well.
  • 224 |
  • "a-" tags do not render correctly as XHTML embeded into SVG, so any parent "a-" elements of the embed html will be converted to div tags for rendering. This may mean your css will require modification.
  • 225 |
  • Elements that require rendering outside of the DOM such as the iframe and canvas element will not work. 226 |
  • :before and :after pseudo elements can't be accessed via the DOM so they can't be used in the element to determine the object bounds. As such, use them with caution.
  • 227 |
  • Form elements are not consistently rendered to the canvas element so some basic default styles are included for consistency.
  • 228 |
  • Currently there is no support for css transitions.
  • 229 |
230 | 231 |
232 |
233 | 234 |

Links

235 |
A-Frame
236 |
Github
237 |
npm
238 |
239 | 240 |
241 |
242 |
243 |
244 | 245 | 246 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./src/aframe-htmlembed-component.js'); 2 | 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-htmlembed-component", 3 | "version": "1.0.0", 4 | "description": "HTML Embed is a component created for A-Frame. The HTML Embed component allows for arbitrary html to be inserted into your aframe scene. It allows you to update the display within A-Frame simply by manipulating the DOM as you normally would.", 5 | "main": "dist/build.js", 6 | "scripts": { 7 | "build": "rm -fR dist/* && webpack --mode=production", 8 | "start": "webpack-dev-server --mode development --open-page examples/" 9 | }, 10 | "author": "supereggbert", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@babel/core": "^7.4.5", 14 | "@babel/preset-env": "^7.4.5", 15 | "babel-loader": "^8.0.6", 16 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 17 | "copy-webpack-plugin": "^5.0.3", 18 | "webpack": "^4.35.0", 19 | "webpack-cli": "^3.3.5", 20 | "webpack-dev-server": "^3.7.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/aframe-htmlembed-component.js: -------------------------------------------------------------------------------- 1 | if (typeof AFRAME === 'undefined') { 2 | throw new Error('Component attempted to register before AFRAME was available.'); 3 | } 4 | 5 | const HTMLCanvas = require('./htmlcanvas.js'); 6 | 7 | AFRAME.registerComponent('htmlembed', { 8 | schema: { 9 | ppu: { 10 | type: 'number', 11 | default: 256 12 | } 13 | }, 14 | init: function() { 15 | var htmlcanvas = new HTMLCanvas(this.el, () => { 16 | if (texture) texture.needsUpdate = true; 17 | }, (event, data) => { 18 | switch (event) { 19 | case 'resize': 20 | this.el.emit("resize"); 21 | break; 22 | case 'rendered': 23 | this.el.emit("rendered"); 24 | break; 25 | case 'focusableenter': 26 | this.el.emit("focusableenter", data); 27 | break; 28 | case 'focusableleave': 29 | this.el.emit("focusableleave", data); 30 | break; 31 | case 'inputrequired': 32 | this.el.emit("inputrequired", data); 33 | break; 34 | } 35 | }); 36 | this.htmlcanvas = htmlcanvas; 37 | var texture = new THREE.CanvasTexture(htmlcanvas.canvas); 38 | texture.minFilter = THREE.LinearFilter; 39 | texture.wrapS = THREE.ClampToEdgeWrapping; 40 | texture.wrapT = THREE.ClampToEdgeWrapping; 41 | const material = new THREE.MeshBasicMaterial({ 42 | map: texture, 43 | transparent: true 44 | }); 45 | var geometry = new THREE.PlaneGeometry(); 46 | var screen = new THREE.Mesh(geometry, material); 47 | this.el.setObject3D('screen', screen); 48 | this.screen = screen; 49 | 50 | this.el.addEventListener('raycaster-intersected', evt => { 51 | this.raycaster = evt.detail.el; 52 | }); 53 | this.el.addEventListener('raycaster-intersected-cleared', evt => { 54 | this.htmlcanvas.clearHover(); 55 | this.raycaster = null; 56 | }); 57 | this.el.addEventListener('mousedown', evt => { 58 | if (evt instanceof CustomEvent) { 59 | this.htmlcanvas.mousedown(this.lastX, this.lastY); 60 | } else { 61 | evt.stopPropagation(); 62 | } 63 | }); 64 | this.el.addEventListener('mouseup', evt => { 65 | if (evt instanceof CustomEvent) { 66 | this.htmlcanvas.mouseup(this.lastX, this.lastY); 67 | } else { 68 | evt.stopPropagation(); 69 | } 70 | }); 71 | this.resize(); 72 | }, 73 | resize() { 74 | this.width = this.htmlcanvas.width / this.data.ppu; 75 | this.height = this.htmlcanvas.height / this.data.ppu; 76 | this.screen.scale.x = Math.max(0.0001,this.width); 77 | this.screen.scale.y = Math.max(0.0001,this.height); 78 | }, 79 | update() { 80 | this.resize(); 81 | }, 82 | forceRender() { 83 | this.htmlcanvas.forceRender(); 84 | }, 85 | tick: function() { 86 | this.resize(); 87 | if (!this.raycaster) { 88 | return; 89 | } 90 | 91 | var intersection = this.raycaster.components.raycaster.getIntersection(this.el); 92 | if (!intersection) { 93 | return; 94 | } 95 | var localPoint = intersection.point; 96 | this.el.object3D.worldToLocal(localPoint); 97 | var w = this.width / 2; 98 | var h = this.height / 2; 99 | var x = Math.round((localPoint.x + w) / this.width * this.htmlcanvas.canvas.width); 100 | var y = Math.round((1 - (localPoint.y + h) / this.height) * this.htmlcanvas.canvas.height); 101 | if (this.lastX != x || this.lastY != y) { 102 | this.htmlcanvas.mousemove(x, y); 103 | } 104 | this.lastX = x; 105 | this.lastY = y; 106 | }, 107 | remove: function() { 108 | this.el.removeObject3D('screen'); 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /src/htmlcanvas.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // We need to set some default styles on form elements for consistency when rendering to canvas 3 | var inputStyles = document.createElement("style"); 4 | inputStyles.innerHTML = "input, select,textarea{border: 1px solid #000000;margin: 0;background-color: #ffffff;-webkit-appearance: none;}:-webkit-autofill {color: #fff !important;}input[type='checkbox']{width: 20px;height: 20px;display: inline-block;}input[type='radio']{width: 20px;height: 20px;display: inline-block;border-radius: 50%;}input[type='checkbox'][checked],input[type='radio'][checked]{background-color: #555555;}a-entity[htmlembed] img{display:inline-block}a-entity[htmlembed]{display:none}"; 5 | var head = document.querySelector("head"); 6 | head.insertBefore(inputStyles, head.firstChild); 7 | })(); 8 | 9 | class HTMLCanvas { 10 | constructor(html, updateCallback, eventCallback) { 11 | if (!html) throw "Container Element is Required"; 12 | 13 | this.updateCallback = updateCallback; 14 | this.eventCallback = eventCallback; 15 | 16 | // Create the canvas to be drawn to 17 | this.canvas = document.createElement("canvas"); 18 | this.ctx = this.canvas.getContext("2d"); 19 | 20 | // Set some basic styles for the embed HTML 21 | this.html = html; 22 | this.html.style.display = 'block'; 23 | this.width = 0; 24 | this.height = 0; 25 | this.html.style.display = 'none'; 26 | this.html.style.position = 'absolute'; 27 | this.html.style.top = '0'; 28 | this.html.style.left = '0'; 29 | this.html.style.overflow = 'hidden'; 30 | 31 | 32 | // We have to stop propergation of the mouse at the root of the embed HTML otherwise it may effect other elements of the page 33 | this.mousemovehtml = (e) => { 34 | e.stopPropagation(); 35 | } 36 | this.html.addEventListener('mousemove', this.mousemovehtml); 37 | 38 | // We need to change targethack when windows has location changes 39 | this.hashChangeEvent = () => { 40 | this.hashChanged(); 41 | } 42 | window.addEventListener('hashchange', this.hashChangeEvent, false); 43 | 44 | 45 | this.overElements = []; // Element currently in the hover state 46 | 47 | this.focusElement = null; // The element that currently has focus 48 | 49 | // Image used to draw SVG to the canvas element 50 | this.img = new Image; 51 | // When image content has changed render it to the canvas 52 | this.img.addEventListener("load", () => { 53 | this.render(); 54 | }); 55 | 56 | // Add css hacks to current styles to ensure that the styles can be rendered to canvas 57 | this.csshack(); 58 | 59 | // Timer used to limit the re-renders due to DOM updates 60 | var timer; 61 | 62 | // Setup the mutation observer 63 | var callback = (mutationsList, observer) => { 64 | // Don't update if we are manipulating DOM for render 65 | if (this.nowatch) return; 66 | 67 | for (var i = 0; i < mutationsList.length; i++) { 68 | // Skip the emebed html element if attributes change 69 | if (mutationsList[i].target == this.html && mutationsList[i].type == "attributes") continue; 70 | 71 | // If a class changes has no style change then there is no need to rerender 72 | if (!mutationsList[i].target.styleRef || mutationsList[i].attributeName == "class") { 73 | var styleRef = this.csssig(mutationsList[i].target); 74 | if (mutationsList[i].target.styleRef == styleRef) { 75 | continue; 76 | } 77 | mutationsList[i].target.styleRef = styleRef; 78 | } 79 | 80 | // Limit render rate so if we get multiple updates per frame we only do once. 81 | if (!timer) { 82 | timer = setTimeout(() => { 83 | this.svgToImg(); 84 | timer = false; 85 | }); 86 | } 87 | } 88 | }; 89 | 90 | var config = { 91 | attributes: true, 92 | childList: true, 93 | subtree: true 94 | }; 95 | var observer = new MutationObserver(callback); 96 | observer.observe(this.html, config); 97 | this.observer = observer; 98 | 99 | this.cssgenerated = []; // Remeber what css sheets have already been passed 100 | this.cssembed = []; // The text of the css to included in the SVG to render 101 | 102 | this.serializer = new XMLSerializer(); 103 | 104 | // Trigger an initially hash change to set up targethack classes 105 | this.hashChanged(); 106 | } 107 | 108 | // Forces a complete rerender 109 | forceRender() { 110 | // Clear any class hash as this may have changed 111 | Array.from(document.querySelectorAll('*')).map((ele) => ele.classCache = {}); 112 | // Load the svg to the image 113 | this.svgToImg(); 114 | } 115 | 116 | // Updates the targethack class when a Hash is changed 117 | hashChanged() { 118 | if (window.clearedHash != window.location.hash) { 119 | Array.from(document.querySelectorAll('*')).map((ele) => ele.classCache = {}); 120 | var currentTarget = document.querySelector('.targethack'); 121 | if (currentTarget) { 122 | currentTarget.classList.remove('targethack'); 123 | } 124 | if (window.location.hash) { 125 | var newTarget = document.querySelector(window.location.hash); 126 | if (newTarget) { 127 | newTarget.classList.add('targethack'); 128 | } 129 | } 130 | } 131 | window.clearedHash = window.location.hash; 132 | this.svgToImg(); 133 | } 134 | 135 | // Cleans up all eventlistners, etc when they are no longer needed 136 | cleanUp() { 137 | // Stop observing for changes 138 | this.observer.disconnect(); 139 | 140 | // Remove event listeners 141 | window.removeEventListener('hashchange', this.hashChangeEvent, ); 142 | this.html.addEventListener('mousemove', this.mousrmovehtml); 143 | } 144 | 145 | // Add hack css rules to the page so they will update the css styles of the embed html 146 | csshack() { 147 | var sheets = document.styleSheets; 148 | for (var i = 0; i < sheets.length; i++) { 149 | try { 150 | var rules = sheets[i].cssRules; 151 | var toadd = []; 152 | for (var j = 0; j < rules.length; j++) { 153 | if (rules[j].cssText.indexOf(':hover') > -1) { 154 | toadd.push(rules[j].cssText.replace(new RegExp(":hover", "g"), ".hoverhack")) 155 | } 156 | if (rules[j].cssText.indexOf(':active') > -1) { 157 | toadd.push(rules[j].cssText.replace(new RegExp(":active", "g"), ".activehack")) 158 | } 159 | if (rules[j].cssText.indexOf(':focus') > -1) { 160 | toadd.push(rules[j].cssText.replace(new RegExp(":focus", "g"), ".focushack")) 161 | } 162 | if (rules[j].cssText.indexOf(':target') > -1) { 163 | toadd.push(rules[j].cssText.replace(new RegExp(":target", "g"), ".targethack")) 164 | } 165 | var idx = toadd.indexOf(rules[j].cssText); 166 | if (idx > -1) { 167 | toadd.splice(idx, 1); 168 | } 169 | } 170 | for (var j = 0; j < toadd.length; j++) { 171 | sheets[i].insertRule(toadd[j]); 172 | } 173 | } catch (e) {} 174 | } 175 | } 176 | 177 | // Simple hash function used for style signature 178 | dbj2(text) { 179 | var hash = 5381, 180 | c; 181 | for (var i = 0; i < text.length; i++) { 182 | c = text.charCodeAt(i); 183 | hash = ((hash << 5) + hash) + c; 184 | } 185 | return hash; 186 | } 187 | 188 | // Generate a singature for the current styles so we know if updated 189 | csssig(el) { 190 | if (!el.classCache) el.classCache = {}; 191 | if (!el.classCache[el.className]) { 192 | var styles = getComputedStyle(el); 193 | var style = ""; 194 | for (var i = 0; i < styles.length; i++) { 195 | style += styles[styles[i]]; 196 | } 197 | el.classCache[el.className] = this.dbj2(style); 198 | } 199 | return el.classCache[el.className]; 200 | } 201 | 202 | // Does what it says on the tin 203 | arrayBufferToBase64(bytes) { 204 | var binary = ''; 205 | var len = bytes.byteLength; 206 | for (var i = 0; i < len; i++) { 207 | binary += String.fromCharCode(bytes[i]); 208 | } 209 | return window.btoa(binary); 210 | } 211 | 212 | // Get an embeded version of the css for use in img svg 213 | // url - baseref of css so we know where to look up resourses 214 | // css - string content of the css 215 | embedCss(url, css) { 216 | return new Promise(resolve => { 217 | var found; 218 | var promises = []; 219 | 220 | // Add hacks to get selectors working on img 221 | css = css.replace(new RegExp(":hover", "g"), ".hoverhack"); 222 | css = css.replace(new RegExp(":active", "g"), ".activehack"); 223 | css = css.replace(new RegExp(":focus", "g"), ".focushack"); 224 | css = css.replace(new RegExp(":target", "g"), ".targethack"); 225 | 226 | // Replace all urls in the css 227 | const regEx = RegExp(/url\((?!['"]?(?:data):)['"]?([^'"\)]*)['"]?\)/gi); 228 | while (found = regEx.exec(css)) { 229 | promises.push( 230 | this.getDataURL(new URL(found[1], url)).then(((found) => { 231 | return url => { 232 | css = css.replace(found[1], url); 233 | }; 234 | })(found)) 235 | ); 236 | } 237 | Promise.all(promises).then((values) => { 238 | resolve(css); 239 | }); 240 | }); 241 | } 242 | 243 | // Does what is says on the tin 244 | getURL(url) { 245 | url = (new URL(url, window.location)).href; 246 | return new Promise(resolve => { 247 | var xhr = new XMLHttpRequest(); 248 | 249 | xhr.open('GET', url, true); 250 | 251 | xhr.responseType = 'arraybuffer'; 252 | 253 | xhr.onload = () => { 254 | resolve(xhr); 255 | }; 256 | 257 | xhr.send(); 258 | 259 | }) 260 | } 261 | 262 | // Generate the embed page CSS from all the page styles 263 | generatePageCSS() { 264 | // Fine all elements we are intrested in 265 | var elements = Array.from(document.querySelectorAll("style, link[type='text/css'],link[rel='stylesheet']")); 266 | var promises = []; 267 | for (var i = 0; i < elements.length; i++) { 268 | var element = elements[i]; 269 | if (this.cssgenerated.indexOf(element) == -1) { 270 | // Make sure all css hacks have been applied to the page 271 | this.csshack(); 272 | // Get embed version of style elements 273 | var idx = this.cssgenerated.length; 274 | this.cssgenerated.push(element); 275 | if (element.tagName == "STYLE") { 276 | promises.push( 277 | this.embedCss(window.location, element.innerHTML).then(((element, idx) => { 278 | return css => { 279 | this.cssembed[idx] = css; 280 | } 281 | })(element, idx)) 282 | ); 283 | } else { 284 | // Get embeded version of externally link stylesheets 285 | promises.push(this.getURL(element.getAttribute("href")).then(((idx) => { 286 | return xhr => { 287 | var css = new TextDecoder("utf-8").decode(xhr.response); 288 | return this.embedCss(window.location, css).then(((element, idx) => { 289 | return css => { 290 | this.cssembed[idx] = css; 291 | } 292 | })(element, idx)) 293 | }; 294 | })(idx)) 295 | ); 296 | } 297 | } 298 | } 299 | return Promise.all(promises); 300 | } 301 | 302 | // Generate and returns a dataurl for the given url 303 | getDataURL(url) { 304 | return new Promise(resolve => { 305 | this.getURL(url).then(xhr => { 306 | var arr = new Uint8Array(xhr.response); 307 | var contentType = xhr.getResponseHeader("Content-Type").split(";")[0]; 308 | if (contentType == "text/css") { 309 | var css = new TextDecoder("utf-8").decode(arr); 310 | this.embedCss(url, css).then((css) => { 311 | var base64 = window.btoa(css); 312 | if (base64.length > 0) { 313 | var dataURL = 'data:' + contentType + ';base64,' + base64; 314 | resolve(dataURL); 315 | } else { 316 | resolve(''); 317 | } 318 | }); 319 | } else { 320 | var b64 = this.arrayBufferToBase64(arr); 321 | var dataURL = 'data:' + contentType + ';base64,' + b64; 322 | resolve(dataURL); 323 | } 324 | }); 325 | }); 326 | } 327 | 328 | // Embeds and externally linked elements for rendering to img 329 | embededSVG() { 330 | var promises = []; 331 | var elements = this.html.querySelectorAll("*"); 332 | for (var i = 0; i < elements.length; i++) { 333 | 334 | // convert and xlink:href to standard href 335 | var link = elements[i].getAttributeNS("http://www.w3.org/1999/xlink", "href"); 336 | if (link) { 337 | promises.push(this.getDataURL(link).then(((element) => { 338 | return dataURL => { 339 | element.removeAttributeNS("http://www.w3.org/1999/xlink", "href"); 340 | element.setAttribute("href", dataURL); 341 | }; 342 | })(elements[i]))); 343 | } 344 | 345 | // Convert and images to data url 346 | if (elements[i].tagName == "IMG" && elements[i].src.substr(0, 4) != "data") { 347 | promises.push(this.getDataURL(elements[i].src).then(((element) => { 348 | return dataURL => { 349 | element.setAttribute("src", dataURL); 350 | }; 351 | })(elements[i]))); 352 | } 353 | 354 | // If there is a style attribute make sure external references are converted to dataurl 355 | if (elements[i].namespaceURI == "http://www.w3.org/1999/xhtml" && elements[i].hasAttribute("style")) { 356 | var style = elements[i].getAttribute("style"); 357 | promises.push( 358 | this.embedCss(window.location, style).then(((style, element) => { 359 | return (css) => { 360 | if (style != css) element.setAttribute("style", css); 361 | } 362 | })(style, elements[i])) 363 | ); 364 | } 365 | } 366 | // If there are any inline style within the embeded html make sure they have the selector hacks 367 | var styles = this.html.querySelectorAll("style"); 368 | for (var i = 0; i < styles.length; i++) { 369 | promises.push( 370 | this.embedCss(window.location, styles[i].innerHTML).then(((style) => { 371 | return (css) => { 372 | if (style.innerHTML != css) style.innerHTML = css; 373 | } 374 | })(styles[i])) 375 | ); 376 | } 377 | return Promise.all(promises) 378 | } 379 | 380 | // Override elements focus and blur functions as these do not perform as expected when embeded html is not being directly displayed 381 | updateFocusBlur() { 382 | var allElements = this.html.querySelectorAll("*"); 383 | for (var i = 0; i < allElements.length; i++) { 384 | var element = allElements[i]; 385 | if (element.tabIndex > -1) { 386 | if (!element.hasOwnProperty('focus')) { 387 | element.focus = ((element) => { 388 | return () => this.setFocus(element); 389 | })(element) 390 | } 391 | if (!element.hasOwnProperty('blur')) { 392 | element.blur = ((element) => { 393 | return () => this.focusElement == element ? this.setBlur() : false; 394 | })(element) 395 | } 396 | } else { 397 | delete(element.focus); 398 | delete(element.blur); 399 | } 400 | } 401 | } 402 | 403 | // Get all parents of the embeded html as these can effect the resulting styles 404 | getParents() { 405 | var opens = []; 406 | var closes = []; 407 | var parent = this.html.parentNode; 408 | do { 409 | var tag = parent.tagName.toLowerCase(); 410 | if (tag.substr(0, 2) == 'a-') tag = 'div'; // We need to replace A-Frame tags with div as they're not valid xhtml so mess up the rendering of images 411 | var open = '<' + (tag == 'body' ? 'body xmlns="http://www.w3.org/1999/xhtml"' : tag) + ' style="transform: none;left: 0;top: 0;position:static;display: block" class="' + parent.className + '"' + (parent.id ? ' id="' + parent.id + '"' : '') + '>'; 412 | opens.unshift(open); 413 | var close = ''; 414 | closes.push(close); 415 | if (tag == 'body') break; 416 | } while (parent = parent.parentNode) 417 | return [opens.join(''), closes.join('')]; 418 | } 419 | 420 | // If an element is checked make sure it has a checked attribute so it renders to the canvas 421 | updateCheckedAttributes() { 422 | var inputElements = this.html.getElementsByTagName("input"); 423 | for (var i = 0; i < inputElements.length; i++) { 424 | var element = inputElements[i]; 425 | if (element.hasAttribute("checked")) { 426 | if (!element.checked) element.removeAttribute("checked"); 427 | } else { 428 | if (element.checked) element.setAttribute("checked", ""); 429 | } 430 | } 431 | } 432 | 433 | // Set the src to be rendered to the Image 434 | svgToImg() { 435 | this.updateFocusBlur(); 436 | Promise.all([this.embededSVG(), this.generatePageCSS()]).then(() => { 437 | // Make sure the element is visible before processing 438 | this.html.style.display = 'block'; 439 | // If embeded html elements dimensions have change then update the canvas 440 | if (this.width != this.html.offsetWidth || this.height != this.html.offsetHeight) { 441 | this.width = this.html.offsetWidth; 442 | this.height = this.html.offsetHeight; 443 | this.canvas.width = this.width; 444 | this.canvas.height = this.height; 445 | if (this.eventCallback) this.eventCallback('resized'); // Notify a resize has happened 446 | } 447 | var docString = this.serializer.serializeToString(this.html); 448 | var parent = this.getParents(); 449 | docString = '' + parent[0] + docString + parent[1] + ''; 450 | this.img.src = "data:image/svg+xml;utf8," + encodeURIComponent(docString); 451 | // Hide the html after processing 452 | this.html.style.display = 'none'; 453 | }); 454 | } 455 | 456 | // Renders the image containing the SVG to the Canvas 457 | render() { 458 | this.canvas.width = this.width; 459 | this.canvas.height = this.height; 460 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 461 | this.ctx.drawImage(this.img, 0, 0); 462 | if (this.updateCallback) this.updateCallback(); 463 | if (this.eventCallback) this.eventCallback('rendered'); 464 | } 465 | 466 | // Transforms a point into an elements frame of reference 467 | transformPoint(elementStyles, x, y, offsetX, offsetY) { 468 | // Get the elements tranform matrix 469 | var transformcss = elementStyles["transform"]; 470 | if (transformcss.indexOf("matrix(") == 0) { 471 | var transform = new THREE.Matrix4(); 472 | var mat = transformcss.substring(7, transformcss.length - 1).split(", ").map(parseFloat); 473 | transform.elements[0] = mat[0]; 474 | transform.elements[1] = mat[1]; 475 | transform.elements[4] = mat[2]; 476 | transform.elements[5] = mat[3]; 477 | transform.elements[12] = mat[4]; 478 | transform.elements[13] = mat[5]; 479 | } else if (transformcss.indexOf("matrix3d(") == 0) { 480 | var transform = new THREE.Matrix4(); 481 | var mat = transformcss.substring(9, transformcss.length - 1).split(", ").map(parseFloat); 482 | transform.elements = mat; 483 | } else { 484 | return [x, y, z] 485 | } 486 | // Get the elements tranform origin 487 | var origincss = elementStyles["transform-origin"]; 488 | origincss = origincss.replace(new RegExp("px", "g"), "").split(" ").map(parseFloat); 489 | 490 | // Apply the transform to the origin 491 | var ox = offsetX + origincss[0]; 492 | var oy = offsetY + origincss[1]; 493 | var oz = 0; 494 | if (origincss[2]) oz += origincss[2]; 495 | 496 | var T1 = new THREE.Matrix4().makeTranslation(-ox, -oy, -oz); 497 | var T2 = new THREE.Matrix4().makeTranslation(ox, oy, oz); 498 | 499 | transform = T2.multiply(transform).multiply(T1) 500 | 501 | // return if matrix determinate is not zero 502 | if(transform.determinant()!=0) return [x,y]; 503 | 504 | // Inverse the transform so we can go from page space to element space 505 | var inverse = new THREE.Matrix4().getInverse(transform); 506 | 507 | // Calculate a ray in the direction of the plane 508 | var v1 = new THREE.Vector3(x, y, 0); 509 | var v2 = new THREE.Vector3(x, y, -1); 510 | v1.applyMatrix4(inverse); 511 | v2.applyMatrix4(inverse); 512 | var dir = v2.sub(v1).normalize(); 513 | 514 | // If ray is parallel to the plane then there is no intersection 515 | if (dir.z == 0) { 516 | return false; 517 | } 518 | 519 | // Get the point of intersection on the element plane 520 | var result = dir.multiplyScalar(-v1.z / dir.z).add(v1); 521 | 522 | return [result.x, result.y]; 523 | } 524 | 525 | // Get the absolute border radii for each corner 526 | getBorderRadii(element, style) { 527 | var properties = ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius']; 528 | var result; 529 | // Parse the css results 530 | var corners = []; 531 | for (var i = 0; i < properties.length; i++) { 532 | var borderRadiusString = style[properties[i]]; 533 | var reExp = /(\d*)([a-z%]{1,3})/gi; 534 | var rec = []; 535 | while (result = reExp.exec(borderRadiusString)) { 536 | rec.push({ 537 | value: result[1], 538 | unit: result[2] 539 | }); 540 | } 541 | if (rec.length == 1) rec.push(rec[0]); 542 | corners.push(rec); 543 | } 544 | 545 | // Convertion values 546 | const unitConv = { 547 | 'px': 1, 548 | '%': element.offsetWidth / 100 549 | }; 550 | 551 | // Convert all corners into pixels 552 | var pixelCorners = []; 553 | for (var i = 0; i < corners.length; i++) { 554 | var corner = corners[i]; 555 | var rec = [] 556 | for (var j = 0; j < corner.length; j++) { 557 | rec.push(corner[j].value * unitConv[corner[j].unit]); 558 | } 559 | pixelCorners.push(rec); 560 | } 561 | 562 | // Initial corner point scales 563 | var c1scale = 1; 564 | var c2scale = 1; 565 | var c3scale = 1; 566 | var c4scale = 1; 567 | 568 | // Change scales of top left and top right corners based on offsetWidth 569 | var borderTop = pixelCorners[0][0] + pixelCorners[1][0]; 570 | if (borderTop > element.offsetWidth) { 571 | var f = 1 / borderTop * element.offsetWidth; 572 | c1scale = Math.min(c1scale, f); 573 | c2scale = Math.min(c2scale, f); 574 | } 575 | 576 | // Change scales of bottom right and top right corners based on offsetHeight 577 | var borderLeft = pixelCorners[1][1] + pixelCorners[2][1]; 578 | if (borderLeft > element.offsetHeight) { 579 | f = 1 / borderLeft * element.offsetHeight; 580 | c3scale = Math.min(c3scale, f); 581 | c2scale = Math.min(c2scale, f); 582 | } 583 | 584 | // Change scales of bottom left and bottom right corners based on offsetWidth 585 | var borderBottom = pixelCorners[2][0] + pixelCorners[3][0]; 586 | if (borderBottom > element.offsetWidth) { 587 | f = 1 / borderBottom * element.offsetWidth; 588 | c3scale = Math.min(c3scale, f); 589 | c4scale = Math.min(c4scale, f); 590 | } 591 | 592 | // Change scales of bottom left and top right corners based on offsetHeight 593 | var borderRight = pixelCorners[0][1] + pixelCorners[3][1]; 594 | if (borderRight > element.offsetHeight) { 595 | f = 1 / borderRight * element.offsetHeight; 596 | c1scale = Math.min(c1scale, f); 597 | c4scale = Math.min(c4scale, f); 598 | } 599 | 600 | // Scale the corners to fix within the confines of the element 601 | pixelCorners[0][0] = pixelCorners[0][0] * c1scale; 602 | pixelCorners[0][1] = pixelCorners[0][1] * c1scale; 603 | pixelCorners[1][0] = pixelCorners[1][0] * c2scale; 604 | pixelCorners[1][1] = pixelCorners[1][1] * c2scale; 605 | pixelCorners[2][0] = pixelCorners[2][0] * c3scale; 606 | pixelCorners[2][1] = pixelCorners[2][1] * c3scale; 607 | pixelCorners[3][0] = pixelCorners[3][0] * c4scale; 608 | pixelCorners[3][1] = pixelCorners[3][1] * c4scale; 609 | 610 | return pixelCorners; 611 | } 612 | 613 | // Check that the element is with the confines of rounded corners 614 | checkInBorder(element, style, x, y, left, top) { 615 | if (style['border-radius'] == "0px") return true; 616 | var width = element.offsetWidth; 617 | var height = element.offsetHeight; 618 | var corners = this.getBorderRadii(element, style); 619 | 620 | // Check top left corner 621 | if (x < corners[0][0] + left && y < corners[0][1] + top) { 622 | var x1 = (corners[0][0] + left - x) / corners[0][0]; 623 | var y1 = (corners[0][1] + top - y) / corners[0][1]; 624 | if (x1 * x1 + y1 * y1 > 1) { 625 | return false; 626 | } 627 | } 628 | // Check top right corner 629 | if (x > left + width - corners[1][0] && y < corners[1][1] + top) { 630 | var x1 = (x - (left + width - corners[1][0])) / corners[1][0]; 631 | var y1 = (corners[1][1] + top - y) / corners[1][1]; 632 | if (x1 * x1 + y1 * y1 > 1) { 633 | return false; 634 | } 635 | } 636 | // Check bottom right corner 637 | if (x > left + width - corners[2][0] && y > top + height - corners[2][1]) { 638 | var x1 = (x - (left + width - corners[2][0])) / corners[2][0]; 639 | var y1 = (y - (top + height - corners[2][1])) / corners[2][1]; 640 | if (x1 * x1 + y1 * y1 > 1) { 641 | return false; 642 | } 643 | } 644 | // Check bottom left corner 645 | if (x < corners[3][0] + left && y > top + height - corners[3][1]) { 646 | var x1 = (corners[3][0] + left - x) / corners[3][0]; 647 | var y1 = (y - (top + height - corners[3][1])) / corners[3][1]; 648 | if (x1 * x1 + y1 * y1 > 1) { 649 | return false; 650 | } 651 | } 652 | return true; 653 | } 654 | 655 | // Check if element it under the current position 656 | // x,y - the position to check 657 | // offsetx, offsety - the current left and top offsets 658 | // offsetz - the current z offset on the current z-index 659 | // level - the current z-index 660 | // element - element being tested 661 | // result - the final result of the hover target 662 | checkElement(x, y, offsetx, offsety, offsetz, level, element, result) { 663 | // Return if this element isn't visible 664 | if (!element.offsetParent) return; 665 | 666 | var style = window.getComputedStyle(element); 667 | 668 | // Calculate absolute position and dimensions 669 | var left = element.offsetLeft + offsetx; 670 | var top = element.offsetTop + offsety; 671 | var width = element.offsetWidth; 672 | var height = element.offsetHeight; 673 | 674 | var zIndex = style['z-index']; 675 | if (zIndex != 'auto') { 676 | offsetz = 0; 677 | level = parseInt(zIndex); 678 | } 679 | 680 | // If the element isn't static the increment the offsetz 681 | if (style['position'] != 'static' && element != this.html) { 682 | if (zIndex == 'auto') offsetz += 1; 683 | } 684 | // If there is a transform then transform point 685 | if ((style['display'] == "block" || style['display'] == "inline-block") && style['transform'] != 'none') { 686 | // Apply css transforms to click point 687 | var newcoord = this.transformPoint(style, x, y, left, top); 688 | if (!newcoord) return; 689 | x = newcoord[0]; 690 | y = newcoord[1]; 691 | if (zIndex == 'auto') offsetz += 1; 692 | } 693 | // Check if in confines of bounding box 694 | if (x > left && x < left + width && y > top && y < top + height) { 695 | // Check if in confines of rounded corders 696 | if (this.checkInBorder(element, style, x, y, left, top)) { 697 | //check if above other elements 698 | if ((offsetz >= result.zIndex || level > result.level) && level >= result.level && style['pointer-events'] != "none") { 699 | result.zIndex = offsetz; 700 | result.ele = element; 701 | result.level = level; 702 | } 703 | } 704 | } else if (style['overflow'] != 'visible') { 705 | // If the element has no overflow and the point is outsize then skip it's children 706 | return; 707 | } 708 | // Check each of the child elements for intersection of the point 709 | var child = element.firstChild; 710 | if (child) 711 | do { 712 | if (child.nodeType == 1) { 713 | if (child.offsetParent == element) { 714 | this.checkElement(x, y, offsetx + left, offsety + top, offsetz, level, child, result); 715 | } else { 716 | this.checkElement(x, y, offsetx, offsety, offsetz, level, child, result); 717 | } 718 | } 719 | } while (child = child.nextSibling); 720 | } 721 | 722 | // Gets the element under the given x,y coordinates 723 | elementAt(x, y) { 724 | this.html.style.display = 'block'; 725 | var result = { 726 | zIndex: 0, 727 | ele: null, 728 | level: 0 729 | }; 730 | this.checkElement(x, y, 0, 0, 0, 0, this.html, result); 731 | this.html.style.display = 'none'; 732 | return result.ele; 733 | } 734 | 735 | // Process a movment of the mouse 736 | moveMouse() { 737 | var x = this.moveX; 738 | var y = this.moveY; 739 | var button = this.moveButton; 740 | var mouseState = { 741 | screenX: x, 742 | screenY: y, 743 | clientX: x, 744 | clientY: y, 745 | button: button ? button : 0, 746 | bubbles: true, 747 | cancelable: true 748 | }; 749 | var mouseStateHover = { 750 | clientX: x, 751 | clientY: y, 752 | button: button ? button : 0, 753 | bubbles: false 754 | }; 755 | 756 | var ele = this.elementAt(x, y); 757 | // If the element under cusor isn't the same as lasttime then update hoverstates and fire off events 758 | if (ele != this.lastEle) { 759 | if (ele) { 760 | // If the element has a tabIndex then notify of a focusable enter 761 | if (ele.tabIndex > -1) { 762 | if (this.eventCallback) this.eventCallback('focusableenter', { 763 | target: ele 764 | }); 765 | } 766 | // If the element has a tabIndex then notify of a focusable leave 767 | if (this.lastEle && this.lastEle.tabIndex > -1) { 768 | if (this.eventCallback) this.eventCallback('focusableleave', { 769 | target: this.lastEle 770 | }); 771 | } 772 | var parents = []; 773 | var current = ele; 774 | if (this.lastEle) this.lastEle.dispatchEvent(new MouseEvent('mouseout', mouseState)); 775 | ele.dispatchEvent(new MouseEvent('mouseover', mouseState)); 776 | // Update overElements and fire corresponding events 777 | do { 778 | if (current == this.html) break; 779 | if (this.overElements.indexOf(current) == -1) { 780 | if (current.classList) current.classList.add("hoverhack"); 781 | current.dispatchEvent(new MouseEvent('mouseenter', mouseStateHover)); 782 | this.overElements.push(current); 783 | } 784 | parents.push(current); 785 | } while (current = current.parentNode); 786 | 787 | for (var i = 0; i < this.overElements.length; i++) { 788 | var element = this.overElements[i]; 789 | if (parents.indexOf(element) == -1) { 790 | if (element.classList) element.classList.remove("hoverhack"); 791 | element.dispatchEvent(new MouseEvent('mouseleave', mouseStateHover)); 792 | this.overElements.splice(i, 1); 793 | i--; 794 | } 795 | } 796 | } else { 797 | while (element = this.overElements.pop()) { 798 | if (element.classList) element.classList.remove("hoverhack"); 799 | element.dispatchEvent(new MouseEvent('mouseout', mouseState)); 800 | } 801 | } 802 | } 803 | if (ele && this.overElements.indexOf(ele) == -1) this.overElements.push(ele); 804 | this.lastEle = ele; 805 | if (ele) ele.dispatchEvent(new MouseEvent('mousemove', mouseState)); 806 | this.moveTimer = false; 807 | } 808 | 809 | // Move the mouse on the html element 810 | mousemove(x, y, button) { 811 | this.moveX = x; 812 | this.moveY = y; 813 | this.moveButton = button; 814 | // Limit frames rate of mouse move for performance 815 | if (this.moveTimer) return; 816 | this.moveTimer = setTimeout(this.moveMouse.bind(this), 20); 817 | } 818 | 819 | // Mouse down on the HTML Element 820 | mousedown(x, y, button) { 821 | var mouseState = { 822 | screenX: x, 823 | screenY: y, 824 | clientX: x, 825 | clientY: y, 826 | button: button ? button : 0, 827 | bubbles: true, 828 | cancelable: true 829 | }; 830 | var ele = this.elementAt(x, y); 831 | if (ele) { 832 | this.activeElement = ele; 833 | ele.classList.add("activehack"); 834 | ele.classList.remove("hoverhack"); 835 | ele.dispatchEvent(new MouseEvent('mousedown', mouseState)); 836 | } 837 | this.mousedownElement = ele; 838 | } 839 | 840 | // Sets the element that currently has focus 841 | setFocus(ele) { 842 | ele.dispatchEvent(new FocusEvent('focus')); 843 | ele.dispatchEvent(new CustomEvent('focusin', { 844 | bubbles: true, 845 | cancelable: false 846 | })); 847 | ele.classList.add('focushack'); 848 | this.focusElement = ele; 849 | } 850 | 851 | // Blurs the element that currently has focus 852 | setBlur() { 853 | if (this.focusElement) { 854 | this.focusElement.classList.remove("focushack"); 855 | this.focusElement.dispatchEvent(new FocusEvent('blur')); 856 | this.focusElement.dispatchEvent(new CustomEvent('focusout', { 857 | bubbles: true, 858 | cancelable: false 859 | })); 860 | } 861 | } 862 | 863 | // Clear all hover states 864 | clearHover() { 865 | if (this.moveTimer) { 866 | clearTimeout(this.moveTimer); 867 | this.moveTimer = false; 868 | } 869 | var element; 870 | while (element = this.overElements.pop()) { 871 | if (element.classList) element.classList.remove("hoverhack"); 872 | element.dispatchEvent(new MouseEvent('mouseout', { 873 | bubbles: true, 874 | cancelable: true 875 | })); 876 | } 877 | if (this.lastEle) this.lastEle.dispatchEvent(new MouseEvent('mouseleave', { 878 | bubbles: true, 879 | cancelable: true 880 | })); 881 | this.lastEle = null; 882 | var activeElement = document.querySelector(".activeElement"); 883 | if (activeElement) { 884 | activeElement.classList.remove("activehack"); 885 | this.activeElement = null; 886 | } 887 | } 888 | 889 | // Mouse up on the HTML Element 890 | mouseup(x, y, button) { 891 | var mouseState = { 892 | screenX: x, 893 | screenY: y, 894 | clientX: x, 895 | clientY: y, 896 | button: button ? button : 0, 897 | bubbles: true, 898 | cancelable: true 899 | }; 900 | var ele = this.elementAt(x, y); 901 | if (this.activeElement) { 902 | this.activeElement.classList.remove("activehack"); 903 | if(ele){ 904 | ele.classList.add("hoverhack"); 905 | if(this.overElements.indexOf(ele)==-1) this.overElements.push(ele); 906 | } 907 | this.activeElement = null; 908 | } 909 | if (ele) { 910 | ele.dispatchEvent(new MouseEvent('mouseup', mouseState)); 911 | if (ele != this.focusElement) { 912 | this.setBlur(); 913 | if (ele.tabIndex > -1) { 914 | this.setFocus(ele); 915 | } else { 916 | this.focusElement = null; 917 | } 918 | } 919 | 920 | if (ele == this.mousedownElement) { 921 | ele.dispatchEvent(new MouseEvent('click', mouseState)); 922 | if (ele.tagName == "INPUT") this.updateCheckedAttributes(); 923 | // If the element requires some sort of keyboard interaction then notify of an input requirment 924 | if (ele.tagName == "INPUT" || ele.tagName == "TEXTAREA" || ele.tagName == "SELECT") { 925 | if (this.eventCallback) this.eventCallback('inputrequired', { 926 | target: ele 927 | }); 928 | } 929 | } 930 | } else { 931 | if (this.focusElement) this.focusElement.dispatchEvent(new FocusEvent('blur')); 932 | this.focusElement = null; 933 | } 934 | } 935 | } 936 | 937 | module.exports = HTMLCanvas; 938 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './index.js', 7 | 8 | output: { 9 | filename: 'build.js', 10 | path: path.resolve(__dirname, 'dist') 11 | }, 12 | 13 | plugins: [new webpack.ProgressPlugin(), new CopyPlugin([ 14 | { from: 'examples', to: 'examples' } 15 | ])], 16 | 17 | module: { 18 | rules: [ 19 | { 20 | test: /.(js|jsx)$/, 21 | include: [], 22 | loader: 'babel-loader', 23 | 24 | options: { 25 | plugins: ['syntax-dynamic-import'], 26 | 27 | presets: [ 28 | [ 29 | '@babel/preset-env', 30 | { 31 | modules: false 32 | } 33 | ] 34 | ] 35 | } 36 | } 37 | ] 38 | }, 39 | 40 | devServer: { 41 | open: true 42 | } 43 | }; 44 | --------------------------------------------------------------------------------