@webarkit/ar-nft
Version:
WebAR Javscript library for markerless AR
492 lines (450 loc) • 19.3 kB
text/typescript
/*
* ARnft.ts
* ARnft
*
* This file is part of ARnft - WebARKit.
*
* ARnft is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ARnft is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ARnft. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules, and to
* copy and distribute the resulting executable under terms of your choice,
* provided that you also meet, for each linked independent module, the terms and
* conditions of the license of that module. An independent module is a module
* which is neither derived from nor based on this library. If you modify this
* library, you may extend this exception to your version of the library, but you
* are not obligated to do so. If you do not wish to do so, delete this exception
* statement from your version.
*
* Copyright 2021-2025 WebARKit.
*
* Author(s): Walter Perdan @kalwalt https://github.com/kalwalt
*
*/
import Container from "./utils/html/Container";
import { ConfigData } from "./config/ConfigData";
import Stats from "stats.js";
import { CameraViewRenderer, ICameraViewRenderer } from "./renderers/CameraViewRenderer";
import { getConfig } from "./utils/ARnftUtils";
import NFTWorker from "./NFTWorker.simd";
import { v4 as uuidv4 } from "uuid";
import packageJson from "../package.json";
const { version } = packageJson;
/**
* Basic interface for an Entity.
* @param name the name of the Entity
* @param markerUrl the marker url associated
*/
export interface IEntity {
name: string;
markerUrl: string;
}
/**
* IInitConfig interface for the base configuration.
* @param width the width in pixels of the video camera.
* @param height the height in pixels of the video camera.
* @param configUrl the url of the config.json file.
* @param stats true if you want the stats.
* @param autoUpdate false if you want to maintain it yourself
*/
export interface IInitConfig {
/** the width in pixels of the video camera. */
width: number;
/** the height in pixels of the video camera. */
height: number;
/** the url of the config.json file. */
configUrl: string;
/** true if you want the stats. */
stats?: boolean;
/** false if you want to maintain it yourself */
autoUpdate?: boolean;
}
/**
* INameInitConfig extends IInitConfig and it is used by the initWithConfig method.
* @param markerUrls an Array of Array of marker urls.
* @param names an Array of Array of entity names.
*/
export interface INameInitConfig extends IInitConfig {
/** the Array of url of the markers (without the extension) */
markerUrls: Array<Array<string>>;
/** the names of the markers */
names: Array<Array<string>>;
}
/**
* IEntityInitConfig used by the initWithEntities method
* @param entities an Array of Entity
*/
export interface IEntityInitConfig extends IInitConfig {
/** the Array of Entity. */
entities: IEntity[];
}
/**
* IViews is used internally by ARnft
*/
export interface IViews {
container: HTMLDivElement;
canvas: HTMLCanvasElement;
video: HTMLVideoElement;
loading?: HTMLElement;
stats?: HTMLElement;
}
export default class ARnft {
public cameraView: CameraViewRenderer;
public appData: ConfigData;
public addPath: string;
public width: number;
public height: number;
public configUrl: string;
public markerUrl: string;
public camData: string;
public autoUpdate: boolean = true;
private controllers: NFTWorker[];
private static entities: IEntity[];
private target: EventTarget;
private uuid: string;
private version: string;
private initialized: boolean;
private _views: IViews;
/**
* The **ARnft** constructor to create a new instance of the ARnft class.
* Example code:
* ```javascript
* const nft = new ARnft(640, 480, 'config.json');
* ```
* @param width (number) the width in pixels of the video camera.
* @param height (number) the height in pixels of the video camera.
* @param configUrl (string) the url of the config.json file
*/
constructor(width: number, height: number, configUrl: string) {
this.width = width;
this.height = height;
this.configUrl = configUrl;
this.target = window || global;
this.uuid = uuidv4();
this.version = version;
console.log("ARnft ", this.version);
}
/**
* The init function let define the basic set-up for the NFT marker.
* Internally use the initialize function, that is responsible to load all the resources.
* @param width (number) the width in pixels of the video camera.
* @param height (number) the height in pixels of the video camera.
* @param markerUrls (Array<string>) the Array of url of the markers (without the extension)
* @param names the names of the markers
* @param configUrl (string) the url of the config.json file
* @param stats (boolean) true if you want the stats.
* @returns (object) the nft object.
*/
static async init(
width: number,
height: number,
markerUrls: Array<Array<string>>,
names: Array<Array<string>>,
configUrl: string,
stats: boolean
): Promise<object> {
return ARnft.initWithConfig({ width, height, markerUrls, names, configUrl, stats });
}
/**
* The initWithEntities function let set up the NFT markers with an Entity object.
* We set an Array of Entity for multiple NFT markers. An Entity is composed of a unique name and
* a markerUrl.
* Internally use the initialize function, that is responsible to load all the resources.
* @param width (number) the width in pixels of the video camera.
* @param height (number) the height in pixels of the video camera.
* @param entities (Entity[]) the Array of Entity
* @param configUrl (string) the url of the config.json file
* @param stats (boolean) true if you want the stats.
* @returns (object) the nft object.
*/
static async initWithEntities(
width: number,
height: number,
entities: Array<IEntity>,
configUrl: string,
stats: boolean
): Promise<object> {
return ARnft.initWithConfig({ width, height, entities, configUrl, stats });
}
/**
* Initializes the ARnft instance with the provided configuration.
* This method can accept either marker URLs and names or an array of entities.
* It sets up the necessary resources, including the HTML container, stats,
* camera renderer, and NFT workers. Used internally by the initWithEntities method.
*
* @param params - The configuration parameters for initialization.
* @param params.width - The width in pixels of the video camera.
* @param params.height - The height in pixels of the video camera.
* @param params.configUrl - The URL of the config.json file.
* @param params.stats - Optional. True if you want the stats.
* @param params.autoUpdate - Optional. False if you want to maintain it yourself.
* @param params.markerUrls - Optional. An array of arrays of marker URLs.
* @param params.names - Optional. An array of arrays of entity names.
* @param params.entities - Optional. An array of entities.
*
* @returns A promise that resolves to the ARnft object.
*
* @throws Will throw an error if neither markerUrls nor entities are provided.
*/
static async initWithConfig(params: INameInitConfig | IEntityInitConfig) {
const _arnft = new ARnft(params.width, params.height, params.configUrl);
if (params.autoUpdate != null) {
_arnft.autoUpdate = params.autoUpdate;
}
try {
let markerUrls: string[][] = [];
let names;
const nameParams = params as INameInitConfig;
const entityParams = params as IEntityInitConfig;
if (nameParams.markerUrls != null && nameParams.names != null) {
if (entityParams.entities == null) {
markerUrls = nameParams.markerUrls;
names = nameParams.names;
this.entities = names.map(function (v, k, a) {
return { name: v[0], markerUrl: markerUrls[k][0] };
});
}
} else if (entityParams.entities != null) {
this.entities = entityParams.entities;
markerUrls = this.entities.map((x) => [x.markerUrl]);
names = this.entities.map((x) => [x.name]);
} else {
throw new Error("markerUrls or entities can't be undefined");
}
return await _arnft._initialize(markerUrls, names, params.stats);
} catch (error) {
if ((error as { code: string }).code) {
console.error(error);
return Promise.reject(error);
}
}
}
/**
* Used internally by the init static function. It creates the html Container,
* stats, initialize the CameraRenderer for the video stream, and the NFTWorker.
* @param markerUrls the url Array of the markers.
* @param names the names of the markers
* @param stats choose if you want the stats.
* @returns {Promise<this>} A promise that resolves to the ARnft object
*/
private async _initialize(
markerUrls: Array<Array<string>>,
names: Array<Array<string>>,
stats: boolean
): Promise<this> {
const initEvent = new Event("initARnft");
this.target.dispatchEvent(initEvent);
console.log(
"ARnft init() %cstart...",
"color: yellow; background-color: blue; border-radius: 4px; padding: 2px"
);
let statsMain: any, statsWorker: any;
getConfig(this.configUrl)
.then((data) => {
this.appData = data;
this.addPath = data.addPath;
// views
this._views = Container.createContainer(this.appData);
this._views.loading = Container.createLoading(this.appData);
this._views.stats = Container.createStats(this.appData.stats.createHtml, this.appData);
if (stats) {
statsMain = new Stats();
statsMain.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.getElementById("stats1").appendChild(statsMain.dom);
statsWorker = new Stats();
statsWorker.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.getElementById("stats2").appendChild(statsWorker.dom);
}
const containerEvent = new Event("containerEvent");
document.dispatchEvent(containerEvent);
this.controllers = [];
this.cameraView = new CameraViewRenderer(this._views.video);
return this.cameraView.initialize(this.appData.videoSettings);
})
.then(() => {
const renderUpdate = () => (stats ? statsMain.update() : null);
const trackUpdate = () => (stats ? statsWorker.update() : null);
markerUrls.forEach((markerUrl: Array<string>, index: number) => {
this.controllers.push(
new NFTWorker(markerUrl, this.width, this.height, this.uuid, names[index][0], this.addPath)
);
this.controllers[index].initialize(
this.appData.cameraPara,
renderUpdate,
trackUpdate,
this.appData.oef
);
});
this.initialized = true;
})
.catch(function (error: any) {
return Promise.reject(error);
});
this.target.addEventListener("nftLoaded-" + this.uuid, () => {
const nftWorkersNotReady = this.controllers.filter((nftWorker) => {
return nftWorker.isReady() === false;
});
if (nftWorkersNotReady.length === 0) {
this.target.dispatchEvent(new CustomEvent<object>("ARnftIsReady"));
}
});
let _update = () => {
if (this.initialized && this.autoUpdate) {
this.controllers.forEach((controller) =>
controller.process(this.cameraView.image, this.cameraView.frame)
);
}
requestAnimationFrame(_update);
};
_update();
return this;
}
/**
* Used for a custom initialization of the camera and mediaStream. It creates the html Container,
* stats, initialize the CameraRenderer for the video stream, and the NFTWorker. You must provide
* your own cameraView based on the ICameraViewRenderer interface.
* @param markerUrls the url Array of the markers.
* @param names the names of the markers.
* @param cameraView the own CameraViewRenderer class instance.
* @param stats choose if you want the stats.
* @returns {Promise<this>} A promise that resolves to the ARnft object
*/
public async initializeRaw(
markerUrls: Array<Array<string>>,
names: Array<string>,
cameraView: ICameraViewRenderer,
stats: boolean
): Promise<this> {
const initEvent = new Event("initARnft");
this.target.dispatchEvent(initEvent);
console.log(
"ARnft init() %cstart...",
"color: yellow; background-color: blue; border-radius: 4px; padding: 2px"
);
let statsMain: any, statsWorker: any;
getConfig(this.configUrl)
.then((data) => {
this.appData = data;
this.addPath = data.addPath;
// views
this._views = Container.createContainer(this.appData);
this._views.loading = Container.createLoading(this.appData);
this._views.stats = Container.createStats(this.appData.stats.createHtml, this.appData);
if (stats) {
statsMain = new Stats();
statsMain.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.getElementById("stats1").appendChild(statsMain.dom);
statsWorker = new Stats();
statsWorker.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.getElementById("stats2").appendChild(statsWorker.dom);
}
const containerEvent = new Event("containerEvent");
document.dispatchEvent(containerEvent);
this.controllers = [];
return cameraView.initialize(this.appData.videoSettings);
})
.then(() => {
const renderUpdate = () => (stats ? statsMain.update() : null);
const trackUpdate = () => (stats ? statsWorker.update() : null);
markerUrls.forEach((markerUrl: Array<string>, index: number) => {
this.controllers.push(
new NFTWorker(markerUrl, this.width, this.height, this.uuid, names[index], this.addPath)
);
this.controllers[index].initialize(
this.appData.cameraPara,
renderUpdate,
trackUpdate,
this.appData.oef
);
});
this.initialized = true;
})
.catch(function (error: any) {
return Promise.reject(error);
});
this.target.addEventListener("nftLoaded-" + this.uuid, () => {
const nftWorkersNotReady = this.controllers.filter((nftWorker) => {
return nftWorker.isReady() === false;
});
if (nftWorkersNotReady.length === 0) {
this.target.dispatchEvent(new CustomEvent<object>("ARnftIsReady"));
}
});
let _update = () => {
if (this.initialized && this.autoUpdate) {
this.controllers.forEach((controller) =>
controller.process(cameraView.getImage(), cameraView.getFrame())
);
}
requestAnimationFrame(_update);
};
_update();
return this;
}
/**
* Default autoUpdate true. If set, don't call this function. When it isn't, then you have to maintain it yourself.
*/
public update(): void {
if (!this.initialized || this.autoUpdate) return;
if (this.cameraView != null) {
this.controllers.forEach((controller) => controller.process(this.cameraView.image, this.cameraView.frame));
}
}
public static getEntities(): IEntity[] {
return this.entities;
}
/**
*
* @returns the event target
*/
public getEventTarget(): EventTarget {
return this.target;
}
public get views() {
return Object.freeze(this._views);
}
/**
* Dispose the Video stream and the NFTWorker.
*/
public dispose() {
this.disposeVideoStream();
this.disposeAllNFTs();
}
/**
* Dispose only the NFTWorker.
*/
public disposeNFT(name: string) {
let terminateWorker = "terminateWorker-" + name;
const event = new Event(terminateWorker);
this.target.dispatchEvent(event);
}
/**
* Dispose the Array of NFTWorkers.
*/
public disposeAllNFTs() {
const entities = ARnft.getEntities();
entities.forEach((entity) => {
this.disposeNFT(entity.name);
});
}
/**
* Dispose only the video stream.
*/
public disposeVideoStream() {
this.cameraView.destroy();
const event = new Event("stopVideoStreaming");
this.target.dispatchEvent(event);
}
}