@srsergio/taptapp-ar 1.0.80 → 1.0.82

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.
@@ -65,12 +65,9 @@ declare class Controller {
65
65
  }>;
66
66
  _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number): Promise<{
67
67
  modelViewTransform: any;
68
- screenCoords: {
69
- x: number;
70
- y: number;
71
- }[];
68
+ screenCoords: any[];
72
69
  reliabilities: number[];
73
- stabilities: any[];
70
+ stabilities: number[];
74
71
  }>;
75
72
  processVideo(input: any): void;
76
73
  stopProcessVideo(): void;
@@ -192,51 +192,73 @@ class Controller {
192
192
  const state = this.trackingStates[targetIndex];
193
193
  if (!state.pointStabilities)
194
194
  state.pointStabilities = [];
195
+ if (!state.lastScreenCoords)
196
+ state.lastScreenCoords = [];
195
197
  if (!state.pointStabilities[octaveIndex]) {
196
- // Initialize stabilities for this octave if not exists
197
198
  const numPoints = this.tracker.prebuiltData[targetIndex][octaveIndex].px.length;
198
- state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0.5); // Start at 0.5
199
+ state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0);
200
+ state.lastScreenCoords[octaveIndex] = new Array(numPoints).fill(null);
199
201
  }
200
202
  const stabilities = state.pointStabilities[octaveIndex];
201
- const currentStabilities = [];
202
- // Update all points in this octave
203
+ const lastCoords = state.lastScreenCoords[octaveIndex];
204
+ // Update stability for ALL points in the current octave
203
205
  for (let i = 0; i < stabilities.length; i++) {
204
- const isTracked = indices.includes(i);
205
- if (isTracked) {
206
- stabilities[i] = Math.min(1.0, stabilities[i] + 0.35); // Fast recovery (approx 3 frames)
206
+ const isCurrentlyTracked = indices.includes(i);
207
+ if (isCurrentlyTracked) {
208
+ const idxInResult = indices.indexOf(i);
209
+ stabilities[i] = Math.min(1.0, stabilities[i] + 0.4); // Fast attack
210
+ lastCoords[i] = screenCoords[idxInResult]; // Update last known position
207
211
  }
208
212
  else {
209
- stabilities[i] = Math.max(0.0, stabilities[i] - 0.12); // Slightly more forgiving loss
213
+ stabilities[i] = Math.max(0.0, stabilities[i] - 0.08); // Slow decay (approx 12 frames/0.2s)
210
214
  }
211
215
  }
212
- // Collect stabilities and FILTER OUT excessive flickerers (Dead Zone)
213
- const filteredWorldCoords = [];
214
- const filteredScreenCoords = [];
215
- const filteredStabilities = [];
216
- for (let i = 0; i < indices.length; i++) {
217
- const s = stabilities[indices[i]];
218
- if (s > 0.3) { // Hard Cutoff: points with <30% stability are ignored
219
- filteredWorldCoords.push(worldCoords[i]);
220
- filteredScreenCoords.push(screenCoords[i]);
221
- filteredStabilities.push(s);
216
+ // Collect points for the UI: both currently tracked AND hibernating
217
+ const finalScreenCoords = [];
218
+ const finalReliabilities = [];
219
+ const finalStabilities = [];
220
+ const finalWorldCoords = [];
221
+ for (let i = 0; i < stabilities.length; i++) {
222
+ if (stabilities[i] > 0) {
223
+ const isCurrentlyTracked = indices.includes(i);
224
+ finalScreenCoords.push(lastCoords[i]);
225
+ finalStabilities.push(stabilities[i]);
226
+ if (isCurrentlyTracked) {
227
+ const idxInResult = indices.indexOf(i);
228
+ finalReliabilities.push(reliabilities[idxInResult]);
229
+ finalWorldCoords.push(worldCoords[idxInResult]);
230
+ }
231
+ else {
232
+ finalReliabilities.push(0); // Hibernating points have 0 reliability
233
+ }
222
234
  }
223
235
  }
224
- // STRICT QUALITY CHECK: Prevent "sticky" tracking on background noise.
225
- // We require a minimum number of high-confidence AND STABLE points.
226
- const stableAndReliable = reliabilities.filter((r, idx) => r > 0.75 && stabilities[indices[idx]] > 0.5).length;
227
- if (stableAndReliable < 6 || filteredWorldCoords.length < 8) {
228
- return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [] };
236
+ // STRICT QUALITY CHECK: We only update the transform if we have enough HIGH CONFIDENCE points
237
+ const stableAndReliable = reliabilities.filter((r, idx) => r > 0.8 && stabilities[indices[idx]] > 0.6).length;
238
+ if (stableAndReliable < 6 || finalWorldCoords.length < 8) {
239
+ return {
240
+ modelViewTransform: null,
241
+ screenCoords: finalScreenCoords,
242
+ reliabilities: finalReliabilities,
243
+ stabilities: finalStabilities
244
+ };
229
245
  }
230
246
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
231
- worldCoords: filteredWorldCoords,
232
- screenCoords: filteredScreenCoords,
233
- stabilities: filteredStabilities
247
+ worldCoords: finalWorldCoords,
248
+ screenCoords: finalWorldCoords.map((_, i) => {
249
+ const globalIdx = indices[i];
250
+ return lastCoords[globalIdx];
251
+ }),
252
+ stabilities: finalWorldCoords.map((_, i) => {
253
+ const globalIdx = indices[i];
254
+ return stabilities[globalIdx];
255
+ })
234
256
  });
235
257
  return {
236
258
  modelViewTransform,
237
- screenCoords: filteredScreenCoords,
238
- reliabilities: reliabilities.filter((_, idx) => stabilities[indices[idx]] > 0.3),
239
- stabilities: filteredStabilities
259
+ screenCoords: finalScreenCoords,
260
+ reliabilities: finalReliabilities,
261
+ stabilities: finalStabilities
240
262
  };
241
263
  }
242
264
  processVideo(input) {
@@ -282,9 +304,10 @@ class Controller {
282
304
  const result = await this._trackAndUpdate(inputData, trackingState.currentModelViewTransform, i);
283
305
  if (result === null || result.modelViewTransform === null) {
284
306
  trackingState.isTracking = false;
285
- trackingState.screenCoords = [];
286
- trackingState.reliabilities = [];
287
- trackingState.stabilities = [];
307
+ // Keep points for the last update so they can be shown as it "asoma"
308
+ trackingState.screenCoords = result?.screenCoords || [];
309
+ trackingState.reliabilities = result?.reliabilities || [];
310
+ trackingState.stabilities = result?.stabilities || [];
288
311
  }
289
312
  else {
290
313
  trackingState.currentModelViewTransform = result.modelViewTransform;
@@ -297,24 +320,27 @@ class Controller {
297
320
  trackingState.showing = this.featureManager.shouldShow(i, trackingState.isTracking);
298
321
  if (wasShowing && !trackingState.showing) {
299
322
  trackingState.trackingMatrix = null;
300
- this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
301
323
  this.featureManager.notifyUpdate({ type: "reset", targetIndex: i });
302
324
  }
303
- if (trackingState.showing) {
304
- const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
305
- // Calculate confidence score based on point stability
306
- const stabilities = trackingState.stabilities || [];
307
- const avgStability = stabilities.length > 0
308
- ? stabilities.reduce((a, b) => a + b, 0) / stabilities.length
309
- : 0;
310
- const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
311
- trackingState.trackingMatrix = filteredMatrix;
312
- let finalMatrix = [...filteredMatrix];
313
- const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
314
- if (isInputRotated) {
315
- const rotationFeature = this.featureManager.getFeature("auto-rotation");
316
- if (rotationFeature) {
317
- finalMatrix = rotationFeature.rotate(finalMatrix);
325
+ // Always notify update if we have points or if visibility changed
326
+ if (trackingState.showing || (trackingState.screenCoords && trackingState.screenCoords.length > 0) || (wasShowing && !trackingState.showing)) {
327
+ const worldMatrix = trackingState.showing ? this._glModelViewMatrix(trackingState.currentModelViewTransform, i) : null;
328
+ let finalMatrix = null;
329
+ if (worldMatrix) {
330
+ // Calculate confidence score based on point stability
331
+ const stabilities = trackingState.stabilities || [];
332
+ const avgStability = stabilities.length > 0
333
+ ? stabilities.reduce((a, b) => a + b, 0) / stabilities.length
334
+ : 0;
335
+ const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
336
+ trackingState.trackingMatrix = filteredMatrix;
337
+ finalMatrix = [...filteredMatrix];
338
+ const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
339
+ if (isInputRotated) {
340
+ const rotationFeature = this.featureManager.getFeature("auto-rotation");
341
+ if (rotationFeature) {
342
+ finalMatrix = rotationFeature.rotate(finalMatrix);
343
+ }
318
344
  }
319
345
  }
320
346
  this.onUpdate && this.onUpdate({
@@ -114,6 +114,36 @@ class SimpleAR {
114
114
  this._updateDebugPanel(this.isTracking);
115
115
  }
116
116
  const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
117
+ // Project points to screen coordinates
118
+ let projectedPoints = [];
119
+ if (screenCoords && screenCoords.length > 0) {
120
+ const containerRect = this.container.getBoundingClientRect();
121
+ const videoW = this.video.videoWidth;
122
+ const videoH = this.video.videoHeight;
123
+ const isPortrait = containerRect.height > containerRect.width;
124
+ const isVideoLandscape = videoW > videoH;
125
+ const needsRotation = isPortrait && isVideoLandscape;
126
+ const proj = this.controller.projectionTransform;
127
+ const vW = needsRotation ? videoH : videoW;
128
+ const vH = needsRotation ? videoW : videoH;
129
+ const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
130
+ const dW = vW * pScale;
131
+ const dH = vH * pScale;
132
+ const oX = (containerRect.width - dW) / 2;
133
+ const oY = (containerRect.height - dH) / 2;
134
+ projectedPoints = screenCoords.map((p) => {
135
+ let sx, sy;
136
+ if (needsRotation) {
137
+ sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
138
+ sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
139
+ }
140
+ else {
141
+ sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
142
+ sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
143
+ }
144
+ return { x: sx, y: sy };
145
+ });
146
+ }
117
147
  if (worldMatrix) {
118
148
  if (!this.isTracking) {
119
149
  this.isTracking = true;
@@ -121,60 +151,25 @@ class SimpleAR {
121
151
  this.onFound && this.onFound({ targetIndex });
122
152
  }
123
153
  this.lastMatrix = worldMatrix;
124
- // We use the matrix from the controller directly (it's already filtered there)
125
154
  this._positionOverlay(modelViewTransform, targetIndex);
126
- // Project points to screen coordinates
127
- let projectedPoints = [];
128
- if (screenCoords && screenCoords.length > 0) {
129
- const containerRect = this.container.getBoundingClientRect();
130
- const videoW = this.video.videoWidth;
131
- const videoH = this.video.videoHeight;
132
- const isPortrait = containerRect.height > containerRect.width;
133
- const isVideoLandscape = videoW > videoH;
134
- const needsRotation = isPortrait && isVideoLandscape;
135
- const proj = this.controller.projectionTransform;
136
- const vW = needsRotation ? videoH : videoW;
137
- const vH = needsRotation ? videoW : videoH;
138
- const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
139
- const dW = vW * pScale;
140
- const dH = vH * pScale;
141
- const oX = (containerRect.width - dW) / 2;
142
- const oY = (containerRect.height - dH) / 2;
143
- projectedPoints = screenCoords.map((p) => {
144
- let sx, sy;
145
- if (needsRotation) {
146
- sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
147
- sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
148
- }
149
- else {
150
- sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
151
- sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
152
- }
153
- return { x: sx, y: sy };
154
- });
155
- }
156
- this.onUpdateCallback && this.onUpdateCallback({
157
- targetIndex,
158
- worldMatrix,
159
- screenCoords: projectedPoints,
160
- reliabilities,
161
- stabilities
162
- });
163
155
  }
164
156
  else {
165
157
  if (this.isTracking) {
166
158
  this.isTracking = false;
167
159
  this.overlay && (this.overlay.style.opacity = '0');
168
160
  this.onLost && this.onLost({ targetIndex });
169
- this.onUpdateCallback && this.onUpdateCallback({
170
- targetIndex,
171
- worldMatrix: null,
172
- screenCoords: [],
173
- reliabilities: [],
174
- stabilities: []
175
- });
176
161
  }
177
162
  }
163
+ // Always notify the callback if we have points, or if we just lost tracking
164
+ if (projectedPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
165
+ this.onUpdateCallback && this.onUpdateCallback({
166
+ targetIndex,
167
+ worldMatrix,
168
+ screenCoords: projectedPoints,
169
+ reliabilities: reliabilities || [],
170
+ stabilities: stabilities || []
171
+ });
172
+ }
178
173
  }
179
174
  _positionOverlay(mVT, targetIndex) {
180
175
  if (!this.overlay || !this.markerDimensions[targetIndex])
@@ -12,13 +12,20 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
12
12
  const url = config.videoSrc.toLowerCase().split('?')[0];
13
13
  return videoExtensions.some(ext => url.endsWith(ext)) || config.videoSrc.includes('video');
14
14
  }, [config.videoSrc]);
15
- return (_jsxs("div", { className: `taptapp-ar-wrapper ${className} ${status}`, style: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }, children: [showScanningOverlay && status === "scanning" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-scanning", children: _jsxs("div", { className: "scanning-content", children: [_jsxs("div", { className: "scanning-frame", children: [_jsx("img", { className: "target-preview", src: config.targetImageSrc, alt: "Target", crossOrigin: "anonymous" }), _jsx("div", { className: "scanning-line" })] }), _jsx("p", { className: "scanning-text", children: "Apunta a la imagen para comenzar" })] }) })), showErrorOverlay && status === "error" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-error", children: _jsxs("div", { className: "error-content", children: [_jsx("span", { className: "error-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "error-title", children: "No se pudo iniciar AR" }), _jsx("p", { className: "error-text", children: "Verifica los permisos de c\u00E1mara" }), _jsx("button", { className: "retry-btn", onClick: () => window.location.reload(), children: "Reintentar" })] }) })), _jsx("div", { ref: containerRef, className: "taptapp-ar-container", onClick: toggleVideo, style: { width: '100%', height: '100%' }, children: isVideo ? (_jsx("video", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc, preload: "auto", loop: true, playsInline: true, muted: true, crossOrigin: "anonymous" })) : (_jsx("img", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc || config.targetImageSrc, crossOrigin: "anonymous", alt: "AR Overlay" })) }), status === "tracking" && (_jsx("div", { className: "taptapp-ar-points-overlay", children: trackedPoints.filter(p => p.reliability > 0.7).map((point, i) => (_jsx("div", { className: "tracking-point", style: {
16
- left: `${point.x}px`,
17
- top: `${point.y}px`,
18
- width: `${(2 + point.reliability * 6) * (0.4 + point.stability * 0.6)}px`,
19
- height: `${(2 + point.reliability * 6) * (0.4 + point.stability * 0.6)}px`,
20
- opacity: (0.3 + (point.reliability * 0.4)) * (0.2 + point.stability * 0.8)
21
- } }, i))) })), _jsx("style", { children: `
15
+ return (_jsxs("div", { className: `taptapp-ar-wrapper ${className} ${status}`, style: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }, children: [showScanningOverlay && status === "scanning" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-scanning", children: _jsxs("div", { className: "scanning-content", children: [_jsxs("div", { className: "scanning-frame", children: [_jsx("img", { className: "target-preview", src: config.targetImageSrc, alt: "Target", crossOrigin: "anonymous" }), _jsx("div", { className: "scanning-line" })] }), _jsx("p", { className: "scanning-text", children: "Apunta a la imagen para comenzar" })] }) })), showErrorOverlay && status === "error" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-error", children: _jsxs("div", { className: "error-content", children: [_jsx("span", { className: "error-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "error-title", children: "No se pudo iniciar AR" }), _jsx("p", { className: "error-text", children: "Verifica los permisos de c\u00E1mara" }), _jsx("button", { className: "retry-btn", onClick: () => window.location.reload(), children: "Reintentar" })] }) })), _jsx("div", { ref: containerRef, className: "taptapp-ar-container", onClick: toggleVideo, style: { width: '100%', height: '100%' }, children: isVideo ? (_jsx("video", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc, preload: "auto", loop: true, playsInline: true, muted: true, crossOrigin: "anonymous" })) : (_jsx("img", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc || config.targetImageSrc, crossOrigin: "anonymous", alt: "AR Overlay" })) }), trackedPoints.length > 0 && (_jsx("div", { className: "taptapp-ar-points-overlay", style: { opacity: status === "tracking" ? 1 : 0.6 }, children: trackedPoints
16
+ .map((point, i) => {
17
+ const isStable = point.stability > 0.8 && point.reliability > 0.5;
18
+ const size = (2 + point.reliability * 6) * (0.6 + point.stability * 0.4);
19
+ return (_jsx("div", { className: `tracking-point ${!isStable ? 'flickering' : ''}`, style: {
20
+ left: `${point.x}px`,
21
+ top: `${point.y}px`,
22
+ width: `${size}px`,
23
+ height: `${size}px`,
24
+ opacity: (0.3 + (point.reliability * 0.5)) * (0.2 + point.stability * 0.8),
25
+ backgroundColor: isStable ? 'black' : '#00ff00',
26
+ boxShadow: isStable ? '0 0 2px rgba(255, 255, 255, 0.8)' : '0 0 8px #00ff00'
27
+ } }, i));
28
+ }) })), _jsx("style", { children: `
22
29
  .taptapp-ar-wrapper {
23
30
  background: #000;
24
31
  color: white;
@@ -114,11 +121,15 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
114
121
  .tracking-point {
115
122
  position: absolute;
116
123
  background: black;
117
- border: 1px solid rgba(255,255,255,0.5); /* Better contrast */
124
+ border: 1px solid rgba(255,255,255,0.5);
118
125
  border-radius: 50%;
119
126
  transform: translate(-50%, -50%);
120
127
  box-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
121
128
  pointer-events: none;
129
+ transition: background-color 0.3s ease, box-shadow 0.3s ease, width 0.2s ease, height 0.2s ease, opacity 0.2s ease;
130
+ }
131
+ .tracking-point.flickering {
132
+ z-index: 101;
122
133
  }
123
134
  ` })] }));
124
135
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
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",
@@ -51,7 +51,8 @@
51
51
  "scripts": {
52
52
  "build": "tsc",
53
53
  "prepublishOnly": "npm run build",
54
- "test": "vitest"
54
+ "test": "vitest",
55
+ "test:react": "vite --port 4321 --open tests/react-test.html"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "aframe": ">=1.5.0",
@@ -264,58 +264,80 @@ class Controller {
264
264
 
265
265
  const state = this.trackingStates[targetIndex];
266
266
  if (!state.pointStabilities) state.pointStabilities = [];
267
+ if (!state.lastScreenCoords) state.lastScreenCoords = [];
268
+
267
269
  if (!state.pointStabilities[octaveIndex]) {
268
- // Initialize stabilities for this octave if not exists
269
270
  const numPoints = (this.tracker as any).prebuiltData[targetIndex][octaveIndex].px.length;
270
- state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0.5); // Start at 0.5
271
+ state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0);
272
+ state.lastScreenCoords[octaveIndex] = new Array(numPoints).fill(null);
271
273
  }
272
274
 
273
275
  const stabilities = state.pointStabilities[octaveIndex];
274
- const currentStabilities: number[] = [];
276
+ const lastCoords = state.lastScreenCoords[octaveIndex];
275
277
 
276
- // Update all points in this octave
278
+ // Update stability for ALL points in the current octave
277
279
  for (let i = 0; i < stabilities.length; i++) {
278
- const isTracked = indices.includes(i);
279
- if (isTracked) {
280
- stabilities[i] = Math.min(1.0, stabilities[i] + 0.35); // Fast recovery (approx 3 frames)
280
+ const isCurrentlyTracked = indices.includes(i);
281
+ if (isCurrentlyTracked) {
282
+ const idxInResult = indices.indexOf(i);
283
+ stabilities[i] = Math.min(1.0, stabilities[i] + 0.4); // Fast attack
284
+ lastCoords[i] = screenCoords[idxInResult]; // Update last known position
281
285
  } else {
282
- stabilities[i] = Math.max(0.0, stabilities[i] - 0.12); // Slightly more forgiving loss
286
+ stabilities[i] = Math.max(0.0, stabilities[i] - 0.08); // Slow decay (approx 12 frames/0.2s)
283
287
  }
284
288
  }
285
289
 
286
- // Collect stabilities and FILTER OUT excessive flickerers (Dead Zone)
287
- const filteredWorldCoords = [];
288
- const filteredScreenCoords = [];
289
- const filteredStabilities = [];
290
-
291
- for (let i = 0; i < indices.length; i++) {
292
- const s = stabilities[indices[i]];
293
- if (s > 0.3) { // Hard Cutoff: points with <30% stability are ignored
294
- filteredWorldCoords.push(worldCoords[i]);
295
- filteredScreenCoords.push(screenCoords[i]);
296
- filteredStabilities.push(s);
290
+ // Collect points for the UI: both currently tracked AND hibernating
291
+ const finalScreenCoords: any[] = [];
292
+ const finalReliabilities: number[] = [];
293
+ const finalStabilities: number[] = [];
294
+ const finalWorldCoords: any[] = [];
295
+
296
+ for (let i = 0; i < stabilities.length; i++) {
297
+ if (stabilities[i] > 0) {
298
+ const isCurrentlyTracked = indices.includes(i);
299
+ finalScreenCoords.push(lastCoords[i]);
300
+ finalStabilities.push(stabilities[i]);
301
+
302
+ if (isCurrentlyTracked) {
303
+ const idxInResult = indices.indexOf(i);
304
+ finalReliabilities.push(reliabilities[idxInResult]);
305
+ finalWorldCoords.push(worldCoords[idxInResult]);
306
+ } else {
307
+ finalReliabilities.push(0); // Hibernating points have 0 reliability
308
+ }
297
309
  }
298
310
  }
299
311
 
300
- // STRICT QUALITY CHECK: Prevent "sticky" tracking on background noise.
301
- // We require a minimum number of high-confidence AND STABLE points.
302
- const stableAndReliable = reliabilities.filter((r: number, idx: number) => r > 0.75 && stabilities[indices[idx]] > 0.5).length;
312
+ // STRICT QUALITY CHECK: We only update the transform if we have enough HIGH CONFIDENCE points
313
+ const stableAndReliable = reliabilities.filter((r: number, idx: number) => r > 0.8 && stabilities[indices[idx]] > 0.6).length;
303
314
 
304
- if (stableAndReliable < 6 || filteredWorldCoords.length < 8) {
305
- return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [] };
315
+ if (stableAndReliable < 6 || finalWorldCoords.length < 8) {
316
+ return {
317
+ modelViewTransform: null,
318
+ screenCoords: finalScreenCoords,
319
+ reliabilities: finalReliabilities,
320
+ stabilities: finalStabilities
321
+ };
306
322
  }
307
323
 
308
324
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
309
- worldCoords: filteredWorldCoords,
310
- screenCoords: filteredScreenCoords,
311
- stabilities: filteredStabilities
325
+ worldCoords: finalWorldCoords,
326
+ screenCoords: finalWorldCoords.map((_, i) => {
327
+ const globalIdx = indices[i];
328
+ return lastCoords[globalIdx];
329
+ }),
330
+ stabilities: finalWorldCoords.map((_, i) => {
331
+ const globalIdx = indices[i];
332
+ return stabilities[globalIdx];
333
+ })
312
334
  });
313
335
 
314
336
  return {
315
337
  modelViewTransform,
316
- screenCoords: filteredScreenCoords,
317
- reliabilities: reliabilities.filter((_, idx) => stabilities[indices[idx]] > 0.3),
318
- stabilities: filteredStabilities
338
+ screenCoords: finalScreenCoords,
339
+ reliabilities: finalReliabilities,
340
+ stabilities: finalStabilities
319
341
  };
320
342
  }
321
343
 
@@ -371,9 +393,10 @@ class Controller {
371
393
  );
372
394
  if (result === null || result.modelViewTransform === null) {
373
395
  trackingState.isTracking = false;
374
- trackingState.screenCoords = [];
375
- trackingState.reliabilities = [];
376
- trackingState.stabilities = [];
396
+ // Keep points for the last update so they can be shown as it "asoma"
397
+ trackingState.screenCoords = result?.screenCoords || [];
398
+ trackingState.reliabilities = result?.reliabilities || [];
399
+ trackingState.stabilities = result?.stabilities || [];
377
400
  } else {
378
401
  trackingState.currentModelViewTransform = result.modelViewTransform;
379
402
  trackingState.screenCoords = result.screenCoords;
@@ -387,29 +410,33 @@ class Controller {
387
410
 
388
411
  if (wasShowing && !trackingState.showing) {
389
412
  trackingState.trackingMatrix = null;
390
- this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
391
413
  this.featureManager.notifyUpdate({ type: "reset", targetIndex: i });
392
414
  }
393
415
 
394
- if (trackingState.showing) {
395
- const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
416
+ // Always notify update if we have points or if visibility changed
417
+ if (trackingState.showing || (trackingState.screenCoords && trackingState.screenCoords.length > 0) || (wasShowing && !trackingState.showing)) {
418
+ const worldMatrix = trackingState.showing ? this._glModelViewMatrix(trackingState.currentModelViewTransform, i) : null;
419
+
420
+ let finalMatrix = null;
396
421
 
397
- // Calculate confidence score based on point stability
398
- const stabilities = trackingState.stabilities || [];
399
- const avgStability = stabilities.length > 0
400
- ? stabilities.reduce((a: number, b: number) => a + b, 0) / stabilities.length
401
- : 0;
422
+ if (worldMatrix) {
423
+ // Calculate confidence score based on point stability
424
+ const stabilities = trackingState.stabilities || [];
425
+ const avgStability = stabilities.length > 0
426
+ ? stabilities.reduce((a: number, b: number) => a + b, 0) / stabilities.length
427
+ : 0;
402
428
 
403
- const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
404
- trackingState.trackingMatrix = filteredMatrix;
429
+ const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
430
+ trackingState.trackingMatrix = filteredMatrix;
405
431
 
406
- let finalMatrix = [...filteredMatrix];
432
+ finalMatrix = [...filteredMatrix];
407
433
 
408
- const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
409
- if (isInputRotated) {
410
- const rotationFeature = this.featureManager.getFeature<AutoRotationFeature>("auto-rotation");
411
- if (rotationFeature) {
412
- finalMatrix = rotationFeature.rotate(finalMatrix);
434
+ const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
435
+ if (isInputRotated) {
436
+ const rotationFeature = this.featureManager.getFeature<AutoRotationFeature>("auto-rotation");
437
+ if (rotationFeature) {
438
+ finalMatrix = rotationFeature.rotate(finalMatrix);
439
+ }
413
440
  }
414
441
  }
415
442
 
@@ -165,6 +165,38 @@ class SimpleAR {
165
165
 
166
166
  const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
167
167
 
168
+ // Project points to screen coordinates
169
+ let projectedPoints = [];
170
+ if (screenCoords && screenCoords.length > 0) {
171
+ const containerRect = this.container.getBoundingClientRect();
172
+ const videoW = this.video!.videoWidth;
173
+ const videoH = this.video!.videoHeight;
174
+ const isPortrait = containerRect.height > containerRect.width;
175
+ const isVideoLandscape = videoW > videoH;
176
+ const needsRotation = isPortrait && isVideoLandscape;
177
+ const proj = this.controller!.projectionTransform;
178
+
179
+ const vW = needsRotation ? videoH : videoW;
180
+ const vH = needsRotation ? videoW : videoH;
181
+ const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
182
+ const dW = vW * pScale;
183
+ const dH = vH * pScale;
184
+ const oX = (containerRect.width - dW) / 2;
185
+ const oY = (containerRect.height - dH) / 2;
186
+
187
+ projectedPoints = screenCoords.map((p: any) => {
188
+ let sx, sy;
189
+ if (needsRotation) {
190
+ sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
191
+ sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
192
+ } else {
193
+ sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
194
+ sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
195
+ }
196
+ return { x: sx, y: sy };
197
+ });
198
+ }
199
+
168
200
  if (worldMatrix) {
169
201
  if (!this.isTracking) {
170
202
  this.isTracking = true;
@@ -173,63 +205,24 @@ class SimpleAR {
173
205
  }
174
206
 
175
207
  this.lastMatrix = worldMatrix;
176
-
177
- // We use the matrix from the controller directly (it's already filtered there)
178
208
  this._positionOverlay(modelViewTransform, targetIndex);
179
-
180
- // Project points to screen coordinates
181
- let projectedPoints = [];
182
- if (screenCoords && screenCoords.length > 0) {
183
- const containerRect = this.container.getBoundingClientRect();
184
- const videoW = this.video!.videoWidth;
185
- const videoH = this.video!.videoHeight;
186
- const isPortrait = containerRect.height > containerRect.width;
187
- const isVideoLandscape = videoW > videoH;
188
- const needsRotation = isPortrait && isVideoLandscape;
189
- const proj = this.controller!.projectionTransform;
190
-
191
- const vW = needsRotation ? videoH : videoW;
192
- const vH = needsRotation ? videoW : videoH;
193
- const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
194
- const dW = vW * pScale;
195
- const dH = vH * pScale;
196
- const oX = (containerRect.width - dW) / 2;
197
- const oY = (containerRect.height - dH) / 2;
198
-
199
- projectedPoints = screenCoords.map((p: any) => {
200
- let sx, sy;
201
- if (needsRotation) {
202
- sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
203
- sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
204
- } else {
205
- sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
206
- sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
207
- }
208
- return { x: sx, y: sy };
209
- });
209
+ } else {
210
+ if (this.isTracking) {
211
+ this.isTracking = false;
212
+ this.overlay && (this.overlay.style.opacity = '0');
213
+ this.onLost && this.onLost({ targetIndex });
210
214
  }
215
+ }
211
216
 
217
+ // Always notify the callback if we have points, or if we just lost tracking
218
+ if (projectedPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
212
219
  this.onUpdateCallback && this.onUpdateCallback({
213
220
  targetIndex,
214
221
  worldMatrix,
215
222
  screenCoords: projectedPoints,
216
- reliabilities,
217
- stabilities
223
+ reliabilities: reliabilities || [],
224
+ stabilities: stabilities || []
218
225
  });
219
-
220
- } else {
221
- if (this.isTracking) {
222
- this.isTracking = false;
223
- this.overlay && (this.overlay.style.opacity = '0');
224
- this.onLost && this.onLost({ targetIndex });
225
- this.onUpdateCallback && this.onUpdateCallback({
226
- targetIndex,
227
- worldMatrix: null as any,
228
- screenCoords: [],
229
- reliabilities: [],
230
- stabilities: []
231
- });
232
- }
233
226
  }
234
227
  }
235
228
 
@@ -90,21 +90,29 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
90
90
  </div>
91
91
 
92
92
  {/* Tracking Points Layer */}
93
- {status === "tracking" && (
94
- <div className="taptapp-ar-points-overlay">
95
- {trackedPoints.filter(p => p.reliability > 0.7).map((point, i) => (
96
- <div
97
- key={i}
98
- className="tracking-point"
99
- style={{
100
- left: `${point.x}px`,
101
- top: `${point.y}px`,
102
- width: `${(2 + point.reliability * 6) * (0.4 + point.stability * 0.6)}px`,
103
- height: `${(2 + point.reliability * 6) * (0.4 + point.stability * 0.6)}px`,
104
- opacity: (0.3 + (point.reliability * 0.4)) * (0.2 + point.stability * 0.8)
105
- }}
106
- />
107
- ))}
93
+ {trackedPoints.length > 0 && (
94
+ <div className="taptapp-ar-points-overlay" style={{ opacity: status === "tracking" ? 1 : 0.6 }}>
95
+ {trackedPoints
96
+ .map((point, i) => {
97
+ const isStable = point.stability > 0.8 && point.reliability > 0.5;
98
+ const size = (2 + point.reliability * 6) * (0.6 + point.stability * 0.4);
99
+
100
+ return (
101
+ <div
102
+ key={i}
103
+ className={`tracking-point ${!isStable ? 'flickering' : ''}`}
104
+ style={{
105
+ left: `${point.x}px`,
106
+ top: `${point.y}px`,
107
+ width: `${size}px`,
108
+ height: `${size}px`,
109
+ opacity: (0.3 + (point.reliability * 0.5)) * (0.2 + point.stability * 0.8),
110
+ backgroundColor: isStable ? 'black' : '#00ff00',
111
+ boxShadow: isStable ? '0 0 2px rgba(255, 255, 255, 0.8)' : '0 0 8px #00ff00'
112
+ }}
113
+ />
114
+ );
115
+ })}
108
116
  </div>
109
117
  )}
110
118
 
@@ -204,11 +212,15 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
204
212
  .tracking-point {
205
213
  position: absolute;
206
214
  background: black;
207
- border: 1px solid rgba(255,255,255,0.5); /* Better contrast */
215
+ border: 1px solid rgba(255,255,255,0.5);
208
216
  border-radius: 50%;
209
217
  transform: translate(-50%, -50%);
210
218
  box-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
211
219
  pointer-events: none;
220
+ transition: background-color 0.3s ease, box-shadow 0.3s ease, width 0.2s ease, height 0.2s ease, opacity 0.2s ease;
221
+ }
222
+ .tracking-point.flickering {
223
+ z-index: 101;
212
224
  }
213
225
  `}</style>
214
226
  </div>