@ar-js-org/ar.js-threejs
Version:
AR.js (THREEx, ARjs) modular package - Typescript version
402 lines (350 loc) • 13.9 kB
text/typescript
import { IArToolkitSource, IArToolkitContext, ISourceParameters, IUserMediaConstraints } from "./CommonInterfaces/THREEx-interfaces";
import { setParameters } from "./common-functions/utilityFunctions";
import { Renderer, Camera } from 'three'
declare global {
var arToolkitSource: IArToolkitSource;
}
/**
* ArToolkitSource
* @class ArToolkitSource
*/
export class ArToolkitSource implements IArToolkitSource {
public ready: boolean;
//public domElement: HTMLImageElement | HTMLVideoElement;
public domElement: any;
public parameters: ISourceParameters;
private className: string;
/**
* The ArToolkitSource constructor. Create a new instance of the class with all the parameters.
* @constructor
* @param {ISourceParameters} parameters
*/
constructor(parameters: ISourceParameters) {
this.className = "ArToolkitSource";
this.ready = false;
this.domElement = null;
// handle default parameters
this.parameters = {
// type of source - ['webcam', 'image', 'video']
sourceType: "webcam",
// url of the source - valid if sourceType = image|video
sourceUrl: null,
// Device id of the camera to use (optional)
deviceId: null,
// resolution of at which we initialize in the source image
sourceWidth: 640,
sourceHeight: 480,
// resolution displayed for the source
displayWidth: 640,
displayHeight: 480,
};
//////////////////////////////////////////////////////////////////////////////
// setParameters
//////////////////////////////////////////////////////////////////////////////
setParameters(parameters, this);
}
/**
* The most useful function in the ArToolkitSource class.
* It initialize all the necessary to grab the source data and the domElement used internally.
* You must provide a onReady and a onError function as a callback.
* @param onReady
* @param onError
* @returns this
*/
init(onReady: Function, onError: Function): this {
var _this = this;
var domElement: any;
if (this.parameters.sourceType === "image") {
domElement = this._initSourceImage(onSourceReady, onError);
} else if (this.parameters.sourceType === "video") {
domElement = this._initSourceVideo(onSourceReady, onError);
} else if (this.parameters.sourceType === "webcam") {
domElement = this._initSourceWebcam(onSourceReady, onError);
} else {
console.assert(false);
}
// attach
this.domElement = domElement;
this.domElement.style.position = "absolute";
this.domElement.style.top = "0px";
this.domElement.style.left = "0px";
this.domElement.style.zIndex = "-2";
this.domElement.setAttribute("id", "arjs-video");
return this;
function onSourceReady() {
if (!_this.domElement) {
return;
}
document.body.appendChild(_this.domElement);
window.dispatchEvent(
new CustomEvent("arjs-video-loaded", {
detail: {
component: document.querySelector("#arjs-video"),
},
})
);
_this.ready = true;
onReady && onReady();
}
};
/**
* domElementWidth return the style.width of the queried domElement.
* @returns {number} width of the domElement
*/
domElementWidth(): number {
return parseInt(this.domElement.style.width);
};
/**
* domElementHeigth return the style.height of the queried domElement.
* @returns {number} height of the domElement
*/
domElementHeight(): number {
return parseInt(this.domElement.style.height);
};
/**
* onResizeElement resize the domElement when source change.
* You need to call copyElementSizeTo after it.
*/
onResizeElement(): void {
var screenWidth = window.innerWidth;
var screenHeight = window.innerHeight;
// sanity check
console.assert(arguments.length === 0);
// compute sourceWidth, sourceHeight
if (this.domElement.nodeName === "IMG" && this.domElement instanceof HTMLImageElement) {
var sourceWidth: number = this.domElement.naturalWidth;
var sourceHeight = this.domElement.naturalHeight;
} else if (this.domElement.nodeName === "VIDEO" && this.domElement instanceof HTMLVideoElement) {
var sourceWidth = this.domElement.videoWidth;
var sourceHeight = this.domElement.videoHeight;
} else {
console.assert(false);
}
// compute sourceAspect
var sourceAspect = sourceWidth / sourceHeight;
// compute screenAspect
var screenAspect = screenWidth / screenHeight;
// if screenAspect < sourceAspect, then change the width, else change the height
if (screenAspect < sourceAspect) {
// compute newWidth and set .width/.marginLeft
var newWidth = sourceAspect * screenHeight;
this.domElement.style.width = newWidth + "px";
this.domElement.style.marginLeft = -(newWidth - screenWidth) / 2 + "px";
// init style.height/.marginTop to normal value
this.domElement.style.height = screenHeight + "px";
this.domElement.style.marginTop = "0px";
} else {
// compute newHeight and set .height/.marginTop
var newHeight = 1 / (sourceAspect / screenWidth);
this.domElement.style.height = newHeight + "px";
this.domElement.style.marginTop = -(newHeight - screenHeight) / 2 + "px";
// init style.width/.marginLeft to normal value
this.domElement.style.width = screenWidth + "px";
this.domElement.style.marginLeft = "0px";
}
};
/*
Source.prototype.copyElementSizeTo = function(otherElement){
otherElement.style.width = this.domElement.style.width
otherElement.style.height = this.domElement.style.height
otherElement.style.marginLeft = this.domElement.style.marginLeft
otherElement.style.marginTop = this.domElement.style.marginTop
}
*/
/**
* copyElementSizeTo let make a copy of the style settings of the domElement.
* It copy width, height, marginLeft and marginTop.
* @param {any} otherElement
*/
copyElementSizeTo(otherElement: any): void {
if (window.innerWidth > window.innerHeight) {
//landscape
otherElement.style.width = this.domElement.style.width;
otherElement.style.height = this.domElement.style.height;
otherElement.style.marginLeft = this.domElement.style.marginLeft;
otherElement.style.marginTop = this.domElement.style.marginTop;
} else {
//portrait
otherElement.style.height = this.domElement.style.height;
otherElement.style.width =
(parseInt(otherElement.style.height) * 4) / 3 + "px";
otherElement.style.marginLeft =
(window.innerWidth - parseInt(otherElement.style.width)) / 2 + "px";
otherElement.style.marginTop = 0;
}
};
/**
* Depreacted function use copyELementSizeTo instead.
*/
copySizeTo() {
console.warn(
"obsolete function arToolkitSource.copySizeTo. Use arToolkitSource.copyElementSizeTo"
);
//@ts-ignore
this.copyElementSizeTo.apply(this, arguments);
};
/**
* Depreacted function use onResizeElement instead.
*/
onResize(arToolkitContext: IArToolkitContext, renderer: Renderer, camera: Camera) {
if (arguments.length !== 3) {
console.warn(
"obsolete function arToolkitSource.onResize. Use arToolkitSource.onResizeElement"
);
//@ts-ignore
return this.onResizeElement.apply(this, arguments);
}
var trackingBackend = arToolkitContext.parameters.trackingBackend;
// RESIZE DOMELEMENT
if (trackingBackend === "artoolkit") {
this.onResizeElement();
var isAframe = renderer.domElement.dataset.aframeCanvas ? true : false;
if (isAframe === false) {
this.copyElementSizeTo(renderer.domElement);
} else {
}
if (arToolkitContext.arController !== null) {
this.copyElementSizeTo(arToolkitContext.arController.canvas);
}
} else console.assert(false, "unhandled trackingBackend " + trackingBackend);
// UPDATE CAMERA
if (trackingBackend === "artoolkit") {
if (arToolkitContext.arController !== null) {
camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
}
} else console.assert(false, "unhandled trackingBackend " + trackingBackend);
};
// private methods used internally by the class.
private onInitialClick() {
if (this.domElement && this.domElement instanceof HTMLVideoElement) {
this.domElement.play().then(() => { });
}
};
private _initSourceImage(onReady: any, onError: any): HTMLImageElement {
// TODO make it static
var domElement = document.createElement("img") as unknown as HTMLImageElement;
domElement.src = this.parameters.sourceUrl;
domElement.width = this.parameters.sourceWidth;
domElement.height = this.parameters.sourceHeight;
domElement.style.width = this.parameters.displayWidth + "px";
domElement.style.height = this.parameters.displayHeight + "px";
domElement.onload = onReady;
return domElement;
};
private _initSourceVideo(onReady: any, onError: any): HTMLVideoElement {
// TODO make it static
var domElement = document.createElement("video") as unknown as HTMLVideoElement;
domElement.src = this.parameters.sourceUrl;
domElement.style.objectFit = "initial";
domElement.autoplay = true;
// error in tsc!
//domElement.webkitPlaysinline = true;
domElement.controls = false;
domElement.loop = true;
domElement.muted = true;
// start the video on first click if not started automatically
document.body.addEventListener("click", this.onInitialClick, { once: true });
domElement.width = this.parameters.sourceWidth;
domElement.height = this.parameters.sourceHeight;
domElement.style.width = this.parameters.displayWidth + "px";
domElement.style.height = this.parameters.displayHeight + "px";
domElement.onloadeddata = onReady;
return domElement;
};
private _initSourceWebcam(onReady: any, onError: any): HTMLVideoElement {
var _this = this;
// init default value
onError =
onError ||
function (error: any) {
var event = new CustomEvent("camera-error", { detail: { error: error } });
window.dispatchEvent(event);
setTimeout(() => {
if (!document.getElementById("error-popup")) {
var errorPopup = document.createElement("div");
errorPopup.innerHTML =
"Webcam Error\nName: " + error.name + "\nMessage: " + error.message;
errorPopup.setAttribute("id", "error-popup");
document.body.appendChild(errorPopup);
}
}, 1000);
};
var domElement = document.createElement("video") as unknown as HTMLVideoElement;
domElement.setAttribute("autoplay", "");
domElement.setAttribute("muted", "");
domElement.setAttribute("playsinline", "");
domElement.style.width = this.parameters.displayWidth + "px";
domElement.style.height = this.parameters.displayHeight + "px";
// check API is available
if (
navigator.mediaDevices === undefined ||
navigator.mediaDevices.enumerateDevices === undefined ||
navigator.mediaDevices.getUserMedia === undefined
) {
if (navigator.mediaDevices === undefined)
var fctName = "navigator.mediaDevices";
else if (navigator.mediaDevices.enumerateDevices === undefined)
var fctName = "navigator.mediaDevices.enumerateDevices";
else if (navigator.mediaDevices.getUserMedia === undefined)
var fctName = "navigator.mediaDevices.getUserMedia";
else console.assert(false);
onError({
name: "",
message: "WebRTC issue-! " + fctName + " not present in your browser",
});
return null;
}
// get available devices
navigator.mediaDevices
.enumerateDevices()
.then(function (devices) {
var userMediaConstraints: IUserMediaConstraints = {
audio: false,
//@ts-ignore
video: {
facingMode: "environment",
width: {
ideal: _this.parameters.sourceWidth,
// min: 1024,
// max: 1920
},
height: {
ideal: _this.parameters.sourceHeight,
// min: 776,
// max: 1080
},
//deviceId: { exact: '' }
},
};
if (!!_this.parameters.deviceId) {
userMediaConstraints.video.deviceId.exact = _this.parameters.deviceId;
}
// get a device which satisfy the constraints
navigator.mediaDevices
.getUserMedia(userMediaConstraints)
.then((stream) => {
// set the .src of the domElement
domElement.srcObject = stream;
var event = new CustomEvent<object>("camera-init", { detail: { stream: stream } });
window.dispatchEvent(event);
// start the video on first click if not started automatically
document.body.addEventListener("click", _this.onInitialClick, {
once: true,
});
onReady();
})
.catch(function (error) {
onError({
name: error.name,
message: error.message,
});
});
})
.catch(function (error) {
onError({
message: error.message,
});
});
return domElement;
};
}