@aivue/360-spin 1.0.3 → 2.0.0
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/CHANGELOG.md +29 -0
- package/README.md +125 -3
- package/dist/360-spin.css +1 -1
- package/dist/components/Ai360Generator.vue.d.ts +26 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +674 -168
- package/dist/index.mjs.map +1 -1
- package/dist/types/index.d.ts +109 -0
- package/dist/utils/ai-generator.d.ts +45 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.0] - 2025-12-12
|
|
9
|
+
|
|
10
|
+
### Added - 🤖 AI 360° Generation
|
|
11
|
+
- **NEW: `Ai360Generator` Component** - Upload a single product image and generate 360° views using AI
|
|
12
|
+
- **OpenAI DALL-E 3 Integration** - High-quality AI-generated frames at different angles
|
|
13
|
+
- **Stability AI Support** - Alternative AI provider for 360° generation
|
|
14
|
+
- **GPT-4 Vision Analysis** - Automatic product analysis for better generation results
|
|
15
|
+
- **AI360Generator Utility Class** - Programmatic API for AI generation
|
|
16
|
+
- **Customizable Generation Options**:
|
|
17
|
+
- Frame count: 12, 24, 36, or 72 frames
|
|
18
|
+
- Background color: white, transparent, black, or custom
|
|
19
|
+
- Quality settings: standard, high, ultra
|
|
20
|
+
- Image size options
|
|
21
|
+
- **Real-time Progress Tracking** - Live updates during generation
|
|
22
|
+
- **Frame Download** - Export all generated frames
|
|
23
|
+
- **Interactive Preview** - View generated 360° immediately with Ai360Spin
|
|
24
|
+
- **API Key Management** - Secure storage in localStorage
|
|
25
|
+
|
|
26
|
+
### Enhanced
|
|
27
|
+
- Updated TypeScript types for AI generation features
|
|
28
|
+
- Extended documentation with AI generation examples
|
|
29
|
+
- Added comprehensive README section for AI features
|
|
30
|
+
- Improved package description to highlight AI capabilities
|
|
31
|
+
|
|
32
|
+
### Breaking Changes
|
|
33
|
+
- Major version bump to 2.0.0 due to new AI features
|
|
34
|
+
- New exports: `Ai360Generator` component and `AI360Generator` class
|
|
35
|
+
- New types: `AIProvider`, `BackgroundColor`, `AI360GeneratorConfig`, etc.
|
|
36
|
+
|
|
8
37
|
## [1.0.2] - 2025-12-09
|
|
9
38
|
|
|
10
39
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @aivue/360-spin
|
|
2
2
|
|
|
3
|
-
> Interactive 360-degree product image spin component for Vue.js
|
|
3
|
+
> Interactive 360-degree product image spin component for Vue.js with AI-powered generation
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@aivue/360-spin)
|
|
6
6
|
[](https://www.npmjs.com/package/@aivue/360-spin)
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
+
### 360° Viewer
|
|
11
12
|
- 🖼️ **Static to Animated**: Show static product image by default, animate on hover/tap
|
|
12
13
|
- 🎬 **Multiple Modes**: Support for GIF animations or frame sequences
|
|
13
14
|
- 📱 **Mobile Optimized**: Touch and drag to spin on mobile devices
|
|
@@ -17,6 +18,15 @@
|
|
|
17
18
|
- 🎨 **Customizable**: Full control over styling and behavior
|
|
18
19
|
- ♿ **Accessible**: Keyboard navigation and screen reader support
|
|
19
20
|
|
|
21
|
+
### 🤖 AI 360° Generator (NEW!)
|
|
22
|
+
- 📤 **Upload & Generate**: Upload a single product image and AI generates 360° views
|
|
23
|
+
- 🎨 **OpenAI DALL-E 3**: High-quality AI-generated frames at different angles
|
|
24
|
+
- 🔄 **Stability AI Support**: Alternative AI provider for generation
|
|
25
|
+
- 🎯 **Customizable**: Choose frame count (12/24/36/72), background color, quality
|
|
26
|
+
- 📊 **Real-time Progress**: Track generation progress with live updates
|
|
27
|
+
- 💾 **Download Frames**: Export all generated frames for use elsewhere
|
|
28
|
+
- 🔍 **Vision Analysis**: GPT-4 Vision analyzes your product for better results
|
|
29
|
+
|
|
20
30
|
## 📦 Installation
|
|
21
31
|
|
|
22
32
|
```bash
|
|
@@ -89,9 +99,76 @@ const frameUrls = [
|
|
|
89
99
|
</template>
|
|
90
100
|
```
|
|
91
101
|
|
|
102
|
+
### 🤖 AI 360° Generator
|
|
103
|
+
|
|
104
|
+
Generate 360-degree product views from a single image using AI:
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<template>
|
|
108
|
+
<Ai360Generator
|
|
109
|
+
provider="openai"
|
|
110
|
+
api-key="your-openai-api-key"
|
|
111
|
+
:auto-save-api-key="true"
|
|
112
|
+
:show-frame-preview="true"
|
|
113
|
+
@frames-generated="handleFramesGenerated"
|
|
114
|
+
@generation-complete="handleComplete"
|
|
115
|
+
/>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script setup>
|
|
119
|
+
import { Ai360Generator } from '@aivue/360-spin';
|
|
120
|
+
import '@aivue/360-spin/360-spin.css';
|
|
121
|
+
|
|
122
|
+
function handleFramesGenerated(frames) {
|
|
123
|
+
console.log('Generated frames:', frames);
|
|
124
|
+
// Use frames with Ai360Spin component
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleComplete(frames) {
|
|
128
|
+
console.log('Generation complete!', frames.length, 'frames');
|
|
129
|
+
}
|
|
130
|
+
</script>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Programmatic AI Generation
|
|
134
|
+
|
|
135
|
+
Use the AI generator utility directly:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { AI360Generator } from '@aivue/360-spin';
|
|
139
|
+
|
|
140
|
+
const generator = new AI360Generator(
|
|
141
|
+
{
|
|
142
|
+
provider: 'openai',
|
|
143
|
+
apiKey: 'your-api-key',
|
|
144
|
+
frameCount: 36,
|
|
145
|
+
backgroundColor: 'white',
|
|
146
|
+
quality: 80,
|
|
147
|
+
useVisionAnalysis: true
|
|
148
|
+
},
|
|
149
|
+
(progress) => {
|
|
150
|
+
console.log(`Progress: ${progress.percentage}%`);
|
|
151
|
+
console.log(`Status: ${progress.status}`);
|
|
152
|
+
console.log(`Frame ${progress.currentFrame}/${progress.totalFrames}`);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Generate from File
|
|
157
|
+
const file = document.querySelector('input[type="file"]').files[0];
|
|
158
|
+
const result = await generator.generate(file);
|
|
159
|
+
console.log('Generated frames:', result.frames);
|
|
160
|
+
console.log('Product description:', result.productDescription);
|
|
161
|
+
|
|
162
|
+
// Or generate from base64 image
|
|
163
|
+
const base64Image = 'data:image/jpeg;base64,...';
|
|
164
|
+
const result = await generator.generate(base64Image);
|
|
165
|
+
```
|
|
166
|
+
|
|
92
167
|
## 📖 API Reference
|
|
93
168
|
|
|
94
|
-
###
|
|
169
|
+
### Ai360Spin Component
|
|
170
|
+
|
|
171
|
+
#### Props
|
|
95
172
|
|
|
96
173
|
| Prop | Type | Default | Description |
|
|
97
174
|
|------|------|---------|-------------|
|
|
@@ -113,7 +190,7 @@ const frameUrls = [
|
|
|
113
190
|
| `containerClass` | `string` | `''` | CSS class for container |
|
|
114
191
|
| `imageClass` | `string` | `''` | CSS class for images |
|
|
115
192
|
|
|
116
|
-
|
|
193
|
+
#### Events
|
|
117
194
|
|
|
118
195
|
| Event | Payload | Description |
|
|
119
196
|
|-------|---------|-------------|
|
|
@@ -123,6 +200,51 @@ const frameUrls = [
|
|
|
123
200
|
| `error` | `Error` | Fired on loading error |
|
|
124
201
|
| `frame-change` | `number` | Fired when frame changes (frames mode) |
|
|
125
202
|
|
|
203
|
+
### Ai360Generator Component
|
|
204
|
+
|
|
205
|
+
#### Props
|
|
206
|
+
|
|
207
|
+
| Prop | Type | Default | Description |
|
|
208
|
+
|------|------|---------|-------------|
|
|
209
|
+
| `provider` | `'openai' \| 'stability'` | `'openai'` | AI provider for generation |
|
|
210
|
+
| `apiKey` | `string` | `''` | API key (can be saved in localStorage) |
|
|
211
|
+
| `autoSaveApiKey` | `boolean` | `true` | Auto-save API key to localStorage |
|
|
212
|
+
| `showFramePreview` | `boolean` | `true` | Show preview grid of generated frames |
|
|
213
|
+
|
|
214
|
+
#### Events
|
|
215
|
+
|
|
216
|
+
| Event | Payload | Description |
|
|
217
|
+
|-------|---------|-------------|
|
|
218
|
+
| `frames-generated` | `string[]` | Emitted when user clicks "Use These Frames" |
|
|
219
|
+
| `generation-start` | `void` | Emitted when generation starts |
|
|
220
|
+
| `generation-complete` | `string[]` | Emitted when generation completes |
|
|
221
|
+
| `generation-error` | `Error` | Emitted on generation error |
|
|
222
|
+
|
|
223
|
+
### AI360Generator Class
|
|
224
|
+
|
|
225
|
+
#### Constructor Options
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
interface AI360GeneratorConfig {
|
|
229
|
+
provider?: 'openai' | 'stability';
|
|
230
|
+
apiKey: string;
|
|
231
|
+
frameCount?: number; // Default: 36
|
|
232
|
+
backgroundColor?: 'white' | 'transparent' | 'black' | 'custom';
|
|
233
|
+
customBackgroundColor?: string;
|
|
234
|
+
quality?: number; // 0-100, Default: 80
|
|
235
|
+
imageSize?: '1024x1024' | '1024x1792' | '1792x1024';
|
|
236
|
+
model?: string; // Default: 'dall-e-3' for OpenAI
|
|
237
|
+
useVisionAnalysis?: boolean; // Default: true
|
|
238
|
+
promptTemplate?: string; // Custom prompt template
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Methods
|
|
243
|
+
|
|
244
|
+
- `generate(imageFile: File | string): Promise<AI360GenerationResult>`
|
|
245
|
+
- Generates 360° frames from a single image
|
|
246
|
+
- Returns frames, product description, and metadata
|
|
247
|
+
|
|
126
248
|
## 🎨 Styling
|
|
127
249
|
|
|
128
250
|
The component comes with default styles, but you can customize them:
|
package/dist/360-spin.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.ai-360-spin[data-v-915b2a52]{position:relative;display:inline-block;overflow:hidden;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.ai-360-spin__image[data-v-915b2a52]{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;display:block;transition:opacity .3s ease}.ai-360-spin__image--static[data-v-915b2a52]{opacity:1}.ai-360-spin__image--animated[data-v-915b2a52],.ai-360-spin__image--frame[data-v-915b2a52]{position:absolute;top:0;left:0;opacity:1}.ai-360-spin--animating .ai-360-spin__image--static[data-v-915b2a52]{opacity:0}.ai-360-spin__loading[data-v-915b2a52]{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#ffffffe6;z-index:10}.ai-360-spin__spinner[data-v-915b2a52]{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:ai-360-spin-rotate-915b2a52 1s linear infinite}@keyframes ai-360-spin-rotate-915b2a52{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ai-360-spin__loading-text[data-v-915b2a52]{margin-top:12px;font-size:14px;color:#666}.ai-360-spin__hint[data-v-915b2a52]{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;padding:8px 16px;background:#000000b3;color:#fff;border-radius:20px;font-size:12px;pointer-events:none;opacity:0;transition:opacity .3s ease}.ai-360-spin:hover .ai-360-spin__hint[data-v-915b2a52]{opacity:1}.ai-360-spin__hint-icon[data-v-915b2a52]{width:16px;height:16px;stroke-width:2}@media(max-width:768px){.ai-360-spin[data-v-915b2a52]{touch-action:none}.ai-360-spin__hint[data-v-915b2a52]{font-size:11px;padding:6px 12px}}.ai-360-spin{position:relative;display:inline-block;overflow:hidden;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;background:#f5f5f5}.ai-360-spin__image{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;display:block;transition:opacity .3s ease}.ai-360-spin__image--static{opacity:1}.ai-360-spin__image--animated,.ai-360-spin__image--frame{position:absolute;top:0;left:0;opacity:1}.ai-360-spin--animating .ai-360-spin__image--static{opacity:0}.ai-360-spin__loading{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#fffffff2;z-index:10}.ai-360-spin__spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:ai-360-spin-rotate 1s linear infinite}@keyframes ai-360-spin-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ai-360-spin__loading-text{margin-top:12px;font-size:14px;color:#666;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif}.ai-360-spin__hint{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;padding:8px 16px;background:#000000b3;color:#fff;border-radius:20px;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;pointer-events:none;opacity:0;transition:opacity .3s ease;z-index:5}.ai-360-spin:hover .ai-360-spin__hint{opacity:1}.ai-360-spin__hint-icon{width:16px;height:16px;stroke-width:2}.ai-360-spin--card{width:100%;aspect-ratio:1 / 1}.ai-360-spin--card .ai-360-spin__image{-o-object-fit:cover;object-fit:cover}.ai-360-spin--carousel{width:100%;height:100%}.ai-360-spin--grid{width:100%;aspect-ratio:4 / 3}@media(max-width:768px){.ai-360-spin{touch-action:none}.ai-360-spin__hint{font-size:11px;padding:6px 12px;bottom:8px}.ai-360-spin__hint-icon{width:14px;height:14px}.ai-360-spin__loading-text{font-size:12px}.ai-360-spin__spinner{width:32px;height:32px;border-width:3px}}.ai-360-spin:focus{outline:2px solid #3498db;outline-offset:2px}.ai-360-spin:focus:not(:focus-visible){outline:none}
|
|
1
|
+
.ai-360-spin[data-v-915b2a52]{position:relative;display:inline-block;overflow:hidden;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.ai-360-spin__image[data-v-915b2a52]{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;display:block;transition:opacity .3s ease}.ai-360-spin__image--static[data-v-915b2a52]{opacity:1}.ai-360-spin__image--animated[data-v-915b2a52],.ai-360-spin__image--frame[data-v-915b2a52]{position:absolute;top:0;left:0;opacity:1}.ai-360-spin--animating .ai-360-spin__image--static[data-v-915b2a52]{opacity:0}.ai-360-spin__loading[data-v-915b2a52]{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#ffffffe6;z-index:10}.ai-360-spin__spinner[data-v-915b2a52]{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:ai-360-spin-rotate-915b2a52 1s linear infinite}@keyframes ai-360-spin-rotate-915b2a52{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ai-360-spin__loading-text[data-v-915b2a52]{margin-top:12px;font-size:14px;color:#666}.ai-360-spin__hint[data-v-915b2a52]{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;padding:8px 16px;background:#000000b3;color:#fff;border-radius:20px;font-size:12px;pointer-events:none;opacity:0;transition:opacity .3s ease}.ai-360-spin:hover .ai-360-spin__hint[data-v-915b2a52]{opacity:1}.ai-360-spin__hint-icon[data-v-915b2a52]{width:16px;height:16px;stroke-width:2}@media(max-width:768px){.ai-360-spin[data-v-915b2a52]{touch-action:none}.ai-360-spin__hint[data-v-915b2a52]{font-size:11px;padding:6px 12px}}.ai-360-generator[data-v-fc0644b2]{width:100%;max-width:800px;margin:0 auto;padding:20px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif}.ai-360-generator__api-key[data-v-fc0644b2]{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.ai-360-generator__api-key label[data-v-fc0644b2]{display:block;margin-bottom:8px;font-weight:600;color:#333}.ai-360-generator__input[data-v-fc0644b2]{width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:14px;margin-bottom:10px}.ai-360-generator__dropzone[data-v-fc0644b2]{border:2px dashed #ddd;border-radius:8px;padding:40px;text-align:center;cursor:pointer;transition:all .3s ease;background:#fafafa}.ai-360-generator__dropzone[data-v-fc0644b2]:hover{border-color:#4caf50;background:#f0f8f0}.ai-360-generator__dropzone--dragging[data-v-fc0644b2]{border-color:#4caf50;background:#e8f5e9}.ai-360-generator__upload-icon[data-v-fc0644b2]{width:48px;height:48px;margin:0 auto 16px;color:#666}.ai-360-generator__dropzone-text[data-v-fc0644b2]{font-size:16px;font-weight:600;color:#333;margin:0 0 8px}.ai-360-generator__dropzone-hint[data-v-fc0644b2]{font-size:14px;color:#666;margin:0}.ai-360-generator__preview[data-v-fc0644b2]{position:relative;max-width:400px;margin:0 auto}.ai-360-generator__preview-image[data-v-fc0644b2]{width:100%;border-radius:8px}.ai-360-generator__clear-button[data-v-fc0644b2]{position:absolute;top:10px;right:10px;background:#000000b3;color:#fff;border:none;border-radius:50%;width:32px;height:32px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.ai-360-generator__options[data-v-fc0644b2]{margin-top:20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:15px}.ai-360-generator__option[data-v-fc0644b2]{display:flex;flex-direction:column}.ai-360-generator__option label[data-v-fc0644b2]{font-weight:600;margin-bottom:5px;color:#333;font-size:14px}.ai-360-generator__select[data-v-fc0644b2]{padding:8px;border:1px solid #ddd;border-radius:4px;font-size:14px}.ai-360-generator__generate-button[data-v-fc0644b2]{grid-column:1 / -1;padding:12px 24px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:6px;font-size:16px;font-weight:600;cursor:pointer;transition:transform .2s}.ai-360-generator__generate-button[data-v-fc0644b2]:hover{transform:translateY(-2px)}.ai-360-generator__progress[data-v-fc0644b2]{padding:30px;text-align:center}.ai-360-generator__progress-bar[data-v-fc0644b2]{width:100%;height:8px;background:#e0e0e0;border-radius:4px;overflow:hidden;margin-bottom:15px}.ai-360-generator__progress-fill[data-v-fc0644b2]{height:100%;background:linear-gradient(90deg,#667eea,#764ba2);transition:width .3s ease}.ai-360-generator__progress-text[data-v-fc0644b2]{font-size:16px;font-weight:600;color:#333;margin:0 0 8px}.ai-360-generator__progress-detail[data-v-fc0644b2]{font-size:14px;color:#666;margin:0}.ai-360-generator__result[data-v-fc0644b2]{text-align:center}.ai-360-generator__result-title[data-v-fc0644b2]{font-size:24px;margin-bottom:20px;color:#4caf50}.ai-360-generator__viewer[data-v-fc0644b2]{margin:20px 0;border-radius:8px;overflow:hidden;box-shadow:0 4px 12px #0000001a}.ai-360-generator__actions[data-v-fc0644b2]{display:flex;gap:10px;justify-content:center;margin:20px 0;flex-wrap:wrap}.ai-360-generator__button[data-v-fc0644b2]{padding:10px 20px;background:#4caf50;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}.ai-360-generator__button[data-v-fc0644b2]:hover{background:#45a049;transform:translateY(-1px)}.ai-360-generator__button--secondary[data-v-fc0644b2]{background:#757575}.ai-360-generator__button--secondary[data-v-fc0644b2]:hover{background:#616161}.ai-360-generator__button--primary[data-v-fc0644b2]{background:linear-gradient(135deg,#667eea,#764ba2)}.ai-360-generator__frame-grid[data-v-fc0644b2]{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:10px;margin-top:20px}.ai-360-generator__frame-thumbnail[data-v-fc0644b2]{width:100%;aspect-ratio:1;-o-object-fit:cover;object-fit:cover;border-radius:4px;border:2px solid #e0e0e0;transition:transform .2s}.ai-360-generator__frame-thumbnail[data-v-fc0644b2]:hover{transform:scale(1.05);border-color:#4caf50}.ai-360-generator__error[data-v-fc0644b2]{background:#ffebee;color:#c62828;padding:15px;border-radius:6px;margin-top:20px;text-align:center}.ai-360-generator__error p[data-v-fc0644b2]{margin:0 0 10px;font-weight:600}.ai-360-spin{position:relative;display:inline-block;overflow:hidden;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;background:#f5f5f5}.ai-360-spin__image{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;display:block;transition:opacity .3s ease}.ai-360-spin__image--static{opacity:1}.ai-360-spin__image--animated,.ai-360-spin__image--frame{position:absolute;top:0;left:0;opacity:1}.ai-360-spin--animating .ai-360-spin__image--static{opacity:0}.ai-360-spin__loading{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#fffffff2;z-index:10}.ai-360-spin__spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:ai-360-spin-rotate 1s linear infinite}@keyframes ai-360-spin-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ai-360-spin__loading-text{margin-top:12px;font-size:14px;color:#666;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif}.ai-360-spin__hint{position:absolute;bottom:12px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;padding:8px 16px;background:#000000b3;color:#fff;border-radius:20px;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;pointer-events:none;opacity:0;transition:opacity .3s ease;z-index:5}.ai-360-spin:hover .ai-360-spin__hint{opacity:1}.ai-360-spin__hint-icon{width:16px;height:16px;stroke-width:2}.ai-360-spin--card{width:100%;aspect-ratio:1 / 1}.ai-360-spin--card .ai-360-spin__image{-o-object-fit:cover;object-fit:cover}.ai-360-spin--carousel{width:100%;height:100%}.ai-360-spin--grid{width:100%;aspect-ratio:4 / 3}@media(max-width:768px){.ai-360-spin{touch-action:none}.ai-360-spin__hint{font-size:11px;padding:6px 12px;bottom:8px}.ai-360-spin__hint-icon{width:14px;height:14px}.ai-360-spin__loading-text{font-size:12px}.ai-360-spin__spinner{width:32px;height:32px;border-width:3px}}.ai-360-spin:focus{outline:2px solid #3498db;outline-offset:2px}.ai-360-spin:focus:not(:focus-visible){outline:none}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AIProvider } from '../types';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
provider?: AIProvider;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
autoSaveApiKey?: boolean;
|
|
6
|
+
showFramePreview?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
9
|
+
"frames-generated": (frames: string[]) => any;
|
|
10
|
+
"generation-start": () => any;
|
|
11
|
+
"generation-complete": (frames: string[]) => any;
|
|
12
|
+
"generation-error": (error: Error) => any;
|
|
13
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
|
+
"onFrames-generated"?: ((frames: string[]) => any) | undefined;
|
|
15
|
+
"onGeneration-start"?: (() => any) | undefined;
|
|
16
|
+
"onGeneration-complete"?: ((frames: string[]) => any) | undefined;
|
|
17
|
+
"onGeneration-error"?: ((error: Error) => any) | undefined;
|
|
18
|
+
}>, {
|
|
19
|
+
provider: AIProvider;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
autoSaveApiKey: boolean;
|
|
22
|
+
showFramePreview: boolean;
|
|
23
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
24
|
+
fileInputRef: HTMLInputElement;
|
|
25
|
+
}, HTMLDivElement>;
|
|
26
|
+
export default _default;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { App } from 'vue';
|
|
2
2
|
import { default as Ai360Spin } from './components/Ai360Spin.vue';
|
|
3
|
+
import { default as Ai360Generator } from './components/Ai360Generator.vue';
|
|
3
4
|
import { use360Spin } from './composables/use360Spin';
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { AI360Generator } from './utils/ai-generator';
|
|
6
|
+
import { Spin360Config, SpinMode, SpinTrigger, SpinDirection, Spin360Events, AIProvider, BackgroundColor, AI360GeneratorConfig, AI360GenerationProgress, AI360GenerationResult } from './types';
|
|
7
|
+
export { Ai360Spin, Ai360Generator };
|
|
6
8
|
export { use360Spin };
|
|
7
|
-
export
|
|
9
|
+
export { AI360Generator };
|
|
10
|
+
export type { Spin360Config, SpinMode, SpinTrigger, SpinDirection, Spin360Events, AIProvider, BackgroundColor, AI360GeneratorConfig, AI360GenerationProgress, AI360GenerationResult };
|
|
8
11
|
declare const _default: {
|
|
9
12
|
install(app: App): void;
|
|
10
13
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");function
|
|
1
|
+
"use strict";var L=Object.defineProperty;var x=(c,a,t)=>a in c?L(c,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[a]=t;var T=(c,a,t)=>x(c,typeof a!="symbol"?a+"":a,t);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");function $(c,a){const t=e.ref(!1),s=e.ref(!0),n=e.ref(0),i=e.ref(null),l=e.ref([]),f=e.ref(!1),u=e.ref(0),g=e.ref(0),p=e.computed(()=>c.mode&&c.mode!=="auto"?c.mode:Array.isArray(c.animatedImage)?"frames":"gif"),k=e.computed(()=>p.value==="gif"&&typeof c.animatedImage=="string"?c.animatedImage:""),S=e.computed(()=>{if(p.value==="frames"&&Array.isArray(c.animatedImage)){const d=c.animatedImage,h=n.value%d.length;return d[h]}return""}),E=e.computed(()=>Array.isArray(c.animatedImage)?c.animatedImage:[]),v=e.computed(()=>E.value.length);async function C(){s.value=!0;try{if(p.value==="gif")await V(k.value);else if(p.value==="frames"){const d=E.value.map(_=>V(_)),h=await Promise.all(d);l.value=h}await V(c.staticImage),s.value=!1,a("loaded")}catch(d){s.value=!1,a("error",d)}}function V(d,h=5e3){return new Promise((_,w)=>{const y=new Image,r=setTimeout(()=>{w(new Error(`Image load timeout: ${d}`))},h);y.onload=()=>{clearTimeout(r),_(y)},y.onerror=()=>{clearTimeout(r),w(new Error(`Failed to load image: ${d}`))},y.src=d})}function B(){t.value||s.value||(t.value=!0,a("animation-start"),p.value==="frames"&&I())}function N(){t.value&&(t.value=!1,a("animation-end"),i.value!==null&&(cancelAnimationFrame(i.value),i.value=null),p.value==="frames"&&(n.value=0))}function I(){if(v.value===0)return;const d=1e3/(c.frameRate||30);let h=Date.now();function _(){const w=Date.now();if(w-h>=d&&(c.direction==="clockwise"?n.value=(n.value+1)%v.value:n.value=n.value===0?v.value-1:n.value-1,a("frame-change",n.value),h=w,!c.loop&&n.value===0)){N();return}t.value&&(i.value=requestAnimationFrame(_))}i.value=requestAnimationFrame(_)}function F(d){p.value!=="frames"||v.value===0||(f.value=!0,g.value=n.value,d instanceof TouchEvent?u.value=d.touches[0].clientX:u.value=d.clientX,t.value&&N())}function A(d){if(!f.value||p.value!=="frames")return;d.preventDefault();let h;d instanceof TouchEvent?h=d.touches[0].clientX:h=d.clientX;const _=h-u.value,w=c.dragSensitivity||10,y=Math.floor(_/w);let r=g.value+y;for(;r<0;)r+=v.value;for(;r>=v.value;)r-=v.value;r!==n.value&&(n.value=r,a("frame-change",n.value))}function D(d){f.value=!1}return e.onUnmounted(()=>{i.value!==null&&cancelAnimationFrame(i.value)}),{isAnimating:t,isLoading:s,currentFrameIndex:n,currentMode:p,animatedImageUrl:k,currentFrameUrl:S,totalFrames:v,startAnimation:B,stopAnimation:N,preloadImages:C,handleDragStart:F,handleDragMove:A,handleDragEnd:D}}const G={key:0,class:"ai-360-spin__loading"},K={class:"ai-360-spin__loading-text"},O=["src","alt"],R=["src","alt"],q=["src","alt"],j={key:3,class:"ai-360-spin__hint"},X=e.defineComponent({__name:"Ai360Spin",props:{staticImage:{},animatedImage:{},mode:{default:"auto"},trigger:{default:"hover"},width:{default:"100%"},height:{default:"auto"},alt:{default:"Product 360 view"},frameRate:{default:30},loop:{type:Boolean,default:!0},reverseOnSecondHover:{type:Boolean,default:!1},direction:{default:"clockwise"},preload:{type:Boolean,default:!0},loadingImage:{},containerClass:{},imageClass:{},showLoading:{type:Boolean,default:!0},loadingText:{default:"Loading..."},enableDragSpin:{type:Boolean,default:!0},dragSensitivity:{default:10}},emits:["animation-start","animation-end","loaded","error","frame-change"],setup(c,{emit:a}){const t=c,s=a,n=e.ref(null),{isAnimating:i,isLoading:l,currentMode:f,animatedImageUrl:u,currentFrameUrl:g,startAnimation:p,stopAnimation:k,preloadImages:S,handleDragStart:E,handleDragMove:v,handleDragEnd:C}=$(t,s),V=e.computed(()=>({width:typeof t.width=="number"?`${t.width}px`:t.width,height:typeof t.height=="number"?`${t.height}px`:t.height})),B=e.computed(()=>t.trigger==="hover"&&!i.value),N=e.computed(()=>"Hover to spin");function I(){t.trigger==="hover"&&p()}function F(){t.trigger==="hover"&&k()}function A(){t.trigger==="click"&&(i.value?k():p())}function D(r){t.enableDragSpin&&f.value==="frames"?E(r):t.trigger==="click"&&A()}function d(r){t.enableDragSpin&&f.value==="frames"&&v(r)}function h(r){t.enableDragSpin&&f.value==="frames"&&C(r)}function _(){console.log("[Ai360Spin] Static image loaded, clearing loading state"),l.value&&(l.value=!1),console.log("[Ai360Spin] isLoading after static load:",l.value)}function w(){s("loaded")}function y(r){var o;console.error("Image failed to load:",(o=r.target)==null?void 0:o.src),l.value&&(l.value=!1),s("error",new Error("Failed to load image"))}return e.onMounted(async()=>{if(console.log("[Ai360Spin] onMounted - preload:",t.preload,"isLoading:",l.value),t.preload)try{await S()}catch(r){console.error("[Ai360Spin] Preload failed:",r),l.value=!1}else console.log("[Ai360Spin] Preload disabled, clearing loading state"),l.value=!1,console.log("[Ai360Spin] isLoading after clear:",l.value);t.trigger==="auto"&&p()}),(r,o)=>(e.openBlock(),e.createElementBlock("div",{ref_key:"containerRef",ref:n,class:e.normalizeClass(["ai-360-spin",r.containerClass,{"ai-360-spin--animating":e.unref(i),"ai-360-spin--loading":e.unref(l)}]),style:e.normalizeStyle(V.value),onMouseenter:I,onMouseleave:F,onClick:A,onTouchstart:D,onTouchmove:d,onTouchend:h},[e.unref(l)&&r.showLoading?(e.openBlock(),e.createElementBlock("div",G,[o[0]||(o[0]=e.createElementVNode("div",{class:"ai-360-spin__spinner"},null,-1)),e.createElementVNode("p",K,e.toDisplayString(r.loadingText),1)])):e.createCommentVNode("",!0),e.withDirectives(e.createElementVNode("img",{src:r.staticImage,alt:r.alt,class:e.normalizeClass(["ai-360-spin__image","ai-360-spin__image--static",r.imageClass]),onLoad:_,onError:y},null,42,O),[[e.vShow,!e.unref(i)&&!e.unref(l)]]),e.unref(f)==="gif"&&!e.unref(l)?e.withDirectives((e.openBlock(),e.createElementBlock("img",{key:1,src:e.unref(u),alt:r.alt,class:e.normalizeClass(["ai-360-spin__image","ai-360-spin__image--animated",r.imageClass]),onLoad:w,onError:y},null,42,R)),[[e.vShow,e.unref(i)]]):e.createCommentVNode("",!0),e.unref(f)==="frames"&&!e.unref(l)?e.withDirectives((e.openBlock(),e.createElementBlock("img",{key:2,src:e.unref(g),alt:r.alt,class:e.normalizeClass(["ai-360-spin__image","ai-360-spin__image--frame",r.imageClass])},null,10,q)),[[e.vShow,e.unref(i)]]):e.createCommentVNode("",!0),!e.unref(i)&&!e.unref(l)&&B.value?(e.openBlock(),e.createElementBlock("div",j,[o[1]||(o[1]=e.createElementVNode("svg",{class:"ai-360-spin__hint-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor"},[e.createElementVNode("path",{d:"M12 2L2 7l10 5 10-5-10-5z"}),e.createElementVNode("path",{d:"M2 17l10 5 10-5"}),e.createElementVNode("path",{d:"M2 12l10 5 10-5"})],-1)),e.createElementVNode("span",null,e.toDisplayString(N.value),1)])):e.createCommentVNode("",!0)],38))}}),M=(c,a)=>{const t=c.__vccOpts||c;for(const[s,n]of a)t[s]=n;return t},P=M(X,[["__scopeId","data-v-915b2a52"]]);class z{constructor(a,t){T(this,"config");T(this,"onProgress");this.config={provider:a.provider||"openai",apiKey:a.apiKey,frameCount:a.frameCount||36,backgroundColor:a.backgroundColor||"white",customBackgroundColor:a.customBackgroundColor||"#ffffff",quality:a.quality||80,imageSize:a.imageSize||"1024x1024",model:a.model||(a.provider==="openai"?"dall-e-3":"stable-diffusion-xl-1024-v1-0"),useVisionAnalysis:a.useVisionAnalysis!==!1,promptTemplate:a.promptTemplate||""},this.onProgress=t}async generate(a){const t=Date.now(),s=[];try{const n=typeof a=="string"?a:await this.fileToBase64(a);this.updateProgress(0,"Analyzing product image...");let i="";this.config.useVisionAnalysis&&this.config.provider==="openai"?i=await this.analyzeProduct(n):i="Product image",this.updateProgress(10,"Product analyzed. Generating frames...");const l=360/this.config.frameCount;for(let u=0;u<this.config.frameCount;u++){const g=Math.round(u*l),p=10+Math.round(u/this.config.frameCount*85);this.updateProgress(p,`Generating frame ${u+1}/${this.config.frameCount} (${g}°)...`,s);const k=await this.generateFrame(i,g,u);s.push(k)}this.updateProgress(100,"Generation complete!",s);const f=Date.now()-t;return{frames:s,productDescription:i,metadata:{provider:this.config.provider,frameCount:this.config.frameCount,generationTime:f,model:this.config.model}}}catch(n){throw console.error("AI 360 Generation error:",n),n}}async analyzeProduct(a){var n,i;const t=await fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({model:"gpt-4o",messages:[{role:"user",content:[{type:"text",text:"Analyze this product image and provide a detailed description including: product type, color, material, key features, and style. Be concise but specific. This will be used to generate 360-degree views."},{type:"image_url",image_url:{url:a}}]}],max_tokens:300})});if(!t.ok)throw new Error(`Vision API error: ${t.statusText}`);return((i=(n=(await t.json()).choices[0])==null?void 0:n.message)==null?void 0:i.content)||"Product"}async generateFrame(a,t,s){return this.config.provider==="openai"?this.generateFrameOpenAI(a,t,s):this.generateFrameStability(a,t,s)}async generateFrameOpenAI(a,t,s){var g;const n=this.getBackgroundColorValue(),i=this.config.promptTemplate||`Create a high-quality product photograph of: ${a}
|
|
2
|
+
View angle: ${t} degrees rotation (0° is front view, rotating clockwise around vertical axis)
|
|
3
|
+
Background: ${n}
|
|
4
|
+
Style: Professional product photography, studio lighting, high detail, sharp focus, centered composition
|
|
5
|
+
The product should be clearly visible and well-lit from this ${t}° angle.`,l=await fetch("https://api.openai.com/v1/images/generations",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({model:this.config.model,prompt:i.substring(0,4e3),n:1,size:this.config.imageSize,quality:"hd",style:"natural"})});if(!l.ok)throw new Error(`OpenAI API error: ${l.statusText}`);const u=(g=(await l.json()).data[0])==null?void 0:g.url;if(!u)throw new Error("No image URL returned from OpenAI");return this.urlToBase64(u)}async generateFrameStability(a,t,s){var g;const n=this.getBackgroundColorValue(),i=this.config.promptTemplate||`Professional product photograph of ${a}, viewed from ${t} degrees angle, ${n} background, studio lighting, high quality, detailed, centered`,l=await fetch("https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`,Accept:"application/json"},body:JSON.stringify({text_prompts:[{text:i,weight:1}],cfg_scale:7,height:1024,width:1024,samples:1,steps:30})});if(!l.ok)throw new Error(`Stability AI error: ${l.statusText}`);const u=(g=(await l.json()).artifacts[0])==null?void 0:g.base64;if(!u)throw new Error("No image returned from Stability AI");return`data:image/png;base64,${u}`}getBackgroundColorValue(){switch(this.config.backgroundColor){case"white":return"pure white";case"transparent":return"transparent/alpha channel";case"black":return"pure black";case"custom":return this.config.customBackgroundColor;default:return"white"}}fileToBase64(a){return new Promise((t,s)=>{const n=new FileReader;n.onload=()=>t(n.result),n.onerror=s,n.readAsDataURL(a)})}async urlToBase64(a){try{const s=await(await fetch(a)).blob();return new Promise((n,i)=>{const l=new FileReader;l.onload=()=>n(l.result),l.onerror=i,l.readAsDataURL(s)})}catch(t){return console.error("Error converting URL to base64:",t),a}}updateProgress(a,t,s=[]){this.onProgress&&this.onProgress({currentFrame:s.length,totalFrames:this.config.frameCount,percentage:a,status:t,generatedFrames:s})}}const H={class:"ai-360-generator"},J={key:0,class:"ai-360-generator__api-key"},W={for:"api-key-input"},Q=["placeholder"],Y={key:1,class:"ai-360-generator__upload"},Z={key:0,class:"ai-360-generator__dropzone-content"},ee={key:1,class:"ai-360-generator__preview"},te=["src"],ae={key:0,class:"ai-360-generator__options"},oe={class:"ai-360-generator__option"},re={class:"ai-360-generator__option"},ne={class:"ai-360-generator__option"},ie={key:2,class:"ai-360-generator__progress"},le={class:"ai-360-generator__progress-bar"},se={class:"ai-360-generator__progress-text"},ce={class:"ai-360-generator__progress-detail"},ue={key:3,class:"ai-360-generator__result"},de={class:"ai-360-generator__viewer"},me={key:0,class:"ai-360-generator__frame-grid"},ge=["src","alt"],pe={key:4,class:"ai-360-generator__error"},fe=e.defineComponent({__name:"Ai360Generator",props:{provider:{default:"openai"},apiKey:{default:""},autoSaveApiKey:{type:Boolean,default:!0},showFramePreview:{type:Boolean,default:!0}},emits:["frames-generated","generation-start","generation-complete","generation-error"],setup(c,{emit:a}){const t=c,s=a,n=e.ref(t.apiKey||localStorage.getItem(`ai_360_api_key_${t.provider}`)||""),i=e.ref(null),l=e.ref(!1),f=e.ref(!1),u=e.ref([]),g=e.ref(null),p=e.ref(null),k=e.ref(36),S=e.ref("white"),E=e.ref(80),v=e.ref({currentFrame:0,totalFrames:0,percentage:0,status:"",generatedFrames:[]}),C=e.computed(()=>n.value.length>0),V=e.computed(()=>t.provider==="openai"?"OpenAI":"Stability AI");function B(){t.autoSaveApiKey&&localStorage.setItem(`ai_360_api_key_${t.provider}`,n.value)}function N(){var r;(r=p.value)==null||r.click()}function I(r){var b;const m=(b=r.target.files)==null?void 0:b[0];m&&A(m)}function F(r){var m;l.value=!1;const o=(m=r.dataTransfer)==null?void 0:m.files[0];o&&o.type.startsWith("image/")&&A(o)}function A(r){const o=new FileReader;o.onload=m=>{var b;i.value=(b=m.target)==null?void 0:b.result},o.readAsDataURL(r)}function D(){i.value=null,p.value&&(p.value.value="")}async function d(){if(!(!i.value||!n.value)){f.value=!0,g.value=null,u.value=[],s("generation-start");try{const o=await new z({provider:t.provider,apiKey:n.value,frameCount:k.value,backgroundColor:S.value,quality:E.value,useVisionAnalysis:!0},m=>{v.value=m}).generate(i.value);u.value=o.frames,s("generation-complete",o.frames)}catch(r){const o=r instanceof Error?r.message:"Generation failed";g.value=o,s("generation-error",r instanceof Error?r:new Error(o))}finally{f.value=!1}}}function h(){u.value.forEach((r,o)=>{const m=document.createElement("a");m.href=r,m.download=`360-frame-${String(o+1).padStart(3,"0")}.png`,m.click()})}function _(){u.value=[],i.value=null,g.value=null,v.value={currentFrame:0,totalFrames:0,percentage:0,status:"",generatedFrames:[]}}function w(){s("frames-generated",u.value)}function y(){g.value=null}return(r,o)=>(e.openBlock(),e.createElementBlock("div",H,[C.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",J,[e.createElementVNode("label",W,e.toDisplayString(V.value)+" API Key:",1),e.withDirectives(e.createElementVNode("input",{id:"api-key-input","onUpdate:modelValue":o[0]||(o[0]=m=>n.value=m),type:"password",placeholder:`Enter your ${V.value} API key`,class:"ai-360-generator__input"},null,8,Q),[[e.vModelText,n.value]]),e.createElementVNode("button",{onClick:B,class:"ai-360-generator__button"}," Save API Key ")])),C.value&&!f.value&&u.value.length===0?(e.openBlock(),e.createElementBlock("div",Y,[e.createElementVNode("div",{class:e.normalizeClass(["ai-360-generator__dropzone",{"ai-360-generator__dropzone--dragging":l.value}]),onDrop:e.withModifiers(F,["prevent"]),onDragover:o[1]||(o[1]=e.withModifiers(m=>l.value=!0,["prevent"])),onDragleave:o[2]||(o[2]=e.withModifiers(m=>l.value=!1,["prevent"])),onClick:N},[e.createElementVNode("input",{ref_key:"fileInputRef",ref:p,type:"file",accept:"image/*",style:{display:"none"},onChange:I},null,544),i.value?(e.openBlock(),e.createElementBlock("div",ee,[e.createElementVNode("img",{src:i.value,alt:"Uploaded product",class:"ai-360-generator__preview-image"},null,8,te),e.createElementVNode("button",{onClick:e.withModifiers(D,["stop"]),class:"ai-360-generator__clear-button"}," ✕ ")])):(e.openBlock(),e.createElementBlock("div",Z,o[6]||(o[6]=[e.createElementVNode("svg",{class:"ai-360-generator__upload-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor"},[e.createElementVNode("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"})],-1),e.createElementVNode("p",{class:"ai-360-generator__dropzone-text"}," Drop an image here or click to upload ",-1),e.createElementVNode("p",{class:"ai-360-generator__dropzone-hint"}," Upload a product image to generate 360° views ",-1)])))],34),i.value?(e.openBlock(),e.createElementBlock("div",ae,[e.createElementVNode("div",oe,[o[8]||(o[8]=e.createElementVNode("label",null,"Frame Count:",-1)),e.withDirectives(e.createElementVNode("select",{"onUpdate:modelValue":o[3]||(o[3]=m=>k.value=m),class:"ai-360-generator__select"},o[7]||(o[7]=[e.createElementVNode("option",{value:12},"12 frames (Fast)",-1),e.createElementVNode("option",{value:24},"24 frames (Balanced)",-1),e.createElementVNode("option",{value:36},"36 frames (Smooth)",-1),e.createElementVNode("option",{value:72},"72 frames (Ultra Smooth)",-1)]),512),[[e.vModelSelect,k.value,void 0,{number:!0}]])]),e.createElementVNode("div",re,[o[10]||(o[10]=e.createElementVNode("label",null,"Background:",-1)),e.withDirectives(e.createElementVNode("select",{"onUpdate:modelValue":o[4]||(o[4]=m=>S.value=m),class:"ai-360-generator__select"},o[9]||(o[9]=[e.createElementVNode("option",{value:"white"},"White",-1),e.createElementVNode("option",{value:"transparent"},"Transparent",-1),e.createElementVNode("option",{value:"black"},"Black",-1)]),512),[[e.vModelSelect,S.value]])]),e.createElementVNode("div",ne,[o[12]||(o[12]=e.createElementVNode("label",null,"Quality:",-1)),e.withDirectives(e.createElementVNode("select",{"onUpdate:modelValue":o[5]||(o[5]=m=>E.value=m),class:"ai-360-generator__select"},o[11]||(o[11]=[e.createElementVNode("option",{value:60},"Standard",-1),e.createElementVNode("option",{value:80},"High",-1),e.createElementVNode("option",{value:100},"Ultra",-1)]),512),[[e.vModelSelect,E.value,void 0,{number:!0}]])]),e.createElementVNode("button",{onClick:d,class:"ai-360-generator__generate-button"}," 🤖 Generate 360° View ")])):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),f.value?(e.openBlock(),e.createElementBlock("div",ie,[e.createElementVNode("div",le,[e.createElementVNode("div",{class:"ai-360-generator__progress-fill",style:e.normalizeStyle({width:v.value.percentage+"%"})},null,4)]),e.createElementVNode("p",se,e.toDisplayString(v.value.status),1),e.createElementVNode("p",ce," Frame "+e.toDisplayString(v.value.currentFrame)+" / "+e.toDisplayString(v.value.totalFrames)+" ("+e.toDisplayString(Math.round(v.value.percentage))+"%) ",1)])):e.createCommentVNode("",!0),u.value.length>0&&!f.value?(e.openBlock(),e.createElementBlock("div",ue,[o[13]||(o[13]=e.createElementVNode("h3",{class:"ai-360-generator__result-title"},"✅ 360° View Generated!",-1)),e.createElementVNode("div",de,[e.createVNode(P,{"static-image":u.value[0],"animated-image":u.value,mode:"frames",trigger:"hover","enable-drag-spin":!0,"frame-rate":30},null,8,["static-image","animated-image"])]),e.createElementVNode("div",{class:"ai-360-generator__actions"},[e.createElementVNode("button",{onClick:h,class:"ai-360-generator__button"}," 📥 Download Frames "),e.createElementVNode("button",{onClick:_,class:"ai-360-generator__button ai-360-generator__button--secondary"}," 🔄 Generate Another "),e.createElementVNode("button",{onClick:w,class:"ai-360-generator__button ai-360-generator__button--primary"}," ✓ Use These Frames ")]),r.showFramePreview?(e.openBlock(),e.createElementBlock("div",me,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value.slice(0,12),(m,b)=>(e.openBlock(),e.createElementBlock("img",{key:b,src:m,alt:`Frame ${b+1}`,class:"ai-360-generator__frame-thumbnail"},null,8,ge))),128))])):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),g.value?(e.openBlock(),e.createElementBlock("div",pe,[e.createElementVNode("p",null,"❌ "+e.toDisplayString(g.value),1),e.createElementVNode("button",{onClick:y,class:"ai-360-generator__button"}," Dismiss ")])):e.createCommentVNode("",!0)]))}}),U=M(fe,[["__scopeId","data-v-fc0644b2"]]),ve={install(c){c.component("Ai360Spin",P),c.component("Ai360Generator",U)}};exports.AI360Generator=z;exports.Ai360Generator=U;exports.Ai360Spin=P;exports.default=ve;exports.use360Spin=$;
|
|
2
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/composables/use360Spin.ts","../src/components/Ai360Spin.vue","../src/index.ts"],"sourcesContent":["import { ref, computed, onUnmounted } from 'vue';\nimport type { Spin360Config, SpinMode } from '../types';\n\nexport function use360Spin(\n props: Spin360Config,\n emit: any\n) {\n const isAnimating = ref(false);\n const isLoading = ref(true);\n const currentFrameIndex = ref(0);\n const animationFrameId = ref<number | null>(null);\n const preloadedImages = ref<HTMLImageElement[]>([]);\n \n // Touch/drag state\n const isDragging = ref(false);\n const dragStartX = ref(0);\n const dragStartFrame = ref(0);\n\n // Determine animation mode\n const currentMode = computed<SpinMode>(() => {\n if (props.mode && props.mode !== 'auto') {\n return props.mode;\n }\n // Auto-detect based on animatedImage type\n return Array.isArray(props.animatedImage) ? 'frames' : 'gif';\n });\n\n // Get animated image URL (for GIF mode)\n const animatedImageUrl = computed(() => {\n if (currentMode.value === 'gif' && typeof props.animatedImage === 'string') {\n return props.animatedImage;\n }\n return '';\n });\n\n // Get current frame URL (for frames mode)\n const currentFrameUrl = computed(() => {\n if (currentMode.value === 'frames' && Array.isArray(props.animatedImage)) {\n const frames = props.animatedImage;\n const index = currentFrameIndex.value % frames.length;\n return frames[index];\n }\n return '';\n });\n\n // Get frame array\n const frameArray = computed(() => {\n if (Array.isArray(props.animatedImage)) {\n return props.animatedImage;\n }\n return [];\n });\n\n // Total number of frames\n const totalFrames = computed(() => frameArray.value.length);\n\n // Preload all images\n async function preloadImages() {\n isLoading.value = true;\n\n try {\n if (currentMode.value === 'gif') {\n // Preload GIF\n await loadImage(animatedImageUrl.value);\n } else if (currentMode.value === 'frames') {\n // Preload all frames\n const loadPromises = frameArray.value.map(url => loadImage(url));\n const images = await Promise.all(loadPromises);\n preloadedImages.value = images;\n }\n\n // Also preload static image\n await loadImage(props.staticImage);\n\n isLoading.value = false;\n emit('loaded');\n } catch (error) {\n isLoading.value = false;\n emit('error', error);\n }\n }\n\n // Load a single image with timeout\n function loadImage(url: string, timeout = 5000): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n\n // Don't set crossOrigin - it causes CORS issues with many image services\n // If you need CORS support, the image server must explicitly allow it\n\n const timeoutId = setTimeout(() => {\n reject(new Error(`Image load timeout: ${url}`));\n }, timeout);\n\n img.onload = () => {\n clearTimeout(timeoutId);\n resolve(img);\n };\n\n img.onerror = () => {\n clearTimeout(timeoutId);\n reject(new Error(`Failed to load image: ${url}`));\n };\n\n img.src = url;\n });\n }\n\n // Start animation\n function startAnimation() {\n if (isAnimating.value || isLoading.value) return;\n\n isAnimating.value = true;\n emit('animation-start');\n\n if (currentMode.value === 'frames') {\n startFrameAnimation();\n }\n }\n\n // Stop animation\n function stopAnimation() {\n if (!isAnimating.value) return;\n\n isAnimating.value = false;\n emit('animation-end');\n\n if (animationFrameId.value !== null) {\n cancelAnimationFrame(animationFrameId.value);\n animationFrameId.value = null;\n }\n\n // Reset to first frame\n if (currentMode.value === 'frames') {\n currentFrameIndex.value = 0;\n }\n }\n\n // Start frame-by-frame animation\n function startFrameAnimation() {\n if (totalFrames.value === 0) return;\n\n const frameDelay = 1000 / (props.frameRate || 30);\n let lastFrameTime = Date.now();\n\n function animate() {\n const now = Date.now();\n const elapsed = now - lastFrameTime;\n\n if (elapsed >= frameDelay) {\n // Update frame\n if (props.direction === 'clockwise') {\n currentFrameIndex.value = (currentFrameIndex.value + 1) % totalFrames.value;\n } else {\n currentFrameIndex.value = currentFrameIndex.value === 0 \n ? totalFrames.value - 1 \n : currentFrameIndex.value - 1;\n }\n\n emit('frame-change', currentFrameIndex.value);\n lastFrameTime = now;\n\n // Check if we should loop\n if (!props.loop && currentFrameIndex.value === 0) {\n stopAnimation();\n return;\n }\n }\n\n if (isAnimating.value) {\n animationFrameId.value = requestAnimationFrame(animate);\n }\n }\n\n animationFrameId.value = requestAnimationFrame(animate);\n }\n\n // Handle drag start\n function handleDragStart(event: TouchEvent | MouseEvent) {\n if (currentMode.value !== 'frames' || totalFrames.value === 0) return;\n\n isDragging.value = true;\n dragStartFrame.value = currentFrameIndex.value;\n\n if (event instanceof TouchEvent) {\n dragStartX.value = event.touches[0].clientX;\n } else {\n dragStartX.value = event.clientX;\n }\n\n // Stop auto animation if running\n if (isAnimating.value) {\n stopAnimation();\n }\n }\n\n // Handle drag move\n function handleDragMove(event: TouchEvent | MouseEvent) {\n if (!isDragging.value || currentMode.value !== 'frames') return;\n\n event.preventDefault();\n\n let currentX: number;\n if (event instanceof TouchEvent) {\n currentX = event.touches[0].clientX;\n } else {\n currentX = event.clientX;\n }\n\n const deltaX = currentX - dragStartX.value;\n const sensitivity = props.dragSensitivity || 10;\n const frameDelta = Math.floor(deltaX / sensitivity);\n\n let newFrame = dragStartFrame.value + frameDelta;\n \n // Wrap around\n while (newFrame < 0) newFrame += totalFrames.value;\n while (newFrame >= totalFrames.value) newFrame -= totalFrames.value;\n\n if (newFrame !== currentFrameIndex.value) {\n currentFrameIndex.value = newFrame;\n emit('frame-change', currentFrameIndex.value);\n }\n }\n\n // Handle drag end\n function handleDragEnd(_event: TouchEvent | MouseEvent) {\n isDragging.value = false;\n }\n\n // Cleanup on unmount\n onUnmounted(() => {\n if (animationFrameId.value !== null) {\n cancelAnimationFrame(animationFrameId.value);\n }\n });\n\n return {\n isAnimating,\n isLoading,\n currentFrameIndex,\n currentMode,\n animatedImageUrl,\n currentFrameUrl,\n totalFrames,\n startAnimation,\n stopAnimation,\n preloadImages,\n handleDragStart,\n handleDragMove,\n handleDragEnd\n };\n}\n\n","<template>\n <div\n ref=\"containerRef\"\n :class=\"['ai-360-spin', containerClass, { 'ai-360-spin--animating': isAnimating, 'ai-360-spin--loading': isLoading }]\"\n :style=\"containerStyle\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n @click=\"handleClick\"\n @touchstart=\"handleTouchStart\"\n @touchmove=\"handleTouchMove\"\n @touchend=\"handleTouchEnd\"\n >\n <!-- Loading State -->\n <div v-if=\"isLoading && showLoading\" class=\"ai-360-spin__loading\">\n <div class=\"ai-360-spin__spinner\"></div>\n <p class=\"ai-360-spin__loading-text\">{{ loadingText }}</p>\n </div>\n\n <!-- Static Image -->\n <img\n v-show=\"!isAnimating && !isLoading\"\n :src=\"staticImage\"\n :alt=\"alt\"\n :class=\"['ai-360-spin__image', 'ai-360-spin__image--static', imageClass]\"\n @load=\"handleStaticImageLoad\"\n @error=\"handleImageError\"\n />\n\n <!-- Animated Image (GIF mode) -->\n <img\n v-if=\"currentMode === 'gif' && !isLoading\"\n v-show=\"isAnimating\"\n :src=\"animatedImageUrl\"\n :alt=\"alt\"\n :class=\"['ai-360-spin__image', 'ai-360-spin__image--animated', imageClass]\"\n @load=\"handleAnimatedImageLoad\"\n @error=\"handleImageError\"\n />\n\n <!-- Frame Sequence Mode -->\n <img\n v-if=\"currentMode === 'frames' && !isLoading\"\n v-show=\"isAnimating\"\n :src=\"currentFrameUrl\"\n :alt=\"alt\"\n :class=\"['ai-360-spin__image', 'ai-360-spin__image--frame', imageClass]\"\n />\n\n <!-- Hover Hint (optional) -->\n <div v-if=\"!isAnimating && !isLoading && showHint\" class=\"ai-360-spin__hint\">\n <svg class=\"ai-360-spin__hint-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\">\n <path d=\"M12 2L2 7l10 5 10-5-10-5z\"/>\n <path d=\"M2 17l10 5 10-5\"/>\n <path d=\"M2 12l10 5 10-5\"/>\n </svg>\n <span>{{ hintText }}</span>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue';\nimport type { Spin360Config } from '../types';\nimport { use360Spin } from '../composables/use360Spin';\n\nconst props = withDefaults(defineProps<Spin360Config>(), {\n mode: 'auto',\n trigger: 'hover',\n width: '100%',\n height: 'auto',\n alt: 'Product 360 view',\n frameRate: 30,\n loop: true,\n reverseOnSecondHover: false,\n direction: 'clockwise',\n preload: true,\n showLoading: true,\n loadingText: 'Loading...',\n enableDragSpin: true,\n dragSensitivity: 10\n});\n\nconst emit = defineEmits<{\n 'animation-start': [];\n 'animation-end': [];\n 'loaded': [];\n 'error': [error: Error];\n 'frame-change': [frame: number];\n}>();\n\nconst containerRef = ref<HTMLElement | null>(null);\n\nconst {\n isAnimating,\n isLoading,\n currentMode,\n animatedImageUrl,\n currentFrameUrl,\n startAnimation,\n stopAnimation,\n preloadImages,\n handleDragStart,\n handleDragMove,\n handleDragEnd\n} = use360Spin(props, emit);\n\n// Computed styles\nconst containerStyle = computed(() => ({\n width: typeof props.width === 'number' ? `${props.width}px` : props.width,\n height: typeof props.height === 'number' ? `${props.height}px` : props.height\n}));\n\n// Show hint on hover trigger\nconst showHint = computed(() => props.trigger === 'hover' && !isAnimating.value);\nconst hintText = computed(() => 'Hover to spin');\n\n// Event handlers\nfunction handleMouseEnter() {\n if (props.trigger === 'hover') {\n startAnimation();\n }\n}\n\nfunction handleMouseLeave() {\n if (props.trigger === 'hover') {\n stopAnimation();\n }\n}\n\nfunction handleClick() {\n if (props.trigger === 'click') {\n if (isAnimating.value) {\n stopAnimation();\n } else {\n startAnimation();\n }\n }\n}\n\nfunction handleTouchStart(event: TouchEvent) {\n if (props.enableDragSpin && currentMode.value === 'frames') {\n handleDragStart(event);\n } else if (props.trigger === 'click') {\n handleClick();\n }\n}\n\nfunction handleTouchMove(event: TouchEvent) {\n if (props.enableDragSpin && currentMode.value === 'frames') {\n handleDragMove(event);\n }\n}\n\nfunction handleTouchEnd(event: TouchEvent) {\n if (props.enableDragSpin && currentMode.value === 'frames') {\n handleDragEnd(event);\n }\n}\n\nfunction handleStaticImageLoad() {\n console.log('[Ai360Spin] Static image loaded, clearing loading state');\n // Static image loaded - clear loading state\n if (isLoading.value) {\n isLoading.value = false;\n }\n console.log('[Ai360Spin] isLoading after static load:', isLoading.value);\n}\n\nfunction handleAnimatedImageLoad() {\n emit('loaded');\n}\n\nfunction handleImageError(event: Event) {\n console.error('Image failed to load:', (event.target as HTMLImageElement)?.src);\n // Clear loading state even on error so component doesn't stay stuck\n if (isLoading.value) {\n isLoading.value = false;\n }\n emit('error', new Error('Failed to load image'));\n}\n\n// Preload images on mount\nonMounted(async () => {\n console.log('[Ai360Spin] onMounted - preload:', props.preload, 'isLoading:', isLoading.value);\n\n if (props.preload) {\n try {\n await preloadImages();\n } catch (error) {\n console.error('[Ai360Spin] Preload failed:', error);\n // Clear loading state even if preload fails\n isLoading.value = false;\n }\n } else {\n // If not preloading, clear loading state immediately\n console.log('[Ai360Spin] Preload disabled, clearing loading state');\n isLoading.value = false;\n console.log('[Ai360Spin] isLoading after clear:', isLoading.value);\n }\n\n // Auto-start animation if trigger is 'auto'\n if (props.trigger === 'auto') {\n startAnimation();\n }\n});\n</script>\n\n<style scoped>\n.ai-360-spin {\n position: relative;\n display: inline-block;\n overflow: hidden;\n cursor: pointer;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}\n\n.ai-360-spin__image {\n width: 100%;\n height: 100%;\n object-fit: contain;\n display: block;\n transition: opacity 0.3s ease;\n}\n\n.ai-360-spin__image--static {\n opacity: 1;\n}\n\n.ai-360-spin__image--animated,\n.ai-360-spin__image--frame {\n position: absolute;\n top: 0;\n left: 0;\n opacity: 1;\n}\n\n.ai-360-spin--animating .ai-360-spin__image--static {\n opacity: 0;\n}\n\n.ai-360-spin__loading {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(255, 255, 255, 0.9);\n z-index: 10;\n}\n\n.ai-360-spin__spinner {\n width: 40px;\n height: 40px;\n border: 4px solid #f3f3f3;\n border-top: 4px solid #3498db;\n border-radius: 50%;\n animation: ai-360-spin-rotate 1s linear infinite;\n}\n\n@keyframes ai-360-spin-rotate {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n.ai-360-spin__loading-text {\n margin-top: 12px;\n font-size: 14px;\n color: #666;\n}\n\n.ai-360-spin__hint {\n position: absolute;\n bottom: 12px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 16px;\n background: rgba(0, 0, 0, 0.7);\n color: white;\n border-radius: 20px;\n font-size: 12px;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.3s ease;\n}\n\n.ai-360-spin:hover .ai-360-spin__hint {\n opacity: 1;\n}\n\n.ai-360-spin__hint-icon {\n width: 16px;\n height: 16px;\n stroke-width: 2;\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n .ai-360-spin {\n touch-action: none;\n }\n\n .ai-360-spin__hint {\n font-size: 11px;\n padding: 6px 12px;\n }\n}\n</style>\n\n\n","import type { App } from 'vue';\nimport Ai360Spin from './components/Ai360Spin.vue';\nimport { use360Spin } from './composables/use360Spin';\nimport type { Spin360Config, SpinMode, SpinTrigger, SpinDirection, Spin360Events } from './types';\n\n// Import styles\nimport './styles/360-spin.css';\n\n// Export components\nexport { Ai360Spin };\n\n// Export composables\nexport { use360Spin };\n\n// Export types\nexport type {\n Spin360Config,\n SpinMode,\n SpinTrigger,\n SpinDirection,\n Spin360Events\n};\n\n// Vue plugin\nexport default {\n install(app: App) {\n app.component('Ai360Spin', Ai360Spin);\n }\n};\n\n"],"names":["use360Spin","props","emit","isAnimating","ref","isLoading","currentFrameIndex","animationFrameId","preloadedImages","isDragging","dragStartX","dragStartFrame","currentMode","computed","animatedImageUrl","currentFrameUrl","frames","index","frameArray","totalFrames","preloadImages","loadImage","loadPromises","url","images","error","timeout","resolve","reject","img","timeoutId","startAnimation","startFrameAnimation","stopAnimation","frameDelay","lastFrameTime","animate","now","handleDragStart","event","handleDragMove","currentX","deltaX","sensitivity","frameDelta","newFrame","handleDragEnd","_event","onUnmounted","__props","__emit","containerRef","containerStyle","showHint","hintText","handleMouseEnter","handleMouseLeave","handleClick","handleTouchStart","handleTouchMove","handleTouchEnd","handleStaticImageLoad","handleAnimatedImageLoad","handleImageError","_a","onMounted","app","Ai360Spin"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/composables/use360Spin.ts","../src/components/Ai360Spin.vue","../src/utils/ai-generator.ts","../src/components/Ai360Generator.vue","../src/index.ts"],"sourcesContent":["import { ref, computed, onUnmounted } from 'vue';\nimport type { Spin360Config, SpinMode } from '../types';\n\nexport function use360Spin(\n props: Spin360Config,\n emit: any\n) {\n const isAnimating = ref(false);\n const isLoading = ref(true);\n const currentFrameIndex = ref(0);\n const animationFrameId = ref<number | null>(null);\n const preloadedImages = ref<HTMLImageElement[]>([]);\n \n // Touch/drag state\n const isDragging = ref(false);\n const dragStartX = ref(0);\n const dragStartFrame = ref(0);\n\n // Determine animation mode\n const currentMode = computed<SpinMode>(() => {\n if (props.mode && props.mode !== 'auto') {\n return props.mode;\n }\n // Auto-detect based on animatedImage type\n return Array.isArray(props.animatedImage) ? 'frames' : 'gif';\n });\n\n // Get animated image URL (for GIF mode)\n const animatedImageUrl = computed(() => {\n if (currentMode.value === 'gif' && typeof props.animatedImage === 'string') {\n return props.animatedImage;\n }\n return '';\n });\n\n // Get current frame URL (for frames mode)\n const currentFrameUrl = computed(() => {\n if (currentMode.value === 'frames' && Array.isArray(props.animatedImage)) {\n const frames = props.animatedImage;\n const index = currentFrameIndex.value % frames.length;\n return frames[index];\n }\n return '';\n });\n\n // Get frame array\n const frameArray = computed(() => {\n if (Array.isArray(props.animatedImage)) {\n return props.animatedImage;\n }\n return [];\n });\n\n // Total number of frames\n const totalFrames = computed(() => frameArray.value.length);\n\n // Preload all images\n async function preloadImages() {\n isLoading.value = true;\n\n try {\n if (currentMode.value === 'gif') {\n // Preload GIF\n await loadImage(animatedImageUrl.value);\n } else if (currentMode.value === 'frames') {\n // Preload all frames\n const loadPromises = frameArray.value.map(url => loadImage(url));\n const images = await Promise.all(loadPromises);\n preloadedImages.value = images;\n }\n\n // Also preload static image\n await loadImage(props.staticImage);\n\n isLoading.value = false;\n emit('loaded');\n } catch (error) {\n isLoading.value = false;\n emit('error', error);\n }\n }\n\n // Load a single image with timeout\n function loadImage(url: string, timeout = 5000): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n\n // Don't set crossOrigin - it causes CORS issues with many image services\n // If you need CORS support, the image server must explicitly allow it\n\n const timeoutId = setTimeout(() => {\n reject(new Error(`Image load timeout: ${url}`));\n }, timeout);\n\n img.onload = () => {\n clearTimeout(timeoutId);\n resolve(img);\n };\n\n img.onerror = () => {\n clearTimeout(timeoutId);\n reject(new Error(`Failed to load image: ${url}`));\n };\n\n img.src = url;\n });\n }\n\n // Start animation\n function startAnimation() {\n if (isAnimating.value || isLoading.value) return;\n\n isAnimating.value = true;\n emit('animation-start');\n\n if (currentMode.value === 'frames') {\n startFrameAnimation();\n }\n }\n\n // Stop animation\n function stopAnimation() {\n if (!isAnimating.value) return;\n\n isAnimating.value = false;\n emit('animation-end');\n\n if (animationFrameId.value !== null) {\n cancelAnimationFrame(animationFrameId.value);\n animationFrameId.value = null;\n }\n\n // Reset to first frame\n if (currentMode.value === 'frames') {\n currentFrameIndex.value = 0;\n }\n }\n\n // Start frame-by-frame animation\n function startFrameAnimation() {\n if (totalFrames.value === 0) return;\n\n const frameDelay = 1000 / (props.frameRate || 30);\n let lastFrameTime = Date.now();\n\n function animate() {\n const now = Date.now();\n const elapsed = now - lastFrameTime;\n\n if (elapsed >= frameDelay) {\n // Update frame\n if (props.direction === 'clockwise') {\n currentFrameIndex.value = (currentFrameIndex.value + 1) % totalFrames.value;\n } else {\n currentFrameIndex.value = currentFrameIndex.value === 0 \n ? totalFrames.value - 1 \n : currentFrameIndex.value - 1;\n }\n\n emit('frame-change', currentFrameIndex.value);\n lastFrameTime = now;\n\n // Check if we should loop\n if (!props.loop && currentFrameIndex.value === 0) {\n stopAnimation();\n return;\n }\n }\n\n if (isAnimating.value) {\n animationFrameId.value = requestAnimationFrame(animate);\n }\n }\n\n animationFrameId.value = requestAnimationFrame(animate);\n }\n\n // Handle drag start\n function handleDragStart(event: TouchEvent | MouseEvent) {\n if (currentMode.value !== 'frames' || totalFrames.value === 0) return;\n\n isDragging.value = true;\n dragStartFrame.value = currentFrameIndex.value;\n\n if (event instanceof TouchEvent) {\n dragStartX.value = event.touches[0].clientX;\n } else {\n dragStartX.value = event.clientX;\n }\n\n // Stop auto animation if running\n if (isAnimating.value) {\n stopAnimation();\n }\n }\n\n // Handle drag move\n function handleDragMove(event: TouchEvent | MouseEvent) {\n if (!isDragging.value || currentMode.value !== 'frames') return;\n\n event.preventDefault();\n\n let currentX: number;\n if (event instanceof TouchEvent) {\n currentX = event.touches[0].clientX;\n } else {\n currentX = event.clientX;\n }\n\n const deltaX = currentX - dragStartX.value;\n const sensitivity = props.dragSensitivity || 10;\n const frameDelta = Math.floor(deltaX / sensitivity);\n\n let newFrame = dragStartFrame.value + frameDelta;\n \n // Wrap around\n while (newFrame < 0) newFrame += totalFrames.value;\n while (newFrame >= totalFrames.value) newFrame -= totalFrames.value;\n\n if (newFrame !== currentFrameIndex.value) {\n currentFrameIndex.value = newFrame;\n emit('frame-change', currentFrameIndex.value);\n }\n }\n\n // Handle drag end\n function handleDragEnd(_event: TouchEvent | MouseEvent) {\n isDragging.value = false;\n }\n\n // Cleanup on unmount\n onUnmounted(() => {\n if (animationFrameId.value !== null) {\n cancelAnimationFrame(animationFrameId.value);\n }\n });\n\n return {\n isAnimating,\n isLoading,\n currentFrameIndex,\n currentMode,\n animatedImageUrl,\n currentFrameUrl,\n totalFrames,\n startAnimation,\n stopAnimation,\n preloadImages,\n handleDragStart,\n handleDragMove,\n handleDragEnd\n };\n}\n\n","<template>\n <div\n ref=\"containerRef\"\n :class=\"['ai-360-spin', containerClass, { 'ai-360-spin--animating': isAnimating, 'ai-360-spin--loading': isLoading }]\"\n :style=\"containerStyle\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n @click=\"handleClick\"\n @touchstart=\"handleTouchStart\"\n @touchmove=\"handleTouchMove\"\n @touchend=\"handleTouchEnd\"\n >\n <!-- Loading State -->\n <div v-if=\"isLoading && showLoading\" class=\"ai-360-spin__loading\">\n <div class=\"ai-360-spin__spinner\"></div>\n <p class=\"ai-360-spin__loading-text\">{{ loadingText }}</p>\n </div>\n\n <!-- Static Image -->\n <img\n v-show=\"!isAnimating && !isLoading\"\n :src=\"staticImage\"\n :alt=\"alt\"\n :class=\"['ai-360-spin__image', 'ai-360-spin__image--static', imageClass]\"\n @load=\"handleStaticImageLoad\"\n @error=\"handleImageError\"\n />\n\n <!-- Animated Image (GIF mode) -->\n <img\n v-if=\"currentMode === 'gif' && !isLoading\"\n v-show=\"isAnimating\"\n :src=\"animatedImageUrl\"\n :alt=\"alt\"\n :class=\"['ai-360-spin__image', 'ai-360-spin__image--animated', imageClass]\"\n @load=\"handleAnimatedImageLoad\"\n @error=\"handleImageError\"\n />\n\n <!-- Frame Sequence Mode -->\n <img\n v-if=\"currentMode === 'frames' && !isLoading\"\n v-show=\"isAnimating\"\n :src=\"currentFrameUrl\"\n :alt=\"alt\"\n :class=\"['ai-360-spin__image', 'ai-360-spin__image--frame', imageClass]\"\n />\n\n <!-- Hover Hint (optional) -->\n <div v-if=\"!isAnimating && !isLoading && showHint\" class=\"ai-360-spin__hint\">\n <svg class=\"ai-360-spin__hint-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\">\n <path d=\"M12 2L2 7l10 5 10-5-10-5z\"/>\n <path d=\"M2 17l10 5 10-5\"/>\n <path d=\"M2 12l10 5 10-5\"/>\n </svg>\n <span>{{ hintText }}</span>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue';\nimport type { Spin360Config } from '../types';\nimport { use360Spin } from '../composables/use360Spin';\n\nconst props = withDefaults(defineProps<Spin360Config>(), {\n mode: 'auto',\n trigger: 'hover',\n width: '100%',\n height: 'auto',\n alt: 'Product 360 view',\n frameRate: 30,\n loop: true,\n reverseOnSecondHover: false,\n direction: 'clockwise',\n preload: true,\n showLoading: true,\n loadingText: 'Loading...',\n enableDragSpin: true,\n dragSensitivity: 10\n});\n\nconst emit = defineEmits<{\n 'animation-start': [];\n 'animation-end': [];\n 'loaded': [];\n 'error': [error: Error];\n 'frame-change': [frame: number];\n}>();\n\nconst containerRef = ref<HTMLElement | null>(null);\n\nconst {\n isAnimating,\n isLoading,\n currentMode,\n animatedImageUrl,\n currentFrameUrl,\n startAnimation,\n stopAnimation,\n preloadImages,\n handleDragStart,\n handleDragMove,\n handleDragEnd\n} = use360Spin(props, emit);\n\n// Computed styles\nconst containerStyle = computed(() => ({\n width: typeof props.width === 'number' ? `${props.width}px` : props.width,\n height: typeof props.height === 'number' ? `${props.height}px` : props.height\n}));\n\n// Show hint on hover trigger\nconst showHint = computed(() => props.trigger === 'hover' && !isAnimating.value);\nconst hintText = computed(() => 'Hover to spin');\n\n// Event handlers\nfunction handleMouseEnter() {\n if (props.trigger === 'hover') {\n startAnimation();\n }\n}\n\nfunction handleMouseLeave() {\n if (props.trigger === 'hover') {\n stopAnimation();\n }\n}\n\nfunction handleClick() {\n if (props.trigger === 'click') {\n if (isAnimating.value) {\n stopAnimation();\n } else {\n startAnimation();\n }\n }\n}\n\nfunction handleTouchStart(event: TouchEvent) {\n if (props.enableDragSpin && currentMode.value === 'frames') {\n handleDragStart(event);\n } else if (props.trigger === 'click') {\n handleClick();\n }\n}\n\nfunction handleTouchMove(event: TouchEvent) {\n if (props.enableDragSpin && currentMode.value === 'frames') {\n handleDragMove(event);\n }\n}\n\nfunction handleTouchEnd(event: TouchEvent) {\n if (props.enableDragSpin && currentMode.value === 'frames') {\n handleDragEnd(event);\n }\n}\n\nfunction handleStaticImageLoad() {\n console.log('[Ai360Spin] Static image loaded, clearing loading state');\n // Static image loaded - clear loading state\n if (isLoading.value) {\n isLoading.value = false;\n }\n console.log('[Ai360Spin] isLoading after static load:', isLoading.value);\n}\n\nfunction handleAnimatedImageLoad() {\n emit('loaded');\n}\n\nfunction handleImageError(event: Event) {\n console.error('Image failed to load:', (event.target as HTMLImageElement)?.src);\n // Clear loading state even on error so component doesn't stay stuck\n if (isLoading.value) {\n isLoading.value = false;\n }\n emit('error', new Error('Failed to load image'));\n}\n\n// Preload images on mount\nonMounted(async () => {\n console.log('[Ai360Spin] onMounted - preload:', props.preload, 'isLoading:', isLoading.value);\n\n if (props.preload) {\n try {\n await preloadImages();\n } catch (error) {\n console.error('[Ai360Spin] Preload failed:', error);\n // Clear loading state even if preload fails\n isLoading.value = false;\n }\n } else {\n // If not preloading, clear loading state immediately\n console.log('[Ai360Spin] Preload disabled, clearing loading state');\n isLoading.value = false;\n console.log('[Ai360Spin] isLoading after clear:', isLoading.value);\n }\n\n // Auto-start animation if trigger is 'auto'\n if (props.trigger === 'auto') {\n startAnimation();\n }\n});\n</script>\n\n<style scoped>\n.ai-360-spin {\n position: relative;\n display: inline-block;\n overflow: hidden;\n cursor: pointer;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}\n\n.ai-360-spin__image {\n width: 100%;\n height: 100%;\n object-fit: contain;\n display: block;\n transition: opacity 0.3s ease;\n}\n\n.ai-360-spin__image--static {\n opacity: 1;\n}\n\n.ai-360-spin__image--animated,\n.ai-360-spin__image--frame {\n position: absolute;\n top: 0;\n left: 0;\n opacity: 1;\n}\n\n.ai-360-spin--animating .ai-360-spin__image--static {\n opacity: 0;\n}\n\n.ai-360-spin__loading {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(255, 255, 255, 0.9);\n z-index: 10;\n}\n\n.ai-360-spin__spinner {\n width: 40px;\n height: 40px;\n border: 4px solid #f3f3f3;\n border-top: 4px solid #3498db;\n border-radius: 50%;\n animation: ai-360-spin-rotate 1s linear infinite;\n}\n\n@keyframes ai-360-spin-rotate {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n.ai-360-spin__loading-text {\n margin-top: 12px;\n font-size: 14px;\n color: #666;\n}\n\n.ai-360-spin__hint {\n position: absolute;\n bottom: 12px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 16px;\n background: rgba(0, 0, 0, 0.7);\n color: white;\n border-radius: 20px;\n font-size: 12px;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.3s ease;\n}\n\n.ai-360-spin:hover .ai-360-spin__hint {\n opacity: 1;\n}\n\n.ai-360-spin__hint-icon {\n width: 16px;\n height: 16px;\n stroke-width: 2;\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n .ai-360-spin {\n touch-action: none;\n }\n\n .ai-360-spin__hint {\n font-size: 11px;\n padding: 6px 12px;\n }\n}\n</style>\n\n\n","import type { AI360GeneratorConfig, AI360GenerationProgress, AI360GenerationResult } from '../types';\n\n/**\n * AI 360 Generator - Generates 360-degree product views from a single image\n */\nexport class AI360Generator {\n private config: Required<AI360GeneratorConfig>;\n private onProgress?: (progress: AI360GenerationProgress) => void;\n\n constructor(config: AI360GeneratorConfig, onProgress?: (progress: AI360GenerationProgress) => void) {\n this.config = {\n provider: config.provider || 'openai',\n apiKey: config.apiKey,\n frameCount: config.frameCount || 36,\n backgroundColor: config.backgroundColor || 'white',\n customBackgroundColor: config.customBackgroundColor || '#ffffff',\n quality: config.quality || 80,\n imageSize: config.imageSize || '1024x1024',\n model: config.model || (config.provider === 'openai' ? 'dall-e-3' : 'stable-diffusion-xl-1024-v1-0'),\n useVisionAnalysis: config.useVisionAnalysis !== false,\n promptTemplate: config.promptTemplate || ''\n };\n this.onProgress = onProgress;\n }\n\n /**\n * Generate 360-degree frames from a single product image\n */\n async generate(imageFile: File | string): Promise<AI360GenerationResult> {\n const startTime = Date.now();\n const frames: string[] = [];\n\n try {\n // Step 1: Convert image to base64 if it's a File\n const imageBase64 = typeof imageFile === 'string' \n ? imageFile \n : await this.fileToBase64(imageFile);\n\n this.updateProgress(0, 'Analyzing product image...');\n\n // Step 2: Analyze the product using GPT-4 Vision (if enabled)\n let productDescription = '';\n if (this.config.useVisionAnalysis && this.config.provider === 'openai') {\n productDescription = await this.analyzeProduct(imageBase64);\n } else {\n productDescription = 'Product image';\n }\n\n this.updateProgress(10, 'Product analyzed. Generating frames...');\n\n // Step 3: Generate frames at different angles\n const angleStep = 360 / this.config.frameCount;\n\n for (let i = 0; i < this.config.frameCount; i++) {\n const angle = Math.round(i * angleStep);\n const progress = 10 + Math.round((i / this.config.frameCount) * 85);\n \n this.updateProgress(progress, `Generating frame ${i + 1}/${this.config.frameCount} (${angle}°)...`, frames);\n\n const frameUrl = await this.generateFrame(productDescription, angle, i);\n frames.push(frameUrl);\n }\n\n this.updateProgress(100, 'Generation complete!', frames);\n\n const generationTime = Date.now() - startTime;\n\n return {\n frames,\n productDescription,\n metadata: {\n provider: this.config.provider,\n frameCount: this.config.frameCount,\n generationTime,\n model: this.config.model\n }\n };\n } catch (error) {\n console.error('AI 360 Generation error:', error);\n throw error;\n }\n }\n\n /**\n * Analyze product image using GPT-4 Vision\n */\n private async analyzeProduct(imageBase64: string): Promise<string> {\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.config.apiKey}`\n },\n body: JSON.stringify({\n model: 'gpt-4o',\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: 'Analyze this product image and provide a detailed description including: product type, color, material, key features, and style. Be concise but specific. This will be used to generate 360-degree views.'\n },\n {\n type: 'image_url',\n image_url: { url: imageBase64 }\n }\n ]\n }\n ],\n max_tokens: 300\n })\n });\n\n if (!response.ok) {\n throw new Error(`Vision API error: ${response.statusText}`);\n }\n\n const data = await response.json();\n return data.choices[0]?.message?.content || 'Product';\n }\n\n /**\n * Generate a single frame at a specific angle\n */\n private async generateFrame(productDescription: string, angle: number, frameIndex: number): Promise<string> {\n if (this.config.provider === 'openai') {\n return this.generateFrameOpenAI(productDescription, angle, frameIndex);\n } else {\n return this.generateFrameStability(productDescription, angle, frameIndex);\n }\n }\n\n /**\n * Generate frame using OpenAI DALL-E\n */\n private async generateFrameOpenAI(productDescription: string, angle: number, _frameIndex: number): Promise<string> {\n const backgroundColor = this.getBackgroundColorValue();\n \n const prompt = this.config.promptTemplate || \n `Create a high-quality product photograph of: ${productDescription}\nView angle: ${angle} degrees rotation (0° is front view, rotating clockwise around vertical axis)\nBackground: ${backgroundColor}\nStyle: Professional product photography, studio lighting, high detail, sharp focus, centered composition\nThe product should be clearly visible and well-lit from this ${angle}° angle.`;\n\n const response = await fetch('https://api.openai.com/v1/images/generations', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.config.apiKey}`\n },\n body: JSON.stringify({\n model: this.config.model,\n prompt: prompt.substring(0, 4000), // DALL-E has prompt limits\n n: 1,\n size: this.config.imageSize,\n quality: 'hd',\n style: 'natural'\n })\n });\n\n if (!response.ok) {\n throw new Error(`OpenAI API error: ${response.statusText}`);\n }\n\n const data = await response.json();\n const imageUrl = data.data[0]?.url;\n\n if (!imageUrl) {\n throw new Error('No image URL returned from OpenAI');\n }\n\n // Convert to base64 for local storage\n return this.urlToBase64(imageUrl);\n }\n\n /**\n * Generate frame using Stability AI\n */\n private async generateFrameStability(productDescription: string, angle: number, _frameIndex: number): Promise<string> {\n const backgroundColor = this.getBackgroundColorValue();\n\n const prompt = this.config.promptTemplate ||\n `Professional product photograph of ${productDescription}, viewed from ${angle} degrees angle, ${backgroundColor} background, studio lighting, high quality, detailed, centered`;\n\n const response = await fetch('https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Accept': 'application/json'\n },\n body: JSON.stringify({\n text_prompts: [\n {\n text: prompt,\n weight: 1\n }\n ],\n cfg_scale: 7,\n height: 1024,\n width: 1024,\n samples: 1,\n steps: 30\n })\n });\n\n if (!response.ok) {\n throw new Error(`Stability AI error: ${response.statusText}`);\n }\n\n const data = await response.json();\n const base64Image = data.artifacts[0]?.base64;\n\n if (!base64Image) {\n throw new Error('No image returned from Stability AI');\n }\n\n return `data:image/png;base64,${base64Image}`;\n }\n\n /**\n * Get background color value based on config\n */\n private getBackgroundColorValue(): string {\n switch (this.config.backgroundColor) {\n case 'white':\n return 'pure white';\n case 'transparent':\n return 'transparent/alpha channel';\n case 'black':\n return 'pure black';\n case 'custom':\n return this.config.customBackgroundColor;\n default:\n return 'white';\n }\n }\n\n /**\n * Convert File to base64\n */\n private fileToBase64(file: File): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n }\n\n /**\n * Convert URL to base64\n */\n private async urlToBase64(url: string): Promise<string> {\n try {\n const response = await fetch(url);\n const blob = await response.blob();\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n } catch (error) {\n console.error('Error converting URL to base64:', error);\n return url; // Return original URL if conversion fails\n }\n }\n\n /**\n * Update progress callback\n */\n private updateProgress(percentage: number, status: string, generatedFrames: string[] = []): void {\n if (this.onProgress) {\n this.onProgress({\n currentFrame: generatedFrames.length,\n totalFrames: this.config.frameCount,\n percentage,\n status,\n generatedFrames\n });\n }\n }\n}\n","<template>\n <div class=\"ai-360-generator\">\n <!-- API Key Input (if not provided) -->\n <div v-if=\"!hasApiKey\" class=\"ai-360-generator__api-key\">\n <label for=\"api-key-input\">{{ providerLabel }} API Key:</label>\n <input\n id=\"api-key-input\"\n v-model=\"apiKeyInput\"\n type=\"password\"\n :placeholder=\"`Enter your ${providerLabel} API key`\"\n class=\"ai-360-generator__input\"\n />\n <button @click=\"saveApiKey\" class=\"ai-360-generator__button\">\n Save API Key\n </button>\n </div>\n\n <!-- Upload Section -->\n <div v-if=\"hasApiKey && !isGenerating && generatedFrames.length === 0\" class=\"ai-360-generator__upload\">\n <div\n class=\"ai-360-generator__dropzone\"\n :class=\"{ 'ai-360-generator__dropzone--dragging': isDragging }\"\n @drop.prevent=\"handleDrop\"\n @dragover.prevent=\"isDragging = true\"\n @dragleave.prevent=\"isDragging = false\"\n @click=\"triggerFileInput\"\n >\n <input\n ref=\"fileInputRef\"\n type=\"file\"\n accept=\"image/*\"\n style=\"display: none\"\n @change=\"handleFileSelect\"\n />\n \n <div v-if=\"!uploadedImage\" class=\"ai-360-generator__dropzone-content\">\n <svg class=\"ai-360-generator__upload-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n <p class=\"ai-360-generator__dropzone-text\">\n Drop an image here or click to upload\n </p>\n <p class=\"ai-360-generator__dropzone-hint\">\n Upload a product image to generate 360° views\n </p>\n </div>\n\n <div v-else class=\"ai-360-generator__preview\">\n <img :src=\"uploadedImage\" alt=\"Uploaded product\" class=\"ai-360-generator__preview-image\" />\n <button @click.stop=\"clearUpload\" class=\"ai-360-generator__clear-button\">\n ✕\n </button>\n </div>\n </div>\n\n <!-- Generation Options -->\n <div v-if=\"uploadedImage\" class=\"ai-360-generator__options\">\n <div class=\"ai-360-generator__option\">\n <label>Frame Count:</label>\n <select v-model.number=\"frameCount\" class=\"ai-360-generator__select\">\n <option :value=\"12\">12 frames (Fast)</option>\n <option :value=\"24\">24 frames (Balanced)</option>\n <option :value=\"36\">36 frames (Smooth)</option>\n <option :value=\"72\">72 frames (Ultra Smooth)</option>\n </select>\n </div>\n\n <div class=\"ai-360-generator__option\">\n <label>Background:</label>\n <select v-model=\"backgroundColor\" class=\"ai-360-generator__select\">\n <option value=\"white\">White</option>\n <option value=\"transparent\">Transparent</option>\n <option value=\"black\">Black</option>\n </select>\n </div>\n\n <div class=\"ai-360-generator__option\">\n <label>Quality:</label>\n <select v-model.number=\"quality\" class=\"ai-360-generator__select\">\n <option :value=\"60\">Standard</option>\n <option :value=\"80\">High</option>\n <option :value=\"100\">Ultra</option>\n </select>\n </div>\n\n <button @click=\"startGeneration\" class=\"ai-360-generator__generate-button\">\n 🤖 Generate 360° View\n </button>\n </div>\n </div>\n\n <!-- Progress Section -->\n <div v-if=\"isGenerating\" class=\"ai-360-generator__progress\">\n <div class=\"ai-360-generator__progress-bar\">\n <div \n class=\"ai-360-generator__progress-fill\" \n :style=\"{ width: progress.percentage + '%' }\"\n ></div>\n </div>\n <p class=\"ai-360-generator__progress-text\">\n {{ progress.status }}\n </p>\n <p class=\"ai-360-generator__progress-detail\">\n Frame {{ progress.currentFrame }} / {{ progress.totalFrames }} ({{ Math.round(progress.percentage) }}%)\n </p>\n </div>\n\n <!-- Result Section -->\n <div v-if=\"generatedFrames.length > 0 && !isGenerating\" class=\"ai-360-generator__result\">\n <h3 class=\"ai-360-generator__result-title\">✅ 360° View Generated!</h3>\n \n <!-- 360 Viewer -->\n <div class=\"ai-360-generator__viewer\">\n <Ai360Spin\n :static-image=\"generatedFrames[0]\"\n :animated-image=\"generatedFrames\"\n mode=\"frames\"\n trigger=\"hover\"\n :enable-drag-spin=\"true\"\n :frame-rate=\"30\"\n />\n </div>\n\n <!-- Actions -->\n <div class=\"ai-360-generator__actions\">\n <button @click=\"downloadFrames\" class=\"ai-360-generator__button\">\n 📥 Download Frames\n </button>\n <button @click=\"reset\" class=\"ai-360-generator__button ai-360-generator__button--secondary\">\n 🔄 Generate Another\n </button>\n <button @click=\"emitFrames\" class=\"ai-360-generator__button ai-360-generator__button--primary\">\n ✓ Use These Frames\n </button>\n </div>\n\n <!-- Frame Preview Grid -->\n <div v-if=\"showFramePreview\" class=\"ai-360-generator__frame-grid\">\n <img\n v-for=\"(frame, index) in generatedFrames.slice(0, 12)\"\n :key=\"index\"\n :src=\"frame\"\n :alt=\"`Frame ${index + 1}`\"\n class=\"ai-360-generator__frame-thumbnail\"\n />\n </div>\n </div>\n\n <!-- Error Display -->\n <div v-if=\"error\" class=\"ai-360-generator__error\">\n <p>❌ {{ error }}</p>\n <button @click=\"clearError\" class=\"ai-360-generator__button\">\n Dismiss\n </button>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed } from 'vue';\nimport { AI360Generator } from '../utils/ai-generator';\nimport type { AIProvider, BackgroundColor, AI360GenerationProgress } from '../types';\nimport Ai360Spin from './Ai360Spin.vue';\n\n// Props\nconst props = withDefaults(defineProps<{\n provider?: AIProvider;\n apiKey?: string;\n autoSaveApiKey?: boolean;\n showFramePreview?: boolean;\n}>(), {\n provider: 'openai',\n apiKey: '',\n autoSaveApiKey: true,\n showFramePreview: true\n});\n\n// Emits\nconst emit = defineEmits<{\n 'frames-generated': [frames: string[]];\n 'generation-start': [];\n 'generation-complete': [frames: string[]];\n 'generation-error': [error: Error];\n}>();\n\n// State\nconst apiKeyInput = ref(props.apiKey || localStorage.getItem(`ai_360_api_key_${props.provider}`) || '');\nconst uploadedImage = ref<string | null>(null);\nconst isDragging = ref(false);\nconst isGenerating = ref(false);\nconst generatedFrames = ref<string[]>([]);\nconst error = ref<string | null>(null);\nconst fileInputRef = ref<HTMLInputElement | null>(null);\n\n// Generation options\nconst frameCount = ref(36);\nconst backgroundColor = ref<BackgroundColor>('white');\nconst quality = ref(80);\n\n// Progress\nconst progress = ref<AI360GenerationProgress>({\n currentFrame: 0,\n totalFrames: 0,\n percentage: 0,\n status: '',\n generatedFrames: []\n});\n\n// Computed\nconst hasApiKey = computed(() => apiKeyInput.value.length > 0);\nconst providerLabel = computed(() => props.provider === 'openai' ? 'OpenAI' : 'Stability AI');\n\n// Methods\nfunction saveApiKey() {\n if (props.autoSaveApiKey) {\n localStorage.setItem(`ai_360_api_key_${props.provider}`, apiKeyInput.value);\n }\n}\n\nfunction triggerFileInput() {\n fileInputRef.value?.click();\n}\n\nfunction handleFileSelect(event: Event) {\n const target = event.target as HTMLInputElement;\n const file = target.files?.[0];\n if (file) {\n processFile(file);\n }\n}\n\nfunction handleDrop(event: DragEvent) {\n isDragging.value = false;\n const file = event.dataTransfer?.files[0];\n if (file && file.type.startsWith('image/')) {\n processFile(file);\n }\n}\n\nfunction processFile(file: File) {\n const reader = new FileReader();\n reader.onload = (e) => {\n uploadedImage.value = e.target?.result as string;\n };\n reader.readAsDataURL(file);\n}\n\nfunction clearUpload() {\n uploadedImage.value = null;\n if (fileInputRef.value) {\n fileInputRef.value.value = '';\n }\n}\n\nasync function startGeneration() {\n if (!uploadedImage.value || !apiKeyInput.value) return;\n\n isGenerating.value = true;\n error.value = null;\n generatedFrames.value = [];\n emit('generation-start');\n\n try {\n const generator = new AI360Generator(\n {\n provider: props.provider,\n apiKey: apiKeyInput.value,\n frameCount: frameCount.value,\n backgroundColor: backgroundColor.value,\n quality: quality.value,\n useVisionAnalysis: true\n },\n (progressData) => {\n progress.value = progressData;\n }\n );\n\n const result = await generator.generate(uploadedImage.value);\n generatedFrames.value = result.frames;\n emit('generation-complete', result.frames);\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Generation failed';\n error.value = errorMessage;\n emit('generation-error', err instanceof Error ? err : new Error(errorMessage));\n } finally {\n isGenerating.value = false;\n }\n}\n\nfunction downloadFrames() {\n generatedFrames.value.forEach((frame, index) => {\n const link = document.createElement('a');\n link.href = frame;\n link.download = `360-frame-${String(index + 1).padStart(3, '0')}.png`;\n link.click();\n });\n}\n\nfunction reset() {\n generatedFrames.value = [];\n uploadedImage.value = null;\n error.value = null;\n progress.value = {\n currentFrame: 0,\n totalFrames: 0,\n percentage: 0,\n status: '',\n generatedFrames: []\n };\n}\n\nfunction emitFrames() {\n emit('frames-generated', generatedFrames.value);\n}\n\nfunction clearError() {\n error.value = null;\n}\n</script>\n\n<style scoped>\n.ai-360-generator {\n width: 100%;\n max-width: 800px;\n margin: 0 auto;\n padding: 20px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n}\n\n.ai-360-generator__api-key {\n background: #f8f9fa;\n padding: 20px;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.ai-360-generator__api-key label {\n display: block;\n margin-bottom: 8px;\n font-weight: 600;\n color: #333;\n}\n\n.ai-360-generator__input {\n width: 100%;\n padding: 10px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 14px;\n margin-bottom: 10px;\n}\n\n.ai-360-generator__dropzone {\n border: 2px dashed #ddd;\n border-radius: 8px;\n padding: 40px;\n text-align: center;\n cursor: pointer;\n transition: all 0.3s ease;\n background: #fafafa;\n}\n\n.ai-360-generator__dropzone:hover {\n border-color: #4CAF50;\n background: #f0f8f0;\n}\n\n.ai-360-generator__dropzone--dragging {\n border-color: #4CAF50;\n background: #e8f5e9;\n}\n\n.ai-360-generator__upload-icon {\n width: 48px;\n height: 48px;\n margin: 0 auto 16px;\n color: #666;\n}\n\n.ai-360-generator__dropzone-text {\n font-size: 16px;\n font-weight: 600;\n color: #333;\n margin: 0 0 8px;\n}\n\n.ai-360-generator__dropzone-hint {\n font-size: 14px;\n color: #666;\n margin: 0;\n}\n\n.ai-360-generator__preview {\n position: relative;\n max-width: 400px;\n margin: 0 auto;\n}\n\n.ai-360-generator__preview-image {\n width: 100%;\n border-radius: 8px;\n}\n\n.ai-360-generator__clear-button {\n position: absolute;\n top: 10px;\n right: 10px;\n background: rgba(0, 0, 0, 0.7);\n color: white;\n border: none;\n border-radius: 50%;\n width: 32px;\n height: 32px;\n cursor: pointer;\n font-size: 18px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.ai-360-generator__options {\n margin-top: 20px;\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 15px;\n}\n\n.ai-360-generator__option {\n display: flex;\n flex-direction: column;\n}\n\n.ai-360-generator__option label {\n font-weight: 600;\n margin-bottom: 5px;\n color: #333;\n font-size: 14px;\n}\n\n.ai-360-generator__select {\n padding: 8px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 14px;\n}\n\n.ai-360-generator__generate-button {\n grid-column: 1 / -1;\n padding: 12px 24px;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n border: none;\n border-radius: 6px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s;\n}\n\n.ai-360-generator__generate-button:hover {\n transform: translateY(-2px);\n}\n\n.ai-360-generator__progress {\n padding: 30px;\n text-align: center;\n}\n\n.ai-360-generator__progress-bar {\n width: 100%;\n height: 8px;\n background: #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 15px;\n}\n\n.ai-360-generator__progress-fill {\n height: 100%;\n background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);\n transition: width 0.3s ease;\n}\n\n.ai-360-generator__progress-text {\n font-size: 16px;\n font-weight: 600;\n color: #333;\n margin: 0 0 8px;\n}\n\n.ai-360-generator__progress-detail {\n font-size: 14px;\n color: #666;\n margin: 0;\n}\n\n.ai-360-generator__result {\n text-align: center;\n}\n\n.ai-360-generator__result-title {\n font-size: 24px;\n margin-bottom: 20px;\n color: #4CAF50;\n}\n\n.ai-360-generator__viewer {\n margin: 20px 0;\n border-radius: 8px;\n overflow: hidden;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n}\n\n.ai-360-generator__actions {\n display: flex;\n gap: 10px;\n justify-content: center;\n margin: 20px 0;\n flex-wrap: wrap;\n}\n\n.ai-360-generator__button {\n padding: 10px 20px;\n background: #4CAF50;\n color: white;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.ai-360-generator__button:hover {\n background: #45a049;\n transform: translateY(-1px);\n}\n\n.ai-360-generator__button--secondary {\n background: #757575;\n}\n\n.ai-360-generator__button--secondary:hover {\n background: #616161;\n}\n\n.ai-360-generator__button--primary {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n}\n\n.ai-360-generator__frame-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));\n gap: 10px;\n margin-top: 20px;\n}\n\n.ai-360-generator__frame-thumbnail {\n width: 100%;\n aspect-ratio: 1;\n object-fit: cover;\n border-radius: 4px;\n border: 2px solid #e0e0e0;\n transition: transform 0.2s;\n}\n\n.ai-360-generator__frame-thumbnail:hover {\n transform: scale(1.05);\n border-color: #4CAF50;\n}\n\n.ai-360-generator__error {\n background: #ffebee;\n color: #c62828;\n padding: 15px;\n border-radius: 6px;\n margin-top: 20px;\n text-align: center;\n}\n\n.ai-360-generator__error p {\n margin: 0 0 10px;\n font-weight: 600;\n}\n</style>\n\n","import type { App } from 'vue';\nimport Ai360Spin from './components/Ai360Spin.vue';\nimport Ai360Generator from './components/Ai360Generator.vue';\nimport { use360Spin } from './composables/use360Spin';\nimport { AI360Generator } from './utils/ai-generator';\nimport type {\n Spin360Config,\n SpinMode,\n SpinTrigger,\n SpinDirection,\n Spin360Events,\n AIProvider,\n BackgroundColor,\n AI360GeneratorConfig,\n AI360GenerationProgress,\n AI360GenerationResult\n} from './types';\n\n// Import styles\nimport './styles/360-spin.css';\n\n// Export components\nexport { Ai360Spin, Ai360Generator };\n\n// Export composables\nexport { use360Spin };\n\n// Export utilities\nexport { AI360Generator };\n\n// Export types\nexport type {\n Spin360Config,\n SpinMode,\n SpinTrigger,\n SpinDirection,\n Spin360Events,\n AIProvider,\n BackgroundColor,\n AI360GeneratorConfig,\n AI360GenerationProgress,\n AI360GenerationResult\n};\n\n// Vue plugin\nexport default {\n install(app: App) {\n app.component('Ai360Spin', Ai360Spin);\n app.component('Ai360Generator', Ai360Generator);\n }\n};\n\n"],"names":["use360Spin","props","emit","isAnimating","ref","isLoading","currentFrameIndex","animationFrameId","preloadedImages","isDragging","dragStartX","dragStartFrame","currentMode","computed","animatedImageUrl","currentFrameUrl","frames","index","frameArray","totalFrames","preloadImages","loadImage","loadPromises","url","images","error","timeout","resolve","reject","img","timeoutId","startAnimation","startFrameAnimation","stopAnimation","frameDelay","lastFrameTime","animate","now","handleDragStart","event","handleDragMove","currentX","deltaX","sensitivity","frameDelta","newFrame","handleDragEnd","_event","onUnmounted","__props","__emit","containerRef","containerStyle","showHint","hintText","handleMouseEnter","handleMouseLeave","handleClick","handleTouchStart","handleTouchMove","handleTouchEnd","handleStaticImageLoad","handleAnimatedImageLoad","handleImageError","_a","onMounted","AI360Generator","config","onProgress","__publicField","imageFile","startTime","imageBase64","productDescription","angleStep","i","angle","progress","frameUrl","generationTime","response","_b","frameIndex","_frameIndex","backgroundColor","prompt","imageUrl","base64Image","file","reader","blob","percentage","status","generatedFrames","apiKeyInput","uploadedImage","isGenerating","fileInputRef","frameCount","quality","hasApiKey","providerLabel","saveApiKey","triggerFileInput","handleFileSelect","processFile","handleDrop","e","clearUpload","startGeneration","result","progressData","err","errorMessage","downloadFrames","frame","link","reset","emitFrames","clearError","app","Ai360Spin","Ai360Generator"],"mappings":"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"}
|