@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.
- package/dist/compiler/controller.d.ts +3 -5
- package/dist/compiler/controller.js +75 -49
- package/dist/compiler/matching/matching.js +19 -2
- package/dist/compiler/offline-compiler.d.ts +3 -0
- package/dist/compiler/offline-compiler.js +12 -0
- package/dist/compiler/simple-ar.d.ts +8 -0
- package/dist/compiler/simple-ar.js +71 -46
- package/dist/compiler/utils/fourier-encoder.d.ts +25 -0
- package/dist/compiler/utils/fourier-encoder.js +47 -0
- package/dist/react/TaptappAR.js +19 -8
- package/dist/react/use-ar.js +2 -1
- package/package.json +1 -1
- package/src/compiler/controller.ts +77 -50
- package/src/compiler/matching/matching.js +21 -2
- package/src/compiler/offline-compiler.ts +13 -0
- package/src/compiler/simple-ar.ts +79 -52
- package/src/compiler/utils/fourier-encoder.ts +53 -0
- package/src/react/TaptappAR.tsx +28 -16
- package/src/react/use-ar.ts +3 -2
|
@@ -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:
|
|
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
|
|
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
|
|
202
|
-
// Update
|
|
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
|
|
205
|
-
if (
|
|
206
|
-
|
|
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.
|
|
213
|
+
stabilities[i] = Math.max(0.0, stabilities[i] - 0.08); // Slow decay (approx 12 frames/0.2s)
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
|
-
// Collect
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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:
|
|
232
|
-
screenCoords:
|
|
233
|
-
|
|
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:
|
|
238
|
-
reliabilities:
|
|
239
|
-
stabilities:
|
|
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
|
-
|
|
286
|
-
trackingState.
|
|
287
|
-
trackingState.
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 =
|
|
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
|
+
}
|
package/dist/react/TaptappAR.js
CHANGED
|
@@ -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" })) }),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
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/dist/react/use-ar.js
CHANGED
|
@@ -40,7 +40,8 @@ export const useAR = (config) => {
|
|
|
40
40
|
overlay: overlayRef.current,
|
|
41
41
|
scale: config.scale,
|
|
42
42
|
debug: false,
|
|
43
|
-
onUpdate: (
|
|
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.
|
|
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
|
|
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
|
|
276
|
+
const lastCoords = state.lastScreenCoords[octaveIndex];
|
|
275
277
|
|
|
276
|
-
// Update
|
|
278
|
+
// Update stability for ALL points in the current octave
|
|
277
279
|
for (let i = 0; i < stabilities.length; i++) {
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
|
|
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.
|
|
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
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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:
|
|
301
|
-
|
|
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 ||
|
|
305
|
-
return {
|
|
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:
|
|
310
|
-
screenCoords:
|
|
311
|
-
|
|
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:
|
|
317
|
-
reliabilities:
|
|
318
|
-
stabilities:
|
|
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
|
-
|
|
375
|
-
trackingState.
|
|
376
|
-
trackingState.
|
|
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
|
|
395
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
429
|
+
const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
|
|
430
|
+
trackingState.trackingMatrix = filteredMatrix;
|
|
405
431
|
|
|
406
|
-
|
|
432
|
+
finalMatrix = [...filteredMatrix];
|
|
407
433
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 =
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
package/src/react/TaptappAR.tsx
CHANGED
|
@@ -90,21 +90,29 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
|
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
92
|
{/* Tracking Points Layer */}
|
|
93
|
-
{
|
|
94
|
-
<div className="taptapp-ar-points-overlay">
|
|
95
|
-
{trackedPoints
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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);
|
|
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>
|
package/src/react/use-ar.ts
CHANGED
|
@@ -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: (
|
|
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],
|