@srsergio/taptapp-ar 1.0.81 → 1.0.83

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.
@@ -62,15 +62,13 @@ declare class Controller {
62
62
  _detectAndMatch(inputData: any, targetIndexes: number[]): Promise<{
63
63
  targetIndex: any;
64
64
  modelViewTransform: any;
65
+ featurePoints: any[];
65
66
  }>;
66
67
  _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number): Promise<{
67
68
  modelViewTransform: any;
68
- screenCoords: {
69
- x: number;
70
- y: number;
71
- }[];
69
+ screenCoords: any[];
72
70
  reliabilities: number[];
73
- stabilities: any[];
71
+ stabilities: number[];
74
72
  }>;
75
73
  processVideo(input: any): void;
76
74
  stopProcessVideo(): void;
@@ -185,58 +185,80 @@ class Controller {
185
185
  async _detectAndMatch(inputData, targetIndexes) {
186
186
  const { featurePoints } = this.fullDetector.detect(inputData);
187
187
  const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._workerMatch(featurePoints, targetIndexes);
188
- return { targetIndex: matchedTargetIndex, modelViewTransform };
188
+ return { targetIndex: matchedTargetIndex, modelViewTransform, featurePoints };
189
189
  }
190
190
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
191
191
  const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0 } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
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({
@@ -3,6 +3,8 @@ import { compute as hammingCompute } from "./hamming-distance.js";
3
3
  import { computeHoughMatches } from "./hough.js";
4
4
  import { computeHomography } from "./ransacHomography.js";
5
5
  import { multiplyPointHomographyInhomogenous, matrixInverse33 } from "../utils/geometry.js";
6
+ import { FourierEncoder } from "../utils/fourier-encoder.js";
7
+ const encoder = new FourierEncoder(4);
6
8
  const INLIER_THRESHOLD = 5.0; // Tightened from 10 to 5 for better precision
7
9
  const MIN_NUM_INLIERS = 8; // Restored to 8
8
10
  const CLUSTER_MAX_POP = 20;
@@ -94,7 +96,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
94
96
  return { debugExtra };
95
97
  // Second pass with homography guided matching
96
98
  const HInv = matrixInverse33(H, 0.00001);
97
- const dThreshold2 = 100; // 10 * 10
99
+ const dThreshold2 = 400; // 20 * 20 - Expanded search window thanks to Fourier filtering
98
100
  const matches2 = [];
99
101
  const hi00 = HInv[0], hi01 = HInv[1], hi02 = HInv[2];
100
102
  const hi10 = HInv[3], hi11 = HInv[4], hi12 = HInv[5];
@@ -113,14 +115,29 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
113
115
  const col = querypoint.maxima ? kmax : kmin;
114
116
  if (!col)
115
117
  continue;
116
- const cx = col.x, cy = col.y, cd = col.d;
118
+ const cx = col.x, cy = col.y, cd = col.d, cf = col.f;
117
119
  const qDesc = querypoint.descriptors;
120
+ // Fourier encoding of the mapped point (where it SHOULD be in the keyframe)
121
+ const qFourier = encoder.encode(mapX / keyframe.w, mapY / keyframe.h);
118
122
  for (let k = 0, clen = cx.length; k < clen; k++) {
119
123
  const dx = cx[k] - mapX;
120
124
  const dy = cy[k] - mapY;
121
125
  const d2 = dx * dx + dy * dy;
122
126
  if (d2 > dThreshold2)
123
127
  continue;
128
+ // 🚀 MOONSHOT: Fourier Spatial Harmony Check
129
+ // We check if the stored point's Fourier signature matches its predicted position
130
+ let fourierSim = 0;
131
+ if (cf) {
132
+ for (let fidx = 0; fidx < 16; fidx++) {
133
+ fourierSim += (cf[k * 16 + fidx] / 127) * qFourier[fidx];
134
+ }
135
+ }
136
+ else {
137
+ fourierSim = 16; // Backward compatibility
138
+ }
139
+ if (fourierSim < 8)
140
+ continue; // Reject if spatially dissonant (low harmonic match)
124
141
  const d = hammingCompute({ v1: cd, v1Offset: k * descSize, v2: qDesc });
125
142
  if (d < bestD1) {
126
143
  bestD2 = bestD1;
@@ -5,8 +5,10 @@
5
5
  * que NO depende de TensorFlow, eliminando todos los problemas de
6
6
  * inicialización, bloqueos y compatibilidad.
7
7
  */
8
+ import { FourierEncoder } from "./utils/fourier-encoder.js";
8
9
  export declare class OfflineCompiler {
9
10
  data: any;
11
+ fourierEncoder: FourierEncoder;
10
12
  constructor();
11
13
  compileImageTargets(images: any[], progressCallback: (p: number) => void): Promise<any>;
12
14
  _compileTarget(targetImages: any[], progressCallback: (p: number) => void): Promise<{
@@ -117,6 +119,7 @@ export declare class OfflineCompiler {
117
119
  a: Int16Array<ArrayBuffer>;
118
120
  s: Uint8Array<ArrayBuffer>;
119
121
  d: Uint32Array<ArrayBuffer>;
122
+ f: Int8Array<ArrayBuffer>;
120
123
  t: any;
121
124
  };
122
125
  _compactTree(node: any): any;
@@ -9,6 +9,7 @@ import { buildTrackingImageList, buildImageList } from "./image-list.js";
9
9
  import { extractTrackingFeatures } from "./tracker/extract-utils.js";
10
10
  import { DetectorLite } from "./detector/detector-lite.js";
11
11
  import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
12
+ import { FourierEncoder } from "./utils/fourier-encoder.js";
12
13
  import * as msgpack from "@msgpack/msgpack";
13
14
  // Detect environment
14
15
  const isNode = typeof process !== "undefined" &&
@@ -17,6 +18,7 @@ const isNode = typeof process !== "undefined" &&
17
18
  const CURRENT_VERSION = 7; // Protocol v7: Moonshot - 4-bit Packed Tracking Data
18
19
  export class OfflineCompiler {
19
20
  data = null;
21
+ fourierEncoder = new FourierEncoder(4);
20
22
  constructor() {
21
23
  console.log("⚡ OfflineCompiler: Main thread mode (no workers)");
22
24
  }
@@ -186,6 +188,7 @@ export class OfflineCompiler {
186
188
  const angle = new Int16Array(count);
187
189
  const scale = new Uint8Array(count);
188
190
  const descriptors = new Uint32Array(count * 2);
191
+ const fourier = new Int8Array(count * 16); // 4 frequencies * 4 components (sin/cos x/y)
189
192
  for (let i = 0; i < count; i++) {
190
193
  x[i] = Math.round((points[i].x / width) * 65535);
191
194
  y[i] = Math.round((points[i].y / height) * 65535);
@@ -195,6 +198,11 @@ export class OfflineCompiler {
195
198
  descriptors[i * 2] = points[i].descriptors[0];
196
199
  descriptors[(i * 2) + 1] = points[i].descriptors[1];
197
200
  }
201
+ // 🚀 MOONSHOT: Fourier Positional Encoding
202
+ const feat = this.fourierEncoder.encode(points[i].x / width, points[i].y / height);
203
+ for (let j = 0; j < 16; j++) {
204
+ fourier[i * 16 + j] = Math.round(feat[j] * 127);
205
+ }
198
206
  }
199
207
  return {
200
208
  x,
@@ -202,6 +210,7 @@ export class OfflineCompiler {
202
210
  a: angle,
203
211
  s: scale,
204
212
  d: descriptors,
213
+ f: fourier,
205
214
  t: this._compactTree(tree.rootNode),
206
215
  };
207
216
  }
@@ -278,6 +287,9 @@ export class OfflineCompiler {
278
287
  if (col.d instanceof Uint8Array) {
279
288
  col.d = new Uint32Array(col.d.buffer.slice(col.d.byteOffset, col.d.byteOffset + col.d.byteLength));
280
289
  }
290
+ if (col.f instanceof Uint8Array) {
291
+ col.f = new Int8Array(col.f.buffer.slice(col.f.byteOffset, col.f.byteOffset + col.f.byteLength));
292
+ }
281
293
  }
282
294
  }
283
295
  }
@@ -23,6 +23,10 @@ export interface SimpleAROptions {
23
23
  }[];
24
24
  reliabilities?: number[];
25
25
  stabilities?: number[];
26
+ detectionPoints?: {
27
+ x: number;
28
+ y: number;
29
+ }[];
26
30
  }) => void) | null;
27
31
  cameraConfig?: MediaStreamConstraints['video'];
28
32
  debug?: boolean;
@@ -47,6 +51,10 @@ declare class SimpleAR {
47
51
  }[];
48
52
  reliabilities?: number[];
49
53
  stabilities?: number[];
54
+ detectionPoints?: {
55
+ x: number;
56
+ y: number;
57
+ }[];
50
58
  }) => void) | null;
51
59
  cameraConfig: MediaStreamConstraints['video'];
52
60
  debug: boolean;
@@ -113,7 +113,66 @@ class SimpleAR {
113
113
  if (this.debug)
114
114
  this._updateDebugPanel(this.isTracking);
115
115
  }
116
- const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
116
+ const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities, detectionPoints } = 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
+ }
147
+ let projectedDetectionPoints = [];
148
+ if (detectionPoints && detectionPoints.length > 0) {
149
+ const containerRect = this.container.getBoundingClientRect();
150
+ const videoW = this.video.videoWidth;
151
+ const videoH = this.video.videoHeight;
152
+ const isPortrait = containerRect.height > containerRect.width;
153
+ const isVideoLandscape = videoW > videoH;
154
+ const needsRotation = isPortrait && isVideoLandscape;
155
+ const proj = this.controller.projectionTransform;
156
+ const vW = needsRotation ? videoH : videoW;
157
+ const vH = needsRotation ? videoW : videoH;
158
+ const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
159
+ const dW = vW * pScale;
160
+ const dH = vH * pScale;
161
+ const oX = (containerRect.width - dW) / 2;
162
+ const oY = (containerRect.height - dH) / 2;
163
+ projectedDetectionPoints = detectionPoints.map((p) => {
164
+ let sx, sy;
165
+ if (needsRotation) {
166
+ sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
167
+ sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
168
+ }
169
+ else {
170
+ sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
171
+ sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
172
+ }
173
+ return { x: sx, y: sy };
174
+ });
175
+ }
117
176
  if (worldMatrix) {
118
177
  if (!this.isTracking) {
119
178
  this.isTracking = true;
@@ -121,60 +180,26 @@ class SimpleAR {
121
180
  this.onFound && this.onFound({ targetIndex });
122
181
  }
123
182
  this.lastMatrix = worldMatrix;
124
- // We use the matrix from the controller directly (it's already filtered there)
125
183
  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
184
  }
164
185
  else {
165
186
  if (this.isTracking) {
166
187
  this.isTracking = false;
167
188
  this.overlay && (this.overlay.style.opacity = '0');
168
189
  this.onLost && this.onLost({ targetIndex });
169
- this.onUpdateCallback && this.onUpdateCallback({
170
- targetIndex,
171
- worldMatrix: null,
172
- screenCoords: [],
173
- reliabilities: [],
174
- stabilities: []
175
- });
176
190
  }
177
191
  }
192
+ // Always notify the callback if we have points, or if we just lost tracking
193
+ if (projectedPoints.length > 0 || projectedDetectionPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
194
+ this.onUpdateCallback && this.onUpdateCallback({
195
+ targetIndex,
196
+ worldMatrix,
197
+ screenCoords: projectedPoints,
198
+ reliabilities: reliabilities || [],
199
+ stabilities: stabilities || [],
200
+ detectionPoints: projectedDetectionPoints
201
+ });
202
+ }
178
203
  }
179
204
  _positionOverlay(mVT, targetIndex) {
180
205
  if (!this.overlay || !this.markerDimensions[targetIndex])
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 🚀 Moonshot: Fourier Positional Encoding
3
+ *
4
+ * Maps 2D coordinates (x, y) to a high-dimensional frequency space.
5
+ * Used in Transformer Positional Encoding, NeRFs, and modern Generative AI.
6
+ *
7
+ * Theory: gamma(p) = [sin(2^0 * pi * p), cos(2^0 * pi * p), ..., sin(2^L-1 * pi * p), cos(2^L-1 * pi * p)]
8
+ */
9
+ export declare class FourierEncoder {
10
+ private frequencies;
11
+ private L;
12
+ constructor(L?: number);
13
+ /**
14
+ * Encodes a normalized coordinate (0-1) into Fourier features
15
+ * @param x Normalized X
16
+ * @param y Normalized Y
17
+ * @returns Float32Array of size 4 * L
18
+ */
19
+ encode(x: number, y: number): Float32Array;
20
+ /**
21
+ * Fast dot product between two fourier encodings
22
+ * This measures "harmonic spatial similarity"
23
+ */
24
+ static similarity(v1: Float32Array, v2: Float32Array): number;
25
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 🚀 Moonshot: Fourier Positional Encoding
3
+ *
4
+ * Maps 2D coordinates (x, y) to a high-dimensional frequency space.
5
+ * Used in Transformer Positional Encoding, NeRFs, and modern Generative AI.
6
+ *
7
+ * Theory: gamma(p) = [sin(2^0 * pi * p), cos(2^0 * pi * p), ..., sin(2^L-1 * pi * p), cos(2^L-1 * pi * p)]
8
+ */
9
+ export class FourierEncoder {
10
+ frequencies;
11
+ L;
12
+ constructor(L = 4) {
13
+ this.L = L;
14
+ this.frequencies = [];
15
+ for (let i = 0; i < L; i++) {
16
+ this.frequencies.push(Math.pow(2, i) * Math.PI);
17
+ }
18
+ }
19
+ /**
20
+ * Encodes a normalized coordinate (0-1) into Fourier features
21
+ * @param x Normalized X
22
+ * @param y Normalized Y
23
+ * @returns Float32Array of size 4 * L
24
+ */
25
+ encode(x, y) {
26
+ const result = new Float32Array(this.L * 4);
27
+ let idx = 0;
28
+ for (const freq of this.frequencies) {
29
+ result[idx++] = Math.sin(freq * x);
30
+ result[idx++] = Math.cos(freq * x);
31
+ result[idx++] = Math.sin(freq * y);
32
+ result[idx++] = Math.cos(freq * y);
33
+ }
34
+ return result;
35
+ }
36
+ /**
37
+ * Fast dot product between two fourier encodings
38
+ * This measures "harmonic spatial similarity"
39
+ */
40
+ static similarity(v1, v2) {
41
+ let dot = 0;
42
+ for (let i = 0; i < v1.length; i++) {
43
+ dot += v1[i] * v2[i];
44
+ }
45
+ return dot / (v1.length / 2); // Normalize by number of components
46
+ }
47
+ }
@@ -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
  };
@@ -40,7 +40,8 @@ export const useAR = (config) => {
40
40
  overlay: overlayRef.current,
41
41
  scale: config.scale,
42
42
  debug: false,
43
- onUpdate: ({ screenCoords, reliabilities, stabilities }) => {
43
+ onUpdate: (data) => {
44
+ const { screenCoords, reliabilities, stabilities } = data;
44
45
  if (screenCoords && reliabilities && stabilities) {
45
46
  const points = screenCoords.map((p, i) => ({
46
47
  x: p.x,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
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",
@@ -252,7 +252,7 @@ class Controller {
252
252
  featurePoints,
253
253
  targetIndexes,
254
254
  );
255
- return { targetIndex: matchedTargetIndex, modelViewTransform };
255
+ return { targetIndex: matchedTargetIndex, modelViewTransform, featurePoints };
256
256
  }
257
257
 
258
258
  async _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number) {
@@ -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
 
@@ -3,6 +3,9 @@ import { compute as hammingCompute } from "./hamming-distance.js";
3
3
  import { computeHoughMatches } from "./hough.js";
4
4
  import { computeHomography } from "./ransacHomography.js";
5
5
  import { multiplyPointHomographyInhomogenous, matrixInverse33 } from "../utils/geometry.js";
6
+ import { FourierEncoder } from "../utils/fourier-encoder.js";
7
+
8
+ const encoder = new FourierEncoder(4);
6
9
 
7
10
  const INLIER_THRESHOLD = 5.0; // Tightened from 10 to 5 for better precision
8
11
  const MIN_NUM_INLIERS = 8; // Restored to 8
@@ -108,7 +111,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
108
111
 
109
112
  // Second pass with homography guided matching
110
113
  const HInv = matrixInverse33(H, 0.00001);
111
- const dThreshold2 = 100; // 10 * 10
114
+ const dThreshold2 = 400; // 20 * 20 - Expanded search window thanks to Fourier filtering
112
115
  const matches2 = [];
113
116
 
114
117
  const hi00 = HInv[0], hi01 = HInv[1], hi02 = HInv[2];
@@ -132,9 +135,12 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
132
135
  const col = querypoint.maxima ? kmax : kmin;
133
136
  if (!col) continue;
134
137
 
135
- const cx = col.x, cy = col.y, cd = col.d;
138
+ const cx = col.x, cy = col.y, cd = col.d, cf = col.f;
136
139
  const qDesc = querypoint.descriptors;
137
140
 
141
+ // Fourier encoding of the mapped point (where it SHOULD be in the keyframe)
142
+ const qFourier = encoder.encode(mapX / keyframe.w, mapY / keyframe.h);
143
+
138
144
  for (let k = 0, clen = cx.length; k < clen; k++) {
139
145
  const dx = cx[k] - mapX;
140
146
  const dy = cy[k] - mapY;
@@ -142,6 +148,19 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
142
148
 
143
149
  if (d2 > dThreshold2) continue;
144
150
 
151
+ // 🚀 MOONSHOT: Fourier Spatial Harmony Check
152
+ // We check if the stored point's Fourier signature matches its predicted position
153
+ let fourierSim = 0;
154
+ if (cf) {
155
+ for (let fidx = 0; fidx < 16; fidx++) {
156
+ fourierSim += (cf[k * 16 + fidx] / 127) * qFourier[fidx];
157
+ }
158
+ } else {
159
+ fourierSim = 16; // Backward compatibility
160
+ }
161
+
162
+ if (fourierSim < 8) continue; // Reject if spatially dissonant (low harmonic match)
163
+
145
164
  const d = hammingCompute({ v1: cd, v1Offset: k * descSize, v2: qDesc });
146
165
 
147
166
  if (d < bestD1) {
@@ -10,6 +10,7 @@ import { buildTrackingImageList, buildImageList } from "./image-list.js";
10
10
  import { extractTrackingFeatures } from "./tracker/extract-utils.js";
11
11
  import { DetectorLite } from "./detector/detector-lite.js";
12
12
  import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
13
+ import { FourierEncoder } from "./utils/fourier-encoder.js";
13
14
  import * as msgpack from "@msgpack/msgpack";
14
15
 
15
16
  // Detect environment
@@ -21,6 +22,7 @@ const CURRENT_VERSION = 7; // Protocol v7: Moonshot - 4-bit Packed Tracking Data
21
22
 
22
23
  export class OfflineCompiler {
23
24
  data: any = null;
25
+ fourierEncoder = new FourierEncoder(4);
24
26
 
25
27
  constructor() {
26
28
  console.log("⚡ OfflineCompiler: Main thread mode (no workers)");
@@ -230,6 +232,7 @@ export class OfflineCompiler {
230
232
  const angle = new Int16Array(count);
231
233
  const scale = new Uint8Array(count);
232
234
  const descriptors = new Uint32Array(count * 2);
235
+ const fourier = new Int8Array(count * 16); // 4 frequencies * 4 components (sin/cos x/y)
233
236
 
234
237
  for (let i = 0; i < count; i++) {
235
238
  x[i] = Math.round((points[i].x / width) * 65535);
@@ -241,6 +244,12 @@ export class OfflineCompiler {
241
244
  descriptors[i * 2] = points[i].descriptors[0];
242
245
  descriptors[(i * 2) + 1] = points[i].descriptors[1];
243
246
  }
247
+
248
+ // 🚀 MOONSHOT: Fourier Positional Encoding
249
+ const feat = this.fourierEncoder.encode(points[i].x / width, points[i].y / height);
250
+ for (let j = 0; j < 16; j++) {
251
+ fourier[i * 16 + j] = Math.round(feat[j] * 127);
252
+ }
244
253
  }
245
254
 
246
255
  return {
@@ -249,6 +258,7 @@ export class OfflineCompiler {
249
258
  a: angle,
250
259
  s: scale,
251
260
  d: descriptors,
261
+ f: fourier,
252
262
  t: this._compactTree(tree.rootNode),
253
263
  };
254
264
  }
@@ -337,6 +347,9 @@ export class OfflineCompiler {
337
347
  if (col.d instanceof Uint8Array) {
338
348
  col.d = new Uint32Array(col.d.buffer.slice(col.d.byteOffset, col.d.byteOffset + col.d.byteLength));
339
349
  }
350
+ if (col.f instanceof Uint8Array) {
351
+ col.f = new Int8Array(col.f.buffer.slice(col.f.byteOffset, col.f.byteOffset + col.f.byteLength));
352
+ }
340
353
  }
341
354
  }
342
355
  }
@@ -18,7 +18,8 @@ export interface SimpleAROptions {
18
18
  worldMatrix: number[],
19
19
  screenCoords?: { x: number, y: number }[],
20
20
  reliabilities?: number[],
21
- stabilities?: number[]
21
+ stabilities?: number[],
22
+ detectionPoints?: { x: number, y: number }[]
22
23
  }) => void) | null;
23
24
  cameraConfig?: MediaStreamConstraints['video'];
24
25
  debug?: boolean;
@@ -36,7 +37,8 @@ class SimpleAR {
36
37
  worldMatrix: number[],
37
38
  screenCoords?: { x: number, y: number }[],
38
39
  reliabilities?: number[],
39
- stabilities?: number[]
40
+ stabilities?: number[],
41
+ detectionPoints?: { x: number, y: number }[]
40
42
  }) => void) | null;
41
43
  cameraConfig: MediaStreamConstraints['video'];
42
44
  debug: boolean;
@@ -163,7 +165,70 @@ class SimpleAR {
163
165
  if (this.debug) this._updateDebugPanel(this.isTracking);
164
166
  }
165
167
 
166
- const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
168
+ const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities, detectionPoints } = data;
169
+
170
+ // Project points to screen coordinates
171
+ let projectedPoints = [];
172
+ if (screenCoords && screenCoords.length > 0) {
173
+ const containerRect = this.container.getBoundingClientRect();
174
+ const videoW = this.video!.videoWidth;
175
+ const videoH = this.video!.videoHeight;
176
+ const isPortrait = containerRect.height > containerRect.width;
177
+ const isVideoLandscape = videoW > videoH;
178
+ const needsRotation = isPortrait && isVideoLandscape;
179
+ const proj = this.controller!.projectionTransform;
180
+
181
+ const vW = needsRotation ? videoH : videoW;
182
+ const vH = needsRotation ? videoW : videoH;
183
+ const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
184
+ const dW = vW * pScale;
185
+ const dH = vH * pScale;
186
+ const oX = (containerRect.width - dW) / 2;
187
+ const oY = (containerRect.height - dH) / 2;
188
+
189
+ projectedPoints = screenCoords.map((p: any) => {
190
+ let sx, sy;
191
+ if (needsRotation) {
192
+ sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
193
+ sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
194
+ } else {
195
+ sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
196
+ sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
197
+ }
198
+ return { x: sx, y: sy };
199
+ });
200
+ }
201
+
202
+ let projectedDetectionPoints = [];
203
+ if (detectionPoints && detectionPoints.length > 0) {
204
+ const containerRect = this.container.getBoundingClientRect();
205
+ const videoW = this.video!.videoWidth;
206
+ const videoH = this.video!.videoHeight;
207
+ const isPortrait = containerRect.height > containerRect.width;
208
+ const isVideoLandscape = videoW > videoH;
209
+ const needsRotation = isPortrait && isVideoLandscape;
210
+ const proj = this.controller!.projectionTransform;
211
+
212
+ const vW = needsRotation ? videoH : videoW;
213
+ const vH = needsRotation ? videoW : videoH;
214
+ const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
215
+ const dW = vW * pScale;
216
+ const dH = vH * pScale;
217
+ const oX = (containerRect.width - dW) / 2;
218
+ const oY = (containerRect.height - dH) / 2;
219
+
220
+ projectedDetectionPoints = detectionPoints.map((p: any) => {
221
+ let sx, sy;
222
+ if (needsRotation) {
223
+ sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
224
+ sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
225
+ } else {
226
+ sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
227
+ sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
228
+ }
229
+ return { x: sx, y: sy };
230
+ });
231
+ }
167
232
 
168
233
  if (worldMatrix) {
169
234
  if (!this.isTracking) {
@@ -173,63 +238,25 @@ class SimpleAR {
173
238
  }
174
239
 
175
240
  this.lastMatrix = worldMatrix;
176
-
177
- // We use the matrix from the controller directly (it's already filtered there)
178
241
  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
- });
242
+ } else {
243
+ if (this.isTracking) {
244
+ this.isTracking = false;
245
+ this.overlay && (this.overlay.style.opacity = '0');
246
+ this.onLost && this.onLost({ targetIndex });
210
247
  }
248
+ }
211
249
 
250
+ // Always notify the callback if we have points, or if we just lost tracking
251
+ if (projectedPoints.length > 0 || projectedDetectionPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
212
252
  this.onUpdateCallback && this.onUpdateCallback({
213
253
  targetIndex,
214
254
  worldMatrix,
215
255
  screenCoords: projectedPoints,
216
- reliabilities,
217
- stabilities
256
+ reliabilities: reliabilities || [],
257
+ stabilities: stabilities || [],
258
+ detectionPoints: projectedDetectionPoints
218
259
  });
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
260
  }
234
261
  }
235
262
 
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 🚀 Moonshot: Fourier Positional Encoding
3
+ *
4
+ * Maps 2D coordinates (x, y) to a high-dimensional frequency space.
5
+ * Used in Transformer Positional Encoding, NeRFs, and modern Generative AI.
6
+ *
7
+ * Theory: gamma(p) = [sin(2^0 * pi * p), cos(2^0 * pi * p), ..., sin(2^L-1 * pi * p), cos(2^L-1 * pi * p)]
8
+ */
9
+
10
+ export class FourierEncoder {
11
+ private frequencies: number[];
12
+ private L: number;
13
+
14
+ constructor(L: number = 4) {
15
+ this.L = L;
16
+ this.frequencies = [];
17
+ for (let i = 0; i < L; i++) {
18
+ this.frequencies.push(Math.pow(2, i) * Math.PI);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Encodes a normalized coordinate (0-1) into Fourier features
24
+ * @param x Normalized X
25
+ * @param y Normalized Y
26
+ * @returns Float32Array of size 4 * L
27
+ */
28
+ encode(x: number, y: number): Float32Array {
29
+ const result = new Float32Array(this.L * 4);
30
+ let idx = 0;
31
+
32
+ for (const freq of this.frequencies) {
33
+ result[idx++] = Math.sin(freq * x);
34
+ result[idx++] = Math.cos(freq * x);
35
+ result[idx++] = Math.sin(freq * y);
36
+ result[idx++] = Math.cos(freq * y);
37
+ }
38
+
39
+ return result;
40
+ }
41
+
42
+ /**
43
+ * Fast dot product between two fourier encodings
44
+ * This measures "harmonic spatial similarity"
45
+ */
46
+ static similarity(v1: Float32Array, v2: Float32Array): number {
47
+ let dot = 0;
48
+ for (let i = 0; i < v1.length; i++) {
49
+ dot += v1[i] * v2[i];
50
+ }
51
+ return dot / (v1.length / 2); // Normalize by number of components
52
+ }
53
+ }
@@ -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>
@@ -62,9 +62,10 @@ export const useAR = (config: ARConfig): UseARReturn => {
62
62
  overlay: overlayRef.current!,
63
63
  scale: config.scale,
64
64
  debug: false,
65
- onUpdate: ({ screenCoords, reliabilities, stabilities }) => {
65
+ onUpdate: (data: any) => {
66
+ const { screenCoords, reliabilities, stabilities } = data;
66
67
  if (screenCoords && reliabilities && stabilities) {
67
- const points = screenCoords.map((p, i) => ({
68
+ const points = screenCoords.map((p: any, i: number) => ({
68
69
  x: p.x,
69
70
  y: p.y,
70
71
  reliability: reliabilities[i],