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 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
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ import { SpinIT } from './SpinIT.js';
2
+
3
+ export { SpinIT };
4
+ export default SpinIT;