UNPKG

@ar-js-org/ar.js-threejs

Version:

AR.js (THREEx, ARjs) modular package - Typescript version

402 lines (350 loc) 13.9 kB
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; }; }