You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

aframe-with-encantar.js 24KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. /**
  2. * A-Frame plugin for encantar.js
  3. * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
  4. * @license LGPL-3.0-or-later
  5. */
  6. (function() {
  7. /* Usage of the indicated versions is encouraged */
  8. __THIS_PLUGIN_HAS_BEEN_TESTED_WITH__({
  9. 'encantar.js': { version: '0.4.0' },
  10. 'A-Frame': { version: '1.4.2' }
  11. });
  12. /**
  13. * AR Utilities
  14. */
  15. const ARUtils = () => ({
  16. findTrackedImage(frame, name = '')
  17. {
  18. if(frame === null)
  19. return null;
  20. for(const result of frame.results) {
  21. if(result.tracker.type == 'image-tracker') {
  22. for(const trackable of result.trackables) {
  23. if(name === '' || name === trackable.referenceImage.name) {
  24. return {
  25. projectionMatrix: result.viewer.view.projectionMatrix,
  26. viewMatrixInverse: result.viewer.pose.transform.matrix,
  27. modelMatrix: trackable.pose.transform.matrix,
  28. };
  29. }
  30. }
  31. }
  32. }
  33. return null;
  34. },
  35. getTrackablePointers(frame)
  36. {
  37. if(frame === null)
  38. return [];
  39. for(const result of frame.results) {
  40. if(result.tracker.type == 'pointer-tracker')
  41. return result.trackables;
  42. }
  43. return [];
  44. },
  45. });
  46. /* ========================================================================= */
  47. /**
  48. * AR System
  49. */
  50. AFRAME.registerSystem('ar', {
  51. // el;
  52. // data;
  53. // schema;
  54. session: /** @type {Session | null} */ (null),
  55. frame: /** @type {Frame | null} */ (null),
  56. pointers: /** @type {TrackablePointer[]} */ ([]),
  57. _utils: ARUtils(),
  58. _started: false,
  59. _components: [],
  60. _roots: [],
  61. init()
  62. {
  63. const scene = this.el;
  64. // validate
  65. if(!scene.getAttribute('ar-session')) {
  66. scene.setAttribute('ar-session', {}); // use a default ar-session
  67. //throw new Error('Missing ar-session in a-scene'); // other errors will appear
  68. }
  69. // initial setup
  70. scene.setAttribute('vr-mode-ui', { enabled: false });
  71. scene.setAttribute('embedded', true);
  72. scene.setAttribute('renderer', { alpha: true });
  73. // pause the scene until we're ready
  74. scene.addEventListener('ar-started', () => {
  75. scene.play();
  76. });
  77. scene.addEventListener('loaded', () => {
  78. //scene.pause();
  79. Promise.resolve().then(() => scene.pause());
  80. });
  81. /*
  82. // we take control of the rendering
  83. scene.addEventListener('loaded', () => {
  84. scene.renderer.setAnimationLoop(null);
  85. });
  86. */
  87. },
  88. tick()
  89. {
  90. const scene = this.el;
  91. // read trackable pointers
  92. this.pointers.length = 0;
  93. if(this.frame) {
  94. const newPointers = this._utils.getTrackablePointers(this.frame);
  95. if(newPointers.length > 0)
  96. this.pointers.push.apply(this.pointers, newPointers);
  97. }
  98. // we take control of the rendering
  99. scene.renderer.setAnimationLoop(null);
  100. // manually update the roots
  101. for(let i = 0; i < this._roots.length; i++)
  102. this._roots[i].teek();
  103. },
  104. startSession()
  105. {
  106. if(this._started)
  107. throw new Error('Can\'t start an AR session twice');
  108. this._started = true;
  109. for(const component of this._components)
  110. component.validate();
  111. return Speedy.Promise.all([
  112. this._loadSources(),
  113. this._loadTrackers(),
  114. this._loadViewport(),
  115. this._loadPreferences(),
  116. ])
  117. .then(([
  118. sources,
  119. trackers,
  120. viewport,
  121. preferences,
  122. ]) => AR.startSession(
  123. Object.assign({}, preferences, {
  124. sources,
  125. trackers,
  126. viewport
  127. })
  128. ))
  129. .then(session => {
  130. // setup
  131. this.session = session;
  132. this._configureSession();
  133. this._startAnimationLoop();
  134. // we're done!
  135. const scene = this.el;
  136. scene.emit('ar-started', { ar: this });
  137. return session;
  138. })
  139. .catch(error => {
  140. console.error(error);
  141. throw error;
  142. });
  143. },
  144. register(component)
  145. {
  146. this._register(this._components, component);
  147. },
  148. unregister(component)
  149. {
  150. this._unregister(this._components, component);
  151. },
  152. registerRoot(component)
  153. {
  154. this._register(this._roots, component);
  155. },
  156. unregisterRoot(component)
  157. {
  158. this._unregister(this._roots, component);
  159. },
  160. _register(arr, component)
  161. {
  162. const j = arr.indexOf(component);
  163. if(j < 0)
  164. arr.push(component);
  165. },
  166. _unregister(arr, component)
  167. {
  168. const j = arr.indexOf(component);
  169. if(j >= 0)
  170. arr.splice(j, 1);
  171. },
  172. _configureSession()
  173. {
  174. const scene = this.el;
  175. const session = this.session;
  176. if(session.viewport.canvas !== scene.canvas) {
  177. session.end();
  178. throw new Error('Invalid A-Frame canvas');
  179. }
  180. session.addEventListener('end', () => {
  181. this.frame = null;
  182. });
  183. session.viewport.addEventListener('resize', () => {
  184. // timeout : internals of aframe (a-scene.js)
  185. setTimeout(() => this._resize(), 200);
  186. this._resize();
  187. });
  188. this._resize(); // initial setup
  189. },
  190. _startAnimationLoop()
  191. {
  192. const scene = this.el;
  193. const session = this.session;
  194. scene.object3D.background = null;
  195. const animate = (time, frame) => {
  196. this.frame = frame;
  197. scene.render();
  198. session.requestAnimationFrame(animate);
  199. };
  200. session.requestAnimationFrame(animate);
  201. },
  202. _resize()
  203. {
  204. const scene = this.el;
  205. const size = this.session.viewport.virtualSize;
  206. scene.renderer.setPixelRatio(1.0);
  207. scene.renderer.setSize(size.width, size.height, false);
  208. },
  209. _loadTrackers()
  210. {
  211. const scene = this.el;
  212. const groups = Array.from(
  213. scene.querySelectorAll('[ar-trackers]'),
  214. el => el.components['ar-trackers']
  215. );
  216. if(groups.length > 1)
  217. throw new Error('Can\'t define multiple groups of ar-trackers');
  218. else if(groups.length == 0)
  219. throw new Error('Missing ar-trackers');
  220. return groups[0].trackers();
  221. },
  222. _loadSources()
  223. {
  224. const scene = this.el;
  225. const groups = Array.from(
  226. scene.querySelectorAll('[ar-sources]'),
  227. el => el.components['ar-sources']
  228. );
  229. if(groups.length > 1)
  230. throw new Error('Can\'t define multiple groups of ar-sources');
  231. else if(groups.length == 0)
  232. throw new Error('Missing ar-sources');
  233. return groups[0].sources();
  234. },
  235. _loadViewport()
  236. {
  237. const scene = this.el;
  238. const viewports = Array.from(
  239. scene.querySelectorAll('[ar-viewport]'),
  240. el => el.components['ar-viewport']
  241. );
  242. if(viewports.length > 1)
  243. throw new Error('Can\'t define multiple ar-viewport\'s');
  244. else if(viewports.length == 0)
  245. throw new Error('Missing ar-viewport');
  246. return viewports[0].viewport();
  247. },
  248. _loadPreferences()
  249. {
  250. const scene = this.el;
  251. const sessionComponent = scene.components['ar-session'];
  252. if(sessionComponent === undefined)
  253. throw new Error('Missing ar-session in a-scene');
  254. return sessionComponent.preferences();
  255. },
  256. });
  257. /**
  258. * AR Component
  259. */
  260. const ARComponent = obj => Object.assign({}, obj, {
  261. // el;
  262. // data;
  263. // id;
  264. init()
  265. {
  266. Object.defineProperty(this, 'ar', {
  267. get: function() { return this.el.sceneEl.systems.ar; }
  268. });
  269. this.ar.register(this);
  270. if(obj.init !== undefined)
  271. obj.init.call(this);
  272. if(this.el.sceneEl.hasLoaded)
  273. this.validate();
  274. },
  275. remove()
  276. {
  277. if(obj.remove !== undefined)
  278. obj.remove.call(this);
  279. this.ar.unregister(this);
  280. },
  281. validate()
  282. {
  283. if(obj.validate !== undefined)
  284. obj.validate.call(this);
  285. }
  286. });
  287. /**
  288. * AR Session
  289. */
  290. AFRAME.registerComponent('ar-session', ARComponent({
  291. schema: {
  292. /** session mode: "immersive" | "inline" */
  293. 'mode': { type: 'string', default: 'immersive' },
  294. /** show stats panel? */
  295. 'stats': { type: 'boolean', default: false },
  296. /** show gizmos? */
  297. 'gizmos': { type: 'boolean', default: false },
  298. /** start the session automatically? */
  299. 'autoplay': { type: 'boolean', default: true },
  300. },
  301. sceneOnly: true,
  302. _started: false,
  303. init()
  304. {
  305. this._started = false;
  306. },
  307. play()
  308. {
  309. // start the session (run once)
  310. if(!this._started) {
  311. this._started = true;
  312. if(this.data.autoplay)
  313. this.startSession();
  314. }
  315. },
  316. remove()
  317. {
  318. // end the session
  319. if(this.ar.session !== null)
  320. this.ar.session.end();
  321. },
  322. preferences()
  323. {
  324. return {
  325. mode: this.data.mode,
  326. stats: this.data.stats,
  327. gizmos: this.data.gizmos
  328. };
  329. },
  330. startSession()
  331. {
  332. return this.ar.startSession();
  333. },
  334. }));
  335. /* ========================================================================= */
  336. /**
  337. * AR Camera
  338. */
  339. AFRAME.registerComponent('ar-camera', ARComponent({
  340. dependencies: ['camera'],
  341. init()
  342. {
  343. const el = this.el;
  344. el.setAttribute('camera', { active: true });
  345. el.setAttribute('wasd-controls', { enabled: false });
  346. el.setAttribute('look-controls', { enabled: false });
  347. el.setAttribute('position', { x: 0, y: 0, z: 0 }); // A-Frame sets y = 1.6m for VR
  348. const camera = el.getObject3D('camera');
  349. camera.matrixAutoUpdate = false;
  350. },
  351. tick()
  352. {
  353. const ar = this.ar;
  354. const el = this.el;
  355. const tracked = ar._utils.findTrackedImage(ar.frame);
  356. if(tracked === null)
  357. return;
  358. const camera = el.getObject3D('camera');
  359. camera.projectionMatrix.fromArray(tracked.projectionMatrix.read());
  360. camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
  361. camera.matrix.fromArray(tracked.viewMatrixInverse.read());
  362. camera.updateMatrixWorld(true);
  363. //console.log('projectionMatrix', tracked.projectionMatrix.read());
  364. //console.log('viewMatrixInverse', tracked.viewMatrixInverse.read());
  365. },
  366. validate()
  367. {
  368. if(!this.el.getAttribute('camera'))
  369. throw new Error('Incorrect ar-camera');
  370. if(this.el.parentNode !== this.el.sceneEl)
  371. throw new Error('ar-camera must be a direct child of a-scene');
  372. },
  373. }));
  374. AFRAME.registerPrimitive('ar-camera', {
  375. defaultComponents: {
  376. 'ar-camera': {},
  377. 'camera': {}
  378. }
  379. });
  380. /**
  381. * AR Root node
  382. */
  383. AFRAME.registerComponent('ar-root', ARComponent({
  384. schema: {
  385. /** the name of a reference image (target) or the empty string (to match any target) */
  386. 'referenceImage': { type: 'string', default: '' },
  387. },
  388. _origin: null,
  389. _firstRun: true,
  390. init()
  391. {
  392. const origin = new THREE.Group();
  393. origin.matrixAutoUpdate = false;
  394. const root = this.el.object3D;
  395. root.parent.add(origin);
  396. origin.add(root);
  397. this._origin = origin;
  398. this._firstRun = true;
  399. this.ar.registerRoot(this);
  400. },
  401. remove()
  402. {
  403. const origin = this._origin;
  404. const root = this.el.object3D;
  405. origin.parent.add(root);
  406. origin.removeFromParent();
  407. this._origin = null;
  408. this.ar.unregisterRoot(this);
  409. },
  410. play()
  411. {
  412. const origin = this._origin;
  413. origin.visible = true;
  414. if(this._firstRun) {
  415. this._firstRun = false;
  416. origin.visible = false;
  417. this.el.pause();
  418. }
  419. },
  420. pause()
  421. {
  422. const origin = this._origin;
  423. origin.visible = false;
  424. },
  425. teek()
  426. {
  427. const ar = this.ar;
  428. const targetName = this.data.referenceImage;
  429. const tracked = ar._utils.findTrackedImage(ar.frame, targetName);
  430. if(tracked === null) {
  431. this.el.pause();
  432. return;
  433. }
  434. const origin = this._origin;
  435. origin.matrix.fromArray(tracked.modelMatrix.read());
  436. origin.updateMatrixWorld(true);
  437. this.el.play();
  438. //console.log('modelMatrix', tracked.modelMatrix.toString());
  439. },
  440. validate()
  441. {
  442. if(this.el.parentNode !== this.el.sceneEl)
  443. throw new Error('ar-root must be a direct child of a-scene');
  444. },
  445. }));
  446. AFRAME.registerPrimitive('ar-root', {
  447. defaultComponents: {
  448. 'ar-root': {}
  449. },
  450. mappings: {
  451. 'reference-image': 'ar-root.referenceImage'
  452. }
  453. });
  454. /* ========================================================================= */
  455. /**
  456. * AR Sources
  457. */
  458. AFRAME.registerComponent('ar-sources', ARComponent({
  459. validate()
  460. {
  461. if(this.el.parentNode !== this.el.sceneEl)
  462. throw new Error('ar-sources must be a direct child of a-scene');
  463. },
  464. sources()
  465. {
  466. const sources = [];
  467. for(const child of this.el.children) {
  468. if(child.components !== undefined) {
  469. for(const name in child.components) {
  470. const component = child.components[name];
  471. if(component.ar === this.ar && typeof component.source == 'function')
  472. sources.push(component.source());
  473. }
  474. }
  475. }
  476. return sources;
  477. },
  478. }));
  479. AFRAME.registerPrimitive('ar-sources', {
  480. defaultComponents: {
  481. 'ar-sources': {}
  482. }
  483. });
  484. /**
  485. * AR Camera Source
  486. */
  487. AFRAME.registerComponent('ar-camera-source', ARComponent({
  488. schema: {
  489. /** video resolution */
  490. 'resolution': { type: 'string', default: 'md' },
  491. /** facing mode: "environment" | "user" */
  492. 'facingMode': { type: 'string', default: 'environment' },
  493. },
  494. validate()
  495. {
  496. if(!this.el.parentNode.getAttribute('ar-sources'))
  497. throw new Error('ar-camera-source must be a direct child of ar-sources');
  498. },
  499. source()
  500. {
  501. return AR.Source.Camera({
  502. resolution: this.data.resolution,
  503. constraints: {
  504. facingMode: this.data.facingMode
  505. }
  506. });
  507. },
  508. }));
  509. AFRAME.registerPrimitive('ar-camera-source', {
  510. defaultComponents: {
  511. 'ar-camera-source': {}
  512. },
  513. mappings: {
  514. 'resolution': 'ar-camera-source.resolution',
  515. 'facing-mode': 'ar-camera-source.facingMode',
  516. }
  517. });
  518. /**
  519. * AR Video Source
  520. */
  521. AFRAME.registerComponent('ar-video-source', ARComponent({
  522. schema: {
  523. /** selector for a <video> element */
  524. 'video': { type: 'selector' },
  525. },
  526. validate()
  527. {
  528. if(!this.el.parentNode.getAttribute('ar-sources'))
  529. throw new Error('ar-video-source must be a direct child of ar-sources');
  530. },
  531. source()
  532. {
  533. return AR.Source.Video(this.data.video);
  534. },
  535. }));
  536. AFRAME.registerPrimitive('ar-video-source', {
  537. defaultComponents: {
  538. 'ar-video-source': {}
  539. },
  540. mappings: {
  541. 'video': 'ar-video-source.video'
  542. }
  543. });
  544. /**
  545. * AR Canvas Source
  546. */
  547. AFRAME.registerComponent('ar-canvas-source', ARComponent({
  548. schema: {
  549. /** selector for a <canvas> element */
  550. 'canvas': { type: 'selector' },
  551. },
  552. validate()
  553. {
  554. if(!this.el.parentNode.getAttribute('ar-sources'))
  555. throw new Error('ar-canvas-source must be a direct child of ar-sources');
  556. },
  557. source()
  558. {
  559. return AR.Source.Canvas(this.data.canvas);
  560. },
  561. }));
  562. AFRAME.registerPrimitive('ar-canvas-source', {
  563. defaultComponents: {
  564. 'ar-canvas-source': {}
  565. },
  566. mappings: {
  567. 'canvas': 'ar-canvas-source.canvas'
  568. }
  569. });
  570. /**
  571. * AR Pointer Source
  572. */
  573. AFRAME.registerComponent('ar-pointer-source', ARComponent({
  574. schema: {
  575. },
  576. validate()
  577. {
  578. if(!this.el.parentNode.getAttribute('ar-sources'))
  579. throw new Error('ar-pointer-source must be a direct child of ar-sources');
  580. },
  581. source()
  582. {
  583. return AR.Source.Pointer();
  584. },
  585. }));
  586. AFRAME.registerPrimitive('ar-pointer-source', {
  587. defaultComponents: {
  588. 'ar-pointer-source': {}
  589. },
  590. mappings: {
  591. }
  592. });
  593. /* ========================================================================= */
  594. /**
  595. * AR Trackers
  596. */
  597. AFRAME.registerComponent('ar-trackers', ARComponent({
  598. validate()
  599. {
  600. if(this.el.parentNode !== this.el.sceneEl)
  601. throw new Error('ar-trackers must be a direct child of a-scene');
  602. },
  603. /* async */ trackers()
  604. {
  605. const trackers = [];
  606. for(const child of this.el.children) {
  607. if(child.components !== undefined) {
  608. for(const name in child.components) {
  609. const component = child.components[name];
  610. if(component.ar === this.ar && typeof component.tracker == 'function')
  611. trackers.push(component.tracker());
  612. }
  613. }
  614. }
  615. return Speedy.Promise.all(trackers);
  616. },
  617. }));
  618. AFRAME.registerPrimitive('ar-trackers', {
  619. defaultComponents: {
  620. 'ar-trackers': {}
  621. }
  622. });
  623. /**
  624. * AR Image Tracker
  625. */
  626. AFRAME.registerComponent('ar-image-tracker', ARComponent({
  627. schema: {
  628. /** resolution of the tracker */
  629. 'resolution': { type: 'string', default: 'sm' },
  630. },
  631. validate()
  632. {
  633. if(!this.el.parentNode.getAttribute('ar-trackers'))
  634. throw new Error('ar-image-tracker must be a direct child of ar-trackers');
  635. },
  636. /* async */ tracker()
  637. {
  638. const tracker = AR.Tracker.ImageTracker();
  639. const referenceImages = [];
  640. tracker.resolution = this.data.resolution;
  641. for(const child of this.el.children) {
  642. if(child.components !== undefined) {
  643. for(const name in child.components) {
  644. const component = child.components[name];
  645. if(component.ar === this.ar && typeof component.referenceImage == 'function')
  646. referenceImages.push(component.referenceImage());
  647. }
  648. }
  649. }
  650. return tracker.database.add(referenceImages).then(() => tracker);
  651. },
  652. }));
  653. AFRAME.registerPrimitive('ar-image-tracker', {
  654. defaultComponents: {
  655. 'ar-image-tracker': {}
  656. }
  657. });
  658. /**
  659. * AR Reference Image
  660. */
  661. AFRAME.registerComponent('ar-reference-image', ARComponent({
  662. schema: {
  663. /** the name of the reference image */
  664. 'name': { type: 'string', default: '' },
  665. /** URL of an image */
  666. 'src': { type: 'string', default: '' }
  667. },
  668. validate()
  669. {
  670. if(!this.el.parentNode.getAttribute('ar-image-tracker'))
  671. throw new Error('ar-reference-image must be a direct child of ar-image-tracker');
  672. },
  673. referenceImage()
  674. {
  675. if(this.data.src == '')
  676. throw new Error('Unspecified src attribute of ar-reference-image');
  677. const img = new Image();
  678. img.src = this.data.src;
  679. return {
  680. name: this.data.name != '' ? this.data.name : undefined,
  681. image: img
  682. };
  683. }
  684. }));
  685. AFRAME.registerPrimitive('ar-reference-image', {
  686. defaultComponents: {
  687. 'ar-reference-image': {}
  688. },
  689. mappings: {
  690. 'name': 'ar-reference-image.name',
  691. 'src': 'ar-reference-image.src'
  692. }
  693. });
  694. /**
  695. * AR Pointer Tracker
  696. */
  697. AFRAME.registerComponent('ar-pointer-tracker', ARComponent({
  698. schema: {
  699. },
  700. validate()
  701. {
  702. if(!this.el.parentNode.getAttribute('ar-trackers'))
  703. throw new Error('ar-pointer-tracker must be a direct child of ar-trackers');
  704. },
  705. tracker()
  706. {
  707. return AR.Tracker.Pointer();
  708. },
  709. }));
  710. AFRAME.registerPrimitive('ar-pointer-tracker', {
  711. defaultComponents: {
  712. 'ar-pointer-tracker': {}
  713. }
  714. });
  715. /* ========================================================================= */
  716. /**
  717. * AR Viewport
  718. */
  719. AFRAME.registerComponent('ar-viewport', ARComponent({
  720. schema: {
  721. /** viewport resolution */
  722. 'resolution': { type: 'string', default: 'lg' },
  723. /** viewport style: "best-fit" | "stretch" | "inline" */
  724. 'style': { type: 'string', default: 'best-fit' },
  725. },
  726. validate()
  727. {
  728. if(this.el.parentNode !== this.el.sceneEl)
  729. throw new Error('ar-viewport must be a direct child of a-scene');
  730. },
  731. viewport()
  732. {
  733. const scene = this.el.sceneEl;
  734. const huds = [];
  735. const fullscreenUI = this.el.components['ar-viewport-fullscreen-ui'];
  736. for(const child of this.el.children) {
  737. if(child.components !== undefined) {
  738. for(const name in child.components) {
  739. const component = child.components[name];
  740. if(component.ar === this.ar && typeof component.hudContainer == 'function')
  741. huds.push(component.hudContainer());
  742. }
  743. }
  744. }
  745. if(huds.length > 1)
  746. throw new Error('Can\'t define multiple ar-hud\'s in an ar-viewport');
  747. else if(huds.length == 0)
  748. huds.push(undefined);
  749. return AR.Viewport(Object.assign(
  750. {
  751. container: this.el,
  752. hudContainer: huds[0],
  753. canvas: scene.canvas,
  754. resolution: this.data.resolution,
  755. style: this.data.style,
  756. },
  757. !fullscreenUI ? {} : { fullscreenUI: fullscreenUI.data.enabled }
  758. ));
  759. },
  760. }));
  761. AFRAME.registerPrimitive('ar-viewport', {
  762. defaultComponents: {
  763. 'ar-viewport': {}
  764. },
  765. mappings: {
  766. 'resolution': 'ar-viewport.resolution',
  767. 'style': 'ar-viewport.style',
  768. 'fullscreen-ui': 'ar-viewport-fullscreen-ui'
  769. }
  770. });
  771. AFRAME.registerComponent('ar-viewport-fullscreen-ui', ARComponent({
  772. schema: {
  773. /** whether or not to include the built-in fullscreen button */
  774. 'enabled': { type: 'boolean', default: true },
  775. }
  776. }));
  777. /**
  778. * AR Heads-Up Display
  779. */
  780. let hudStyle = (function() { // hide on page load
  781. const style = document.createElement('style');
  782. style.textContent = 'ar-hud, [ar-hud] { display: none; }';
  783. document.head.appendChild(style);
  784. return style;
  785. })();
  786. AFRAME.registerComponent('ar-hud', ARComponent({
  787. init()
  788. {
  789. if(hudStyle !== null) {
  790. document.head.removeChild(hudStyle);
  791. hudStyle = null;
  792. }
  793. this.el.hidden = true;
  794. },
  795. validate()
  796. {
  797. if(!this.el.parentNode.getAttribute('ar-viewport'))
  798. throw new Error('ar-hud must be a direct child of ar-viewport');
  799. },
  800. hudContainer()
  801. {
  802. return this.el;
  803. },
  804. }));
  805. AFRAME.registerPrimitive('ar-hud', {
  806. defaultComponents: {
  807. 'ar-hud': {}
  808. }
  809. });
  810. /* ========================================================================= */
  811. /**
  812. * Version check
  813. * @param {object} libs
  814. */
  815. function __THIS_PLUGIN_HAS_BEEN_TESTED_WITH__(libs)
  816. {
  817. window.addEventListener('load', () => {
  818. try { AR, AFRAME;
  819. const versionOf = { 'encantar.js': AR.version.replace(/-.*$/, ''), 'A-Frame': AFRAME.version };
  820. const check = (x,v,w) => v != w ? console.warn(`\n\n\nWARNING\n\nThis plugin has been tested with ${x} version ${v}. The version in use is ${w}. Usage of ${x} version ${v} is recommended instead.\n\n\n`) : void 0;
  821. for(const [lib, expected] of Object.entries(libs))
  822. check(lib, expected.version, versionOf[lib]);
  823. }
  824. catch(e) {
  825. alert(e.message);
  826. }
  827. });
  828. }
  829. })();