@srsergio/taptapp-ar 1.0.94 → 1.0.95

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,7 +1,6 @@
1
- import { Controller } from "./controller.js";
2
- import { OneEuroFilter } from "../libs/one-euro-filter.js";
3
1
  /**
4
- * 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
2
+ * 🍦 SimpleAR - High-performance Vanilla AR for image overlays
3
+ * Now powered by Bio-Inspired Perception and Nanite Virtualized Features.
5
4
  */
6
5
  export interface SimpleAROptions {
7
6
  container: HTMLElement;
@@ -58,25 +57,30 @@ declare class SimpleAR {
58
57
  }) => void) | null;
59
58
  cameraConfig: MediaStreamConstraints['video'];
60
59
  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[][];
60
+ private video;
61
+ private controller;
62
+ private smoother;
63
+ private isTracking;
64
+ private markerDimensions;
65
+ private debugPanel;
66
+ private debugCanvas;
67
+ private debugCtx;
68
+ private lastTime;
69
+ private fps;
70
+ private frameCount;
71
71
  constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, debug, }: SimpleAROptions);
72
72
  start(): Promise<this>;
73
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;
74
+ private _createVideo;
75
+ private _startCamera;
76
+ private _initController;
77
+ private _handleUpdate;
78
+ private _positionOverlay;
79
+ private _solveHomography;
80
+ private _createDebugPanel;
81
+ private _createDebugCanvas;
82
+ private _updateHUD;
83
+ private _drawDebugPoints;
84
+ private _drawDebugFeatures;
81
85
  }
82
86
  export { SimpleAR };
@@ -1,5 +1,54 @@
1
- import { Controller } from "./controller.js";
1
+ import { BioInspiredController } from "./bio-inspired-controller.js";
2
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
+ }
3
52
  class SimpleAR {
4
53
  container;
5
54
  targetSrc;
@@ -10,16 +59,17 @@ class SimpleAR {
10
59
  onUpdateCallback;
11
60
  cameraConfig;
12
61
  debug;
13
- lastTime;
14
- frameCount;
15
- fps;
16
- debugPanel = null;
17
62
  video = null;
18
63
  controller = null;
64
+ smoother = new SmoothingManager();
19
65
  isTracking = false;
20
- lastMatrix = null;
21
- filters = [];
22
66
  markerDimensions = [];
67
+ debugPanel = null;
68
+ debugCanvas = null;
69
+ debugCtx = null;
70
+ lastTime = 0;
71
+ fps = 0;
72
+ frameCount = 0;
23
73
  constructor({ container, targetSrc, overlay, scale = 1.0, onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, debug = false, }) {
24
74
  this.container = container;
25
75
  this.targetSrc = targetSrc;
@@ -30,23 +80,19 @@ class SimpleAR {
30
80
  this.onUpdateCallback = onUpdate;
31
81
  this.cameraConfig = cameraConfig;
32
82
  this.debug = debug;
33
- // @ts-ignore
34
- if (this.debug)
35
- window.AR_DEBUG = true;
36
- this.lastTime = performance.now();
37
- this.frameCount = 0;
38
- this.fps = 0;
39
83
  }
40
84
  async start() {
41
85
  this._createVideo();
42
86
  await this._startCamera();
43
87
  this._initController();
44
- if (this.debug)
88
+ if (this.debug) {
45
89
  this._createDebugPanel();
90
+ this._createDebugCanvas();
91
+ }
46
92
  const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
47
93
  const result = await this.controller.addImageTargets(targets);
48
94
  this.markerDimensions = result.dimensions;
49
- console.log("Targets loaded. Dimensions:", this.markerDimensions);
95
+ // Kick off loop
50
96
  this.controller.processVideo(this.video);
51
97
  return this;
52
98
  }
@@ -61,7 +107,7 @@ class SimpleAR {
61
107
  this.video = null;
62
108
  }
63
109
  this.isTracking = false;
64
- this.markerDimensions = [];
110
+ this.smoother.reset();
65
111
  }
66
112
  _createVideo() {
67
113
  this.video = document.createElement('video');
@@ -70,21 +116,15 @@ class SimpleAR {
70
116
  this.video.setAttribute('muted', '');
71
117
  this.video.style.cssText = `
72
118
  position: absolute;
73
- top: 0;
74
- left: 0;
75
- width: 100%;
76
- height: 100%;
77
- object-fit: cover;
78
- z-index: 0;
119
+ top: 0; left: 0; width: 100%; height: 100%;
120
+ object-fit: cover; z-index: 0;
79
121
  `;
80
122
  this.container.style.position = 'relative';
81
123
  this.container.style.overflow = 'hidden';
82
124
  this.container.insertBefore(this.video, this.container.firstChild);
83
125
  }
84
126
  async _startCamera() {
85
- const stream = await navigator.mediaDevices.getUserMedia({
86
- video: this.cameraConfig
87
- });
127
+ const stream = await navigator.mediaDevices.getUserMedia({ video: this.cameraConfig });
88
128
  this.video.srcObject = stream;
89
129
  await this.video.play();
90
130
  await new Promise(resolve => {
@@ -94,114 +134,76 @@ class SimpleAR {
94
134
  });
95
135
  }
96
136
  _initController() {
97
- this.controller = new Controller({
137
+ this.controller = new BioInspiredController({
98
138
  inputWidth: this.video.videoWidth,
99
139
  inputHeight: this.video.videoHeight,
100
140
  debugMode: this.debug,
101
- warmupTolerance: 3, // 🚀 Faster lock than default
102
- missTolerance: 10, // 🛡️ More resilient to temporary occlusion
141
+ bioInspired: {
142
+ enabled: true,
143
+ aggressiveSkipping: false
144
+ },
103
145
  onUpdate: (data) => this._handleUpdate(data)
104
146
  });
105
147
  }
106
148
  _handleUpdate(data) {
107
- if (data.type !== 'updateMatrix')
149
+ if (data.type !== 'updateMatrix') {
150
+ if (data.type === 'featurePoints' && this.debugCtx) {
151
+ this._drawDebugFeatures(data.featurePoints);
152
+ }
108
153
  return;
154
+ }
155
+ // FPS Meter
109
156
  const now = performance.now();
110
157
  this.frameCount++;
111
158
  if (now - this.lastTime >= 1000) {
112
159
  this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
113
160
  this.frameCount = 0;
114
161
  this.lastTime = now;
115
- if (this.debug)
116
- this._updateDebugPanel(this.isTracking);
117
162
  }
118
- const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities, detectionPoints } = data;
119
- // Project points to screen coordinates
120
- let projectedPoints = [];
163
+ const { targetIndex, worldMatrix, modelViewTransform, reliabilities, stabilities, screenCoords, pixelsSaved } = data;
164
+ // Apply Smoothing
165
+ let smoothedCoords = screenCoords || [];
121
166
  if (screenCoords && screenCoords.length > 0) {
122
- const containerRect = this.container.getBoundingClientRect();
123
- const videoW = this.video.videoWidth;
124
- const videoH = this.video.videoHeight;
125
- const isPortrait = containerRect.height > containerRect.width;
126
- const isVideoLandscape = videoW > videoH;
127
- const needsRotation = isPortrait && isVideoLandscape;
128
- const proj = this.controller.projectionTransform;
129
- const vW = needsRotation ? videoH : videoW;
130
- const vH = needsRotation ? videoW : videoH;
131
- const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
132
- const dW = vW * pScale;
133
- const dH = vH * pScale;
134
- const oX = (containerRect.width - dW) / 2;
135
- const oY = (containerRect.height - dH) / 2;
136
- projectedPoints = screenCoords.map((p) => {
137
- let sx, sy;
138
- if (needsRotation) {
139
- sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
140
- sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
141
- }
142
- else {
143
- sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
144
- sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
145
- }
146
- return { x: sx, y: sy };
147
- });
148
- }
149
- let projectedDetectionPoints = [];
150
- if (detectionPoints && detectionPoints.length > 0) {
151
- const containerRect = this.container.getBoundingClientRect();
152
- const videoW = this.video.videoWidth;
153
- const videoH = this.video.videoHeight;
154
- const isPortrait = containerRect.height > containerRect.width;
155
- const isVideoLandscape = videoW > videoH;
156
- const needsRotation = isPortrait && isVideoLandscape;
157
- const proj = this.controller.projectionTransform;
158
- const vW = needsRotation ? videoH : videoW;
159
- const vH = needsRotation ? videoW : videoH;
160
- const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
161
- const dW = vW * pScale;
162
- const dH = vH * pScale;
163
- const oX = (containerRect.width - dW) / 2;
164
- const oY = (containerRect.height - dH) / 2;
165
- projectedDetectionPoints = detectionPoints.map((p) => {
166
- let sx, sy;
167
- if (needsRotation) {
168
- sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
169
- sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
170
- }
171
- else {
172
- sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
173
- sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
174
- }
175
- return { x: sx, y: sy };
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 };
176
171
  });
177
172
  }
178
173
  if (worldMatrix) {
179
174
  if (!this.isTracking) {
180
175
  this.isTracking = true;
181
- this.overlay && (this.overlay.style.opacity = '1');
176
+ if (this.overlay)
177
+ this.overlay.style.opacity = '1';
182
178
  this.onFound && this.onFound({ targetIndex });
183
179
  }
184
- this.lastMatrix = worldMatrix;
185
180
  this._positionOverlay(modelViewTransform, targetIndex);
186
181
  }
187
182
  else {
188
183
  if (this.isTracking) {
189
184
  this.isTracking = false;
190
- this.overlay && (this.overlay.style.opacity = '0');
185
+ if (this.overlay)
186
+ this.overlay.style.opacity = '0';
191
187
  this.onLost && this.onLost({ targetIndex });
188
+ this.smoother.reset();
192
189
  }
193
190
  }
194
- // Always notify the callback if we have points, or if we just lost tracking
195
- if (projectedPoints.length > 0 || projectedDetectionPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
196
- this.onUpdateCallback && this.onUpdateCallback({
191
+ // Notify callback
192
+ if (this.onUpdateCallback) {
193
+ this.onUpdateCallback({
197
194
  targetIndex,
198
195
  worldMatrix,
199
- screenCoords: projectedPoints,
196
+ screenCoords: smoothedCoords,
200
197
  reliabilities: reliabilities || [],
201
198
  stabilities: stabilities || [],
202
- detectionPoints: projectedDetectionPoints
199
+ detectionPoints: data.featurePoints
203
200
  });
204
201
  }
202
+ // Draw Debug UI
203
+ if (this.debug) {
204
+ this._updateHUD(data);
205
+ this._drawDebugPoints(smoothedCoords, stabilities);
206
+ }
205
207
  }
206
208
  _positionOverlay(mVT, targetIndex) {
207
209
  if (!this.overlay || !this.markerDimensions[targetIndex])
@@ -210,51 +212,16 @@ class SimpleAR {
210
212
  const containerRect = this.container.getBoundingClientRect();
211
213
  const videoW = this.video.videoWidth;
212
214
  const videoH = this.video.videoHeight;
215
+ const proj = this.controller.projectionTransform;
216
+ // Handle portrait rotation for mobile
213
217
  const isPortrait = containerRect.height > containerRect.width;
214
218
  const isVideoLandscape = videoW > videoH;
215
219
  const needsRotation = isPortrait && isVideoLandscape;
216
- const proj = this.controller.projectionTransform;
217
220
  const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
218
221
  const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
219
222
  const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
220
223
  const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
221
- const solveHomography = (w, h, p1, p2, p3, p4) => {
222
- const x1 = p1.sx, y1 = p1.sy;
223
- const x2 = p2.sx, y2 = p2.sy;
224
- const x3 = p3.sx, y3 = p3.sy;
225
- const x4 = p4.sx, y4 = p4.sy;
226
- const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
227
- const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
228
- let a, b, c, d, e, f, g, h_coeff;
229
- if (dx3 === 0 && dy3 === 0) {
230
- a = x2 - x1;
231
- b = x3 - x1;
232
- c = x1;
233
- d = y2 - y1;
234
- e = y3 - y1;
235
- f = y1;
236
- g = 0;
237
- h_coeff = 0;
238
- }
239
- else {
240
- const det = dx1 * dy2 - dx2 * dy1;
241
- g = (dx3 * dy2 - dx2 * dy3) / det;
242
- h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
243
- a = x2 - x1 + g * x2;
244
- b = x3 - x1 + h_coeff * x3;
245
- c = x1;
246
- d = y2 - y1 + g * y2;
247
- e = y3 - y1 + h_coeff * y3;
248
- f = y1;
249
- }
250
- return [
251
- a / w, d / w, 0, g / w,
252
- b / h, e / h, 0, h_coeff / h,
253
- 0, 0, 1, 0,
254
- c, f, 0, 1
255
- ];
256
- };
257
- const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
224
+ const matrix = this._solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
258
225
  this.overlay.style.maxWidth = 'none';
259
226
  this.overlay.style.width = `${markerW}px`;
260
227
  this.overlay.style.height = `${markerH}px`;
@@ -270,38 +237,87 @@ class SimpleAR {
270
237
  translate(${-markerW / 2}px, ${-markerH / 2}px)
271
238
  `;
272
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 ---
273
264
  _createDebugPanel() {
274
265
  this.debugPanel = document.createElement('div');
275
266
  this.debugPanel.style.cssText = `
276
- position: absolute;
277
- top: 10px;
278
- left: 10px;
279
- background: rgba(0, 0, 0, 0.8);
280
- color: #0f0;
281
- font-family: monospace;
282
- font-size: 12px;
283
- padding: 8px;
284
- border-radius: 4px;
285
- z-index: 99999;
286
- pointer-events: none;
287
- line-height: 1.5;
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;
288
272
  `;
289
273
  this.container.appendChild(this.debugPanel);
290
274
  }
291
- _updateDebugPanel(isTracking) {
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) {
292
287
  if (!this.debugPanel)
293
288
  return;
294
- // @ts-ignore
295
- const memory = performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) : '?';
296
- const color = isTracking ? '#0f0' : '#f00';
297
- const status = isTracking ? 'TRACKING' : 'SEARCHING';
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";
298
292
  this.debugPanel.innerHTML = `
299
- <div>HEAD-UP DISPLAY</div>
300
- <div>----------------</div>
301
- <div>FPS: ${this.fps}</div>
302
- <div>STATUS: <span style="color:${color}">${status}</span></div>
303
- <div>MEM: ${memory} MB</div>
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}
304
301
  `;
305
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
+ }
306
322
  }
307
323
  export { SimpleAR };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.94",
3
+ "version": "1.0.95",
4
4
  "description": "Ultra-fast Augmented Reality (AR) SDK for Node.js and Browser. Image tracking with 100% pure JavaScript, zero-dependencies, and high-performance compilation.",
5
5
  "keywords": [
6
6
  "augmented reality",
@@ -1,9 +1,9 @@
1
- import { Controller } from "./controller.js";
2
- import { OneEuroFilter } from "../libs/one-euro-filter.js";
1
+ import { BioInspiredController } from "./bio-inspired-controller.js";
3
2
  import { projectToScreen } from "../core/utils/projection.js";
4
3
 
5
4
  /**
6
- * 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
5
+ * 🍦 SimpleAR - High-performance Vanilla AR for image overlays
6
+ * Now powered by Bio-Inspired Perception and Nanite Virtualized Features.
7
7
  */
8
8
 
9
9
  export interface SimpleAROptions {
@@ -25,6 +25,58 @@ export interface SimpleAROptions {
25
25
  debug?: boolean;
26
26
  }
27
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
+
28
80
  class SimpleAR {
29
81
  container: HTMLElement;
30
82
  targetSrc: string | string[];
@@ -43,16 +95,18 @@ class SimpleAR {
43
95
  cameraConfig: MediaStreamConstraints['video'];
44
96
  debug: boolean;
45
97
 
46
- lastTime: number;
47
- frameCount: number;
48
- fps: number;
49
- debugPanel: HTMLElement | null = null;
50
- video: HTMLVideoElement | null = null;
51
- controller: Controller | null = null;
52
- isTracking: boolean = false;
53
- lastMatrix: number[] | null = null;
54
- filters: OneEuroFilter[] = [];
55
- markerDimensions: number[][] = [];
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;
56
110
 
57
111
  constructor({
58
112
  container,
@@ -74,12 +128,6 @@ class SimpleAR {
74
128
  this.onUpdateCallback = onUpdate;
75
129
  this.cameraConfig = cameraConfig;
76
130
  this.debug = debug;
77
- // @ts-ignore
78
- if (this.debug) window.AR_DEBUG = true;
79
-
80
- this.lastTime = performance.now();
81
- this.frameCount = 0;
82
- this.fps = 0;
83
131
  }
84
132
 
85
133
  async start() {
@@ -87,14 +135,17 @@ class SimpleAR {
87
135
  await this._startCamera();
88
136
  this._initController();
89
137
 
90
- if (this.debug) this._createDebugPanel();
138
+ if (this.debug) {
139
+ this._createDebugPanel();
140
+ this._createDebugCanvas();
141
+ }
91
142
 
92
143
  const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
93
144
  const result = await this.controller!.addImageTargets(targets);
94
145
  this.markerDimensions = result.dimensions;
95
- console.log("Targets loaded. Dimensions:", this.markerDimensions);
96
146
 
97
- this.controller!.processVideo(this.video);
147
+ // Kick off loop
148
+ this.controller!.processVideo(this.video!);
98
149
  return this;
99
150
  }
100
151
 
@@ -109,32 +160,26 @@ class SimpleAR {
109
160
  this.video = null;
110
161
  }
111
162
  this.isTracking = false;
112
- this.markerDimensions = [];
163
+ this.smoother.reset();
113
164
  }
114
165
 
115
- _createVideo() {
166
+ private _createVideo() {
116
167
  this.video = document.createElement('video');
117
168
  this.video.setAttribute('autoplay', '');
118
169
  this.video.setAttribute('playsinline', '');
119
170
  this.video.setAttribute('muted', '');
120
171
  this.video.style.cssText = `
121
172
  position: absolute;
122
- top: 0;
123
- left: 0;
124
- width: 100%;
125
- height: 100%;
126
- object-fit: cover;
127
- z-index: 0;
173
+ top: 0; left: 0; width: 100%; height: 100%;
174
+ object-fit: cover; z-index: 0;
128
175
  `;
129
176
  this.container.style.position = 'relative';
130
177
  this.container.style.overflow = 'hidden';
131
178
  this.container.insertBefore(this.video, this.container.firstChild);
132
179
  }
133
180
 
134
- async _startCamera() {
135
- const stream = await navigator.mediaDevices.getUserMedia({
136
- video: this.cameraConfig
137
- });
181
+ private async _startCamera() {
182
+ const stream = await navigator.mediaDevices.getUserMedia({ video: this.cameraConfig });
138
183
  this.video!.srcObject = stream;
139
184
  await this.video!.play();
140
185
 
@@ -144,178 +189,103 @@ class SimpleAR {
144
189
  });
145
190
  }
146
191
 
147
- _initController() {
148
- this.controller = new Controller({
192
+ private _initController() {
193
+ this.controller = new BioInspiredController({
149
194
  inputWidth: this.video!.videoWidth,
150
195
  inputHeight: this.video!.videoHeight,
151
196
  debugMode: this.debug,
152
- warmupTolerance: 3, // 🚀 Faster lock than default
153
- missTolerance: 10, // 🛡️ More resilient to temporary occlusion
197
+ bioInspired: {
198
+ enabled: true,
199
+ aggressiveSkipping: false
200
+ },
154
201
  onUpdate: (data) => this._handleUpdate(data)
155
202
  });
156
203
  }
157
204
 
158
- _handleUpdate(data: any) {
159
- if (data.type !== 'updateMatrix') return;
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
+ }
160
212
 
213
+ // FPS Meter
161
214
  const now = performance.now();
162
215
  this.frameCount++;
163
216
  if (now - this.lastTime >= 1000) {
164
217
  this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
165
218
  this.frameCount = 0;
166
219
  this.lastTime = now;
167
- if (this.debug) this._updateDebugPanel(this.isTracking);
168
220
  }
169
221
 
170
- const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities, detectionPoints } = data;
222
+ const { targetIndex, worldMatrix, modelViewTransform, reliabilities, stabilities, screenCoords, pixelsSaved } = data;
171
223
 
172
- // Project points to screen coordinates
173
- let projectedPoints = [];
224
+ // Apply Smoothing
225
+ let smoothedCoords = screenCoords || [];
174
226
  if (screenCoords && screenCoords.length > 0) {
175
- const containerRect = this.container.getBoundingClientRect();
176
- const videoW = this.video!.videoWidth;
177
- const videoH = this.video!.videoHeight;
178
- const isPortrait = containerRect.height > containerRect.width;
179
- const isVideoLandscape = videoW > videoH;
180
- const needsRotation = isPortrait && isVideoLandscape;
181
- const proj = this.controller!.projectionTransform;
182
-
183
- const vW = needsRotation ? videoH : videoW;
184
- const vH = needsRotation ? videoW : videoH;
185
- const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
186
- const dW = vW * pScale;
187
- const dH = vH * pScale;
188
- const oX = (containerRect.width - dW) / 2;
189
- const oY = (containerRect.height - dH) / 2;
190
-
191
- projectedPoints = screenCoords.map((p: any) => {
192
- let sx, sy;
193
- if (needsRotation) {
194
- sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
195
- sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
196
- } else {
197
- sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
198
- sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
199
- }
200
- return { x: sx, y: sy };
201
- });
202
- }
203
-
204
- let projectedDetectionPoints = [];
205
- if (detectionPoints && detectionPoints.length > 0) {
206
- const containerRect = this.container.getBoundingClientRect();
207
- const videoW = this.video!.videoWidth;
208
- const videoH = this.video!.videoHeight;
209
- const isPortrait = containerRect.height > containerRect.width;
210
- const isVideoLandscape = videoW > videoH;
211
- const needsRotation = isPortrait && isVideoLandscape;
212
- const proj = this.controller!.projectionTransform;
213
-
214
- const vW = needsRotation ? videoH : videoW;
215
- const vH = needsRotation ? videoW : videoH;
216
- const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
217
- const dW = vW * pScale;
218
- const dH = vH * pScale;
219
- const oX = (containerRect.width - dW) / 2;
220
- const oY = (containerRect.height - dH) / 2;
221
-
222
- projectedDetectionPoints = detectionPoints.map((p: any) => {
223
- let sx, sy;
224
- if (needsRotation) {
225
- sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
226
- sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
227
- } else {
228
- sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
229
- sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
230
- }
231
- return { x: sx, y: sy };
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 };
232
231
  });
233
232
  }
234
233
 
235
234
  if (worldMatrix) {
236
235
  if (!this.isTracking) {
237
236
  this.isTracking = true;
238
- this.overlay && (this.overlay.style.opacity = '1');
237
+ if (this.overlay) this.overlay.style.opacity = '1';
239
238
  this.onFound && this.onFound({ targetIndex });
240
239
  }
241
-
242
- this.lastMatrix = worldMatrix;
243
240
  this._positionOverlay(modelViewTransform, targetIndex);
244
241
  } else {
245
242
  if (this.isTracking) {
246
243
  this.isTracking = false;
247
- this.overlay && (this.overlay.style.opacity = '0');
244
+ if (this.overlay) this.overlay.style.opacity = '0';
248
245
  this.onLost && this.onLost({ targetIndex });
246
+ this.smoother.reset();
249
247
  }
250
248
  }
251
249
 
252
- // Always notify the callback if we have points, or if we just lost tracking
253
- if (projectedPoints.length > 0 || projectedDetectionPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
254
- this.onUpdateCallback && this.onUpdateCallback({
250
+ // Notify callback
251
+ if (this.onUpdateCallback) {
252
+ this.onUpdateCallback({
255
253
  targetIndex,
256
254
  worldMatrix,
257
- screenCoords: projectedPoints,
255
+ screenCoords: smoothedCoords,
258
256
  reliabilities: reliabilities || [],
259
257
  stabilities: stabilities || [],
260
- detectionPoints: projectedDetectionPoints
258
+ detectionPoints: data.featurePoints
261
259
  });
262
260
  }
261
+
262
+ // Draw Debug UI
263
+ if (this.debug) {
264
+ this._updateHUD(data);
265
+ this._drawDebugPoints(smoothedCoords, stabilities);
266
+ }
263
267
  }
264
268
 
265
- _positionOverlay(mVT: number[][], targetIndex: number) {
269
+ private _positionOverlay(mVT: number[][], targetIndex: number) {
266
270
  if (!this.overlay || !this.markerDimensions[targetIndex]) return;
267
271
 
268
272
  const [markerW, markerH] = this.markerDimensions[targetIndex];
269
273
  const containerRect = this.container.getBoundingClientRect();
270
274
  const videoW = this.video!.videoWidth;
271
275
  const videoH = this.video!.videoHeight;
276
+ const proj = this.controller!.projectionTransform;
272
277
 
278
+ // Handle portrait rotation for mobile
273
279
  const isPortrait = containerRect.height > containerRect.width;
274
280
  const isVideoLandscape = videoW > videoH;
275
281
  const needsRotation = isPortrait && isVideoLandscape;
276
282
 
277
- const proj = this.controller!.projectionTransform;
278
-
279
283
  const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
280
284
  const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
281
285
  const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
282
286
  const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
283
287
 
284
- const solveHomography = (w: number, h: number, p1: any, p2: any, p3: any, p4: any) => {
285
- const x1 = p1.sx, y1 = p1.sy;
286
- const x2 = p2.sx, y2 = p2.sy;
287
- const x3 = p3.sx, y3 = p3.sy;
288
- const x4 = p4.sx, y4 = p4.sy;
289
-
290
- const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
291
- const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
292
-
293
- let a, b, c, d, e, f, g, h_coeff;
294
-
295
- if (dx3 === 0 && dy3 === 0) {
296
- a = x2 - x1; b = x3 - x1; c = x1;
297
- d = y2 - y1; e = y3 - y1; f = y1;
298
- g = 0; h_coeff = 0;
299
- } else {
300
- const det = dx1 * dy2 - dx2 * dy1;
301
- g = (dx3 * dy2 - dx2 * dy3) / det;
302
- h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
303
- a = x2 - x1 + g * x2;
304
- b = x3 - x1 + h_coeff * x3;
305
- c = x1;
306
- d = y2 - y1 + g * y2;
307
- e = y3 - y1 + h_coeff * y3;
308
- f = y1;
309
- }
310
- return [
311
- a / w, d / w, 0, g / w,
312
- b / h, e / h, 0, h_coeff / h,
313
- 0, 0, 1, 0,
314
- c, f, 0, 1
315
- ];
316
- };
317
-
318
- const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
288
+ const matrix = this._solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
319
289
 
320
290
  this.overlay.style.maxWidth = 'none';
321
291
  this.overlay.style.width = `${markerW}px`;
@@ -334,40 +304,96 @@ class SimpleAR {
334
304
  `;
335
305
  }
336
306
 
337
- _createDebugPanel() {
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() {
338
337
  this.debugPanel = document.createElement('div');
339
338
  this.debugPanel.style.cssText = `
340
- position: absolute;
341
- top: 10px;
342
- left: 10px;
343
- background: rgba(0, 0, 0, 0.8);
344
- color: #0f0;
345
- font-family: monospace;
346
- font-size: 12px;
347
- padding: 8px;
348
- border-radius: 4px;
349
- z-index: 99999;
350
- pointer-events: none;
351
- line-height: 1.5;
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;
352
344
  `;
353
345
  this.container.appendChild(this.debugPanel);
354
346
  }
355
347
 
356
- _updateDebugPanel(isTracking: boolean) {
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) {
357
361
  if (!this.debugPanel) return;
358
- // @ts-ignore
359
- const memory = performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) : '?';
360
- const color = isTracking ? '#0f0' : '#f00';
361
- const status = isTracking ? 'TRACKING' : 'SEARCHING';
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";
362
365
 
363
366
  this.debugPanel.innerHTML = `
364
- <div>HEAD-UP DISPLAY</div>
365
- <div>----------------</div>
366
- <div>FPS: ${this.fps}</div>
367
- <div>STATUS: <span style="color:${color}">${status}</span></div>
368
- <div>MEM: ${memory} MB</div>
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}
369
375
  `;
370
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
+ }
371
397
  }
372
398
 
373
399
  export { SimpleAR };