fancoolo-fx 1.7.1 → 1.8.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/readme.txt +12 -0
  3. package/src/fx.js +76 -141
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fancoolo-fx",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "A class-driven GSAP animation wrapper for WordPress and static sites.",
5
5
  "main": "src/fx.js",
6
6
  "homepage": "https://krstivoja.github.io/fancoolo-fx/",
package/readme.txt CHANGED
@@ -68,6 +68,18 @@ Yes. Use the `fx-start-[top center]` modifier class, or set `scrollStart` in the
68
68
 
69
69
  == Changelog ==
70
70
 
71
+ = 1.8.1 =
72
+ * Fix: Removed split.revert() calls from textReveal, typeWriter, and splitWords — revert destroys JS state (event listeners, injected DOM) added after SplitText ran
73
+ * Fix: textReveal resize re-splitting fully handled by autoSplit, no manual revert needed
74
+
75
+ = 1.8.0 =
76
+ * Refactor: textReveal uses native SplitText `autoSplit`, `mask: "lines"`, and `onSplit` — removes manual overflow wrappers and resize handler
77
+ * Refactor: Responsive and reduced-motion handling via `gsap.matchMedia()` — animations auto-revert when conditions change
78
+ * Refactor: Idempotent `init()` using persistent WeakSet — safe to call multiple times without double-animating
79
+ * Refactor: Scrub effects (tiltIn, parallax, drawSVG scrub) routed through `buildScrollTrigger()` — now support debug markers and `fx-start-[...]` overrides
80
+ * Removed: Manual resize listener, `_splitRegistry`, `document.fonts.ready` blocking — all handled natively by GSAP
81
+ * Enhancement: `FX.refresh()` simplified to `ScrollTrigger.refresh()`
82
+
71
83
  = 1.7.1 =
72
84
  * Fix: FOUC prevention — all effects now use autoAlpha instead of opacity, elements start with visibility:hidden and are revealed by GSAP
73
85
  * New: WordPress plugin injects visibility:hidden CSS automatically in the head
package/src/fx.js CHANGED
@@ -109,6 +109,11 @@
109
109
  slideIn: { duration: 1, ease: 'power3.out' },
110
110
  };
111
111
 
112
+ // ── State ───────────────────────────────────
113
+
114
+ var _animated = new WeakSet();
115
+ var _mm = gsap.matchMedia();
116
+
112
117
  // ── Helpers ──────────────────────────────────
113
118
 
114
119
  function getClassModifier(el, name, fallback) {
@@ -157,90 +162,36 @@
157
162
  return st;
158
163
  }
159
164
 
160
- // ── SplitText resize handling ───────────────
161
-
162
- var _splitRegistry = [];
163
- var _lastWidth = window.innerWidth;
164
- var _resizeTimer;
165
-
166
- function registerSplit(entry) {
167
- _splitRegistry.push(entry);
168
- }
169
-
170
- function unregisterSplit(entry) {
171
- var idx = _splitRegistry.indexOf(entry);
172
- if (idx > -1) _splitRegistry.splice(idx, 1);
173
- }
174
-
175
- function refreshSplits() {
176
- if (_splitRegistry.length === 0) return;
177
-
178
- var pending = [];
179
-
180
- for (var i = _splitRegistry.length - 1; i >= 0; i--) {
181
- var entry = _splitRegistry[i];
182
- if (entry.tween) entry.tween.kill();
183
- if (entry.split) entry.split.revert();
184
- pending.push(entry);
185
- }
186
-
187
- _splitRegistry.length = 0;
188
-
189
- pending.forEach(function (entry) {
190
- entry.effectFn(entry.el, entry.opts);
191
- });
192
-
193
- ScrollTrigger.refresh();
194
- }
195
-
196
- window.addEventListener('resize', function () {
197
- if (window.innerWidth === _lastWidth) return;
198
- _lastWidth = window.innerWidth;
199
- clearTimeout(_resizeTimer);
200
- _resizeTimer = setTimeout(refreshSplits, 200);
201
- });
202
-
203
165
  // ── Effects ──────────────────────────────────
204
166
 
205
167
  function textReveal(el, opts) {
206
168
  opts = opts || {};
207
169
  var o = resolveOptions(el, 'textReveal', opts);
170
+ var isScroll = opts.trigger === 'scroll' || opts.scrollTrigger;
208
171
 
209
172
  gsap.set(el, { visibility: 'inherit' });
210
- var split = new SplitText(el, { type: 'lines', linesClass: 'line-wrapper' });
211
-
212
- split.lines.forEach(function (line) {
213
- var wrapper = document.createElement('div');
214
- wrapper.style.overflow = 'hidden';
215
- line.parentNode.insertBefore(wrapper, line);
216
- wrapper.appendChild(line);
217
- });
218
-
219
- var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
220
- var entry = { el: el, split: split, tween: null, effectFn: textReveal, opts: opts };
221
-
222
- var tweenVars = {
223
- y: '100%',
224
- autoAlpha: 0,
225
- duration: o.duration,
226
- ease: o.ease,
227
- stagger: o.stagger,
228
- delay: o.delay,
229
- };
230
173
 
231
- if (isOneShot) {
232
- tweenVars.onComplete = function () {
233
- split.revert();
234
- unregisterSplit(entry);
235
- };
236
- }
174
+ SplitText.create(el, {
175
+ type: 'lines',
176
+ mask: 'lines',
177
+ autoSplit: true,
178
+ onSplit: function (self) {
179
+ var tweenVars = {
180
+ y: '100%',
181
+ autoAlpha: 0,
182
+ duration: o.duration,
183
+ ease: o.ease,
184
+ stagger: o.stagger,
185
+ delay: o.delay,
186
+ };
237
187
 
238
- if (opts.trigger === 'scroll' || opts.scrollTrigger) {
239
- tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
240
- }
188
+ if (isScroll) {
189
+ tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
190
+ }
241
191
 
242
- entry.tween = gsap.from(split.lines, tweenVars);
243
- registerSplit(entry);
192
+ return gsap.from(self.lines, tweenVars);
193
+ },
194
+ });
244
195
  }
245
196
 
246
197
  function reveal(el, opts) {
@@ -400,6 +351,11 @@
400
351
  opts = opts || {};
401
352
  var o = resolveOptions(el, 'tiltIn', opts);
402
353
 
354
+ var st = buildScrollTrigger(el, opts.scrollTrigger || {});
355
+ st.end = opts.end || 'top 20%';
356
+ st.scrub = opts.scrub != null ? opts.scrub : 0.6;
357
+ delete st.once;
358
+
403
359
  gsap.fromTo(el, {
404
360
  rotationX: opts.rotationX != null ? opts.rotationX : 45,
405
361
  scale: opts.scale != null ? opts.scale : 0.8,
@@ -412,12 +368,7 @@
412
368
  autoAlpha: 1,
413
369
  transformPerspective: 1000,
414
370
  ease: o.ease,
415
- scrollTrigger: {
416
- trigger: (opts.scrollTrigger && opts.scrollTrigger.trigger) || el,
417
- start: config.scrollStart || 'top 85%',
418
- end: opts.end || 'top 20%',
419
- scrub: opts.scrub != null ? opts.scrub : 0.6,
420
- },
371
+ scrollTrigger: st,
421
372
  });
422
373
  }
423
374
 
@@ -429,9 +380,6 @@
429
380
  var split = new SplitText(el, { type: 'chars' });
430
381
  gsap.set(split.chars, { autoAlpha: 0 });
431
382
 
432
- var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
433
- var entry = { el: el, split: split, tween: null, effectFn: typeWriter, opts: opts };
434
-
435
383
  var tweenVars = {
436
384
  autoAlpha: 1,
437
385
  duration: o.duration,
@@ -440,19 +388,11 @@
440
388
  delay: o.delay,
441
389
  };
442
390
 
443
- if (isOneShot) {
444
- tweenVars.onComplete = function () {
445
- split.revert();
446
- unregisterSplit(entry);
447
- };
448
- }
449
-
450
391
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
451
392
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
452
393
  }
453
394
 
454
- entry.tween = gsap.to(split.chars, tweenVars);
455
- registerSplit(entry);
395
+ gsap.to(split.chars, tweenVars);
456
396
  }
457
397
 
458
398
  function drawSVG(el, opts) {
@@ -476,15 +416,15 @@
476
416
  // Scrub mode: SVG draws as user scrolls (class fx-scrub-[0.6] or opts.scrub)
477
417
  var scrubVal = getClassModifier(el, 'scrub', opts.scrub != null ? opts.scrub : null);
478
418
  if (scrubVal !== null) {
419
+ var st = buildScrollTrigger(el, opts.scrollTrigger || {});
420
+ st.end = opts.end || 'top 20%';
421
+ st.scrub = scrubVal === true || scrubVal === 'true' ? true : scrubVal;
422
+ delete st.once;
423
+
479
424
  gsap.to(paths, {
480
425
  strokeDashoffset: 0,
481
426
  ease: o.ease,
482
- scrollTrigger: {
483
- trigger: (opts.scrollTrigger && opts.scrollTrigger.trigger) || el,
484
- start: config.scrollStart || 'top 85%',
485
- end: opts.end || 'top 20%',
486
- scrub: scrubVal === true || scrubVal === 'true' ? true : scrubVal,
487
- },
427
+ scrollTrigger: st,
488
428
  });
489
429
  return;
490
430
  }
@@ -508,17 +448,17 @@
508
448
  // Read y from modifier class fx-y-[80] or opts or default 50
509
449
  var yShift = getClassModifier(el, 'y', opts.y != null ? opts.y : 50);
510
450
 
451
+ var st = buildScrollTrigger(el, opts.scrollTrigger || {});
452
+ st.end = opts.end || 'bottom top';
453
+ st.scrub = opts.scrub != null ? opts.scrub : true;
454
+ delete st.once;
455
+
511
456
  gsap.fromTo(el, {
512
457
  y: -yShift,
513
458
  }, {
514
459
  y: yShift,
515
460
  ease: 'none',
516
- scrollTrigger: {
517
- trigger: (opts.scrollTrigger && opts.scrollTrigger.trigger) || el,
518
- start: config.scrollStart || 'top 85%',
519
- end: opts.end || 'bottom top',
520
- scrub: opts.scrub != null ? opts.scrub : true,
521
- },
461
+ scrollTrigger: st,
522
462
  });
523
463
  }
524
464
 
@@ -529,9 +469,6 @@
529
469
  gsap.set(el, { visibility: 'inherit' });
530
470
  var split = new SplitText(el, { type: 'words' });
531
471
 
532
- var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
533
- var entry = { el: el, split: split, tween: null, effectFn: splitWords, opts: opts };
534
-
535
472
  var tweenVars = {
536
473
  y: opts.y != null ? opts.y : 30,
537
474
  autoAlpha: 0,
@@ -541,19 +478,11 @@
541
478
  delay: o.delay,
542
479
  };
543
480
 
544
- if (isOneShot) {
545
- tweenVars.onComplete = function () {
546
- split.revert();
547
- unregisterSplit(entry);
548
- };
549
- }
550
-
551
481
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
552
482
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
553
483
  }
554
484
 
555
- entry.tween = gsap.from(split.words, tweenVars);
556
- registerSplit(entry);
485
+ gsap.from(split.words, tweenVars);
557
486
  }
558
487
 
559
488
  function slideIn(el, opts) {
@@ -650,7 +579,6 @@
650
579
  // ── Init ────────────────────────────────────
651
580
 
652
581
  function init() {
653
- var processed = new Set();
654
582
 
655
583
  Object.keys(effects).forEach(function (name) {
656
584
  var fn = effects[name];
@@ -660,8 +588,9 @@
660
588
  plGroups.forEach(function (group) {
661
589
  group = group.filter(function (el) { return !isExcluded(el); });
662
590
  group.forEach(function (el, i) {
591
+ if (_animated.has(el)) return;
663
592
  fn(el, { delay: i * 0.15 });
664
- processed.add(el);
593
+ _animated.add(el);
665
594
  });
666
595
  });
667
596
 
@@ -673,12 +602,13 @@
673
602
  stGroups.forEach(function (group) {
674
603
  group = group.filter(function (el) { return !isExcluded(el); });
675
604
  group.forEach(function (el, i) {
605
+ if (_animated.has(el)) return;
676
606
  fn(el, {
677
607
  trigger: 'scroll',
678
608
  delay: i * 0.15,
679
609
  scrollTrigger: { trigger: el },
680
610
  });
681
- processed.add(el);
611
+ _animated.add(el);
682
612
  });
683
613
  });
684
614
 
@@ -688,18 +618,19 @@
688
618
  if (config.sectionSelector) {
689
619
  document.querySelectorAll(config.sectionSelector).forEach(function (section) {
690
620
  var bareEls = Array.from(section.querySelectorAll('.' + name))
691
- .filter(function (el) { return !processed.has(el) && !isExcluded(el); });
621
+ .filter(function (el) { return !_animated.has(el) && !isExcluded(el); });
692
622
  if (bareEls.length === 0) return;
693
623
 
694
624
  var groups = groupByParent(bareEls);
695
625
  groups.forEach(function (group) {
696
626
  group.forEach(function (el, i) {
627
+ if (_animated.has(el)) return;
697
628
  fn(el, {
698
629
  trigger: 'scroll',
699
630
  delay: i * 0.15,
700
631
  scrollTrigger: { trigger: el },
701
632
  });
702
- processed.add(el);
633
+ _animated.add(el);
703
634
  });
704
635
  });
705
636
  });
@@ -708,21 +639,21 @@
708
639
 
709
640
  // 4. Scrub-based effects — always scroll-linked, processed before tagMap.
710
641
  document.querySelectorAll('.fx-tilt-in-st, .fx-tilt-in-pl, .fx-tilt-in').forEach(function (el) {
711
- if (!processed.has(el) && !isExcluded(el)) {
642
+ if (!_animated.has(el) && !isExcluded(el)) {
712
643
  tiltIn(el);
713
- processed.add(el);
644
+ _animated.add(el);
714
645
  }
715
646
  });
716
647
  document.querySelectorAll('.fx-parallax-st, .fx-parallax-pl, .fx-parallax').forEach(function (el) {
717
- if (!processed.has(el) && !isExcluded(el)) {
648
+ if (!_animated.has(el) && !isExcluded(el)) {
718
649
  parallax(el);
719
- processed.add(el);
650
+ _animated.add(el);
720
651
  }
721
652
  });
722
653
  document.querySelectorAll('.fx-draw-svg-scrub').forEach(function (el) {
723
- if (!processed.has(el) && !isExcluded(el)) {
654
+ if (!_animated.has(el) && !isExcluded(el)) {
724
655
  drawSVG(el, { scrub: getClassModifier(el, 'scrub', 0.6) });
725
- processed.add(el);
656
+ _animated.add(el);
726
657
  }
727
658
  });
728
659
 
@@ -735,18 +666,19 @@
735
666
  if (!fn) return;
736
667
 
737
668
  var els = Array.from(section.querySelectorAll(selector))
738
- .filter(function (el) { return !processed.has(el) && !isExcluded(el); });
669
+ .filter(function (el) { return !_animated.has(el) && !isExcluded(el); });
739
670
  if (els.length === 0) return;
740
671
 
741
672
  var groups = groupByParent(els);
742
673
  groups.forEach(function (group) {
743
674
  applyScrollGroup(fn, group, section);
744
- group.forEach(function (el) { processed.add(el); });
675
+ group.forEach(function (el) { _animated.add(el); });
745
676
  });
746
677
  });
747
678
  });
748
679
  }
749
- // 5. fx-stagger-all-[selector] — target children, effect from sibling class
680
+
681
+ // 6. fx-stagger-all-[selector] — target children, effect from sibling class
750
682
  // Requires an effect class on the same element (e.g. fx-reveal-st).
751
683
  document.querySelectorAll('[class*="fx-stagger-all-"]').forEach(function (container) {
752
684
  // Parse selector from fx-stagger-all-[img,p]
@@ -776,7 +708,7 @@
776
708
  var isScroll = container.classList.contains(effectName + '-st') ||
777
709
  container.classList.contains(effectName);
778
710
  var children = Array.from(container.querySelectorAll(childSelector))
779
- .filter(function (el) { return !processed.has(el); });
711
+ .filter(function (el) { return !_animated.has(el); });
780
712
  if (children.length === 0) return;
781
713
 
782
714
  children.forEach(function (child, i) {
@@ -786,7 +718,7 @@
786
718
  opts.scrollTrigger = { trigger: child };
787
719
  }
788
720
  effectFn(child, opts);
789
- processed.add(child);
721
+ _animated.add(child);
790
722
  });
791
723
  });
792
724
 
@@ -811,17 +743,20 @@
811
743
  function boot() {
812
744
  applyPreConfig();
813
745
 
814
- // Skip animations if OS reduced motion is enabled
815
- if (config.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
816
- return;
746
+ // Build media query from config — animations auto-revert when conditions stop matching
747
+ var parts = [];
748
+ if (config.disableMobile) {
749
+ parts.push('(min-width: ' + (config.mobileBreakpoint + 1) + 'px)');
817
750
  }
818
-
819
- // Skip animations on mobile
820
- if (config.disableMobile && window.innerWidth <= config.mobileBreakpoint) {
821
- return;
751
+ if (config.respectReducedMotion) {
752
+ parts.push('(prefers-reduced-motion: no-preference)');
822
753
  }
754
+ var conditions = parts.length > 0 ? parts.join(' and ') : 'all';
823
755
 
824
- init();
756
+ _mm.add(conditions, function () {
757
+ _animated = new WeakSet();
758
+ init();
759
+ });
825
760
  }
826
761
 
827
762
  if (document.readyState === 'loading') {
@@ -850,6 +785,6 @@
850
785
  splitWords: splitWords,
851
786
  slideIn: slideIn,
852
787
  init: init,
853
- refresh: refreshSplits,
788
+ refresh: function () { ScrollTrigger.refresh(); },
854
789
  };
855
790
  })();