@hortonstudio/main 1.4.2 → 1.4.4

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.
@@ -1,7 +1,6 @@
1
1
  const API_NAME = "hsmain";
2
2
 
3
3
  // Module-scoped variables for resize handling
4
- let resizeTimeout;
5
4
  let resizeHandler;
6
5
 
7
6
  // Check for reduced motion preference
@@ -107,36 +106,40 @@ async function waitForFonts() {
107
106
  }
108
107
 
109
108
  function findTextElement(container) {
110
- // Find the deepest element that contains actual text
111
- const walker = document.createTreeWalker(
112
- container,
113
- NodeFilter.SHOW_ELEMENT,
114
- {
115
- acceptNode: (node) => {
116
- // Check if this element has direct text content (not just whitespace)
117
- const hasDirectText = Array.from(node.childNodes).some(child =>
118
- child.nodeType === Node.TEXT_NODE && child.textContent.trim()
119
- );
120
- return hasDirectText ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
121
- }
109
+ // Safari-optimized: Use simple querySelector instead of TreeWalker
110
+ // TreeWalker is notoriously slow in Safari/WebKit
111
+ const directTextElement = container.querySelector(':scope > *:first-child');
112
+ if (directTextElement && directTextElement.textContent.trim()) {
113
+ return directTextElement;
114
+ }
115
+
116
+ // Fallback: check if container itself has text
117
+ if (container.textContent.trim()) {
118
+ return container;
119
+ }
120
+
121
+ // Last resort: find any element with text content
122
+ const textElements = container.querySelectorAll('*');
123
+ for (let i = 0; i < textElements.length; i++) {
124
+ const el = textElements[i];
125
+ if (el.textContent.trim() && !el.querySelector('*')) {
126
+ return el;
122
127
  }
123
- );
128
+ }
124
129
 
125
- return walker.nextNode() || container;
130
+ return container;
126
131
  }
127
132
 
128
133
  function showElementsWithoutAnimation() {
129
- // Simply show all text elements without any animation or split text
130
- const allTextElements = [
131
- ...document.querySelectorAll('[data-hs-anim="char"]'),
132
- ...document.querySelectorAll('[data-hs-anim="word"]'),
133
- ...document.querySelectorAll('[data-hs-anim="line"]'),
134
- ...document.querySelectorAll('[data-hs-anim="appear"]'),
135
- ...document.querySelectorAll('[data-hs-anim="reveal"]'),
136
- ...document.querySelectorAll('[data-hs-anim="group"]'),
137
- ];
138
-
139
- allTextElements.forEach((element) => {
134
+ // Safari-optimized: Use single query with comma-separated selectors
135
+ // This is significantly faster than multiple querySelectorAll calls
136
+ const allTextElements = document.querySelectorAll(
137
+ '[data-hs-anim="char"], [data-hs-anim="word"], [data-hs-anim="line"], [data-hs-anim="appear"], [data-hs-anim="reveal"], [data-hs-anim="group"]'
138
+ );
139
+
140
+ // Batch DOM operations for better Safari performance
141
+ const elementsArray = Array.from(allTextElements);
142
+ elementsArray.forEach((element) => {
140
143
  try {
141
144
  gsap.set(element, {
142
145
  autoAlpha: 1,
@@ -160,30 +163,56 @@ const CharSplitAnimations = {
160
163
 
161
164
  const elements = document.querySelectorAll('[data-hs-anim="char"]');
162
165
 
163
- elements.forEach((container) => {
164
- // Skip if already processed
165
- if (container.hasAttribute('data-split-processed')) return;
166
-
167
- const textElement = findTextElement(container);
168
- if (!textElement) return;
169
- try {
170
- const split = SplitText.create(textElement, {
171
- type: "words,chars",
172
- mask: "chars",
173
- charsClass: "char",
174
- });
175
- container.splitTextInstance = split;
176
- container.setAttribute('data-split-processed', 'true');
166
+ // Safari optimization: Process elements in smaller batches to avoid blocking
167
+ const processInBatches = (elements, batchSize = 5) => {
168
+ return new Promise((resolve) => {
169
+ let index = 0;
170
+
171
+ const processBatch = () => {
172
+ const endIndex = Math.min(index + batchSize, elements.length);
173
+
174
+ for (let i = index; i < endIndex; i++) {
175
+ const container = elements[i];
176
+
177
+ // Skip if already processed
178
+ if (container.hasAttribute('data-split-processed')) continue;
179
+
180
+ const textElement = findTextElement(container);
181
+ if (!textElement) continue;
182
+
183
+ try {
184
+ const split = SplitText.create(textElement, {
185
+ type: "words,chars",
186
+ mask: "chars",
187
+ charsClass: "char",
188
+ });
189
+ container.splitTextInstance = split;
190
+ container.setAttribute('data-split-processed', 'true');
191
+
192
+ gsap.set(split.chars, {
193
+ yPercent: config.charSplit.yPercent,
194
+ });
195
+ gsap.set(textElement, { autoAlpha: 1 });
196
+ } catch (error) {
197
+ console.warn("Error creating char split:", error);
198
+ gsap.set(textElement, { autoAlpha: 1 });
199
+ }
200
+ }
201
+
202
+ index = endIndex;
203
+
204
+ if (index < elements.length) {
205
+ requestAnimationFrame(processBatch);
206
+ } else {
207
+ resolve();
208
+ }
209
+ };
210
+
211
+ processBatch();
212
+ });
213
+ };
177
214
 
178
- gsap.set(split.chars, {
179
- yPercent: config.charSplit.yPercent,
180
- });
181
- gsap.set(textElement, { autoAlpha: 1 });
182
- } catch (error) {
183
- console.warn("Error creating char split:", error);
184
- gsap.set(textElement, { autoAlpha: 1 });
185
- }
186
- });
215
+ await processInBatches(elements);
187
216
  },
188
217
 
189
218
  async animate() {
@@ -193,42 +222,49 @@ const CharSplitAnimations = {
193
222
  return;
194
223
  }
195
224
 
196
- document
197
- .querySelectorAll('[data-hs-anim="char"]')
198
- .forEach((container) => {
199
- const textElement = findTextElement(container);
200
- if (!textElement) return;
201
- try {
202
- const chars = container.querySelectorAll(".char");
203
- const tl = gsap.timeline({
204
- scrollTrigger: {
205
- trigger: container,
206
- start: config.charSplit.start,
207
- invalidateOnRefresh: true,
208
- toggleActions: "play none none none",
209
- once: true,
210
- },
211
- onComplete: () => {
225
+ // Safari optimization: Cache elements and reduce DOM queries
226
+ const elements = document.querySelectorAll('[data-hs-anim="char"]');
227
+
228
+ elements.forEach((container) => {
229
+ const textElement = findTextElement(container);
230
+ if (!textElement) return;
231
+
232
+ try {
233
+ const chars = container.querySelectorAll(".char");
234
+ if (chars.length === 0) return;
235
+
236
+ const tl = gsap.timeline({
237
+ scrollTrigger: {
238
+ trigger: container,
239
+ start: config.charSplit.start,
240
+ invalidateOnRefresh: true,
241
+ toggleActions: "play none none none",
242
+ once: true,
243
+ },
244
+ onComplete: () => {
245
+ // Use requestAnimationFrame for cleanup to avoid Safari blocking
246
+ requestAnimationFrame(() => {
212
247
  if (container.splitTextInstance) {
213
248
  container.splitTextInstance.revert();
214
249
  delete container.splitTextInstance;
215
250
  container.removeAttribute('data-split-processed');
216
251
  }
217
- },
218
- });
252
+ });
253
+ },
254
+ });
219
255
 
220
- tl.to(chars, {
221
- yPercent: 0,
222
- duration: config.charSplit.duration,
223
- stagger: config.charSplit.stagger,
224
- ease: config.charSplit.ease,
225
- });
256
+ tl.to(chars, {
257
+ yPercent: 0,
258
+ duration: config.charSplit.duration,
259
+ stagger: config.charSplit.stagger,
260
+ ease: config.charSplit.ease,
261
+ });
226
262
 
227
- activeAnimations.push({ timeline: tl, element: textElement });
228
- } catch (error) {
229
- console.warn("Error animating char split:", error);
230
- }
231
- });
263
+ activeAnimations.push({ timeline: tl, element: textElement });
264
+ } catch (error) {
265
+ console.warn("Error animating char split:", error);
266
+ }
267
+ });
232
268
  },
233
269
  };
234
270
 
@@ -624,39 +660,40 @@ export async function init() {
624
660
  initAnimations();
625
661
  }
626
662
 
627
- // Set up resize handler with cleanup
663
+ // Safari optimization: Throttled resize handler using requestAnimationFrame
664
+ let resizeScheduled = false;
628
665
  let lastWidth = window.innerWidth;
666
+
629
667
  resizeHandler = () => {
630
668
  const currentWidth = window.innerWidth;
631
- if (currentWidth !== lastWidth) {
669
+ if (currentWidth !== lastWidth && !resizeScheduled) {
632
670
  lastWidth = currentWidth;
633
- clearTimeout(resizeTimeout);
634
- resizeTimeout = setTimeout(() => {
671
+ resizeScheduled = true;
672
+
673
+ // Use requestAnimationFrame instead of setTimeout for better Safari performance
674
+ requestAnimationFrame(() => {
635
675
  try {
636
676
  ScrollTrigger.refresh();
637
677
  } catch (error) {
638
678
  console.warn("Error refreshing ScrollTrigger:", error);
679
+ } finally {
680
+ resizeScheduled = false;
639
681
  }
640
- }, 300);
682
+ });
641
683
  }
642
684
  };
643
- window.addEventListener("resize", resizeHandler);
685
+ window.addEventListener("resize", resizeHandler, { passive: true });
644
686
 
645
- // Add page load handler for proper ScrollTrigger refresh timing
646
- const handlePageLoad = () => {
647
- setTimeout(() => {
687
+ // Safari optimization: Simplified page load handling
688
+ // Complex event handlers and multiple timeouts cause Safari lag
689
+ if (document.readyState === 'complete') {
690
+ requestAnimationFrame(() => {
648
691
  try {
649
692
  ScrollTrigger.refresh();
650
693
  } catch (error) {
651
- console.warn("Error refreshing ScrollTrigger on page load:", error);
694
+ console.warn("Error refreshing ScrollTrigger:", error);
652
695
  }
653
- }, 100);
654
- };
655
-
656
- if (document.readyState === 'complete') {
657
- handlePageLoad();
658
- } else {
659
- window.addEventListener('load', handlePageLoad);
696
+ });
660
697
  }
661
698
 
662
699
  // Initialize API with proper checks
@@ -677,7 +714,6 @@ export async function init() {
677
714
  cleanup: () => {
678
715
  killTextAnimations();
679
716
  window.removeEventListener("resize", resizeHandler);
680
- clearTimeout(resizeTimeout);
681
717
  },
682
718
  };
683
719
 
@@ -691,11 +727,6 @@ export async function init() {
691
727
  window.removeEventListener("resize", resizeHandler);
692
728
  }
693
729
 
694
- // Clear timeout
695
- if (resizeTimeout) {
696
- clearTimeout(resizeTimeout);
697
- }
698
-
699
730
  // Remove initialized markers
700
731
  document.querySelectorAll("[data-initialized]").forEach((el) => {
701
732
  el.removeAttribute("data-initialized");
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version:1.4.2
1
+ // Version:1.4.4
2
2
 
3
3
  const API_NAME = "hsmain";
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/styles.css CHANGED
@@ -9,6 +9,7 @@ body .transition {display: block}
9
9
  margin-bottom: -.1em;
10
10
  padding-inline: .1em;
11
11
  margin-inline: -.1em;
12
+ will-change: transform, opacity;
12
13
  }
13
14
 
14
15