@srsergio/taptapp-ar 1.0.79 → 1.0.80
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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/compiler/controller.d.ts +22 -6
- package/dist/compiler/controller.js +99 -26
- package/dist/compiler/controller.worker.js +2 -1
- package/dist/compiler/estimation/refine-estimate.d.ts +2 -1
- package/dist/compiler/estimation/refine-estimate.js +18 -5
- package/dist/compiler/features/feature-base.d.ts +1 -1
- package/dist/compiler/features/feature-manager.d.ts +1 -1
- package/dist/compiler/features/feature-manager.js +2 -2
- package/dist/compiler/features/one-euro-filter-feature.d.ts +1 -1
- package/dist/compiler/features/one-euro-filter-feature.js +8 -1
- package/dist/compiler/matching/matching.js +1 -1
- package/dist/compiler/simple-ar.d.ts +12 -0
- package/dist/compiler/simple-ar.js +46 -19
- package/dist/compiler/tracker/tracker.d.ts +10 -0
- package/dist/compiler/tracker/tracker.js +6 -4
- package/dist/react/TaptappAR.js +26 -2
- package/dist/react/use-ar.d.ts +7 -0
- package/dist/react/use-ar.js +15 -1
- package/package.json +24 -2
- package/src/compiler/controller.ts +112 -26
- package/src/compiler/controller.worker.js +2 -1
- package/src/compiler/estimation/refine-estimate.js +20 -3
- package/src/compiler/features/feature-base.ts +1 -1
- package/src/compiler/features/feature-manager.ts +2 -2
- package/src/compiler/features/one-euro-filter-feature.ts +11 -1
- package/src/compiler/matching/matching.js +1 -1
- package/src/compiler/simple-ar.ts +62 -20
- package/src/compiler/tracker/tracker.js +7 -4
- package/src/react/TaptappAR.tsx +38 -1
- package/src/react/use-ar.ts +23 -1
|
@@ -13,7 +13,13 @@ export interface SimpleAROptions {
|
|
|
13
13
|
scale?: number;
|
|
14
14
|
onFound?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
15
15
|
onLost?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
16
|
-
onUpdate?: ((data: {
|
|
16
|
+
onUpdate?: ((data: {
|
|
17
|
+
targetIndex: number,
|
|
18
|
+
worldMatrix: number[],
|
|
19
|
+
screenCoords?: { x: number, y: number }[],
|
|
20
|
+
reliabilities?: number[],
|
|
21
|
+
stabilities?: number[]
|
|
22
|
+
}) => void) | null;
|
|
17
23
|
cameraConfig?: MediaStreamConstraints['video'];
|
|
18
24
|
debug?: boolean;
|
|
19
25
|
}
|
|
@@ -25,7 +31,13 @@ class SimpleAR {
|
|
|
25
31
|
scaleMultiplier: number;
|
|
26
32
|
onFound: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
27
33
|
onLost: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
28
|
-
onUpdateCallback: ((data: {
|
|
34
|
+
onUpdateCallback: ((data: {
|
|
35
|
+
targetIndex: number,
|
|
36
|
+
worldMatrix: number[],
|
|
37
|
+
screenCoords?: { x: number, y: number }[],
|
|
38
|
+
reliabilities?: number[],
|
|
39
|
+
stabilities?: number[]
|
|
40
|
+
}) => void) | null;
|
|
29
41
|
cameraConfig: MediaStreamConstraints['video'];
|
|
30
42
|
debug: boolean;
|
|
31
43
|
|
|
@@ -151,7 +163,7 @@ class SimpleAR {
|
|
|
151
163
|
if (this.debug) this._updateDebugPanel(this.isTracking);
|
|
152
164
|
}
|
|
153
165
|
|
|
154
|
-
const { targetIndex, worldMatrix, modelViewTransform } = data;
|
|
166
|
+
const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
|
|
155
167
|
|
|
156
168
|
if (worldMatrix) {
|
|
157
169
|
if (!this.isTracking) {
|
|
@@ -162,31 +174,61 @@ class SimpleAR {
|
|
|
162
174
|
|
|
163
175
|
this.lastMatrix = worldMatrix;
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
// We use the matrix from the controller directly (it's already filtered there)
|
|
178
|
+
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
|
+
});
|
|
167
210
|
}
|
|
168
211
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
[smoothedFlat[0], smoothedFlat[1], smoothedFlat[2], smoothedFlat[3]],
|
|
177
|
-
[smoothedFlat[4], smoothedFlat[5], smoothedFlat[6], smoothedFlat[7]],
|
|
178
|
-
[smoothedFlat[8], smoothedFlat[9], smoothedFlat[10], smoothedFlat[11]]
|
|
179
|
-
];
|
|
180
|
-
|
|
181
|
-
this._positionOverlay(smoothedMVT, targetIndex);
|
|
182
|
-
this.onUpdateCallback && this.onUpdateCallback({ targetIndex, worldMatrix });
|
|
212
|
+
this.onUpdateCallback && this.onUpdateCallback({
|
|
213
|
+
targetIndex,
|
|
214
|
+
worldMatrix,
|
|
215
|
+
screenCoords: projectedPoints,
|
|
216
|
+
reliabilities,
|
|
217
|
+
stabilities
|
|
218
|
+
});
|
|
183
219
|
|
|
184
220
|
} else {
|
|
185
221
|
if (this.isTracking) {
|
|
186
222
|
this.isTracking = false;
|
|
187
|
-
if (this.filters[targetIndex]) this.filters[targetIndex].reset();
|
|
188
223
|
this.overlay && (this.overlay.style.opacity = '0');
|
|
189
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
|
+
});
|
|
190
232
|
}
|
|
191
233
|
}
|
|
192
234
|
}
|
|
@@ -2,9 +2,9 @@ import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../es
|
|
|
2
2
|
|
|
3
3
|
const AR2_DEFAULT_TS = 6;
|
|
4
4
|
const AR2_DEFAULT_TS_GAP = 1;
|
|
5
|
-
const AR2_SEARCH_SIZE =
|
|
5
|
+
const AR2_SEARCH_SIZE = 25; // Reduced from 34 to 25 to prevent background latching
|
|
6
6
|
const AR2_SEARCH_GAP = 1;
|
|
7
|
-
const AR2_SIM_THRESH = 0.6
|
|
7
|
+
const AR2_SIM_THRESH = 0.65; // Increased from 0.6 to reduce false positives
|
|
8
8
|
|
|
9
9
|
const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
|
|
10
10
|
|
|
@@ -119,6 +119,8 @@ class Tracker {
|
|
|
119
119
|
|
|
120
120
|
const { px, py, s: scale } = trackingFrame;
|
|
121
121
|
|
|
122
|
+
const reliabilities = [];
|
|
123
|
+
|
|
122
124
|
for (let i = 0; i < matchingPoints.length; i++) {
|
|
123
125
|
if (sim[i] > AR2_SIM_THRESH && i < px.length) {
|
|
124
126
|
goodTrack.push(i);
|
|
@@ -133,6 +135,7 @@ class Tracker {
|
|
|
133
135
|
y: py[i] / scale,
|
|
134
136
|
z: 0,
|
|
135
137
|
});
|
|
138
|
+
reliabilities.push(sim[i]);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -147,7 +150,7 @@ class Tracker {
|
|
|
147
150
|
|
|
148
151
|
// If the points cover too little space compared to the screen size of the marker, it's a glitch
|
|
149
152
|
if (detectedDiagonal < screenW * 0.15) {
|
|
150
|
-
return { worldCoords: [], screenCoords: [], debugExtra };
|
|
153
|
+
return { worldCoords: [], screenCoords: [], reliabilities: [], debugExtra };
|
|
151
154
|
}
|
|
152
155
|
}
|
|
153
156
|
if (this.debugMode) {
|
|
@@ -160,7 +163,7 @@ class Tracker {
|
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
return { worldCoords, screenCoords, debugExtra };
|
|
166
|
+
return { worldCoords, screenCoords, reliabilities, indices: goodTrack, octaveIndex, debugExtra };
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/**
|
package/src/react/TaptappAR.tsx
CHANGED
|
@@ -15,7 +15,7 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
|
|
|
15
15
|
showScanningOverlay = true,
|
|
16
16
|
showErrorOverlay = true
|
|
17
17
|
}) => {
|
|
18
|
-
const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
|
|
18
|
+
const { containerRef, overlayRef, status, toggleVideo, trackedPoints } = useAR(config);
|
|
19
19
|
|
|
20
20
|
// Simple heuristic to determine if it's a video or image
|
|
21
21
|
// based on the presence of videoSrc and common extensions
|
|
@@ -89,6 +89,25 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
|
|
|
89
89
|
)}
|
|
90
90
|
</div>
|
|
91
91
|
|
|
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
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
92
111
|
<style>{`
|
|
93
112
|
.taptapp-ar-wrapper {
|
|
94
113
|
background: #000;
|
|
@@ -173,6 +192,24 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
|
|
|
173
192
|
pointer-events: none;
|
|
174
193
|
transition: opacity 0.3s ease;
|
|
175
194
|
}
|
|
195
|
+
.taptapp-ar-points-overlay {
|
|
196
|
+
position: absolute;
|
|
197
|
+
top: 0;
|
|
198
|
+
left: 0;
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 100%;
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
z-index: 100; /* High z-index to be above overlay */
|
|
203
|
+
}
|
|
204
|
+
.tracking-point {
|
|
205
|
+
position: absolute;
|
|
206
|
+
background: black;
|
|
207
|
+
border: 1px solid rgba(255,255,255,0.5); /* Better contrast */
|
|
208
|
+
border-radius: 50%;
|
|
209
|
+
transform: translate(-50%, -50%);
|
|
210
|
+
box-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
}
|
|
176
213
|
`}</style>
|
|
177
214
|
</div>
|
|
178
215
|
);
|
package/src/react/use-ar.ts
CHANGED
|
@@ -4,12 +4,20 @@ import type { SimpleAR as SimpleARType } from "../compiler/simple-ar.js";
|
|
|
4
4
|
|
|
5
5
|
export type ARStatus = "scanning" | "tracking" | "error";
|
|
6
6
|
|
|
7
|
+
export interface TrackedPoint {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
reliability: number;
|
|
11
|
+
stability: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export interface UseARReturn {
|
|
8
15
|
containerRef: React.RefObject<HTMLDivElement>;
|
|
9
16
|
overlayRef: React.RefObject<HTMLVideoElement | HTMLImageElement>;
|
|
10
17
|
status: ARStatus;
|
|
11
18
|
isPlaying: boolean;
|
|
12
19
|
toggleVideo: () => Promise<void>;
|
|
20
|
+
trackedPoints: TrackedPoint[];
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export const useAR = (config: ARConfig): UseARReturn => {
|
|
@@ -17,6 +25,7 @@ export const useAR = (config: ARConfig): UseARReturn => {
|
|
|
17
25
|
const overlayRef = useRef<HTMLVideoElement | HTMLImageElement>(null);
|
|
18
26
|
const [status, setStatus] = useState<ARStatus>("scanning");
|
|
19
27
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
28
|
+
const [trackedPoints, setTrackedPoints] = useState<TrackedPoint[]>([]);
|
|
20
29
|
const arInstanceRef = useRef<SimpleARType | null>(null);
|
|
21
30
|
|
|
22
31
|
const toggleVideo = useCallback(async () => {
|
|
@@ -53,6 +62,17 @@ export const useAR = (config: ARConfig): UseARReturn => {
|
|
|
53
62
|
overlay: overlayRef.current!,
|
|
54
63
|
scale: config.scale,
|
|
55
64
|
debug: false,
|
|
65
|
+
onUpdate: ({ screenCoords, reliabilities, stabilities }) => {
|
|
66
|
+
if (screenCoords && reliabilities && stabilities) {
|
|
67
|
+
const points = screenCoords.map((p, i) => ({
|
|
68
|
+
x: p.x,
|
|
69
|
+
y: p.y,
|
|
70
|
+
reliability: reliabilities[i],
|
|
71
|
+
stability: stabilities[i]
|
|
72
|
+
}));
|
|
73
|
+
setTrackedPoints(points);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
56
76
|
onFound: async ({ targetIndex }) => {
|
|
57
77
|
console.log(`🎯 Target ${targetIndex} detected!`);
|
|
58
78
|
if (!isMounted) return;
|
|
@@ -72,6 +92,7 @@ export const useAR = (config: ARConfig): UseARReturn => {
|
|
|
72
92
|
console.log(`👋 Target ${targetIndex} lost`);
|
|
73
93
|
if (!isMounted) return;
|
|
74
94
|
setStatus("scanning");
|
|
95
|
+
setTrackedPoints([]);
|
|
75
96
|
|
|
76
97
|
const overlay = overlayRef.current;
|
|
77
98
|
if (overlay instanceof HTMLVideoElement) {
|
|
@@ -105,6 +126,7 @@ export const useAR = (config: ARConfig): UseARReturn => {
|
|
|
105
126
|
overlayRef,
|
|
106
127
|
status,
|
|
107
128
|
isPlaying,
|
|
108
|
-
toggleVideo
|
|
129
|
+
toggleVideo,
|
|
130
|
+
trackedPoints
|
|
109
131
|
};
|
|
110
132
|
};
|