@andresclua/perlindom 0.0.6 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ 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.0] - 2026-11-07
11
+
12
+ ### Added
13
+ - **`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
14
+ - **`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()`
15
+ - **`legacyNoise` option** (default `false`): `true` restores the pre-0.0.7 noise rows
16
+ - **`pauseOffscreen` option** (default `true`): `false` keeps an instance stepping while offscreen
17
+ - **`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
18
+
19
+ ### Changed
20
+ - **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
21
+ - **Shared loop**: all instances now share a single `requestAnimationFrame` loop
22
+ - **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
23
+ - **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
24
+
25
+ ### Removed
26
+ - Internal instance fields `animate` and `raf` (undocumented; replaced by the shared loop)
27
+
8
28
  ## [0.0.6] - 2025-02-09
9
29
 
10
30
  ## 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.
@@ -0,0 +1,212 @@
1
+ var C = (i) => {
2
+ throw TypeError(i);
3
+ };
4
+ var I = (i, t, e) => t.has(i) || C("Cannot " + e);
5
+ var M = (i, t, e) => (I(i, t, "read from private field"), e ? e.call(i) : t.get(i)), B = (i, t, e) => t.has(i) ? C("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(i) : t.set(i, e), D = (i, t, e, s) => (I(i, t, "write to private field"), s ? s.call(i, e) : t.set(i, e), e), h = (i, t, e) => (I(i, t, "access private method"), e);
6
+ const w = /* @__PURE__ */ new Set();
7
+ let v = null;
8
+ function G() {
9
+ v = null;
10
+ for (const i of [...w])
11
+ w.has(i) && (!i.onScreen && !i._dirty || (i._dirty = !1, i.step() === !1 && w.delete(i)));
12
+ w.size > 0 && v === null && (v = requestAnimationFrame(G));
13
+ }
14
+ function $(i) {
15
+ w.add(i), v === null && (v = requestAnimationFrame(G));
16
+ }
17
+ function Z(i) {
18
+ w.delete(i), w.size === 0 && v !== null && (cancelAnimationFrame(v), v = null);
19
+ }
20
+ const z = /* @__PURE__ */ new Map(), Y = /* @__PURE__ */ new WeakMap();
21
+ let O = null;
22
+ function V(i) {
23
+ if (typeof IntersectionObserver > "u" || !i.el || !i.pauseOffscreen || Y.has(i)) return;
24
+ O ?? (O = new IntersectionObserver((s) => {
25
+ for (const r of s) {
26
+ const a = z.get(r.target);
27
+ if (a) {
28
+ a.visible = r.isIntersecting;
29
+ for (const n of a.insts) n.onScreen = r.isIntersecting;
30
+ }
31
+ }
32
+ }));
33
+ const t = i.el.parentElement || i.el;
34
+ let e = z.get(t);
35
+ e || (e = { visible: !0, insts: /* @__PURE__ */ new Set() }, z.set(t, e), O.observe(t)), e.insts.add(i), Y.set(i, t), i.onScreen = e.visible;
36
+ }
37
+ function _(i) {
38
+ const t = Y.get(i);
39
+ if (!t) return;
40
+ Y.delete(i);
41
+ const e = z.get(t);
42
+ e && (e.insts.delete(i), e.insts.size === 0 && (z.delete(t), O.unobserve(t)));
43
+ }
44
+ var F, l, J, K, y, L, c, Q, U, S, X, E;
45
+ const f = class f {
46
+ constructor({
47
+ element: t,
48
+ x: e = null,
49
+ y: s = null,
50
+ speed: r = 0.01,
51
+ seed: a = 0,
52
+ lerpSpeed: n = 0.1,
53
+ preserveBaseTranslation: o = !1,
54
+ // <-- si querés mantener la traslación base
55
+ amplitude: u = 1,
56
+ target: m = null,
57
+ // {x, y} en px, offset desde la posición base
58
+ legacyNoise: g = !1,
59
+ // true = sample noise at y=0/100 like <=0.0.6 (flat spots on one axis)
60
+ pauseOffscreen: R = !0
61
+ // false = keep stepping even when the parent box is out of view
62
+ }) {
63
+ B(this, c);
64
+ B(this, F, 1);
65
+ var b, x, T, d;
66
+ if (this._dirty = !1, this.pauseOffscreen = R !== !1, this._ny = g ? [0, 100] : [0.5, 100.5], this.amplitude = u, this.setTarget(m), this.el = t, this.xRange = e, this.yRange = s, this.speed = r, this.seed = a, this.time = a, this.isPlaying = !0, this.lerpFactor = 1, this.lerpSpeed = n, this.targetLerpFactor = 1, this.lastX = 0, this.lastY = 0, this.preserveBaseTranslation = o, this.hasPercentages = !!(e && (h(b = f, l, y).call(b, e.min) || h(x = f, l, y).call(x, e.max)) || s && (h(T = f, l, y).call(T, s.min) || h(d = f, l, y).call(d, s.max))), t) {
67
+ const p = window.getComputedStyle(t);
68
+ p.position === "static" && (t.style.position = "relative"), this.originalInlineTransform = t.style.transform || "", this.baseTransform = h(this, c, E).call(this, p.transform, this.preserveBaseTranslation), this.hasPercentages && (this.handleResize = this.handleResize.bind(this), window.addEventListener("resize", this.handleResize));
69
+ } else
70
+ this.originalInlineTransform = "", this.baseTransform = "";
71
+ this.onScreen = !0, this.init(a);
72
+ }
73
+ init(t) {
74
+ var e;
75
+ this._destroyed = !1, this.noise = h(this, c, Q).call(this, h(e = f, l, K).call(e, t)), this.isPlaying = !0, this.lerpFactor = 1, this.targetLerpFactor = 1, V(this), $(this);
76
+ }
77
+ // 0 = clavado en target (o 0,0), 1 = deriva libre; siempre clampeado a [0, 1], NaN cuenta como 1
78
+ get amplitude() {
79
+ return M(this, F);
80
+ }
81
+ set amplitude(t) {
82
+ const e = typeof t == "number" && !Number.isNaN(t) ? Math.min(1, Math.max(0, t)) : 1;
83
+ e !== M(this, F) && (this._dirty = !0), D(this, F, e);
84
+ }
85
+ setTarget(t) {
86
+ const e = this.target;
87
+ if (t === null || typeof t != "object")
88
+ this.target = null;
89
+ else {
90
+ const r = (a) => typeof a == "number" && Number.isFinite(a) ? a : 0;
91
+ this.target = { x: r(t.x), y: r(t.y) };
92
+ }
93
+ const s = this.target;
94
+ ((e == null ? void 0 : e.x) !== (s == null ? void 0 : s.x) || (e == null ? void 0 : e.y) !== (s == null ? void 0 : s.y)) && (this._dirty = !0);
95
+ }
96
+ // Permite refrescar el transform base si cambian clases/estilos en runtime
97
+ refreshBaseTransform(t = this.preserveBaseTranslation) {
98
+ if (!this.el) return;
99
+ const e = window.getComputedStyle(this.el);
100
+ this.baseTransform = h(this, c, E).call(this, e.transform, t);
101
+ }
102
+ // Handle window resize for percentage-based values
103
+ handleResize() {
104
+ }
105
+ pause() {
106
+ this.isPlaying && (this.targetLerpFactor = 0, this.isPlaying = !1);
107
+ }
108
+ play() {
109
+ this.isPlaying || (this.targetLerpFactor = 1, this.isPlaying = !0, this.lerpFactor === 0 && $(this));
110
+ }
111
+ // Un frame de este instance; devuelve false cuando queda totalmente pausado
112
+ step() {
113
+ var t, e, s, r, a, n, o, u, m, g, R, b;
114
+ if (this.lerpFactor !== this.targetLerpFactor && (this.lerpFactor = h(this, c, U).call(this, this.lerpFactor, this.targetLerpFactor, this.lerpSpeed), Math.abs(this.lerpFactor - this.targetLerpFactor) < 1e-3 && (this.lerpFactor = this.targetLerpFactor)), this.lerpFactor > 0) {
115
+ this.time += this.speed * this.lerpFactor;
116
+ const x = this.noise.perlin2(this.time, this._ny[0]), T = this.noise.perlin2(this.time, this._ny[1]);
117
+ if (this.el) {
118
+ let d, p, P, A;
119
+ if (this.hasPercentages) {
120
+ const j = this.el.parentElement || document.body, k = j.clientWidth, q = j.clientHeight;
121
+ this.xRange && (d = h(t = f, l, y).call(t, this.xRange.min) ? h(e = f, l, L).call(e, this.xRange.min, k) : this.xRange.min, p = h(s = f, l, y).call(s, this.xRange.max) ? h(r = f, l, L).call(r, this.xRange.max, k) : this.xRange.max), this.yRange && (P = h(a = f, l, y).call(a, this.yRange.min) ? h(n = f, l, L).call(n, this.yRange.min, q) : this.yRange.min, A = h(o = f, l, y).call(o, this.yRange.max) ? h(u = f, l, L).call(u, this.yRange.max, q) : this.yRange.max);
122
+ } else
123
+ d = (m = this.xRange) == null ? void 0 : m.min, p = (g = this.xRange) == null ? void 0 : g.max, P = (R = this.yRange) == null ? void 0 : R.min, A = (b = this.yRange) == null ? void 0 : b.max;
124
+ const N = h(this, c, S).call(this, this.xRange ? h(this, c, X).call(this, x, -1, 1, d, p) : 0, "x"), W = h(this, c, S).call(this, this.yRange ? h(this, c, X).call(this, T, -1, 1, P, A) : 0, "y");
125
+ this.lastX = N, this.lastY = W, this.el.style.transform = `translate3d(${N}px, ${W}px, 0) ${this.baseTransform}`.trim();
126
+ } else {
127
+ const d = h(this, c, S).call(this, this.xRange ? h(this, c, X).call(this, x, -1, 1, this.xRange.min, this.xRange.max) : 0, "x"), p = h(this, c, S).call(this, this.yRange ? h(this, c, X).call(this, T, -1, 1, this.yRange.min, this.yRange.max) : 0, "y");
128
+ this.lastX = d, this.lastY = p;
129
+ }
130
+ }
131
+ return this.lerpFactor > 0 || this.targetLerpFactor > 0;
132
+ }
133
+ // Back to the starting state, as if just created: the noise path rewinds to its seed and the
134
+ // element returns to its own transform (no drift). Keeps play/pause, amplitude and target.
135
+ reset() {
136
+ this._destroyed || (this.time = this.seed, this.lastX = 0, this.lastY = 0, this.el && (this.el.style.transform = this.originalInlineTransform || ""), this.isPlaying && $(this));
137
+ }
138
+ destroy() {
139
+ this._destroyed = !0, Z(this), _(this), this.el && (this.el.style.transform = this.originalInlineTransform || ""), this.hasPercentages && window.removeEventListener("resize", this.handleResize);
140
+ }
141
+ };
142
+ F = new WeakMap(), l = new WeakSet(), J = function(t) {
143
+ return function() {
144
+ let e = t += 1831565813;
145
+ return e = Math.imul(e ^ e >>> 15, e | 1), e ^= e + Math.imul(e ^ e >>> 7, e | 61), ((e ^ e >>> 14) >>> 0) / 4294967296;
146
+ };
147
+ }, K = function(t = 0) {
148
+ var r;
149
+ const e = Array.from({ length: 256 }, (a, n) => n), s = h(r = f, l, J).call(r, t);
150
+ for (let a = 255; a > 0; a--) {
151
+ const n = Math.floor(s() * (a + 1));
152
+ [e[a], e[n]] = [e[n], e[a]];
153
+ }
154
+ return e.concat(e);
155
+ }, y = function(t) {
156
+ return typeof t == "string" && t.endsWith("%");
157
+ }, L = function(t, e) {
158
+ var r;
159
+ return h(r = f, l, y).call(r, t) ? parseFloat(t) / 100 * e : t;
160
+ }, c = new WeakSet(), Q = function(t) {
161
+ const e = (n) => n * n * n * (n * (n * 6 - 15) + 10), s = (n, o, u) => o + n * (u - o), r = (n, o, u) => {
162
+ const m = n & 15, g = m < 8 ? o : u, R = m < 4 ? u : m === 12 || m === 14 ? o : 0;
163
+ return (m & 1 ? -g : g) + (m & 2 ? -R : R);
164
+ }, a = (n, o) => n & 1 ? -o : o;
165
+ return {
166
+ // Original 1D Perlin noise function
167
+ perlin1(n) {
168
+ const o = Math.floor(n) & 255;
169
+ n -= Math.floor(n);
170
+ const u = e(n);
171
+ return s(u, a(t[o], n), a(t[o + 1], n - 1));
172
+ },
173
+ // New 2D Perlin noise function
174
+ perlin2(n, o) {
175
+ const u = Math.floor(n) & 255, m = Math.floor(o) & 255;
176
+ n -= Math.floor(n), o -= Math.floor(o);
177
+ const g = e(n), R = e(o), b = t[u] + m, x = t[u + 1] + m, T = t[b], d = t[b + 1], p = t[x], P = t[x + 1];
178
+ return s(
179
+ R,
180
+ s(g, r(t[T], n, o), r(t[p], n - 1, o)),
181
+ s(g, r(t[d], n, o - 1), r(t[P], n - 1, o - 1))
182
+ );
183
+ }
184
+ };
185
+ }, U = function(t, e, s) {
186
+ return t + s * (e - t);
187
+ }, // target + (noise - target) * amplitude; amplitude 1 returns noise untouched (exact legacy values)
188
+ S = function(t, e) {
189
+ var r;
190
+ if (M(this, F) === 1) return t;
191
+ const s = ((r = this.target) == null ? void 0 : r[e]) ?? 0;
192
+ return s + (t - s) * M(this, F);
193
+ }, X = function(t, e, s, r, a) {
194
+ return (t - e) * (a - r) / (s - e) + r;
195
+ }, // Quita o preserva la traslación del transform base en 2D según flag
196
+ E = function(t, e = !1) {
197
+ if (!t || t === "none") return "";
198
+ try {
199
+ const s = new DOMMatrixReadOnly(t);
200
+ if (s.is2D) {
201
+ const r = e ? s.e : 0, a = e ? s.f : 0;
202
+ return `matrix(${s.a}, ${s.b}, ${s.c}, ${s.d}, ${r}, ${a})`;
203
+ } else
204
+ return t;
205
+ } catch {
206
+ return t;
207
+ }
208
+ }, B(f, l);
209
+ let H = f;
210
+ export {
211
+ H as default
212
+ };
@@ -0,0 +1 @@
1
+ (function(r,o){typeof exports=="object"&&typeof module<"u"?module.exports=o():typeof define=="function"&&define.amd?define(o):(r=typeof globalThis<"u"?globalThis:r||self,r.PerlinDOM=o())})(this,function(){"use strict";var H=r=>{throw TypeError(r)};var N=(r,o,d)=>o.has(r)||H("Cannot "+d);var X=(r,o,d)=>(N(r,o,"read from private field"),d?d.call(r):o.get(r)),I=(r,o,d)=>o.has(r)?H("Cannot add the same private member more than once"):o instanceof WeakSet?o.add(r):o.set(r,d),G=(r,o,d,M)=>(N(r,o,"write to private field"),M?M.call(r,d):o.set(r,d),d),h=(r,o,d)=>(N(r,o,"access private method"),d);var v,f,J,K,x,z,u,Q,U,B,Y,W;const r=new Set;let o=null;function d(){o=null;for(const c of[...r])r.has(c)&&(!c.onScreen&&!c._dirty||(c._dirty=!1,c.step()===!1&&r.delete(c)));r.size>0&&o===null&&(o=requestAnimationFrame(d))}function M(c){r.add(c),o===null&&(o=requestAnimationFrame(d))}function Z(c){r.delete(c),r.size===0&&o!==null&&(cancelAnimationFrame(o),o=null)}const L=new Map,A=new WeakMap;let $=null;function V(c){if(typeof IntersectionObserver>"u"||!c.el||!c.pauseOffscreen||A.has(c))return;$??($=new IntersectionObserver(s=>{for(const n of s){const a=L.get(n.target);if(a){a.visible=n.isIntersecting;for(const i of a.insts)i.onScreen=n.isIntersecting}}}));const t=c.el.parentElement||c.el;let e=L.get(t);e||(e={visible:!0,insts:new Set},L.set(t,e),$.observe(t)),e.insts.add(c),A.set(c,t),c.onScreen=e.visible}function _(c){const t=A.get(c);if(!t)return;A.delete(c);const e=L.get(t);e&&(e.insts.delete(c),e.insts.size===0&&(L.delete(t),$.unobserve(t)))}const m=class m{constructor({element:t,x:e=null,y:s=null,speed:n=.01,seed:a=0,lerpSpeed:i=.1,preserveBaseTranslation:l=!1,amplitude:g=1,target:p=null,legacyNoise:y=!1,pauseOffscreen:F=!0}){I(this,u);I(this,v,1);var T,w,P,R;if(this._dirty=!1,this.pauseOffscreen=F!==!1,this._ny=y?[0,100]:[.5,100.5],this.amplitude=g,this.setTarget(p),this.el=t,this.xRange=e,this.yRange=s,this.speed=n,this.seed=a,this.time=a,this.isPlaying=!0,this.lerpFactor=1,this.lerpSpeed=i,this.targetLerpFactor=1,this.lastX=0,this.lastY=0,this.preserveBaseTranslation=l,this.hasPercentages=!!(e&&(h(T=m,f,x).call(T,e.min)||h(w=m,f,x).call(w,e.max))||s&&(h(P=m,f,x).call(P,s.min)||h(R=m,f,x).call(R,s.max))),t){const b=window.getComputedStyle(t);b.position==="static"&&(t.style.position="relative"),this.originalInlineTransform=t.style.transform||"",this.baseTransform=h(this,u,W).call(this,b.transform,this.preserveBaseTranslation),this.hasPercentages&&(this.handleResize=this.handleResize.bind(this),window.addEventListener("resize",this.handleResize))}else this.originalInlineTransform="",this.baseTransform="";this.onScreen=!0,this.init(a)}init(t){var e;this._destroyed=!1,this.noise=h(this,u,Q).call(this,h(e=m,f,K).call(e,t)),this.isPlaying=!0,this.lerpFactor=1,this.targetLerpFactor=1,V(this),M(this)}get amplitude(){return X(this,v)}set amplitude(t){const e=typeof t=="number"&&!Number.isNaN(t)?Math.min(1,Math.max(0,t)):1;e!==X(this,v)&&(this._dirty=!0),G(this,v,e)}setTarget(t){const e=this.target;if(t===null||typeof t!="object")this.target=null;else{const n=a=>typeof a=="number"&&Number.isFinite(a)?a:0;this.target={x:n(t.x),y:n(t.y)}}const s=this.target;((e==null?void 0:e.x)!==(s==null?void 0:s.x)||(e==null?void 0:e.y)!==(s==null?void 0:s.y))&&(this._dirty=!0)}refreshBaseTransform(t=this.preserveBaseTranslation){if(!this.el)return;const e=window.getComputedStyle(this.el);this.baseTransform=h(this,u,W).call(this,e.transform,t)}handleResize(){}pause(){this.isPlaying&&(this.targetLerpFactor=0,this.isPlaying=!1)}play(){this.isPlaying||(this.targetLerpFactor=1,this.isPlaying=!0,this.lerpFactor===0&&M(this))}step(){var t,e,s,n,a,i,l,g,p,y,F,T;if(this.lerpFactor!==this.targetLerpFactor&&(this.lerpFactor=h(this,u,U).call(this,this.lerpFactor,this.targetLerpFactor,this.lerpSpeed),Math.abs(this.lerpFactor-this.targetLerpFactor)<.001&&(this.lerpFactor=this.targetLerpFactor)),this.lerpFactor>0){this.time+=this.speed*this.lerpFactor;const w=this.noise.perlin2(this.time,this._ny[0]),P=this.noise.perlin2(this.time,this._ny[1]);if(this.el){let R,b,S,E;if(this.hasPercentages){const k=this.el.parentElement||document.body,q=k.clientWidth,C=k.clientHeight;this.xRange&&(R=h(t=m,f,x).call(t,this.xRange.min)?h(e=m,f,z).call(e,this.xRange.min,q):this.xRange.min,b=h(s=m,f,x).call(s,this.xRange.max)?h(n=m,f,z).call(n,this.xRange.max,q):this.xRange.max),this.yRange&&(S=h(a=m,f,x).call(a,this.yRange.min)?h(i=m,f,z).call(i,this.yRange.min,C):this.yRange.min,E=h(l=m,f,x).call(l,this.yRange.max)?h(g=m,f,z).call(g,this.yRange.max,C):this.yRange.max)}else R=(p=this.xRange)==null?void 0:p.min,b=(y=this.xRange)==null?void 0:y.max,S=(F=this.yRange)==null?void 0:F.min,E=(T=this.yRange)==null?void 0:T.max;const j=h(this,u,B).call(this,this.xRange?h(this,u,Y).call(this,w,-1,1,R,b):0,"x"),D=h(this,u,B).call(this,this.yRange?h(this,u,Y).call(this,P,-1,1,S,E):0,"y");this.lastX=j,this.lastY=D,this.el.style.transform=`translate3d(${j}px, ${D}px, 0) ${this.baseTransform}`.trim()}else{const R=h(this,u,B).call(this,this.xRange?h(this,u,Y).call(this,w,-1,1,this.xRange.min,this.xRange.max):0,"x"),b=h(this,u,B).call(this,this.yRange?h(this,u,Y).call(this,P,-1,1,this.yRange.min,this.yRange.max):0,"y");this.lastX=R,this.lastY=b}}return this.lerpFactor>0||this.targetLerpFactor>0}reset(){this._destroyed||(this.time=this.seed,this.lastX=0,this.lastY=0,this.el&&(this.el.style.transform=this.originalInlineTransform||""),this.isPlaying&&M(this))}destroy(){this._destroyed=!0,Z(this),_(this),this.el&&(this.el.style.transform=this.originalInlineTransform||""),this.hasPercentages&&window.removeEventListener("resize",this.handleResize)}};v=new WeakMap,f=new WeakSet,J=function(t){return function(){let e=t+=1831565813;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}},K=function(t=0){var n;const e=Array.from({length:256},(a,i)=>i),s=h(n=m,f,J).call(n,t);for(let a=255;a>0;a--){const i=Math.floor(s()*(a+1));[e[a],e[i]]=[e[i],e[a]]}return e.concat(e)},x=function(t){return typeof t=="string"&&t.endsWith("%")},z=function(t,e){var n;return h(n=m,f,x).call(n,t)?parseFloat(t)/100*e:t},u=new WeakSet,Q=function(t){const e=i=>i*i*i*(i*(i*6-15)+10),s=(i,l,g)=>l+i*(g-l),n=(i,l,g)=>{const p=i&15,y=p<8?l:g,F=p<4?g:p===12||p===14?l:0;return(p&1?-y:y)+(p&2?-F:F)},a=(i,l)=>i&1?-l:l;return{perlin1(i){const l=Math.floor(i)&255;i-=Math.floor(i);const g=e(i);return s(g,a(t[l],i),a(t[l+1],i-1))},perlin2(i,l){const g=Math.floor(i)&255,p=Math.floor(l)&255;i-=Math.floor(i),l-=Math.floor(l);const y=e(i),F=e(l),T=t[g]+p,w=t[g+1]+p,P=t[T],R=t[T+1],b=t[w],S=t[w+1];return s(F,s(y,n(t[P],i,l),n(t[b],i-1,l)),s(y,n(t[R],i,l-1),n(t[S],i-1,l-1)))}}},U=function(t,e,s){return t+s*(e-t)},B=function(t,e){var n;if(X(this,v)===1)return t;const s=((n=this.target)==null?void 0:n[e])??0;return s+(t-s)*X(this,v)},Y=function(t,e,s,n,a){return(t-e)*(a-n)/(s-e)+n},W=function(t,e=!1){if(!t||t==="none")return"";try{const s=new DOMMatrixReadOnly(t);if(s.is2D){const n=e?s.e:0,a=e?s.f:0;return`matrix(${s.a}, ${s.b}, ${s.c}, ${s.d}, ${n}, ${a})`}else return t}catch{return t}},I(m,f);let O=m;return O});
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "@andresclua/perlindom",
3
3
  "private": false,
4
- "version": "0.0.6",
4
+ "version": "0.1.0",
5
5
  "description": "A lightweight JavaScript library for creating smooth, organic animations for DOM elements using Perlin noise",
6
+ "homepage": "https://perlindom.js.org",
7
+ "repository": { "type": "git", "url": "git+https://github.com/andresclua/PerlinDOM.git" },
8
+ "bugs": "https://github.com/andresclua/PerlinDOM/issues",
9
+ "files": ["dist", "CHANGELOG.md"],
6
10
  "type": "module",
7
- "main": "./dist/PerlinDOM.umd.js",
8
- "module": "./dist/PerlinDOM.es.js",
11
+ "main": "./dist/perlindom.umd.js",
12
+ "module": "./dist/perlindom.es.js",
9
13
  "exports": {
10
14
  ".": {
11
- "import": "./dist/PerlinDOM.es.js",
12
- "require": "./dist/PerlinDOM.umd.js"
15
+ "import": "./dist/perlindom.es.js",
16
+ "require": "./dist/perlindom.umd.js"
13
17
  }
14
18
  },
15
19
  "keywords": [
@@ -24,9 +28,13 @@
24
28
  "scripts": {
25
29
  "dev": "vite",
26
30
  "build": "vite build",
27
- "preview": "vite preview"
31
+ "build:lib": "vite build",
32
+ "build:site": "vite build --mode site",
33
+ "preview": "vite preview",
34
+ "test": "node --test 'tests/*.test.mjs'"
28
35
  },
29
36
  "devDependencies": {
37
+ "gsap": "^3.15.1",
30
38
  "sass": "^1.80.6",
31
39
  "vite": "^5.4.10"
32
40
  }
package/readme.md CHANGED
@@ -12,11 +12,16 @@ A lightweight JavaScript library for creating smooth, organic animations on DOM
12
12
  - [Installation](#-installation)
13
13
  - [Quick Start](#-quick-start)
14
14
  - [Configuration](#-configuration)
15
+ - [Amplitude and target](#-amplitude-and-target)
16
+ - [Use with GSAP + ScrollTrigger](#-use-with-gsap--scrolltrigger)
15
17
  - [Examples](#-examples)
16
18
  - [Best Practices](#-best-practices)
17
19
  - [API Reference](#-api-reference)
18
20
  - [Troubleshooting](#-troubleshooting)
19
21
  - [Browser Compatibility](#-browser-compatibility)
22
+ - [Live site](#-live-site)
23
+ - [Contributing](#-contributing)
24
+ - [Sponsor](#-sponsor)
20
25
  - [Changelog](#-changelog)
21
26
  - [License](#-license)
22
27
 
@@ -25,7 +30,8 @@ A lightweight JavaScript library for creating smooth, organic animations on DOM
25
30
  - **🌊 Organic Animations**: Natural movements using Perlin noise that avoid mechanical rigidity
26
31
  - **📱 Responsive Design**: Support for percentage-based values that adapt to container size
27
32
  - **🎯 Smart Positioning**: Automatically works from element's current position (CSS or inline styles)
28
- - **⚡ High Performance**: Uses `transform` and `requestAnimationFrame` for smooth 60fps animations
33
+ - **⚡ High Performance**: Uses `transform` and one shared `requestAnimationFrame` loop for all instances; instances scrolled out of the viewport pause automatically
34
+ - **🧲 Amplitude & Target**: Scale the drift from 0 to 1 and pin elements to a target position, ideal for scroll-driven effects
29
35
  - **🎛️ Full Control**: Play, pause, and destroy animations with smooth transitions
30
36
  - **📦 Lightweight**: Minimal footprint with zero dependencies
31
37
  - **🔧 Easy Integration**: Simple API that works with any DOM element
@@ -62,35 +68,85 @@ const animation = new PerlinDOM({
62
68
  // Control the animation
63
69
  animation.pause(); // Smooth pause
64
70
  animation.play(); // Resume
71
+ animation.reset(); // Rewind to the start (same path again)
65
72
  animation.destroy(); // Clean up
66
73
  ```
67
74
 
68
- ```css
69
- /* Your element needs positioning for transforms to work */
70
- .my-element {
71
- position: relative; /* or absolute, or fixed */
72
- /* PerlinDOM will animate from wherever this element is positioned */
73
- }
74
- ```
75
+ 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.
75
76
 
76
77
  ## ⚙️ Configuration
77
78
 
78
79
  | Option | Type | Default | Description |
79
80
  |--------|------|---------|-------------|
80
- | `element` | `HTMLElement` | **required** | The DOM element to animate |
81
+ | `element` | `HTMLElement \| null` | `undefined` | The DOM element to animate. Optional: with `null` only `lastX` / `lastY` are computed |
81
82
  | `x` | `Object \| null` | `null` | Horizontal movement: `{min: number, max: number}` |
82
83
  | `y` | `Object \| null` | `null` | Vertical movement: `{min: number, max: number}` |
83
84
  | `speed` | `number` | `0.01` | Animation speed (lower = slower) |
84
85
  | `seed` | `number` | `0` | Seed for reproducible patterns |
85
86
  | `lerpSpeed` | `number` | `0.1` | Transition smoothness for pause/play |
87
+ | `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 |
88
+ | `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) |
89
+ | `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` |
90
+ | `legacyNoise` | `boolean` | `false` | `true` keeps the old noise rows (0 / 100) for movement identical to 0.0.6 and earlier |
91
+ | `pauseOffscreen` | `boolean` | `true` | Skip frames while the element's parent is outside the viewport. `false` keeps it running everywhere |
86
92
 
87
93
  ### Value Types
88
94
 
89
95
  - **Pixels**: `{ min: -50, max: 50 }` - Fixed pixel movement
90
- - **Percentages**: `{ min: '-10%', max: '10%' }` - Responsive to parent size
96
+ - **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`)
91
97
  - **Mixed**: `x: { min: '-5%', max: '5%' }, y: { min: -20, max: 20 }`
92
98
  - **Single Axis**: Set unused axis to `null`
93
99
 
100
+ ## 🎚️ Amplitude and Target
101
+
102
+ `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.
103
+
104
+ ```javascript
105
+ const dot = new PerlinDOM({
106
+ element: document.querySelector('.dot'),
107
+ x: { min: -120, max: 120 },
108
+ y: { min: -120, max: 120 },
109
+ target: { x: 40, y: -20 }
110
+ });
111
+
112
+ dot.amplitude = 0.5; // half the drift, pulled toward the target
113
+ dot.amplitude = 0; // pinned at { x: 40, y: -20 }
114
+ dot.setTarget({ x: 0, y: 0 }); // move the target; setTarget(null) removes it
115
+ dot.amplitude = 1; // free drift again
116
+ ```
117
+
118
+ ## 🎬 Use with GSAP + ScrollTrigger
119
+
120
+ 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.
121
+
122
+ ```javascript
123
+ import gsap from 'gsap';
124
+ import { ScrollTrigger } from 'gsap/ScrollTrigger';
125
+ import PerlinDOM from '@andresclua/perlindom';
126
+
127
+ gsap.registerPlugin(ScrollTrigger);
128
+
129
+ const shape = [{ x: 0, y: -80 }, { x: 70, y: 40 }, { x: -70, y: 40 }]; // one { x, y } per .dot
130
+
131
+ const instances = [...document.querySelectorAll('.dot')].map((el, i) =>
132
+ new PerlinDOM({
133
+ element: el,
134
+ x: { min: -120, max: 120 },
135
+ y: { min: -120, max: 120 },
136
+ seed: i * 7919,
137
+ target: shape[i], // { x, y } offsets that draw your shape
138
+ })
139
+ );
140
+
141
+ const state = { amplitude: 1 };
142
+ gsap.to(state, {
143
+ amplitude: 0,
144
+ ease: 'none',
145
+ scrollTrigger: { trigger: '.scene', start: 'top top', end: '+=1000', pin: true, scrub: 0.8 },
146
+ onUpdate: () => instances.forEach(inst => { inst.amplitude = state.amplitude; }),
147
+ });
148
+ ```
149
+
94
150
  ## 📚 Examples
95
151
 
96
152
  ### 1. Basic Element Animation
@@ -104,7 +160,7 @@ animation.destroy(); // Clean up
104
160
  position: relative;
105
161
  left: 50%;
106
162
  top: 50%;
107
- transform: translate(-50%, -50%);
163
+ transform: translate(-50%, -50%); /* centring translate */
108
164
  }
109
165
  ```
110
166
 
@@ -114,7 +170,8 @@ new PerlinDOM({
114
170
  x: { min: -20, max: 20 },
115
171
  y: { min: -15, max: 15 },
116
172
  speed: 0.008,
117
- seed: 42
173
+ seed: 42,
174
+ preserveBaseTranslation: true // keep the centring translate
118
175
  });
119
176
  ```
120
177
 
@@ -140,13 +197,17 @@ const button = document.querySelector('.animated-button');
140
197
  let animation;
141
198
 
142
199
  button.addEventListener('mouseenter', () => {
143
- animation = new PerlinDOM({
144
- element: button.querySelector('.effect'),
145
- x: { min: -5, max: 5 },
146
- y: { min: -5, max: 5 },
147
- speed: 0.02,
148
- lerpSpeed: 0.15
149
- });
200
+ if (!animation) {
201
+ animation = new PerlinDOM({
202
+ element: button.querySelector('.effect'),
203
+ x: { min: -5, max: 5 },
204
+ y: { min: -5, max: 5 },
205
+ speed: 0.02,
206
+ lerpSpeed: 0.15
207
+ });
208
+ } else {
209
+ animation.play();
210
+ }
150
211
  });
151
212
 
152
213
  button.addEventListener('mouseleave', () => {
@@ -218,24 +279,24 @@ updatePath();
218
279
 
219
280
  ### ✅ Do
220
281
 
221
- - **Use CSS for initial positioning**: Position elements with CSS classes rather than inline styles
222
- - **Set proper positioning**: Ensure elements have `position: relative`, `absolute`, or `fixed`
282
+ - **Position with CSS, not with `translate`**: Use `left`/`top`, margins, grid or flex. A CSS `translate` is dropped unless you pass `preserveBaseTranslation: true`
283
+ - **One instance per element**: Two instances on the same element overwrite each other's transform
223
284
  - **Use percentage values for responsive designs**: Great for mobile-friendly animations
224
285
  - **Choose appropriate speeds**: Start with `0.01` and adjust based on desired effect
225
286
  - **Clean up animations**: Call `destroy()` when removing elements from DOM
226
287
 
227
288
  ### ❌ Don't
228
289
 
229
- - **Don't rely on inline positioning**: PerlinDOM works from any position, but CSS is cleaner
230
- - **Don't use on static elements**: Transform won't work without proper positioning
231
- - **Don't forget to destroy**: Memory leaks can occur if animations aren't cleaned up
232
- - **Don't use extreme speeds**: Very high speeds can cause jittery animations
290
+ - **Don't tween `transform` with another library on the same element**: PerlinDOM writes it every frame (see the GSAP recipe above)
291
+ - **Don't forget to destroy**: A live instance keeps a reference to its element, so call `destroy()` before removing it
292
+ - **Don't use extreme speeds**: Above about `0.05` the motion turns jittery
293
+ - **Don't forget reduced motion**: PerlinDOM does not check `prefers-reduced-motion`, so skip creating instances when it is set
233
294
 
234
295
  ### 🎯 Performance Tips
235
296
 
236
297
  ```javascript
237
- // Good: Reuse animation instances
238
- const sharedAnimation = new PerlinDOM({...});
298
+ // Good: Many instances are cheap, they all share one requestAnimationFrame loop
299
+ // and pause on their own while scrolled out of view
239
300
 
240
301
  // Good: Use percentage values for responsive design
241
302
  x: { min: '-5%', max: '5%' }
@@ -252,17 +313,20 @@ animation.destroy();
252
313
  ### Constructor
253
314
 
254
315
  ```javascript
255
- new PerlinDOM(options)
316
+ new PerlinDOM(options) // options: see Configuration
256
317
  ```
257
318
 
258
319
  ### Methods
259
320
 
260
321
  | Method | Description | Returns |
261
322
  |--------|-------------|---------|
262
- | `play()` | Start or resume animation | `void` |
323
+ | `play()` | Resume after `pause()` (instances start playing on construction) | `void` |
263
324
  | `pause()` | Smoothly pause animation | `void` |
264
- | `destroy()` | Stop animation and clean up resources | `void` |
265
- | `init(seed?)` | Reinitialize with optional new seed | `void` |
325
+ | `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` |
326
+ | `destroy()` | Stop animation, restore the original inline `transform` and clean up | `void` |
327
+ | `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` |
328
+ | `setTarget({x, y} \| null)` | Set the target offset in px; non-finite coordinates become `0`, the object is copied, `null` removes it | `void` |
329
+ | `refreshBaseTransform(preserveTranslation?)` | Re-read the element's CSS transform after classes or styles changed | `void` |
266
330
 
267
331
  ### Properties
268
332
 
@@ -273,22 +337,22 @@ new PerlinDOM(options)
273
337
  | `isPlaying` | `boolean` | Current animation state |
274
338
  | `speed` | `number` | Animation speed (writable) |
275
339
  | `lerpSpeed` | `number` | Transition speed (writable) |
340
+ | `amplitude` | `number` | Drift strength, clamped to `[0, 1]`, non-numbers reset to `1` (writable) |
341
+ | `target` | `{x, y} \| null` | Current target offset (set it with `setTarget()`) |
342
+ | `xRange` / `yRange` | `Object \| null` | Movement ranges (writable; whether percentages are used is fixed at construction) |
276
343
 
277
344
  ## 🔧 Troubleshooting
278
345
 
279
346
  ### Common Issues
280
347
 
281
348
  **❓ Animation not visible**
282
- ```css
283
- /* Ensure element has positioning */
284
- .my-element {
285
- position: relative; /* or absolute/fixed */
286
- }
287
- ```
349
+ - Set at least one of `x` / `y`
350
+ - The element must not be `display: inline` (e.g. a plain `<span>`): transforms do not apply to inline boxes. Use `inline-block` or `block`
351
+ - Static positioning is not the problem: PerlinDOM sets `position: relative` itself
288
352
 
289
353
  **❓ Element jumps on initialization**
290
- - PerlinDOM automatically detects current position
291
- - If element has conflicting inline styles, consider moving to CSS
354
+ - Usually a CSS `translate`, such as `translate(-50%, -50%)` for centring, which PerlinDOM drops by default
355
+ - Pass `preserveBaseTranslation: true`, or centre with `left: calc(50% - half)` or margins
292
356
 
293
357
  **❓ Animation too fast/slow**
294
358
  ```javascript
@@ -300,7 +364,10 @@ speed: 0.1 // Fast
300
364
 
301
365
  **❓ Percentage values not working**
302
366
  - Ensure parent container has defined dimensions
303
- - PerlinDOM calculates percentages based on parent size
367
+ - Percentages use the parent's `clientWidth` / `clientHeight`, and need an `element`
368
+
369
+ **❓ Element stops moving while off screen**
370
+ - 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
304
371
 
305
372
  **❓ Memory leaks**
306
373
  ```javascript
@@ -316,19 +383,23 @@ Monitor performance in DevTools:
316
383
 
317
384
  ## 🌐 Browser Compatibility
318
385
 
319
- PerlinDOM works in all modern browsers supporting:
386
+ 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+.
387
+
388
+ - `requestAnimationFrame`, `getComputedStyle` and CSS transforms are required
389
+ - `DOMMatrixReadOnly` separates the CSS translate from scale/rotate/skew; without it the computed transform is used as it is
390
+ - `IntersectionObserver` powers the offscreen pause; without it everything keeps running
391
+
392
+ ## 🔗 Live site
393
+
394
+ 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`)
395
+
396
+ ## 🤝 Contributing
320
397
 
321
- - ✅ **ES6 Modules** (2015+)
322
- - ✅ **requestAnimationFrame** (IE10+)
323
- - ✅ **CSS Transforms** (IE9+)
324
- - ✅ **getComputedStyle** (IE9+)
398
+ 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).
325
399
 
326
- ### Tested Browsers
400
+ ## 💜 Sponsor
327
401
 
328
- - Chrome 60+
329
- - Firefox 55+
330
- - Safari 12+
331
- - Edge 79+
402
+ PerlinDOM is free and MIT licensed. If it saves you time, you can support its development through [GitHub Sponsors](https://github.com/sponsors/andresclua).
332
403
 
333
404
  ## 📝 Changelog
334
405