@srsergio/taptapp-ar 1.0.94 → 1.0.96
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/TaptappAR.js +20 -6
- package/dist/react/types.d.ts +1 -1
- package/dist/react/use-ar.d.ts +2 -1
- package/dist/react/use-ar.js +52 -45
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/track.d.ts +170 -0
- package/dist/runtime/track.js +381 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/react/TaptappAR.tsx +31 -6
- package/src/react/types.ts +1 -1
- package/src/react/use-ar.ts +59 -49
- package/src/runtime/index.ts +2 -1
- package/src/runtime/track.ts +623 -0
- package/dist/runtime/simple-ar.d.ts +0 -82
- package/dist/runtime/simple-ar.js +0 -307
- package/src/runtime/simple-ar.ts +0 -373
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TapTapp AR - Easy Tracking Configuration
|
|
3
|
+
*
|
|
4
|
+
* Simple API for configuring image target tracking with minimal setup.
|
|
5
|
+
* Based on the reliable configuration from reliability-test.html.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createTracker } from 'taptapp-ar';
|
|
10
|
+
*
|
|
11
|
+
* const tracker = await createTracker({
|
|
12
|
+
* targetSrc: './my-target.png',
|
|
13
|
+
* container: document.getElementById('ar-container')!,
|
|
14
|
+
* overlay: document.getElementById('overlay')!,
|
|
15
|
+
* callbacks: {
|
|
16
|
+
* onFound: () => console.log('Target found!'),
|
|
17
|
+
* onLost: () => console.log('Target lost'),
|
|
18
|
+
* onUpdate: (data) => console.log('Update:', data)
|
|
19
|
+
* }
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Start tracking from camera
|
|
23
|
+
* tracker.startCamera();
|
|
24
|
+
*
|
|
25
|
+
* // Or track from a video/canvas element
|
|
26
|
+
* tracker.startVideo(videoElement);
|
|
27
|
+
*
|
|
28
|
+
* // Stop tracking
|
|
29
|
+
* tracker.stop();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { BioInspiredController } from './bio-inspired-controller.js';
|
|
34
|
+
import { OfflineCompiler } from '../compiler/offline-compiler.js';
|
|
35
|
+
import { projectToScreen } from '../core/utils/projection.js';
|
|
36
|
+
import { AR_CONFIG } from '../core/constants.js';
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Types
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tracking update data passed to onUpdate callback
|
|
44
|
+
*/
|
|
45
|
+
export interface TrackingUpdate {
|
|
46
|
+
/** Whether the target is currently being tracked */
|
|
47
|
+
isTracking: boolean;
|
|
48
|
+
|
|
49
|
+
/** 4x4 world transformation matrix (column-major, for WebGL/Three.js) */
|
|
50
|
+
worldMatrix: number[] | null;
|
|
51
|
+
|
|
52
|
+
/** 3x4 model-view transform matrix */
|
|
53
|
+
modelViewTransform: number[][] | null;
|
|
54
|
+
|
|
55
|
+
/** Screen coordinates of tracked feature points */
|
|
56
|
+
screenCoords: Array<{ x: number; y: number; id: number }>;
|
|
57
|
+
|
|
58
|
+
/** Reliability scores (0-1) for each tracked point */
|
|
59
|
+
reliabilities: number[];
|
|
60
|
+
|
|
61
|
+
/** Stability scores (0-1) for each tracked point */
|
|
62
|
+
stabilities: number[];
|
|
63
|
+
|
|
64
|
+
/** Average reliability across all points */
|
|
65
|
+
avgReliability: number;
|
|
66
|
+
|
|
67
|
+
/** Average stability across all points */
|
|
68
|
+
avgStability: number;
|
|
69
|
+
|
|
70
|
+
/** Reference to the controller for advanced usage */
|
|
71
|
+
controller: BioInspiredController;
|
|
72
|
+
|
|
73
|
+
/** Index of the tracked target (for multi-target tracking) */
|
|
74
|
+
targetIndex: number;
|
|
75
|
+
|
|
76
|
+
/** Target dimensions [width, height] */
|
|
77
|
+
targetDimensions: [number, number];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Tracking event callbacks
|
|
82
|
+
*/
|
|
83
|
+
export interface TrackingCallbacks {
|
|
84
|
+
/**
|
|
85
|
+
* Called when the target is first detected
|
|
86
|
+
* @param data Initial tracking data
|
|
87
|
+
*/
|
|
88
|
+
onFound?: (data: TrackingUpdate) => void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Called when tracking is lost
|
|
92
|
+
* @param data Last known tracking data
|
|
93
|
+
*/
|
|
94
|
+
onLost?: (data: TrackingUpdate) => void;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Called on every frame update while tracking
|
|
98
|
+
* @param data Current tracking data
|
|
99
|
+
*/
|
|
100
|
+
onUpdate?: (data: TrackingUpdate) => void;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Called during target compilation
|
|
104
|
+
* @param progress Progress percentage (0-100)
|
|
105
|
+
*/
|
|
106
|
+
onCompileProgress?: (progress: number) => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Configuration options for the tracker
|
|
111
|
+
*/
|
|
112
|
+
export interface TrackerConfig {
|
|
113
|
+
/**
|
|
114
|
+
* Source of the target image to track.
|
|
115
|
+
* Can be a URL string, HTMLImageElement, ImageData, or ArrayBuffer (pre-compiled .taar)
|
|
116
|
+
*/
|
|
117
|
+
targetSrc: string | HTMLImageElement | ImageData | ArrayBuffer;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Container element for the video/canvas display
|
|
121
|
+
*/
|
|
122
|
+
container: HTMLElement;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Optional overlay element to position over the tracked target
|
|
126
|
+
*/
|
|
127
|
+
overlay?: HTMLElement;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Tracking event callbacks
|
|
131
|
+
*/
|
|
132
|
+
callbacks?: TrackingCallbacks;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Camera configuration (MediaStreamConstraints['video'])
|
|
136
|
+
* @default { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 960 } }
|
|
137
|
+
*/
|
|
138
|
+
cameraConfig?: MediaStreamConstraints['video'];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Viewport width for processing
|
|
142
|
+
* @default 1280
|
|
143
|
+
*/
|
|
144
|
+
viewportWidth?: number;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Viewport height for processing
|
|
148
|
+
* @default 960
|
|
149
|
+
*/
|
|
150
|
+
viewportHeight?: number;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Enable debug mode for additional logging
|
|
154
|
+
* @default false
|
|
155
|
+
*/
|
|
156
|
+
debugMode?: boolean;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Enable bio-inspired perception optimizations
|
|
160
|
+
* @default true
|
|
161
|
+
*/
|
|
162
|
+
bioInspiredEnabled?: boolean;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Scale multiplier for the overlay
|
|
166
|
+
* @default 1.0
|
|
167
|
+
*/
|
|
168
|
+
scale?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Tracker instance returned by createTracker
|
|
173
|
+
*/
|
|
174
|
+
export interface Tracker {
|
|
175
|
+
/** Start tracking from device camera */
|
|
176
|
+
startCamera(): Promise<void>;
|
|
177
|
+
|
|
178
|
+
/** Start tracking from a video or canvas element */
|
|
179
|
+
startVideo(source: HTMLVideoElement | HTMLCanvasElement): void;
|
|
180
|
+
|
|
181
|
+
/** Stop tracking and release resources */
|
|
182
|
+
stop(): void;
|
|
183
|
+
|
|
184
|
+
/** Whether the tracker is currently active */
|
|
185
|
+
readonly isActive: boolean;
|
|
186
|
+
|
|
187
|
+
/** Whether a target is currently being tracked */
|
|
188
|
+
readonly isTracking: boolean;
|
|
189
|
+
|
|
190
|
+
/** The underlying BioInspiredController instance */
|
|
191
|
+
readonly controller: BioInspiredController;
|
|
192
|
+
|
|
193
|
+
/** Target dimensions [width, height] */
|
|
194
|
+
readonly targetDimensions: [number, number];
|
|
195
|
+
|
|
196
|
+
/** Get the projection matrix for 3D rendering */
|
|
197
|
+
getProjectionMatrix(): number[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Implementation
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Load an image from a URL
|
|
206
|
+
*/
|
|
207
|
+
async function loadImage(url: string): Promise<HTMLImageElement> {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const img = new Image();
|
|
210
|
+
img.crossOrigin = 'anonymous';
|
|
211
|
+
img.onload = () => resolve(img);
|
|
212
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
|
|
213
|
+
img.src = url;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get ImageData from various source types
|
|
219
|
+
*/
|
|
220
|
+
async function getImageData(
|
|
221
|
+
source: string | HTMLImageElement | ImageData
|
|
222
|
+
): Promise<{ imageData: ImageData; width: number; height: number }> {
|
|
223
|
+
let img: HTMLImageElement;
|
|
224
|
+
|
|
225
|
+
if (typeof source === 'string') {
|
|
226
|
+
img = await loadImage(source);
|
|
227
|
+
} else if (source instanceof HTMLImageElement) {
|
|
228
|
+
img = source;
|
|
229
|
+
if (!img.complete) {
|
|
230
|
+
await new Promise((resolve, reject) => {
|
|
231
|
+
img.onload = resolve;
|
|
232
|
+
img.onerror = reject;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// Already ImageData
|
|
237
|
+
return { imageData: source, width: source.width, height: source.height };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const canvas = document.createElement('canvas');
|
|
241
|
+
canvas.width = img.width;
|
|
242
|
+
canvas.height = img.height;
|
|
243
|
+
const ctx = canvas.getContext('2d')!;
|
|
244
|
+
ctx.drawImage(img, 0, 0);
|
|
245
|
+
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
|
246
|
+
|
|
247
|
+
return { imageData, width: img.width, height: img.height };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Solve homography for overlay positioning (from reliability-test.html)
|
|
252
|
+
*/
|
|
253
|
+
function solveHomography(
|
|
254
|
+
w: number, h: number,
|
|
255
|
+
p1: { sx: number; sy: number },
|
|
256
|
+
p2: { sx: number; sy: number },
|
|
257
|
+
p3: { sx: number; sy: number },
|
|
258
|
+
p4: { sx: number; sy: number }
|
|
259
|
+
): number[] {
|
|
260
|
+
const x1 = p1.sx, y1 = p1.sy;
|
|
261
|
+
const x2 = p2.sx, y2 = p2.sy;
|
|
262
|
+
const x3 = p3.sx, y3 = p3.sy;
|
|
263
|
+
const x4 = p4.sx, y4 = p4.sy;
|
|
264
|
+
|
|
265
|
+
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
266
|
+
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
267
|
+
|
|
268
|
+
const det = dx1 * dy2 - dx2 * dy1;
|
|
269
|
+
const g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
270
|
+
const h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
271
|
+
const a = x2 - x1 + g * x2;
|
|
272
|
+
const b = x3 - x1 + h_coeff * x3;
|
|
273
|
+
const c = x1;
|
|
274
|
+
const d = y2 - y1 + g * y2;
|
|
275
|
+
const e = y3 - y1 + h_coeff * y3;
|
|
276
|
+
const f = y1;
|
|
277
|
+
|
|
278
|
+
return [
|
|
279
|
+
a / w, d / w, 0, g / w,
|
|
280
|
+
b / h, e / h, 0, h_coeff / h,
|
|
281
|
+
0, 0, 1, 0,
|
|
282
|
+
c, f, 0, 1
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create and configure an AR tracker with minimal setup
|
|
288
|
+
*/
|
|
289
|
+
export async function createTracker(config: TrackerConfig): Promise<Tracker> {
|
|
290
|
+
const {
|
|
291
|
+
targetSrc,
|
|
292
|
+
container,
|
|
293
|
+
overlay,
|
|
294
|
+
callbacks = {},
|
|
295
|
+
cameraConfig = {
|
|
296
|
+
facingMode: 'environment',
|
|
297
|
+
width: { ideal: 1280 },
|
|
298
|
+
height: { ideal: 960 }
|
|
299
|
+
},
|
|
300
|
+
viewportWidth = 1280,
|
|
301
|
+
viewportHeight = 960,
|
|
302
|
+
debugMode = false,
|
|
303
|
+
bioInspiredEnabled = true,
|
|
304
|
+
scale = 1.0
|
|
305
|
+
} = config;
|
|
306
|
+
|
|
307
|
+
// State
|
|
308
|
+
let isActive = false;
|
|
309
|
+
let wasTracking = false;
|
|
310
|
+
let mediaStream: MediaStream | null = null;
|
|
311
|
+
let videoElement: HTMLVideoElement | null = null;
|
|
312
|
+
let targetDimensions: [number, number] = [0, 0];
|
|
313
|
+
|
|
314
|
+
// Create video canvas for camera input
|
|
315
|
+
const videoCanvas = document.createElement('canvas');
|
|
316
|
+
videoCanvas.width = viewportWidth;
|
|
317
|
+
videoCanvas.height = viewportHeight;
|
|
318
|
+
videoCanvas.style.width = '100%';
|
|
319
|
+
videoCanvas.style.height = '100%';
|
|
320
|
+
videoCanvas.style.objectFit = 'cover';
|
|
321
|
+
videoCanvas.style.position = 'absolute';
|
|
322
|
+
videoCanvas.style.top = '0';
|
|
323
|
+
videoCanvas.style.left = '0';
|
|
324
|
+
videoCanvas.style.zIndex = '0';
|
|
325
|
+
const videoCtx = videoCanvas.getContext('2d')!;
|
|
326
|
+
|
|
327
|
+
// Setup overlay styles if provided
|
|
328
|
+
if (overlay) {
|
|
329
|
+
overlay.style.position = 'absolute';
|
|
330
|
+
overlay.style.transformOrigin = '0 0';
|
|
331
|
+
overlay.style.display = 'none';
|
|
332
|
+
overlay.style.pointerEvents = 'none';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Compile target or load pre-compiled data
|
|
336
|
+
let compiledBuffer: ArrayBuffer;
|
|
337
|
+
|
|
338
|
+
if (targetSrc instanceof ArrayBuffer) {
|
|
339
|
+
compiledBuffer = targetSrc;
|
|
340
|
+
} else if (typeof targetSrc === 'string' && targetSrc.toLowerCase().split('?')[0].endsWith('.taar')) {
|
|
341
|
+
// Pre-compiled .taar file URL
|
|
342
|
+
if (debugMode) console.log(`[TapTapp AR] Fetching pre-compiled target: ${targetSrc}`);
|
|
343
|
+
const response = await fetch(targetSrc);
|
|
344
|
+
if (!response.ok) throw new Error(`Failed to fetch .taar file: ${response.statusText}`);
|
|
345
|
+
compiledBuffer = await response.arrayBuffer();
|
|
346
|
+
} else {
|
|
347
|
+
// Source is an image or ImageData that needs compilation
|
|
348
|
+
if (debugMode) console.log('[TapTapp AR] Compiling image target...');
|
|
349
|
+
const { imageData, width, height } = await getImageData(targetSrc as any);
|
|
350
|
+
targetDimensions = [width, height];
|
|
351
|
+
|
|
352
|
+
const compiler = new OfflineCompiler();
|
|
353
|
+
await compiler.compileImageTargets(
|
|
354
|
+
[{ width, height, data: imageData.data }],
|
|
355
|
+
(progress) => callbacks.onCompileProgress?.(progress)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const exported = compiler.exportData();
|
|
359
|
+
compiledBuffer = exported.buffer.slice(
|
|
360
|
+
exported.byteOffset,
|
|
361
|
+
exported.byteOffset + exported.byteLength
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Create controller with bio-inspired perception
|
|
366
|
+
const controller = new BioInspiredController({
|
|
367
|
+
inputWidth: viewportWidth,
|
|
368
|
+
inputHeight: viewportHeight,
|
|
369
|
+
debugMode,
|
|
370
|
+
bioInspired: {
|
|
371
|
+
enabled: bioInspiredEnabled,
|
|
372
|
+
aggressiveSkipping: false // Keep stable for real-world conditions
|
|
373
|
+
},
|
|
374
|
+
onUpdate: (data) => handleControllerUpdate(data)
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Load compiled targets
|
|
378
|
+
const loadResult = await controller.addImageTargetsFromBuffer(compiledBuffer);
|
|
379
|
+
if (loadResult.dimensions && loadResult.dimensions[0]) {
|
|
380
|
+
targetDimensions = loadResult.dimensions[0] as [number, number];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Handle controller updates and dispatch to user callbacks
|
|
385
|
+
*/
|
|
386
|
+
function handleControllerUpdate(data: any) {
|
|
387
|
+
if (data.type === 'processDone') return;
|
|
388
|
+
if (data.type !== 'updateMatrix') return;
|
|
389
|
+
|
|
390
|
+
const {
|
|
391
|
+
targetIndex,
|
|
392
|
+
worldMatrix,
|
|
393
|
+
modelViewTransform,
|
|
394
|
+
screenCoords = [],
|
|
395
|
+
reliabilities = [],
|
|
396
|
+
stabilities = []
|
|
397
|
+
} = data;
|
|
398
|
+
|
|
399
|
+
const isTracking = worldMatrix !== null;
|
|
400
|
+
|
|
401
|
+
// Calculate averages
|
|
402
|
+
const avgReliability = reliabilities.length > 0
|
|
403
|
+
? reliabilities.reduce((a: number, b: number) => a + b, 0) / reliabilities.length
|
|
404
|
+
: 0;
|
|
405
|
+
const avgStability = stabilities.length > 0
|
|
406
|
+
? stabilities.reduce((a: number, b: number) => a + b, 0) / stabilities.length
|
|
407
|
+
: 0;
|
|
408
|
+
|
|
409
|
+
const updateData: TrackingUpdate = {
|
|
410
|
+
isTracking,
|
|
411
|
+
worldMatrix,
|
|
412
|
+
modelViewTransform,
|
|
413
|
+
screenCoords,
|
|
414
|
+
reliabilities,
|
|
415
|
+
stabilities,
|
|
416
|
+
avgReliability,
|
|
417
|
+
avgStability,
|
|
418
|
+
controller,
|
|
419
|
+
targetIndex,
|
|
420
|
+
targetDimensions
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Dispatch state change callbacks
|
|
424
|
+
if (isTracking && !wasTracking) {
|
|
425
|
+
callbacks.onFound?.(updateData);
|
|
426
|
+
} else if (!isTracking && wasTracking) {
|
|
427
|
+
callbacks.onLost?.(updateData);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Always call onUpdate when tracking
|
|
431
|
+
if (isTracking || wasTracking) {
|
|
432
|
+
callbacks.onUpdate?.(updateData);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Update overlay position if provided
|
|
436
|
+
if (overlay && modelViewTransform && worldMatrix) {
|
|
437
|
+
positionOverlay(modelViewTransform);
|
|
438
|
+
} else if (overlay && !isTracking) {
|
|
439
|
+
overlay.style.display = 'none';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
wasTracking = isTracking;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Position the overlay element using homography transform
|
|
447
|
+
*/
|
|
448
|
+
function positionOverlay(modelViewTransform: number[][]) {
|
|
449
|
+
if (!overlay) return;
|
|
450
|
+
|
|
451
|
+
const [markerW, markerH] = targetDimensions;
|
|
452
|
+
const proj = controller.projectionTransform;
|
|
453
|
+
const containerRect = container.getBoundingClientRect();
|
|
454
|
+
|
|
455
|
+
// Get corners in screen space
|
|
456
|
+
const pUL = projectToScreen(0, 0, 0, modelViewTransform, proj, viewportWidth, viewportHeight, containerRect, false);
|
|
457
|
+
const pUR = projectToScreen(markerW, 0, 0, modelViewTransform, proj, viewportWidth, viewportHeight, containerRect, false);
|
|
458
|
+
const pLL = projectToScreen(0, markerH, 0, modelViewTransform, proj, viewportWidth, viewportHeight, containerRect, false);
|
|
459
|
+
const pLR = projectToScreen(markerW, markerH, 0, modelViewTransform, proj, viewportWidth, viewportHeight, containerRect, false);
|
|
460
|
+
|
|
461
|
+
const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
462
|
+
|
|
463
|
+
overlay.style.width = `${markerW}px`;
|
|
464
|
+
overlay.style.height = `${markerH}px`;
|
|
465
|
+
|
|
466
|
+
// Apply custom scale if provided
|
|
467
|
+
let matrixString = matrix.join(',');
|
|
468
|
+
if (scale !== 1.0) {
|
|
469
|
+
overlay.style.transform = `matrix3d(${matrixString}) scale(${scale})`;
|
|
470
|
+
} else {
|
|
471
|
+
overlay.style.transform = `matrix3d(${matrixString})`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
overlay.style.display = 'block';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Draw video frame to canvas
|
|
479
|
+
*/
|
|
480
|
+
function drawVideoToCanvas(source: HTMLVideoElement | HTMLCanvasElement) {
|
|
481
|
+
if (source instanceof HTMLVideoElement) {
|
|
482
|
+
videoCtx.drawImage(source, 0, 0, viewportWidth, viewportHeight);
|
|
483
|
+
} else {
|
|
484
|
+
videoCtx.drawImage(source, 0, 0, viewportWidth, viewportHeight);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ========================================================================
|
|
489
|
+
// Public API
|
|
490
|
+
// ========================================================================
|
|
491
|
+
|
|
492
|
+
const tracker: Tracker = {
|
|
493
|
+
async startCamera() {
|
|
494
|
+
if (isActive) return;
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
// Try environment mode first (mobile back camera)
|
|
498
|
+
try {
|
|
499
|
+
mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
500
|
+
video: cameraConfig,
|
|
501
|
+
audio: false
|
|
502
|
+
});
|
|
503
|
+
} catch (e) {
|
|
504
|
+
console.warn('[TapTapp AR] Failed to open environment camera, falling back to default:', e);
|
|
505
|
+
// Fallback to any camera
|
|
506
|
+
mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
507
|
+
video: true,
|
|
508
|
+
audio: false
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
videoElement = document.createElement('video');
|
|
513
|
+
videoElement.srcObject = mediaStream;
|
|
514
|
+
videoElement.playsInline = true;
|
|
515
|
+
videoElement.muted = true;
|
|
516
|
+
|
|
517
|
+
await videoElement.play();
|
|
518
|
+
|
|
519
|
+
// Add video canvas to container (at the beginning to be behind)
|
|
520
|
+
container.style.position = 'relative';
|
|
521
|
+
if (container.firstChild) {
|
|
522
|
+
container.insertBefore(videoCanvas, container.firstChild);
|
|
523
|
+
} else {
|
|
524
|
+
container.appendChild(videoCanvas);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
isActive = true;
|
|
528
|
+
|
|
529
|
+
// Start processing loop
|
|
530
|
+
const processLoop = () => {
|
|
531
|
+
if (!isActive || !videoElement) return;
|
|
532
|
+
|
|
533
|
+
drawVideoToCanvas(videoElement);
|
|
534
|
+
requestAnimationFrame(processLoop);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
processLoop();
|
|
538
|
+
controller.processVideo(videoCanvas);
|
|
539
|
+
|
|
540
|
+
} catch (error) {
|
|
541
|
+
console.error('[TapTapp AR] Camera access failed:', error);
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
startVideo(source: HTMLVideoElement | HTMLCanvasElement) {
|
|
547
|
+
if (isActive) return;
|
|
548
|
+
|
|
549
|
+
container.style.position = 'relative';
|
|
550
|
+
container.appendChild(videoCanvas);
|
|
551
|
+
|
|
552
|
+
isActive = true;
|
|
553
|
+
|
|
554
|
+
// Start processing loop
|
|
555
|
+
const processLoop = () => {
|
|
556
|
+
if (!isActive) return;
|
|
557
|
+
|
|
558
|
+
drawVideoToCanvas(source);
|
|
559
|
+
requestAnimationFrame(processLoop);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
processLoop();
|
|
563
|
+
controller.processVideo(videoCanvas);
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
stop() {
|
|
567
|
+
isActive = false;
|
|
568
|
+
controller.stopProcessVideo();
|
|
569
|
+
|
|
570
|
+
if (mediaStream) {
|
|
571
|
+
mediaStream.getTracks().forEach(track => track.stop());
|
|
572
|
+
mediaStream = null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (videoElement) {
|
|
576
|
+
videoElement.srcObject = null;
|
|
577
|
+
videoElement = null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (videoCanvas.parentNode) {
|
|
581
|
+
videoCanvas.parentNode.removeChild(videoCanvas);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (overlay) {
|
|
585
|
+
overlay.style.display = 'none';
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
get isActive() {
|
|
590
|
+
return isActive;
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
get isTracking() {
|
|
594
|
+
return wasTracking;
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
get controller() {
|
|
598
|
+
return controller;
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
get targetDimensions() {
|
|
602
|
+
return targetDimensions;
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
getProjectionMatrix() {
|
|
606
|
+
return controller.getProjectionMatrix();
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
return tracker;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Convenience function to create a tracker with camera autostart
|
|
615
|
+
*/
|
|
616
|
+
export async function startTracking(config: TrackerConfig): Promise<Tracker> {
|
|
617
|
+
const tracker = await createTracker(config);
|
|
618
|
+
await tracker.startCamera();
|
|
619
|
+
return tracker;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Default export for easy importing
|
|
623
|
+
export default createTracker;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { Controller } from "./controller.js";
|
|
2
|
-
import { OneEuroFilter } from "../libs/one-euro-filter.js";
|
|
3
|
-
/**
|
|
4
|
-
* 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
|
|
5
|
-
*/
|
|
6
|
-
export interface SimpleAROptions {
|
|
7
|
-
container: HTMLElement;
|
|
8
|
-
targetSrc: string | string[];
|
|
9
|
-
overlay: HTMLElement;
|
|
10
|
-
scale?: number;
|
|
11
|
-
onFound?: ((data: {
|
|
12
|
-
targetIndex: number;
|
|
13
|
-
}) => void | Promise<void>) | null;
|
|
14
|
-
onLost?: ((data: {
|
|
15
|
-
targetIndex: number;
|
|
16
|
-
}) => void | Promise<void>) | null;
|
|
17
|
-
onUpdate?: ((data: {
|
|
18
|
-
targetIndex: number;
|
|
19
|
-
worldMatrix: number[];
|
|
20
|
-
screenCoords?: {
|
|
21
|
-
x: number;
|
|
22
|
-
y: number;
|
|
23
|
-
}[];
|
|
24
|
-
reliabilities?: number[];
|
|
25
|
-
stabilities?: number[];
|
|
26
|
-
detectionPoints?: {
|
|
27
|
-
x: number;
|
|
28
|
-
y: number;
|
|
29
|
-
}[];
|
|
30
|
-
}) => void) | null;
|
|
31
|
-
cameraConfig?: MediaStreamConstraints['video'];
|
|
32
|
-
debug?: boolean;
|
|
33
|
-
}
|
|
34
|
-
declare class SimpleAR {
|
|
35
|
-
container: HTMLElement;
|
|
36
|
-
targetSrc: string | string[];
|
|
37
|
-
overlay: HTMLElement;
|
|
38
|
-
scaleMultiplier: number;
|
|
39
|
-
onFound: ((data: {
|
|
40
|
-
targetIndex: number;
|
|
41
|
-
}) => void | Promise<void>) | null;
|
|
42
|
-
onLost: ((data: {
|
|
43
|
-
targetIndex: number;
|
|
44
|
-
}) => void | Promise<void>) | null;
|
|
45
|
-
onUpdateCallback: ((data: {
|
|
46
|
-
targetIndex: number;
|
|
47
|
-
worldMatrix: number[];
|
|
48
|
-
screenCoords?: {
|
|
49
|
-
x: number;
|
|
50
|
-
y: number;
|
|
51
|
-
}[];
|
|
52
|
-
reliabilities?: number[];
|
|
53
|
-
stabilities?: number[];
|
|
54
|
-
detectionPoints?: {
|
|
55
|
-
x: number;
|
|
56
|
-
y: number;
|
|
57
|
-
}[];
|
|
58
|
-
}) => void) | null;
|
|
59
|
-
cameraConfig: MediaStreamConstraints['video'];
|
|
60
|
-
debug: boolean;
|
|
61
|
-
lastTime: number;
|
|
62
|
-
frameCount: number;
|
|
63
|
-
fps: number;
|
|
64
|
-
debugPanel: HTMLElement | null;
|
|
65
|
-
video: HTMLVideoElement | null;
|
|
66
|
-
controller: Controller | null;
|
|
67
|
-
isTracking: boolean;
|
|
68
|
-
lastMatrix: number[] | null;
|
|
69
|
-
filters: OneEuroFilter[];
|
|
70
|
-
markerDimensions: number[][];
|
|
71
|
-
constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, debug, }: SimpleAROptions);
|
|
72
|
-
start(): Promise<this>;
|
|
73
|
-
stop(): void;
|
|
74
|
-
_createVideo(): void;
|
|
75
|
-
_startCamera(): Promise<void>;
|
|
76
|
-
_initController(): void;
|
|
77
|
-
_handleUpdate(data: any): void;
|
|
78
|
-
_positionOverlay(mVT: number[][], targetIndex: number): void;
|
|
79
|
-
_createDebugPanel(): void;
|
|
80
|
-
_updateDebugPanel(isTracking: boolean): void;
|
|
81
|
-
}
|
|
82
|
-
export { SimpleAR };
|