@srsergio/taptapp-ar 1.0.11 → 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
@@ -95,27 +95,144 @@ renderer.setAnimationLoop(() => {
95
95
  });
96
96
  ```
97
97
 
98
- ### 3. Raw Controller (Custom Logic)
99
- Use the `Controller` directly for maximum control:
98
+ ### 3. Raw Controller (Advanced & Custom Engines)
99
+ The `Controller` is the core engine of TapTapp AR. You can use it to build your own AR components or integrate tracking into custom 3D engines.
100
+
101
+ #### ⚙️ Controller Configuration
102
+ | Property | Default | Description |
103
+ | :--- | :--- | :--- |
104
+ | `inputWidth` | **Required** | The width of the video or image source. |
105
+ | `inputHeight` | **Required** | The height of the video or image source. |
106
+ | `maxTrack` | `1` | Max number of images to track simultaneously. |
107
+ | `warmupTolerance` | `5` | Frames of consistent detection needed to "lock" a target. |
108
+ | `missTolerance` | `5` | Frames of missed detection before considering the target "lost". |
109
+ | `filterMinCF` | `0.001` | Min cutoff frequency for the OneEuroFilter (reduces jitter). |
110
+ | `filterBeta` | `1000` | Filter beta parameter (higher = more responsive, lower = smoother). |
111
+ | `onUpdate` | `null` | Callback for tracking events (Found, Lost, ProcessDone). |
112
+ | `debugMode` | `false` | If true, returns extra debug data (cropped images, feature points). |
113
+ | `worker` | `null` | Pass a custom worker instance if using a specialized environment. |
114
+
115
+ #### 🚀 Example: Tracking a Video Stream
116
+ Ideal for real-time AR apps in the browser:
100
117
 
101
118
  ```javascript
102
119
  import { Controller } from '@srsergio/taptapp-ar';
103
120
 
104
121
  const controller = new Controller({
105
- inputWidth: 640,
106
- inputHeight: 480,
122
+ inputWidth: video.videoWidth,
123
+ inputHeight: video.videoHeight,
107
124
  onUpdate: (data) => {
108
125
  if (data.type === 'updateMatrix') {
109
- // worldMatrix found! Apply to your 3D engine
110
126
  const { targetIndex, worldMatrix } = data;
127
+ if (worldMatrix) {
128
+ console.log(`Target ${targetIndex} detected! Matrix:`, worldMatrix);
129
+ // Apply worldMatrix (Float32Array[16]) to your 3D object
130
+ } else {
131
+ console.log(`Target ${targetIndex} lost.`);
132
+ }
111
133
  }
112
134
  }
113
135
  });
114
136
 
137
+ // Single target
115
138
  await controller.addImageTargets('./targets.mind');
116
- controller.processVideo(videoElement);
139
+
140
+ // OR multiple targets from different .mind files
141
+ await controller.addImageTargets(['./target1.mind', './target2.mind', './target3.mind']);
142
+ controller.processVideo(videoElement); // Starts the internal RAF loop
143
+ ```
144
+
145
+ #### 📸 Example: One-shot Image Matching
146
+ Use this for "Snap and Detect" features without a continuous video loop:
147
+
148
+ ```javascript
149
+ const controller = new Controller({ inputWidth: 1024, inputHeight: 1024 });
150
+ await controller.addImageTargets('./targets.mind');
151
+
152
+ // 1. Detect features in a static image
153
+ const { featurePoints } = await controller.detect(canvasElement);
154
+
155
+ // 2. Attempt to match against a specific target index
156
+ const { targetIndex, modelViewTransform } = await controller.match(featurePoints, 0);
157
+
158
+ if (targetIndex !== -1) {
159
+ // Found a match! Use modelViewTransform for initial pose estimation
160
+ }
161
+ ```
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();
117
181
  ```
118
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
+
216
+ #### 🛠️ Life-cycle Management
217
+ Properly management is crucial to avoid memory leaks:
218
+
219
+ ```javascript
220
+ // Stop the video loop
221
+ controller.stopProcessVideo();
222
+
223
+ // Clean up workers and internal buffers
224
+ controller.dispose();
225
+ ```
226
+
227
+ ---
228
+
229
+ ## 🏗️ Protocol V3 (Columnar Binary Format)
230
+ TapTapp AR uses a proprietary columnar binary format that is significantly more efficient than standard JSON-based formats.
231
+
232
+ - **Zero-Copy Restoration**: Binary buffers are mapped directly to TypedArrays.
233
+ - **Cache Locality**: Performance is optimized for modern CPUs by keeping coordinates and descriptors adjacent in memory.
234
+ - **Alignment Safe**: Automatically handles `ArrayBuffer` alignment for predictable behavior across all browsers.
235
+
119
236
  ---
120
237
 
121
238
  ## 📄 License & Credits
@@ -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.11",
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";