@srsergio/taptapp-ar 1.0.34 → 1.0.36

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.
@@ -16,7 +16,7 @@ catch (e) {
16
16
  const DEFAULT_FILTER_CUTOFF = 0.1; // Menor cutoff para filtrar más ruidos cuando está quieto
17
17
  const DEFAULT_FILTER_BETA = 0.01; // Beta bajo para suavizar movimientos rápidos
18
18
  const DEFAULT_WARMUP_TOLERANCE = 8; // Más frames de calentamiento para asegurar estabilidad inicial
19
- const DEFAULT_MISS_TOLERANCE = 5;
19
+ const DEFAULT_MISS_TOLERANCE = 2; // Reducido para que el objeto desaparezca más rápido tras pérdida
20
20
  class Controller {
21
21
  constructor({ inputWidth, inputHeight, onUpdate = null, debugMode = false, maxTrack = 1, warmupTolerance = null, missTolerance = null, filterMinCF = null, filterBeta = null, worker = null, // Allow custom worker injection
22
22
  }) {
@@ -182,8 +182,8 @@ class Controller {
182
182
  }
183
183
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
184
184
  const { worldCoords, screenCoords } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
185
- if (worldCoords.length < 4)
186
- return null;
185
+ if (worldCoords.length < 6)
186
+ return null; // Umbral de puntos mínimos para mantener el seguimiento
187
187
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
188
188
  worldCoords,
189
189
  screenCoords,
@@ -217,9 +217,9 @@ export class OfflineCompiler {
217
217
  }
218
218
  const dataList = this.data.map((item) => {
219
219
  const matchingData = item.matchingData.map((kf) => this._packKeyframe(kf));
220
- const trackingData = [item.trackingData[0]].map((td) => {
220
+ const trackingData = item.trackingData.map((td) => {
221
221
  const count = td.points.length;
222
- // Step 1: Packed Coords - Normalize width/height to 16-bit
222
+ // Packed Coords - Float32 for now as in current import logic
223
223
  const px = new Float32Array(count);
224
224
  const py = new Float32Array(count);
225
225
  for (let i = 0; i < count; i++) {
@@ -26,7 +26,7 @@ export class SimpleAR {
26
26
  * @param {((data: {targetIndex: number, worldMatrix: number[]}) => void) | null} [options.onUpdate]
27
27
  * @param {Object} [options.cameraConfig]
28
28
  */
29
- constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, }: {
29
+ constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, debug, }: {
30
30
  container: HTMLElement;
31
31
  targetSrc: string | string[];
32
32
  overlay: HTMLElement;
@@ -58,6 +58,11 @@ export class SimpleAR {
58
58
  worldMatrix: number[];
59
59
  }) => void) | null;
60
60
  cameraConfig: Object;
61
+ debug: any;
62
+ lastTime: number;
63
+ frameCount: number;
64
+ fps: number;
65
+ debugPanel: HTMLDivElement | null;
61
66
  video: HTMLVideoElement | null;
62
67
  controller: Controller | null;
63
68
  isTracking: boolean;
@@ -77,5 +82,7 @@ export class SimpleAR {
77
82
  _initController(): void;
78
83
  _handleUpdate(data: any): void;
79
84
  _positionOverlay(mVT: any, targetIndex: any): void;
85
+ _createDebugPanel(): void;
86
+ _updateDebugPanel(isTracking: any): void;
80
87
  }
81
88
  import { Controller } from "./controller.js";
@@ -30,7 +30,7 @@ class SimpleAR {
30
30
  * @param {Object} [options.cameraConfig]
31
31
  */
32
32
  constructor({ container, targetSrc, overlay, scale = 1.0, // Multiplicador de escala personalizado
33
- onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, }) {
33
+ onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, debug = false, }) {
34
34
  this.container = container;
35
35
  this.targetSrc = targetSrc;
36
36
  this.overlay = overlay;
@@ -39,6 +39,13 @@ class SimpleAR {
39
39
  this.onLost = onLost;
40
40
  this.onUpdateCallback = onUpdate;
41
41
  this.cameraConfig = cameraConfig;
42
+ this.debug = debug;
43
+ if (this.debug)
44
+ window.AR_DEBUG = true;
45
+ this.lastTime = performance.now();
46
+ this.frameCount = 0;
47
+ this.fps = 0;
48
+ this.debugPanel = null;
42
49
  this.video = null;
43
50
  this.controller = null;
44
51
  this.isTracking = false;
@@ -55,6 +62,8 @@ class SimpleAR {
55
62
  await this._startCamera();
56
63
  // 3. Initialize controller
57
64
  this._initController();
65
+ if (this.debug)
66
+ this._createDebugPanel();
58
67
  // 4. Load targets (supports single URL or array of URLs)
59
68
  const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
60
69
  const result = await this.controller.addImageTargets(targets);
@@ -114,12 +123,23 @@ class SimpleAR {
114
123
  this.controller = new Controller({
115
124
  inputWidth: this.video.videoWidth,
116
125
  inputHeight: this.video.videoHeight,
126
+ debugMode: this.debug,
117
127
  onUpdate: (data) => this._handleUpdate(data)
118
128
  });
119
129
  }
120
130
  _handleUpdate(data) {
121
131
  if (data.type !== 'updateMatrix')
122
132
  return;
133
+ // FPS Calculation
134
+ const now = performance.now();
135
+ this.frameCount++;
136
+ if (now - this.lastTime >= 1000) {
137
+ this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
138
+ this.frameCount = 0;
139
+ this.lastTime = now;
140
+ if (this.debug)
141
+ this._updateDebugPanel(this.isTracking);
142
+ }
123
143
  const { targetIndex, worldMatrix, modelViewTransform } = data;
124
144
  if (worldMatrix) {
125
145
  // Target found
@@ -170,50 +190,104 @@ class SimpleAR {
170
190
  const isPortrait = containerRect.height > containerRect.width;
171
191
  const isVideoLandscape = videoW > videoH;
172
192
  const needsRotation = isPortrait && isVideoLandscape;
173
- // 2. Get intrinsic projection from controller
193
+ // 3. Get intrinsic projection from controller
174
194
  const proj = this.controller.projectionTransform;
175
- // 3. Project 3 points to determine position, scale, and rotation
176
- // Points in Marker Space: Center, Right-Edge, and Down-Edge
177
- const pMid = projectToScreen(markerW / 2, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
178
- const pRight = projectToScreen(markerW / 2 + 100, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
179
- // 4. Calculate Screen Position
180
- const screenX = pMid.sx;
181
- const screenY = pMid.sy;
182
- // 5. Calculate Rotation and Scale from the projected X-axis vector
183
- const dx = pRight.sx - pMid.sx;
184
- const dy = pRight.sy - pMid.sy;
185
- const rotation = Math.atan2(dy, dx);
186
- const pixelDistance100 = Math.sqrt(dx * dx + dy * dy);
187
- // Since we projected 100 units, the scale for the whole markerW is:
188
- const finalScale = (pixelDistance100 / 100) * this.scaleMultiplier;
189
- // DEBUG LOGS
190
- if (window.AR_DEBUG) {
191
- console.log('--- AR POSITION DEBUG (Point Projection) ---');
192
- console.log('Container:', containerRect.width.toFixed(0), 'x', containerRect.height.toFixed(0));
193
- console.log('Video:', videoW, 'x', videoH, 'needsRotation:', needsRotation);
194
- console.log('Screen Pos:', screenX.toFixed(1), screenY.toFixed(1));
195
- console.log('Rotated Angle:', (rotation * 180 / Math.PI).toFixed(1), 'deg');
196
- console.log('Final Scale:', finalScale.toFixed(4));
197
- }
198
- // Apply styles to prevent CSS interference (like max-width: 100%)
195
+ // 3. Project 4 corners to determine a full 3D perspective (homography)
196
+ const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
197
+ const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
198
+ const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
199
+ const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
200
+ // Helper to solve for 2D Homography (maps 0..1 square to pUL, pUR, pLL, pLR)
201
+ const solveHomography = (w, h, p1, p2, p3, p4) => {
202
+ const x1 = p1.sx, y1 = p1.sy;
203
+ const x2 = p2.sx, y2 = p2.sy;
204
+ const x3 = p3.sx, y3 = p3.sy;
205
+ const x4 = p4.sx, y4 = p4.sy;
206
+ const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
207
+ const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
208
+ let a, b, c, d, e, f, g, h_coeff;
209
+ if (dx3 === 0 && dy3 === 0) {
210
+ a = x2 - x1;
211
+ b = x3 - x1;
212
+ c = x1;
213
+ d = y2 - y1;
214
+ e = y3 - y1;
215
+ f = y1;
216
+ g = 0;
217
+ h_coeff = 0;
218
+ }
219
+ else {
220
+ const det = dx1 * dy2 - dx2 * dy1;
221
+ g = (dx3 * dy2 - dx2 * dy3) / det;
222
+ h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
223
+ a = x2 - x1 + g * x2;
224
+ b = x3 - x1 + h_coeff * x3;
225
+ c = x1;
226
+ d = y2 - y1 + g * y2;
227
+ e = y3 - y1 + h_coeff * y3;
228
+ f = y1;
229
+ }
230
+ // This maps unit square (0..1) to the quadrilateral.
231
+ // We need to scale it by 1/w and 1/h to map (0..w, 0..h)
232
+ return [
233
+ a / w, d / w, 0, g / w,
234
+ b / h, e / h, 0, h_coeff / h,
235
+ 0, 0, 1, 0,
236
+ c, f, 0, 1
237
+ ];
238
+ };
239
+ const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
240
+ // Apply styles
199
241
  this.overlay.style.maxWidth = 'none';
200
- this.overlay.style.maxHeight = 'none';
201
242
  this.overlay.style.width = `${markerW}px`;
202
- this.overlay.style.height = 'auto'; // Maintain aspect ratio if user has a custom overlay
243
+ this.overlay.style.height = `${markerH}px`;
203
244
  this.overlay.style.position = 'absolute';
204
- this.overlay.style.transformOrigin = 'center center';
205
- this.overlay.style.display = 'block';
206
- this.overlay.style.margin = '0';
245
+ this.overlay.style.transformOrigin = '0 0';
207
246
  this.overlay.style.left = '0';
208
247
  this.overlay.style.top = '0';
209
- // Apply final transform
210
- // We use translate to move the center of the elements to 0,0
211
- // Then apply our calculated screen position
248
+ this.overlay.style.display = 'block';
249
+ // Apply 3D transform with matrix3d
250
+ // We also apply the user's custom scaleMultiplier AFTER the perspective transform
251
+ // but since we want to scale around the marker center, we apply it as a prefix/suffix
252
+ // Scale around top-left (0,0) is easy. Scale around center requires offset.
212
253
  this.overlay.style.transform = `
213
- translate(${screenX}px, ${screenY}px)
214
- translate(-50%, -50%)
215
- rotate(${rotation}rad)
216
- scale(${finalScale})
254
+ matrix3d(${matrix.join(',')})
255
+ translate(${markerW / 2}px, ${markerH / 2}px)
256
+ scale(${this.scaleMultiplier})
257
+ translate(${-markerW / 2}px, ${-markerH / 2}px)
258
+ `;
259
+ }
260
+ // Unified projection logic moved to ./utils/projection.js
261
+ _createDebugPanel() {
262
+ this.debugPanel = document.createElement('div');
263
+ this.debugPanel.style.cssText = `
264
+ position: absolute;
265
+ top: 10px;
266
+ left: 10px;
267
+ background: rgba(0, 0, 0, 0.8);
268
+ color: #0f0;
269
+ font-family: monospace;
270
+ font-size: 12px;
271
+ padding: 8px;
272
+ border-radius: 4px;
273
+ z-index: 99999;
274
+ pointer-events: none;
275
+ line-height: 1.5;
276
+ `;
277
+ this.container.appendChild(this.debugPanel);
278
+ }
279
+ _updateDebugPanel(isTracking) {
280
+ if (!this.debugPanel)
281
+ return;
282
+ const memory = performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) : '?';
283
+ const color = isTracking ? '#0f0' : '#f00';
284
+ const status = isTracking ? 'TRACKING' : 'SEARCHING';
285
+ this.debugPanel.innerHTML = `
286
+ <div>HEAD-UP DISPLAY</div>
287
+ <div>----------------</div>
288
+ <div>FPS: ${this.fps}</div>
289
+ <div>STATUS: <span style="color:${color}">${status}</span></div>
290
+ <div>MEM: ${memory} MB</div>
217
291
  `;
218
292
  }
219
293
  }
@@ -7,15 +7,7 @@ export class Tracker {
7
7
  inputHeight: any;
8
8
  debugMode: boolean;
9
9
  trackingKeyframeList: any[];
10
- prebuiltData: {
11
- px: Float32Array<any>;
12
- py: Float32Array<any>;
13
- data: Uint8Array<any>;
14
- width: any;
15
- height: any;
16
- scale: any;
17
- projectedImage: Float32Array<ArrayBuffer>;
18
- }[];
10
+ prebuiltData: any[];
19
11
  templateBuffer: Float32Array<ArrayBuffer>;
20
12
  dummyRun(inputData: any): void;
21
13
  track(inputData: any, lastModelViewTransform: any, targetIndex: any): {
@@ -1,7 +1,7 @@
1
1
  import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
2
2
  const AR2_DEFAULT_TS = 6;
3
3
  const AR2_DEFAULT_TS_GAP = 1;
4
- const AR2_SEARCH_SIZE = 10;
4
+ const AR2_SEARCH_SIZE = 18;
5
5
  const AR2_SEARCH_GAP = 1;
6
6
  const AR2_SIM_THRESH = 0.6;
7
7
  const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
@@ -13,15 +13,12 @@ class Tracker {
13
13
  this.inputWidth = inputWidth;
14
14
  this.inputHeight = inputHeight;
15
15
  this.debugMode = debugMode;
16
- this.trackingKeyframeList = [];
16
+ this.trackingKeyframeList = []; // All octaves for all targets: [targetIndex][octaveIndex]
17
+ this.prebuiltData = []; // [targetIndex][octaveIndex]
17
18
  for (let i = 0; i < trackingDataList.length; i++) {
18
- this.trackingKeyframeList.push(trackingDataList[i][TRACKING_KEYFRAME]);
19
- }
20
- // Prebuild TypedArrays for features and pixels
21
- this.prebuiltData = [];
22
- for (let i = 0; i < this.trackingKeyframeList.length; i++) {
23
- const keyframe = this.trackingKeyframeList[i];
24
- this.prebuiltData[i] = {
19
+ const targetOctaves = trackingDataList[i];
20
+ this.trackingKeyframeList[i] = targetOctaves;
21
+ this.prebuiltData[i] = targetOctaves.map(keyframe => ({
25
22
  px: new Float32Array(keyframe.px),
26
23
  py: new Float32Array(keyframe.py),
27
24
  data: new Uint8Array(keyframe.d),
@@ -30,7 +27,7 @@ class Tracker {
30
27
  scale: keyframe.s,
31
28
  // Recyclable projected image buffer
32
29
  projectedImage: new Float32Array(keyframe.w * keyframe.h)
33
- };
30
+ }));
34
31
  }
35
32
  // Pre-allocate template data buffer to avoid garbage collection
36
33
  const templateOneSize = AR2_DEFAULT_TS;
@@ -49,14 +46,31 @@ class Tracker {
49
46
  }
50
47
  track(inputData, lastModelViewTransform, targetIndex) {
51
48
  let debugExtra = {};
49
+ // Select the best octave based on current estimated distance/scale
50
+ // We want the octave where the marker size is closest to its projected size on screen
52
51
  const modelViewProjectionTransform = buildModelViewProjectionTransform(this.projectionTransform, lastModelViewTransform);
53
- const prebuilt = this.prebuiltData[targetIndex];
52
+ // Estimate current marker width on screen
53
+ const [mW, mH] = this.markerDimensions[targetIndex];
54
+ const p0 = computeScreenCoordiate(modelViewProjectionTransform, 0, 0);
55
+ const p1 = computeScreenCoordiate(modelViewProjectionTransform, mW, 0);
56
+ const screenW = Math.sqrt((p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2);
57
+ // Select octave whose image width is closest to screenW
58
+ let octaveIndex = 0;
59
+ let minDiff = Infinity;
60
+ for (let i = 0; i < this.prebuiltData[targetIndex].length; i++) {
61
+ const diff = Math.abs(this.prebuiltData[targetIndex][i].width - screenW);
62
+ if (diff < minDiff) {
63
+ minDiff = diff;
64
+ octaveIndex = i;
65
+ }
66
+ }
67
+ const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
54
68
  // 1. Compute Projection (Warping)
55
69
  this._computeProjection(modelViewProjectionTransform, inputData, prebuilt);
56
70
  const projectedImage = prebuilt.projectedImage;
57
71
  // 2. Compute Matching (NCC)
58
72
  const { matchingPoints, sim } = this._computeMatching(prebuilt, projectedImage);
59
- const trackingFrame = this.trackingKeyframeList[targetIndex];
73
+ const trackingFrame = this.trackingKeyframeList[targetIndex][octaveIndex];
60
74
  const worldCoords = [];
61
75
  const screenCoords = [];
62
76
  const goodTrack = [];
@@ -75,6 +89,7 @@ class Tracker {
75
89
  }
76
90
  if (this.debugMode) {
77
91
  debugExtra = {
92
+ octaveIndex,
78
93
  projectedImage: Array.from(projectedImage),
79
94
  matchingPoints,
80
95
  goodTrack,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@ const DEFAULT_FILTER_BETA = 0.01; // Beta bajo para suavizar movimientos rápid
20
20
 
21
21
  const DEFAULT_WARMUP_TOLERANCE = 8; // Más frames de calentamiento para asegurar estabilidad inicial
22
22
 
23
- const DEFAULT_MISS_TOLERANCE = 5;
23
+ const DEFAULT_MISS_TOLERANCE = 2; // Reducido para que el objeto desaparezca más rápido tras pérdida
24
24
 
25
25
  class Controller {
26
26
  constructor({
@@ -231,7 +231,7 @@ class Controller {
231
231
  lastModelViewTransform,
232
232
  targetIndex,
233
233
  );
234
- if (worldCoords.length < 4) return null;
234
+ if (worldCoords.length < 6) return null; // Umbral de puntos mínimos para mantener el seguimiento
235
235
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
236
236
  worldCoords,
237
237
  screenCoords,
@@ -260,9 +260,9 @@ export class OfflineCompiler {
260
260
  const dataList = this.data.map((item) => {
261
261
  const matchingData = item.matchingData.map((kf) => this._packKeyframe(kf));
262
262
 
263
- const trackingData = [item.trackingData[0]].map((td) => {
263
+ const trackingData = item.trackingData.map((td) => {
264
264
  const count = td.points.length;
265
- // Step 1: Packed Coords - Normalize width/height to 16-bit
265
+ // Packed Coords - Float32 for now as in current import logic
266
266
  const px = new Float32Array(count);
267
267
  const py = new Float32Array(count);
268
268
  for (let i = 0; i < count; i++) {
@@ -39,6 +39,7 @@ class SimpleAR {
39
39
  onLost = null,
40
40
  onUpdate = null,
41
41
  cameraConfig = { facingMode: 'environment', width: 1280, height: 720 },
42
+ debug = false,
42
43
  }) {
43
44
  this.container = container;
44
45
  this.targetSrc = targetSrc;
@@ -48,6 +49,13 @@ class SimpleAR {
48
49
  this.onLost = onLost;
49
50
  this.onUpdateCallback = onUpdate;
50
51
  this.cameraConfig = cameraConfig;
52
+ this.debug = debug;
53
+ if (this.debug) window.AR_DEBUG = true;
54
+
55
+ this.lastTime = performance.now();
56
+ this.frameCount = 0;
57
+ this.fps = 0;
58
+ this.debugPanel = null;
51
59
 
52
60
  this.video = null;
53
61
  this.controller = null;
@@ -69,6 +77,8 @@ class SimpleAR {
69
77
  // 3. Initialize controller
70
78
  this._initController();
71
79
 
80
+ if (this.debug) this._createDebugPanel();
81
+
72
82
  // 4. Load targets (supports single URL or array of URLs)
73
83
  const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
74
84
  const result = await this.controller.addImageTargets(targets);
@@ -134,6 +144,7 @@ class SimpleAR {
134
144
  this.controller = new Controller({
135
145
  inputWidth: this.video.videoWidth,
136
146
  inputHeight: this.video.videoHeight,
147
+ debugMode: this.debug,
137
148
  onUpdate: (data) => this._handleUpdate(data)
138
149
  });
139
150
  }
@@ -141,6 +152,16 @@ class SimpleAR {
141
152
  _handleUpdate(data) {
142
153
  if (data.type !== 'updateMatrix') return;
143
154
 
155
+ // FPS Calculation
156
+ const now = performance.now();
157
+ this.frameCount++;
158
+ if (now - this.lastTime >= 1000) {
159
+ this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
160
+ this.frameCount = 0;
161
+ this.lastTime = now;
162
+ if (this.debug) this._updateDebugPanel(this.isTracking);
163
+ }
164
+
144
165
  const { targetIndex, worldMatrix, modelViewTransform } = data;
145
166
 
146
167
  if (worldMatrix) {
@@ -198,62 +219,111 @@ class SimpleAR {
198
219
  const isVideoLandscape = videoW > videoH;
199
220
  const needsRotation = isPortrait && isVideoLandscape;
200
221
 
201
- // 2. Get intrinsic projection from controller
222
+ // 3. Get intrinsic projection from controller
202
223
  const proj = this.controller.projectionTransform;
203
224
 
204
- // 3. Project 3 points to determine position, scale, and rotation
205
- // Points in Marker Space: Center, Right-Edge, and Down-Edge
206
- const pMid = projectToScreen(markerW / 2, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
207
- const pRight = projectToScreen(markerW / 2 + 100, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
208
-
209
- // 4. Calculate Screen Position
210
- const screenX = pMid.sx;
211
- const screenY = pMid.sy;
212
-
213
- // 5. Calculate Rotation and Scale from the projected X-axis vector
214
- const dx = pRight.sx - pMid.sx;
215
- const dy = pRight.sy - pMid.sy;
216
-
217
- const rotation = Math.atan2(dy, dx);
218
- const pixelDistance100 = Math.sqrt(dx * dx + dy * dy);
219
-
220
- // Since we projected 100 units, the scale for the whole markerW is:
221
- const finalScale = (pixelDistance100 / 100) * this.scaleMultiplier;
222
-
223
- // DEBUG LOGS
224
- if (window.AR_DEBUG) {
225
- console.log('--- AR POSITION DEBUG (Point Projection) ---');
226
- console.log('Container:', containerRect.width.toFixed(0), 'x', containerRect.height.toFixed(0));
227
- console.log('Video:', videoW, 'x', videoH, 'needsRotation:', needsRotation);
228
- console.log('Screen Pos:', screenX.toFixed(1), screenY.toFixed(1));
229
- console.log('Rotated Angle:', (rotation * 180 / Math.PI).toFixed(1), 'deg');
230
- console.log('Final Scale:', finalScale.toFixed(4));
231
- }
225
+ // 3. Project 4 corners to determine a full 3D perspective (homography)
226
+ const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
227
+ const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
228
+ const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
229
+ const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
230
+
231
+ // Helper to solve for 2D Homography (maps 0..1 square to pUL, pUR, pLL, pLR)
232
+ const solveHomography = (w, h, p1, p2, p3, p4) => {
233
+ const x1 = p1.sx, y1 = p1.sy;
234
+ const x2 = p2.sx, y2 = p2.sy;
235
+ const x3 = p3.sx, y3 = p3.sy;
236
+ const x4 = p4.sx, y4 = p4.sy;
237
+
238
+ const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
239
+ const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
240
+
241
+ let a, b, c, d, e, f, g, h_coeff;
242
+
243
+ if (dx3 === 0 && dy3 === 0) {
244
+ a = x2 - x1; b = x3 - x1; c = x1;
245
+ d = y2 - y1; e = y3 - y1; f = y1;
246
+ g = 0; h_coeff = 0;
247
+ } else {
248
+ const det = dx1 * dy2 - dx2 * dy1;
249
+ g = (dx3 * dy2 - dx2 * dy3) / det;
250
+ h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
251
+ a = x2 - x1 + g * x2;
252
+ b = x3 - x1 + h_coeff * x3;
253
+ c = x1;
254
+ d = y2 - y1 + g * y2;
255
+ e = y3 - y1 + h_coeff * y3;
256
+ f = y1;
257
+ }
258
+ // This maps unit square (0..1) to the quadrilateral.
259
+ // We need to scale it by 1/w and 1/h to map (0..w, 0..h)
260
+ return [
261
+ a / w, d / w, 0, g / w,
262
+ b / h, e / h, 0, h_coeff / h,
263
+ 0, 0, 1, 0,
264
+ c, f, 0, 1
265
+ ];
266
+ };
267
+
268
+ const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
232
269
 
233
- // Apply styles to prevent CSS interference (like max-width: 100%)
270
+ // Apply styles
234
271
  this.overlay.style.maxWidth = 'none';
235
- this.overlay.style.maxHeight = 'none';
236
272
  this.overlay.style.width = `${markerW}px`;
237
- this.overlay.style.height = 'auto'; // Maintain aspect ratio if user has a custom overlay
273
+ this.overlay.style.height = `${markerH}px`;
238
274
  this.overlay.style.position = 'absolute';
239
- this.overlay.style.transformOrigin = 'center center';
240
- this.overlay.style.display = 'block';
241
- this.overlay.style.margin = '0';
275
+ this.overlay.style.transformOrigin = '0 0';
242
276
  this.overlay.style.left = '0';
243
277
  this.overlay.style.top = '0';
278
+ this.overlay.style.display = 'block';
244
279
 
245
- // Apply final transform
246
- // We use translate to move the center of the elements to 0,0
247
- // Then apply our calculated screen position
280
+ // Apply 3D transform with matrix3d
281
+ // We also apply the user's custom scaleMultiplier AFTER the perspective transform
282
+ // but since we want to scale around the marker center, we apply it as a prefix/suffix
283
+ // Scale around top-left (0,0) is easy. Scale around center requires offset.
248
284
  this.overlay.style.transform = `
249
- translate(${screenX}px, ${screenY}px)
250
- translate(-50%, -50%)
251
- rotate(${rotation}rad)
252
- scale(${finalScale})
285
+ matrix3d(${matrix.join(',')})
286
+ translate(${markerW / 2}px, ${markerH / 2}px)
287
+ scale(${this.scaleMultiplier})
288
+ translate(${-markerW / 2}px, ${-markerH / 2}px)
253
289
  `;
254
290
  }
255
291
 
256
292
  // Unified projection logic moved to ./utils/projection.js
293
+
294
+ _createDebugPanel() {
295
+ this.debugPanel = document.createElement('div');
296
+ this.debugPanel.style.cssText = `
297
+ position: absolute;
298
+ top: 10px;
299
+ left: 10px;
300
+ background: rgba(0, 0, 0, 0.8);
301
+ color: #0f0;
302
+ font-family: monospace;
303
+ font-size: 12px;
304
+ padding: 8px;
305
+ border-radius: 4px;
306
+ z-index: 99999;
307
+ pointer-events: none;
308
+ line-height: 1.5;
309
+ `;
310
+ this.container.appendChild(this.debugPanel);
311
+ }
312
+
313
+ _updateDebugPanel(isTracking) {
314
+ if (!this.debugPanel) return;
315
+ const memory = performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) : '?';
316
+ const color = isTracking ? '#0f0' : '#f00';
317
+ const status = isTracking ? 'TRACKING' : 'SEARCHING';
318
+
319
+ this.debugPanel.innerHTML = `
320
+ <div>HEAD-UP DISPLAY</div>
321
+ <div>----------------</div>
322
+ <div>FPS: ${this.fps}</div>
323
+ <div>STATUS: <span style="color:${color}">${status}</span></div>
324
+ <div>MEM: ${memory} MB</div>
325
+ `;
326
+ }
257
327
  }
258
328
 
259
329
  export { SimpleAR };
@@ -2,7 +2,7 @@ import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../es
2
2
 
3
3
  const AR2_DEFAULT_TS = 6;
4
4
  const AR2_DEFAULT_TS_GAP = 1;
5
- const AR2_SEARCH_SIZE = 10;
5
+ const AR2_SEARCH_SIZE = 18;
6
6
  const AR2_SEARCH_GAP = 1;
7
7
  const AR2_SIM_THRESH = 0.6;
8
8
 
@@ -24,16 +24,13 @@ class Tracker {
24
24
  this.inputHeight = inputHeight;
25
25
  this.debugMode = debugMode;
26
26
 
27
- this.trackingKeyframeList = [];
28
- for (let i = 0; i < trackingDataList.length; i++) {
29
- this.trackingKeyframeList.push(trackingDataList[i][TRACKING_KEYFRAME]);
30
- }
27
+ this.trackingKeyframeList = []; // All octaves for all targets: [targetIndex][octaveIndex]
28
+ this.prebuiltData = []; // [targetIndex][octaveIndex]
31
29
 
32
- // Prebuild TypedArrays for features and pixels
33
- this.prebuiltData = [];
34
- for (let i = 0; i < this.trackingKeyframeList.length; i++) {
35
- const keyframe = this.trackingKeyframeList[i];
36
- this.prebuiltData[i] = {
30
+ for (let i = 0; i < trackingDataList.length; i++) {
31
+ const targetOctaves = trackingDataList[i];
32
+ this.trackingKeyframeList[i] = targetOctaves;
33
+ this.prebuiltData[i] = targetOctaves.map(keyframe => ({
37
34
  px: new Float32Array(keyframe.px),
38
35
  py: new Float32Array(keyframe.py),
39
36
  data: new Uint8Array(keyframe.d),
@@ -42,7 +39,7 @@ class Tracker {
42
39
  scale: keyframe.s,
43
40
  // Recyclable projected image buffer
44
41
  projectedImage: new Float32Array(keyframe.w * keyframe.h)
45
- };
42
+ }));
46
43
  }
47
44
 
48
45
  // Pre-allocate template data buffer to avoid garbage collection
@@ -65,12 +62,31 @@ class Tracker {
65
62
  track(inputData, lastModelViewTransform, targetIndex) {
66
63
  let debugExtra = {};
67
64
 
65
+ // Select the best octave based on current estimated distance/scale
66
+ // We want the octave where the marker size is closest to its projected size on screen
68
67
  const modelViewProjectionTransform = buildModelViewProjectionTransform(
69
68
  this.projectionTransform,
70
69
  lastModelViewTransform,
71
70
  );
72
71
 
73
- const prebuilt = this.prebuiltData[targetIndex];
72
+ // Estimate current marker width on screen
73
+ const [mW, mH] = this.markerDimensions[targetIndex];
74
+ const p0 = computeScreenCoordiate(modelViewProjectionTransform, 0, 0);
75
+ const p1 = computeScreenCoordiate(modelViewProjectionTransform, mW, 0);
76
+ const screenW = Math.sqrt((p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2);
77
+
78
+ // Select octave whose image width is closest to screenW
79
+ let octaveIndex = 0;
80
+ let minDiff = Infinity;
81
+ for (let i = 0; i < this.prebuiltData[targetIndex].length; i++) {
82
+ const diff = Math.abs(this.prebuiltData[targetIndex][i].width - screenW);
83
+ if (diff < minDiff) {
84
+ minDiff = diff;
85
+ octaveIndex = i;
86
+ }
87
+ }
88
+
89
+ const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
74
90
 
75
91
  // 1. Compute Projection (Warping)
76
92
  this._computeProjection(
@@ -87,7 +103,7 @@ class Tracker {
87
103
  projectedImage
88
104
  );
89
105
 
90
- const trackingFrame = this.trackingKeyframeList[targetIndex];
106
+ const trackingFrame = this.trackingKeyframeList[targetIndex][octaveIndex];
91
107
  const worldCoords = [];
92
108
  const screenCoords = [];
93
109
  const goodTrack = [];
@@ -113,6 +129,7 @@ class Tracker {
113
129
 
114
130
  if (this.debugMode) {
115
131
  debugExtra = {
132
+ octaveIndex,
116
133
  projectedImage: Array.from(projectedImage),
117
134
  matchingPoints,
118
135
  goodTrack,