@aivue/360-spin 1.0.1

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 ADDED
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.1] - 2024-12-06
9
+
10
+ ### Fixed
11
+ - Fixed demo to use real 360° product photography sequences instead of static image swaps
12
+ - Removed CORS crossOrigin setting that was causing image loading errors
13
+ - Updated demo with Scaleflex CDN 360° product images (car: 36 frames, Nike: 35 frames)
14
+ - Changed demo mode from "gif" to "frames" for proper frame sequence animation
15
+ - Added frame-rate configuration for smooth 24 FPS animation
16
+
17
+ ### Changed
18
+ - Demo now showcases true 360° rotation with professional product photography
19
+ - Updated demo products to Luxury Car and Nike Sneakers with real frame sequences
20
+ - Improved demo descriptions to clarify real 360° spin functionality
21
+
22
+ ## [1.0.0] - 2024-12-06
23
+
24
+ ### Added
25
+ - Initial release of @aivue/360-spin
26
+ - Core `Ai360Spin` component with static/animated image switching
27
+ - Support for GIF animations
28
+ - Support for frame sequence animations
29
+ - Hover, click, and auto-play triggers
30
+ - Mobile touch and drag-to-spin functionality
31
+ - Preloading of images for better performance
32
+ - Loading indicator with customizable text
33
+ - Configurable frame rate and animation direction
34
+ - Event emissions for animation lifecycle
35
+ - TypeScript support with full type definitions
36
+ - Comprehensive documentation and examples
37
+ - Pre-built CSS classes for common use cases
38
+ - Accessibility features (keyboard navigation, ARIA labels)
39
+ - Responsive design for mobile and desktop
40
+ - Vue 2 and Vue 3 compatibility
41
+
42
+ ### Features
43
+ - 🖼️ Static to animated image switching
44
+ - 🎬 GIF and frame sequence support
45
+ - 📱 Mobile drag-to-spin
46
+ - 🎯 Multiple trigger modes (hover, click, auto)
47
+ - ⚡ Image preloading
48
+ - 🎨 Customizable styling
49
+ - ♿ Accessible
50
+ - 📦 E-commerce ready
51
+
52
+ [1.0.0]: https://github.com/reachbrt/vueai/releases/tag/@aivue/360-spin@1.0.0
53
+
package/README.md ADDED
@@ -0,0 +1,345 @@
1
+ # @aivue/360-spin
2
+
3
+ > Interactive 360-degree product image spin component for Vue.js
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@aivue/360-spin.svg)](https://www.npmjs.com/package/@aivue/360-spin)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@aivue/360-spin.svg)](https://www.npmjs.com/package/@aivue/360-spin)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## ✨ Features
10
+
11
+ - 🖼️ **Static to Animated**: Show static product image by default, animate on hover/tap
12
+ - 🎬 **Multiple Modes**: Support for GIF animations or frame sequences
13
+ - 📱 **Mobile Optimized**: Touch and drag to spin on mobile devices
14
+ - 🎯 **Flexible Triggers**: Hover, click, or auto-play animations
15
+ - 🛍️ **E-commerce Ready**: Perfect for product cards, carousels, and search results
16
+ - ⚡ **Performance**: Preloading and optimized frame rendering
17
+ - 🎨 **Customizable**: Full control over styling and behavior
18
+ - ♿ **Accessible**: Keyboard navigation and screen reader support
19
+
20
+ ## 📦 Installation
21
+
22
+ ```bash
23
+ npm install @aivue/360-spin
24
+ ```
25
+
26
+ ## 🚀 Quick Start
27
+
28
+ ### Basic Usage (GIF Mode)
29
+
30
+ ```vue
31
+ <template>
32
+ <Ai360Spin
33
+ static-image="/images/product-static.jpg"
34
+ animated-image="/images/product-360.gif"
35
+ alt="Product 360 view"
36
+ width="400px"
37
+ height="400px"
38
+ />
39
+ </template>
40
+
41
+ <script setup>
42
+ import { Ai360Spin } from '@aivue/360-spin';
43
+ import '@aivue/360-spin/360-spin.css';
44
+ </script>
45
+ ```
46
+
47
+ ### Frame Sequence Mode
48
+
49
+ ```vue
50
+ <template>
51
+ <Ai360Spin
52
+ static-image="/images/product-static.jpg"
53
+ :animated-image="frameUrls"
54
+ mode="frames"
55
+ :frame-rate="30"
56
+ enable-drag-spin
57
+ alt="Product 360 view"
58
+ />
59
+ </template>
60
+
61
+ <script setup>
62
+ import { Ai360Spin } from '@aivue/360-spin';
63
+ import '@aivue/360-spin/360-spin.css';
64
+
65
+ const frameUrls = [
66
+ '/images/frame-001.jpg',
67
+ '/images/frame-002.jpg',
68
+ '/images/frame-003.jpg',
69
+ // ... more frames
70
+ ];
71
+ </script>
72
+ ```
73
+
74
+ ### Product Card Integration
75
+
76
+ ```vue
77
+ <template>
78
+ <div class="product-card">
79
+ <Ai360Spin
80
+ :static-image="product.image"
81
+ :animated-image="product.spin360"
82
+ container-class="ai-360-spin--card"
83
+ :alt="product.name"
84
+ trigger="hover"
85
+ />
86
+ <h3>{{ product.name }}</h3>
87
+ <p>{{ product.price }}</p>
88
+ </div>
89
+ </template>
90
+ ```
91
+
92
+ ## 📖 API Reference
93
+
94
+ ### Props
95
+
96
+ | Prop | Type | Default | Description |
97
+ |------|------|---------|-------------|
98
+ | `staticImage` | `string` | **required** | URL of the static product image |
99
+ | `animatedImage` | `string \| string[]` | **required** | GIF URL or array of frame URLs |
100
+ | `mode` | `'gif' \| 'frames' \| 'auto'` | `'auto'` | Animation mode |
101
+ | `trigger` | `'hover' \| 'click' \| 'auto'` | `'hover'` | How to start animation |
102
+ | `width` | `string \| number` | `'100%'` | Container width |
103
+ | `height` | `string \| number` | `'auto'` | Container height |
104
+ | `alt` | `string` | `'Product 360 view'` | Alt text for images |
105
+ | `frameRate` | `number` | `30` | FPS for frame sequence |
106
+ | `loop` | `boolean` | `true` | Whether to loop animation |
107
+ | `direction` | `'clockwise' \| 'counterclockwise'` | `'clockwise'` | Spin direction |
108
+ | `preload` | `boolean` | `true` | Preload images on mount |
109
+ | `showLoading` | `boolean` | `true` | Show loading indicator |
110
+ | `loadingText` | `string` | `'Loading...'` | Loading text |
111
+ | `enableDragSpin` | `boolean` | `true` | Enable drag to spin on mobile |
112
+ | `dragSensitivity` | `number` | `10` | Pixels per frame for drag |
113
+ | `containerClass` | `string` | `''` | CSS class for container |
114
+ | `imageClass` | `string` | `''` | CSS class for images |
115
+
116
+ ### Events
117
+
118
+ | Event | Payload | Description |
119
+ |-------|---------|-------------|
120
+ | `animation-start` | `void` | Fired when animation starts |
121
+ | `animation-end` | `void` | Fired when animation ends |
122
+ | `loaded` | `void` | Fired when images are loaded |
123
+ | `error` | `Error` | Fired on loading error |
124
+ | `frame-change` | `number` | Fired when frame changes (frames mode) |
125
+
126
+ ## 🎨 Styling
127
+
128
+ The component comes with default styles, but you can customize them:
129
+
130
+ ```css
131
+ /* Custom container styles */
132
+ .ai-360-spin {
133
+ border-radius: 12px;
134
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
135
+ }
136
+
137
+ /* Custom loading spinner */
138
+ .ai-360-spin__spinner {
139
+ border-top-color: #your-brand-color;
140
+ }
141
+
142
+ /* Custom hint badge */
143
+ .ai-360-spin__hint {
144
+ background: rgba(your-color, 0.9);
145
+ }
146
+ ```
147
+
148
+ ### Pre-built Classes
149
+
150
+ - `.ai-360-spin--card` - Optimized for product cards (1:1 aspect ratio)
151
+ - `.ai-360-spin--grid` - Optimized for grid layouts (4:3 aspect ratio)
152
+ - `.ai-360-spin--carousel` - Optimized for carousels (full width/height)
153
+
154
+ ## 💡 Use Cases
155
+
156
+ ### E-commerce Product Listings
157
+
158
+ ```vue
159
+ <div class="product-grid">
160
+ <div v-for="product in products" :key="product.id" class="product-item">
161
+ <Ai360Spin
162
+ :static-image="product.thumbnail"
163
+ :animated-image="product.spin360Gif"
164
+ container-class="ai-360-spin--grid"
165
+ :alt="product.name"
166
+ />
167
+ </div>
168
+ </div>
169
+ ```
170
+
171
+ ### Product Detail Page
172
+
173
+ ```vue
174
+ <Ai360Spin
175
+ :static-image="product.mainImage"
176
+ :animated-image="product.frames"
177
+ mode="frames"
178
+ :frame-rate="60"
179
+ enable-drag-spin
180
+ width="600px"
181
+ height="600px"
182
+ trigger="click"
183
+ />
184
+ ```
185
+
186
+ ### Carousel Integration
187
+
188
+ ```vue
189
+ <div class="carousel">
190
+ <div v-for="item in carouselItems" :key="item.id" class="carousel-slide">
191
+ <Ai360Spin
192
+ :static-image="item.image"
193
+ :animated-image="item.spin"
194
+ container-class="ai-360-spin--carousel"
195
+ trigger="hover"
196
+ />
197
+ </div>
198
+ </div>
199
+ ```
200
+
201
+ ## 🔧 Advanced Usage
202
+
203
+ ### Using the Composable
204
+
205
+ ```vue
206
+ <script setup>
207
+ import { use360Spin } from '@aivue/360-spin';
208
+
209
+ const config = {
210
+ staticImage: '/product.jpg',
211
+ animatedImage: frameUrls,
212
+ mode: 'frames',
213
+ frameRate: 30
214
+ };
215
+
216
+ const {
217
+ isAnimating,
218
+ isLoading,
219
+ currentFrameIndex,
220
+ startAnimation,
221
+ stopAnimation,
222
+ preloadImages
223
+ } = use360Spin(config, emit);
224
+
225
+ // Manually control animation
226
+ function handleCustomTrigger() {
227
+ if (isAnimating.value) {
228
+ stopAnimation();
229
+ } else {
230
+ startAnimation();
231
+ }
232
+ }
233
+ </script>
234
+ ```
235
+
236
+ ### Programmatic Control
237
+
238
+ ```vue
239
+ <template>
240
+ <div>
241
+ <Ai360Spin
242
+ ref="spinRef"
243
+ :static-image="staticImg"
244
+ :animated-image="frames"
245
+ trigger="click"
246
+ @animation-start="onStart"
247
+ @animation-end="onEnd"
248
+ @frame-change="onFrameChange"
249
+ />
250
+ <button @click="toggleSpin">Toggle Spin</button>
251
+ </div>
252
+ </template>
253
+
254
+ <script setup>
255
+ import { ref } from 'vue';
256
+ import { Ai360Spin } from '@aivue/360-spin';
257
+
258
+ const spinRef = ref(null);
259
+
260
+ function onStart() {
261
+ console.log('Animation started');
262
+ }
263
+
264
+ function onEnd() {
265
+ console.log('Animation ended');
266
+ }
267
+
268
+ function onFrameChange(frameIndex) {
269
+ console.log('Current frame:', frameIndex);
270
+ }
271
+ </script>
272
+ ```
273
+
274
+ ## 📱 Mobile Optimization
275
+
276
+ The component automatically optimizes for mobile devices:
277
+
278
+ - **Touch Events**: Drag to spin through frames
279
+ - **Performance**: Optimized rendering for mobile browsers
280
+ - **Responsive**: Adapts to different screen sizes
281
+ - **Touch Feedback**: Visual feedback for touch interactions
282
+
283
+ ## 🎯 Best Practices
284
+
285
+ ### Frame Sequence Tips
286
+
287
+ 1. **Frame Count**: Use 24-36 frames for smooth rotation
288
+ 2. **Image Size**: Optimize images (WebP format recommended)
289
+ 3. **Naming**: Use sequential naming (frame-001.jpg, frame-002.jpg, etc.)
290
+ 4. **Preloading**: Enable preloading for better UX
291
+
292
+ ### Performance Optimization
293
+
294
+ ```vue
295
+ <Ai360Spin
296
+ :static-image="product.thumbnail"
297
+ :animated-image="product.frames"
298
+ :preload="true"
299
+ :frame-rate="24"
300
+ loading-image="/placeholder.jpg"
301
+ />
302
+ ```
303
+
304
+ ### Accessibility
305
+
306
+ ```vue
307
+ <Ai360Spin
308
+ :static-image="product.image"
309
+ :animated-image="product.spin"
310
+ :alt="`360-degree view of ${product.name}`"
311
+ role="img"
312
+ aria-label="Interactive product view"
313
+ />
314
+ ```
315
+
316
+ ## 🌐 Browser Support
317
+
318
+ - Chrome/Edge (latest)
319
+ - Firefox (latest)
320
+ - Safari (latest)
321
+ - Mobile browsers (iOS Safari, Chrome Mobile)
322
+
323
+ ## 📄 License
324
+
325
+ MIT © [reachbrt](https://github.com/reachbrt)
326
+
327
+ ## 🤝 Contributing
328
+
329
+ Contributions are welcome! Please feel free to submit a Pull Request.
330
+
331
+ ## 🔗 Links
332
+
333
+ - [GitHub Repository](https://github.com/reachbrt/vueai)
334
+ - [Demo](https://aivue.netlify.app)
335
+ - [Report Issues](https://github.com/reachbrt/vueai/issues)
336
+
337
+ ## 📝 Changelog
338
+
339
+ See [CHANGELOG.md](./CHANGELOG.md) for details.
340
+
341
+ ---
342
+
343
+ Made with ❤️ by [reachbrt](https://github.com/reachbrt)
344
+
345
+
@@ -0,0 +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}
@@ -0,0 +1,32 @@
1
+ import { Spin360Config } from '../types';
2
+ declare const _default: import('vue').DefineComponent<Spin360Config, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
3
+ loaded: () => any;
4
+ error: (error: Error) => any;
5
+ "animation-start": () => any;
6
+ "animation-end": () => any;
7
+ "frame-change": (frame: number) => any;
8
+ }, string, import('vue').PublicProps, Readonly<Spin360Config> & Readonly<{
9
+ onLoaded?: (() => any) | undefined;
10
+ onError?: ((error: Error) => any) | undefined;
11
+ "onAnimation-start"?: (() => any) | undefined;
12
+ "onAnimation-end"?: (() => any) | undefined;
13
+ "onFrame-change"?: ((frame: number) => any) | undefined;
14
+ }>, {
15
+ mode: import('..').SpinMode;
16
+ trigger: import('..').SpinTrigger;
17
+ width: string | number;
18
+ height: string | number;
19
+ alt: string;
20
+ frameRate: number;
21
+ loop: boolean;
22
+ reverseOnSecondHover: boolean;
23
+ direction: import('..').SpinDirection;
24
+ preload: boolean;
25
+ showLoading: boolean;
26
+ loadingText: string;
27
+ enableDragSpin: boolean;
28
+ dragSensitivity: number;
29
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
30
+ containerRef: HTMLDivElement;
31
+ }, HTMLDivElement>;
32
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { Spin360Config, SpinMode } from '../types';
2
+ export declare function use360Spin(props: Spin360Config, emit: any): {
3
+ isAnimating: import('vue').Ref<boolean, boolean>;
4
+ isLoading: import('vue').Ref<boolean, boolean>;
5
+ currentFrameIndex: import('vue').Ref<number, number>;
6
+ currentMode: import('vue').ComputedRef<SpinMode>;
7
+ animatedImageUrl: import('vue').ComputedRef<string>;
8
+ currentFrameUrl: import('vue').ComputedRef<string>;
9
+ totalFrames: import('vue').ComputedRef<number>;
10
+ startAnimation: () => void;
11
+ stopAnimation: () => void;
12
+ preloadImages: () => Promise<void>;
13
+ handleDragStart: (event: TouchEvent | MouseEvent) => void;
14
+ handleDragMove: (event: TouchEvent | MouseEvent) => void;
15
+ handleDragEnd: (_event: TouchEvent | MouseEvent) => void;
16
+ };
@@ -0,0 +1,11 @@
1
+ import { App } from 'vue';
2
+ import { default as Ai360Spin } from './components/Ai360Spin.vue';
3
+ import { use360Spin } from './composables/use360Spin';
4
+ import { Spin360Config, SpinMode, SpinTrigger, SpinDirection, Spin360Events } from './types';
5
+ export { Ai360Spin };
6
+ export { use360Spin };
7
+ export type { Spin360Config, SpinMode, SpinTrigger, SpinDirection, Spin360Events };
8
+ declare const _default: {
9
+ install(app: App): void;
10
+ };
11
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");function F(i,d){const n=e.ref(!1),c=e.ref(!0),o=e.ref(0),r=e.ref(null),l=e.ref([]),v=e.ref(!1),_=e.ref(0),A=e.ref(0),u=e.computed(()=>i.mode&&i.mode!=="auto"?i.mode:Array.isArray(i.animatedImage)?"frames":"gif"),y=e.computed(()=>u.value==="gif"&&typeof i.animatedImage=="string"?i.animatedImage:""),k=e.computed(()=>{if(u.value==="frames"&&Array.isArray(i.animatedImage)){const t=i.animatedImage,s=o.value%t.length;return t[s]}return""}),I=e.computed(()=>Array.isArray(i.animatedImage)?i.animatedImage:[]),m=e.computed(()=>I.value.length);async function D(){c.value=!0;try{if(u.value==="gif")await S(y.value);else if(u.value==="frames"){const t=I.value.map(f=>S(f)),s=await Promise.all(t);l.value=s}await S(i.staticImage),c.value=!1,d("loaded")}catch(t){c.value=!1,d("error",t)}}function S(t,s=5e3){return new Promise((f,h)=>{const g=new Image,a=setTimeout(()=>{h(new Error(`Image load timeout: ${t}`))},s);g.onload=()=>{clearTimeout(a),f(g)},g.onerror=()=>{clearTimeout(a),h(new Error(`Failed to load image: ${t}`))},g.src=t})}function C(){n.value||c.value||(n.value=!0,d("animation-start"),u.value==="frames"&&M())}function w(){n.value&&(n.value=!1,d("animation-end"),r.value!==null&&(cancelAnimationFrame(r.value),r.value=null),u.value==="frames"&&(o.value=0))}function M(){if(m.value===0)return;const t=1e3/(i.frameRate||30);let s=Date.now();function f(){const h=Date.now();if(h-s>=t&&(i.direction==="clockwise"?o.value=(o.value+1)%m.value:o.value=o.value===0?m.value-1:o.value-1,d("frame-change",o.value),s=h,!i.loop&&o.value===0)){w();return}n.value&&(r.value=requestAnimationFrame(f))}r.value=requestAnimationFrame(f)}function B(t){u.value!=="frames"||m.value===0||(v.value=!0,A.value=o.value,t instanceof TouchEvent?_.value=t.touches[0].clientX:_.value=t.clientX,n.value&&w())}function E(t){if(!v.value||u.value!=="frames")return;t.preventDefault();let s;t instanceof TouchEvent?s=t.touches[0].clientX:s=t.clientX;const f=s-_.value,h=i.dragSensitivity||10,g=Math.floor(f/h);let a=A.value+g;for(;a<0;)a+=m.value;for(;a>=m.value;)a-=m.value;a!==o.value&&(o.value=a,d("frame-change",o.value))}function T(t){v.value=!1}return e.onUnmounted(()=>{r.value!==null&&cancelAnimationFrame(r.value)}),{isAnimating:n,isLoading:c,currentFrameIndex:o,currentMode:u,animatedImageUrl:y,currentFrameUrl:k,totalFrames:m,startAnimation:C,stopAnimation:w,preloadImages:D,handleDragStart:B,handleDragMove:E,handleDragEnd:T}}const N={key:0,class:"ai-360-spin__loading"},V={class:"ai-360-spin__loading-text"},b=["src","alt"],P=["src","alt"],X=["src","alt"],z={key:3,class:"ai-360-spin__hint"},U=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(i,{emit:d}){const n=i,c=d,o=e.ref(null),{isAnimating:r,isLoading:l,currentMode:v,animatedImageUrl:_,currentFrameUrl:A,startAnimation:u,stopAnimation:y,preloadImages:k,handleDragStart:I,handleDragMove:m,handleDragEnd:D}=F(n,c),S=e.computed(()=>({width:typeof n.width=="number"?`${n.width}px`:n.width,height:typeof n.height=="number"?`${n.height}px`:n.height})),C=e.computed(()=>n.trigger==="hover"&&!r.value),w=e.computed(()=>"Hover to spin");function M(){n.trigger==="hover"&&u()}function B(){n.trigger==="hover"&&y()}function E(){n.trigger==="click"&&(r.value?y():u())}function T(a){n.enableDragSpin&&v.value==="frames"?I(a):n.trigger==="click"&&E()}function t(a){n.enableDragSpin&&v.value==="frames"&&m(a)}function s(a){n.enableDragSpin&&v.value==="frames"&&D(a)}function f(){console.log("[Ai360Spin] Static image loaded, clearing loading state"),l.value&&(l.value=!1),console.log("[Ai360Spin] isLoading after static load:",l.value)}function h(){c("loaded")}function g(a){var p;console.error("Image failed to load:",(p=a.target)==null?void 0:p.src),l.value&&(l.value=!1),c("error",new Error("Failed to load image"))}return e.onMounted(async()=>{if(console.log("[Ai360Spin] onMounted - preload:",n.preload,"isLoading:",l.value),n.preload)try{await k()}catch(a){console.error("[Ai360Spin] Preload failed:",a),l.value=!1}else console.log("[Ai360Spin] Preload disabled, clearing loading state"),l.value=!1,console.log("[Ai360Spin] isLoading after clear:",l.value);n.trigger==="auto"&&u()}),(a,p)=>(e.openBlock(),e.createElementBlock("div",{ref_key:"containerRef",ref:o,class:e.normalizeClass(["ai-360-spin",a.containerClass,{"ai-360-spin--animating":e.unref(r),"ai-360-spin--loading":e.unref(l)}]),style:e.normalizeStyle(S.value),onMouseenter:M,onMouseleave:B,onClick:E,onTouchstart:T,onTouchmove:t,onTouchend:s},[e.unref(l)&&a.showLoading?(e.openBlock(),e.createElementBlock("div",N,[p[0]||(p[0]=e.createElementVNode("div",{class:"ai-360-spin__spinner"},null,-1)),e.createElementVNode("p",V,e.toDisplayString(a.loadingText),1)])):e.createCommentVNode("",!0),e.withDirectives(e.createElementVNode("img",{src:a.staticImage,alt:a.alt,class:e.normalizeClass(["ai-360-spin__image","ai-360-spin__image--static",a.imageClass]),onLoad:f,onError:g},null,42,b),[[e.vShow,!e.unref(r)&&!e.unref(l)]]),e.unref(v)==="gif"&&!e.unref(l)?e.withDirectives((e.openBlock(),e.createElementBlock("img",{key:1,src:e.unref(_),alt:a.alt,class:e.normalizeClass(["ai-360-spin__image","ai-360-spin__image--animated",a.imageClass]),onLoad:h,onError:g},null,42,P)),[[e.vShow,e.unref(r)]]):e.createCommentVNode("",!0),e.unref(v)==="frames"&&!e.unref(l)?e.withDirectives((e.openBlock(),e.createElementBlock("img",{key:2,src:e.unref(A),alt:a.alt,class:e.normalizeClass(["ai-360-spin__image","ai-360-spin__image--frame",a.imageClass])},null,10,X)),[[e.vShow,e.unref(r)]]):e.createCommentVNode("",!0),!e.unref(r)&&!e.unref(l)&&C.value?(e.openBlock(),e.createElementBlock("div",z,[p[1]||(p[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(w.value),1)])):e.createCommentVNode("",!0)],38))}}),R=(i,d)=>{const n=i.__vccOpts||i;for(const[c,o]of d)n[c]=o;return n},L=R(U,[["__scopeId","data-v-915b2a52"]]),$={install(i){i.component("Ai360Spin",L)}};exports.Ai360Spin=L;exports.default=$;exports.use360Spin=F;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,249 @@
1
+ import { ref as p, computed as y, onUnmounted as H, defineComponent as q, onMounted as N, createElementBlock as k, openBlock as F, normalizeStyle as O, normalizeClass as L, unref as r, createCommentVNode as C, withDirectives as $, createElementVNode as _, toDisplayString as z, vShow as x } from "vue";
2
+ function V(t, d) {
3
+ const a = p(!1), c = p(!0), i = p(0), o = p(null), l = p([]), v = p(!1), A = p(0), M = p(0), u = y(() => t.mode && t.mode !== "auto" ? t.mode : Array.isArray(t.animatedImage) ? "frames" : "gif"), I = y(() => u.value === "gif" && typeof t.animatedImage == "string" ? t.animatedImage : ""), b = y(() => {
4
+ if (u.value === "frames" && Array.isArray(t.animatedImage)) {
5
+ const n = t.animatedImage, s = i.value % n.length;
6
+ return n[s];
7
+ }
8
+ return "";
9
+ }), T = y(() => Array.isArray(t.animatedImage) ? t.animatedImage : []), m = y(() => T.value.length);
10
+ async function B() {
11
+ c.value = !0;
12
+ try {
13
+ if (u.value === "gif")
14
+ await S(I.value);
15
+ else if (u.value === "frames") {
16
+ const n = T.value.map((f) => S(f)), s = await Promise.all(n);
17
+ l.value = s;
18
+ }
19
+ await S(t.staticImage), c.value = !1, d("loaded");
20
+ } catch (n) {
21
+ c.value = !1, d("error", n);
22
+ }
23
+ }
24
+ function S(n, s = 5e3) {
25
+ return new Promise((f, h) => {
26
+ const g = new Image(), e = setTimeout(() => {
27
+ h(new Error(`Image load timeout: ${n}`));
28
+ }, s);
29
+ g.onload = () => {
30
+ clearTimeout(e), f(g);
31
+ }, g.onerror = () => {
32
+ clearTimeout(e), h(new Error(`Failed to load image: ${n}`));
33
+ }, g.src = n;
34
+ });
35
+ }
36
+ function X() {
37
+ a.value || c.value || (a.value = !0, d("animation-start"), u.value === "frames" && P());
38
+ }
39
+ function D() {
40
+ a.value && (a.value = !1, d("animation-end"), o.value !== null && (cancelAnimationFrame(o.value), o.value = null), u.value === "frames" && (i.value = 0));
41
+ }
42
+ function P() {
43
+ if (m.value === 0) return;
44
+ const n = 1e3 / (t.frameRate || 30);
45
+ let s = Date.now();
46
+ function f() {
47
+ const h = Date.now();
48
+ if (h - s >= n && (t.direction === "clockwise" ? i.value = (i.value + 1) % m.value : i.value = i.value === 0 ? m.value - 1 : i.value - 1, d("frame-change", i.value), s = h, !t.loop && i.value === 0)) {
49
+ D();
50
+ return;
51
+ }
52
+ a.value && (o.value = requestAnimationFrame(f));
53
+ }
54
+ o.value = requestAnimationFrame(f);
55
+ }
56
+ function U(n) {
57
+ u.value !== "frames" || m.value === 0 || (v.value = !0, M.value = i.value, n instanceof TouchEvent ? A.value = n.touches[0].clientX : A.value = n.clientX, a.value && D());
58
+ }
59
+ function E(n) {
60
+ if (!v.value || u.value !== "frames") return;
61
+ n.preventDefault();
62
+ let s;
63
+ n instanceof TouchEvent ? s = n.touches[0].clientX : s = n.clientX;
64
+ const f = s - A.value, h = t.dragSensitivity || 10, g = Math.floor(f / h);
65
+ let e = M.value + g;
66
+ for (; e < 0; ) e += m.value;
67
+ for (; e >= m.value; ) e -= m.value;
68
+ e !== i.value && (i.value = e, d("frame-change", i.value));
69
+ }
70
+ function R(n) {
71
+ v.value = !1;
72
+ }
73
+ return H(() => {
74
+ o.value !== null && cancelAnimationFrame(o.value);
75
+ }), {
76
+ isAnimating: a,
77
+ isLoading: c,
78
+ currentFrameIndex: i,
79
+ currentMode: u,
80
+ animatedImageUrl: I,
81
+ currentFrameUrl: b,
82
+ totalFrames: m,
83
+ startAnimation: X,
84
+ stopAnimation: D,
85
+ preloadImages: B,
86
+ handleDragStart: U,
87
+ handleDragMove: E,
88
+ handleDragEnd: R
89
+ };
90
+ }
91
+ const G = {
92
+ key: 0,
93
+ class: "ai-360-spin__loading"
94
+ }, J = { class: "ai-360-spin__loading-text" }, K = ["src", "alt"], Q = ["src", "alt"], W = ["src", "alt"], Y = {
95
+ key: 3,
96
+ class: "ai-360-spin__hint"
97
+ }, Z = /* @__PURE__ */ q({
98
+ __name: "Ai360Spin",
99
+ props: {
100
+ staticImage: {},
101
+ animatedImage: {},
102
+ mode: { default: "auto" },
103
+ trigger: { default: "hover" },
104
+ width: { default: "100%" },
105
+ height: { default: "auto" },
106
+ alt: { default: "Product 360 view" },
107
+ frameRate: { default: 30 },
108
+ loop: { type: Boolean, default: !0 },
109
+ reverseOnSecondHover: { type: Boolean, default: !1 },
110
+ direction: { default: "clockwise" },
111
+ preload: { type: Boolean, default: !0 },
112
+ loadingImage: {},
113
+ containerClass: {},
114
+ imageClass: {},
115
+ showLoading: { type: Boolean, default: !0 },
116
+ loadingText: { default: "Loading..." },
117
+ enableDragSpin: { type: Boolean, default: !0 },
118
+ dragSensitivity: { default: 10 }
119
+ },
120
+ emits: ["animation-start", "animation-end", "loaded", "error", "frame-change"],
121
+ setup(t, { emit: d }) {
122
+ const a = t, c = d, i = p(null), {
123
+ isAnimating: o,
124
+ isLoading: l,
125
+ currentMode: v,
126
+ animatedImageUrl: A,
127
+ currentFrameUrl: M,
128
+ startAnimation: u,
129
+ stopAnimation: I,
130
+ preloadImages: b,
131
+ handleDragStart: T,
132
+ handleDragMove: m,
133
+ handleDragEnd: B
134
+ } = V(a, c), S = y(() => ({
135
+ width: typeof a.width == "number" ? `${a.width}px` : a.width,
136
+ height: typeof a.height == "number" ? `${a.height}px` : a.height
137
+ })), X = y(() => a.trigger === "hover" && !o.value), D = y(() => "Hover to spin");
138
+ function P() {
139
+ a.trigger === "hover" && u();
140
+ }
141
+ function U() {
142
+ a.trigger === "hover" && I();
143
+ }
144
+ function E() {
145
+ a.trigger === "click" && (o.value ? I() : u());
146
+ }
147
+ function R(e) {
148
+ a.enableDragSpin && v.value === "frames" ? T(e) : a.trigger === "click" && E();
149
+ }
150
+ function n(e) {
151
+ a.enableDragSpin && v.value === "frames" && m(e);
152
+ }
153
+ function s(e) {
154
+ a.enableDragSpin && v.value === "frames" && B(e);
155
+ }
156
+ function f() {
157
+ console.log("[Ai360Spin] Static image loaded, clearing loading state"), l.value && (l.value = !1), console.log("[Ai360Spin] isLoading after static load:", l.value);
158
+ }
159
+ function h() {
160
+ c("loaded");
161
+ }
162
+ function g(e) {
163
+ var w;
164
+ console.error("Image failed to load:", (w = e.target) == null ? void 0 : w.src), l.value && (l.value = !1), c("error", new Error("Failed to load image"));
165
+ }
166
+ return N(async () => {
167
+ if (console.log("[Ai360Spin] onMounted - preload:", a.preload, "isLoading:", l.value), a.preload)
168
+ try {
169
+ await b();
170
+ } catch (e) {
171
+ console.error("[Ai360Spin] Preload failed:", e), l.value = !1;
172
+ }
173
+ else
174
+ console.log("[Ai360Spin] Preload disabled, clearing loading state"), l.value = !1, console.log("[Ai360Spin] isLoading after clear:", l.value);
175
+ a.trigger === "auto" && u();
176
+ }), (e, w) => (F(), k("div", {
177
+ ref_key: "containerRef",
178
+ ref: i,
179
+ class: L(["ai-360-spin", e.containerClass, { "ai-360-spin--animating": r(o), "ai-360-spin--loading": r(l) }]),
180
+ style: O(S.value),
181
+ onMouseenter: P,
182
+ onMouseleave: U,
183
+ onClick: E,
184
+ onTouchstart: R,
185
+ onTouchmove: n,
186
+ onTouchend: s
187
+ }, [
188
+ r(l) && e.showLoading ? (F(), k("div", G, [
189
+ w[0] || (w[0] = _("div", { class: "ai-360-spin__spinner" }, null, -1)),
190
+ _("p", J, z(e.loadingText), 1)
191
+ ])) : C("", !0),
192
+ $(_("img", {
193
+ src: e.staticImage,
194
+ alt: e.alt,
195
+ class: L(["ai-360-spin__image", "ai-360-spin__image--static", e.imageClass]),
196
+ onLoad: f,
197
+ onError: g
198
+ }, null, 42, K), [
199
+ [x, !r(o) && !r(l)]
200
+ ]),
201
+ r(v) === "gif" && !r(l) ? $((F(), k("img", {
202
+ key: 1,
203
+ src: r(A),
204
+ alt: e.alt,
205
+ class: L(["ai-360-spin__image", "ai-360-spin__image--animated", e.imageClass]),
206
+ onLoad: h,
207
+ onError: g
208
+ }, null, 42, Q)), [
209
+ [x, r(o)]
210
+ ]) : C("", !0),
211
+ r(v) === "frames" && !r(l) ? $((F(), k("img", {
212
+ key: 2,
213
+ src: r(M),
214
+ alt: e.alt,
215
+ class: L(["ai-360-spin__image", "ai-360-spin__image--frame", e.imageClass])
216
+ }, null, 10, W)), [
217
+ [x, r(o)]
218
+ ]) : C("", !0),
219
+ !r(o) && !r(l) && X.value ? (F(), k("div", Y, [
220
+ w[1] || (w[1] = _("svg", {
221
+ class: "ai-360-spin__hint-icon",
222
+ viewBox: "0 0 24 24",
223
+ fill: "none",
224
+ stroke: "currentColor"
225
+ }, [
226
+ _("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
227
+ _("path", { d: "M2 17l10 5 10-5" }),
228
+ _("path", { d: "M2 12l10 5 10-5" })
229
+ ], -1)),
230
+ _("span", null, z(D.value), 1)
231
+ ])) : C("", !0)
232
+ ], 38));
233
+ }
234
+ }), j = (t, d) => {
235
+ const a = t.__vccOpts || t;
236
+ for (const [c, i] of d)
237
+ a[c] = i;
238
+ return a;
239
+ }, ee = /* @__PURE__ */ j(Z, [["__scopeId", "data-v-915b2a52"]]), ne = {
240
+ install(t) {
241
+ t.component("Ai360Spin", ee);
242
+ }
243
+ };
244
+ export {
245
+ ee as Ai360Spin,
246
+ ne as default,
247
+ V as use360Spin
248
+ };
249
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","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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"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Animation mode for 360-degree spin
3
+ */
4
+ export type SpinMode = 'gif' | 'frames' | 'auto';
5
+ /**
6
+ * Trigger type for starting the spin animation
7
+ */
8
+ export type SpinTrigger = 'hover' | 'click' | 'auto';
9
+ /**
10
+ * Direction of the spin animation
11
+ */
12
+ export type SpinDirection = 'clockwise' | 'counterclockwise';
13
+ /**
14
+ * Configuration options for 360-degree spin
15
+ */
16
+ export interface Spin360Config {
17
+ /**
18
+ * Static image URL (shown by default)
19
+ */
20
+ staticImage: string;
21
+ /**
22
+ * Animated image URL (GIF) or array of frame URLs
23
+ */
24
+ animatedImage: string | string[];
25
+ /**
26
+ * Animation mode
27
+ * @default 'auto' - automatically detects based on animatedImage type
28
+ */
29
+ mode?: SpinMode;
30
+ /**
31
+ * Trigger for starting animation
32
+ * @default 'hover'
33
+ */
34
+ trigger?: SpinTrigger;
35
+ /**
36
+ * Width of the image container
37
+ * @default '100%'
38
+ */
39
+ width?: string | number;
40
+ /**
41
+ * Height of the image container
42
+ * @default 'auto'
43
+ */
44
+ height?: string | number;
45
+ /**
46
+ * Alt text for the image
47
+ */
48
+ alt?: string;
49
+ /**
50
+ * Frame rate for frame sequence animation (frames per second)
51
+ * @default 30
52
+ */
53
+ frameRate?: number;
54
+ /**
55
+ * Whether to loop the animation
56
+ * @default true
57
+ */
58
+ loop?: boolean;
59
+ /**
60
+ * Whether to reverse the animation on second hover
61
+ * @default false
62
+ */
63
+ reverseOnSecondHover?: boolean;
64
+ /**
65
+ * Spin direction
66
+ * @default 'clockwise'
67
+ */
68
+ direction?: SpinDirection;
69
+ /**
70
+ * Whether to preload animated images
71
+ * @default true
72
+ */
73
+ preload?: boolean;
74
+ /**
75
+ * Loading placeholder image
76
+ */
77
+ loadingImage?: string;
78
+ /**
79
+ * CSS class for the container
80
+ */
81
+ containerClass?: string;
82
+ /**
83
+ * CSS class for the image
84
+ */
85
+ imageClass?: string;
86
+ /**
87
+ * Whether to show loading indicator
88
+ * @default true
89
+ */
90
+ showLoading?: boolean;
91
+ /**
92
+ * Custom loading text
93
+ * @default 'Loading...'
94
+ */
95
+ loadingText?: string;
96
+ /**
97
+ * Whether to enable touch/drag to spin on mobile
98
+ * @default true
99
+ */
100
+ enableDragSpin?: boolean;
101
+ /**
102
+ * Sensitivity for drag spin (pixels per frame)
103
+ * @default 10
104
+ */
105
+ dragSensitivity?: number;
106
+ }
107
+ /**
108
+ * Events emitted by the 360-spin component
109
+ */
110
+ export interface Spin360Events {
111
+ /**
112
+ * Emitted when animation starts
113
+ */
114
+ 'animation-start': void;
115
+ /**
116
+ * Emitted when animation ends
117
+ */
118
+ 'animation-end': void;
119
+ /**
120
+ * Emitted when images are loaded
121
+ */
122
+ 'loaded': void;
123
+ /**
124
+ * Emitted when loading fails
125
+ */
126
+ 'error': Error;
127
+ /**
128
+ * Emitted when current frame changes (for frame sequence mode)
129
+ */
130
+ 'frame-change': number;
131
+ }
@@ -0,0 +1,6 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue';
3
+ const component: DefineComponent<{}, {}, any>;
4
+ export default component;
5
+ }
6
+
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@aivue/360-spin",
3
+ "version": "1.0.1",
4
+ "description": "Interactive 360-degree product image spin component for Vue.js",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./360-spin.css": "./dist/360-spin.css",
15
+ "./dist/360-spin.css": "./dist/360-spin.css"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "npm run clean && vite build",
24
+ "dev": "vite build --watch",
25
+ "clean": "rm -rf dist",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "vue",
30
+ "360",
31
+ "spin",
32
+ "product",
33
+ "image",
34
+ "rotation",
35
+ "3d",
36
+ "viewer",
37
+ "ecommerce",
38
+ "product-view",
39
+ "interactive",
40
+ "ui",
41
+ "components"
42
+ ],
43
+ "author": "reachbrt",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/reachbrt/vueai.git",
48
+ "directory": "packages/360-spin"
49
+ },
50
+ "homepage": "https://github.com/reachbrt/vueai#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/reachbrt/vueai/issues"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "peerDependencies": {
58
+ "vue": "^2.6.0 || ^3.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^20.16.0",
62
+ "@vitejs/plugin-vue": "^5.0.0",
63
+ "@vue/compiler-sfc": "^3.5.13",
64
+ "typescript": "^5.3.0",
65
+ "vite": "^6.3.5",
66
+ "vite-plugin-dts": "^4.5.3",
67
+ "vue": "^3.5.0",
68
+ "vue-tsc": "^2.2.10"
69
+ }
70
+ }
71
+