@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 +57 -0
- package/dist/compiler/controller.d.ts +24 -2
- package/dist/compiler/controller.js +41 -22
- package/dist/compiler/simple-ar.d.ts +60 -0
- package/dist/compiler/simple-ar.js +173 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +15 -1
- package/src/compiler/controller.js +49 -24
- package/src/compiler/simple-ar.js +203 -0
- package/src/index.ts +2 -0
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
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 =
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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 =
|
|
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