@andresclua/perlindom 0.0.6 → 0.1.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 CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.1] - 2026-11-07
11
+
12
+ ### Changed
13
+ - README: links to the website at the top; file renamed to `README.md`
14
+
15
+ ## [0.1.0] - 2026-11-07
16
+
17
+ ### Added
18
+ - **`amplitude` option and property**: scales movement strength, clamped to [0, 1] (1 = free drift, 0 = pinned to the target or the base position); non-number values are treated as 1, no string/null coercion
19
+ - **`reset()` method**: rewinds the noise path to the seed and returns the element to its own transform, keeping play/pause, `amplitude` and `target`; a no-op after `destroy()`
20
+ - **`legacyNoise` option** (default `false`): `true` restores the pre-0.0.7 noise rows
21
+ - **`pauseOffscreen` option** (default `true`): `false` keeps an instance stepping while offscreen
22
+ - **`target` option and `setTarget`**: `target` is a `{x, y}` offset in px from the element's base position that `amplitude` pulls toward (amplitude 0 pins the element at the target); `setTarget` copies the object and turns non-finite coordinates into 0
23
+
24
+ ### Changed
25
+ - **Noise sampling (behavior change)**: the two axes are now sampled at rows 0.5 and 100.5 instead of 0 and 100, where Perlin noise is flat on the lattice. Movement now reaches roughly ±1 of the range instead of ±0.5, and no longer sits nearly still at the start for some seeds (seed 42 used to take ~3.7 s). Pass `legacyNoise: true` to keep the previous movement exactly
26
+ - **Shared loop**: all instances now share a single `requestAnimationFrame` loop
27
+ - **Offscreen pause**: instances pause while their element's parent is outside the viewport (the parent, so an element's own drift cannot freeze it); changing `amplitude` or `target` still renders while paused
28
+ - **Build output**: `dist` files are now `perlindom.es.js` and `perlindom.umd.js` (lowercase), matching `package.json` and `vite.config.js`; the entry path casing was fixed for case-sensitive filesystems
29
+
30
+ ### Removed
31
+ - Internal instance fields `animate` and `raf` (undocumented; replaced by the shared loop)
32
+
8
33
  ## [0.0.6] - 2025-02-09
9
34
 
10
35
  ## Changed
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrés Clúa
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,426 @@
1
+ **[perlindom.js.org](https://perlindom.js.org)** · [API](https://perlindom.js.org/api.html) · [Examples](https://perlindom.js.org/examples.html) · [Showcase](https://perlindom.js.org/showcase.html)
2
+
3
+ # PerlinDOM
4
+
5
+ [![npm version](https://badge.fury.io/js/@andresclua%2Fperlindom.svg)](https://badge.fury.io/js/@andresclua%2Fperlindom)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![JavaScript](https://img.shields.io/badge/JavaScript-ES6+-yellow.svg)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
8
+
9
+ A lightweight JavaScript library for creating smooth, organic animations on DOM elements using Perlin noise. Create natural, fluid movements that bring your web interfaces to life.
10
+
11
+ ## 📋 Table of Contents
12
+
13
+ - [Features](#-features)
14
+ - [Installation](#-installation)
15
+ - [Quick Start](#-quick-start)
16
+ - [Configuration](#-configuration)
17
+ - [Amplitude and target](#-amplitude-and-target)
18
+ - [Use with GSAP + ScrollTrigger](#-use-with-gsap--scrolltrigger)
19
+ - [Examples](#-examples)
20
+ - [Best Practices](#-best-practices)
21
+ - [API Reference](#-api-reference)
22
+ - [Troubleshooting](#-troubleshooting)
23
+ - [Browser Compatibility](#-browser-compatibility)
24
+ - [Live site](#-live-site)
25
+ - [Contributing](#-contributing)
26
+ - [Sponsor](#-sponsor)
27
+ - [Changelog](#-changelog)
28
+ - [License](#-license)
29
+
30
+ ## ✨ Features
31
+
32
+ - **🌊 Organic Animations**: Natural movements using Perlin noise that avoid mechanical rigidity
33
+ - **📱 Responsive Design**: Support for percentage-based values that adapt to container size
34
+ - **🎯 Smart Positioning**: Automatically works from element's current position (CSS or inline styles)
35
+ - **⚡ High Performance**: Uses `transform` and one shared `requestAnimationFrame` loop for all instances; instances scrolled out of the viewport pause automatically
36
+ - **🧲 Amplitude & Target**: Scale the drift from 0 to 1 and pin elements to a target position, ideal for scroll-driven effects
37
+ - **🎛️ Full Control**: Play, pause, and destroy animations with smooth transitions
38
+ - **📦 Lightweight**: Minimal footprint with zero dependencies
39
+ - **🔧 Easy Integration**: Simple API that works with any DOM element
40
+ - **🎨 Flexible**: Mix pixel and percentage values, animate on single or both axes
41
+
42
+ ## 📦 Installation
43
+
44
+ ```bash
45
+ npm install @andresclua/perlindom
46
+ ```
47
+
48
+ Or include directly in your HTML:
49
+
50
+ ```html
51
+ <script type="module">
52
+ import PerlinDOM from 'https://unpkg.com/@andresclua/perlindom/dist/perlindom.es.js';
53
+ </script>
54
+ ```
55
+
56
+ ## 🚀 Quick Start
57
+
58
+ ```javascript
59
+ import PerlinDOM from '@andresclua/perlindom';
60
+
61
+ // Basic animation - element will animate from its current position
62
+ const animation = new PerlinDOM({
63
+ element: document.querySelector('.my-element'),
64
+ x: { min: -50, max: 50 }, // Move ±50px horizontally
65
+ y: { min: -30, max: 30 }, // Move ±30px vertically
66
+ speed: 0.01, // Animation speed
67
+ seed: 123 // Reproducible pattern
68
+ });
69
+
70
+ // Control the animation
71
+ animation.pause(); // Smooth pause
72
+ animation.play(); // Resume
73
+ animation.reset(); // Rewind to the start (same path again)
74
+ animation.destroy(); // Clean up
75
+ ```
76
+
77
+ Instances start playing as soon as they are created. PerlinDOM sets `position: relative` itself if the element is `static`, and animates from wherever the element is laid out.
78
+
79
+ ## ⚙️ Configuration
80
+
81
+ | Option | Type | Default | Description |
82
+ |--------|------|---------|-------------|
83
+ | `element` | `HTMLElement \| null` | `undefined` | The DOM element to animate. Optional: with `null` only `lastX` / `lastY` are computed |
84
+ | `x` | `Object \| null` | `null` | Horizontal movement: `{min: number, max: number}` |
85
+ | `y` | `Object \| null` | `null` | Vertical movement: `{min: number, max: number}` |
86
+ | `speed` | `number` | `0.01` | Animation speed (lower = slower) |
87
+ | `seed` | `number` | `0` | Seed for reproducible patterns |
88
+ | `lerpSpeed` | `number` | `0.1` | Transition smoothness for pause/play |
89
+ | `preserveBaseTranslation` | `boolean` | `false` | Keep the element's CSS `translate` (e.g. `translate(-50%, -50%)`); by default it is dropped and only scale/rotate/skew are kept |
90
+ | `amplitude` | `number` | `1` | Strength of the drift, clamped to `[0, 1]`; non-numbers become `1`. `1` = free drift, `0` = pinned to `target` (or the base position) |
91
+ | `target` | `{x, y} \| null` | `null` | Offset in px from the element's base position that `amplitude` pulls toward. Coordinates that are not finite numbers become `0` |
92
+ | `legacyNoise` | `boolean` | `false` | `true` keeps the old noise rows (0 / 100) for movement identical to 0.0.6 and earlier |
93
+ | `pauseOffscreen` | `boolean` | `true` | Skip frames while the element's parent is outside the viewport. `false` keeps it running everywhere |
94
+
95
+ ### Value Types
96
+
97
+ - **Pixels**: `{ min: -50, max: 50 }` - Fixed pixel movement
98
+ - **Percentages**: `{ min: '-10%', max: '10%' }` - Relative to the parent's `clientWidth` / `clientHeight` (padding included), re-read every frame. They need an `element` (with `element: null` they produce `NaN`)
99
+ - **Mixed**: `x: { min: '-5%', max: '5%' }, y: { min: -20, max: 20 }`
100
+ - **Single Axis**: Set unused axis to `null`
101
+
102
+ ## 🎚️ Amplitude and Target
103
+
104
+ `amplitude` scales how far the noise moves the element. `target` is where it settles as `amplitude` goes to `0`: an `{ x, y }` offset in px from the element's base position (the object is copied). Without a `target` the element settles on its base position.
105
+
106
+ ```javascript
107
+ const dot = new PerlinDOM({
108
+ element: document.querySelector('.dot'),
109
+ x: { min: -120, max: 120 },
110
+ y: { min: -120, max: 120 },
111
+ target: { x: 40, y: -20 }
112
+ });
113
+
114
+ dot.amplitude = 0.5; // half the drift, pulled toward the target
115
+ dot.amplitude = 0; // pinned at { x: 40, y: -20 }
116
+ dot.setTarget({ x: 0, y: 0 }); // move the target; setTarget(null) removes it
117
+ dot.amplitude = 1; // free drift again
118
+ ```
119
+
120
+ ## 🎬 Use with GSAP + ScrollTrigger
121
+
122
+ PerlinDOM owns the element's `transform`, so GSAP must not write `transform` (`x`, `y`, `scale`, ...) on the same element. Tween `amplitude` instead; to move an element with GSAP and drift it too, put PerlinDOM on a wrapper or a child.
123
+
124
+ ```javascript
125
+ import gsap from 'gsap';
126
+ import { ScrollTrigger } from 'gsap/ScrollTrigger';
127
+ import PerlinDOM from '@andresclua/perlindom';
128
+
129
+ gsap.registerPlugin(ScrollTrigger);
130
+
131
+ const shape = [{ x: 0, y: -80 }, { x: 70, y: 40 }, { x: -70, y: 40 }]; // one { x, y } per .dot
132
+
133
+ const instances = [...document.querySelectorAll('.dot')].map((el, i) =>
134
+ new PerlinDOM({
135
+ element: el,
136
+ x: { min: -120, max: 120 },
137
+ y: { min: -120, max: 120 },
138
+ seed: i * 7919,
139
+ target: shape[i], // { x, y } offsets that draw your shape
140
+ })
141
+ );
142
+
143
+ const state = { amplitude: 1 };
144
+ gsap.to(state, {
145
+ amplitude: 0,
146
+ ease: 'none',
147
+ scrollTrigger: { trigger: '.scene', start: 'top top', end: '+=1000', pin: true, scrub: 0.8 },
148
+ onUpdate: () => instances.forEach(inst => { inst.amplitude = state.amplitude; }),
149
+ });
150
+ ```
151
+
152
+ ## 📚 Examples
153
+
154
+ ### 1. Basic Element Animation
155
+
156
+ ```html
157
+ <div class="floating-card">Content</div>
158
+ ```
159
+
160
+ ```css
161
+ .floating-card {
162
+ position: relative;
163
+ left: 50%;
164
+ top: 50%;
165
+ transform: translate(-50%, -50%); /* centring translate */
166
+ }
167
+ ```
168
+
169
+ ```javascript
170
+ new PerlinDOM({
171
+ element: document.querySelector('.floating-card'),
172
+ x: { min: -20, max: 20 },
173
+ y: { min: -15, max: 15 },
174
+ speed: 0.008,
175
+ seed: 42,
176
+ preserveBaseTranslation: true // keep the centring translate
177
+ });
178
+ ```
179
+
180
+ ### 2. Responsive Background Elements
181
+
182
+ ```javascript
183
+ // Elements adapt to container size changes
184
+ document.querySelectorAll('.bg-particle').forEach((el, index) => {
185
+ new PerlinDOM({
186
+ element: el,
187
+ x: { min: '-15%', max: '15%' }, // 15% of parent width
188
+ y: { min: '-10%', max: '10%' }, // 10% of parent height
189
+ speed: 0.003,
190
+ seed: index * 1000
191
+ });
192
+ });
193
+ ```
194
+
195
+ ### 3. Interactive Hover Effects
196
+
197
+ ```javascript
198
+ const button = document.querySelector('.animated-button');
199
+ let animation;
200
+
201
+ button.addEventListener('mouseenter', () => {
202
+ if (!animation) {
203
+ animation = new PerlinDOM({
204
+ element: button.querySelector('.effect'),
205
+ x: { min: -5, max: 5 },
206
+ y: { min: -5, max: 5 },
207
+ speed: 0.02,
208
+ lerpSpeed: 0.15
209
+ });
210
+ } else {
211
+ animation.play();
212
+ }
213
+ });
214
+
215
+ button.addEventListener('mouseleave', () => {
216
+ animation?.pause();
217
+ });
218
+ ```
219
+
220
+ ### 4. Multiple Elements with Synchronized Patterns
221
+
222
+ ```javascript
223
+ // Same seed = synchronized movement
224
+ document.querySelectorAll('.sync-element').forEach(el => {
225
+ new PerlinDOM({
226
+ element: el,
227
+ x: { min: -30, max: 30 },
228
+ y: { min: -30, max: 30 },
229
+ speed: 0.01,
230
+ seed: 12345 // Same seed for all elements
231
+ });
232
+ });
233
+ ```
234
+
235
+ ### 5. Custom Animation Patterns
236
+
237
+ ```javascript
238
+ // Horizontal-only movement for sliding effects
239
+ new PerlinDOM({
240
+ element: document.querySelector('.slider'),
241
+ x: { min: -100, max: 100 },
242
+ y: null, // No vertical movement
243
+ speed: 0.005
244
+ });
245
+
246
+ // Vertical-only for floating effects
247
+ new PerlinDOM({
248
+ element: document.querySelector('.floater'),
249
+ x: null, // No horizontal movement
250
+ y: { min: -40, max: 40 },
251
+ speed: 0.008
252
+ });
253
+ ```
254
+
255
+ ### 6. Advanced: SVG Path Animation
256
+
257
+ ```javascript
258
+ // Animate SVG elements or paths
259
+ const svgElement = document.querySelector('#animated-path');
260
+ const pathAnimation = new PerlinDOM({
261
+ element: null, // No direct element
262
+ x: { min: -20, max: 20 },
263
+ y: { min: -15, max: 15 },
264
+ speed: 0.01,
265
+ seed: 999
266
+ });
267
+
268
+ function updatePath() {
269
+ const offsetX = pathAnimation.lastX;
270
+ const offsetY = pathAnimation.lastY;
271
+
272
+ // Update SVG path or transform
273
+ svgElement.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
274
+
275
+ requestAnimationFrame(updatePath);
276
+ }
277
+ updatePath();
278
+ ```
279
+
280
+ ## 💡 Best Practices
281
+
282
+ ### ✅ Do
283
+
284
+ - **Position with CSS, not with `translate`**: Use `left`/`top`, margins, grid or flex. A CSS `translate` is dropped unless you pass `preserveBaseTranslation: true`
285
+ - **One instance per element**: Two instances on the same element overwrite each other's transform
286
+ - **Use percentage values for responsive designs**: Great for mobile-friendly animations
287
+ - **Choose appropriate speeds**: Start with `0.01` and adjust based on desired effect
288
+ - **Clean up animations**: Call `destroy()` when removing elements from DOM
289
+
290
+ ### ❌ Don't
291
+
292
+ - **Don't tween `transform` with another library on the same element**: PerlinDOM writes it every frame (see the GSAP recipe above)
293
+ - **Don't forget to destroy**: A live instance keeps a reference to its element, so call `destroy()` before removing it
294
+ - **Don't use extreme speeds**: Above about `0.05` the motion turns jittery
295
+ - **Don't forget reduced motion**: PerlinDOM does not check `prefers-reduced-motion`, so skip creating instances when it is set
296
+
297
+ ### 🎯 Performance Tips
298
+
299
+ ```javascript
300
+ // Good: Many instances are cheap, they all share one requestAnimationFrame loop
301
+ // and pause on their own while scrolled out of view
302
+
303
+ // Good: Use percentage values for responsive design
304
+ x: { min: '-5%', max: '5%' }
305
+
306
+ // Good: Appropriate speeds for smooth animation
307
+ speed: 0.01 // Smooth and natural
308
+
309
+ // Good: Clean up when done
310
+ animation.destroy();
311
+ ```
312
+
313
+ ## 📖 API Reference
314
+
315
+ ### Constructor
316
+
317
+ ```javascript
318
+ new PerlinDOM(options) // options: see Configuration
319
+ ```
320
+
321
+ ### Methods
322
+
323
+ | Method | Description | Returns |
324
+ |--------|-------------|---------|
325
+ | `play()` | Resume after `pause()` (instances start playing on construction) | `void` |
326
+ | `pause()` | Smoothly pause animation | `void` |
327
+ | `reset()` | Back to the starting state: the noise path rewinds to the seed and the element returns to its own transform. Keeps play/pause, `amplitude` and `target`; does nothing after `destroy()` | `void` |
328
+ | `destroy()` | Stop animation, restore the original inline `transform` and clean up | `void` |
329
+ | `init(seed?)` | Rebuild the noise from `seed` (without an argument: seed `0`, not the constructed seed; `time` is not reset) and restart playback, also after `destroy()` | `void` |
330
+ | `setTarget({x, y} \| null)` | Set the target offset in px; non-finite coordinates become `0`, the object is copied, `null` removes it | `void` |
331
+ | `refreshBaseTransform(preserveTranslation?)` | Re-read the element's CSS transform after classes or styles changed | `void` |
332
+
333
+ ### Properties
334
+
335
+ | Property | Type | Description |
336
+ |----------|------|-------------|
337
+ | `lastX` | `number` | Current X offset value |
338
+ | `lastY` | `number` | Current Y offset value |
339
+ | `isPlaying` | `boolean` | Current animation state |
340
+ | `speed` | `number` | Animation speed (writable) |
341
+ | `lerpSpeed` | `number` | Transition speed (writable) |
342
+ | `amplitude` | `number` | Drift strength, clamped to `[0, 1]`, non-numbers reset to `1` (writable) |
343
+ | `target` | `{x, y} \| null` | Current target offset (set it with `setTarget()`) |
344
+ | `xRange` / `yRange` | `Object \| null` | Movement ranges (writable; whether percentages are used is fixed at construction) |
345
+
346
+ ## 🔧 Troubleshooting
347
+
348
+ ### Common Issues
349
+
350
+ **❓ Animation not visible**
351
+ - Set at least one of `x` / `y`
352
+ - The element must not be `display: inline` (e.g. a plain `<span>`): transforms do not apply to inline boxes. Use `inline-block` or `block`
353
+ - Static positioning is not the problem: PerlinDOM sets `position: relative` itself
354
+
355
+ **❓ Element jumps on initialization**
356
+ - Usually a CSS `translate`, such as `translate(-50%, -50%)` for centring, which PerlinDOM drops by default
357
+ - Pass `preserveBaseTranslation: true`, or centre with `left: calc(50% - half)` or margins
358
+
359
+ **❓ Animation too fast/slow**
360
+ ```javascript
361
+ // Adjust speed value
362
+ speed: 0.001 // Very slow
363
+ speed: 0.01 // Normal
364
+ speed: 0.1 // Fast
365
+ ```
366
+
367
+ **❓ Percentage values not working**
368
+ - Ensure parent container has defined dimensions
369
+ - Percentages use the parent's `clientWidth` / `clientHeight`, and need an `element`
370
+
371
+ **❓ Element stops moving while off screen**
372
+ - Intended: instances pause while their element's parent is outside the viewport and resume when it returns (needs `IntersectionObserver`; without it everything keeps running). The parent is watched, not the element, so an element's own drift never pauses it. Changing `amplitude` or calling `setTarget()` still renders while paused. Pass `pauseOffscreen: false` to never pause
373
+
374
+ **❓ Memory leaks**
375
+ ```javascript
376
+ // Always clean up when removing elements
377
+ animation.destroy();
378
+ ```
379
+
380
+ ### Browser DevTools
381
+
382
+ Monitor performance in DevTools:
383
+ - Check for smooth 60fps in Performance tab
384
+ - Verify transform properties are being animated (not layout properties)
385
+
386
+ ## 🌐 Browser Compatibility
387
+
388
+ The npm build (`dist/`) targets browsers with native ES modules and ES2020: **Chrome 87+, Edge 88+, Firefox 78+, Safari 14+**. Importing the source file directly needs private class methods and `??=`: Chrome 85+, Edge 85+, Firefox 90+, Safari 15+.
389
+
390
+ - `requestAnimationFrame`, `getComputedStyle` and CSS transforms are required
391
+ - `DOMMatrixReadOnly` separates the CSS translate from scale/rotate/skew; without it the computed transform is used as it is
392
+ - `IntersectionObserver` powers the offscreen pause; without it everything keeps running
393
+
394
+ ## 🔗 Live site
395
+
396
+ Live site: **https://perlindom.js.org** · [API reference](https://perlindom.js.org/api.html) · [Examples](https://perlindom.js.org/examples.html) · [Showcase](https://perlindom.js.org/showcase.html) (add yours with a PR to `src/showcase.json`)
397
+
398
+ ## 🤝 Contributing
399
+
400
+ Bug reports, fixes and showcase entries are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, tests and how to add your project to the [showcase](https://perlindom.js.org/showcase.html).
401
+
402
+ ## 💜 Sponsor
403
+
404
+ PerlinDOM is free and MIT licensed. If it saves you time, you can support its development through [GitHub Sponsors](https://github.com/sponsors/andresclua).
405
+
406
+ ## 📝 Changelog
407
+
408
+ See [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
409
+
410
+ ### Recent Updates
411
+
412
+ - **v0.0.6**: Enhanced PerlinDOM to preserve design transforms
413
+ - **v0.0.4**: Package now public on npm, enhanced module exports configuration
414
+ - **v0.0.3**: Fixed positioning conflicts, improved automatic position detection
415
+ - **v0.0.2**: Added percentage support and responsive animations
416
+ - **v0.0.1**: Initial release with core Perlin noise animations
417
+
418
+ ## 📄 License
419
+
420
+ MIT License - see [LICENSE](./LICENSE) file for details.
421
+
422
+ ---
423
+
424
+ **Made with ❤️ by [Andres Clua](https://github.com/andresclua)**
425
+
426
+ *PerlinDOM - Bringing organic motion to the web*