@srsergio/taptapp-ar 1.0.12 β†’ 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,7 +134,11 @@ const controller = new Controller({
134
134
  }
135
135
  });
136
136
 
137
+ // Single target
137
138
  await controller.addImageTargets('./targets.mind');
139
+
140
+ // OR multiple targets from different .mind files
141
+ await controller.addImageTargets(['./target1.mind', './target2.mind', './target3.mind']);
138
142
  controller.processVideo(videoElement); // Starts the internal RAF loop
139
143
  ```
140
144
 
@@ -156,6 +160,59 @@ if (targetIndex !== -1) {
156
160
  }
157
161
  ```
158
162
 
163
+ ### 4. Vanilla JS (No Framework) 🍦
164
+ The **simplest way** to use ARβ€”no Three.js, no A-Frame. Just overlay an image on the tracked target.
165
+
166
+ ```javascript
167
+ import { SimpleAR } from '@srsergio/taptapp-ar';
168
+
169
+ const ar = new SimpleAR({
170
+ container: document.getElementById('ar-container'),
171
+ targetSrc: './my-target.mind', // Single URL or array: ['./a.mind', './b.mind']
172
+ overlay: document.getElementById('my-overlay'),
173
+ onFound: ({ targetIndex }) => console.log(`Target ${targetIndex} detected! 🎯`),
174
+ onLost: ({ targetIndex }) => console.log(`Target ${targetIndex} lost πŸ‘‹`)
175
+ });
176
+
177
+ await ar.start();
178
+
179
+ // When done:
180
+ ar.stop();
181
+ ```
182
+
183
+ #### πŸ“ Minimal HTML
184
+ ```html
185
+ <div id="ar-container" style="width: 100vw; height: 100vh;">
186
+ <img id="my-overlay" src="./overlay.png"
187
+ style="opacity: 0; z-index: 1; width: 200px; transition: opacity 0.3s;" />
188
+ </div>
189
+
190
+ <script type="module">
191
+ import { SimpleAR } from '@srsergio/taptapp-ar';
192
+
193
+ const ar = new SimpleAR({
194
+ container: document.getElementById('ar-container'),
195
+ targetSrc: './targets.mind',
196
+ overlay: document.getElementById('my-overlay'),
197
+ });
198
+
199
+ ar.start();
200
+ </script>
201
+ ```
202
+
203
+ #### βš™οΈ SimpleAR Options
204
+ | Option | Required | Description |
205
+ | :--- | :--- | :--- |
206
+ | `container` | βœ… | DOM element where video + overlay render |
207
+ | `targetSrc` | βœ… | URL to your `.mind` file |
208
+ | `overlay` | βœ… | DOM element to position on the target |
209
+ | `onFound` | ❌ | Callback when target is detected |
210
+ | `onLost` | ❌ | Callback when target is lost |
211
+ | `onUpdate` | ❌ | Called each frame with `{ targetIndex, worldMatrix }` |
212
+ | `cameraConfig` | ❌ | Camera constraints (default: `{ facingMode: 'environment', width: 1280, height: 720 }`) |
213
+
214
+ ---
215
+
159
216
  #### πŸ› οΈ Life-cycle Management
160
217
  Properly management is crucial to avoid memory leaks:
161
218
 
@@ -31,13 +31,35 @@ export class Controller {
31
31
  projectionMatrix: number[];
32
32
  _setupWorkerListener(): void;
33
33
  _ensureWorker(): void;
34
- addImageTargets(fileURL: any): Promise<any>;
35
- addImageTargetsFromBuffer(buffer: any): {
34
+ /**
35
+ * Load image targets from one or multiple .mind files
36
+ * @param {string|string[]} fileURLs - Single URL or array of URLs to .mind files
37
+ * @returns {Promise<{dimensions, matchingDataList, trackingDataList}>}
38
+ */
39
+ addImageTargets(fileURLs: string | string[]): Promise<{
40
+ dimensions: any;
41
+ matchingDataList: any;
42
+ trackingDataList: any;
43
+ }>;
44
+ /**
45
+ * Load image targets from multiple ArrayBuffers
46
+ * @param {ArrayBuffer[]} buffers - Array of .mind file buffers
47
+ */
48
+ addImageTargetsFromBuffers(buffers: ArrayBuffer[]): {
36
49
  dimensions: any[][];
37
50
  matchingDataList: any[];
38
51
  trackingDataList: any[];
39
52
  };
40
53
  tracker: Tracker | undefined;
54
+ /**
55
+ * Load image targets from a single ArrayBuffer (backward compatible)
56
+ * @param {ArrayBuffer} buffer - Single .mind file buffer
57
+ */
58
+ addImageTargetsFromBuffer(buffer: ArrayBuffer): {
59
+ dimensions: any[][];
60
+ matchingDataList: any[];
61
+ trackingDataList: any[];
62
+ };
41
63
  dispose(): void;
42
64
  dummyRun(input: any): void;
43
65
  getProjectionMatrix(): number[];
@@ -75,27 +75,39 @@ class Controller {
75
75
  this._setupWorkerListener();
76
76
  }
77
77
  }
78
- addImageTargets(fileURL) {
79
- return new Promise(async (resolve) => {
80
- const content = await fetch(fileURL);
81
- const buffer = await content.arrayBuffer();
82
- const result = this.addImageTargetsFromBuffer(buffer);
83
- resolve(result);
84
- });
78
+ /**
79
+ * Load image targets from one or multiple .mind files
80
+ * @param {string|string[]} fileURLs - Single URL or array of URLs to .mind files
81
+ * @returns {Promise<{dimensions, matchingDataList, trackingDataList}>}
82
+ */
83
+ async addImageTargets(fileURLs) {
84
+ const urls = Array.isArray(fileURLs) ? fileURLs : [fileURLs];
85
+ // Fetch all .mind files in parallel
86
+ const buffers = await Promise.all(urls.map(async (url) => {
87
+ const response = await fetch(url);
88
+ return response.arrayBuffer();
89
+ }));
90
+ // Combine all buffers into a single target list
91
+ return this.addImageTargetsFromBuffers(buffers);
85
92
  }
86
- addImageTargetsFromBuffer(buffer) {
87
- const compiler = new Compiler();
88
- const dataList = compiler.importData(buffer);
89
- const trackingDataList = [];
90
- const matchingDataList = [];
91
- const dimensions = [];
92
- for (let i = 0; i < dataList.length; i++) {
93
- const item = dataList[i];
94
- matchingDataList.push(item.matchingData);
95
- trackingDataList.push(item.trackingData);
96
- dimensions.push([item.targetImage.width, item.targetImage.height]);
93
+ /**
94
+ * Load image targets from multiple ArrayBuffers
95
+ * @param {ArrayBuffer[]} buffers - Array of .mind file buffers
96
+ */
97
+ addImageTargetsFromBuffers(buffers) {
98
+ const allTrackingData = [];
99
+ const allMatchingData = [];
100
+ const allDimensions = [];
101
+ for (const buffer of buffers) {
102
+ const compiler = new Compiler();
103
+ const dataList = compiler.importData(buffer);
104
+ for (const item of dataList) {
105
+ allMatchingData.push(item.matchingData);
106
+ allTrackingData.push(item.trackingData);
107
+ allDimensions.push([item.targetImage.width, item.targetImage.height]);
108
+ }
97
109
  }
98
- this.tracker = new Tracker(dimensions, trackingDataList, this.projectionTransform, this.inputWidth, this.inputHeight, this.debugMode);
110
+ this.tracker = new Tracker(allDimensions, allTrackingData, this.projectionTransform, this.inputWidth, this.inputHeight, this.debugMode);
99
111
  this._ensureWorker();
100
112
  if (this.worker) {
101
113
  this.worker.postMessage({
@@ -104,11 +116,18 @@ class Controller {
104
116
  inputHeight: this.inputHeight,
105
117
  projectionTransform: this.projectionTransform,
106
118
  debugMode: this.debugMode,
107
- matchingDataList,
119
+ matchingDataList: allMatchingData,
108
120
  });
109
121
  }
110
- this.markerDimensions = dimensions;
111
- return { dimensions, matchingDataList, trackingDataList };
122
+ this.markerDimensions = allDimensions;
123
+ return { dimensions: allDimensions, matchingDataList: allMatchingData, trackingDataList: allTrackingData };
124
+ }
125
+ /**
126
+ * Load image targets from a single ArrayBuffer (backward compatible)
127
+ * @param {ArrayBuffer} buffer - Single .mind file buffer
128
+ */
129
+ addImageTargetsFromBuffer(buffer) {
130
+ return this.addImageTargetsFromBuffers([buffer]);
112
131
  }
113
132
  dispose() {
114
133
  this.stopProcessVideo();
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
3
+ *
4
+ * No Three.js. No A-Frame. Just HTML, CSS, and JavaScript.
5
+ *
6
+ * @example
7
+ * const ar = new SimpleAR({
8
+ * container: document.getElementById('ar-container'),
9
+ * targetSrc: './my-target.mind',
10
+ * overlay: document.getElementById('my-overlay'),
11
+ * onFound: () => console.log('Target found!'),
12
+ * onLost: () => console.log('Target lost!')
13
+ * });
14
+ *
15
+ * await ar.start();
16
+ */
17
+ export class SimpleAR {
18
+ constructor({ container, targetSrc, overlay, onFound, onLost, onUpdate, cameraConfig, }: {
19
+ container: any;
20
+ targetSrc: any;
21
+ overlay: any;
22
+ onFound?: null | undefined;
23
+ onLost?: null | undefined;
24
+ onUpdate?: null | undefined;
25
+ cameraConfig?: {
26
+ facingMode: string;
27
+ width: number;
28
+ height: number;
29
+ } | undefined;
30
+ });
31
+ container: any;
32
+ targetSrc: any;
33
+ overlay: any;
34
+ onFound: any;
35
+ onLost: any;
36
+ onUpdateCallback: any;
37
+ cameraConfig: {
38
+ facingMode: string;
39
+ width: number;
40
+ height: number;
41
+ };
42
+ video: HTMLVideoElement | null;
43
+ controller: Controller | null;
44
+ isTracking: boolean;
45
+ lastMatrix: any;
46
+ /**
47
+ * Initialize and start AR tracking
48
+ */
49
+ start(): Promise<this>;
50
+ /**
51
+ * Stop AR tracking and release resources
52
+ */
53
+ stop(): void;
54
+ _createVideo(): void;
55
+ _startCamera(): Promise<void>;
56
+ _initController(): void;
57
+ _handleUpdate(data: any): void;
58
+ _positionOverlay(worldMatrix: any): void;
59
+ }
60
+ import { Controller } from "./controller.js";
@@ -0,0 +1,173 @@
1
+ import { Controller } from "./controller.js";
2
+ /**
3
+ * 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
4
+ *
5
+ * No Three.js. No A-Frame. Just HTML, CSS, and JavaScript.
6
+ *
7
+ * @example
8
+ * const ar = new SimpleAR({
9
+ * container: document.getElementById('ar-container'),
10
+ * targetSrc: './my-target.mind',
11
+ * overlay: document.getElementById('my-overlay'),
12
+ * onFound: () => console.log('Target found!'),
13
+ * onLost: () => console.log('Target lost!')
14
+ * });
15
+ *
16
+ * await ar.start();
17
+ */
18
+ class SimpleAR {
19
+ constructor({ container, targetSrc, overlay, onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, }) {
20
+ this.container = container;
21
+ this.targetSrc = targetSrc;
22
+ this.overlay = overlay;
23
+ this.onFound = onFound;
24
+ this.onLost = onLost;
25
+ this.onUpdateCallback = onUpdate;
26
+ this.cameraConfig = cameraConfig;
27
+ this.video = null;
28
+ this.controller = null;
29
+ this.isTracking = false;
30
+ this.lastMatrix = null;
31
+ }
32
+ /**
33
+ * Initialize and start AR tracking
34
+ */
35
+ async start() {
36
+ // 1. Create video element
37
+ this._createVideo();
38
+ // 2. Start camera
39
+ await this._startCamera();
40
+ // 3. Initialize controller
41
+ this._initController();
42
+ // 4. Load targets (supports single URL or array of URLs)
43
+ const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
44
+ await this.controller.addImageTargets(targets);
45
+ this.controller.processVideo(this.video);
46
+ return this;
47
+ }
48
+ /**
49
+ * Stop AR tracking and release resources
50
+ */
51
+ stop() {
52
+ if (this.controller) {
53
+ this.controller.dispose();
54
+ this.controller = null;
55
+ }
56
+ if (this.video && this.video.srcObject) {
57
+ this.video.srcObject.getTracks().forEach(track => track.stop());
58
+ this.video.remove();
59
+ this.video = null;
60
+ }
61
+ this.isTracking = false;
62
+ }
63
+ _createVideo() {
64
+ this.video = document.createElement('video');
65
+ this.video.setAttribute('autoplay', '');
66
+ this.video.setAttribute('playsinline', '');
67
+ this.video.setAttribute('muted', '');
68
+ this.video.style.cssText = `
69
+ position: absolute;
70
+ top: 0;
71
+ left: 0;
72
+ width: 100%;
73
+ height: 100%;
74
+ object-fit: cover;
75
+ z-index: 0;
76
+ `;
77
+ this.container.style.position = 'relative';
78
+ this.container.style.overflow = 'hidden';
79
+ this.container.insertBefore(this.video, this.container.firstChild);
80
+ }
81
+ async _startCamera() {
82
+ const stream = await navigator.mediaDevices.getUserMedia({
83
+ video: this.cameraConfig
84
+ });
85
+ this.video.srcObject = stream;
86
+ await this.video.play();
87
+ // Wait for video dimensions to be available
88
+ await new Promise(resolve => {
89
+ if (this.video.videoWidth > 0)
90
+ return resolve();
91
+ this.video.onloadedmetadata = resolve;
92
+ });
93
+ }
94
+ _initController() {
95
+ this.controller = new Controller({
96
+ inputWidth: this.video.videoWidth,
97
+ inputHeight: this.video.videoHeight,
98
+ onUpdate: (data) => this._handleUpdate(data)
99
+ });
100
+ }
101
+ _handleUpdate(data) {
102
+ if (data.type !== 'updateMatrix')
103
+ return;
104
+ const { targetIndex, worldMatrix } = data;
105
+ if (worldMatrix) {
106
+ // Target found
107
+ if (!this.isTracking) {
108
+ this.isTracking = true;
109
+ this.overlay && (this.overlay.style.opacity = '1');
110
+ this.onFound && this.onFound({ targetIndex });
111
+ }
112
+ this.lastMatrix = worldMatrix;
113
+ this._positionOverlay(worldMatrix);
114
+ this.onUpdateCallback && this.onUpdateCallback({ targetIndex, worldMatrix });
115
+ }
116
+ else {
117
+ // Target lost
118
+ if (this.isTracking) {
119
+ this.isTracking = false;
120
+ this.overlay && (this.overlay.style.opacity = '0');
121
+ this.onLost && this.onLost({ targetIndex });
122
+ }
123
+ }
124
+ }
125
+ _positionOverlay(worldMatrix) {
126
+ if (!this.overlay)
127
+ return;
128
+ const containerRect = this.container.getBoundingClientRect();
129
+ const videoW = this.video.videoWidth;
130
+ const videoH = this.video.videoHeight;
131
+ // Calculate display area considering object-fit: cover
132
+ const containerAspect = containerRect.width / containerRect.height;
133
+ const videoAspect = videoW / videoH;
134
+ let displayW, displayH, offsetX, offsetY;
135
+ if (containerAspect > videoAspect) {
136
+ // Container is wider - video fills width, crops height
137
+ displayW = containerRect.width;
138
+ displayH = containerRect.width / videoAspect;
139
+ offsetX = 0;
140
+ offsetY = (containerRect.height - displayH) / 2;
141
+ }
142
+ else {
143
+ // Container is taller - video fills height, crops width
144
+ displayH = containerRect.height;
145
+ displayW = containerRect.height * videoAspect;
146
+ offsetX = (containerRect.width - displayW) / 2;
147
+ offsetY = 0;
148
+ }
149
+ const scaleX = displayW / videoW;
150
+ const scaleY = displayH / videoH;
151
+ // Extract position and rotation from world matrix
152
+ // Matrix is column-major: [m0,m1,m2,m3, m4,m5,m6,m7, m8,m9,m10,m11, m12,m13,m14,m15]
153
+ const tx = worldMatrix[12];
154
+ const ty = worldMatrix[13];
155
+ const scale = Math.sqrt(worldMatrix[0] ** 2 + worldMatrix[1] ** 2);
156
+ const rotation = Math.atan2(worldMatrix[1], worldMatrix[0]);
157
+ // Convert from normalized coords to screen coords
158
+ const screenX = offsetX + (videoW / 2 + tx) * scaleX;
159
+ const screenY = offsetY + (videoH / 2 - ty) * scaleY;
160
+ // Apply transform
161
+ this.overlay.style.position = 'absolute';
162
+ this.overlay.style.transformOrigin = 'center center';
163
+ this.overlay.style.left = '0';
164
+ this.overlay.style.top = '0';
165
+ this.overlay.style.transform = `
166
+ translate(${screenX}px, ${screenY}px)
167
+ translate(-50%, -50%)
168
+ scale(${scale * scaleX * 0.01})
169
+ rotate(${-rotation}rad)
170
+ `;
171
+ }
172
+ }
173
+ export { SimpleAR };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./react/types";
2
2
  export * from "./compiler/offline-compiler";
3
+ export { Controller } from "./compiler/controller.js";
4
+ export { SimpleAR } from "./compiler/simple-ar.js";
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./react/types";
2
2
  export * from "./compiler/offline-compiler";
3
+ export { Controller } from "./compiler/controller.js";
4
+ export { SimpleAR } from "./compiler/simple-ar.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,20 @@
37
37
  "react-dom": ">=18.0.0",
38
38
  "three": ">=0.160.0"
39
39
  },
40
+ "peerDependenciesMeta": {
41
+ "aframe": {
42
+ "optional": true
43
+ },
44
+ "react": {
45
+ "optional": true
46
+ },
47
+ "react-dom": {
48
+ "optional": true
49
+ },
50
+ "three": {
51
+ "optional": true
52
+ }
53
+ },
40
54
  "dependencies": {
41
55
  "@msgpack/msgpack": "^3.0.0-beta2",
42
56
  "ml-matrix": "^6.10.4",
@@ -91,32 +91,49 @@ class Controller {
91
91
  }
92
92
  }
93
93
 
94
- addImageTargets(fileURL) {
95
- return new Promise(async (resolve) => {
96
- const content = await fetch(fileURL);
97
- const buffer = await content.arrayBuffer();
98
- const result = this.addImageTargetsFromBuffer(buffer);
99
- resolve(result);
100
- });
94
+ /**
95
+ * Load image targets from one or multiple .mind files
96
+ * @param {string|string[]} fileURLs - Single URL or array of URLs to .mind files
97
+ * @returns {Promise<{dimensions, matchingDataList, trackingDataList}>}
98
+ */
99
+ async addImageTargets(fileURLs) {
100
+ const urls = Array.isArray(fileURLs) ? fileURLs : [fileURLs];
101
+
102
+ // Fetch all .mind files in parallel
103
+ const buffers = await Promise.all(
104
+ urls.map(async (url) => {
105
+ const response = await fetch(url);
106
+ return response.arrayBuffer();
107
+ })
108
+ );
109
+
110
+ // Combine all buffers into a single target list
111
+ return this.addImageTargetsFromBuffers(buffers);
101
112
  }
102
113
 
103
- addImageTargetsFromBuffer(buffer) {
104
- const compiler = new Compiler();
105
- const dataList = compiler.importData(buffer);
106
-
107
- const trackingDataList = [];
108
- const matchingDataList = [];
109
- const dimensions = [];
110
- for (let i = 0; i < dataList.length; i++) {
111
- const item = dataList[i];
112
- matchingDataList.push(item.matchingData);
113
- trackingDataList.push(item.trackingData);
114
- dimensions.push([item.targetImage.width, item.targetImage.height]);
114
+ /**
115
+ * Load image targets from multiple ArrayBuffers
116
+ * @param {ArrayBuffer[]} buffers - Array of .mind file buffers
117
+ */
118
+ addImageTargetsFromBuffers(buffers) {
119
+ const allTrackingData = [];
120
+ const allMatchingData = [];
121
+ const allDimensions = [];
122
+
123
+ for (const buffer of buffers) {
124
+ const compiler = new Compiler();
125
+ const dataList = compiler.importData(buffer);
126
+
127
+ for (const item of dataList) {
128
+ allMatchingData.push(item.matchingData);
129
+ allTrackingData.push(item.trackingData);
130
+ allDimensions.push([item.targetImage.width, item.targetImage.height]);
131
+ }
115
132
  }
116
133
 
117
134
  this.tracker = new Tracker(
118
- dimensions,
119
- trackingDataList,
135
+ allDimensions,
136
+ allTrackingData,
120
137
  this.projectionTransform,
121
138
  this.inputWidth,
122
139
  this.inputHeight,
@@ -131,12 +148,20 @@ class Controller {
131
148
  inputHeight: this.inputHeight,
132
149
  projectionTransform: this.projectionTransform,
133
150
  debugMode: this.debugMode,
134
- matchingDataList,
151
+ matchingDataList: allMatchingData,
135
152
  });
136
153
  }
137
154
 
138
- this.markerDimensions = dimensions;
139
- return { dimensions, matchingDataList, trackingDataList };
155
+ this.markerDimensions = allDimensions;
156
+ return { dimensions: allDimensions, matchingDataList: allMatchingData, trackingDataList: allTrackingData };
157
+ }
158
+
159
+ /**
160
+ * Load image targets from a single ArrayBuffer (backward compatible)
161
+ * @param {ArrayBuffer} buffer - Single .mind file buffer
162
+ */
163
+ addImageTargetsFromBuffer(buffer) {
164
+ return this.addImageTargetsFromBuffers([buffer]);
140
165
  }
141
166
 
142
167
  dispose() {
@@ -0,0 +1,203 @@
1
+ import { Controller } from "./controller.js";
2
+
3
+ /**
4
+ * 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
5
+ *
6
+ * No Three.js. No A-Frame. Just HTML, CSS, and JavaScript.
7
+ *
8
+ * @example
9
+ * const ar = new SimpleAR({
10
+ * container: document.getElementById('ar-container'),
11
+ * targetSrc: './my-target.mind',
12
+ * overlay: document.getElementById('my-overlay'),
13
+ * onFound: () => console.log('Target found!'),
14
+ * onLost: () => console.log('Target lost!')
15
+ * });
16
+ *
17
+ * await ar.start();
18
+ */
19
+ class SimpleAR {
20
+ constructor({
21
+ container,
22
+ targetSrc,
23
+ overlay,
24
+ onFound = null,
25
+ onLost = null,
26
+ onUpdate = null,
27
+ cameraConfig = { facingMode: 'environment', width: 1280, height: 720 },
28
+ }) {
29
+ this.container = container;
30
+ this.targetSrc = targetSrc;
31
+ this.overlay = overlay;
32
+ this.onFound = onFound;
33
+ this.onLost = onLost;
34
+ this.onUpdateCallback = onUpdate;
35
+ this.cameraConfig = cameraConfig;
36
+
37
+ this.video = null;
38
+ this.controller = null;
39
+ this.isTracking = false;
40
+ this.lastMatrix = null;
41
+ }
42
+
43
+ /**
44
+ * Initialize and start AR tracking
45
+ */
46
+ async start() {
47
+ // 1. Create video element
48
+ this._createVideo();
49
+
50
+ // 2. Start camera
51
+ await this._startCamera();
52
+
53
+ // 3. Initialize controller
54
+ this._initController();
55
+
56
+ // 4. Load targets (supports single URL or array of URLs)
57
+ const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
58
+ await this.controller.addImageTargets(targets);
59
+ this.controller.processVideo(this.video);
60
+
61
+ return this;
62
+ }
63
+
64
+ /**
65
+ * Stop AR tracking and release resources
66
+ */
67
+ stop() {
68
+ if (this.controller) {
69
+ this.controller.dispose();
70
+ this.controller = null;
71
+ }
72
+ if (this.video && this.video.srcObject) {
73
+ this.video.srcObject.getTracks().forEach(track => track.stop());
74
+ this.video.remove();
75
+ this.video = null;
76
+ }
77
+ this.isTracking = false;
78
+ }
79
+
80
+ _createVideo() {
81
+ this.video = document.createElement('video');
82
+ this.video.setAttribute('autoplay', '');
83
+ this.video.setAttribute('playsinline', '');
84
+ this.video.setAttribute('muted', '');
85
+ this.video.style.cssText = `
86
+ position: absolute;
87
+ top: 0;
88
+ left: 0;
89
+ width: 100%;
90
+ height: 100%;
91
+ object-fit: cover;
92
+ z-index: 0;
93
+ `;
94
+ this.container.style.position = 'relative';
95
+ this.container.style.overflow = 'hidden';
96
+ this.container.insertBefore(this.video, this.container.firstChild);
97
+ }
98
+
99
+ async _startCamera() {
100
+ const stream = await navigator.mediaDevices.getUserMedia({
101
+ video: this.cameraConfig
102
+ });
103
+ this.video.srcObject = stream;
104
+ await this.video.play();
105
+
106
+ // Wait for video dimensions to be available
107
+ await new Promise(resolve => {
108
+ if (this.video.videoWidth > 0) return resolve();
109
+ this.video.onloadedmetadata = resolve;
110
+ });
111
+ }
112
+
113
+ _initController() {
114
+ this.controller = new Controller({
115
+ inputWidth: this.video.videoWidth,
116
+ inputHeight: this.video.videoHeight,
117
+ onUpdate: (data) => this._handleUpdate(data)
118
+ });
119
+ }
120
+
121
+ _handleUpdate(data) {
122
+ if (data.type !== 'updateMatrix') return;
123
+
124
+ const { targetIndex, worldMatrix } = data;
125
+
126
+ if (worldMatrix) {
127
+ // Target found
128
+ if (!this.isTracking) {
129
+ this.isTracking = true;
130
+ this.overlay && (this.overlay.style.opacity = '1');
131
+ this.onFound && this.onFound({ targetIndex });
132
+ }
133
+
134
+ this.lastMatrix = worldMatrix;
135
+ this._positionOverlay(worldMatrix);
136
+ this.onUpdateCallback && this.onUpdateCallback({ targetIndex, worldMatrix });
137
+
138
+ } else {
139
+ // Target lost
140
+ if (this.isTracking) {
141
+ this.isTracking = false;
142
+ this.overlay && (this.overlay.style.opacity = '0');
143
+ this.onLost && this.onLost({ targetIndex });
144
+ }
145
+ }
146
+ }
147
+
148
+ _positionOverlay(worldMatrix) {
149
+ if (!this.overlay) return;
150
+
151
+ const containerRect = this.container.getBoundingClientRect();
152
+ const videoW = this.video.videoWidth;
153
+ const videoH = this.video.videoHeight;
154
+
155
+ // Calculate display area considering object-fit: cover
156
+ const containerAspect = containerRect.width / containerRect.height;
157
+ const videoAspect = videoW / videoH;
158
+
159
+ let displayW, displayH, offsetX, offsetY;
160
+
161
+ if (containerAspect > videoAspect) {
162
+ // Container is wider - video fills width, crops height
163
+ displayW = containerRect.width;
164
+ displayH = containerRect.width / videoAspect;
165
+ offsetX = 0;
166
+ offsetY = (containerRect.height - displayH) / 2;
167
+ } else {
168
+ // Container is taller - video fills height, crops width
169
+ displayH = containerRect.height;
170
+ displayW = containerRect.height * videoAspect;
171
+ offsetX = (containerRect.width - displayW) / 2;
172
+ offsetY = 0;
173
+ }
174
+
175
+ const scaleX = displayW / videoW;
176
+ const scaleY = displayH / videoH;
177
+
178
+ // Extract position and rotation from world matrix
179
+ // Matrix is column-major: [m0,m1,m2,m3, m4,m5,m6,m7, m8,m9,m10,m11, m12,m13,m14,m15]
180
+ const tx = worldMatrix[12];
181
+ const ty = worldMatrix[13];
182
+ const scale = Math.sqrt(worldMatrix[0] ** 2 + worldMatrix[1] ** 2);
183
+ const rotation = Math.atan2(worldMatrix[1], worldMatrix[0]);
184
+
185
+ // Convert from normalized coords to screen coords
186
+ const screenX = offsetX + (videoW / 2 + tx) * scaleX;
187
+ const screenY = offsetY + (videoH / 2 - ty) * scaleY;
188
+
189
+ // Apply transform
190
+ this.overlay.style.position = 'absolute';
191
+ this.overlay.style.transformOrigin = 'center center';
192
+ this.overlay.style.left = '0';
193
+ this.overlay.style.top = '0';
194
+ this.overlay.style.transform = `
195
+ translate(${screenX}px, ${screenY}px)
196
+ translate(-50%, -50%)
197
+ scale(${scale * scaleX * 0.01})
198
+ rotate(${-rotation}rad)
199
+ `;
200
+ }
201
+ }
202
+
203
+ export { SimpleAR };
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./react/types";
2
2
  export * from "./compiler/offline-compiler";
3
+ export { Controller } from "./compiler/controller.js";
4
+ export { SimpleAR } from "./compiler/simple-ar.js";