@srsergio/taptapp-ar 1.0.73 → 1.0.75

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.
@@ -18,7 +18,7 @@ const getControllerWorker = async () => {
18
18
  }
19
19
  };
20
20
  ControllerWorker = await getControllerWorker();
21
- const DEFAULT_FILTER_CUTOFF = 1.0;
21
+ const DEFAULT_FILTER_CUTOFF = 0.5;
22
22
  const DEFAULT_FILTER_BETA = 0.1;
23
23
  const DEFAULT_WARMUP_TOLERANCE = 8;
24
24
  const DEFAULT_MISS_TOLERANCE = 2;
@@ -22,6 +22,19 @@ export class CropDetector {
22
22
  projectedImage?: undefined;
23
23
  };
24
24
  };
25
+ /**
26
+ * Scans the ENTIRE frame by downsampling it to cropSize
27
+ */
28
+ _detectGlobal(imageData: any): {
29
+ featurePoints: any[];
30
+ debugExtra: {
31
+ projectedImage: number[];
32
+ isGlobal: boolean;
33
+ } | {
34
+ projectedImage?: undefined;
35
+ isGlobal?: undefined;
36
+ };
37
+ };
25
38
  _detect(imageData: any, startX: any, startY: any): {
26
39
  featurePoints: any[];
27
40
  debugExtra: {
@@ -24,27 +24,51 @@ class CropDetector {
24
24
  }
25
25
  detectMoving(input) {
26
26
  const imageData = input;
27
- // Expanded to 5x5 grid (25 positions) for better coverage
27
+ // 🚀 MOONSHOT: Alternate between local crops and GLOBAL scan
28
+ // This solves the "not reading the whole screen" issue.
29
+ // Every 3 frames, we do a full screen downsampled scan.
30
+ if (this.lastRandomIndex % 3 === 0) {
31
+ this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
32
+ return this._detectGlobal(imageData);
33
+ }
34
+ // Original moving crop logic for high-detail local detection
28
35
  const gridSize = 5;
29
- const dx = this.lastRandomIndex % gridSize;
30
- const dy = Math.floor(this.lastRandomIndex / gridSize);
31
- // Calculate offset from center, with overlap for better detection
36
+ const idx = (this.lastRandomIndex - 1) % (gridSize * gridSize);
37
+ const dx = idx % gridSize;
38
+ const dy = Math.floor(idx / gridSize);
32
39
  const stepX = this.cropSize / 3;
33
40
  const stepY = this.cropSize / 3;
34
41
  let startY = Math.floor(this.height / 2 - this.cropSize / 2 + (dy - 2) * stepY);
35
42
  let startX = Math.floor(this.width / 2 - this.cropSize / 2 + (dx - 2) * stepX);
36
- // Clamp to valid bounds
37
- if (startX < 0)
38
- startX = 0;
39
- if (startY < 0)
40
- startY = 0;
41
- if (startX >= this.width - this.cropSize)
42
- startX = this.width - this.cropSize - 1;
43
- if (startY >= this.height - this.cropSize)
44
- startY = this.height - this.cropSize - 1;
45
- this.lastRandomIndex = (this.lastRandomIndex + 1) % (gridSize * gridSize);
46
- const result = this._detect(imageData, startX, startY);
47
- return result;
43
+ startX = Math.max(0, Math.min(this.width - this.cropSize - 1, startX));
44
+ startY = Math.max(0, Math.min(this.height - this.cropSize - 1, startY));
45
+ this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
46
+ return this._detect(imageData, startX, startY);
47
+ }
48
+ /**
49
+ * Scans the ENTIRE frame by downsampling it to cropSize
50
+ */
51
+ _detectGlobal(imageData) {
52
+ const croppedData = new Float32Array(this.cropSize * this.cropSize);
53
+ const scaleX = this.width / this.cropSize;
54
+ const scaleY = this.height / this.cropSize;
55
+ // Fast downsample (nearest neighbor is enough for initial feature detection)
56
+ for (let y = 0; y < this.cropSize; y++) {
57
+ const srcY = Math.floor(y * scaleY) * this.width;
58
+ const dstY = y * this.cropSize;
59
+ for (let x = 0; x < this.cropSize; x++) {
60
+ croppedData[dstY + x] = imageData[srcY + Math.floor(x * scaleX)];
61
+ }
62
+ }
63
+ const { featurePoints } = this.detector.detect(croppedData);
64
+ featurePoints.forEach((p) => {
65
+ p.x *= scaleX;
66
+ p.y *= scaleY;
67
+ });
68
+ return {
69
+ featurePoints,
70
+ debugExtra: this.debugMode ? { projectedImage: Array.from(croppedData), isGlobal: true } : {}
71
+ };
48
72
  }
49
73
  _detect(imageData, startX, startY) {
50
74
  // Crop manually since imageData is now a flat array (width * height)
@@ -123,7 +123,7 @@ class SimpleAR {
123
123
  }
124
124
  this.lastMatrix = worldMatrix;
125
125
  if (!this.filters[targetIndex]) {
126
- this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 1.0, beta: 0.1 });
126
+ this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 0.8, beta: 0.2 });
127
127
  }
128
128
  const flatMVT = [
129
129
  modelViewTransform[0][0], modelViewTransform[0][1], modelViewTransform[0][2], modelViewTransform[0][3],
@@ -1,7 +1,7 @@
1
1
  import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
2
2
  const AR2_DEFAULT_TS = 6;
3
3
  const AR2_DEFAULT_TS_GAP = 1;
4
- const AR2_SEARCH_SIZE = 18;
4
+ const AR2_SEARCH_SIZE = 34; // Increased from 18 to 34 for much better fast-motion tracking
5
5
  const AR2_SEARCH_GAP = 1;
6
6
  const AR2_SIM_THRESH = 0.6;
7
7
  const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
@@ -165,18 +165,18 @@ class Tracker {
165
165
  matchingPoints.push([bestX, bestY]);
166
166
  continue;
167
167
  }
168
- // Search in projected image
169
- for (let sy = -searchOneSize; sy <= searchOneSize; sy += searchGap) {
168
+ // 🚀 MOONSHOT: Coarse-to-Fine Search for MAXIMUM FPS
169
+ // Step 1: Coarse search (Gap 4)
170
+ const coarseGap = 4;
171
+ for (let sy = -searchOneSize; sy <= searchOneSize; sy += coarseGap) {
170
172
  const cy = sCenterY + sy;
171
173
  if (cy < templateOneSize || cy >= markerHeight - templateOneSize)
172
174
  continue;
173
- for (let sx = -searchOneSize; sx <= searchOneSize; sx += searchGap) {
175
+ for (let sx = -searchOneSize; sx <= searchOneSize; sx += coarseGap) {
174
176
  const cx = sCenterX + sx;
175
177
  if (cx < templateOneSize || cx >= markerWidth - templateOneSize)
176
178
  continue;
177
- let sumI = 0;
178
- let sumI2 = 0;
179
- let sumIT = 0;
179
+ let sumI = 0, sumI2 = 0, sumIT = 0;
180
180
  for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
181
181
  const rowOffset = (cy + ty) * markerWidth;
182
182
  const tRowOffset = (ty + templateOneSize) * templateSize;
@@ -199,6 +199,43 @@ class Tracker {
199
199
  }
200
200
  }
201
201
  }
202
+ // Step 2: Fine refinement (Gap 1) only around the best coarse match
203
+ if (bestSim > AR2_SIM_THRESH) {
204
+ const fineCenterX = (bestX * scale) | 0;
205
+ const fineCenterY = (bestY * scale) | 0;
206
+ const fineSearch = coarseGap;
207
+ for (let sy = -fineSearch; sy <= fineSearch; sy++) {
208
+ const cy = fineCenterY + sy;
209
+ if (cy < templateOneSize || cy >= markerHeight - templateOneSize)
210
+ continue;
211
+ for (let sx = -fineSearch; sx <= fineSearch; sx++) {
212
+ const cx = fineCenterX + sx;
213
+ if (cx < templateOneSize || cx >= markerWidth - templateOneSize)
214
+ continue;
215
+ let sumI = 0, sumI2 = 0, sumIT = 0;
216
+ for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
217
+ const rowOffset = (cy + ty) * markerWidth;
218
+ const tRowOffset = (ty + templateOneSize) * templateSize;
219
+ for (let tx = -templateOneSize; tx <= templateOneSize; tx++) {
220
+ const valI = projectedImage[rowOffset + (cx + tx)];
221
+ const valT = templateData[tRowOffset + (tx + templateOneSize)];
222
+ sumI += valI;
223
+ sumI2 += valI * valI;
224
+ sumIT += valI * valT;
225
+ }
226
+ }
227
+ const varI = Math.sqrt(Math.max(0, sumI2 - (sumI * sumI) * oneOverNPixels));
228
+ if (varI < 0.0001)
229
+ continue;
230
+ const sim = (sumIT - (sumI * sumT) * oneOverNPixels) / (varI * varT);
231
+ if (sim > bestSim) {
232
+ bestSim = sim;
233
+ bestX = cx / scale;
234
+ bestY = cy / scale;
235
+ }
236
+ }
237
+ }
238
+ }
202
239
  sims[f] = bestSim;
203
240
  matchingPoints.push([bestX, bestY]);
204
241
  }
@@ -92,6 +92,9 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
92
92
  display: block;
93
93
  width: 100%;
94
94
  height: auto;
95
+ will-change: transform;
96
+ /* Visual interpolation to smooth out frame gaps */
97
+ transition: transform 0.1s linear, opacity 0.3s ease;
95
98
  }
96
99
  ` })] }));
97
100
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@ const getControllerWorker = async () => {
19
19
  };
20
20
  ControllerWorker = await getControllerWorker();
21
21
 
22
- const DEFAULT_FILTER_CUTOFF = 1.0;
22
+ const DEFAULT_FILTER_CUTOFF = 0.5;
23
23
  const DEFAULT_FILTER_BETA = 0.1;
24
24
  const DEFAULT_WARMUP_TOLERANCE = 8;
25
25
  const DEFAULT_MISS_TOLERANCE = 2;
@@ -33,28 +33,62 @@ class CropDetector {
33
33
  detectMoving(input) {
34
34
  const imageData = input;
35
35
 
36
- // Expanded to 5x5 grid (25 positions) for better coverage
36
+ // 🚀 MOONSHOT: Alternate between local crops and GLOBAL scan
37
+ // This solves the "not reading the whole screen" issue.
38
+ // Every 3 frames, we do a full screen downsampled scan.
39
+ if (this.lastRandomIndex % 3 === 0) {
40
+ this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
41
+ return this._detectGlobal(imageData);
42
+ }
43
+
44
+ // Original moving crop logic for high-detail local detection
37
45
  const gridSize = 5;
38
- const dx = this.lastRandomIndex % gridSize;
39
- const dy = Math.floor(this.lastRandomIndex / gridSize);
46
+ const idx = (this.lastRandomIndex - 1) % (gridSize * gridSize);
47
+ const dx = idx % gridSize;
48
+ const dy = Math.floor(idx / gridSize);
40
49
 
41
- // Calculate offset from center, with overlap for better detection
42
50
  const stepX = this.cropSize / 3;
43
51
  const stepY = this.cropSize / 3;
44
52
 
45
53
  let startY = Math.floor(this.height / 2 - this.cropSize / 2 + (dy - 2) * stepY);
46
54
  let startX = Math.floor(this.width / 2 - this.cropSize / 2 + (dx - 2) * stepX);
47
55
 
48
- // Clamp to valid bounds
49
- if (startX < 0) startX = 0;
50
- if (startY < 0) startY = 0;
51
- if (startX >= this.width - this.cropSize) startX = this.width - this.cropSize - 1;
52
- if (startY >= this.height - this.cropSize) startY = this.height - this.cropSize - 1;
56
+ startX = Math.max(0, Math.min(this.width - this.cropSize - 1, startX));
57
+ startY = Math.max(0, Math.min(this.height - this.cropSize - 1, startY));
53
58
 
54
- this.lastRandomIndex = (this.lastRandomIndex + 1) % (gridSize * gridSize);
59
+ this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
55
60
 
56
- const result = this._detect(imageData, startX, startY);
57
- return result;
61
+ return this._detect(imageData, startX, startY);
62
+ }
63
+
64
+ /**
65
+ * Scans the ENTIRE frame by downsampling it to cropSize
66
+ */
67
+ _detectGlobal(imageData) {
68
+ const croppedData = new Float32Array(this.cropSize * this.cropSize);
69
+ const scaleX = this.width / this.cropSize;
70
+ const scaleY = this.height / this.cropSize;
71
+
72
+ // Fast downsample (nearest neighbor is enough for initial feature detection)
73
+ for (let y = 0; y < this.cropSize; y++) {
74
+ const srcY = Math.floor(y * scaleY) * this.width;
75
+ const dstY = y * this.cropSize;
76
+ for (let x = 0; x < this.cropSize; x++) {
77
+ croppedData[dstY + x] = imageData[srcY + Math.floor(x * scaleX)];
78
+ }
79
+ }
80
+
81
+ const { featurePoints } = this.detector.detect(croppedData);
82
+
83
+ featurePoints.forEach((p) => {
84
+ p.x *= scaleX;
85
+ p.y *= scaleY;
86
+ });
87
+
88
+ return {
89
+ featurePoints,
90
+ debugExtra: this.debugMode ? { projectedImage: Array.from(croppedData), isGlobal: true } : {}
91
+ };
58
92
  }
59
93
 
60
94
  _detect(imageData, startX, startY) {
@@ -163,7 +163,7 @@ class SimpleAR {
163
163
  this.lastMatrix = worldMatrix;
164
164
 
165
165
  if (!this.filters[targetIndex]) {
166
- this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 1.0, beta: 0.1 });
166
+ this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 0.8, beta: 0.2 });
167
167
  }
168
168
 
169
169
  const flatMVT = [
@@ -2,7 +2,7 @@ import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../es
2
2
 
3
3
  const AR2_DEFAULT_TS = 6;
4
4
  const AR2_DEFAULT_TS_GAP = 1;
5
- const AR2_SEARCH_SIZE = 18;
5
+ const AR2_SEARCH_SIZE = 34; // Increased from 18 to 34 for much better fast-motion tracking
6
6
  const AR2_SEARCH_GAP = 1;
7
7
  const AR2_SIM_THRESH = 0.6;
8
8
 
@@ -214,29 +214,25 @@ class Tracker {
214
214
  continue;
215
215
  }
216
216
 
217
- // Search in projected image
218
- for (let sy = -searchOneSize; sy <= searchOneSize; sy += searchGap) {
217
+ // 🚀 MOONSHOT: Coarse-to-Fine Search for MAXIMUM FPS
218
+ // Step 1: Coarse search (Gap 4)
219
+ const coarseGap = 4;
220
+ for (let sy = -searchOneSize; sy <= searchOneSize; sy += coarseGap) {
219
221
  const cy = sCenterY + sy;
220
222
  if (cy < templateOneSize || cy >= markerHeight - templateOneSize) continue;
221
223
 
222
- for (let sx = -searchOneSize; sx <= searchOneSize; sx += searchGap) {
224
+ for (let sx = -searchOneSize; sx <= searchOneSize; sx += coarseGap) {
223
225
  const cx = sCenterX + sx;
224
226
  if (cx < templateOneSize || cx >= markerWidth - templateOneSize) continue;
225
227
 
226
- let sumI = 0;
227
- let sumI2 = 0;
228
- let sumIT = 0;
229
-
228
+ let sumI = 0, sumI2 = 0, sumIT = 0;
230
229
  for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
231
230
  const rowOffset = (cy + ty) * markerWidth;
232
231
  const tRowOffset = (ty + templateOneSize) * templateSize;
233
232
  for (let tx = -templateOneSize; tx <= templateOneSize; tx++) {
234
233
  const valI = projectedImage[rowOffset + (cx + tx)];
235
234
  const valT = templateData[tRowOffset + (tx + templateOneSize)];
236
-
237
- sumI += valI;
238
- sumI2 += valI * valI;
239
- sumIT += valI * valT;
235
+ sumI += valI; sumI2 += valI * valI; sumIT += valI * valT;
240
236
  }
241
237
  }
242
238
 
@@ -252,6 +248,41 @@ class Tracker {
252
248
  }
253
249
  }
254
250
 
251
+ // Step 2: Fine refinement (Gap 1) only around the best coarse match
252
+ if (bestSim > AR2_SIM_THRESH) {
253
+ const fineCenterX = (bestX * scale) | 0;
254
+ const fineCenterY = (bestY * scale) | 0;
255
+ const fineSearch = coarseGap;
256
+
257
+ for (let sy = -fineSearch; sy <= fineSearch; sy++) {
258
+ const cy = fineCenterY + sy;
259
+ if (cy < templateOneSize || cy >= markerHeight - templateOneSize) continue;
260
+ for (let sx = -fineSearch; sx <= fineSearch; sx++) {
261
+ const cx = fineCenterX + sx;
262
+ if (cx < templateOneSize || cx >= markerWidth - templateOneSize) continue;
263
+
264
+ let sumI = 0, sumI2 = 0, sumIT = 0;
265
+ for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
266
+ const rowOffset = (cy + ty) * markerWidth;
267
+ const tRowOffset = (ty + templateOneSize) * templateSize;
268
+ for (let tx = -templateOneSize; tx <= templateOneSize; tx++) {
269
+ const valI = projectedImage[rowOffset + (cx + tx)];
270
+ const valT = templateData[tRowOffset + (tx + templateOneSize)];
271
+ sumI += valI; sumI2 += valI * valI; sumIT += valI * valT;
272
+ }
273
+ }
274
+ const varI = Math.sqrt(Math.max(0, sumI2 - (sumI * sumI) * oneOverNPixels));
275
+ if (varI < 0.0001) continue;
276
+ const sim = (sumIT - (sumI * sumT) * oneOverNPixels) / (varI * varT);
277
+ if (sim > bestSim) {
278
+ bestSim = sim;
279
+ bestX = cx / scale;
280
+ bestY = cy / scale;
281
+ }
282
+ }
283
+ }
284
+ }
285
+
255
286
  sims[f] = bestSim;
256
287
  matchingPoints.push([bestX, bestY]);
257
288
  }
@@ -169,6 +169,9 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
169
169
  display: block;
170
170
  width: 100%;
171
171
  height: auto;
172
+ will-change: transform;
173
+ /* Visual interpolation to smooth out frame gaps */
174
+ transition: transform 0.1s linear, opacity 0.3s ease;
172
175
  }
173
176
  `}</style>
174
177
  </div>