UNPKG

@webarkit/ar-nft

Version:

WebAR Javscript library for markerless AR

492 lines (450 loc) 18.8 kB
/* * 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); } }