sorcherer
Version:
A library for attaching dynamic HTML overlays to Three.js Object3D instances with distance-based scaling, rotation, auto-centering, and DOM culling.
2 lines (1 loc) • 7.91 kB
JavaScript
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("three")):"function"==typeof define&&define.amd?define(["exports","three"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Sorcherer={},t.THREE)}(this,function(t,e){"use strict";!function(){if("undefined"!=typeof document&&!document.getElementById("magical-style")){const t=document.createElement("style");t.id="magical-style",t.textContent="\n.sorcherer-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n",document.head.appendChild(t)}}();class n{static allLoadedElements=new Set;static frustum=new e.Frustum;static matrix=new e.Matrix4;static container="undefined"!=typeof document?document.createElement("div"):null;static autoUpdateRunning=!1;static _timeoutHandle=null;static _containerAttachPending=!1;static objectRegistry=new Map;static instancesById={};static get elements(){return n.instancesById}static defaultScaleMultiplier=1;static _tempWorldPos=new e.Vector3;static _tempProjectedPos=new e.Vector3;static _tempFrustumPos=new e.Vector3;static ensureContainerAttached(){if("undefined"!=typeof document&&n.container&&(n.container.classList.add("sorcherer-container"),!n.container.isConnected))return document.body?(document.body.appendChild(n.container),void(n._containerAttachPending=!1)):void(n._containerAttachPending||(n._containerAttachPending=!0,document.addEventListener("DOMContentLoaded",()=>{n._containerAttachPending=!1,document.body&&n.container&&!n.container.isConnected&&document.body.appendChild(n.container)},{once:!0})))}static _hideOverlay(t){t?._parentSpan?.style&&(t._parentSpan.style.display="none")}static _readBooleanAttribute(t,e){return"true"===(t.getAttribute(e)||"").trim().toLowerCase()}static _parseVector3Attribute(t,n){if(!t.hasAttribute(n))return new e.Vector3;const i=t.getAttribute(n);if(!i)return new e.Vector3;const a=i.split(",").map(t=>parseFloat(t.trim()));return a.length<3||a.slice(0,3).some(t=>Number.isNaN(t))?new e.Vector3:new e.Vector3(a[0],a[1],a[2])}static _parseNumberAttribute(t,e){if(!t.hasAttribute(e))return;const n=t.getAttribute(e);if(null==n)return;const i=parseFloat(n);return Number.isFinite(i)?i:void 0}constructor(t,i=new e.Vector3,a=!1,s=!1,r=!1,o){this.object=t,this.offset=i&&"function"==typeof i.clone?i.clone():i,this.simulate3D=a,this.simulateRotation=s,this.autoCenter=r,this.scaleMultiplier=void 0!==o?o:n.defaultScaleMultiplier,this._parentSpan=this.createSpan(),this.template="",this.dynamicVars={},this._dynamicVarNames=new Set,this._disposed=!1,this._removedListener=null,"undefined"!=typeof document&&n.container&&(n.ensureContainerAttached(),n.container.appendChild(this._parentSpan),n.allLoadedElements.add(this)),this.object&&"function"==typeof this.object.addEventListener&&(this._removedListener=t=>{t?.target===this.object&&this.dispose()},this.object.addEventListener("removed",this._removedListener))}createSpan(){if("undefined"==typeof document)return{style:{},classList:{add(){}},innerHTML:"",parentElement:null};const t=document.createElement("span");return t.classList.add("magic-MinusOne"),t.style.position="absolute",t.style.display="none",t.style.transformOrigin="top left",t}_clearDynamicVarProperties(){for(const t of this._dynamicVarNames)delete this[t];this._dynamicVarNames.clear()}_defineDynamicVarAccessor(t){this._dynamicVarNames.has(t)||(this._dynamicVarNames.add(t),Object.defineProperty(this,t,{get:()=>this.getDynamicVar(t),set:e=>{this.setDynamicVar(t,e)},enumerable:!0,configurable:!0}))}attach(t){this.template=String(t??""),this.dynamicVars={},this._clearDynamicVarProperties();this.template.replace(/\$([a-zA-Z0-9_]+)(?:=([^$]+))?\$/g,(t,e,n)=>(e in this.dynamicVars||(this.dynamicVars[e]=void 0!==n?n:""),this._defineDynamicVarAccessor(e),t)),this.renderDynamicVars(),this._parentSpan.style.display="block"}renderDynamicVars(){const t=this.template.replace(/\$([a-zA-Z0-9_]+)(?:=[^$]+)?\$/g,(t,e)=>void 0!==this.dynamicVars[e]?this.dynamicVars[e]:"");this._parentSpan.innerHTML=t}setDynamicVar(t,e){this.dynamicVars[t]=e,this.renderDynamicVars()}getDynamicVar(t){return this.dynamicVars[t]}bufferInstance(t,e){if(!this.object||!t||!e?.domElement)return;if(!this.object.visible)return void n._hideOverlay(this);const i=e.domElement,a=i.clientWidth||i.width||0,s=i.clientHeight||i.height||0;if(!a||!s)return void n._hideOverlay(this);const r=n._tempWorldPos;this.object.getWorldPosition(r),this.offset&&r.add(this.offset);let o=t.position.distanceTo(r);(!Number.isFinite(o)||o<=0)&&(o=1e-4);const c=n._tempProjectedPos;c.copy(r).project(t);const d=s/2;let l=`translate(${a/2*(c.x+1)}px, ${d*(1-c.y)}px)`;if(this.autoCenter&&(l+=" translate(-50%, -50%)"),this.simulate3D){const t=5;l+=` scale(${Math.max(.1,this.scaleMultiplier*(t/o))})`}if(this.simulateRotation){l+=` rotate(${(this.object.rotation?.z||0)*(180/Math.PI)}deg)`}this._parentSpan.style.transform=l,this._parentSpan.style.zIndex=Math.round(1e3/o).toString(),this._parentSpan.style.display="block"}static bufferAll(t,e){if(t&&e){n.ensureContainerAttached(),n.matrix.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),n.frustum.setFromProjectionMatrix(n.matrix);for(const i of n.allLoadedElements){if(!i.object){n._hideOverlay(i);continue}const a=n._tempFrustumPos;i.object.getWorldPosition(a),n.frustum.containsPoint(a)?i.bufferInstance(t,e):n._hideOverlay(i)}}}static autoSetup(t,e,i=16){if(n.autoUpdateRunning||!t||!e)return;n.autoUpdateRunning=!0;const a=Number.isFinite(i)?Math.max(0,i):16;if("function"==typeof requestAnimationFrame){let i=0;const s=r=>{n.autoUpdateRunning&&((!i||r-i>=a)&&(i=r,n.bufferAll(t,e)),requestAnimationFrame(s))};requestAnimationFrame(s)}else{const i=()=>{n.autoUpdateRunning&&(n.bufferAll(t,e),n._timeoutHandle=setTimeout(i,a))};i()}}static stopAutoSetup(){n.autoUpdateRunning=!1,n._timeoutHandle&&(clearTimeout(n._timeoutHandle),n._timeoutHandle=null)}dispose(){if(this._disposed)return;this._disposed=!0;const t=this.object;t&&this._removedListener&&"function"==typeof t.removeEventListener&&t.removeEventListener("removed",this._removedListener),this._parentSpan.parentElement&&this._parentSpan.parentElement.removeChild(this._parentSpan),n.allLoadedElements.delete(this),t&&t.name&&n.instancesById[t.name]===this&&delete n.instancesById[t.name],this._removedListener=null,this.object=null}static registerObject3D(t){t&&t.name&&n.objectRegistry.set(t.name,t)}static registerScene(t){t&&"function"==typeof t.traverse&&t.traverse(t=>{t&&t.name&&n.registerObject3D(t)})}static bootstrap(t,e,i,a={}){const{interval:s=16,autoAttach:r=!0,autoRegister:o=!0}=a;e&&i?(o&&n.registerScene(t),r&&"undefined"!=typeof document&&n.attachFromRealm(),n.autoSetup(e,i,s)):console.warn("[Sorcherer] bootstrap() requires a camera and renderer.")}static attachFromRealm(t){if("undefined"==typeof document)return;const e=t||document;if(!e||"function"!=typeof e.querySelectorAll)return;const i=e.querySelectorAll("realm");i.length&&i.forEach(t=>{t.querySelectorAll("[idm]").forEach(t=>{const e=t.getAttribute("idm");if(!e)return;const i=n.objectRegistry.get(e);if(!i)return;const a=n._readBooleanAttribute(t,"simulate3D"),s=n._readBooleanAttribute(t,"simulateRotation"),r=n._readBooleanAttribute(t,"autoCenter"),o=n._parseVector3Attribute(t,"offset"),c=n._parseNumberAttribute(t,"scaleMultiplier");i.name&&n.instancesById[i.name]&&n.instancesById[i.name].dispose();const d=new n(i,o,a,s,r,c);d.attach(t.innerHTML),t.remove(),i.name&&(n.instancesById[i.name]=d)}),0===t.children.length&&t.remove()})}attachClone(t,e){const i=new n(t,this.offset&&"function"==typeof this.offset.clone?this.offset.clone():this.offset,this.simulate3D,this.simulateRotation,this.autoCenter,this.scaleMultiplier);i.attach(this.template);for(const[t,e]of Object.entries(this.dynamicVars))i.setDynamicVar(t,e);return e&&(t.name=e,n.instancesById[e]=i),i}}t.Sorcherer=n});