@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 CHANGED
@@ -1,18 +1,18 @@
1
1
  # @srsergio/taptapp-ar
2
2
 
3
- 🚀 **TapTapp AR** is a high-performance Augmented Reality (AR) toolkit specifically designed for **Astro** and **Node.js** environments. It provides a seamless way to integrate image tracking, video overlays, and an offline compiler for image targets.
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 on top of **MindAR** and **A-Frame**, this package features a **pure JavaScript offline compiler** that requires **no TensorFlow** for backend compilation, while still supporting TensorFlow.js for real-time tracking in the browser.
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/compiler/offline-compiler.js';
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/compiler/offline-compiler.js';
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: import("./utils/worker-pool.js").WorkerPool | null;
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<Object[][]>;
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
- const [os, path, url, workerModule] = await Promise.all([
45
- import("os"),
46
- import("path"),
47
- import("url"),
48
- import("./utils/worker-pool.js")
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 workerModule.WorkerPool(workerPath, numWorkers);
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,7 +1,8 @@
1
1
  export class WorkerPool {
2
- constructor(workerPath: any, poolSize?: any);
2
+ constructor(workerPath: any, poolSize: any, WorkerClass: any);
3
3
  workerPath: any;
4
4
  poolSize: any;
5
+ WorkerClass: any;
5
6
  workers: any[];
6
7
  queue: any[];
7
8
  activeWorkers: number;
@@ -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 = os.cpus().length) {
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 Worker(this.workerPath);
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.7",
4
- "description": "AR Visualizer and Compiler for Astro and React",
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
- const [os, path, url, workerModule] = await Promise.all([
50
- import("os"),
51
- import("path"),
52
- import("url"),
53
- import("./utils/worker-pool.js")
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
- this.workerPool = new workerModule.WorkerPool(workerPath, numWorkers);
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
- const results = [];
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
- // Procesar secuencialmente para evitar overhead de workers
148
- // (los workers son útiles para muchas imágenes, pero añaden latencia)
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
- const results = [];
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 = os.cpus().length) {
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 Worker(this.workerPath);
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) {
@@ -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>