@srsergio/taptapp-ar 1.0.53 → 1.0.55
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 +58 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/TaptappAR.d.ts +9 -0
- package/dist/react/TaptappAR.js +97 -0
- package/dist/react/use-ar.d.ts +10 -0
- package/dist/react/use-ar.js +94 -0
- package/package.json +3 -1
- package/src/index.ts +3 -0
- package/src/react/TaptappAR.tsx +176 -0
- package/src/react/use-ar.ts +109 -0
package/README.md
CHANGED
|
@@ -131,12 +131,57 @@ ar.stop();
|
|
|
131
131
|
| `overlay` | ✅ | DOM element to position on the target |
|
|
132
132
|
| `onFound` | ❌ | Callback when target is detected |
|
|
133
133
|
| `onLost` | ❌ | Callback when target is lost |
|
|
134
|
-
| `onUpdate` | ❌ | Called
|
|
135
|
-
| `cameraConfig` | ❌ |
|
|
134
|
+
| `onUpdate` | ❌ | Called cada frame con `{ targetIndex, worldMatrix }` |
|
|
135
|
+
| `cameraConfig` | ❌ | Config de cámara (por defecto: `{ facingMode: 'environment', width: 1280, height: 720 }`) |
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
-
### 2.
|
|
139
|
+
### 2. React Integration (Vite & SSR Safe) ⚛️
|
|
140
|
+
|
|
141
|
+
The fastest and most modern way to build AR apps with React. It supports **Code Splitting** and is **100% SSR-Safe** (Next.js, Astro, Remix).
|
|
142
|
+
|
|
143
|
+
#### 🚀 Quick Start: `<TaptappAR />`
|
|
144
|
+
Drop the component into your app. It handles camera permissions, scanning animations, and video/image overlays automatically.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { TaptappAR, mapDataToPropsConfig } from '@srsergio/taptapp-ar';
|
|
148
|
+
|
|
149
|
+
const MyARComponent = ({ data }) => {
|
|
150
|
+
// mapDataToPropsConfig helps you format your data for the component
|
|
151
|
+
const config = mapDataToPropsConfig(data);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
155
|
+
<TaptappAR config={config} />
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### 🛠️ Custom UI: `useAR()` Hook
|
|
162
|
+
If you want to build your own UI while keeping the powerful tracking logic:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { useAR } from '@srsergio/taptapp-ar';
|
|
166
|
+
|
|
167
|
+
const CustomAR = ({ config }) => {
|
|
168
|
+
const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div ref={containerRef} style={{ position: 'relative' }} onClick={toggleVideo}>
|
|
172
|
+
{/* Custom Scanning UI */}
|
|
173
|
+
{status === 'scanning' && <div className="my-loader">Scanning...</div>}
|
|
174
|
+
|
|
175
|
+
{/* Video Overlay */}
|
|
176
|
+
<video ref={overlayRef} src={config.videoSrc} loop muted playsInline />
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 3. Raw Controller (Advanced & Custom Engines)
|
|
140
185
|
The `Controller` is the core engine of TapTapp AR. You can use it to build your own AR components or integrate tracking into custom 3D engines.
|
|
141
186
|
|
|
142
187
|
#### ⚙️ Controller Configuration
|
|
@@ -216,8 +261,15 @@ TapTapp AR uses a proprietary **Moonshot Vision Codec** that is significantly mo
|
|
|
216
261
|
|
|
217
262
|
---
|
|
218
263
|
|
|
219
|
-
## 📄 License &
|
|
264
|
+
## 📄 License & Recognition
|
|
265
|
+
|
|
266
|
+
**Taptapp AR** is created and maintained by **Sergio Lazaro**.
|
|
267
|
+
|
|
268
|
+
This project is licensed under the **GPL-3.0 License**.
|
|
269
|
+
This ensures that the project remains open and free, and that authorship is properly recognized. No "closed-source" usage is allowed without a commercial agreement.
|
|
270
|
+
|
|
271
|
+
Commercial licenses are available for proprietary applications. Please contact the author for details.
|
|
220
272
|
|
|
221
|
-
|
|
273
|
+
### Acknowledgements
|
|
274
|
+
This project evolved from the incredible work of [MindAR](https://github.com/hiukim/mind-ar-js). While the codebase has been extensively rewritten and optimized for performance, we gratefully acknowledge the foundation laid by the original authors.
|
|
222
275
|
|
|
223
|
-
Based on the core research of MindAR, but completely re-written for high-performance binary processing and JS-only execution.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./react/types.js";
|
|
2
|
+
export * from "./react/use-ar.js";
|
|
3
|
+
export * from "./react/TaptappAR.js";
|
|
2
4
|
export * from "./compiler/offline-compiler.js";
|
|
3
5
|
export { Controller } from "./compiler/controller.js";
|
|
4
6
|
export { SimpleAR } from "./compiler/simple-ar.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./react/types.js";
|
|
2
|
+
export * from "./react/use-ar.js";
|
|
3
|
+
export * from "./react/TaptappAR.js";
|
|
2
4
|
export * from "./compiler/offline-compiler.js";
|
|
3
5
|
export { Controller } from "./compiler/controller.js";
|
|
4
6
|
export { SimpleAR } from "./compiler/simple-ar.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { PropsConfig } from "./types.js";
|
|
3
|
+
export interface TaptappARProps {
|
|
4
|
+
config: PropsConfig;
|
|
5
|
+
className?: string;
|
|
6
|
+
showScanningOverlay?: boolean;
|
|
7
|
+
showErrorOverlay?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const TaptappAR: React.FC<TaptappARProps>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useAR } from "./use-ar.js";
|
|
4
|
+
export const TaptappAR = ({ config, className = "", showScanningOverlay = true, showErrorOverlay = true }) => {
|
|
5
|
+
const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
|
|
6
|
+
// Simple heuristic to determine if it's a video or image
|
|
7
|
+
// based on the presence of videoSrc and common extensions
|
|
8
|
+
const isVideo = useMemo(() => {
|
|
9
|
+
if (!config.videoSrc)
|
|
10
|
+
return false;
|
|
11
|
+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'];
|
|
12
|
+
const url = config.videoSrc.toLowerCase().split('?')[0];
|
|
13
|
+
return videoExtensions.some(ext => url.endsWith(ext)) || config.videoSrc.includes('video');
|
|
14
|
+
}, [config.videoSrc]);
|
|
15
|
+
return (_jsxs("div", { className: `taptapp-ar-wrapper ${className} ${status}`, style: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }, children: [showScanningOverlay && status === "scanning" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-scanning", children: _jsxs("div", { className: "scanning-content", children: [_jsxs("div", { className: "scanning-frame", children: [_jsx("img", { className: "target-preview", src: config.targetImageSrc, alt: "Target", crossOrigin: "anonymous" }), _jsx("div", { className: "scanning-line" })] }), _jsx("p", { className: "scanning-text", children: "Apunta a la imagen para comenzar" })] }) })), showErrorOverlay && status === "error" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-error", children: _jsxs("div", { className: "error-content", children: [_jsx("span", { className: "error-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "error-title", children: "No se pudo iniciar AR" }), _jsx("p", { className: "error-text", children: "Verifica los permisos de c\u00E1mara" }), _jsx("button", { className: "retry-btn", onClick: () => window.location.reload(), children: "Reintentar" })] }) })), _jsx("div", { ref: containerRef, className: "taptapp-ar-container", onClick: toggleVideo, style: { width: '100%', height: '100%' }, children: isVideo ? (_jsx("video", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc, preload: "auto", loop: true, playsInline: true, muted: true, crossOrigin: "anonymous" })) : (_jsx("img", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc || config.targetImageSrc, crossOrigin: "anonymous", alt: "AR Overlay" })) }), _jsx("style", { children: `
|
|
16
|
+
.taptapp-ar-wrapper {
|
|
17
|
+
background: #000;
|
|
18
|
+
color: white;
|
|
19
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
.taptapp-ar-overlay {
|
|
22
|
+
position: absolute;
|
|
23
|
+
top: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
right: 0;
|
|
26
|
+
bottom: 0;
|
|
27
|
+
z-index: 20;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
background: rgba(0,0,0,0.7);
|
|
32
|
+
backdrop-filter: blur(4px);
|
|
33
|
+
transition: opacity 0.3s ease;
|
|
34
|
+
}
|
|
35
|
+
.scanning-content, .error-content {
|
|
36
|
+
text-align: center;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
align-items: center;
|
|
40
|
+
padding: 20px;
|
|
41
|
+
}
|
|
42
|
+
.scanning-frame {
|
|
43
|
+
position: relative;
|
|
44
|
+
width: 200px;
|
|
45
|
+
height: 200px;
|
|
46
|
+
border: 2px solid rgba(255,255,255,0.3);
|
|
47
|
+
border-radius: 20px;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
margin-bottom: 20px;
|
|
50
|
+
}
|
|
51
|
+
.target-preview {
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
54
|
+
object-fit: cover;
|
|
55
|
+
opacity: 0.6;
|
|
56
|
+
}
|
|
57
|
+
.scanning-line {
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: 0;
|
|
60
|
+
left: 0;
|
|
61
|
+
width: 100%;
|
|
62
|
+
height: 2px;
|
|
63
|
+
background: #00e5ff;
|
|
64
|
+
box-shadow: 0 0 15px #00e5ff;
|
|
65
|
+
animation: scan 2s linear infinite;
|
|
66
|
+
}
|
|
67
|
+
@keyframes scan {
|
|
68
|
+
0% { top: 0; }
|
|
69
|
+
50% { top: 100%; }
|
|
70
|
+
100% { top: 0; }
|
|
71
|
+
}
|
|
72
|
+
.scanning-text {
|
|
73
|
+
font-size: 1.1rem;
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
letter-spacing: 0.5px;
|
|
76
|
+
}
|
|
77
|
+
.error-icon { font-size: 3rem; margin-bottom: 10px; }
|
|
78
|
+
.error-title { font-size: 1.2rem; font-weight: bold; margin: 0; }
|
|
79
|
+
.error-text { opacity: 0.8; margin: 5px 0 20px; }
|
|
80
|
+
.retry-btn {
|
|
81
|
+
padding: 10px 25px;
|
|
82
|
+
border-radius: 30px;
|
|
83
|
+
border: none;
|
|
84
|
+
background: #fff;
|
|
85
|
+
color: #000;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
transition: transform 0.2s;
|
|
89
|
+
}
|
|
90
|
+
.retry-btn:active { transform: scale(0.95); }
|
|
91
|
+
.taptapp-ar-overlay-element {
|
|
92
|
+
display: block;
|
|
93
|
+
width: 100%;
|
|
94
|
+
height: auto;
|
|
95
|
+
}
|
|
96
|
+
` })] }));
|
|
97
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PropsConfig } from "./types.js";
|
|
2
|
+
export type ARStatus = "scanning" | "tracking" | "error";
|
|
3
|
+
export interface UseARReturn {
|
|
4
|
+
containerRef: React.RefObject<HTMLDivElement>;
|
|
5
|
+
overlayRef: React.RefObject<HTMLVideoElement | HTMLImageElement>;
|
|
6
|
+
status: ARStatus;
|
|
7
|
+
isPlaying: boolean;
|
|
8
|
+
toggleVideo: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare const useAR: (config: PropsConfig) => UseARReturn;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
2
|
+
export const useAR = (config) => {
|
|
3
|
+
const containerRef = useRef(null);
|
|
4
|
+
const overlayRef = useRef(null);
|
|
5
|
+
const [status, setStatus] = useState("scanning");
|
|
6
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
7
|
+
const arInstanceRef = useRef(null);
|
|
8
|
+
const toggleVideo = useCallback(async () => {
|
|
9
|
+
const overlay = overlayRef.current;
|
|
10
|
+
if (!(overlay instanceof HTMLVideoElement))
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
if (overlay.paused) {
|
|
14
|
+
await overlay.play();
|
|
15
|
+
setIsPlaying(true);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
overlay.pause();
|
|
19
|
+
setIsPlaying(false);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error("Error toggling video:", err);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (typeof window === "undefined" || !containerRef.current || !overlayRef.current)
|
|
28
|
+
return;
|
|
29
|
+
let isMounted = true;
|
|
30
|
+
const initAR = async () => {
|
|
31
|
+
try {
|
|
32
|
+
// Safe hybrid import for SSR + Speed
|
|
33
|
+
const { SimpleAR } = await import("../compiler/simple-ar.js");
|
|
34
|
+
if (!isMounted)
|
|
35
|
+
return;
|
|
36
|
+
const instance = new SimpleAR({
|
|
37
|
+
container: containerRef.current,
|
|
38
|
+
targetSrc: config.targetTaarSrc,
|
|
39
|
+
overlay: overlayRef.current,
|
|
40
|
+
scale: config.scale,
|
|
41
|
+
onFound: async ({ targetIndex }) => {
|
|
42
|
+
console.log(`🎯 Target ${targetIndex} detected!`);
|
|
43
|
+
if (!isMounted)
|
|
44
|
+
return;
|
|
45
|
+
setStatus("tracking");
|
|
46
|
+
const overlay = overlayRef.current;
|
|
47
|
+
if (overlay instanceof HTMLVideoElement) {
|
|
48
|
+
try {
|
|
49
|
+
await overlay.play();
|
|
50
|
+
setIsPlaying(true);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.warn("Auto-play blocked:", err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
onLost: ({ targetIndex }) => {
|
|
58
|
+
console.log(`👋 Target ${targetIndex} lost`);
|
|
59
|
+
if (!isMounted)
|
|
60
|
+
return;
|
|
61
|
+
setStatus("scanning");
|
|
62
|
+
const overlay = overlayRef.current;
|
|
63
|
+
if (overlay instanceof HTMLVideoElement) {
|
|
64
|
+
overlay.pause();
|
|
65
|
+
setIsPlaying(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
arInstanceRef.current = instance;
|
|
70
|
+
await instance.start();
|
|
71
|
+
if (isMounted)
|
|
72
|
+
setStatus("scanning");
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error("Failed to initialize AR:", err);
|
|
76
|
+
if (isMounted)
|
|
77
|
+
setStatus("error");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
initAR();
|
|
81
|
+
return () => {
|
|
82
|
+
isMounted = false;
|
|
83
|
+
arInstanceRef.current?.stop();
|
|
84
|
+
arInstanceRef.current = null;
|
|
85
|
+
};
|
|
86
|
+
}, [config.targetTaarSrc, config.scale]);
|
|
87
|
+
return {
|
|
88
|
+
containerRef,
|
|
89
|
+
overlayRef,
|
|
90
|
+
status,
|
|
91
|
+
isPlaying,
|
|
92
|
+
toggleVideo
|
|
93
|
+
};
|
|
94
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.55",
|
|
4
|
+
"author": "Sergio Lazaro <srsergiolazaro@gmail.com>",
|
|
5
|
+
"license": "GPL-3.0",
|
|
4
6
|
"description": "Ultra-fast, lightweight Augmented Reality Image Tracking SDK for the web. Features an optimized offline compiler, React components, and compatibility with Three.js/A-Frame. No heavy ML frameworks required.",
|
|
5
7
|
"keywords": [
|
|
6
8
|
"web-ar",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from "./react/types.js";
|
|
2
|
+
export * from "./react/use-ar.js";
|
|
3
|
+
export * from "./react/TaptappAR.js";
|
|
2
4
|
export * from "./compiler/offline-compiler.js";
|
|
3
5
|
export { Controller } from "./compiler/controller.js";
|
|
4
6
|
export { SimpleAR } from "./compiler/simple-ar.js";
|
|
7
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { useAR } from "./use-ar.js";
|
|
3
|
+
import type { PropsConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export interface TaptappARProps {
|
|
6
|
+
config: PropsConfig;
|
|
7
|
+
className?: string;
|
|
8
|
+
showScanningOverlay?: boolean;
|
|
9
|
+
showErrorOverlay?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TaptappAR: React.FC<TaptappARProps> = ({
|
|
13
|
+
config,
|
|
14
|
+
className = "",
|
|
15
|
+
showScanningOverlay = true,
|
|
16
|
+
showErrorOverlay = true
|
|
17
|
+
}) => {
|
|
18
|
+
const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
|
|
19
|
+
|
|
20
|
+
// Simple heuristic to determine if it's a video or image
|
|
21
|
+
// based on the presence of videoSrc and common extensions
|
|
22
|
+
const isVideo = useMemo(() => {
|
|
23
|
+
if (!config.videoSrc) return false;
|
|
24
|
+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'];
|
|
25
|
+
const url = config.videoSrc.toLowerCase().split('?')[0];
|
|
26
|
+
return videoExtensions.some(ext => url.endsWith(ext)) || config.videoSrc.includes('video');
|
|
27
|
+
}, [config.videoSrc]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={`taptapp-ar-wrapper ${className} ${status}`} style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
|
|
31
|
+
{/* Scanning Overlay */}
|
|
32
|
+
{showScanningOverlay && status === "scanning" && (
|
|
33
|
+
<div className="taptapp-ar-overlay taptapp-ar-scanning">
|
|
34
|
+
<div className="scanning-content">
|
|
35
|
+
<div className="scanning-frame">
|
|
36
|
+
<img
|
|
37
|
+
className="target-preview"
|
|
38
|
+
src={config.targetImageSrc}
|
|
39
|
+
alt="Target"
|
|
40
|
+
crossOrigin="anonymous"
|
|
41
|
+
/>
|
|
42
|
+
<div className="scanning-line"></div>
|
|
43
|
+
</div>
|
|
44
|
+
<p className="scanning-text">Apunta a la imagen para comenzar</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{/* Error Overlay */}
|
|
50
|
+
{showErrorOverlay && status === "error" && (
|
|
51
|
+
<div className="taptapp-ar-overlay taptapp-ar-error">
|
|
52
|
+
<div className="error-content">
|
|
53
|
+
<span className="error-icon">⚠️</span>
|
|
54
|
+
<p className="error-title">No se pudo iniciar AR</p>
|
|
55
|
+
<p className="error-text">Verifica los permisos de cámara</p>
|
|
56
|
+
<button className="retry-btn" onClick={() => window.location.reload()}>
|
|
57
|
+
Reintentar
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* AR Container */}
|
|
64
|
+
<div
|
|
65
|
+
ref={containerRef}
|
|
66
|
+
className="taptapp-ar-container"
|
|
67
|
+
onClick={toggleVideo}
|
|
68
|
+
style={{ width: '100%', height: '100%' }}
|
|
69
|
+
>
|
|
70
|
+
{isVideo ? (
|
|
71
|
+
<video
|
|
72
|
+
ref={overlayRef as React.RefObject<HTMLVideoElement>}
|
|
73
|
+
className="taptapp-ar-overlay-element"
|
|
74
|
+
src={config.videoSrc}
|
|
75
|
+
preload="auto"
|
|
76
|
+
loop
|
|
77
|
+
playsInline
|
|
78
|
+
muted
|
|
79
|
+
crossOrigin="anonymous"
|
|
80
|
+
/>
|
|
81
|
+
) : (
|
|
82
|
+
<img
|
|
83
|
+
ref={overlayRef as React.RefObject<HTMLImageElement>}
|
|
84
|
+
className="taptapp-ar-overlay-element"
|
|
85
|
+
src={config.videoSrc || config.targetImageSrc}
|
|
86
|
+
crossOrigin="anonymous"
|
|
87
|
+
alt="AR Overlay"
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>{`
|
|
93
|
+
.taptapp-ar-wrapper {
|
|
94
|
+
background: #000;
|
|
95
|
+
color: white;
|
|
96
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
97
|
+
}
|
|
98
|
+
.taptapp-ar-overlay {
|
|
99
|
+
position: absolute;
|
|
100
|
+
top: 0;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
bottom: 0;
|
|
104
|
+
z-index: 20;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
background: rgba(0,0,0,0.7);
|
|
109
|
+
backdrop-filter: blur(4px);
|
|
110
|
+
transition: opacity 0.3s ease;
|
|
111
|
+
}
|
|
112
|
+
.scanning-content, .error-content {
|
|
113
|
+
text-align: center;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
align-items: center;
|
|
117
|
+
padding: 20px;
|
|
118
|
+
}
|
|
119
|
+
.scanning-frame {
|
|
120
|
+
position: relative;
|
|
121
|
+
width: 200px;
|
|
122
|
+
height: 200px;
|
|
123
|
+
border: 2px solid rgba(255,255,255,0.3);
|
|
124
|
+
border-radius: 20px;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
margin-bottom: 20px;
|
|
127
|
+
}
|
|
128
|
+
.target-preview {
|
|
129
|
+
width: 100%;
|
|
130
|
+
height: 100%;
|
|
131
|
+
object-fit: cover;
|
|
132
|
+
opacity: 0.6;
|
|
133
|
+
}
|
|
134
|
+
.scanning-line {
|
|
135
|
+
position: absolute;
|
|
136
|
+
top: 0;
|
|
137
|
+
left: 0;
|
|
138
|
+
width: 100%;
|
|
139
|
+
height: 2px;
|
|
140
|
+
background: #00e5ff;
|
|
141
|
+
box-shadow: 0 0 15px #00e5ff;
|
|
142
|
+
animation: scan 2s linear infinite;
|
|
143
|
+
}
|
|
144
|
+
@keyframes scan {
|
|
145
|
+
0% { top: 0; }
|
|
146
|
+
50% { top: 100%; }
|
|
147
|
+
100% { top: 0; }
|
|
148
|
+
}
|
|
149
|
+
.scanning-text {
|
|
150
|
+
font-size: 1.1rem;
|
|
151
|
+
font-weight: 500;
|
|
152
|
+
letter-spacing: 0.5px;
|
|
153
|
+
}
|
|
154
|
+
.error-icon { font-size: 3rem; margin-bottom: 10px; }
|
|
155
|
+
.error-title { font-size: 1.2rem; font-weight: bold; margin: 0; }
|
|
156
|
+
.error-text { opacity: 0.8; margin: 5px 0 20px; }
|
|
157
|
+
.retry-btn {
|
|
158
|
+
padding: 10px 25px;
|
|
159
|
+
border-radius: 30px;
|
|
160
|
+
border: none;
|
|
161
|
+
background: #fff;
|
|
162
|
+
color: #000;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
transition: transform 0.2s;
|
|
166
|
+
}
|
|
167
|
+
.retry-btn:active { transform: scale(0.95); }
|
|
168
|
+
.taptapp-ar-overlay-element {
|
|
169
|
+
display: block;
|
|
170
|
+
width: 100%;
|
|
171
|
+
height: auto;
|
|
172
|
+
}
|
|
173
|
+
`}</style>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
2
|
+
import type { PropsConfig } from "./types.js";
|
|
3
|
+
import type { SimpleAR as SimpleARType } from "../compiler/simple-ar.js";
|
|
4
|
+
|
|
5
|
+
export type ARStatus = "scanning" | "tracking" | "error";
|
|
6
|
+
|
|
7
|
+
export interface UseARReturn {
|
|
8
|
+
containerRef: React.RefObject<HTMLDivElement>;
|
|
9
|
+
overlayRef: React.RefObject<HTMLVideoElement | HTMLImageElement>;
|
|
10
|
+
status: ARStatus;
|
|
11
|
+
isPlaying: boolean;
|
|
12
|
+
toggleVideo: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useAR = (config: PropsConfig): UseARReturn => {
|
|
16
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
const overlayRef = useRef<HTMLVideoElement | HTMLImageElement>(null);
|
|
18
|
+
const [status, setStatus] = useState<ARStatus>("scanning");
|
|
19
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
20
|
+
const arInstanceRef = useRef<SimpleARType | null>(null);
|
|
21
|
+
|
|
22
|
+
const toggleVideo = useCallback(async () => {
|
|
23
|
+
const overlay = overlayRef.current;
|
|
24
|
+
if (!(overlay instanceof HTMLVideoElement)) return;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (overlay.paused) {
|
|
28
|
+
await overlay.play();
|
|
29
|
+
setIsPlaying(true);
|
|
30
|
+
} else {
|
|
31
|
+
overlay.pause();
|
|
32
|
+
setIsPlaying(false);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error("Error toggling video:", err);
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (typeof window === "undefined" || !containerRef.current || !overlayRef.current) return;
|
|
41
|
+
|
|
42
|
+
let isMounted = true;
|
|
43
|
+
|
|
44
|
+
const initAR = async () => {
|
|
45
|
+
try {
|
|
46
|
+
// Safe hybrid import for SSR + Speed
|
|
47
|
+
const { SimpleAR } = await import("../compiler/simple-ar.js");
|
|
48
|
+
if (!isMounted) return;
|
|
49
|
+
|
|
50
|
+
const instance = new SimpleAR({
|
|
51
|
+
container: containerRef.current!,
|
|
52
|
+
targetSrc: config.targetTaarSrc,
|
|
53
|
+
overlay: overlayRef.current!,
|
|
54
|
+
scale: config.scale,
|
|
55
|
+
onFound: async ({ targetIndex }) => {
|
|
56
|
+
console.log(`🎯 Target ${targetIndex} detected!`);
|
|
57
|
+
if (!isMounted) return;
|
|
58
|
+
setStatus("tracking");
|
|
59
|
+
|
|
60
|
+
const overlay = overlayRef.current;
|
|
61
|
+
if (overlay instanceof HTMLVideoElement) {
|
|
62
|
+
try {
|
|
63
|
+
await overlay.play();
|
|
64
|
+
setIsPlaying(true);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn("Auto-play blocked:", err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
onLost: ({ targetIndex }) => {
|
|
71
|
+
console.log(`👋 Target ${targetIndex} lost`);
|
|
72
|
+
if (!isMounted) return;
|
|
73
|
+
setStatus("scanning");
|
|
74
|
+
|
|
75
|
+
const overlay = overlayRef.current;
|
|
76
|
+
if (overlay instanceof HTMLVideoElement) {
|
|
77
|
+
overlay.pause();
|
|
78
|
+
setIsPlaying(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
arInstanceRef.current = instance;
|
|
84
|
+
await instance.start();
|
|
85
|
+
|
|
86
|
+
if (isMounted) setStatus("scanning");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("Failed to initialize AR:", err);
|
|
89
|
+
if (isMounted) setStatus("error");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
initAR();
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
isMounted = false;
|
|
97
|
+
arInstanceRef.current?.stop();
|
|
98
|
+
arInstanceRef.current = null;
|
|
99
|
+
};
|
|
100
|
+
}, [config.targetTaarSrc, config.scale]);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
containerRef,
|
|
104
|
+
overlayRef,
|
|
105
|
+
status,
|
|
106
|
+
isPlaying,
|
|
107
|
+
toggleVideo
|
|
108
|
+
};
|
|
109
|
+
};
|