The project ended up being pretty big, but dynamic. I will copy-paste parts of the code, not to deliver a working solution, but to point in a general direction of how it can be done.
<!-- index.html ->
<a-scene
loading-localization
xrextras-loading
pool__imagetarget="mixin: imagetarget; size: 3; dynamic: false"
camera-feed-delegator
...
renderer="colorManagement:true; webgl2: true"
xrweb="disableWorldTracking: true"
>
<a-assets>
<a-mixin id="imagetarget" ec-named-image-target></a-mixin>
</a-assets>
</a-scene>
// camera-feed-delegator.js
const cameraFeedDelegator = {
init() {
const dynamicTargeting = new DynamicTargeting();
const addElementChildToImageTargetContainer = ({detail}) => {
dynamicTargeting.foundTarget(detail.name, detail, this.el.sceneEl, point);
}
window.addEventListener('xrimagefound', addElementChildToImageTargetContainer);
window.addEventListener('xrimagelost', ({detail}) => {
dynamicTargeting.lostTarget(detail.name, this.el.sceneEl);
});
}
export {cameraFeedDelegator}
// dynamic.targeting.js
export default class DynamicTargeting {
TWO_SECONDS = 2000;
targets = []; // all image targets in the room
newTargets = []; // a set of max 10 image targets that 8th Wall is scanning for
found = new Set(); // all image targets that can be found. The max pool is set to 3 in index.html.
lost = new Set(); // temporary sets a lost image target to be removed within TWO_SECONDS
ELEMENT_NAME = 'named-image-target';
/**
* Adds image target unless recently lost or already has the target
*/
foundTarget(imageTargetName, {scaledWidth, scaledHeight}, sceneEl, point) {
// console.log(this.is, "found target", imageTargetName, point);
if (this.lost.has(imageTargetName)) {
this.lost.delete(imageTargetName)
} else if (!this.found.has(imageTargetName)) {
this.found.add(imageTargetName);
this.targets.splice(this.targets.indexOf(imageTargetName), 1);
let childEl = this._setupImageTargetAction(imageTargetName, point);
// POOLING
if (childEl) {
let imageTargetEl = sceneEl.components.pool__imagetarget.requestEntity();
if (imageTargetEl) {
// For scaling
childEl.components.minDimension = Math.min(scaledWidth, scaledHeight);
childEl.components.maxDimension = Math.max(scaledWidth, scaledHeight);
imageTargetEl.setAttribute(this.ELEMENT_NAME, `name: ${imageTargetName}`);
imageTargetEl.appendChild(childEl);
} else {
this.found.remove(imageTargetName);
sceneEl.components.pool__imagetarget.returnEntity(imageTargetEl);
}
}
}
}
/**
* Removes image target unless the component is playing (isActive)
*/
lostTarget(imageTargetName, sceneEl) {
// console.log(this.is, "lost target", imageTargetName);
for (const imageTargetComponent of sceneEl.components.pool__imagetarget.usedEls) {
if (imageTargetComponent.getAttribute(this.ELEMENT_NAME)?.name == imageTargetName
&& !imageTargetComponent.components.isActive) {
this.lost.add(imageTargetName);
setTimeout(() => { // Avoids temporary loosing target
if (this.lost.has(imageTargetName)) { // can be removed in foundTarget()
this.removeTarget(imageTargetName);
}
}, this.TWO_SECONDS);
}
}
}
removeTarget(name) {
// console.log(this.is, "remove target", name);
this.lost.delete(name);
this.found.delete(name);
this.targets.push(name);
// Tell 'ec-named-image-target' component that the entity should be removed
window.dispatchEvent(new CustomEvent(
'remove-image-target',
{detail: {name: name}}
));
}
}
// ec-named-image-target.js - set as a component on the three image targets
/*
* A container for image targets. Child entities are appended in dynamic-targeting.js
*/
const ecNamedImageTarget = {
schema: {
name: {type: 'string'},
type: {type: 'string'}, // For debugging
isActive: {type: 'bool'}, // Changed by child entity and used in 'dynamic-targeting'
},
get is() {
return 'named-image-target'
},
init() {
const {object3D} = this.el;
const onready = () => {
this.el.sceneEl.removeEventListener('realityready', onready);
object3D.visible = false;
}
this.el.sceneEl.addEventListener('realityready', onready);
const updatePosition = ({detail}) => {
if (this.data.name === detail.name) {
object3D.position.copy(detail.position);
object3D.quaternion.copy(detail.rotation);
object3D.scale.set(detail.scale, detail.scale, detail.scale);
object3D.visible = true;
this.el.firstElementChild?.classList.add('cantap');
}
}
const hideComponent = ({detail}) => {
if (this.data.name === detail.name) {
object3D.visible = false;
this.el.firstElementChild?.classList.remove('cantap');
}
}
const removeComponent = ({detail}) => {
if (this.data.name === detail.name) {
object3D.visible = false;
this.data.isActive = false;
if (this.el.firstElementChild) {
this.el.firstElementChild.object3D.clear() // removes components, like 'material' and 'geometry'
this.el.firstElementChild.removeFromParent(); // sending an event that detaches the child from the DOM
this.el.removeChild(this.el.firstElementChild); // actually removing the child from the DOM
}
this.data.name = null;
this.el.sceneEl.components.pool__imagetarget.returnEntity(this.el);
}
}
this.el.sceneEl.addEventListener('xrimageupdated', updatePosition);
this.el.sceneEl.addEventListener('xrimagelost', hideComponent);
window.addEventListener('remove-image-target', removeComponent);
},
}
export {ecNamedImageTarget}