@srsergio/taptapp-ar 1.0.7 → 1.0.9
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/README.md +5 -43
- package/dist/compiler/offline-compiler.d.ts +5 -36
- package/dist/compiler/offline-compiler.js +75 -10
- package/dist/compiler/utils/worker-pool.d.ts +2 -1
- package/dist/compiler/utils/worker-pool.js +4 -8
- package/package.json +2 -5
- package/src/compiler/offline-compiler.js +83 -12
- package/src/compiler/utils/worker-pool.js +4 -8
- package/src/astro/ARScene.astro +0 -59
- package/src/astro/ARVideoTrigger.astro +0 -73
- package/src/astro/overlays/ErrorOverlay.astro +0 -40
- package/src/astro/overlays/LoadingOverlay.astro +0 -28
- package/src/astro/overlays/ScanningOverlay.astro +0 -119
- package/src/astro/scripts/ARScripts.astro +0 -118
- package/src/astro/styles/ARStyles.astro +0 -147
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# @srsergio/taptapp-ar
|
|
2
2
|
|
|
3
|
-
🚀 **TapTapp AR** is a high-performance Augmented Reality (AR) toolkit
|
|
3
|
+
🚀 **TapTapp AR** is a high-performance Augmented Reality (AR) compiler toolkit for **Node.js** and **Browser** environments. It provides an ultra-fast offline compiler for image targets.
|
|
4
4
|
|
|
5
|
-
Built
|
|
5
|
+
Built with performance in mind, this package features a **pure JavaScript offline compiler** that requires **no TensorFlow** for compilation, generating high-quality `.mind` files in record time.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 🌟 Key Features
|
|
10
10
|
|
|
11
|
-
- 🚀 **Astro Native**: Optimized components for Astro's Islands architecture.
|
|
12
11
|
- 🖼️ **Ultra-Fast Offline Compiler**: Pure JavaScript compiler that generates `.mind` target files in **~1.3s per image**.
|
|
13
12
|
- ⚡ **Zero TensorFlow for Compilation**: The offline compiler uses optimized pure JS algorithms - no TensorFlow installation required.
|
|
14
13
|
- 🧵 **Multi-threaded Engine**: Truly parallel processing using Node.js `worker_threads` for bulk image compilation.
|
|
15
14
|
- 🚀 **Serverless Ready**: Lightweight compiler with minimal dependencies, perfect for Vercel, AWS Lambda, and Netlify.
|
|
15
|
+
- 📦 **Protocol V3 (Columnar Binary)**: Industry-leading performance with zero-copy loading and 80%+ smaller files.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -26,34 +26,6 @@ npm install @srsergio/taptapp-ar
|
|
|
26
26
|
|
|
27
27
|
> **Note:** TensorFlow is **NOT required** for the offline compiler. It only uses pure JavaScript.
|
|
28
28
|
|
|
29
|
-
For real-time AR tracking in the browser, TensorFlow.js is loaded automatically via CDN.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## 🚀 Astro Integration Guide
|
|
34
|
-
|
|
35
|
-
The easiest way to display AR content is using the `ARVideoTrigger` component.
|
|
36
|
-
|
|
37
|
-
### Usage
|
|
38
|
-
|
|
39
|
-
```astro
|
|
40
|
-
---
|
|
41
|
-
import ARVideoTrigger from '@srsergio/taptapp-ar/astro/ARVideoTrigger.astro';
|
|
42
|
-
|
|
43
|
-
const config = {
|
|
44
|
-
cardId: 'unique-id',
|
|
45
|
-
targetImageSrc: 'https://cdn.example.com/target.jpg',
|
|
46
|
-
targetMindSrc: 'https://cdn.example.com/targets.mind',
|
|
47
|
-
videoSrc: 'https://cdn.example.com/overlay.mp4',
|
|
48
|
-
videoWidth: 1280,
|
|
49
|
-
videoHeight: 720,
|
|
50
|
-
scale: 1.2,
|
|
51
|
-
};
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
<ARVideoTrigger config={config} />
|
|
55
|
-
```
|
|
56
|
-
|
|
57
29
|
---
|
|
58
30
|
|
|
59
31
|
## 🖼️ High-Performance Compiler (Protocol V3)
|
|
@@ -83,7 +55,7 @@ TaptApp AR features the industry's most advanced **pure JavaScript** offline com
|
|
|
83
55
|
Optimized for server-side compilation with multi-core parallelism:
|
|
84
56
|
|
|
85
57
|
```javascript
|
|
86
|
-
import { OfflineCompiler } from '@srsergio/taptapp-ar
|
|
58
|
+
import { OfflineCompiler } from '@srsergio/taptapp-ar';
|
|
87
59
|
|
|
88
60
|
const compiler = new OfflineCompiler();
|
|
89
61
|
|
|
@@ -100,22 +72,12 @@ const binaryBuffer = compiler.exportData(); // Yields a much smaller .mind file
|
|
|
100
72
|
### 🌐 Frontend (Zero-Latency Loading)
|
|
101
73
|
|
|
102
74
|
```javascript
|
|
103
|
-
import { OfflineCompiler } from '@srsergio/taptapp-ar
|
|
75
|
+
import { OfflineCompiler } from '@srsergio/taptapp-ar';
|
|
104
76
|
|
|
105
77
|
const compiler = new OfflineCompiler();
|
|
106
78
|
// Loading 127KB instead of 800KB saves bandwidth and CPU parsing time
|
|
107
79
|
compiler.importData(binaryBuffer);
|
|
108
80
|
```
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## ❓ Troubleshooting
|
|
112
|
-
|
|
113
|
-
| Issue | Solution |
|
|
114
|
-
| :--- | :--- |
|
|
115
|
-
| **Camera not starting** | Ensure your site is served via `HTTPS`. Browsers block camera access on insecure origins. |
|
|
116
|
-
| **Video not playing** | iOS Safari requires `muted` and `playsinline` attributes for autoplaying videos. Our components handle this by default. |
|
|
117
|
-
| **CORS errors** | Ensure that `targetImageSrc`, `targetMindSrc`, and `videoSrc` have CORS headers enabled (`Access-Control-Allow-Origin: *`). |
|
|
118
|
-
| **Memory Outage on Serverless** | Reduce the resolution of your target images. High-res images increase memory pressure during compilation. |
|
|
119
81
|
|
|
120
82
|
---
|
|
121
83
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export class OfflineCompiler {
|
|
5
5
|
data: any;
|
|
6
|
-
workerPool:
|
|
6
|
+
workerPool: WorkerPool | null;
|
|
7
7
|
_initNodeWorkers(): Promise<void>;
|
|
8
8
|
/**
|
|
9
9
|
* Compila una lista de imágenes objetivo
|
|
@@ -15,27 +15,11 @@ export class OfflineCompiler {
|
|
|
15
15
|
/**
|
|
16
16
|
* Compila datos de matching usando DetectorLite (JS puro)
|
|
17
17
|
*/
|
|
18
|
-
_compileMatch(targetImages: any, progressCallback: any): Promise<
|
|
19
|
-
maximaPoints: any[];
|
|
20
|
-
minimaPoints: any[];
|
|
21
|
-
maximaPointsCluster: {
|
|
22
|
-
rootNode: {
|
|
23
|
-
centerPointIndex: any;
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
minimaPointsCluster: {
|
|
27
|
-
rootNode: {
|
|
28
|
-
centerPointIndex: any;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
width: any;
|
|
32
|
-
height: any;
|
|
33
|
-
scale: any;
|
|
34
|
-
}[][]>;
|
|
18
|
+
_compileMatch(targetImages: any, progressCallback: any): Promise<any[]>;
|
|
35
19
|
/**
|
|
36
20
|
* Compila datos de tracking usando extractTrackingFeatures (JS puro)
|
|
37
21
|
*/
|
|
38
|
-
_compileTrack(targetImages: any, progressCallback: any): Promise<
|
|
22
|
+
_compileTrack(targetImages: any, progressCallback: any): Promise<any[]>;
|
|
39
23
|
/**
|
|
40
24
|
* Método público para compilar tracking (compatibilidad con API anterior)
|
|
41
25
|
* @param {Object} options - Opciones de compilación
|
|
@@ -56,23 +40,7 @@ export class OfflineCompiler {
|
|
|
56
40
|
progressCallback: any;
|
|
57
41
|
targetImages: any;
|
|
58
42
|
basePercent?: number | undefined;
|
|
59
|
-
}): Promise<
|
|
60
|
-
maximaPoints: any[];
|
|
61
|
-
minimaPoints: any[];
|
|
62
|
-
maximaPointsCluster: {
|
|
63
|
-
rootNode: {
|
|
64
|
-
centerPointIndex: any;
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
minimaPointsCluster: {
|
|
68
|
-
rootNode: {
|
|
69
|
-
centerPointIndex: any;
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
width: any;
|
|
73
|
-
height: any;
|
|
74
|
-
scale: any;
|
|
75
|
-
}[][]>;
|
|
43
|
+
}): Promise<any[]>;
|
|
76
44
|
/**
|
|
77
45
|
* Exporta datos compilados en formato binario columnar optimizado
|
|
78
46
|
*/
|
|
@@ -173,3 +141,4 @@ export class OfflineCompiler {
|
|
|
173
141
|
*/
|
|
174
142
|
destroy(): Promise<void>;
|
|
175
143
|
}
|
|
144
|
+
import { WorkerPool } from "./utils/worker-pool.js";
|
|
@@ -19,6 +19,7 @@ import { extractTrackingFeatures } from "./tracker/extract-utils.js";
|
|
|
19
19
|
import { DetectorLite } from "./detector/detector-lite.js";
|
|
20
20
|
import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
|
|
21
21
|
import * as msgpack from "@msgpack/msgpack";
|
|
22
|
+
import { WorkerPool } from "./utils/worker-pool.js";
|
|
22
23
|
// Detect environment
|
|
23
24
|
const isNode = typeof process !== "undefined" &&
|
|
24
25
|
process.versions != null &&
|
|
@@ -41,21 +42,27 @@ export class OfflineCompiler {
|
|
|
41
42
|
}
|
|
42
43
|
async _initNodeWorkers() {
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// Use variables to prevent bundlers from trying to bundle these
|
|
46
|
+
const pathModule = "path";
|
|
47
|
+
const urlModule = "url";
|
|
48
|
+
const osModule = "os";
|
|
49
|
+
const workerThreadsModule = "node:worker_threads";
|
|
50
|
+
const [path, url, os, { Worker }] = await Promise.all([
|
|
51
|
+
import(pathModule),
|
|
52
|
+
import(urlModule),
|
|
53
|
+
import(osModule),
|
|
54
|
+
import(workerThreadsModule)
|
|
49
55
|
]);
|
|
50
56
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
51
57
|
const __dirname = path.dirname(__filename);
|
|
52
58
|
const workerPath = path.join(__dirname, "node-worker.js");
|
|
59
|
+
// Limit workers to avoid freezing system
|
|
53
60
|
const numWorkers = Math.min(os.cpus().length, 4);
|
|
54
|
-
this.workerPool = new
|
|
61
|
+
this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
|
|
55
62
|
console.log(`🚀 OfflineCompiler: Node.js mode with ${numWorkers} workers`);
|
|
56
63
|
}
|
|
57
64
|
catch (e) {
|
|
58
|
-
console.log("⚡ OfflineCompiler: Running without workers");
|
|
65
|
+
console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
@@ -120,9 +127,48 @@ export class OfflineCompiler {
|
|
|
120
127
|
async _compileMatch(targetImages, progressCallback) {
|
|
121
128
|
const percentPerImage = 100 / targetImages.length;
|
|
122
129
|
let currentPercent = 0;
|
|
130
|
+
// Use workers if available
|
|
131
|
+
if (this.workerPool) {
|
|
132
|
+
const promises = targetImages.map((targetImage, index) => {
|
|
133
|
+
return this.workerPool.runTask({
|
|
134
|
+
type: 'match',
|
|
135
|
+
targetImage,
|
|
136
|
+
percentPerImage,
|
|
137
|
+
basePercent: index * percentPerImage,
|
|
138
|
+
onProgress: (percent) => {
|
|
139
|
+
// Basic aggregation: this assumes naive progress updates.
|
|
140
|
+
// Ideally we should track exact progress per image.
|
|
141
|
+
// For now, simpler to just let the main thread loop handle overall progress callback?
|
|
142
|
+
// No, workers are async. We need to aggregate.
|
|
143
|
+
// Actually, the main loop below is serial.
|
|
144
|
+
// If we use workers, we run them in parallel.
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
// Progress handling for parallel workers is tricky without a shared state manager.
|
|
149
|
+
// Simplified approach: each worker reports its absolute progress contribution?
|
|
150
|
+
// No, worker reports 'percent' which is base + local.
|
|
151
|
+
// We can use a shared loadedPercent variable.
|
|
152
|
+
let totalPercent = 0;
|
|
153
|
+
const progressMap = new Float32Array(targetImages.length);
|
|
154
|
+
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
155
|
+
return this.workerPool.runTask({
|
|
156
|
+
type: 'match',
|
|
157
|
+
targetImage,
|
|
158
|
+
percentPerImage, // Not really needed for logic but worker expects it
|
|
159
|
+
basePercent: 0, // Worker will report 0-percentPerImage roughly
|
|
160
|
+
onProgress: (p) => {
|
|
161
|
+
// This 'p' from worker is "base + local". If we passed base=0, it's just local (0 to percentPerImage)
|
|
162
|
+
progressMap[index] = p;
|
|
163
|
+
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
164
|
+
progressCallback(sum);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
return Promise.all(wrappedPromises);
|
|
169
|
+
}
|
|
170
|
+
// Serial Fallback
|
|
123
171
|
const results = [];
|
|
124
|
-
// Procesar secuencialmente para evitar overhead de workers
|
|
125
|
-
// (los workers son útiles para muchas imágenes, pero añaden latencia)
|
|
126
172
|
for (let i = 0; i < targetImages.length; i++) {
|
|
127
173
|
const targetImage = targetImages[i];
|
|
128
174
|
const imageList = buildImageList(targetImage);
|
|
@@ -157,6 +203,24 @@ export class OfflineCompiler {
|
|
|
157
203
|
async _compileTrack(targetImages, progressCallback) {
|
|
158
204
|
const percentPerImage = 100 / targetImages.length;
|
|
159
205
|
let currentPercent = 0;
|
|
206
|
+
if (this.workerPool) {
|
|
207
|
+
const progressMap = new Float32Array(targetImages.length);
|
|
208
|
+
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
209
|
+
return this.workerPool.runTask({
|
|
210
|
+
type: 'compile',
|
|
211
|
+
targetImage,
|
|
212
|
+
percentPerImage,
|
|
213
|
+
basePercent: 0,
|
|
214
|
+
onProgress: (p) => {
|
|
215
|
+
progressMap[index] = p;
|
|
216
|
+
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
217
|
+
progressCallback(sum);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
return Promise.all(wrappedPromises);
|
|
222
|
+
}
|
|
223
|
+
// Serial Fallback
|
|
160
224
|
const results = [];
|
|
161
225
|
for (let i = 0; i < targetImages.length; i++) {
|
|
162
226
|
const targetImage = targetImages[i];
|
|
@@ -231,7 +295,8 @@ export class OfflineCompiler {
|
|
|
231
295
|
return msgpack.encode({
|
|
232
296
|
v: CURRENT_VERSION,
|
|
233
297
|
dataList,
|
|
234
|
-
|
|
298
|
+
// eslint-disable-next-line
|
|
299
|
+
}); // eslint-disable-line
|
|
235
300
|
}
|
|
236
301
|
_packKeyframe(kf) {
|
|
237
302
|
return {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Worker } from 'node:worker_threads';
|
|
2
|
-
import os from 'node:os';
|
|
3
1
|
export class WorkerPool {
|
|
4
|
-
constructor(workerPath, poolSize
|
|
2
|
+
constructor(workerPath, poolSize, WorkerClass) {
|
|
5
3
|
this.workerPath = workerPath;
|
|
6
4
|
this.poolSize = poolSize;
|
|
5
|
+
this.WorkerClass = WorkerClass;
|
|
7
6
|
this.workers = [];
|
|
8
7
|
this.queue = [];
|
|
9
8
|
this.activeWorkers = 0;
|
|
@@ -24,7 +23,7 @@ export class WorkerPool {
|
|
|
24
23
|
}
|
|
25
24
|
_createWorker() {
|
|
26
25
|
this.activeWorkers++;
|
|
27
|
-
const worker = new
|
|
26
|
+
const worker = new this.WorkerClass(this.workerPath);
|
|
28
27
|
return worker;
|
|
29
28
|
}
|
|
30
29
|
_executeTask(worker, task) {
|
|
@@ -62,10 +61,7 @@ export class WorkerPool {
|
|
|
62
61
|
serializableData[key] = value;
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
|
-
worker.postMessage(
|
|
66
|
-
type: 'compile',
|
|
67
|
-
...serializableData
|
|
68
|
-
});
|
|
64
|
+
worker.postMessage(serializableData);
|
|
69
65
|
}
|
|
70
66
|
_finishTask(worker, callback, result) {
|
|
71
67
|
if (this.queue.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "AR
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "AR Compiler for Node.js and Browser",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/srsergiolazaro/taptapp-ar.git"
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"import": "./dist/index.js"
|
|
21
21
|
},
|
|
22
|
-
"./astro/*": "./src/astro/*",
|
|
23
22
|
"./compiler/*": "./src/compiler/*"
|
|
24
23
|
},
|
|
25
24
|
"files": [
|
|
@@ -34,7 +33,6 @@
|
|
|
34
33
|
},
|
|
35
34
|
"peerDependencies": {
|
|
36
35
|
"aframe": ">=1.5.0",
|
|
37
|
-
"astro": ">=4.0.0",
|
|
38
36
|
"react": ">=18.0.0",
|
|
39
37
|
"react-dom": ">=18.0.0",
|
|
40
38
|
"three": ">=0.160.0"
|
|
@@ -48,7 +46,6 @@
|
|
|
48
46
|
"@types/react": "^18.3.3",
|
|
49
47
|
"@types/react-dom": "^18.3.0",
|
|
50
48
|
"@types/three": "^0.170.0",
|
|
51
|
-
"astro": "5.16.4",
|
|
52
49
|
"jimp": "^1.6.0",
|
|
53
50
|
"typescript": "^5.4.5",
|
|
54
51
|
"vitest": "^4.0.16"
|
|
@@ -20,6 +20,7 @@ import { extractTrackingFeatures } from "./tracker/extract-utils.js";
|
|
|
20
20
|
import { DetectorLite } from "./detector/detector-lite.js";
|
|
21
21
|
import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
|
|
22
22
|
import * as msgpack from "@msgpack/msgpack";
|
|
23
|
+
import { WorkerPool } from "./utils/worker-pool.js";
|
|
23
24
|
|
|
24
25
|
// Detect environment
|
|
25
26
|
const isNode = typeof process !== "undefined" &&
|
|
@@ -46,22 +47,30 @@ export class OfflineCompiler {
|
|
|
46
47
|
|
|
47
48
|
async _initNodeWorkers() {
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// Use variables to prevent bundlers from trying to bundle these
|
|
51
|
+
const pathModule = "path";
|
|
52
|
+
const urlModule = "url";
|
|
53
|
+
const osModule = "os";
|
|
54
|
+
const workerThreadsModule = "node:worker_threads";
|
|
55
|
+
|
|
56
|
+
const [path, url, os, { Worker }] = await Promise.all([
|
|
57
|
+
import(pathModule),
|
|
58
|
+
import(urlModule),
|
|
59
|
+
import(osModule),
|
|
60
|
+
import(workerThreadsModule)
|
|
54
61
|
]);
|
|
55
62
|
|
|
56
63
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
57
64
|
const __dirname = path.dirname(__filename);
|
|
58
65
|
const workerPath = path.join(__dirname, "node-worker.js");
|
|
59
66
|
|
|
67
|
+
// Limit workers to avoid freezing system
|
|
60
68
|
const numWorkers = Math.min(os.cpus().length, 4);
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
|
|
62
71
|
console.log(`🚀 OfflineCompiler: Node.js mode with ${numWorkers} workers`);
|
|
63
72
|
} catch (e) {
|
|
64
|
-
console.log("⚡ OfflineCompiler: Running without workers");
|
|
73
|
+
console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -142,10 +151,53 @@ export class OfflineCompiler {
|
|
|
142
151
|
const percentPerImage = 100 / targetImages.length;
|
|
143
152
|
let currentPercent = 0;
|
|
144
153
|
|
|
145
|
-
|
|
154
|
+
// Use workers if available
|
|
155
|
+
if (this.workerPool) {
|
|
156
|
+
const promises = targetImages.map((targetImage, index) => {
|
|
157
|
+
return this.workerPool.runTask({
|
|
158
|
+
type: 'match',
|
|
159
|
+
targetImage,
|
|
160
|
+
percentPerImage,
|
|
161
|
+
basePercent: index * percentPerImage,
|
|
162
|
+
onProgress: (percent) => {
|
|
163
|
+
// Basic aggregation: this assumes naive progress updates.
|
|
164
|
+
// Ideally we should track exact progress per image.
|
|
165
|
+
// For now, simpler to just let the main thread loop handle overall progress callback?
|
|
166
|
+
// No, workers are async. We need to aggregate.
|
|
167
|
+
// Actually, the main loop below is serial.
|
|
168
|
+
// If we use workers, we run them in parallel.
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Progress handling for parallel workers is tricky without a shared state manager.
|
|
174
|
+
// Simplified approach: each worker reports its absolute progress contribution?
|
|
175
|
+
// No, worker reports 'percent' which is base + local.
|
|
176
|
+
// We can use a shared loadedPercent variable.
|
|
177
|
+
|
|
178
|
+
let totalPercent = 0;
|
|
179
|
+
const progressMap = new Float32Array(targetImages.length);
|
|
180
|
+
|
|
181
|
+
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
182
|
+
return this.workerPool.runTask({
|
|
183
|
+
type: 'match',
|
|
184
|
+
targetImage,
|
|
185
|
+
percentPerImage, // Not really needed for logic but worker expects it
|
|
186
|
+
basePercent: 0, // Worker will report 0-percentPerImage roughly
|
|
187
|
+
onProgress: (p) => {
|
|
188
|
+
// This 'p' from worker is "base + local". If we passed base=0, it's just local (0 to percentPerImage)
|
|
189
|
+
progressMap[index] = p;
|
|
190
|
+
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
191
|
+
progressCallback(sum);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return Promise.all(wrappedPromises);
|
|
197
|
+
}
|
|
146
198
|
|
|
147
|
-
//
|
|
148
|
-
|
|
199
|
+
// Serial Fallback
|
|
200
|
+
const results = [];
|
|
149
201
|
for (let i = 0; i < targetImages.length; i++) {
|
|
150
202
|
const targetImage = targetImages[i];
|
|
151
203
|
const imageList = buildImageList(targetImage);
|
|
@@ -189,8 +241,26 @@ export class OfflineCompiler {
|
|
|
189
241
|
const percentPerImage = 100 / targetImages.length;
|
|
190
242
|
let currentPercent = 0;
|
|
191
243
|
|
|
192
|
-
|
|
244
|
+
if (this.workerPool) {
|
|
245
|
+
const progressMap = new Float32Array(targetImages.length);
|
|
246
|
+
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
247
|
+
return this.workerPool.runTask({
|
|
248
|
+
type: 'compile',
|
|
249
|
+
targetImage,
|
|
250
|
+
percentPerImage,
|
|
251
|
+
basePercent: 0,
|
|
252
|
+
onProgress: (p) => {
|
|
253
|
+
progressMap[index] = p;
|
|
254
|
+
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
255
|
+
progressCallback(sum);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
return Promise.all(wrappedPromises);
|
|
260
|
+
}
|
|
193
261
|
|
|
262
|
+
// Serial Fallback
|
|
263
|
+
const results = [];
|
|
194
264
|
for (let i = 0; i < targetImages.length; i++) {
|
|
195
265
|
const targetImage = targetImages[i];
|
|
196
266
|
const imageList = buildTrackingImageList(targetImage);
|
|
@@ -274,7 +344,8 @@ export class OfflineCompiler {
|
|
|
274
344
|
return msgpack.encode({
|
|
275
345
|
v: CURRENT_VERSION,
|
|
276
346
|
dataList,
|
|
277
|
-
|
|
347
|
+
// eslint-disable-next-line
|
|
348
|
+
}); // eslint-disable-line
|
|
278
349
|
}
|
|
279
350
|
|
|
280
351
|
_packKeyframe(kf) {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Worker } from 'node:worker_threads';
|
|
2
|
-
import os from 'node:os';
|
|
3
1
|
|
|
4
2
|
export class WorkerPool {
|
|
5
|
-
constructor(workerPath, poolSize
|
|
3
|
+
constructor(workerPath, poolSize, WorkerClass) {
|
|
6
4
|
this.workerPath = workerPath;
|
|
7
5
|
this.poolSize = poolSize;
|
|
6
|
+
this.WorkerClass = WorkerClass;
|
|
8
7
|
this.workers = [];
|
|
9
8
|
this.queue = [];
|
|
10
9
|
this.activeWorkers = 0;
|
|
@@ -25,7 +24,7 @@ export class WorkerPool {
|
|
|
25
24
|
|
|
26
25
|
_createWorker() {
|
|
27
26
|
this.activeWorkers++;
|
|
28
|
-
const worker = new
|
|
27
|
+
const worker = new this.WorkerClass(this.workerPath);
|
|
29
28
|
return worker;
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -66,10 +65,7 @@ export class WorkerPool {
|
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
worker.postMessage(
|
|
70
|
-
type: 'compile',
|
|
71
|
-
...serializableData
|
|
72
|
-
});
|
|
68
|
+
worker.postMessage(serializableData);
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
_finishTask(worker, callback, result) {
|
package/src/astro/ARScene.astro
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { PropsConfig } from "../react/types";
|
|
3
|
-
interface Props {
|
|
4
|
-
config: PropsConfig;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const { config } = Astro.props;
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
<a-scene
|
|
11
|
-
mindar-image={`imageTargetSrc: ${config.targetMindSrc};uiLoading: #loading-overlay;uiScanning: #scanning-overlay;uiError: #error-overlay`}
|
|
12
|
-
color-space="sRGB"
|
|
13
|
-
renderer="colorManagement: true, physicallyCorrectLights"
|
|
14
|
-
vr-mode-ui="enabled: false"
|
|
15
|
-
device-orientation-permission-ui="enabled: false"
|
|
16
|
-
>
|
|
17
|
-
<a-assets>
|
|
18
|
-
<img id="target-image" crossorigin="anonymous" src={config.targetImageSrc} />
|
|
19
|
-
<video
|
|
20
|
-
id="ar-video"
|
|
21
|
-
src={config.videoSrc}
|
|
22
|
-
preload="auto"
|
|
23
|
-
loop
|
|
24
|
-
autoplay
|
|
25
|
-
playsinline
|
|
26
|
-
webkit-playsinline
|
|
27
|
-
muted
|
|
28
|
-
crossorigin="anonymous"
|
|
29
|
-
class="absolute opacity-0"
|
|
30
|
-
width="1"
|
|
31
|
-
height="1"></video></a-assets
|
|
32
|
-
>
|
|
33
|
-
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
|
|
34
|
-
|
|
35
|
-
<a-entity mindar-image-target="targetIndex: 0">
|
|
36
|
-
<a-video
|
|
37
|
-
id="main-video"
|
|
38
|
-
src="#ar-video"
|
|
39
|
-
position="0 0 0"
|
|
40
|
-
opacity="1"
|
|
41
|
-
width={(config.videoWidth / config.videoHeight) * config.scale}
|
|
42
|
-
height={1 * config.scale}
|
|
43
|
-
rotation="0 0 0"
|
|
44
|
-
visible="true"
|
|
45
|
-
play-on-click
|
|
46
|
-
autoplay></a-video>
|
|
47
|
-
</a-entity>
|
|
48
|
-
</a-scene>
|
|
49
|
-
|
|
50
|
-
<style>
|
|
51
|
-
video {
|
|
52
|
-
width: 100% !important;
|
|
53
|
-
height: 100% !important;
|
|
54
|
-
object-fit: cover !important;
|
|
55
|
-
position: absolute !important;
|
|
56
|
-
top: 0 !important;
|
|
57
|
-
left: 0 !important;
|
|
58
|
-
}
|
|
59
|
-
</style>
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import ARStyles from "./styles/ARStyles.astro";
|
|
3
|
-
import LoadingOverlay from "./overlays/LoadingOverlay.astro";
|
|
4
|
-
import ScanningOverlay from "./overlays/ScanningOverlay.astro";
|
|
5
|
-
import ErrorOverlay from "./overlays/ErrorOverlay.astro";
|
|
6
|
-
import ARScene from "./ARScene.astro";
|
|
7
|
-
import ARScripts from "./scripts/ARScripts.astro";
|
|
8
|
-
import type { PropsConfig } from "../react/types";
|
|
9
|
-
interface Props {
|
|
10
|
-
config: PropsConfig;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const {config} = Astro.props ;
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
<!doctype html>
|
|
17
|
-
<html lang="es-PE">
|
|
18
|
-
<head>
|
|
19
|
-
<meta charset="UTF-8" />
|
|
20
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21
|
-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
22
|
-
<meta name="robots" content="index, follow" />
|
|
23
|
-
<link rel="canonical" href="https://go.taptapp.xyz" />
|
|
24
|
-
|
|
25
|
-
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
26
|
-
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any" />
|
|
27
|
-
<meta name="theme-color" content="#3367D6" />
|
|
28
|
-
<meta
|
|
29
|
-
name="description"
|
|
30
|
-
content="Crea impacto duradero y genera confianza en tus clientes con la tarjeta digital TapTapp. Presenta tu información de manera clara y atractiva para conectar con el futuro."
|
|
31
|
-
/>
|
|
32
|
-
<meta name="keywords" content="tarjeta digital, identidad, futuro, confianza, clientes" />
|
|
33
|
-
<meta name="author" content="@srsergiolazaro" />
|
|
34
|
-
|
|
35
|
-
<meta property="og:title" content="TapTapp AR Experience" />
|
|
36
|
-
<meta
|
|
37
|
-
property="og:description"
|
|
38
|
-
content="Descubre nuestra experiencia de realidad aumentada y da vida a tu tarjeta digital TapTapp."
|
|
39
|
-
/>
|
|
40
|
-
<meta property="og:image" content="https://go.taptapp.xyz/Imgsocial.jpg" />
|
|
41
|
-
<meta property="og:url" content="https://go.taptapp.xyz" />
|
|
42
|
-
<link rel="manifest" href="/manifest.webmanifest" crossorigin="anonymous" />
|
|
43
|
-
|
|
44
|
-
<title>TapTapp AR Experience</title>
|
|
45
|
-
<script is:inline src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
|
|
46
|
-
<script is:inline src="https://r2-worker.sergiolazaromondargo.workers.dev/taptapp-ar.prod.js"
|
|
47
|
-
></script>
|
|
48
|
-
<link
|
|
49
|
-
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
|
|
50
|
-
rel="stylesheet"
|
|
51
|
-
/>
|
|
52
|
-
<ARStyles />
|
|
53
|
-
<ARScripts />
|
|
54
|
-
<style>
|
|
55
|
-
body {
|
|
56
|
-
font-family: "Montserrat", sans-serif;
|
|
57
|
-
margin: 0;
|
|
58
|
-
overflow: hidden;
|
|
59
|
-
}
|
|
60
|
-
@view-transition {
|
|
61
|
-
navigation: auto;
|
|
62
|
-
}
|
|
63
|
-
</style>
|
|
64
|
-
</head>
|
|
65
|
-
<body>
|
|
66
|
-
<!-- Custom UI Overlays -->
|
|
67
|
-
<LoadingOverlay />
|
|
68
|
-
<ScanningOverlay targetImage={config.targetImageSrc} />
|
|
69
|
-
<ErrorOverlay />
|
|
70
|
-
|
|
71
|
-
<ARScene config={config} />
|
|
72
|
-
</body>
|
|
73
|
-
</html>
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<div id="error-overlay" class="custom-overlay hidden bg-gradient-to-br from-red-700 to-purple-700">
|
|
2
|
-
<div class="mb-5 text-5xl">⚠️</div>
|
|
3
|
-
<div class="font-semibold tracking-wide text-white/95 text-xl">No se pudo iniciar</div>
|
|
4
|
-
<div class="mt-3 max-w-[80%] text-center font-light text-white/75 text-sm">
|
|
5
|
-
Verifique los permisos de cámara o intente con otro dispositivo
|
|
6
|
-
</div>
|
|
7
|
-
<button
|
|
8
|
-
onclick="location.reload()"
|
|
9
|
-
class="mt-6 rounded-lg bg-blue-500 px-6 py-2.5 font-semibold text-white transition-all hover:bg-blue-600 hover:shadow-lg"
|
|
10
|
-
>
|
|
11
|
-
Reintentar
|
|
12
|
-
</button>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<style>
|
|
16
|
-
#error-overlay {
|
|
17
|
-
background: linear-gradient(135deg, #c0392b, #8e44ad);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.error-icon {
|
|
21
|
-
font-size: 50px;
|
|
22
|
-
margin-bottom: 20px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.retry-button {
|
|
26
|
-
margin-top: 20px;
|
|
27
|
-
padding: 10px 20px;
|
|
28
|
-
background-color: #3498db;
|
|
29
|
-
color: white;
|
|
30
|
-
border: none;
|
|
31
|
-
border-radius: 5px;
|
|
32
|
-
font-weight: 600;
|
|
33
|
-
cursor: pointer;
|
|
34
|
-
transition: background-color 0.3s ease;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.retry-button:hover {
|
|
38
|
-
background-color: #2980b9;
|
|
39
|
-
}
|
|
40
|
-
</style>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<div id="loading-overlay" class="custom-overlay bg-gradient-to-br from-gray-800 to-gray-900">
|
|
2
|
-
<div class="loader"></div>
|
|
3
|
-
<div class="mt-5 font-semibold tracking-wide text-white/95 text-xl">Preparando...</div>
|
|
4
|
-
<div class="mt-3 max-w-[80%] text-center font-light text-white/75 text-sm">
|
|
5
|
-
Un momento mientras optimizamos su experiencia
|
|
6
|
-
</div>
|
|
7
|
-
</div>
|
|
8
|
-
|
|
9
|
-
<style>
|
|
10
|
-
.loader {
|
|
11
|
-
width: 60px;
|
|
12
|
-
height: 60px;
|
|
13
|
-
border: 5px solid rgba(255, 255, 255, 0.2);
|
|
14
|
-
border-top: 5px solid #3498db;
|
|
15
|
-
border-radius: 50%;
|
|
16
|
-
animation: spin 1s linear infinite;
|
|
17
|
-
margin-bottom: 20px;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
@keyframes spin {
|
|
21
|
-
0% {
|
|
22
|
-
transform: rotate(0deg);
|
|
23
|
-
}
|
|
24
|
-
100% {
|
|
25
|
-
transform: rotate(360deg);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
interface Props {
|
|
3
|
-
targetImage?: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const { targetImage } = Astro.props;
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
<div
|
|
10
|
-
id="scanning-overlay"
|
|
11
|
-
class="custom-overlay relative flex flex-col items-center justify-center"
|
|
12
|
-
>
|
|
13
|
-
<div class="scan-region relative mb-8 h-[300px] w-[300px] overflow-hidden">
|
|
14
|
-
<!-- Esquinas del recuadro de escaneo -->
|
|
15
|
-
<div class="corner top-left"></div>
|
|
16
|
-
<div class="corner top-right"></div>
|
|
17
|
-
<div class="corner bottom-left"></div>
|
|
18
|
-
<div class="corner bottom-right"></div>
|
|
19
|
-
|
|
20
|
-
<div class="absolute inset-0 p-6">
|
|
21
|
-
<img id="target-preview" src={targetImage} class="h-full w-full object-contain opacity-90" />
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div class="scan-animation"></div>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<div class="font-semibold tracking-wide text-white/95 text-2xl">Escaneando...</div>
|
|
28
|
-
<div class="mt-3 max-w-[80%] text-center font-light text-white/75 text-sm">
|
|
29
|
-
Centre el sticker dentro del marco
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<style>
|
|
34
|
-
.scan-region img {
|
|
35
|
-
width: 100%;
|
|
36
|
-
height: 100%;
|
|
37
|
-
object-fit: contain;
|
|
38
|
-
opacity: 0.7;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.scan-animation {
|
|
42
|
-
position: absolute;
|
|
43
|
-
top: 0;
|
|
44
|
-
left: 0;
|
|
45
|
-
width: 100%;
|
|
46
|
-
height: 2px;
|
|
47
|
-
background: linear-gradient(
|
|
48
|
-
90deg,
|
|
49
|
-
rgba(52, 152, 219, 0),
|
|
50
|
-
rgba(52, 152, 219, 0.8),
|
|
51
|
-
rgba(155, 89, 182, 0.8),
|
|
52
|
-
rgba(52, 152, 219, 0)
|
|
53
|
-
);
|
|
54
|
-
box-shadow:
|
|
55
|
-
0 0 10px rgba(52, 152, 219, 0.7),
|
|
56
|
-
0 0 20px rgba(155, 89, 182, 0.4);
|
|
57
|
-
animation: scan 2.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@keyframes scan {
|
|
61
|
-
0% {
|
|
62
|
-
transform: translateY(0);
|
|
63
|
-
opacity: 0.8;
|
|
64
|
-
}
|
|
65
|
-
15% {
|
|
66
|
-
opacity: 1;
|
|
67
|
-
}
|
|
68
|
-
50% {
|
|
69
|
-
transform: translateY(298px);
|
|
70
|
-
opacity: 1;
|
|
71
|
-
}
|
|
72
|
-
85% {
|
|
73
|
-
opacity: 0.8;
|
|
74
|
-
}
|
|
75
|
-
100% {
|
|
76
|
-
transform: translateY(0);
|
|
77
|
-
opacity: 0.8;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* Estilos para las esquinas */
|
|
82
|
-
.corner {
|
|
83
|
-
position: absolute;
|
|
84
|
-
width: 24px;
|
|
85
|
-
height: 24px;
|
|
86
|
-
border-color: white;
|
|
87
|
-
border-style: solid;
|
|
88
|
-
opacity: 0.7;
|
|
89
|
-
z-index: 1;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.top-left {
|
|
93
|
-
top: 6px;
|
|
94
|
-
left: 6px;
|
|
95
|
-
border-width: 2px 0 0 2px;
|
|
96
|
-
border-top-left-radius: 6px;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.top-right {
|
|
100
|
-
top: 6px;
|
|
101
|
-
right: 6px;
|
|
102
|
-
border-width: 2px 2px 0 0;
|
|
103
|
-
border-top-right-radius: 6px;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.bottom-left {
|
|
107
|
-
bottom: 6px;
|
|
108
|
-
left: 6px;
|
|
109
|
-
border-width: 0 0 2px 2px;
|
|
110
|
-
border-bottom-left-radius: 6px;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.bottom-right {
|
|
114
|
-
bottom: 6px;
|
|
115
|
-
right: 6px;
|
|
116
|
-
border-width: 0 2px 2px 0;
|
|
117
|
-
border-bottom-right-radius: 6px;
|
|
118
|
-
}
|
|
119
|
-
</style>
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<script is:inline>
|
|
2
|
-
// Lógica mejorada para reproducción de video
|
|
3
|
-
let started = false;
|
|
4
|
-
let playing = false;
|
|
5
|
-
|
|
6
|
-
window.addEventListener("click", async function () {
|
|
7
|
-
try {
|
|
8
|
-
let video = document.querySelector("#ar-video");
|
|
9
|
-
let avideo = document.querySelector("a-video");
|
|
10
|
-
|
|
11
|
-
if (!video || !avideo) {
|
|
12
|
-
console.error("Elementos de video no encontrados");
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (!started) {
|
|
17
|
-
await video.play();
|
|
18
|
-
avideo.setAttribute("opacity", 1);
|
|
19
|
-
started = true;
|
|
20
|
-
playing = true;
|
|
21
|
-
} else {
|
|
22
|
-
if (playing) {
|
|
23
|
-
video.pause();
|
|
24
|
-
playing = false;
|
|
25
|
-
} else {
|
|
26
|
-
await video.play();
|
|
27
|
-
playing = true;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error("Error en la reproducción del video:", error);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<script is:inline>
|
|
37
|
-
document.addEventListener("DOMContentLoaded", function () {
|
|
38
|
-
const scene = document.querySelector("a-scene");
|
|
39
|
-
const errorOverlay = document.getElementById("error-overlay");
|
|
40
|
-
const scanningOverlay = document.getElementById("scanning-overlay");
|
|
41
|
-
const videoEl = document.querySelector("#ar-video");
|
|
42
|
-
const mainVideo = document.querySelector("#main-video");
|
|
43
|
-
const targetImage = document.querySelector("#target-image");
|
|
44
|
-
|
|
45
|
-
if (!scene || !errorOverlay || !scanningOverlay || !videoEl || !mainVideo || !targetImage) {
|
|
46
|
-
console.error("Elementos críticos no encontrados");
|
|
47
|
-
if (errorOverlay) errorOverlay.classList.remove("hidden");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Verificación de recursos
|
|
52
|
-
targetImage.addEventListener("error", () => {
|
|
53
|
-
console.error("Error al cargar la imagen objetivo");
|
|
54
|
-
errorOverlay.classList.remove("hidden");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
videoEl.addEventListener("error", () => {
|
|
58
|
-
console.error("Error al cargar el video");
|
|
59
|
-
errorOverlay.classList.remove("hidden");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Gestión mejorada de errores AR
|
|
63
|
-
scene.addEventListener("arError", (event) => {
|
|
64
|
-
console.error("Error AR:", event);
|
|
65
|
-
errorOverlay.classList.remove("hidden");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Verificar si MindAR está disponible
|
|
69
|
-
if (!window.MINDAR || !window.MINDAR.IMAGE) {
|
|
70
|
-
console.error("MindAR no está disponible");
|
|
71
|
-
errorOverlay.classList.remove("hidden");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Manejo de eventos de target
|
|
76
|
-
const targetEntity = document.querySelector("a-entity[mindar-image-target]");
|
|
77
|
-
|
|
78
|
-
targetEntity.addEventListener("targetFound", () => {
|
|
79
|
-
console.log("¡Objetivo encontrado!");
|
|
80
|
-
scanningOverlay.classList.add("hidden");
|
|
81
|
-
|
|
82
|
-
// Hacer visible el video
|
|
83
|
-
mainVideo.setAttribute("opacity", "1");
|
|
84
|
-
|
|
85
|
-
// Intentar reproducir
|
|
86
|
-
videoEl.play().catch((err) => {
|
|
87
|
-
console.error("Error reproduciendo video:", err);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
targetEntity.addEventListener("targetLost", () => {
|
|
92
|
-
console.log("Objetivo perdido");
|
|
93
|
-
scanningOverlay.classList.remove("hidden");
|
|
94
|
-
mainVideo.setAttribute("opacity", "0");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Función para aplicar estilos adaptativos
|
|
98
|
-
function applyVideoStyles() {
|
|
99
|
-
// Estilos del elemento video HTML
|
|
100
|
-
if (videoEl) {
|
|
101
|
-
videoEl.style.width = "100%";
|
|
102
|
-
videoEl.style.height = "100%";
|
|
103
|
-
videoEl.style.objectFit = "contain";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Aplicar estilos al inicio y cuando cambie la orientación
|
|
108
|
-
applyVideoStyles();
|
|
109
|
-
window.addEventListener("resize", applyVideoStyles);
|
|
110
|
-
|
|
111
|
-
// Manejar toques para dispositivos móviles
|
|
112
|
-
document.addEventListener("touchstart", () => {
|
|
113
|
-
if (videoEl.paused && mainVideo.getAttribute("opacity") === "1") {
|
|
114
|
-
videoEl.play().catch((e) => console.log("Error en touchstart:", e));
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
</script>
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
<style is:global>
|
|
2
|
-
/* Estilos globales básicos de A-Frame */
|
|
3
|
-
.a-enter-vr,
|
|
4
|
-
.a-enter-vr-button {
|
|
5
|
-
display: none !important;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
body,
|
|
9
|
-
a-scene {
|
|
10
|
-
background-color: transparent !important;
|
|
11
|
-
font-family: "Montserrat", sans-serif;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.a-canvas {
|
|
15
|
-
background-color: transparent !important;
|
|
16
|
-
width: 100% !important;
|
|
17
|
-
height: 100% !important;
|
|
18
|
-
position: absolute !important;
|
|
19
|
-
top: 0 !important;
|
|
20
|
-
left: 0 !important;
|
|
21
|
-
right: 0 !important;
|
|
22
|
-
bottom: 0 !important;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/* Clase base compartida para overlays */
|
|
26
|
-
.custom-overlay {
|
|
27
|
-
position: fixed;
|
|
28
|
-
top: 0;
|
|
29
|
-
left: 0;
|
|
30
|
-
width: 100%;
|
|
31
|
-
height: 100%;
|
|
32
|
-
display: flex;
|
|
33
|
-
flex-direction: column;
|
|
34
|
-
justify-content: center;
|
|
35
|
-
align-items: center;
|
|
36
|
-
z-index: 999;
|
|
37
|
-
color: white;
|
|
38
|
-
text-align: center;
|
|
39
|
-
transition: opacity 0.5s ease;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.custom-overlay.hidden {
|
|
43
|
-
display: none;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/* Texto compartido */
|
|
47
|
-
.status-text {
|
|
48
|
-
font-size: 18px;
|
|
49
|
-
font-weight: 600;
|
|
50
|
-
margin-top: 20px;
|
|
51
|
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.instruction-text {
|
|
55
|
-
font-size: 14px;
|
|
56
|
-
max-width: 80%;
|
|
57
|
-
margin-top: 10px;
|
|
58
|
-
opacity: 0.8;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/* Estilos para las pantallas de UI personalizadas */
|
|
62
|
-
.custom-overlay {
|
|
63
|
-
position: fixed;
|
|
64
|
-
top: 0;
|
|
65
|
-
left: 0;
|
|
66
|
-
width: 100%;
|
|
67
|
-
height: 100%;
|
|
68
|
-
display: flex;
|
|
69
|
-
flex-direction: column;
|
|
70
|
-
justify-content: center;
|
|
71
|
-
align-items: center;
|
|
72
|
-
z-index: 999;
|
|
73
|
-
background-color: rgba(0, 0, 0, 0);
|
|
74
|
-
color: white;
|
|
75
|
-
text-align: center;
|
|
76
|
-
transition: opacity 0.5s ease;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.custom-overlay.hidden {
|
|
80
|
-
display: none;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* Pantalla de carga */
|
|
84
|
-
#loading-overlay {
|
|
85
|
-
background: linear-gradient(135deg, #2c3e50, #1a1a2e);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.loader {
|
|
89
|
-
width: 60px;
|
|
90
|
-
height: 60px;
|
|
91
|
-
border: 5px solid rgba(255, 255, 255, 0.2);
|
|
92
|
-
border-top: 5px solid #3498db;
|
|
93
|
-
border-radius: 50%;
|
|
94
|
-
animation: spin 1s linear infinite;
|
|
95
|
-
margin-bottom: 20px;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@keyframes spin {
|
|
99
|
-
0% {
|
|
100
|
-
transform: rotate(0deg);
|
|
101
|
-
}
|
|
102
|
-
100% {
|
|
103
|
-
transform: rotate(360deg);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/* Pantalla de error */
|
|
108
|
-
#error-overlay {
|
|
109
|
-
background: linear-gradient(135deg, #c0392b, #8e44ad);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.error-icon {
|
|
113
|
-
font-size: 50px;
|
|
114
|
-
margin-bottom: 20px;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* Pantalla de escaneo */
|
|
118
|
-
#scanning-overlay {
|
|
119
|
-
background-color: rgba(0, 0, 0, 0);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/* Botón para reintentar */
|
|
123
|
-
.retry-button {
|
|
124
|
-
margin-top: 20px;
|
|
125
|
-
padding: 10px 20px;
|
|
126
|
-
background-color: #3498db;
|
|
127
|
-
color: white;
|
|
128
|
-
border: none;
|
|
129
|
-
border-radius: 5px;
|
|
130
|
-
font-weight: 600;
|
|
131
|
-
cursor: pointer;
|
|
132
|
-
transition: background-color 0.3s ease;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.retry-button:hover {
|
|
136
|
-
background-color: #2980b9;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
video {
|
|
140
|
-
width: 100% !important;
|
|
141
|
-
height: 100% !important;
|
|
142
|
-
object-fit: cover !important;
|
|
143
|
-
position: absolute !important;
|
|
144
|
-
top: 0 !important;
|
|
145
|
-
left: 0 !important;
|
|
146
|
-
}
|
|
147
|
-
</style>
|