@srsergio/taptapp-ar 1.0.94 → 1.0.95
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/runtime/simple-ar.d.ts +24 -20
- package/dist/runtime/simple-ar.js +172 -156
- package/package.json +1 -1
- package/src/runtime/simple-ar.ts +197 -171
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Controller } from "./controller.js";
|
|
2
|
-
import { OneEuroFilter } from "../libs/one-euro-filter.js";
|
|
3
1
|
/**
|
|
4
|
-
* 🍦 SimpleAR -
|
|
2
|
+
* 🍦 SimpleAR - High-performance Vanilla AR for image overlays
|
|
3
|
+
* Now powered by Bio-Inspired Perception and Nanite Virtualized Features.
|
|
5
4
|
*/
|
|
6
5
|
export interface SimpleAROptions {
|
|
7
6
|
container: HTMLElement;
|
|
@@ -58,25 +57,30 @@ declare class SimpleAR {
|
|
|
58
57
|
}) => void) | null;
|
|
59
58
|
cameraConfig: MediaStreamConstraints['video'];
|
|
60
59
|
debug: boolean;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
private video;
|
|
61
|
+
private controller;
|
|
62
|
+
private smoother;
|
|
63
|
+
private isTracking;
|
|
64
|
+
private markerDimensions;
|
|
65
|
+
private debugPanel;
|
|
66
|
+
private debugCanvas;
|
|
67
|
+
private debugCtx;
|
|
68
|
+
private lastTime;
|
|
69
|
+
private fps;
|
|
70
|
+
private frameCount;
|
|
71
71
|
constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, debug, }: SimpleAROptions);
|
|
72
72
|
start(): Promise<this>;
|
|
73
73
|
stop(): void;
|
|
74
|
-
_createVideo
|
|
75
|
-
_startCamera
|
|
76
|
-
_initController
|
|
77
|
-
_handleUpdate
|
|
78
|
-
_positionOverlay
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
private _createVideo;
|
|
75
|
+
private _startCamera;
|
|
76
|
+
private _initController;
|
|
77
|
+
private _handleUpdate;
|
|
78
|
+
private _positionOverlay;
|
|
79
|
+
private _solveHomography;
|
|
80
|
+
private _createDebugPanel;
|
|
81
|
+
private _createDebugCanvas;
|
|
82
|
+
private _updateHUD;
|
|
83
|
+
private _drawDebugPoints;
|
|
84
|
+
private _drawDebugFeatures;
|
|
81
85
|
}
|
|
82
86
|
export { SimpleAR };
|
|
@@ -1,5 +1,54 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BioInspiredController } from "./bio-inspired-controller.js";
|
|
2
2
|
import { projectToScreen } from "../core/utils/projection.js";
|
|
3
|
+
/**
|
|
4
|
+
* 🕵️ Internal Smoothing Manager
|
|
5
|
+
* Applies Median + Adaptive Alpha filtering for sub-pixel stability.
|
|
6
|
+
*/
|
|
7
|
+
class SmoothingManager {
|
|
8
|
+
history = new Map();
|
|
9
|
+
lastFiltered = new Map();
|
|
10
|
+
medianSize = 3;
|
|
11
|
+
deadZone = 0.2;
|
|
12
|
+
smooth(id, raw, reliability) {
|
|
13
|
+
if (!this.history.has(id))
|
|
14
|
+
this.history.set(id, []);
|
|
15
|
+
const h = this.history.get(id);
|
|
16
|
+
h.push(raw);
|
|
17
|
+
if (h.length > this.medianSize)
|
|
18
|
+
h.shift();
|
|
19
|
+
// Get median
|
|
20
|
+
const sortedX = [...h].map(p => p.x).sort((a, b) => a - b);
|
|
21
|
+
const sortedY = [...h].map(p => p.y).sort((a, b) => a - b);
|
|
22
|
+
const median = {
|
|
23
|
+
x: sortedX[Math.floor(sortedX.length / 2)],
|
|
24
|
+
y: sortedY[Math.floor(sortedY.length / 2)]
|
|
25
|
+
};
|
|
26
|
+
// Adaptive Alpha based on reliability
|
|
27
|
+
const baseAlpha = 0.15;
|
|
28
|
+
const alpha = baseAlpha + (reliability * (1.0 - baseAlpha));
|
|
29
|
+
const last = this.lastFiltered.get(id) || median;
|
|
30
|
+
let filteredX = last.x * (1 - alpha) + median.x * alpha;
|
|
31
|
+
let filteredY = last.y * (1 - alpha) + median.y * alpha;
|
|
32
|
+
// Dead-zone to kill jitter at rest
|
|
33
|
+
if (Math.abs(filteredX - last.x) < this.deadZone)
|
|
34
|
+
filteredX = last.x;
|
|
35
|
+
if (Math.abs(filteredY - last.y) < this.deadZone)
|
|
36
|
+
filteredY = last.y;
|
|
37
|
+
const result = { x: filteredX, y: filteredY };
|
|
38
|
+
this.lastFiltered.set(id, result);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
reset(id) {
|
|
42
|
+
if (id !== undefined) {
|
|
43
|
+
this.history.delete(id);
|
|
44
|
+
this.lastFiltered.delete(id);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.history.clear();
|
|
48
|
+
this.lastFiltered.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
3
52
|
class SimpleAR {
|
|
4
53
|
container;
|
|
5
54
|
targetSrc;
|
|
@@ -10,16 +59,17 @@ class SimpleAR {
|
|
|
10
59
|
onUpdateCallback;
|
|
11
60
|
cameraConfig;
|
|
12
61
|
debug;
|
|
13
|
-
lastTime;
|
|
14
|
-
frameCount;
|
|
15
|
-
fps;
|
|
16
|
-
debugPanel = null;
|
|
17
62
|
video = null;
|
|
18
63
|
controller = null;
|
|
64
|
+
smoother = new SmoothingManager();
|
|
19
65
|
isTracking = false;
|
|
20
|
-
lastMatrix = null;
|
|
21
|
-
filters = [];
|
|
22
66
|
markerDimensions = [];
|
|
67
|
+
debugPanel = null;
|
|
68
|
+
debugCanvas = null;
|
|
69
|
+
debugCtx = null;
|
|
70
|
+
lastTime = 0;
|
|
71
|
+
fps = 0;
|
|
72
|
+
frameCount = 0;
|
|
23
73
|
constructor({ container, targetSrc, overlay, scale = 1.0, onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, debug = false, }) {
|
|
24
74
|
this.container = container;
|
|
25
75
|
this.targetSrc = targetSrc;
|
|
@@ -30,23 +80,19 @@ class SimpleAR {
|
|
|
30
80
|
this.onUpdateCallback = onUpdate;
|
|
31
81
|
this.cameraConfig = cameraConfig;
|
|
32
82
|
this.debug = debug;
|
|
33
|
-
// @ts-ignore
|
|
34
|
-
if (this.debug)
|
|
35
|
-
window.AR_DEBUG = true;
|
|
36
|
-
this.lastTime = performance.now();
|
|
37
|
-
this.frameCount = 0;
|
|
38
|
-
this.fps = 0;
|
|
39
83
|
}
|
|
40
84
|
async start() {
|
|
41
85
|
this._createVideo();
|
|
42
86
|
await this._startCamera();
|
|
43
87
|
this._initController();
|
|
44
|
-
if (this.debug)
|
|
88
|
+
if (this.debug) {
|
|
45
89
|
this._createDebugPanel();
|
|
90
|
+
this._createDebugCanvas();
|
|
91
|
+
}
|
|
46
92
|
const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
|
|
47
93
|
const result = await this.controller.addImageTargets(targets);
|
|
48
94
|
this.markerDimensions = result.dimensions;
|
|
49
|
-
|
|
95
|
+
// Kick off loop
|
|
50
96
|
this.controller.processVideo(this.video);
|
|
51
97
|
return this;
|
|
52
98
|
}
|
|
@@ -61,7 +107,7 @@ class SimpleAR {
|
|
|
61
107
|
this.video = null;
|
|
62
108
|
}
|
|
63
109
|
this.isTracking = false;
|
|
64
|
-
this.
|
|
110
|
+
this.smoother.reset();
|
|
65
111
|
}
|
|
66
112
|
_createVideo() {
|
|
67
113
|
this.video = document.createElement('video');
|
|
@@ -70,21 +116,15 @@ class SimpleAR {
|
|
|
70
116
|
this.video.setAttribute('muted', '');
|
|
71
117
|
this.video.style.cssText = `
|
|
72
118
|
position: absolute;
|
|
73
|
-
top: 0;
|
|
74
|
-
|
|
75
|
-
width: 100%;
|
|
76
|
-
height: 100%;
|
|
77
|
-
object-fit: cover;
|
|
78
|
-
z-index: 0;
|
|
119
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
120
|
+
object-fit: cover; z-index: 0;
|
|
79
121
|
`;
|
|
80
122
|
this.container.style.position = 'relative';
|
|
81
123
|
this.container.style.overflow = 'hidden';
|
|
82
124
|
this.container.insertBefore(this.video, this.container.firstChild);
|
|
83
125
|
}
|
|
84
126
|
async _startCamera() {
|
|
85
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
86
|
-
video: this.cameraConfig
|
|
87
|
-
});
|
|
127
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: this.cameraConfig });
|
|
88
128
|
this.video.srcObject = stream;
|
|
89
129
|
await this.video.play();
|
|
90
130
|
await new Promise(resolve => {
|
|
@@ -94,114 +134,76 @@ class SimpleAR {
|
|
|
94
134
|
});
|
|
95
135
|
}
|
|
96
136
|
_initController() {
|
|
97
|
-
this.controller = new
|
|
137
|
+
this.controller = new BioInspiredController({
|
|
98
138
|
inputWidth: this.video.videoWidth,
|
|
99
139
|
inputHeight: this.video.videoHeight,
|
|
100
140
|
debugMode: this.debug,
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
bioInspired: {
|
|
142
|
+
enabled: true,
|
|
143
|
+
aggressiveSkipping: false
|
|
144
|
+
},
|
|
103
145
|
onUpdate: (data) => this._handleUpdate(data)
|
|
104
146
|
});
|
|
105
147
|
}
|
|
106
148
|
_handleUpdate(data) {
|
|
107
|
-
if (data.type !== 'updateMatrix')
|
|
149
|
+
if (data.type !== 'updateMatrix') {
|
|
150
|
+
if (data.type === 'featurePoints' && this.debugCtx) {
|
|
151
|
+
this._drawDebugFeatures(data.featurePoints);
|
|
152
|
+
}
|
|
108
153
|
return;
|
|
154
|
+
}
|
|
155
|
+
// FPS Meter
|
|
109
156
|
const now = performance.now();
|
|
110
157
|
this.frameCount++;
|
|
111
158
|
if (now - this.lastTime >= 1000) {
|
|
112
159
|
this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
|
|
113
160
|
this.frameCount = 0;
|
|
114
161
|
this.lastTime = now;
|
|
115
|
-
if (this.debug)
|
|
116
|
-
this._updateDebugPanel(this.isTracking);
|
|
117
162
|
}
|
|
118
|
-
const { targetIndex, worldMatrix, modelViewTransform,
|
|
119
|
-
//
|
|
120
|
-
let
|
|
163
|
+
const { targetIndex, worldMatrix, modelViewTransform, reliabilities, stabilities, screenCoords, pixelsSaved } = data;
|
|
164
|
+
// Apply Smoothing
|
|
165
|
+
let smoothedCoords = screenCoords || [];
|
|
121
166
|
if (screenCoords && screenCoords.length > 0) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const isVideoLandscape = videoW > videoH;
|
|
127
|
-
const needsRotation = isPortrait && isVideoLandscape;
|
|
128
|
-
const proj = this.controller.projectionTransform;
|
|
129
|
-
const vW = needsRotation ? videoH : videoW;
|
|
130
|
-
const vH = needsRotation ? videoW : videoH;
|
|
131
|
-
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
132
|
-
const dW = vW * pScale;
|
|
133
|
-
const dH = vH * pScale;
|
|
134
|
-
const oX = (containerRect.width - dW) / 2;
|
|
135
|
-
const oY = (containerRect.height - dH) / 2;
|
|
136
|
-
projectedPoints = screenCoords.map((p) => {
|
|
137
|
-
let sx, sy;
|
|
138
|
-
if (needsRotation) {
|
|
139
|
-
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
140
|
-
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
144
|
-
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
145
|
-
}
|
|
146
|
-
return { x: sx, y: sy };
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
let projectedDetectionPoints = [];
|
|
150
|
-
if (detectionPoints && detectionPoints.length > 0) {
|
|
151
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
152
|
-
const videoW = this.video.videoWidth;
|
|
153
|
-
const videoH = this.video.videoHeight;
|
|
154
|
-
const isPortrait = containerRect.height > containerRect.width;
|
|
155
|
-
const isVideoLandscape = videoW > videoH;
|
|
156
|
-
const needsRotation = isPortrait && isVideoLandscape;
|
|
157
|
-
const proj = this.controller.projectionTransform;
|
|
158
|
-
const vW = needsRotation ? videoH : videoW;
|
|
159
|
-
const vH = needsRotation ? videoW : videoH;
|
|
160
|
-
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
161
|
-
const dW = vW * pScale;
|
|
162
|
-
const dH = vH * pScale;
|
|
163
|
-
const oX = (containerRect.width - dW) / 2;
|
|
164
|
-
const oY = (containerRect.height - dH) / 2;
|
|
165
|
-
projectedDetectionPoints = detectionPoints.map((p) => {
|
|
166
|
-
let sx, sy;
|
|
167
|
-
if (needsRotation) {
|
|
168
|
-
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
169
|
-
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
173
|
-
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
174
|
-
}
|
|
175
|
-
return { x: sx, y: sy };
|
|
167
|
+
smoothedCoords = screenCoords.map((p) => {
|
|
168
|
+
const rel = reliabilities ? (reliabilities[p.id] || 0.5) : 0.5;
|
|
169
|
+
const sm = this.smoother.smooth(p.id, p, rel);
|
|
170
|
+
return { ...sm, id: p.id };
|
|
176
171
|
});
|
|
177
172
|
}
|
|
178
173
|
if (worldMatrix) {
|
|
179
174
|
if (!this.isTracking) {
|
|
180
175
|
this.isTracking = true;
|
|
181
|
-
|
|
176
|
+
if (this.overlay)
|
|
177
|
+
this.overlay.style.opacity = '1';
|
|
182
178
|
this.onFound && this.onFound({ targetIndex });
|
|
183
179
|
}
|
|
184
|
-
this.lastMatrix = worldMatrix;
|
|
185
180
|
this._positionOverlay(modelViewTransform, targetIndex);
|
|
186
181
|
}
|
|
187
182
|
else {
|
|
188
183
|
if (this.isTracking) {
|
|
189
184
|
this.isTracking = false;
|
|
190
|
-
|
|
185
|
+
if (this.overlay)
|
|
186
|
+
this.overlay.style.opacity = '0';
|
|
191
187
|
this.onLost && this.onLost({ targetIndex });
|
|
188
|
+
this.smoother.reset();
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
|
-
//
|
|
195
|
-
if (
|
|
196
|
-
this.onUpdateCallback
|
|
191
|
+
// Notify callback
|
|
192
|
+
if (this.onUpdateCallback) {
|
|
193
|
+
this.onUpdateCallback({
|
|
197
194
|
targetIndex,
|
|
198
195
|
worldMatrix,
|
|
199
|
-
screenCoords:
|
|
196
|
+
screenCoords: smoothedCoords,
|
|
200
197
|
reliabilities: reliabilities || [],
|
|
201
198
|
stabilities: stabilities || [],
|
|
202
|
-
detectionPoints:
|
|
199
|
+
detectionPoints: data.featurePoints
|
|
203
200
|
});
|
|
204
201
|
}
|
|
202
|
+
// Draw Debug UI
|
|
203
|
+
if (this.debug) {
|
|
204
|
+
this._updateHUD(data);
|
|
205
|
+
this._drawDebugPoints(smoothedCoords, stabilities);
|
|
206
|
+
}
|
|
205
207
|
}
|
|
206
208
|
_positionOverlay(mVT, targetIndex) {
|
|
207
209
|
if (!this.overlay || !this.markerDimensions[targetIndex])
|
|
@@ -210,51 +212,16 @@ class SimpleAR {
|
|
|
210
212
|
const containerRect = this.container.getBoundingClientRect();
|
|
211
213
|
const videoW = this.video.videoWidth;
|
|
212
214
|
const videoH = this.video.videoHeight;
|
|
215
|
+
const proj = this.controller.projectionTransform;
|
|
216
|
+
// Handle portrait rotation for mobile
|
|
213
217
|
const isPortrait = containerRect.height > containerRect.width;
|
|
214
218
|
const isVideoLandscape = videoW > videoH;
|
|
215
219
|
const needsRotation = isPortrait && isVideoLandscape;
|
|
216
|
-
const proj = this.controller.projectionTransform;
|
|
217
220
|
const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
218
221
|
const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
219
222
|
const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
220
223
|
const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
221
|
-
const
|
|
222
|
-
const x1 = p1.sx, y1 = p1.sy;
|
|
223
|
-
const x2 = p2.sx, y2 = p2.sy;
|
|
224
|
-
const x3 = p3.sx, y3 = p3.sy;
|
|
225
|
-
const x4 = p4.sx, y4 = p4.sy;
|
|
226
|
-
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
227
|
-
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
228
|
-
let a, b, c, d, e, f, g, h_coeff;
|
|
229
|
-
if (dx3 === 0 && dy3 === 0) {
|
|
230
|
-
a = x2 - x1;
|
|
231
|
-
b = x3 - x1;
|
|
232
|
-
c = x1;
|
|
233
|
-
d = y2 - y1;
|
|
234
|
-
e = y3 - y1;
|
|
235
|
-
f = y1;
|
|
236
|
-
g = 0;
|
|
237
|
-
h_coeff = 0;
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
const det = dx1 * dy2 - dx2 * dy1;
|
|
241
|
-
g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
242
|
-
h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
243
|
-
a = x2 - x1 + g * x2;
|
|
244
|
-
b = x3 - x1 + h_coeff * x3;
|
|
245
|
-
c = x1;
|
|
246
|
-
d = y2 - y1 + g * y2;
|
|
247
|
-
e = y3 - y1 + h_coeff * y3;
|
|
248
|
-
f = y1;
|
|
249
|
-
}
|
|
250
|
-
return [
|
|
251
|
-
a / w, d / w, 0, g / w,
|
|
252
|
-
b / h, e / h, 0, h_coeff / h,
|
|
253
|
-
0, 0, 1, 0,
|
|
254
|
-
c, f, 0, 1
|
|
255
|
-
];
|
|
256
|
-
};
|
|
257
|
-
const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
224
|
+
const matrix = this._solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
258
225
|
this.overlay.style.maxWidth = 'none';
|
|
259
226
|
this.overlay.style.width = `${markerW}px`;
|
|
260
227
|
this.overlay.style.height = `${markerH}px`;
|
|
@@ -270,38 +237,87 @@ class SimpleAR {
|
|
|
270
237
|
translate(${-markerW / 2}px, ${-markerH / 2}px)
|
|
271
238
|
`;
|
|
272
239
|
}
|
|
240
|
+
_solveHomography(w, h, p1, p2, p3, p4) {
|
|
241
|
+
const x1 = p1.sx, y1 = p1.sy;
|
|
242
|
+
const x2 = p2.sx, y2 = p2.sy;
|
|
243
|
+
const x3 = p3.sx, y3 = p3.sy;
|
|
244
|
+
const x4 = p4.sx, y4 = p4.sy;
|
|
245
|
+
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
246
|
+
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
247
|
+
const det = dx1 * dy2 - dx2 * dy1;
|
|
248
|
+
const g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
249
|
+
const h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
250
|
+
const a = x2 - x1 + g * x2;
|
|
251
|
+
const b = x3 - x1 + h_coeff * x3;
|
|
252
|
+
const c = x1;
|
|
253
|
+
const d = y2 - y1 + g * y2;
|
|
254
|
+
const e = y3 - y1 + h_coeff * y3;
|
|
255
|
+
const f = y1;
|
|
256
|
+
return [
|
|
257
|
+
a / w, d / w, 0, g / w,
|
|
258
|
+
b / h, e / h, 0, h_coeff / h,
|
|
259
|
+
0, 0, 1, 0,
|
|
260
|
+
c, f, 0, 1
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
// --- DEBUG METHODS ---
|
|
273
264
|
_createDebugPanel() {
|
|
274
265
|
this.debugPanel = document.createElement('div');
|
|
275
266
|
this.debugPanel.style.cssText = `
|
|
276
|
-
position: absolute;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
font-family: monospace;
|
|
282
|
-
font-size: 12px;
|
|
283
|
-
padding: 8px;
|
|
284
|
-
border-radius: 4px;
|
|
285
|
-
z-index: 99999;
|
|
286
|
-
pointer-events: none;
|
|
287
|
-
line-height: 1.5;
|
|
267
|
+
position: absolute; top: 10px; left: 10px;
|
|
268
|
+
background: rgba(0, 0, 0, 0.7); color: #0f0;
|
|
269
|
+
font-family: monospace; font-size: 11px; padding: 10px;
|
|
270
|
+
border-radius: 5px; z-index: 100; pointer-events: none;
|
|
271
|
+
line-height: 1.4; border-left: 3px solid #0f0;
|
|
288
272
|
`;
|
|
289
273
|
this.container.appendChild(this.debugPanel);
|
|
290
274
|
}
|
|
291
|
-
|
|
275
|
+
_createDebugCanvas() {
|
|
276
|
+
this.debugCanvas = document.createElement('canvas');
|
|
277
|
+
this.debugCanvas.width = this.container.clientWidth;
|
|
278
|
+
this.debugCanvas.height = this.container.clientHeight;
|
|
279
|
+
this.debugCanvas.style.cssText = `
|
|
280
|
+
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
281
|
+
pointer-events: none; z-index: 99;
|
|
282
|
+
`;
|
|
283
|
+
this.container.appendChild(this.debugCanvas);
|
|
284
|
+
this.debugCtx = this.debugCanvas.getContext('2d');
|
|
285
|
+
}
|
|
286
|
+
_updateHUD(data) {
|
|
292
287
|
if (!this.debugPanel)
|
|
293
288
|
return;
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
const status = isTracking ? 'TRACKING' : 'SEARCHING';
|
|
289
|
+
const rel = data.reliabilities ? (data.reliabilities.reduce((a, b) => a + b, 0) / data.reliabilities.length).toFixed(2) : "0.00";
|
|
290
|
+
const stab = data.stabilities ? (data.stabilities.reduce((a, b) => a + b, 0) / data.stabilities.length).toFixed(2) : "0.00";
|
|
291
|
+
const savings = data.pixelsSaved ? ((data.pixelsSaved / (this.video.videoWidth * this.video.videoHeight)) * 100).toFixed(0) : "0";
|
|
298
292
|
this.debugPanel.innerHTML = `
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
<b>TapTapp AR HUD</b><br>
|
|
294
|
+
------------------<br>
|
|
295
|
+
STATUS: <span style="color:${this.isTracking ? '#0f0' : '#f00'}">${this.isTracking ? 'TRACKING' : 'SEARCHING'}</span><br>
|
|
296
|
+
FPS: ${this.fps}<br>
|
|
297
|
+
RELIAB: ${rel}<br>
|
|
298
|
+
STABIL: ${stab}<br>
|
|
299
|
+
SAVINGS: ${savings}% Pixels<br>
|
|
300
|
+
POINTS: ${data.screenCoords?.length || 0}
|
|
304
301
|
`;
|
|
305
302
|
}
|
|
303
|
+
_drawDebugPoints(coords, stabilities) {
|
|
304
|
+
if (!this.debugCtx)
|
|
305
|
+
return;
|
|
306
|
+
this.debugCtx.clearRect(0, 0, this.debugCanvas.width, this.debugCanvas.height);
|
|
307
|
+
coords.forEach((p, i) => {
|
|
308
|
+
const s = stabilities ? (stabilities[i] || 0) : 0.5;
|
|
309
|
+
this.debugCtx.fillStyle = `rgba(0, 255, 0, ${0.4 + s * 0.6})`;
|
|
310
|
+
this.debugCtx.fillRect(p.x - 1, p.y - 1, 2, 2);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
_drawDebugFeatures(points) {
|
|
314
|
+
if (!this.debugCtx || this.isTracking)
|
|
315
|
+
return;
|
|
316
|
+
this.debugCtx.clearRect(0, 0, this.debugCanvas.width, this.debugCanvas.height);
|
|
317
|
+
this.debugCtx.fillStyle = 'rgba(255, 255, 0, 0.4)';
|
|
318
|
+
points.slice(0, 200).forEach(p => {
|
|
319
|
+
this.debugCtx.fillRect(p.x - 1, p.y - 1, 2, 2);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
306
322
|
}
|
|
307
323
|
export { SimpleAR };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.95",
|
|
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",
|
package/src/runtime/simple-ar.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OneEuroFilter } from "../libs/one-euro-filter.js";
|
|
1
|
+
import { BioInspiredController } from "./bio-inspired-controller.js";
|
|
3
2
|
import { projectToScreen } from "../core/utils/projection.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* 🍦 SimpleAR -
|
|
5
|
+
* 🍦 SimpleAR - High-performance Vanilla AR for image overlays
|
|
6
|
+
* Now powered by Bio-Inspired Perception and Nanite Virtualized Features.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export interface SimpleAROptions {
|
|
@@ -25,6 +25,58 @@ export interface SimpleAROptions {
|
|
|
25
25
|
debug?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 🕵️ Internal Smoothing Manager
|
|
30
|
+
* Applies Median + Adaptive Alpha filtering for sub-pixel stability.
|
|
31
|
+
*/
|
|
32
|
+
class SmoothingManager {
|
|
33
|
+
private history: Map<number, { x: number, y: number }[]> = new Map();
|
|
34
|
+
private lastFiltered: Map<number, { x: number, y: number }> = new Map();
|
|
35
|
+
private medianSize = 3;
|
|
36
|
+
private deadZone = 0.2;
|
|
37
|
+
|
|
38
|
+
smooth(id: number, raw: { x: number, y: number }, reliability: number) {
|
|
39
|
+
if (!this.history.has(id)) this.history.set(id, []);
|
|
40
|
+
const h = this.history.get(id)!;
|
|
41
|
+
h.push(raw);
|
|
42
|
+
if (h.length > this.medianSize) h.shift();
|
|
43
|
+
|
|
44
|
+
// Get median
|
|
45
|
+
const sortedX = [...h].map(p => p.x).sort((a, b) => a - b);
|
|
46
|
+
const sortedY = [...h].map(p => p.y).sort((a, b) => a - b);
|
|
47
|
+
const median = {
|
|
48
|
+
x: sortedX[Math.floor(sortedX.length / 2)],
|
|
49
|
+
y: sortedY[Math.floor(sortedY.length / 2)]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Adaptive Alpha based on reliability
|
|
53
|
+
const baseAlpha = 0.15;
|
|
54
|
+
const alpha = baseAlpha + (reliability * (1.0 - baseAlpha));
|
|
55
|
+
const last = this.lastFiltered.get(id) || median;
|
|
56
|
+
|
|
57
|
+
let filteredX = last.x * (1 - alpha) + median.x * alpha;
|
|
58
|
+
let filteredY = last.y * (1 - alpha) + median.y * alpha;
|
|
59
|
+
|
|
60
|
+
// Dead-zone to kill jitter at rest
|
|
61
|
+
if (Math.abs(filteredX - last.x) < this.deadZone) filteredX = last.x;
|
|
62
|
+
if (Math.abs(filteredY - last.y) < this.deadZone) filteredY = last.y;
|
|
63
|
+
|
|
64
|
+
const result = { x: filteredX, y: filteredY };
|
|
65
|
+
this.lastFiltered.set(id, result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reset(id?: number) {
|
|
70
|
+
if (id !== undefined) {
|
|
71
|
+
this.history.delete(id);
|
|
72
|
+
this.lastFiltered.delete(id);
|
|
73
|
+
} else {
|
|
74
|
+
this.history.clear();
|
|
75
|
+
this.lastFiltered.clear();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
28
80
|
class SimpleAR {
|
|
29
81
|
container: HTMLElement;
|
|
30
82
|
targetSrc: string | string[];
|
|
@@ -43,16 +95,18 @@ class SimpleAR {
|
|
|
43
95
|
cameraConfig: MediaStreamConstraints['video'];
|
|
44
96
|
debug: boolean;
|
|
45
97
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
private video: HTMLVideoElement | null = null;
|
|
99
|
+
private controller: BioInspiredController | null = null;
|
|
100
|
+
private smoother = new SmoothingManager();
|
|
101
|
+
private isTracking: boolean = false;
|
|
102
|
+
private markerDimensions: number[][] = [];
|
|
103
|
+
private debugPanel: HTMLElement | null = null;
|
|
104
|
+
private debugCanvas: HTMLCanvasElement | null = null;
|
|
105
|
+
private debugCtx: CanvasRenderingContext2D | null = null;
|
|
106
|
+
|
|
107
|
+
private lastTime = 0;
|
|
108
|
+
private fps = 0;
|
|
109
|
+
private frameCount = 0;
|
|
56
110
|
|
|
57
111
|
constructor({
|
|
58
112
|
container,
|
|
@@ -74,12 +128,6 @@ class SimpleAR {
|
|
|
74
128
|
this.onUpdateCallback = onUpdate;
|
|
75
129
|
this.cameraConfig = cameraConfig;
|
|
76
130
|
this.debug = debug;
|
|
77
|
-
// @ts-ignore
|
|
78
|
-
if (this.debug) window.AR_DEBUG = true;
|
|
79
|
-
|
|
80
|
-
this.lastTime = performance.now();
|
|
81
|
-
this.frameCount = 0;
|
|
82
|
-
this.fps = 0;
|
|
83
131
|
}
|
|
84
132
|
|
|
85
133
|
async start() {
|
|
@@ -87,14 +135,17 @@ class SimpleAR {
|
|
|
87
135
|
await this._startCamera();
|
|
88
136
|
this._initController();
|
|
89
137
|
|
|
90
|
-
if (this.debug)
|
|
138
|
+
if (this.debug) {
|
|
139
|
+
this._createDebugPanel();
|
|
140
|
+
this._createDebugCanvas();
|
|
141
|
+
}
|
|
91
142
|
|
|
92
143
|
const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
|
|
93
144
|
const result = await this.controller!.addImageTargets(targets);
|
|
94
145
|
this.markerDimensions = result.dimensions;
|
|
95
|
-
console.log("Targets loaded. Dimensions:", this.markerDimensions);
|
|
96
146
|
|
|
97
|
-
|
|
147
|
+
// Kick off loop
|
|
148
|
+
this.controller!.processVideo(this.video!);
|
|
98
149
|
return this;
|
|
99
150
|
}
|
|
100
151
|
|
|
@@ -109,32 +160,26 @@ class SimpleAR {
|
|
|
109
160
|
this.video = null;
|
|
110
161
|
}
|
|
111
162
|
this.isTracking = false;
|
|
112
|
-
this.
|
|
163
|
+
this.smoother.reset();
|
|
113
164
|
}
|
|
114
165
|
|
|
115
|
-
_createVideo() {
|
|
166
|
+
private _createVideo() {
|
|
116
167
|
this.video = document.createElement('video');
|
|
117
168
|
this.video.setAttribute('autoplay', '');
|
|
118
169
|
this.video.setAttribute('playsinline', '');
|
|
119
170
|
this.video.setAttribute('muted', '');
|
|
120
171
|
this.video.style.cssText = `
|
|
121
172
|
position: absolute;
|
|
122
|
-
top: 0;
|
|
123
|
-
|
|
124
|
-
width: 100%;
|
|
125
|
-
height: 100%;
|
|
126
|
-
object-fit: cover;
|
|
127
|
-
z-index: 0;
|
|
173
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
174
|
+
object-fit: cover; z-index: 0;
|
|
128
175
|
`;
|
|
129
176
|
this.container.style.position = 'relative';
|
|
130
177
|
this.container.style.overflow = 'hidden';
|
|
131
178
|
this.container.insertBefore(this.video, this.container.firstChild);
|
|
132
179
|
}
|
|
133
180
|
|
|
134
|
-
async _startCamera() {
|
|
135
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
136
|
-
video: this.cameraConfig
|
|
137
|
-
});
|
|
181
|
+
private async _startCamera() {
|
|
182
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: this.cameraConfig });
|
|
138
183
|
this.video!.srcObject = stream;
|
|
139
184
|
await this.video!.play();
|
|
140
185
|
|
|
@@ -144,178 +189,103 @@ class SimpleAR {
|
|
|
144
189
|
});
|
|
145
190
|
}
|
|
146
191
|
|
|
147
|
-
_initController() {
|
|
148
|
-
this.controller = new
|
|
192
|
+
private _initController() {
|
|
193
|
+
this.controller = new BioInspiredController({
|
|
149
194
|
inputWidth: this.video!.videoWidth,
|
|
150
195
|
inputHeight: this.video!.videoHeight,
|
|
151
196
|
debugMode: this.debug,
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
bioInspired: {
|
|
198
|
+
enabled: true,
|
|
199
|
+
aggressiveSkipping: false
|
|
200
|
+
},
|
|
154
201
|
onUpdate: (data) => this._handleUpdate(data)
|
|
155
202
|
});
|
|
156
203
|
}
|
|
157
204
|
|
|
158
|
-
_handleUpdate(data: any) {
|
|
159
|
-
if (data.type !== 'updateMatrix')
|
|
205
|
+
private _handleUpdate(data: any) {
|
|
206
|
+
if (data.type !== 'updateMatrix') {
|
|
207
|
+
if (data.type === 'featurePoints' && this.debugCtx) {
|
|
208
|
+
this._drawDebugFeatures(data.featurePoints);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
160
212
|
|
|
213
|
+
// FPS Meter
|
|
161
214
|
const now = performance.now();
|
|
162
215
|
this.frameCount++;
|
|
163
216
|
if (now - this.lastTime >= 1000) {
|
|
164
217
|
this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
|
|
165
218
|
this.frameCount = 0;
|
|
166
219
|
this.lastTime = now;
|
|
167
|
-
if (this.debug) this._updateDebugPanel(this.isTracking);
|
|
168
220
|
}
|
|
169
221
|
|
|
170
|
-
const { targetIndex, worldMatrix, modelViewTransform,
|
|
222
|
+
const { targetIndex, worldMatrix, modelViewTransform, reliabilities, stabilities, screenCoords, pixelsSaved } = data;
|
|
171
223
|
|
|
172
|
-
//
|
|
173
|
-
let
|
|
224
|
+
// Apply Smoothing
|
|
225
|
+
let smoothedCoords = screenCoords || [];
|
|
174
226
|
if (screenCoords && screenCoords.length > 0) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const isVideoLandscape = videoW > videoH;
|
|
180
|
-
const needsRotation = isPortrait && isVideoLandscape;
|
|
181
|
-
const proj = this.controller!.projectionTransform;
|
|
182
|
-
|
|
183
|
-
const vW = needsRotation ? videoH : videoW;
|
|
184
|
-
const vH = needsRotation ? videoW : videoH;
|
|
185
|
-
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
186
|
-
const dW = vW * pScale;
|
|
187
|
-
const dH = vH * pScale;
|
|
188
|
-
const oX = (containerRect.width - dW) / 2;
|
|
189
|
-
const oY = (containerRect.height - dH) / 2;
|
|
190
|
-
|
|
191
|
-
projectedPoints = screenCoords.map((p: any) => {
|
|
192
|
-
let sx, sy;
|
|
193
|
-
if (needsRotation) {
|
|
194
|
-
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
195
|
-
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
196
|
-
} else {
|
|
197
|
-
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
198
|
-
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
199
|
-
}
|
|
200
|
-
return { x: sx, y: sy };
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let projectedDetectionPoints = [];
|
|
205
|
-
if (detectionPoints && detectionPoints.length > 0) {
|
|
206
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
207
|
-
const videoW = this.video!.videoWidth;
|
|
208
|
-
const videoH = this.video!.videoHeight;
|
|
209
|
-
const isPortrait = containerRect.height > containerRect.width;
|
|
210
|
-
const isVideoLandscape = videoW > videoH;
|
|
211
|
-
const needsRotation = isPortrait && isVideoLandscape;
|
|
212
|
-
const proj = this.controller!.projectionTransform;
|
|
213
|
-
|
|
214
|
-
const vW = needsRotation ? videoH : videoW;
|
|
215
|
-
const vH = needsRotation ? videoW : videoH;
|
|
216
|
-
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
217
|
-
const dW = vW * pScale;
|
|
218
|
-
const dH = vH * pScale;
|
|
219
|
-
const oX = (containerRect.width - dW) / 2;
|
|
220
|
-
const oY = (containerRect.height - dH) / 2;
|
|
221
|
-
|
|
222
|
-
projectedDetectionPoints = detectionPoints.map((p: any) => {
|
|
223
|
-
let sx, sy;
|
|
224
|
-
if (needsRotation) {
|
|
225
|
-
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
226
|
-
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
227
|
-
} else {
|
|
228
|
-
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
229
|
-
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
230
|
-
}
|
|
231
|
-
return { x: sx, y: sy };
|
|
227
|
+
smoothedCoords = screenCoords.map((p: any) => {
|
|
228
|
+
const rel = reliabilities ? (reliabilities[p.id] || 0.5) : 0.5;
|
|
229
|
+
const sm = this.smoother.smooth(p.id, p, rel);
|
|
230
|
+
return { ...sm, id: p.id };
|
|
232
231
|
});
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
if (worldMatrix) {
|
|
236
235
|
if (!this.isTracking) {
|
|
237
236
|
this.isTracking = true;
|
|
238
|
-
this.overlay
|
|
237
|
+
if (this.overlay) this.overlay.style.opacity = '1';
|
|
239
238
|
this.onFound && this.onFound({ targetIndex });
|
|
240
239
|
}
|
|
241
|
-
|
|
242
|
-
this.lastMatrix = worldMatrix;
|
|
243
240
|
this._positionOverlay(modelViewTransform, targetIndex);
|
|
244
241
|
} else {
|
|
245
242
|
if (this.isTracking) {
|
|
246
243
|
this.isTracking = false;
|
|
247
|
-
this.overlay
|
|
244
|
+
if (this.overlay) this.overlay.style.opacity = '0';
|
|
248
245
|
this.onLost && this.onLost({ targetIndex });
|
|
246
|
+
this.smoother.reset();
|
|
249
247
|
}
|
|
250
248
|
}
|
|
251
249
|
|
|
252
|
-
//
|
|
253
|
-
if (
|
|
254
|
-
this.onUpdateCallback
|
|
250
|
+
// Notify callback
|
|
251
|
+
if (this.onUpdateCallback) {
|
|
252
|
+
this.onUpdateCallback({
|
|
255
253
|
targetIndex,
|
|
256
254
|
worldMatrix,
|
|
257
|
-
screenCoords:
|
|
255
|
+
screenCoords: smoothedCoords,
|
|
258
256
|
reliabilities: reliabilities || [],
|
|
259
257
|
stabilities: stabilities || [],
|
|
260
|
-
detectionPoints:
|
|
258
|
+
detectionPoints: data.featurePoints
|
|
261
259
|
});
|
|
262
260
|
}
|
|
261
|
+
|
|
262
|
+
// Draw Debug UI
|
|
263
|
+
if (this.debug) {
|
|
264
|
+
this._updateHUD(data);
|
|
265
|
+
this._drawDebugPoints(smoothedCoords, stabilities);
|
|
266
|
+
}
|
|
263
267
|
}
|
|
264
268
|
|
|
265
|
-
_positionOverlay(mVT: number[][], targetIndex: number) {
|
|
269
|
+
private _positionOverlay(mVT: number[][], targetIndex: number) {
|
|
266
270
|
if (!this.overlay || !this.markerDimensions[targetIndex]) return;
|
|
267
271
|
|
|
268
272
|
const [markerW, markerH] = this.markerDimensions[targetIndex];
|
|
269
273
|
const containerRect = this.container.getBoundingClientRect();
|
|
270
274
|
const videoW = this.video!.videoWidth;
|
|
271
275
|
const videoH = this.video!.videoHeight;
|
|
276
|
+
const proj = this.controller!.projectionTransform;
|
|
272
277
|
|
|
278
|
+
// Handle portrait rotation for mobile
|
|
273
279
|
const isPortrait = containerRect.height > containerRect.width;
|
|
274
280
|
const isVideoLandscape = videoW > videoH;
|
|
275
281
|
const needsRotation = isPortrait && isVideoLandscape;
|
|
276
282
|
|
|
277
|
-
const proj = this.controller!.projectionTransform;
|
|
278
|
-
|
|
279
283
|
const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
280
284
|
const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
281
285
|
const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
282
286
|
const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
283
287
|
|
|
284
|
-
const
|
|
285
|
-
const x1 = p1.sx, y1 = p1.sy;
|
|
286
|
-
const x2 = p2.sx, y2 = p2.sy;
|
|
287
|
-
const x3 = p3.sx, y3 = p3.sy;
|
|
288
|
-
const x4 = p4.sx, y4 = p4.sy;
|
|
289
|
-
|
|
290
|
-
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
291
|
-
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
292
|
-
|
|
293
|
-
let a, b, c, d, e, f, g, h_coeff;
|
|
294
|
-
|
|
295
|
-
if (dx3 === 0 && dy3 === 0) {
|
|
296
|
-
a = x2 - x1; b = x3 - x1; c = x1;
|
|
297
|
-
d = y2 - y1; e = y3 - y1; f = y1;
|
|
298
|
-
g = 0; h_coeff = 0;
|
|
299
|
-
} else {
|
|
300
|
-
const det = dx1 * dy2 - dx2 * dy1;
|
|
301
|
-
g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
302
|
-
h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
303
|
-
a = x2 - x1 + g * x2;
|
|
304
|
-
b = x3 - x1 + h_coeff * x3;
|
|
305
|
-
c = x1;
|
|
306
|
-
d = y2 - y1 + g * y2;
|
|
307
|
-
e = y3 - y1 + h_coeff * y3;
|
|
308
|
-
f = y1;
|
|
309
|
-
}
|
|
310
|
-
return [
|
|
311
|
-
a / w, d / w, 0, g / w,
|
|
312
|
-
b / h, e / h, 0, h_coeff / h,
|
|
313
|
-
0, 0, 1, 0,
|
|
314
|
-
c, f, 0, 1
|
|
315
|
-
];
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
288
|
+
const matrix = this._solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
319
289
|
|
|
320
290
|
this.overlay.style.maxWidth = 'none';
|
|
321
291
|
this.overlay.style.width = `${markerW}px`;
|
|
@@ -334,40 +304,96 @@ class SimpleAR {
|
|
|
334
304
|
`;
|
|
335
305
|
}
|
|
336
306
|
|
|
337
|
-
|
|
307
|
+
private _solveHomography(w: number, h: number, p1: any, p2: any, p3: any, p4: any) {
|
|
308
|
+
const x1 = p1.sx, y1 = p1.sy;
|
|
309
|
+
const x2 = p2.sx, y2 = p2.sy;
|
|
310
|
+
const x3 = p3.sx, y3 = p3.sy;
|
|
311
|
+
const x4 = p4.sx, y4 = p4.sy;
|
|
312
|
+
|
|
313
|
+
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
314
|
+
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
315
|
+
|
|
316
|
+
const det = dx1 * dy2 - dx2 * dy1;
|
|
317
|
+
const g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
318
|
+
const h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
319
|
+
const a = x2 - x1 + g * x2;
|
|
320
|
+
const b = x3 - x1 + h_coeff * x3;
|
|
321
|
+
const c = x1;
|
|
322
|
+
const d = y2 - y1 + g * y2;
|
|
323
|
+
const e = y3 - y1 + h_coeff * y3;
|
|
324
|
+
const f = y1;
|
|
325
|
+
|
|
326
|
+
return [
|
|
327
|
+
a / w, d / w, 0, g / w,
|
|
328
|
+
b / h, e / h, 0, h_coeff / h,
|
|
329
|
+
0, 0, 1, 0,
|
|
330
|
+
c, f, 0, 1
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- DEBUG METHODS ---
|
|
335
|
+
|
|
336
|
+
private _createDebugPanel() {
|
|
338
337
|
this.debugPanel = document.createElement('div');
|
|
339
338
|
this.debugPanel.style.cssText = `
|
|
340
|
-
position: absolute;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
font-family: monospace;
|
|
346
|
-
font-size: 12px;
|
|
347
|
-
padding: 8px;
|
|
348
|
-
border-radius: 4px;
|
|
349
|
-
z-index: 99999;
|
|
350
|
-
pointer-events: none;
|
|
351
|
-
line-height: 1.5;
|
|
339
|
+
position: absolute; top: 10px; left: 10px;
|
|
340
|
+
background: rgba(0, 0, 0, 0.7); color: #0f0;
|
|
341
|
+
font-family: monospace; font-size: 11px; padding: 10px;
|
|
342
|
+
border-radius: 5px; z-index: 100; pointer-events: none;
|
|
343
|
+
line-height: 1.4; border-left: 3px solid #0f0;
|
|
352
344
|
`;
|
|
353
345
|
this.container.appendChild(this.debugPanel);
|
|
354
346
|
}
|
|
355
347
|
|
|
356
|
-
|
|
348
|
+
private _createDebugCanvas() {
|
|
349
|
+
this.debugCanvas = document.createElement('canvas');
|
|
350
|
+
this.debugCanvas.width = this.container.clientWidth;
|
|
351
|
+
this.debugCanvas.height = this.container.clientHeight;
|
|
352
|
+
this.debugCanvas.style.cssText = `
|
|
353
|
+
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
354
|
+
pointer-events: none; z-index: 99;
|
|
355
|
+
`;
|
|
356
|
+
this.container.appendChild(this.debugCanvas);
|
|
357
|
+
this.debugCtx = this.debugCanvas.getContext('2d');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private _updateHUD(data: any) {
|
|
357
361
|
if (!this.debugPanel) return;
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const status = isTracking ? 'TRACKING' : 'SEARCHING';
|
|
362
|
+
const rel = data.reliabilities ? (data.reliabilities.reduce((a: any, b: any) => a + b, 0) / data.reliabilities.length).toFixed(2) : "0.00";
|
|
363
|
+
const stab = data.stabilities ? (data.stabilities.reduce((a: any, b: any) => a + b, 0) / data.stabilities.length).toFixed(2) : "0.00";
|
|
364
|
+
const savings = data.pixelsSaved ? ((data.pixelsSaved / (this.video!.videoWidth * this.video!.videoHeight)) * 100).toFixed(0) : "0";
|
|
362
365
|
|
|
363
366
|
this.debugPanel.innerHTML = `
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
<b>TapTapp AR HUD</b><br>
|
|
368
|
+
------------------<br>
|
|
369
|
+
STATUS: <span style="color:${this.isTracking ? '#0f0' : '#f00'}">${this.isTracking ? 'TRACKING' : 'SEARCHING'}</span><br>
|
|
370
|
+
FPS: ${this.fps}<br>
|
|
371
|
+
RELIAB: ${rel}<br>
|
|
372
|
+
STABIL: ${stab}<br>
|
|
373
|
+
SAVINGS: ${savings}% Pixels<br>
|
|
374
|
+
POINTS: ${data.screenCoords?.length || 0}
|
|
369
375
|
`;
|
|
370
376
|
}
|
|
377
|
+
|
|
378
|
+
private _drawDebugPoints(coords: any[], stabilities: any[]) {
|
|
379
|
+
if (!this.debugCtx) return;
|
|
380
|
+
this.debugCtx.clearRect(0, 0, this.debugCanvas!.width, this.debugCanvas!.height);
|
|
381
|
+
|
|
382
|
+
coords.forEach((p, i) => {
|
|
383
|
+
const s = stabilities ? (stabilities[i] || 0) : 0.5;
|
|
384
|
+
this.debugCtx!.fillStyle = `rgba(0, 255, 0, ${0.4 + s * 0.6})`;
|
|
385
|
+
this.debugCtx!.fillRect(p.x - 1, p.y - 1, 2, 2);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private _drawDebugFeatures(points: any[]) {
|
|
390
|
+
if (!this.debugCtx || this.isTracking) return;
|
|
391
|
+
this.debugCtx.clearRect(0, 0, this.debugCanvas!.width, this.debugCanvas!.height);
|
|
392
|
+
this.debugCtx.fillStyle = 'rgba(255, 255, 0, 0.4)';
|
|
393
|
+
points.slice(0, 200).forEach(p => {
|
|
394
|
+
this.debugCtx!.fillRect(p.x - 1, p.y - 1, 2, 2);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
371
397
|
}
|
|
372
398
|
|
|
373
399
|
export { SimpleAR };
|