@srsergio/taptapp-ar 1.0.80 → 1.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler/controller.d.ts +2 -5
- package/dist/compiler/controller.js +74 -48
- package/dist/compiler/simple-ar.js +40 -45
- package/dist/react/TaptappAR.js +19 -8
- package/package.json +3 -2
- package/src/compiler/controller.ts +76 -49
- package/src/compiler/simple-ar.ts +42 -49
- package/src/react/TaptappAR.tsx +28 -16
|
@@ -65,12 +65,9 @@ declare class Controller {
|
|
|
65
65
|
}>;
|
|
66
66
|
_trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number): Promise<{
|
|
67
67
|
modelViewTransform: any;
|
|
68
|
-
screenCoords:
|
|
69
|
-
x: number;
|
|
70
|
-
y: number;
|
|
71
|
-
}[];
|
|
68
|
+
screenCoords: any[];
|
|
72
69
|
reliabilities: number[];
|
|
73
|
-
stabilities:
|
|
70
|
+
stabilities: number[];
|
|
74
71
|
}>;
|
|
75
72
|
processVideo(input: any): void;
|
|
76
73
|
stopProcessVideo(): void;
|
|
@@ -192,51 +192,73 @@ class Controller {
|
|
|
192
192
|
const state = this.trackingStates[targetIndex];
|
|
193
193
|
if (!state.pointStabilities)
|
|
194
194
|
state.pointStabilities = [];
|
|
195
|
+
if (!state.lastScreenCoords)
|
|
196
|
+
state.lastScreenCoords = [];
|
|
195
197
|
if (!state.pointStabilities[octaveIndex]) {
|
|
196
|
-
// Initialize stabilities for this octave if not exists
|
|
197
198
|
const numPoints = this.tracker.prebuiltData[targetIndex][octaveIndex].px.length;
|
|
198
|
-
state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0
|
|
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({
|
|
@@ -114,6 +114,36 @@ class SimpleAR {
|
|
|
114
114
|
this._updateDebugPanel(this.isTracking);
|
|
115
115
|
}
|
|
116
116
|
const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
|
|
117
|
+
// Project points to screen coordinates
|
|
118
|
+
let projectedPoints = [];
|
|
119
|
+
if (screenCoords && screenCoords.length > 0) {
|
|
120
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
121
|
+
const videoW = this.video.videoWidth;
|
|
122
|
+
const videoH = this.video.videoHeight;
|
|
123
|
+
const isPortrait = containerRect.height > containerRect.width;
|
|
124
|
+
const isVideoLandscape = videoW > videoH;
|
|
125
|
+
const needsRotation = isPortrait && isVideoLandscape;
|
|
126
|
+
const proj = this.controller.projectionTransform;
|
|
127
|
+
const vW = needsRotation ? videoH : videoW;
|
|
128
|
+
const vH = needsRotation ? videoW : videoH;
|
|
129
|
+
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
130
|
+
const dW = vW * pScale;
|
|
131
|
+
const dH = vH * pScale;
|
|
132
|
+
const oX = (containerRect.width - dW) / 2;
|
|
133
|
+
const oY = (containerRect.height - dH) / 2;
|
|
134
|
+
projectedPoints = screenCoords.map((p) => {
|
|
135
|
+
let sx, sy;
|
|
136
|
+
if (needsRotation) {
|
|
137
|
+
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
138
|
+
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
142
|
+
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
143
|
+
}
|
|
144
|
+
return { x: sx, y: sy };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
117
147
|
if (worldMatrix) {
|
|
118
148
|
if (!this.isTracking) {
|
|
119
149
|
this.isTracking = true;
|
|
@@ -121,60 +151,25 @@ class SimpleAR {
|
|
|
121
151
|
this.onFound && this.onFound({ targetIndex });
|
|
122
152
|
}
|
|
123
153
|
this.lastMatrix = worldMatrix;
|
|
124
|
-
// We use the matrix from the controller directly (it's already filtered there)
|
|
125
154
|
this._positionOverlay(modelViewTransform, targetIndex);
|
|
126
|
-
// Project points to screen coordinates
|
|
127
|
-
let projectedPoints = [];
|
|
128
|
-
if (screenCoords && screenCoords.length > 0) {
|
|
129
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
130
|
-
const videoW = this.video.videoWidth;
|
|
131
|
-
const videoH = this.video.videoHeight;
|
|
132
|
-
const isPortrait = containerRect.height > containerRect.width;
|
|
133
|
-
const isVideoLandscape = videoW > videoH;
|
|
134
|
-
const needsRotation = isPortrait && isVideoLandscape;
|
|
135
|
-
const proj = this.controller.projectionTransform;
|
|
136
|
-
const vW = needsRotation ? videoH : videoW;
|
|
137
|
-
const vH = needsRotation ? videoW : videoH;
|
|
138
|
-
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
139
|
-
const dW = vW * pScale;
|
|
140
|
-
const dH = vH * pScale;
|
|
141
|
-
const oX = (containerRect.width - dW) / 2;
|
|
142
|
-
const oY = (containerRect.height - dH) / 2;
|
|
143
|
-
projectedPoints = screenCoords.map((p) => {
|
|
144
|
-
let sx, sy;
|
|
145
|
-
if (needsRotation) {
|
|
146
|
-
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
147
|
-
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
151
|
-
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
152
|
-
}
|
|
153
|
-
return { x: sx, y: sy };
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
this.onUpdateCallback && this.onUpdateCallback({
|
|
157
|
-
targetIndex,
|
|
158
|
-
worldMatrix,
|
|
159
|
-
screenCoords: projectedPoints,
|
|
160
|
-
reliabilities,
|
|
161
|
-
stabilities
|
|
162
|
-
});
|
|
163
155
|
}
|
|
164
156
|
else {
|
|
165
157
|
if (this.isTracking) {
|
|
166
158
|
this.isTracking = false;
|
|
167
159
|
this.overlay && (this.overlay.style.opacity = '0');
|
|
168
160
|
this.onLost && this.onLost({ targetIndex });
|
|
169
|
-
this.onUpdateCallback && this.onUpdateCallback({
|
|
170
|
-
targetIndex,
|
|
171
|
-
worldMatrix: null,
|
|
172
|
-
screenCoords: [],
|
|
173
|
-
reliabilities: [],
|
|
174
|
-
stabilities: []
|
|
175
|
-
});
|
|
176
161
|
}
|
|
177
162
|
}
|
|
163
|
+
// Always notify the callback if we have points, or if we just lost tracking
|
|
164
|
+
if (projectedPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
|
|
165
|
+
this.onUpdateCallback && this.onUpdateCallback({
|
|
166
|
+
targetIndex,
|
|
167
|
+
worldMatrix,
|
|
168
|
+
screenCoords: projectedPoints,
|
|
169
|
+
reliabilities: reliabilities || [],
|
|
170
|
+
stabilities: stabilities || []
|
|
171
|
+
});
|
|
172
|
+
}
|
|
178
173
|
}
|
|
179
174
|
_positionOverlay(mVT, targetIndex) {
|
|
180
175
|
if (!this.overlay || !this.markerDimensions[targetIndex])
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.82",
|
|
4
4
|
"description": "Ultra-fast Augmented Reality (AR) SDK for Node.js and Browser. Image tracking with 100% pure JavaScript, zero-dependencies, and high-performance compilation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"augmented reality",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsc",
|
|
53
53
|
"prepublishOnly": "npm run build",
|
|
54
|
-
"test": "vitest"
|
|
54
|
+
"test": "vitest",
|
|
55
|
+
"test:react": "vite --port 4321 --open tests/react-test.html"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"aframe": ">=1.5.0",
|
|
@@ -264,58 +264,80 @@ class Controller {
|
|
|
264
264
|
|
|
265
265
|
const state = this.trackingStates[targetIndex];
|
|
266
266
|
if (!state.pointStabilities) state.pointStabilities = [];
|
|
267
|
+
if (!state.lastScreenCoords) state.lastScreenCoords = [];
|
|
268
|
+
|
|
267
269
|
if (!state.pointStabilities[octaveIndex]) {
|
|
268
|
-
// Initialize stabilities for this octave if not exists
|
|
269
270
|
const numPoints = (this.tracker as any).prebuiltData[targetIndex][octaveIndex].px.length;
|
|
270
|
-
state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0
|
|
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
|
|
|
@@ -165,6 +165,38 @@ class SimpleAR {
|
|
|
165
165
|
|
|
166
166
|
const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
|
|
167
167
|
|
|
168
|
+
// Project points to screen coordinates
|
|
169
|
+
let projectedPoints = [];
|
|
170
|
+
if (screenCoords && screenCoords.length > 0) {
|
|
171
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
172
|
+
const videoW = this.video!.videoWidth;
|
|
173
|
+
const videoH = this.video!.videoHeight;
|
|
174
|
+
const isPortrait = containerRect.height > containerRect.width;
|
|
175
|
+
const isVideoLandscape = videoW > videoH;
|
|
176
|
+
const needsRotation = isPortrait && isVideoLandscape;
|
|
177
|
+
const proj = this.controller!.projectionTransform;
|
|
178
|
+
|
|
179
|
+
const vW = needsRotation ? videoH : videoW;
|
|
180
|
+
const vH = needsRotation ? videoW : videoH;
|
|
181
|
+
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
182
|
+
const dW = vW * pScale;
|
|
183
|
+
const dH = vH * pScale;
|
|
184
|
+
const oX = (containerRect.width - dW) / 2;
|
|
185
|
+
const oY = (containerRect.height - dH) / 2;
|
|
186
|
+
|
|
187
|
+
projectedPoints = screenCoords.map((p: any) => {
|
|
188
|
+
let sx, sy;
|
|
189
|
+
if (needsRotation) {
|
|
190
|
+
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
191
|
+
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
192
|
+
} else {
|
|
193
|
+
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
194
|
+
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
195
|
+
}
|
|
196
|
+
return { x: sx, y: sy };
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
168
200
|
if (worldMatrix) {
|
|
169
201
|
if (!this.isTracking) {
|
|
170
202
|
this.isTracking = true;
|
|
@@ -173,63 +205,24 @@ class SimpleAR {
|
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
this.lastMatrix = worldMatrix;
|
|
176
|
-
|
|
177
|
-
// We use the matrix from the controller directly (it's already filtered there)
|
|
178
208
|
this._positionOverlay(modelViewTransform, targetIndex);
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
});
|
|
209
|
+
} else {
|
|
210
|
+
if (this.isTracking) {
|
|
211
|
+
this.isTracking = false;
|
|
212
|
+
this.overlay && (this.overlay.style.opacity = '0');
|
|
213
|
+
this.onLost && this.onLost({ targetIndex });
|
|
210
214
|
}
|
|
215
|
+
}
|
|
211
216
|
|
|
217
|
+
// Always notify the callback if we have points, or if we just lost tracking
|
|
218
|
+
if (projectedPoints.length > 0 || (worldMatrix === null && data.type === 'updateMatrix')) {
|
|
212
219
|
this.onUpdateCallback && this.onUpdateCallback({
|
|
213
220
|
targetIndex,
|
|
214
221
|
worldMatrix,
|
|
215
222
|
screenCoords: projectedPoints,
|
|
216
|
-
reliabilities,
|
|
217
|
-
stabilities
|
|
223
|
+
reliabilities: reliabilities || [],
|
|
224
|
+
stabilities: stabilities || []
|
|
218
225
|
});
|
|
219
|
-
|
|
220
|
-
} else {
|
|
221
|
-
if (this.isTracking) {
|
|
222
|
-
this.isTracking = false;
|
|
223
|
-
this.overlay && (this.overlay.style.opacity = '0');
|
|
224
|
-
this.onLost && this.onLost({ targetIndex });
|
|
225
|
-
this.onUpdateCallback && this.onUpdateCallback({
|
|
226
|
-
targetIndex,
|
|
227
|
-
worldMatrix: null as any,
|
|
228
|
-
screenCoords: [],
|
|
229
|
-
reliabilities: [],
|
|
230
|
-
stabilities: []
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
226
|
}
|
|
234
227
|
}
|
|
235
228
|
|
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>
|