fancoolo-fx 1.6.1 → 1.7.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/README.md CHANGED
@@ -159,6 +159,16 @@ All functions accept `(element, options)`:
159
159
 
160
160
  Set `trigger: 'scroll'` to enable ScrollTrigger. Pass `scrollTrigger: { trigger: someEl }` to use a different trigger element.
161
161
 
162
+ ### Utility Methods
163
+
164
+ | Method | Description |
165
+ |--------|-------------|
166
+ | `FX.init()` | Re-scan DOM and apply animations (for dynamic content) |
167
+ | `FX.refresh()` | Re-split text after layout change (sidebar toggle, font load) |
168
+ | `FX.config` | Global config object |
169
+
170
+ **Resize handling:** Text-based effects (`textReveal`, `typeWriter`, `splitWords`) automatically re-split when the browser width changes. After one-shot animations complete, the SplitText DOM is reverted so text reflows naturally.
171
+
162
172
  ## Using in a New Project
163
173
 
164
174
  1. Copy this repo (or `npm install`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fancoolo-fx",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
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,12 @@ Yes. Use the `fx-start-[top center]` modifier class, or set `scrollStart` in the
68
68
 
69
69
  == Changelog ==
70
70
 
71
+ = 1.7.0 =
72
+ * Fix: Text-based effects (textReveal, typeWriter, splitWords) now re-split on browser resize — line breaks stay correct at every viewport width
73
+ * New: SplitText is reverted after one-shot animations complete — text reflows naturally without extra DOM wrappers
74
+ * New: FX.refresh() public method — manually re-split text after layout changes (sidebar toggle, font load)
75
+ * New: Automatic debounced resize handler (200ms, width-only) for pending scroll-triggered text animations
76
+
71
77
  = 1.0.0 =
72
78
  * Initial release
73
79
  * 5 animation effects
package/src/fx.js CHANGED
@@ -157,6 +157,49 @@
157
157
  return st;
158
158
  }
159
159
 
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
+
160
203
  // ── Effects ──────────────────────────────────
161
204
 
162
205
  function textReveal(el, opts) {
@@ -172,6 +215,9 @@
172
215
  wrapper.appendChild(line);
173
216
  });
174
217
 
218
+ var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
219
+ var entry = { el: el, split: split, tween: null, effectFn: textReveal, opts: opts };
220
+
175
221
  var tweenVars = {
176
222
  y: '100%',
177
223
  opacity: 0,
@@ -179,19 +225,21 @@
179
225
  ease: o.ease,
180
226
  stagger: o.stagger,
181
227
  delay: o.delay,
182
- onComplete: function () {
183
- split.lines.forEach(function (line) {
184
- line.style.transform = '';
185
- line.style.opacity = '';
186
- });
187
- },
188
228
  };
189
229
 
230
+ if (isOneShot) {
231
+ tweenVars.onComplete = function () {
232
+ split.revert();
233
+ unregisterSplit(entry);
234
+ };
235
+ }
236
+
190
237
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
191
238
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
192
239
  }
193
240
 
194
- gsap.from(split.lines, tweenVars);
241
+ entry.tween = gsap.from(split.lines, tweenVars);
242
+ registerSplit(entry);
195
243
  }
196
244
 
197
245
  function reveal(el, opts) {
@@ -377,6 +425,9 @@
377
425
  var split = new SplitText(el, { type: 'chars' });
378
426
  gsap.set(split.chars, { opacity: 0 });
379
427
 
428
+ var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
429
+ var entry = { el: el, split: split, tween: null, effectFn: typeWriter, opts: opts };
430
+
380
431
  var tweenVars = {
381
432
  opacity: 1,
382
433
  duration: o.duration,
@@ -385,11 +436,19 @@
385
436
  delay: o.delay,
386
437
  };
387
438
 
439
+ if (isOneShot) {
440
+ tweenVars.onComplete = function () {
441
+ split.revert();
442
+ unregisterSplit(entry);
443
+ };
444
+ }
445
+
388
446
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
389
447
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
390
448
  }
391
449
 
392
- gsap.to(split.chars, tweenVars);
450
+ entry.tween = gsap.to(split.chars, tweenVars);
451
+ registerSplit(entry);
393
452
  }
394
453
 
395
454
  function drawSVG(el, opts) {
@@ -464,6 +523,9 @@
464
523
 
465
524
  var split = new SplitText(el, { type: 'words' });
466
525
 
526
+ var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
527
+ var entry = { el: el, split: split, tween: null, effectFn: splitWords, opts: opts };
528
+
467
529
  var tweenVars = {
468
530
  y: opts.y != null ? opts.y : 30,
469
531
  opacity: 0,
@@ -473,11 +535,19 @@
473
535
  delay: o.delay,
474
536
  };
475
537
 
538
+ if (isOneShot) {
539
+ tweenVars.onComplete = function () {
540
+ split.revert();
541
+ unregisterSplit(entry);
542
+ };
543
+ }
544
+
476
545
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
477
546
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
478
547
  }
479
548
 
480
- gsap.from(split.words, tweenVars);
549
+ entry.tween = gsap.from(split.words, tweenVars);
550
+ registerSplit(entry);
481
551
  }
482
552
 
483
553
  function slideIn(el, opts) {
@@ -774,5 +844,6 @@
774
844
  splitWords: splitWords,
775
845
  slideIn: slideIn,
776
846
  init: init,
847
+ refresh: refreshSplits,
777
848
  };
778
849
  })();
package/.distignore DELETED
@@ -1,18 +0,0 @@
1
- node_modules/
2
- src/
3
-
4
- **/.DS_Store
5
-
6
- .distignore
7
- .git/
8
- .github/
9
- .gitignore
10
- .claude/
11
- .nojekyll
12
-
13
- docs/
14
- skills/
15
- package-lock.json
16
- package.json
17
- CLAUDE.md
18
- README.md
package/inc/Admin.php DELETED
@@ -1,48 +0,0 @@
1
- <?php
2
-
3
- namespace FancooloFX;
4
-
5
- if ( ! defined( 'ABSPATH' ) ) {
6
- exit;
7
- }
8
-
9
- class Admin {
10
-
11
- public static function register_menu() {
12
- add_theme_page(
13
- 'Fancoolo FX',
14
- 'Fancoolo FX',
15
- 'edit_theme_options',
16
- 'fancoolo-fx',
17
- array( AdminPage::class, 'render' )
18
- );
19
- }
20
-
21
- public static function enqueue( $hook ) {
22
- if ( 'appearance_page_fancoolo-fx' !== $hook ) {
23
- return;
24
- }
25
-
26
- wp_enqueue_style( 'fancoolo-fx-admin', FANCOOLO_FX_URL . 'assets/admin.css', array(), FANCOOLO_FX_VERSION );
27
- wp_enqueue_script( 'fancoolo-fx-admin', FANCOOLO_FX_URL . 'assets/admin.js', array( 'jquery' ), FANCOOLO_FX_VERSION, true );
28
-
29
- $settings = wp_enqueue_code_editor( array( 'type' => 'text/javascript' ) );
30
-
31
- if ( false !== $settings ) {
32
- wp_add_inline_script(
33
- 'code-editor',
34
- sprintf(
35
- 'jQuery(function($) {
36
- if ($("#fancoolo-fx-editor").length) {
37
- var editor = wp.codeEditor.initialize($("#fancoolo-fx-editor"), %s);
38
- editor.codemirror.on("change", function(cm) {
39
- cm.refresh();
40
- });
41
- }
42
- });',
43
- wp_json_encode( $settings )
44
- )
45
- );
46
- }
47
- }
48
- }
package/inc/AdminPage.php DELETED
@@ -1,398 +0,0 @@
1
- <?php
2
-
3
- namespace FancooloFX;
4
-
5
- if ( ! defined( 'ABSPATH' ) ) {
6
- exit;
7
- }
8
-
9
- class AdminPage {
10
-
11
- public static function render() {
12
- $custom_file = Settings::get_custom_file_path();
13
- $content = file_exists( $custom_file ) ? file_get_contents( $custom_file ) : '';
14
- $s = Settings::get();
15
-
16
- settings_errors( 'fancoolo_fx' );
17
- ?>
18
- <!-- Styles and scripts enqueued via Admin::enqueue() -->
19
-
20
- <div class="wrap">
21
- <h1>Fancoolo FX</h1>
22
- <p>
23
- GSAP animation wrapper is active. Add <code>.fx-*</code> classes to blocks in Gutenberg
24
- and they will animate automatically.
25
- </p>
26
-
27
-
28
- <!-- ── Tabs ── -->
29
- <div class="ffx-tabs">
30
- <button class="ffx-tab active" data-tab="editor">Editor</button>
31
- <button class="ffx-tab" data-tab="config">Config Reference</button>
32
- <button class="ffx-tab" data-tab="classes">Classes Reference</button>
33
- </div>
34
-
35
- <!-- ═══ Editor Tab ═══ -->
36
- <div class="ffx-panel ffx-panel-editor active" data-panel="editor">
37
- <form method="post">
38
- <?php wp_nonce_field( 'fancoolo_fx_save_action', 'fancoolo_fx_nonce' ); ?>
39
-
40
- <!-- Editor main area -->
41
- <div class="ffx-editor-main">
42
- <input type="submit" name="fancoolo_fx_save" class="button button-primary ffx-save-btn" value="Save Changes">
43
- <textarea
44
- id="fancoolo-fx-editor"
45
- name="fancoolo_fx_code"
46
- rows="20"
47
- style="width: 100%; font-family: monospace;"
48
- ><?php echo esc_textarea( $content ); ?></textarea>
49
- </div>
50
-
51
- <!-- Settings sidebar -->
52
- <div class="ffx-sidebar">
53
- <h3>Settings</h3>
54
-
55
- <label for="ffx-scroll-start">Scroll Start</label>
56
- <input type="text" id="ffx-scroll-start" name="fancoolo_fx_scroll_start" value="<?php echo esc_attr( $s['scroll_start'] ); ?>" placeholder="top 85%">
57
- <p class="ffx-hint">When scroll animations trigger (e.g. top 85%, top center)</p>
58
-
59
- <label for="ffx-section-selector">Section Selector</label>
60
- <input type="text" id="ffx-section-selector" name="fancoolo_fx_section_selector" value="<?php echo esc_attr( $s['section_selector'] ); ?>" placeholder="section">
61
- <p class="ffx-hint">Containers for bare-class triggering</p>
62
-
63
- <label for="ffx-exclude-selectors">Exclude Selectors</label>
64
- <input type="text" id="ffx-exclude-selectors" name="fancoolo_fx_exclude_selectors" value="<?php echo esc_attr( $s['exclude_selectors'] ); ?>" placeholder=".no-fx, .wp-block-navigation">
65
- <p class="ffx-hint">Elements matching these selectors are never animated</p>
66
-
67
- <hr>
68
-
69
- <div class="ffx-toggle">
70
- <input type="checkbox" id="ffx-scroll-once" name="fancoolo_fx_scroll_once" value="1" <?php checked( $s['scroll_once'], '1' ); ?>>
71
- <label for="ffx-scroll-once">Play once</label>
72
- </div>
73
- <p class="ffx-hint">Uncheck to replay animations on every scroll</p>
74
-
75
- <div class="ffx-toggle">
76
- <input type="checkbox" id="ffx-debug-markers" name="fancoolo_fx_debug_markers" value="1" <?php checked( $s['debug_markers'], '1' ); ?>>
77
- <label for="ffx-debug-markers">Debug markers</label>
78
- </div>
79
- <p class="ffx-hint">Show ScrollTrigger markers (admin only, visitors won't see them)</p>
80
-
81
- <div class="ffx-toggle">
82
- <input type="checkbox" id="ffx-disable-mobile" name="fancoolo_fx_disable_mobile" value="1" <?php checked( $s['disable_mobile'], '1' ); ?>>
83
- <label for="ffx-disable-mobile">Disable on mobile</label>
84
- </div>
85
- <div class="ffx-mobile-breakpoint" style="<?php echo $s['disable_mobile'] === '1' ? '' : 'display:none;'; ?>">
86
- <label for="ffx-mobile-breakpoint">Breakpoint (px)</label>
87
- <input type="text" id="ffx-mobile-breakpoint" name="fancoolo_fx_mobile_breakpoint" value="<?php echo esc_attr( $s['mobile_breakpoint'] ); ?>" placeholder="768" style="width: 80px; margin-bottom: 12px;">
88
- </div>
89
-
90
- <div class="ffx-toggle">
91
- <input type="checkbox" id="ffx-respect-reduced-motion" name="fancoolo_fx_respect_reduced_motion" value="1" <?php checked( $s['respect_reduced_motion'], '1' ); ?>>
92
- <label for="ffx-respect-reduced-motion">Respect reduced motion</label>
93
- </div>
94
- <p class="ffx-hint">Skip animations when OS has reduced motion enabled</p>
95
-
96
- <label for="ffx-speed-multiplier">Speed Multiplier</label>
97
- <select id="ffx-speed-multiplier" name="fancoolo_fx_speed_multiplier" style="width: 100%; margin-bottom: 12px;">
98
- <?php foreach ( array( '0.5' => '0.5x (faster)', '0.75' => '0.75x', '1' => '1x (default)', '1.25' => '1.25x', '1.5' => '1.5x', '2' => '2x (slower)' ) as $val => $lbl ) : ?>
99
- <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $s['speed_multiplier'], $val ); ?>><?php echo esc_html( $lbl ); ?></option>
100
- <?php endforeach; ?>
101
- </select>
102
-
103
- <div class="ffx-toggle">
104
- <input type="checkbox" id="ffx-gutenberg-panel" name="fancoolo_fx_gutenberg_panel" value="1" <?php checked( $s['gutenberg_panel'], '1' ); ?>>
105
- <label for="ffx-gutenberg-panel">Gutenberg integration</label>
106
- </div>
107
- <p class="ffx-hint">Show FX Animation panel in the block editor sidebar</p>
108
-
109
- <hr>
110
-
111
- <h3>Global Tag Map</h3>
112
- <p class="ffx-hint" style="margin-top: -8px;">Auto-animate elements by tag inside sections. No classes needed.</p>
113
-
114
- <div id="ffx-tagmap-rows">
115
- <?php foreach ( $s['tag_map'] as $i => $row ) : ?>
116
- <div class="ffx-tagmap-row">
117
- <input type="text" name="fancoolo_fx_tag_map[<?php echo $i; ?>][selector]" value="<?php echo esc_attr( $row['selector'] ); ?>" placeholder="h1,h2,h3">
118
- <select name="fancoolo_fx_tag_map[<?php echo $i; ?>][effect]">
119
- <?php foreach ( self::get_effects_list() as $value => $label ) : ?>
120
- <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $row['effect'], $value ); ?>><?php echo esc_html( $label ); ?></option>
121
- <?php endforeach; ?>
122
- </select>
123
- <button type="button" class="ffx-tagmap-remove" title="Remove">&times;</button>
124
- </div>
125
- <?php endforeach; ?>
126
- </div>
127
- <button type="button" id="ffx-tagmap-add" class="button button-small">+ Add Rule</button>
128
-
129
- <hr>
130
-
131
- <h3>Import / Export</h3>
132
- <div class="ffx-import-export">
133
- <button type="button" id="ffx-export" class="button button-small" style="width: 100%; margin-bottom: 6px;">Export Settings</button>
134
- <button type="button" id="ffx-import-btn" class="button button-small" style="width: 100%; margin-bottom: 6px;">Import Settings</button>
135
- <input type="file" id="ffx-import-file" accept=".json" style="display: none;">
136
- <button type="button" id="ffx-reset" class="button button-small" style="width: 100%; color: #a00;">Reset to Defaults</button>
137
- </div>
138
- </div>
139
- </form>
140
- </div>
141
-
142
- <!-- ═══ Config Reference Tab ═══ -->
143
- <div class="ffx-panel" data-panel="config">
144
-
145
- <h3>Config Options</h3>
146
- <table class="widefat fixed striped" style="max-width: 700px;">
147
- <thead>
148
- <tr><th>Option</th><th>Default</th><th>Description</th></tr>
149
- </thead>
150
- <tbody>
151
- <tr><td><code>FX.config.tagMap</code></td><td><code>null</code></td><td>Auto-animate elements by tag/selector — no classes needed</td></tr>
152
- <tr><td><code>FX.config.sectionSelector</code></td><td><code>'section'</code></td><td>Containers for bare-class and tagMap triggering</td></tr>
153
- <tr><td><code>FX.config.scrollStart</code></td><td><code>'top 85%'</code></td><td>When scroll animations trigger (GSAP ScrollTrigger start format)</td></tr>
154
- <tr><td><code>FX.config.scrollOnce</code></td><td><code>true</code></td><td>Play once or replay on every scroll</td></tr>
155
- <tr><td><code>FX.config.excludeSelectors</code></td><td><code>''</code></td><td>CSS selectors for elements to never animate</td></tr>
156
- <tr><td><code>FX.config.disableMobile</code></td><td><code>false</code></td><td>Skip all animations on mobile</td></tr>
157
- <tr><td><code>FX.config.mobileBreakpoint</code></td><td><code>768</code></td><td>Width threshold for mobile detection (px)</td></tr>
158
- <tr><td><code>FX.config.speedMultiplier</code></td><td><code>1</code></td><td>Global duration multiplier (0.5 = faster, 2 = slower)</td></tr>
159
- <tr><td><code>FX.config.respectReducedMotion</code></td><td><code>true</code></td><td>Skip animations when OS prefers reduced motion</td></tr>
160
- </tbody>
161
- </table>
162
-
163
- <h3 style="margin-top: 24px;">JS API Functions</h3>
164
- <table class="widefat fixed striped" style="max-width: 700px;">
165
- <thead>
166
- <tr><th>Function</th><th>Description</th></tr>
167
- </thead>
168
- <tbody>
169
- <tr><td><code>FX.textReveal(el, opts)</code></td><td>Split text lines, masked reveal upward</td></tr>
170
- <tr><td><code>FX.reveal(el, opts)</code></td><td>Slide up with fade</td></tr>
171
- <tr><td><code>FX.spinReveal(el, opts)</code></td><td>Rotate and scale in</td></tr>
172
- <tr><td><code>FX.bgReveal(el, opts)</code></td><td>Background slide up</td></tr>
173
- <tr><td><code>FX.scaleIn(el, opts)</code></td><td>Scale up with fade</td></tr>
174
- <tr><td><code>FX.fadeIn(el, opts)</code></td><td>Opacity + subtle scale, no movement</td></tr>
175
- <tr><td><code>FX.blurIn(el, opts)</code></td><td>Fade in while deblurring</td></tr>
176
- <tr><td><code>FX.clipUp(el, opts)</code></td><td>Clip-path wipe from bottom</td></tr>
177
- <tr><td><code>FX.clipDown(el, opts)</code></td><td>Clip-path wipe from top</td></tr>
178
- <tr><td><code>FX.tiltIn(el, opts)</code></td><td>3D perspective reveal (scrub-based)</td></tr>
179
- <tr><td><code>FX.typeWriter(el, opts)</code></td><td>Character-by-character typing reveal</td></tr>
180
- <tr><td><code>FX.drawSVG(el, opts)</code></td><td>SVG stroke drawing animation</td></tr>
181
- <tr><td><code>FX.parallax(el, opts)</code></td><td>Scroll-linked Y parallax shift</td></tr>
182
- <tr><td><code>FX.splitWords(el, opts)</code></td><td>Word-by-word fade and slide up</td></tr>
183
- <tr><td><code>FX.slideIn(el, opts)</code></td><td>Horizontal slide from left or right</td></tr>
184
- <tr><td><code>FX.init()</code></td><td>Re-scan DOM — call after changing any config</td></tr>
185
- </tbody>
186
- </table>
187
-
188
- <h3 style="margin-top: 24px;">Examples <span style="font-weight:normal;color:#646970;font-size:13px;">(click code to copy)</span></h3>
189
-
190
- <h4 style="margin-top: 16px;">Auto-animate by tag</h4>
191
- <div class="ffx-copy-wrap">
192
- <button class="ffx-copy-btn" data-target="ex1">Copy</button>
193
- <pre class="ffx-pre" id="ex1">FX.config.tagMap = {
194
- 'h1,h2,h3,h4,h5,h6': 'textReveal',
195
- 'p,blockquote': 'textReveal',
196
- 'img,video': 'reveal',
197
- };
198
- FX.config.sectionSelector = 'section, .wp-block-group';
199
- FX.init();</pre>
200
- </div>
201
-
202
- <h4 style="margin-top: 16px;">Change scroll trigger position</h4>
203
- <div class="ffx-copy-wrap">
204
- <button class="ffx-copy-btn" data-target="ex2">Copy</button>
205
- <pre class="ffx-pre" id="ex2">FX.config.scrollStart = 'top center';
206
- FX.init();</pre>
207
- </div>
208
-
209
- <h4 style="margin-top: 16px;">Replay animations on re-scroll</h4>
210
- <div class="ffx-copy-wrap">
211
- <button class="ffx-copy-btn" data-target="ex3">Copy</button>
212
- <pre class="ffx-pre" id="ex3">FX.config.scrollOnce = false;
213
- FX.init();</pre>
214
- </div>
215
-
216
- <h4 style="margin-top: 16px;">Compound sequence</h4>
217
- <div class="ffx-copy-wrap">
218
- <button class="ffx-copy-btn" data-target="ex4">Copy</button>
219
- <pre class="ffx-pre" id="ex4">document.addEventListener('DOMContentLoaded', function () {
220
- var hero = document.querySelector('.wp-block-cover');
221
- if (!hero) return;
222
-
223
- FX.scaleIn(hero, {
224
- trigger: 'scroll',
225
- scrollTrigger: { trigger: hero }
226
- });
227
-
228
- var heading = hero.querySelector('h2');
229
- if (heading) {
230
- FX.textReveal(heading, {
231
- trigger: 'scroll',
232
- delay: 0.2,
233
- scrollTrigger: { trigger: hero }
234
- });
235
- }
236
- });</pre>
237
- </div>
238
- </div>
239
-
240
- <!-- ═══ Classes Reference Tab ═══ -->
241
- <div class="ffx-panel" data-panel="classes">
242
-
243
- <p style="margin-bottom: 16px; color: #646970;">
244
- Add these in Gutenberg: select a block &rarr; Advanced &rarr; Additional CSS class(es). Click any class to copy it.
245
- </p>
246
-
247
- <?php self::render_classes_reference(); ?>
248
- </div>
249
-
250
- <p style="margin-top: 24px; color: #646970;">
251
- This code loads after fx.js on the frontend. Leave empty to use defaults only.<br>
252
- <strong style="color:#1d2327;">Important:</strong> Always add <code>FX.init();</code> at the end when changing config — it re-scans the page with your new settings.
253
- </p>
254
- <p style="margin-top: 12px;">
255
- <a href="https://krstivoja.github.io/fancoolo-fx/documentation/" target="_blank">
256
- Full Documentation &rarr;
257
- </a>
258
- </p>
259
- </div>
260
- <?php
261
- }
262
-
263
- public static function get_effects_list() {
264
- return array(
265
- 'textReveal' => 'Text Reveal',
266
- 'reveal' => 'Reveal',
267
- 'spinReveal' => 'Spin Reveal',
268
- 'bgReveal' => 'BG Reveal',
269
- 'scaleIn' => 'Scale In',
270
- 'fadeIn' => 'Fade In',
271
- 'blurIn' => 'Blur In',
272
- 'clipUp' => 'Clip Up',
273
- 'clipDown' => 'Clip Down',
274
- 'tiltIn' => 'Tilt In',
275
- 'typeWriter' => 'Type Writer',
276
- 'drawSVG' => 'Draw SVG',
277
- 'parallax' => 'Parallax',
278
- 'splitWords' => 'Split Words',
279
- 'slideIn' => 'Slide In',
280
- );
281
- }
282
-
283
- private static function render_classes_reference() {
284
- $groups = array(
285
- 'Text Reveal' => array(
286
- 'fx-text-reveal-pl' => 'Page load — masked line-by-line reveal',
287
- 'fx-text-reveal-st' => 'Scroll triggered',
288
- 'fx-text-reveal' => 'Auto triggered inside a section',
289
- ),
290
- 'Reveal' => array(
291
- 'fx-reveal-pl' => 'Page load — slide up with fade',
292
- 'fx-reveal-st' => 'Scroll triggered',
293
- 'fx-reveal' => 'Auto triggered inside a section',
294
- ),
295
- 'Spin Reveal' => array(
296
- 'fx-spin-reveal-pl' => 'Page load — rotate and scale in',
297
- 'fx-spin-reveal-st' => 'Scroll triggered',
298
- 'fx-spin-reveal' => 'Auto triggered inside a section',
299
- ),
300
- 'BG Reveal' => array(
301
- 'fx-bg-reveal-pl' => 'Page load — background slide up',
302
- 'fx-bg-reveal-st' => 'Scroll triggered',
303
- 'fx-bg-reveal' => 'Auto triggered inside a section',
304
- ),
305
- 'Scale In' => array(
306
- 'fx-scale-in-pl' => 'Page load — scale up with fade',
307
- 'fx-scale-in-st' => 'Scroll triggered',
308
- 'fx-scale-in' => 'Auto triggered inside a section',
309
- ),
310
- 'Fade In' => array(
311
- 'fx-fade-in-pl' => 'Page load — opacity only, no movement',
312
- 'fx-fade-in-st' => 'Scroll triggered',
313
- 'fx-fade-in' => 'Auto triggered inside a section',
314
- ),
315
- 'Blur In' => array(
316
- 'fx-blur-in-pl' => 'Page load — fade in while deblurring',
317
- 'fx-blur-in-st' => 'Scroll triggered',
318
- 'fx-blur-in' => 'Auto triggered inside a section',
319
- ),
320
- 'Clip Reveal' => array(
321
- 'fx-clip-up-pl' => 'Page load — clip-path wipe from bottom',
322
- 'fx-clip-up-st' => 'Scroll triggered',
323
- 'fx-clip-up' => 'Auto triggered inside a section',
324
- 'fx-clip-down-pl' => 'Page load — clip-path wipe from top',
325
- 'fx-clip-down-st' => 'Scroll triggered',
326
- 'fx-clip-down' => 'Auto triggered inside a section',
327
- ),
328
- 'Type Writer' => array(
329
- 'fx-type-writer-pl' => 'Page load — character-by-character typing',
330
- 'fx-type-writer-st' => 'Scroll triggered',
331
- 'fx-type-writer' => 'Auto triggered inside a section',
332
- ),
333
- 'Draw SVG' => array(
334
- 'fx-draw-svg-pl' => 'Page load — SVG stroke drawing animation',
335
- 'fx-draw-svg-st' => 'Scroll triggered',
336
- 'fx-draw-svg' => 'Auto triggered inside a section',
337
- 'fx-draw-svg-scrub' => 'Draws progressively as you scroll (scrub-based)',
338
- ),
339
- 'Split Words' => array(
340
- 'fx-split-words-pl' => 'Page load — word-by-word fade and slide',
341
- 'fx-split-words-st' => 'Scroll triggered',
342
- 'fx-split-words' => 'Auto triggered inside a section',
343
- ),
344
- 'Slide In' => array(
345
- 'fx-slide-left-pl' => 'Page load — slide in from left',
346
- 'fx-slide-left-st' => 'Scroll triggered — from left',
347
- 'fx-slide-left' => 'Auto triggered inside a section — from left',
348
- 'fx-slide-right-pl' => 'Page load — slide in from right',
349
- 'fx-slide-right-st' => 'Scroll triggered — from right',
350
- 'fx-slide-right' => 'Auto triggered inside a section — from right',
351
- ),
352
- 'Tilt In|scrub — tied to scroll position' => array(
353
- 'fx-tilt-in-st' => '3D perspective reveal linked to scroll',
354
- 'fx-tilt-in' => 'Auto triggered inside a section',
355
- ),
356
- 'Parallax|scrub — tied to scroll position' => array(
357
- 'fx-parallax-st' => 'Parallax Y-shift linked to scroll',
358
- 'fx-parallax' => 'Auto triggered inside a section',
359
- ),
360
- );
361
-
362
- foreach ( $groups as $title => $classes ) {
363
- $parts = explode( '|', $title );
364
- $label = $parts[0];
365
- $sub = isset( $parts[1] ) ? ' <span style="font-weight:normal;color:#646970;font-size:12px;">(' . esc_html( $parts[1] ) . ')</span>' : '';
366
- echo '<div class="ffx-group-title">' . esc_html( $label ) . $sub . '</div>';
367
- foreach ( $classes as $cls => $desc ) {
368
- echo '<div class="ffx-class-row"><code data-copy>' . esc_html( $cls ) . '</code><span class="ffx-desc">' . esc_html( $desc ) . '</span></div>';
369
- }
370
- }
371
-
372
- // Stagger Children
373
- echo '<div class="ffx-group-title" style="margin-top: 20px;">Stagger Children <span style="font-weight:normal;color:#646970;font-size:12px;">(pair with an effect class)</span></div>';
374
- $stagger = array(
375
- 'fx-stagger-all-[img]' => 'Target all img children — requires effect class',
376
- 'fx-stagger-all-[img,p]' => 'Target img and p children',
377
- 'fx-stagger-all-[.card]' => 'Target children by CSS class',
378
- );
379
- foreach ( $stagger as $cls => $desc ) {
380
- echo '<div class="ffx-class-row"><code data-copy>' . esc_html( $cls ) . '</code><span class="ffx-desc">' . esc_html( $desc ) . '</span></div>';
381
- }
382
-
383
- // Modifiers
384
- echo '<div class="ffx-group-title" style="margin-top: 20px;">Modifiers <span style="font-weight:normal;color:#646970;font-size:12px;">(combine with any effect class)</span></div>';
385
- $modifiers = array(
386
- 'fx-duration-[1.5]' => 'Custom duration (seconds)',
387
- 'fx-delay-[0.3]' => 'Delay before animating (seconds)',
388
- 'fx-stagger-[0.25]' => 'Delay between staggered siblings',
389
- 'fx-ease-[power2.inOut]' => 'GSAP easing function',
390
- 'fx-start-[top center]' => 'Scroll trigger start position',
391
- 'fx-y-[80]' => 'Parallax Y-shift intensity (parallax only)',
392
- 'fx-scrub-[0.6]' => 'Scrub smoothing (drawSVG scrub mode)',
393
- );
394
- foreach ( $modifiers as $cls => $desc ) {
395
- echo '<div class="ffx-class-row"><code data-copy>' . esc_html( $cls ) . '</code><span class="ffx-desc">' . esc_html( $desc ) . '</span></div>';
396
- }
397
- }
398
- }
package/inc/Editor.php DELETED
@@ -1,31 +0,0 @@
1
- <?php
2
-
3
- namespace FancooloFX;
4
-
5
- if ( ! defined( 'ABSPATH' ) ) {
6
- exit;
7
- }
8
-
9
- class Editor {
10
-
11
- public static function enqueue() {
12
- $s = Settings::get();
13
- if ( ! $s['gutenberg_panel'] ) {
14
- return;
15
- }
16
-
17
- $asset_file = FANCOOLO_FX_PATH . 'assets/editor/index.asset.php';
18
- if ( ! file_exists( $asset_file ) ) {
19
- return;
20
- }
21
-
22
- $asset = include $asset_file;
23
- wp_enqueue_script(
24
- 'fancoolo-fx-editor',
25
- FANCOOLO_FX_URL . 'assets/editor/index.js',
26
- $asset['dependencies'],
27
- $asset['version'],
28
- true
29
- );
30
- }
31
- }
package/inc/Frontend.php DELETED
@@ -1,50 +0,0 @@
1
- <?php
2
-
3
- namespace FancooloFX;
4
-
5
- if ( ! defined( 'ABSPATH' ) ) {
6
- exit;
7
- }
8
-
9
- class Frontend {
10
-
11
- public static function enqueue() {
12
- wp_enqueue_script( 'gsap', FANCOOLO_FX_URL . 'assets/gsap.min.js', array(), '3.14.2', true );
13
- wp_enqueue_script( 'gsap-scrolltrigger', FANCOOLO_FX_URL . 'assets/ScrollTrigger.min.js', array( 'gsap' ), '3.14.2', true );
14
- wp_enqueue_script( 'gsap-splittext', FANCOOLO_FX_URL . 'assets/SplitText.min.js', array( 'gsap' ), '3.14.2', true );
15
- wp_enqueue_script( 'fancoolo-fx', FANCOOLO_FX_URL . 'assets/fx.js', array( 'gsap', 'gsap-scrolltrigger', 'gsap-splittext' ), FANCOOLO_FX_VERSION, true );
16
-
17
- $s = Settings::get();
18
- $config = array(
19
- 'scrollStart' => $s['scroll_start'],
20
- 'scrollOnce' => (bool) $s['scroll_once'],
21
- 'sectionSelector' => $s['section_selector'],
22
- 'disableMobile' => (bool) $s['disable_mobile'],
23
- 'mobileBreakpoint' => (int) $s['mobile_breakpoint'],
24
- 'speedMultiplier' => (float) $s['speed_multiplier'],
25
- 'respectReducedMotion' => (bool) $s['respect_reduced_motion'],
26
- 'excludeSelectors' => $s['exclude_selectors'],
27
- );
28
-
29
- if ( ! empty( $s['tag_map'] ) ) {
30
- $tag_map_obj = array();
31
- foreach ( $s['tag_map'] as $row ) {
32
- $tag_map_obj[ $row['selector'] ] = $row['effect'];
33
- }
34
- $config['tagMap'] = (object) $tag_map_obj;
35
- }
36
-
37
- wp_add_inline_script( 'fancoolo-fx', 'window.__FX_CONFIG__ = ' . wp_json_encode( $config ) . ';', 'before' );
38
-
39
- if ( $s['debug_markers'] && is_user_logged_in() && current_user_can( 'manage_options' ) ) {
40
- wp_add_inline_script( 'fancoolo-fx', 'window.__FX_DEBUG_MARKERS__ = true;', 'before' );
41
- }
42
-
43
- $custom_file = Settings::get_custom_file_path();
44
- if ( file_exists( $custom_file ) && filesize( $custom_file ) > 0 ) {
45
- $upload_dir = wp_upload_dir();
46
- $custom_url = $upload_dir['baseurl'] . '/fancoolo-fx/custom.js';
47
- wp_enqueue_script( 'fancoolo-fx-custom', $custom_url, array( 'fancoolo-fx' ), filemtime( $custom_file ), true );
48
- }
49
- }
50
- }
@@ -1,66 +0,0 @@
1
- <?php
2
-
3
- namespace FancooloFX;
4
-
5
- if ( ! defined( 'ABSPATH' ) ) {
6
- exit;
7
- }
8
-
9
- class SaveHandler {
10
-
11
- public static function handle() {
12
- if ( ! isset( $_POST['fancoolo_fx_save'] ) ) {
13
- return;
14
- }
15
-
16
- if ( ! check_admin_referer( 'fancoolo_fx_save_action', 'fancoolo_fx_nonce' ) ) {
17
- return;
18
- }
19
-
20
- if ( ! current_user_can( 'edit_theme_options' ) ) {
21
- return;
22
- }
23
-
24
- // Save custom JS file.
25
- $content = isset( $_POST['fancoolo_fx_code'] ) ? wp_unslash( $_POST['fancoolo_fx_code'] ) : '';
26
- $upload_dir = wp_upload_dir();
27
- $dir = $upload_dir['basedir'] . '/fancoolo-fx';
28
-
29
- if ( ! file_exists( $dir ) ) {
30
- wp_mkdir_p( $dir );
31
- }
32
-
33
- file_put_contents( $dir . '/custom.js', $content );
34
-
35
- // Build tag map.
36
- $tag_map = array();
37
- if ( ! empty( $_POST['fancoolo_fx_tag_map'] ) && is_array( $_POST['fancoolo_fx_tag_map'] ) ) {
38
- foreach ( $_POST['fancoolo_fx_tag_map'] as $row ) {
39
- $selector = sanitize_text_field( $row['selector'] ?? '' );
40
- $effect = sanitize_text_field( $row['effect'] ?? '' );
41
- if ( '' !== $selector && '' !== $effect ) {
42
- $tag_map[] = array( 'selector' => $selector, 'effect' => $effect );
43
- }
44
- }
45
- }
46
-
47
- // Save settings.
48
- $settings = array(
49
- 'scroll_start' => sanitize_text_field( $_POST['fancoolo_fx_scroll_start'] ?? 'top 85%' ),
50
- 'scroll_once' => isset( $_POST['fancoolo_fx_scroll_once'] ) ? '1' : '0',
51
- 'section_selector' => sanitize_text_field( $_POST['fancoolo_fx_section_selector'] ?? 'section' ),
52
- 'debug_markers' => isset( $_POST['fancoolo_fx_debug_markers'] ) ? '1' : '0',
53
- 'disable_mobile' => isset( $_POST['fancoolo_fx_disable_mobile'] ) ? '1' : '0',
54
- 'mobile_breakpoint' => sanitize_text_field( $_POST['fancoolo_fx_mobile_breakpoint'] ?? '768' ),
55
- 'speed_multiplier' => sanitize_text_field( $_POST['fancoolo_fx_speed_multiplier'] ?? '1' ),
56
- 'respect_reduced_motion' => isset( $_POST['fancoolo_fx_respect_reduced_motion'] ) ? '1' : '0',
57
- 'exclude_selectors' => sanitize_text_field( $_POST['fancoolo_fx_exclude_selectors'] ?? '' ),
58
- 'gutenberg_panel' => isset( $_POST['fancoolo_fx_gutenberg_panel'] ) ? '1' : '0',
59
- 'tag_map' => $tag_map,
60
- );
61
-
62
- Settings::save( $settings );
63
-
64
- add_settings_error( 'fancoolo_fx', 'fancoolo_fx_saved', 'Settings saved.', 'success' );
65
- }
66
- }
package/inc/Settings.php DELETED
@@ -1,41 +0,0 @@
1
- <?php
2
-
3
- namespace FancooloFX;
4
-
5
- if ( ! defined( 'ABSPATH' ) ) {
6
- exit;
7
- }
8
-
9
- class Settings {
10
-
11
- private static $defaults = array(
12
- 'scroll_start' => 'top 85%',
13
- 'scroll_once' => '1',
14
- 'section_selector' => 'section',
15
- 'debug_markers' => '0',
16
- 'disable_mobile' => '0',
17
- 'mobile_breakpoint' => '768',
18
- 'speed_multiplier' => '1',
19
- 'respect_reduced_motion' => '1',
20
- 'exclude_selectors' => '',
21
- 'gutenberg_panel' => '1',
22
- 'tag_map' => array(),
23
- );
24
-
25
- public static function get_defaults() {
26
- return self::$defaults;
27
- }
28
-
29
- public static function get() {
30
- return wp_parse_args( get_option( 'fancoolo_fx_settings', array() ), self::$defaults );
31
- }
32
-
33
- public static function save( $settings ) {
34
- update_option( 'fancoolo_fx_settings', $settings );
35
- }
36
-
37
- public static function get_custom_file_path() {
38
- $upload_dir = wp_upload_dir();
39
- return $upload_dir['basedir'] . '/fancoolo-fx/custom.js';
40
- }
41
- }
package/inc/update.php DELETED
@@ -1,154 +0,0 @@
1
- <?php
2
- // ************************************************************************************************
3
- // Updater Version: 1.0.2
4
- // ************************************************************************************************
5
-
6
- if (!defined('ABSPATH')) exit; // Exit if accessed directly
7
-
8
- if (!class_exists('FANCOOLOFX_DPUpdateChecker')) {
9
-
10
- class FANCOOLOFX_DPUpdateChecker {
11
-
12
- public $slug;
13
- public $version;
14
- public $cache_key;
15
- public $remote_url;
16
- public $type;
17
-
18
- public function __construct($slug, $version, $cache_key, $remote_url, $type = 'plugin') {
19
- $this->slug = $slug;
20
- $this->version = $version;
21
- $this->cache_key = $cache_key;
22
- $this->remote_url = $remote_url;
23
- $this->type = $type;
24
- add_action('admin_init', array($this, 'check_update_conditions'));
25
- add_action('plugin_row_meta', array($this, 'add_check_updates_button'), 10, 2);add_action('admin_post_check_for_updates', array($this, 'check_for_updates'));
26
- }
27
-
28
- public function check_update_conditions() {
29
- add_filter('plugins_api', array($this, 'info'), 20, 3);add_filter('site_transient_update_plugins', array($this, 'update_plugins'));
30
- add_action('upgrader_process_complete', array($this, 'purge'), 10, 2);
31
- }
32
-
33
- public function request() {
34
- $remote = get_transient($this->cache_key);
35
-
36
- if (false === $remote) {
37
- $response = wp_remote_get(
38
- $this->remote_url,
39
- array(
40
- 'timeout' => 10,
41
- 'headers' => array('Accept' => 'application/json')
42
- )
43
- );
44
-
45
- if (
46
- is_wp_error($response)
47
- || 200 !== wp_remote_retrieve_response_code($response)
48
- || empty(wp_remote_retrieve_body($response))
49
- ) {
50
- error_log('Hoster update check error: ' . wp_remote_retrieve_body($response) ?: $response->get_error_message());
51
- return false;
52
- }
53
-
54
- $remote = wp_remote_retrieve_body($response);
55
- set_transient($this->cache_key, $remote, 6 * HOUR_IN_SECONDS);
56
- }
57
-
58
- if(!is_array($remote)){
59
- $remote = json_decode($remote);
60
- }
61
- return $remote;
62
- }
63
-
64
-
65
- public function info($res, $action, $args) {
66
- if ('plugin_information' !== $action || $this->slug !== $args->slug) {
67
- return $res;
68
- }
69
-
70
- $remote = $this->request();
71
- if (!$remote) return $res;
72
-
73
- $res = new stdClass();
74
- $res->name = $remote->name;
75
- $res->slug = $remote->slug;
76
- $res->version = $remote->version;
77
- $res->tested = $remote->tested;
78
- $res->requires = $remote->requires;
79
- $res->author = $remote->author;
80
- $res->author_profile = $remote->author_profile;
81
- $res->download_link = $remote->download_url;
82
- $res->trunk = $remote->download_url;
83
- $res->requires_php = $remote->requires_php;
84
- $res->last_updated = $remote->last_updated;
85
- $res->sections = (array)$remote->sections;
86
-
87
- if (!empty($remote->banners)) {
88
- $res->banners = (array)$remote->banners;
89
- }
90
-
91
- if (!empty($remote->icons)) {
92
- $res->icons = (array)$remote->icons;
93
- }
94
-
95
- return $res;
96
- }
97
-
98
- public function update_plugins($transient) {
99
- if (empty($transient->checked)) return $transient;
100
-
101
- $remote = $this->request();
102
- if (!$remote) return $transient;
103
-
104
- if (
105
- version_compare($this->version, $remote->version, '<')
106
- && version_compare($remote->requires, get_bloginfo('version'), '<=')
107
- && version_compare($remote->requires_php, PHP_VERSION, '<=')
108
- ) {
109
- $res = new stdClass();
110
- $res->slug = $this->slug;
111
- $res->new_version = $remote->version;
112
- $res->tested = $remote->tested;
113
- $res->package = $remote->download_url;
114
-
115
- if (!empty($remote->icons)) {
116
- $res->icons = (array)$remote->icons;
117
- }
118
-
119
- $res->plugin = $this->slug;
120
- $transient->response[$res->plugin] = $res;
121
- }
122
-
123
- return $transient;
124
- }
125
-
126
- public function add_check_updates_button($plugin_meta, $plugin_file) {
127
- if ($plugin_file === $this->slug) {
128
- $url = wp_nonce_url(admin_url('admin-post.php?action=check_for_updates&plugin=' . $this->slug), 'check_for_updates');
129
- $plugin_meta[] = '<a href="' . esc_url($url) . '">Check for updates</a>';
130
- }
131
- return $plugin_meta;
132
- }
133
-
134
- public function check_for_updates() {
135
- if (!current_user_can('update_plugins')) {
136
- wp_die(__('You do not have sufficient permissions to update plugins.'));
137
- }
138
-
139
- check_admin_referer('check_for_updates');
140
- delete_transient($this->cache_key);
141
- $this->request();
142
- wp_redirect(admin_url('plugins.php'));
143
- exit;
144
- }
145
-
146
-
147
-
148
- public function purge($upgrader, $options) {
149
- if ('update' === $options['action'] && $this->type === $options['type']) {
150
- delete_transient($this->cache_key);
151
- }
152
- }
153
- }
154
- }