spinit-js 1.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/LICENSE +21 -0
- package/README.md +69 -0
- package/package.json +30 -0
- package/src/EventManager.js +40 -0
- package/src/Loader.js +55 -0
- package/src/Physics.js +43 -0
- package/src/Renderer.js +139 -0
- package/src/SpinIT.js +318 -0
- package/src/index.js +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BillGR17
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# SpinIt.js
|
|
2
|
+
|
|
3
|
+
A lightweight, high-performance JavaScript library for 360-degree image rotation. Perfect for product displays and interactive image viewers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **No Dependencies**: No external libraries required.
|
|
8
|
+
- **Lightweight**: Minimal overhead and fast loading.
|
|
9
|
+
- **Responsive**: Adapts to various screen sizes.
|
|
10
|
+
- **Interactive**: Support for mouse and touch interactions.
|
|
11
|
+
- **Configurable**: Easy to customize sensitivity, responsiveness, and more.
|
|
12
|
+
- **Preload**: Number of initial images to load before making the viewer interactive. Other images fetch in the background.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Via npm
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install spinit-js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
1. **Prepare your container**:
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<div id="spinit-container" style="width: 100%; height: 500px;"></div>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. **Initialize SpinIt**:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
import { SpinIT } from 'spinit-js';
|
|
36
|
+
|
|
37
|
+
// container, [urlTemplate, startFrame, endFrame], options
|
|
38
|
+
const spinit = new SpinIT("#spinit-container", ["img_##.jpg", 1, 90], {
|
|
39
|
+
loop: true,
|
|
40
|
+
inertia: true,
|
|
41
|
+
friction: 0.95,
|
|
42
|
+
sensitivity: 1.0,
|
|
43
|
+
velocityScale: 1.0,
|
|
44
|
+
responsive: true,
|
|
45
|
+
preload: "all",
|
|
46
|
+
autoplay: true,
|
|
47
|
+
autoplaySpeed: 24
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Description |
|
|
54
|
+
| --- | --- | --- | --- |
|
|
55
|
+
| `loop` | `Boolean` | `true` | Whether the animation should loop when reaching the first or last frame. |
|
|
56
|
+
| `inertia` | `Boolean` | `true` | Enables smooth deceleration after the user stops dragging. |
|
|
57
|
+
| `friction` | `Number` | `0.95` | Controls how quickly the inertia slows down. (0 to 1) |
|
|
58
|
+
| `sensitivity` | `Number` | `1.0` | Adjusts rotation speed. Lower values increase sensitivity (faster rotation). |
|
|
59
|
+
| `velocityScale` | `Number` | `1.0` | Multiplier for the initial inertia velocity. |
|
|
60
|
+
| `responsive` | `Boolean` | `true` | Automatically resizes the canvas to match its container. |
|
|
61
|
+
| `preload` | `Number` \| `"all"` | `"all"` | Number of initial images to load before making the viewer interactive. Other images fetch in the background. |
|
|
62
|
+
| `autoplay` | `Boolean` \| `Number` | `true` | If true, spins automatically until user interaction. If a number, auto-spins until that frame index. |
|
|
63
|
+
| `autoplaySpeed` | `Number` | `24` | Autoplay speed in Frames Per Second (FPS). |
|
|
64
|
+
| `debug` | `Boolean` | `false` | Enables debug mode. |
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spinit-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "git init",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test/server.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"spritespin",
|
|
12
|
+
"360",
|
|
13
|
+
"rotation",
|
|
14
|
+
"image",
|
|
15
|
+
"viewer"
|
|
16
|
+
],
|
|
17
|
+
"author": "BillGR17",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/BillGR17/SpinIt.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/BillGR17/SpinIt/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/BillGR17/SpinIt#readme",
|
|
27
|
+
"files": [
|
|
28
|
+
"src"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventManager module for SpinIT.
|
|
3
|
+
* Handles user interactions via pointer events.
|
|
4
|
+
*/
|
|
5
|
+
export class EventManager {
|
|
6
|
+
/**
|
|
7
|
+
* Sets up pointer events for an element.
|
|
8
|
+
* @param {HTMLElement} element - The target element.
|
|
9
|
+
* @param {Object} callbacks - Object containing callback functions.
|
|
10
|
+
* @param {Function} callbacks.onStart - Called on pointer down.
|
|
11
|
+
* @param {Function} callbacks.onMove - Called on pointer move.
|
|
12
|
+
* @param {Function} callbacks.onEnd - Called on pointer up/cancel.
|
|
13
|
+
*/
|
|
14
|
+
static setupEvents(element, { onStart, onMove, onEnd }) {
|
|
15
|
+
element.style.touchAction = "none";
|
|
16
|
+
|
|
17
|
+
element.addEventListener("pointerdown", (e) => {
|
|
18
|
+
onStart(e);
|
|
19
|
+
element.setPointerCapture(e.pointerId);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
element.addEventListener("pointermove", (e) => {
|
|
23
|
+
onMove(e);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const stopDrag = (e) => {
|
|
27
|
+
onEnd(e);
|
|
28
|
+
try {
|
|
29
|
+
if (element.hasPointerCapture(e.pointerId)) {
|
|
30
|
+
element.releasePointerCapture(e.pointerId);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Suppress browser quirks where hasPointerCapture is true but pointer is already invalidated
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
element.addEventListener("pointerup", stopDrag);
|
|
38
|
+
element.addEventListener("pointercancel", stopDrag);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/Loader.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader module for SpinIT.
|
|
3
|
+
* Handles image URL generation and preloading images.
|
|
4
|
+
*/
|
|
5
|
+
export class Loader {
|
|
6
|
+
/**
|
|
7
|
+
* Generates an array of image URLs from a template.
|
|
8
|
+
* @param {string} template - The URL template (e.g., "img_##.jpg").
|
|
9
|
+
* @param {number} start - Start index.
|
|
10
|
+
* @param {number} end - End index.
|
|
11
|
+
* @param {boolean} [debug=false] - Whether to log warnings/errors.
|
|
12
|
+
* @returns {string[]}
|
|
13
|
+
*/
|
|
14
|
+
static generateUrls(template, start, end, debug = false) {
|
|
15
|
+
const urls = [];
|
|
16
|
+
const match = template.match(/(#+)/);
|
|
17
|
+
|
|
18
|
+
if (!match) {
|
|
19
|
+
if (debug) console.error("SpinIT Loader Error: No '#' pattern found in the URL template.");
|
|
20
|
+
return urls;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const placeholder = match[0];
|
|
24
|
+
const paddingLength = placeholder.length;
|
|
25
|
+
|
|
26
|
+
for (let i = start; i <= end; i++) {
|
|
27
|
+
const numberStr = String(i).padStart(paddingLength, "0");
|
|
28
|
+
urls.push(template.replace(placeholder, numberStr));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return urls;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Preloads a list of images.
|
|
36
|
+
* @param {string[]} urls - The URLs to preload.
|
|
37
|
+
* @param {boolean} [debug=false] - Whether to log warnings/errors.
|
|
38
|
+
* @returns {Promise<HTMLImageElement[]>}
|
|
39
|
+
*/
|
|
40
|
+
static preloadImages(urls, debug = false) {
|
|
41
|
+
const promises = urls.map(url => {
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
const img = new Image();
|
|
44
|
+
img.onload = () => resolve(img);
|
|
45
|
+
img.onerror = () => {
|
|
46
|
+
if (debug) console.warn(`SpinIT Loader Warning: Failed to load image at ${url}`);
|
|
47
|
+
resolve(null);
|
|
48
|
+
};
|
|
49
|
+
img.src = url;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return Promise.all(promises).then(images => images.filter(img => img !== null));
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/Physics.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Physics module for SpinIT.
|
|
3
|
+
* Handles frame calculations, inertia, and friction.
|
|
4
|
+
*/
|
|
5
|
+
export class Physics {
|
|
6
|
+
/**
|
|
7
|
+
* Calculates the current frame based on movement.
|
|
8
|
+
* @param {number} virtualFrame - Current virtual frame.
|
|
9
|
+
* @param {number} movementX - Movement in pixels.
|
|
10
|
+
* @param {number} sensitivity - Pixels per frame.
|
|
11
|
+
* @param {number} totalFrames - Total number of frames.
|
|
12
|
+
* @param {boolean} loop - Whether to loop the animation.
|
|
13
|
+
* @returns {Object} - Updated virtualFrame and final currentFrame.
|
|
14
|
+
*/
|
|
15
|
+
static calculateFrame(virtualFrame, movementX, sensitivity, totalFrames, loop) {
|
|
16
|
+
const frameChange = -(movementX / sensitivity);
|
|
17
|
+
let nextVirtualFrame = virtualFrame + frameChange;
|
|
18
|
+
|
|
19
|
+
if (loop) {
|
|
20
|
+
nextVirtualFrame = ((nextVirtualFrame % totalFrames) + totalFrames) % totalFrames;
|
|
21
|
+
} else {
|
|
22
|
+
nextVirtualFrame = Math.max(0, Math.min(totalFrames - 1, nextVirtualFrame));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
virtualFrame: nextVirtualFrame,
|
|
27
|
+
currentFrame: Math.floor(nextVirtualFrame)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Applies friction to velocity over time.
|
|
33
|
+
* @param {number} velocity - Current velocity pixels/ms.
|
|
34
|
+
* @param {number} friction - Friction coefficient.
|
|
35
|
+
* @param {number} dt - Time delta in ms.
|
|
36
|
+
* @returns {number} - New velocity.
|
|
37
|
+
*/
|
|
38
|
+
static applyFriction(velocity, friction, dt) {
|
|
39
|
+
// Normalize friction to 16ms/frame
|
|
40
|
+
const effectiveFriction = Math.pow(friction, dt / 16);
|
|
41
|
+
return velocity * effectiveFriction;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/Renderer.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer module for SpinIT.
|
|
3
|
+
* Handles canvas initialization and drawing.
|
|
4
|
+
*/
|
|
5
|
+
export class Renderer {
|
|
6
|
+
/**
|
|
7
|
+
* Initializes a canvas within a container.
|
|
8
|
+
* @param {HTMLElement} container - The container element.
|
|
9
|
+
* @param {number} width - Default width if not responsive.
|
|
10
|
+
* @param {number} height - Default height if not responsive.
|
|
11
|
+
* @param {boolean} [responsive=true] - Whether the canvas should be responsive.
|
|
12
|
+
* @returns {Object} - { canvas, ctx }
|
|
13
|
+
*/
|
|
14
|
+
static initCanvas(container, width, height, responsive = true) {
|
|
15
|
+
const canvas = document.createElement("canvas");
|
|
16
|
+
const ctx = canvas.getContext("2d");
|
|
17
|
+
|
|
18
|
+
if (responsive) {
|
|
19
|
+
canvas.width = container.clientWidth;
|
|
20
|
+
canvas.height = container.clientHeight;
|
|
21
|
+
canvas.style.width = "100%";
|
|
22
|
+
canvas.style.height = "100%";
|
|
23
|
+
} else {
|
|
24
|
+
canvas.width = width;
|
|
25
|
+
canvas.height = height;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
canvas.className = "spinit-canvas";
|
|
29
|
+
container.style.position = "relative"; // Ensure loader stays within container
|
|
30
|
+
container.appendChild(canvas);
|
|
31
|
+
return { canvas, ctx };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Injects the required CSS for the loader and blur effect.
|
|
36
|
+
*/
|
|
37
|
+
static injectStyles() {
|
|
38
|
+
const styleId = "spinit-styles";
|
|
39
|
+
if (document.getElementById(styleId)) return;
|
|
40
|
+
|
|
41
|
+
const style = document.createElement("style");
|
|
42
|
+
style.id = styleId;
|
|
43
|
+
style.textContent = `
|
|
44
|
+
.spinit-canvas.blurry {
|
|
45
|
+
filter: blur(15px);
|
|
46
|
+
transition: filter 0.6s ease;
|
|
47
|
+
}
|
|
48
|
+
.spinit-loader {
|
|
49
|
+
position: absolute;
|
|
50
|
+
top: 50%;
|
|
51
|
+
left: 50%;
|
|
52
|
+
transform: translate(-50%, -50%);
|
|
53
|
+
width: 40px;
|
|
54
|
+
height: 40px;
|
|
55
|
+
border: 4px solid rgba(0, 0, 0, 0.1);
|
|
56
|
+
border-top: 4px solid #3498db;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
animation: spinit-spin 1s linear infinite;
|
|
59
|
+
z-index: 10;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
@keyframes spinit-spin {
|
|
63
|
+
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
|
64
|
+
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
document.head.appendChild(style);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Toggles blur effect on the canvas.
|
|
72
|
+
* @param {HTMLCanvasElement} canvas
|
|
73
|
+
* @param {boolean} isBlurry
|
|
74
|
+
*/
|
|
75
|
+
static applyBlur(canvas, isBlurry) {
|
|
76
|
+
if (!canvas) return;
|
|
77
|
+
if (isBlurry) {
|
|
78
|
+
canvas.classList.add("blurry");
|
|
79
|
+
} else {
|
|
80
|
+
canvas.classList.remove("blurry");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Shows a loading indicator in the container.
|
|
86
|
+
* @param {HTMLElement} container
|
|
87
|
+
*/
|
|
88
|
+
static showLoader(container) {
|
|
89
|
+
if (!container) return;
|
|
90
|
+
if (container.querySelector(".spinit-loader")) return;
|
|
91
|
+
const loader = document.createElement("div");
|
|
92
|
+
loader.className = "spinit-loader";
|
|
93
|
+
container.appendChild(loader);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Hides the loading indicator.
|
|
98
|
+
* @param {HTMLElement} container
|
|
99
|
+
*/
|
|
100
|
+
static hideLoader(container) {
|
|
101
|
+
if (!container) return;
|
|
102
|
+
const loader = container.querySelector(".spinit-loader");
|
|
103
|
+
if (loader) {
|
|
104
|
+
loader.remove();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Renders a specific frame to the canvas.
|
|
110
|
+
* @param {CanvasRenderingContext2D} ctx - The canvas context.
|
|
111
|
+
* @param {number} width - Canvas width.
|
|
112
|
+
* @param {number} height - Canvas height.
|
|
113
|
+
* @param {boolean} [debug=false] - Whether debug mode is enabled.
|
|
114
|
+
* @param {number} [frameIndex=0] - The current frame index.
|
|
115
|
+
*/
|
|
116
|
+
static renderFrame(ctx, image, width, height, debug = false, frameIndex = 0) {
|
|
117
|
+
if (!ctx || !image) return;
|
|
118
|
+
ctx.clearRect(0, 0, width, height);
|
|
119
|
+
|
|
120
|
+
// Calculate scale to fit image (equivalent to object-fit: contain)
|
|
121
|
+
const scale = Math.min(width / image.width, height / image.height);
|
|
122
|
+
const drawWidth = image.width * scale;
|
|
123
|
+
const drawHeight = image.height * scale;
|
|
124
|
+
|
|
125
|
+
// Center the image
|
|
126
|
+
const x = (width - drawWidth) / 2;
|
|
127
|
+
const y = (height - drawHeight) / 2;
|
|
128
|
+
|
|
129
|
+
ctx.drawImage(image, x, y, drawWidth, drawHeight);
|
|
130
|
+
|
|
131
|
+
if (debug) {
|
|
132
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
|
133
|
+
ctx.fillRect(10, 10, 140, 36);
|
|
134
|
+
ctx.fillStyle = "white";
|
|
135
|
+
ctx.font = "16px monospace";
|
|
136
|
+
ctx.fillText(`Frame: ${frameIndex}`, 20, 34);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/SpinIT.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Loader } from './Loader.js';
|
|
2
|
+
import { Physics } from './Physics.js';
|
|
3
|
+
import { Renderer } from './Renderer.js';
|
|
4
|
+
import { EventManager } from './EventManager.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SpinIT - A modular, professional JavaScript 360-degree image spinner.
|
|
8
|
+
*/
|
|
9
|
+
export class SpinIT {
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the SpinIT instance.
|
|
12
|
+
* @param {string|HTMLElement} container - The container element or CSS selector.
|
|
13
|
+
* @param {Array} source - Array containing [urlTemplate, startNumber, endNumber].
|
|
14
|
+
* @param {Object} [options] - Configuration options.
|
|
15
|
+
*/
|
|
16
|
+
constructor(container, source, options = {}) {
|
|
17
|
+
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
|
18
|
+
|
|
19
|
+
if (!this.container) {
|
|
20
|
+
throw new Error("SpinIT Error: Container element not found.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.options = {
|
|
24
|
+
loop: options.loop ?? true,
|
|
25
|
+
inertia: options.inertia ?? true,
|
|
26
|
+
friction: options.friction ?? 0.95,
|
|
27
|
+
sensitivity: options.sensitivity ?? 1.0,
|
|
28
|
+
velocityScale: options.velocityScale ?? 1.0,
|
|
29
|
+
responsive: options.responsive ?? true,
|
|
30
|
+
preload: options.preload ?? "all",
|
|
31
|
+
autoplay: options.autoplay ?? true,
|
|
32
|
+
autoplaySpeed: options.autoplaySpeed ?? 24,
|
|
33
|
+
debug: options.debug ?? false
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** @type {HTMLImageElement[]} */
|
|
37
|
+
this.images = [];
|
|
38
|
+
this.currentFrame = 0;
|
|
39
|
+
this.virtualFrame = 0;
|
|
40
|
+
|
|
41
|
+
this.isDragging = false;
|
|
42
|
+
this.lastX = 0;
|
|
43
|
+
this.lastTime = 0;
|
|
44
|
+
this.velocity = 0; // Pixels per ms
|
|
45
|
+
|
|
46
|
+
this.animationId = null;
|
|
47
|
+
this.lastInertiaTime = 0;
|
|
48
|
+
|
|
49
|
+
this.canvas = null;
|
|
50
|
+
this.ctx = null;
|
|
51
|
+
|
|
52
|
+
this.#init(source);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#log(...args) {
|
|
56
|
+
if (this.options.debug) console.log(...args);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#error(...args) {
|
|
60
|
+
if (this.options.debug) console.error(...args);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async #init(source) {
|
|
64
|
+
this.#log("SpinIT: Initializing with source:", source);
|
|
65
|
+
|
|
66
|
+
// 1. Inject styles and show loader
|
|
67
|
+
Renderer.injectStyles();
|
|
68
|
+
Renderer.showLoader(this.container);
|
|
69
|
+
|
|
70
|
+
const urls = Loader.generateUrls(source[0], source[1], source[2], this.options.debug);
|
|
71
|
+
const isPreloadingAll = (this.options.preload === "all" || this.options.preload >= urls.length);
|
|
72
|
+
const preloadCount = isPreloadingAll ? 1 : Math.max(1, this.options.preload);
|
|
73
|
+
|
|
74
|
+
const preloadUrls = urls.slice(0, preloadCount);
|
|
75
|
+
|
|
76
|
+
// 2. Preload the specified number of images to show something immediately
|
|
77
|
+
const preloadedImages = await Loader.preloadImages(preloadUrls, this.options.debug);
|
|
78
|
+
|
|
79
|
+
if (preloadedImages.length === 0) {
|
|
80
|
+
this.#error("SpinIT Error: Could not load the initial images.");
|
|
81
|
+
Renderer.hideLoader(this.container);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Initialize this.images array with the correct length to allow correct math in physics
|
|
86
|
+
this.images = new Array(urls.length);
|
|
87
|
+
for (let i = 0; i < preloadedImages.length; i++) {
|
|
88
|
+
this.images[i] = preloadedImages[i];
|
|
89
|
+
}
|
|
90
|
+
const { width, height } = preloadedImages[0];
|
|
91
|
+
|
|
92
|
+
// 3. Initialize canvas
|
|
93
|
+
const { canvas, ctx } = Renderer.initCanvas(this.container, width, height, this.options.responsive);
|
|
94
|
+
this.canvas = canvas;
|
|
95
|
+
this.ctx = ctx;
|
|
96
|
+
|
|
97
|
+
if (this.options.responsive) {
|
|
98
|
+
this.resizeObserver = new ResizeObserver(() => this.handleResize());
|
|
99
|
+
this.resizeObserver.observe(this.container);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 4. Render the first frame
|
|
103
|
+
this.render();
|
|
104
|
+
|
|
105
|
+
// If preloading all, keep old behavior: blur, wait for all, unblur.
|
|
106
|
+
// Otherwise, unblur immediately and load the rest in background.
|
|
107
|
+
if (isPreloadingAll) {
|
|
108
|
+
Renderer.applyBlur(this.canvas, true);
|
|
109
|
+
this.#log("SpinIT: First frame rendered with blur. Loading remaining images...");
|
|
110
|
+
|
|
111
|
+
Loader.preloadImages(urls, this.options.debug).then(allImages => {
|
|
112
|
+
this.#log(`SpinIT: Preloaded ${allImages.length} images. Clearing loading state...`);
|
|
113
|
+
this.images = allImages;
|
|
114
|
+
Renderer.applyBlur(this.canvas, false);
|
|
115
|
+
Renderer.hideLoader(this.container);
|
|
116
|
+
this.#setupEvents();
|
|
117
|
+
this.render(); // Final crisp render
|
|
118
|
+
this.#startAutoPlay();
|
|
119
|
+
this.#log("SpinIT: Initialization complete. Canvas size:", width, "x", height);
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
Renderer.applyBlur(this.canvas, false);
|
|
123
|
+
Renderer.hideLoader(this.container);
|
|
124
|
+
this.#setupEvents();
|
|
125
|
+
this.#startAutoPlay();
|
|
126
|
+
this.#log(`SpinIT: Preloaded ${preloadCount} images. Enabling interaction and loading remaining in background...`);
|
|
127
|
+
|
|
128
|
+
const remainingUrls = urls.slice(preloadCount);
|
|
129
|
+
const remainingPromises = remainingUrls.map((url, i) => {
|
|
130
|
+
return Loader.preloadImages([url], this.options.debug).then(([img]) => {
|
|
131
|
+
const index = preloadCount + i;
|
|
132
|
+
this.images[index] = img;
|
|
133
|
+
if (this.currentFrame === index) {
|
|
134
|
+
this.render();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
Promise.all(remainingPromises).then(() => {
|
|
140
|
+
this.#log("SpinIT: All background images loaded.");
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#setupEvents() {
|
|
146
|
+
EventManager.setupEvents(this.canvas, {
|
|
147
|
+
onStart: (e) => {
|
|
148
|
+
this.#log("SpinIT: Drag started at", e.clientX);
|
|
149
|
+
this.isDragging = true;
|
|
150
|
+
this.#stopAutoPlay();
|
|
151
|
+
this.lastX = e.clientX;
|
|
152
|
+
this.lastTime = performance.now();
|
|
153
|
+
this.velocity = 0;
|
|
154
|
+
|
|
155
|
+
if (this.animationId) {
|
|
156
|
+
cancelAnimationFrame(this.animationId);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
onMove: (e) => {
|
|
160
|
+
if (!this.isDragging) return;
|
|
161
|
+
|
|
162
|
+
const now = performance.now();
|
|
163
|
+
const dt = now - this.lastTime;
|
|
164
|
+
const dx = e.clientX - this.lastX;
|
|
165
|
+
|
|
166
|
+
if (dt > 0) {
|
|
167
|
+
const currentVelocity = dx / dt;
|
|
168
|
+
this.velocity = (this.velocity * 0.5) + (currentVelocity * 0.5);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.updateFrame(dx);
|
|
172
|
+
|
|
173
|
+
this.lastX = e.clientX;
|
|
174
|
+
this.lastTime = now;
|
|
175
|
+
},
|
|
176
|
+
onEnd: () => {
|
|
177
|
+
this.#log("SpinIT: Drag ended. Velocity:", this.velocity);
|
|
178
|
+
this.isDragging = false;
|
|
179
|
+
|
|
180
|
+
if (performance.now() - this.lastTime > 50) {
|
|
181
|
+
this.velocity = 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.options.inertia && Math.abs(this.velocity) > 0.1) {
|
|
185
|
+
this.velocity *= this.options.velocityScale;
|
|
186
|
+
this.lastInertiaTime = performance.now();
|
|
187
|
+
this.#applyInertia();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Updates the current frame based on movement.
|
|
195
|
+
* @param {number} dx - Movement in pixels.
|
|
196
|
+
*/
|
|
197
|
+
updateFrame(dx) {
|
|
198
|
+
const { virtualFrame, currentFrame } = Physics.calculateFrame(
|
|
199
|
+
this.virtualFrame,
|
|
200
|
+
dx,
|
|
201
|
+
this.options.sensitivity,
|
|
202
|
+
this.images.length,
|
|
203
|
+
this.options.loop
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
this.virtualFrame = virtualFrame;
|
|
207
|
+
|
|
208
|
+
if (this.currentFrame !== currentFrame) {
|
|
209
|
+
this.#log(`SpinIT: Switching to frame ${currentFrame} (virtual: ${virtualFrame.toFixed(2)})`);
|
|
210
|
+
this.currentFrame = currentFrame;
|
|
211
|
+
this.render();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Hard stop for non-looping spin
|
|
215
|
+
if (!this.options.loop && (this.virtualFrame === 0 || this.virtualFrame === this.images.length - 1)) {
|
|
216
|
+
this.velocity = 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#applyInertia() {
|
|
221
|
+
const now = performance.now();
|
|
222
|
+
const dt = now - this.lastInertiaTime;
|
|
223
|
+
this.lastInertiaTime = now;
|
|
224
|
+
|
|
225
|
+
if (dt <= 0) {
|
|
226
|
+
this.animationId = requestAnimationFrame(() => this.#applyInertia());
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.velocity = Physics.applyFriction(this.velocity, this.options.friction, dt);
|
|
231
|
+
const frameMovement = this.velocity * dt;
|
|
232
|
+
|
|
233
|
+
if (Math.abs(frameMovement) < 0.1) {
|
|
234
|
+
this.velocity = 0;
|
|
235
|
+
this.lastInertiaTime = 0;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.updateFrame(frameMovement);
|
|
240
|
+
this.animationId = requestAnimationFrame(() => this.#applyInertia());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handles container resize.
|
|
245
|
+
*/
|
|
246
|
+
handleResize() {
|
|
247
|
+
if (!this.canvas || !this.container) return;
|
|
248
|
+
this.canvas.width = this.container.clientWidth;
|
|
249
|
+
this.canvas.height = this.container.clientHeight;
|
|
250
|
+
this.render();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Renders the current frame.
|
|
255
|
+
*/
|
|
256
|
+
render() {
|
|
257
|
+
Renderer.renderFrame(
|
|
258
|
+
this.ctx,
|
|
259
|
+
this.images[this.currentFrame],
|
|
260
|
+
this.canvas.width,
|
|
261
|
+
this.canvas.height,
|
|
262
|
+
this.options.debug,
|
|
263
|
+
this.currentFrame
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Destroys the SpinIT instance and cleans up.
|
|
269
|
+
*/
|
|
270
|
+
destroy() {
|
|
271
|
+
this.#stopAutoPlay();
|
|
272
|
+
if (this.resizeObserver) {
|
|
273
|
+
this.resizeObserver.disconnect();
|
|
274
|
+
}
|
|
275
|
+
if (this.animationId) {
|
|
276
|
+
cancelAnimationFrame(this.animationId);
|
|
277
|
+
}
|
|
278
|
+
// Remove canvas if needed, or other cleanup...
|
|
279
|
+
if (this.canvas && this.canvas.parentNode) {
|
|
280
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#startAutoPlay() {
|
|
285
|
+
if (this.options.autoplay === false) return;
|
|
286
|
+
|
|
287
|
+
this.isAutoPlaying = true;
|
|
288
|
+
|
|
289
|
+
let targetFrame = null;
|
|
290
|
+
if (typeof this.options.autoplay === 'number') {
|
|
291
|
+
targetFrame = this.options.autoplay;
|
|
292
|
+
if (this.currentFrame === targetFrame) return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.autoPlayInterval = setInterval(() => {
|
|
296
|
+
if (!this.isAutoPlaying) {
|
|
297
|
+
this.#stopAutoPlay();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.updateFrame(-Math.max(1, this.options.sensitivity));
|
|
302
|
+
|
|
303
|
+
if (targetFrame !== null && this.currentFrame === targetFrame) {
|
|
304
|
+
this.#stopAutoPlay();
|
|
305
|
+
} else if (!this.options.loop && this.currentFrame === this.images.length - 1) {
|
|
306
|
+
this.#stopAutoPlay();
|
|
307
|
+
}
|
|
308
|
+
}, 1000 / this.options.autoplaySpeed);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#stopAutoPlay() {
|
|
312
|
+
this.isAutoPlaying = false;
|
|
313
|
+
if (this.autoPlayInterval) {
|
|
314
|
+
clearInterval(this.autoPlayInterval);
|
|
315
|
+
this.autoPlayInterval = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
package/src/index.js
ADDED