@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.
@@ -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 };