@srsergio/taptapp-ar 1.0.95 → 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.
@@ -1,399 +0,0 @@
1
- import { BioInspiredController } from "./bio-inspired-controller.js";
2
- import { projectToScreen } from "../core/utils/projection.js";
3
-
4
- /**
5
- * 🍦 SimpleAR - High-performance Vanilla AR for image overlays
6
- * Now powered by Bio-Inspired Perception and Nanite Virtualized Features.
7
- */
8
-
9
- export interface SimpleAROptions {
10
- container: HTMLElement;
11
- targetSrc: string | string[];
12
- overlay: HTMLElement;
13
- scale?: number;
14
- onFound?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
15
- onLost?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
16
- onUpdate?: ((data: {
17
- targetIndex: number,
18
- worldMatrix: number[],
19
- screenCoords?: { x: number, y: number }[],
20
- reliabilities?: number[],
21
- stabilities?: number[],
22
- detectionPoints?: { x: number, y: number }[]
23
- }) => void) | null;
24
- cameraConfig?: MediaStreamConstraints['video'];
25
- debug?: boolean;
26
- }
27
-
28
- /**
29
- * 🕵️ Internal Smoothing Manager
30
- * Applies Median + Adaptive Alpha filtering for sub-pixel stability.
31
- */
32
- class SmoothingManager {
33
- private history: Map<number, { x: number, y: number }[]> = new Map();
34
- private lastFiltered: Map<number, { x: number, y: number }> = new Map();
35
- private medianSize = 3;
36
- private deadZone = 0.2;
37
-
38
- smooth(id: number, raw: { x: number, y: number }, reliability: number) {
39
- if (!this.history.has(id)) this.history.set(id, []);
40
- const h = this.history.get(id)!;
41
- h.push(raw);
42
- if (h.length > this.medianSize) h.shift();
43
-
44
- // Get median
45
- const sortedX = [...h].map(p => p.x).sort((a, b) => a - b);
46
- const sortedY = [...h].map(p => p.y).sort((a, b) => a - b);
47
- const median = {
48
- x: sortedX[Math.floor(sortedX.length / 2)],
49
- y: sortedY[Math.floor(sortedY.length / 2)]
50
- };
51
-
52
- // Adaptive Alpha based on reliability
53
- const baseAlpha = 0.15;
54
- const alpha = baseAlpha + (reliability * (1.0 - baseAlpha));
55
- const last = this.lastFiltered.get(id) || median;
56
-
57
- let filteredX = last.x * (1 - alpha) + median.x * alpha;
58
- let filteredY = last.y * (1 - alpha) + median.y * alpha;
59
-
60
- // Dead-zone to kill jitter at rest
61
- if (Math.abs(filteredX - last.x) < this.deadZone) filteredX = last.x;
62
- if (Math.abs(filteredY - last.y) < this.deadZone) filteredY = last.y;
63
-
64
- const result = { x: filteredX, y: filteredY };
65
- this.lastFiltered.set(id, result);
66
- return result;
67
- }
68
-
69
- reset(id?: number) {
70
- if (id !== undefined) {
71
- this.history.delete(id);
72
- this.lastFiltered.delete(id);
73
- } else {
74
- this.history.clear();
75
- this.lastFiltered.clear();
76
- }
77
- }
78
- }
79
-
80
- class SimpleAR {
81
- container: HTMLElement;
82
- targetSrc: string | string[];
83
- overlay: HTMLElement;
84
- scaleMultiplier: number;
85
- onFound: ((data: { targetIndex: number }) => void | Promise<void>) | null;
86
- onLost: ((data: { targetIndex: number }) => void | Promise<void>) | null;
87
- onUpdateCallback: ((data: {
88
- targetIndex: number,
89
- worldMatrix: number[],
90
- screenCoords?: { x: number, y: number }[],
91
- reliabilities?: number[],
92
- stabilities?: number[],
93
- detectionPoints?: { x: number, y: number }[]
94
- }) => void) | null;
95
- cameraConfig: MediaStreamConstraints['video'];
96
- debug: boolean;
97
-
98
- private video: HTMLVideoElement | null = null;
99
- private controller: BioInspiredController | null = null;
100
- private smoother = new SmoothingManager();
101
- private isTracking: boolean = false;
102
- private markerDimensions: number[][] = [];
103
- private debugPanel: HTMLElement | null = null;
104
- private debugCanvas: HTMLCanvasElement | null = null;
105
- private debugCtx: CanvasRenderingContext2D | null = null;
106
-
107
- private lastTime = 0;
108
- private fps = 0;
109
- private frameCount = 0;
110
-
111
- constructor({
112
- container,
113
- targetSrc,
114
- overlay,
115
- scale = 1.0,
116
- onFound = null,
117
- onLost = null,
118
- onUpdate = null,
119
- cameraConfig = { facingMode: 'environment', width: 1280, height: 720 },
120
- debug = false,
121
- }: SimpleAROptions) {
122
- this.container = container;
123
- this.targetSrc = targetSrc;
124
- this.overlay = overlay;
125
- this.scaleMultiplier = scale;
126
- this.onFound = onFound;
127
- this.onLost = onLost;
128
- this.onUpdateCallback = onUpdate;
129
- this.cameraConfig = cameraConfig;
130
- this.debug = debug;
131
- }
132
-
133
- async start() {
134
- this._createVideo();
135
- await this._startCamera();
136
- this._initController();
137
-
138
- if (this.debug) {
139
- this._createDebugPanel();
140
- this._createDebugCanvas();
141
- }
142
-
143
- const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
144
- const result = await this.controller!.addImageTargets(targets);
145
- this.markerDimensions = result.dimensions;
146
-
147
- // Kick off loop
148
- this.controller!.processVideo(this.video!);
149
- return this;
150
- }
151
-
152
- stop() {
153
- if (this.controller) {
154
- this.controller.dispose();
155
- this.controller = null;
156
- }
157
- if (this.video && this.video.srcObject) {
158
- (this.video.srcObject as MediaStream).getTracks().forEach(track => track.stop());
159
- this.video.remove();
160
- this.video = null;
161
- }
162
- this.isTracking = false;
163
- this.smoother.reset();
164
- }
165
-
166
- private _createVideo() {
167
- this.video = document.createElement('video');
168
- this.video.setAttribute('autoplay', '');
169
- this.video.setAttribute('playsinline', '');
170
- this.video.setAttribute('muted', '');
171
- this.video.style.cssText = `
172
- position: absolute;
173
- top: 0; left: 0; width: 100%; height: 100%;
174
- object-fit: cover; z-index: 0;
175
- `;
176
- this.container.style.position = 'relative';
177
- this.container.style.overflow = 'hidden';
178
- this.container.insertBefore(this.video, this.container.firstChild);
179
- }
180
-
181
- private async _startCamera() {
182
- const stream = await navigator.mediaDevices.getUserMedia({ video: this.cameraConfig });
183
- this.video!.srcObject = stream;
184
- await this.video!.play();
185
-
186
- await new Promise<void>(resolve => {
187
- if (this.video!.videoWidth > 0) return resolve();
188
- this.video!.onloadedmetadata = () => resolve();
189
- });
190
- }
191
-
192
- private _initController() {
193
- this.controller = new BioInspiredController({
194
- inputWidth: this.video!.videoWidth,
195
- inputHeight: this.video!.videoHeight,
196
- debugMode: this.debug,
197
- bioInspired: {
198
- enabled: true,
199
- aggressiveSkipping: false
200
- },
201
- onUpdate: (data) => this._handleUpdate(data)
202
- });
203
- }
204
-
205
- private _handleUpdate(data: any) {
206
- if (data.type !== 'updateMatrix') {
207
- if (data.type === 'featurePoints' && this.debugCtx) {
208
- this._drawDebugFeatures(data.featurePoints);
209
- }
210
- return;
211
- }
212
-
213
- // FPS Meter
214
- const now = performance.now();
215
- this.frameCount++;
216
- if (now - this.lastTime >= 1000) {
217
- this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
218
- this.frameCount = 0;
219
- this.lastTime = now;
220
- }
221
-
222
- const { targetIndex, worldMatrix, modelViewTransform, reliabilities, stabilities, screenCoords, pixelsSaved } = data;
223
-
224
- // Apply Smoothing
225
- let smoothedCoords = screenCoords || [];
226
- if (screenCoords && screenCoords.length > 0) {
227
- smoothedCoords = screenCoords.map((p: any) => {
228
- const rel = reliabilities ? (reliabilities[p.id] || 0.5) : 0.5;
229
- const sm = this.smoother.smooth(p.id, p, rel);
230
- return { ...sm, id: p.id };
231
- });
232
- }
233
-
234
- if (worldMatrix) {
235
- if (!this.isTracking) {
236
- this.isTracking = true;
237
- if (this.overlay) this.overlay.style.opacity = '1';
238
- this.onFound && this.onFound({ targetIndex });
239
- }
240
- this._positionOverlay(modelViewTransform, targetIndex);
241
- } else {
242
- if (this.isTracking) {
243
- this.isTracking = false;
244
- if (this.overlay) this.overlay.style.opacity = '0';
245
- this.onLost && this.onLost({ targetIndex });
246
- this.smoother.reset();
247
- }
248
- }
249
-
250
- // Notify callback
251
- if (this.onUpdateCallback) {
252
- this.onUpdateCallback({
253
- targetIndex,
254
- worldMatrix,
255
- screenCoords: smoothedCoords,
256
- reliabilities: reliabilities || [],
257
- stabilities: stabilities || [],
258
- detectionPoints: data.featurePoints
259
- });
260
- }
261
-
262
- // Draw Debug UI
263
- if (this.debug) {
264
- this._updateHUD(data);
265
- this._drawDebugPoints(smoothedCoords, stabilities);
266
- }
267
- }
268
-
269
- private _positionOverlay(mVT: number[][], targetIndex: number) {
270
- if (!this.overlay || !this.markerDimensions[targetIndex]) return;
271
-
272
- const [markerW, markerH] = this.markerDimensions[targetIndex];
273
- const containerRect = this.container.getBoundingClientRect();
274
- const videoW = this.video!.videoWidth;
275
- const videoH = this.video!.videoHeight;
276
- const proj = this.controller!.projectionTransform;
277
-
278
- // Handle portrait rotation for mobile
279
- const isPortrait = containerRect.height > containerRect.width;
280
- const isVideoLandscape = videoW > videoH;
281
- const needsRotation = isPortrait && isVideoLandscape;
282
-
283
- const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
284
- const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
285
- const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
286
- const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
287
-
288
- const matrix = this._solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
289
-
290
- this.overlay.style.maxWidth = 'none';
291
- this.overlay.style.width = `${markerW}px`;
292
- this.overlay.style.height = `${markerH}px`;
293
- this.overlay.style.position = 'absolute';
294
- this.overlay.style.transformOrigin = '0 0';
295
- this.overlay.style.left = '0';
296
- this.overlay.style.top = '0';
297
- this.overlay.style.display = 'block';
298
-
299
- this.overlay.style.transform = `
300
- matrix3d(${matrix.join(',')})
301
- translate(${markerW / 2}px, ${markerH / 2}px)
302
- scale(${this.scaleMultiplier})
303
- translate(${-markerW / 2}px, ${-markerH / 2}px)
304
- `;
305
- }
306
-
307
- private _solveHomography(w: number, h: number, p1: any, p2: any, p3: any, p4: any) {
308
- const x1 = p1.sx, y1 = p1.sy;
309
- const x2 = p2.sx, y2 = p2.sy;
310
- const x3 = p3.sx, y3 = p3.sy;
311
- const x4 = p4.sx, y4 = p4.sy;
312
-
313
- const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
314
- const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
315
-
316
- const det = dx1 * dy2 - dx2 * dy1;
317
- const g = (dx3 * dy2 - dx2 * dy3) / det;
318
- const h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
319
- const a = x2 - x1 + g * x2;
320
- const b = x3 - x1 + h_coeff * x3;
321
- const c = x1;
322
- const d = y2 - y1 + g * y2;
323
- const e = y3 - y1 + h_coeff * y3;
324
- const f = y1;
325
-
326
- return [
327
- a / w, d / w, 0, g / w,
328
- b / h, e / h, 0, h_coeff / h,
329
- 0, 0, 1, 0,
330
- c, f, 0, 1
331
- ];
332
- }
333
-
334
- // --- DEBUG METHODS ---
335
-
336
- private _createDebugPanel() {
337
- this.debugPanel = document.createElement('div');
338
- this.debugPanel.style.cssText = `
339
- position: absolute; top: 10px; left: 10px;
340
- background: rgba(0, 0, 0, 0.7); color: #0f0;
341
- font-family: monospace; font-size: 11px; padding: 10px;
342
- border-radius: 5px; z-index: 100; pointer-events: none;
343
- line-height: 1.4; border-left: 3px solid #0f0;
344
- `;
345
- this.container.appendChild(this.debugPanel);
346
- }
347
-
348
- private _createDebugCanvas() {
349
- this.debugCanvas = document.createElement('canvas');
350
- this.debugCanvas.width = this.container.clientWidth;
351
- this.debugCanvas.height = this.container.clientHeight;
352
- this.debugCanvas.style.cssText = `
353
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
354
- pointer-events: none; z-index: 99;
355
- `;
356
- this.container.appendChild(this.debugCanvas);
357
- this.debugCtx = this.debugCanvas.getContext('2d');
358
- }
359
-
360
- private _updateHUD(data: any) {
361
- if (!this.debugPanel) return;
362
- const rel = data.reliabilities ? (data.reliabilities.reduce((a: any, b: any) => a + b, 0) / data.reliabilities.length).toFixed(2) : "0.00";
363
- const stab = data.stabilities ? (data.stabilities.reduce((a: any, b: any) => a + b, 0) / data.stabilities.length).toFixed(2) : "0.00";
364
- const savings = data.pixelsSaved ? ((data.pixelsSaved / (this.video!.videoWidth * this.video!.videoHeight)) * 100).toFixed(0) : "0";
365
-
366
- this.debugPanel.innerHTML = `
367
- <b>TapTapp AR HUD</b><br>
368
- ------------------<br>
369
- STATUS: <span style="color:${this.isTracking ? '#0f0' : '#f00'}">${this.isTracking ? 'TRACKING' : 'SEARCHING'}</span><br>
370
- FPS: ${this.fps}<br>
371
- RELIAB: ${rel}<br>
372
- STABIL: ${stab}<br>
373
- SAVINGS: ${savings}% Pixels<br>
374
- POINTS: ${data.screenCoords?.length || 0}
375
- `;
376
- }
377
-
378
- private _drawDebugPoints(coords: any[], stabilities: any[]) {
379
- if (!this.debugCtx) return;
380
- this.debugCtx.clearRect(0, 0, this.debugCanvas!.width, this.debugCanvas!.height);
381
-
382
- coords.forEach((p, i) => {
383
- const s = stabilities ? (stabilities[i] || 0) : 0.5;
384
- this.debugCtx!.fillStyle = `rgba(0, 255, 0, ${0.4 + s * 0.6})`;
385
- this.debugCtx!.fillRect(p.x - 1, p.y - 1, 2, 2);
386
- });
387
- }
388
-
389
- private _drawDebugFeatures(points: any[]) {
390
- if (!this.debugCtx || this.isTracking) return;
391
- this.debugCtx.clearRect(0, 0, this.debugCanvas!.width, this.debugCanvas!.height);
392
- this.debugCtx.fillStyle = 'rgba(255, 255, 0, 0.4)';
393
- points.slice(0, 200).forEach(p => {
394
- this.debugCtx!.fillRect(p.x - 1, p.y - 1, 2, 2);
395
- });
396
- }
397
- }
398
-
399
- export { SimpleAR };