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