@hortonstudio/main 1.4.3 → 1.4.5

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,22 @@ 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
- }
122
- }
123
- );
124
-
125
- return walker.nextNode() || container;
109
+ // Simple direct approach: if container has direct text content, use it
110
+ // Otherwise use first child with text content
111
+ const firstChild = container.firstElementChild;
112
+ return firstChild && firstChild.textContent.trim() ? firstChild : container;
126
113
  }
127
114
 
128
115
  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) => {
116
+ // Safari-optimized: Use single query with comma-separated selectors
117
+ // This is significantly faster than multiple querySelectorAll calls
118
+ const allTextElements = document.querySelectorAll(
119
+ '[data-hs-anim="char"], [data-hs-anim="word"], [data-hs-anim="line"], [data-hs-anim="appear"], [data-hs-anim="reveal"], [data-hs-anim="group"]'
120
+ );
121
+
122
+ // Batch DOM operations for better Safari performance
123
+ const elementsArray = Array.from(allTextElements);
124
+ elementsArray.forEach((element) => {
140
125
  try {
141
126
  gsap.set(element, {
142
127
  autoAlpha: 1,
@@ -161,28 +146,19 @@ const CharSplitAnimations = {
161
146
  const elements = document.querySelectorAll('[data-hs-anim="char"]');
162
147
 
163
148
  elements.forEach((container) => {
164
- // Skip if already processed
165
- if (container.hasAttribute('data-split-processed')) return;
166
-
167
149
  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');
177
-
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
- }
150
+
151
+ const split = SplitText.create(textElement, {
152
+ type: "words,chars",
153
+ mask: "chars",
154
+ charsClass: "char",
155
+ });
156
+ container.splitTextInstance = split;
157
+
158
+ gsap.set(split.chars, {
159
+ yPercent: config.charSplit.yPercent,
160
+ });
161
+ gsap.set(textElement, { autoAlpha: 1 });
186
162
  });
187
163
  },
188
164
 
@@ -193,42 +169,36 @@ const CharSplitAnimations = {
193
169
  return;
194
170
  }
195
171
 
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: () => {
212
- if (container.splitTextInstance) {
213
- container.splitTextInstance.revert();
214
- delete container.splitTextInstance;
215
- container.removeAttribute('data-split-processed');
216
- }
217
- },
218
- });
219
-
220
- tl.to(chars, {
221
- yPercent: 0,
222
- duration: config.charSplit.duration,
223
- stagger: config.charSplit.stagger,
224
- ease: config.charSplit.ease,
225
- });
226
-
227
- activeAnimations.push({ timeline: tl, element: textElement });
228
- } catch (error) {
229
- console.warn("Error animating char split:", error);
230
- }
172
+ const elements = document.querySelectorAll('[data-hs-anim="char"]');
173
+
174
+ elements.forEach((container) => {
175
+ const chars = container.querySelectorAll(".char");
176
+
177
+ const tl = gsap.timeline({
178
+ scrollTrigger: {
179
+ trigger: container,
180
+ start: config.charSplit.start,
181
+ invalidateOnRefresh: true,
182
+ toggleActions: "play none none none",
183
+ once: true,
184
+ },
185
+ onComplete: () => {
186
+ if (container.splitTextInstance) {
187
+ container.splitTextInstance.revert();
188
+ delete container.splitTextInstance;
189
+ }
190
+ },
231
191
  });
192
+
193
+ tl.to(chars, {
194
+ yPercent: 0,
195
+ duration: config.charSplit.duration,
196
+ stagger: config.charSplit.stagger,
197
+ ease: config.charSplit.ease,
198
+ });
199
+
200
+ activeAnimations.push({ timeline: tl, element: container });
201
+ });
232
202
  },
233
203
  };
234
204
 
@@ -243,28 +213,19 @@ const WordSplitAnimations = {
243
213
  const elements = document.querySelectorAll('[data-hs-anim="word"]');
244
214
 
245
215
  elements.forEach((container) => {
246
- // Skip if already processed
247
- if (container.hasAttribute('data-split-processed')) return;
248
-
249
216
  const textElement = findTextElement(container);
250
- if (!textElement) return;
251
- try {
252
- const split = SplitText.create(textElement, {
253
- type: "words",
254
- mask: "words",
255
- wordsClass: "word",
256
- });
257
- container.splitTextInstance = split;
258
- container.setAttribute('data-split-processed', 'true');
259
-
260
- gsap.set(split.words, {
261
- yPercent: config.wordSplit.yPercent,
262
- });
263
- gsap.set(textElement, { autoAlpha: 1 });
264
- } catch (error) {
265
- console.warn("Error creating word split:", error);
266
- gsap.set(textElement, { autoAlpha: 1 });
267
- }
217
+
218
+ const split = SplitText.create(textElement, {
219
+ type: "words",
220
+ mask: "words",
221
+ wordsClass: "word",
222
+ });
223
+ container.splitTextInstance = split;
224
+
225
+ gsap.set(split.words, {
226
+ yPercent: config.wordSplit.yPercent,
227
+ });
228
+ gsap.set(textElement, { autoAlpha: 1 });
268
229
  });
269
230
  },
270
231
 
@@ -275,42 +236,36 @@ const WordSplitAnimations = {
275
236
  return;
276
237
  }
277
238
 
278
- document
279
- .querySelectorAll('[data-hs-anim="word"]')
280
- .forEach((container) => {
281
- const textElement = findTextElement(container);
282
- if (!textElement) return;
283
- try {
284
- const words = container.querySelectorAll(".word");
285
- const tl = gsap.timeline({
286
- scrollTrigger: {
287
- trigger: container,
288
- start: config.wordSplit.start,
289
- invalidateOnRefresh: true,
290
- toggleActions: "play none none none",
291
- once: true,
292
- },
293
- onComplete: () => {
294
- if (container.splitTextInstance) {
295
- container.splitTextInstance.revert();
296
- delete container.splitTextInstance;
297
- container.removeAttribute('data-split-processed');
298
- }
299
- },
300
- });
301
-
302
- tl.to(words, {
303
- yPercent: 0,
304
- duration: config.wordSplit.duration,
305
- stagger: config.wordSplit.stagger,
306
- ease: config.wordSplit.ease,
307
- });
308
-
309
- activeAnimations.push({ timeline: tl, element: textElement });
310
- } catch (error) {
311
- console.warn("Error animating word split:", error);
312
- }
239
+ const elements = document.querySelectorAll('[data-hs-anim="word"]');
240
+
241
+ elements.forEach((container) => {
242
+ const words = container.querySelectorAll(".word");
243
+
244
+ const tl = gsap.timeline({
245
+ scrollTrigger: {
246
+ trigger: container,
247
+ start: config.wordSplit.start,
248
+ invalidateOnRefresh: true,
249
+ toggleActions: "play none none none",
250
+ once: true,
251
+ },
252
+ onComplete: () => {
253
+ if (container.splitTextInstance) {
254
+ container.splitTextInstance.revert();
255
+ delete container.splitTextInstance;
256
+ }
257
+ },
313
258
  });
259
+
260
+ tl.to(words, {
261
+ yPercent: 0,
262
+ duration: config.wordSplit.duration,
263
+ stagger: config.wordSplit.stagger,
264
+ ease: config.wordSplit.ease,
265
+ });
266
+
267
+ activeAnimations.push({ timeline: tl, element: container });
268
+ });
314
269
  },
315
270
  };
316
271
 
@@ -325,28 +280,19 @@ const LineSplitAnimations = {
325
280
  const elements = document.querySelectorAll('[data-hs-anim="line"]');
326
281
 
327
282
  elements.forEach((container) => {
328
- // Skip if already processed
329
- if (container.hasAttribute('data-split-processed')) return;
330
-
331
283
  const textElement = findTextElement(container);
332
- if (!textElement) return;
333
- try {
334
- const split = SplitText.create(textElement, {
335
- type: "lines",
336
- mask: "lines",
337
- linesClass: "line",
338
- });
339
- container.splitTextInstance = split;
340
- container.setAttribute('data-split-processed', 'true');
341
-
342
- gsap.set(split.lines, {
343
- yPercent: config.lineSplit.yPercent,
344
- });
345
- gsap.set(textElement, { autoAlpha: 1 });
346
- } catch (error) {
347
- console.warn("Error creating line split:", error);
348
- gsap.set(textElement, { autoAlpha: 1 });
349
- }
284
+
285
+ const split = SplitText.create(textElement, {
286
+ type: "lines",
287
+ mask: "lines",
288
+ linesClass: "line",
289
+ });
290
+ container.splitTextInstance = split;
291
+
292
+ gsap.set(split.lines, {
293
+ yPercent: config.lineSplit.yPercent,
294
+ });
295
+ gsap.set(textElement, { autoAlpha: 1 });
350
296
  });
351
297
  },
352
298
 
@@ -357,42 +303,36 @@ const LineSplitAnimations = {
357
303
  return;
358
304
  }
359
305
 
360
- document
361
- .querySelectorAll('[data-hs-anim="line"]')
362
- .forEach((container) => {
363
- const textElement = findTextElement(container);
364
- if (!textElement) return;
365
- try {
366
- const lines = container.querySelectorAll(".line");
367
- const tl = gsap.timeline({
368
- scrollTrigger: {
369
- trigger: container,
370
- start: config.lineSplit.start,
371
- invalidateOnRefresh: true,
372
- toggleActions: "play none none none",
373
- once: true,
374
- },
375
- onComplete: () => {
376
- if (container.splitTextInstance) {
377
- container.splitTextInstance.revert();
378
- delete container.splitTextInstance;
379
- container.removeAttribute('data-split-processed');
380
- }
381
- },
382
- });
383
-
384
- tl.to(lines, {
385
- yPercent: 0,
386
- duration: config.lineSplit.duration,
387
- stagger: config.lineSplit.stagger,
388
- ease: config.lineSplit.ease,
389
- });
390
-
391
- activeAnimations.push({ timeline: tl, element: textElement });
392
- } catch (error) {
393
- console.warn("Error animating line split:", error);
394
- }
306
+ const elements = document.querySelectorAll('[data-hs-anim="line"]');
307
+
308
+ elements.forEach((container) => {
309
+ const lines = container.querySelectorAll(".line");
310
+
311
+ const tl = gsap.timeline({
312
+ scrollTrigger: {
313
+ trigger: container,
314
+ start: config.lineSplit.start,
315
+ invalidateOnRefresh: true,
316
+ toggleActions: "play none none none",
317
+ once: true,
318
+ },
319
+ onComplete: () => {
320
+ if (container.splitTextInstance) {
321
+ container.splitTextInstance.revert();
322
+ delete container.splitTextInstance;
323
+ }
324
+ },
325
+ });
326
+
327
+ tl.to(lines, {
328
+ yPercent: 0,
329
+ duration: config.lineSplit.duration,
330
+ stagger: config.lineSplit.stagger,
331
+ ease: config.lineSplit.ease,
395
332
  });
333
+
334
+ activeAnimations.push({ timeline: tl, element: container });
335
+ });
396
336
  },
397
337
  };
398
338
 
@@ -406,14 +346,10 @@ const AppearAnimations = {
406
346
 
407
347
  const elements = document.querySelectorAll('[data-hs-anim="appear"]');
408
348
  elements.forEach((element) => {
409
- try {
410
- gsap.set(element, {
411
- y: config.appear.y,
412
- opacity: 0,
413
- });
414
- } catch (error) {
415
- console.warn("Error setting appear initial state:", error);
416
- }
349
+ gsap.set(element, {
350
+ y: config.appear.y,
351
+ opacity: 0,
352
+ });
417
353
  });
418
354
  },
419
355
 
@@ -424,29 +360,27 @@ const AppearAnimations = {
424
360
  return;
425
361
  }
426
362
 
427
- document.querySelectorAll('[data-hs-anim="appear"]').forEach((element) => {
428
- try {
429
- const tl = gsap.timeline({
430
- scrollTrigger: {
431
- trigger: element,
432
- start: config.appear.start,
433
- invalidateOnRefresh: true,
434
- toggleActions: "play none none none",
435
- once: true,
436
- },
437
- });
438
-
439
- tl.to(element, {
440
- y: 0,
441
- opacity: 1,
442
- duration: config.appear.duration,
443
- ease: config.appear.ease,
444
- });
445
-
446
- activeAnimations.push({ timeline: tl, element: element });
447
- } catch (error) {
448
- console.warn("Error animating appear:", error);
449
- }
363
+ const elements = document.querySelectorAll('[data-hs-anim="appear"]');
364
+
365
+ elements.forEach((element) => {
366
+ const tl = gsap.timeline({
367
+ scrollTrigger: {
368
+ trigger: element,
369
+ start: config.appear.start,
370
+ invalidateOnRefresh: true,
371
+ toggleActions: "play none none none",
372
+ once: true,
373
+ },
374
+ });
375
+
376
+ tl.to(element, {
377
+ y: 0,
378
+ opacity: 1,
379
+ duration: config.appear.duration,
380
+ ease: config.appear.ease,
381
+ });
382
+
383
+ activeAnimations.push({ timeline: tl, element: element });
450
384
  });
451
385
  },
452
386
  };
@@ -461,14 +395,10 @@ const RevealAnimations = {
461
395
 
462
396
  const elements = document.querySelectorAll('[data-hs-anim="reveal"]');
463
397
  elements.forEach((element) => {
464
- try {
465
- gsap.set(element, {
466
- y: config.reveal.y,
467
- opacity: 0,
468
- });
469
- } catch (error) {
470
- console.warn("Error setting reveal initial state:", error);
471
- }
398
+ gsap.set(element, {
399
+ y: config.reveal.y,
400
+ opacity: 0,
401
+ });
472
402
  });
473
403
  },
474
404
 
@@ -479,29 +409,27 @@ const RevealAnimations = {
479
409
  return;
480
410
  }
481
411
 
482
- document.querySelectorAll('[data-hs-anim="reveal"]').forEach((element) => {
483
- try {
484
- const tl = gsap.timeline({
485
- scrollTrigger: {
486
- trigger: element,
487
- start: config.reveal.start,
488
- invalidateOnRefresh: true,
489
- toggleActions: "play none none none",
490
- once: true,
491
- },
492
- });
493
-
494
- tl.to(element, {
495
- y: 0,
496
- opacity: 1,
497
- duration: config.reveal.duration,
498
- ease: config.reveal.ease,
499
- });
500
-
501
- activeAnimations.push({ timeline: tl, element: element });
502
- } catch (error) {
503
- console.warn("Error animating reveal:", error);
504
- }
412
+ const elements = document.querySelectorAll('[data-hs-anim="reveal"]');
413
+
414
+ elements.forEach((element) => {
415
+ const tl = gsap.timeline({
416
+ scrollTrigger: {
417
+ trigger: element,
418
+ start: config.reveal.start,
419
+ invalidateOnRefresh: true,
420
+ toggleActions: "play none none none",
421
+ once: true,
422
+ },
423
+ });
424
+
425
+ tl.to(element, {
426
+ y: 0,
427
+ opacity: 1,
428
+ duration: config.reveal.duration,
429
+ ease: config.reveal.ease,
430
+ });
431
+
432
+ activeAnimations.push({ timeline: tl, element: element });
505
433
  });
506
434
  },
507
435
  };
@@ -516,15 +444,11 @@ const GroupAnimations = {
516
444
 
517
445
  const elements = document.querySelectorAll('[data-hs-anim="group"]');
518
446
  elements.forEach((element) => {
519
- try {
520
- const children = Array.from(element.children);
521
- gsap.set(children, {
522
- y: config.appear.y,
523
- opacity: 0,
524
- });
525
- } catch (error) {
526
- console.warn("Error setting group initial state:", error);
527
- }
447
+ const children = Array.from(element.children);
448
+ gsap.set(children, {
449
+ y: config.appear.y,
450
+ opacity: 0,
451
+ });
528
452
  });
529
453
  },
530
454
 
@@ -535,31 +459,29 @@ const GroupAnimations = {
535
459
  return;
536
460
  }
537
461
 
538
- document.querySelectorAll('[data-hs-anim="group"]').forEach((element) => {
539
- try {
540
- const children = Array.from(element.children);
541
- const tl = gsap.timeline({
542
- scrollTrigger: {
543
- trigger: element,
544
- start: config.appear.start,
545
- invalidateOnRefresh: true,
546
- toggleActions: "play none none none",
547
- once: true,
548
- },
549
- });
550
-
551
- tl.to(children, {
552
- y: 0,
553
- opacity: 1,
554
- duration: config.appear.duration,
555
- ease: config.appear.ease,
556
- stagger: 0.1,
557
- });
558
-
559
- activeAnimations.push({ timeline: tl, element: element });
560
- } catch (error) {
561
- console.warn("Error animating group:", error);
562
- }
462
+ const elements = document.querySelectorAll('[data-hs-anim="group"]');
463
+
464
+ elements.forEach((element) => {
465
+ const children = Array.from(element.children);
466
+ const tl = gsap.timeline({
467
+ scrollTrigger: {
468
+ trigger: element,
469
+ start: config.appear.start,
470
+ invalidateOnRefresh: true,
471
+ toggleActions: "play none none none",
472
+ once: true,
473
+ },
474
+ });
475
+
476
+ tl.to(children, {
477
+ y: 0,
478
+ opacity: 1,
479
+ duration: config.appear.duration,
480
+ ease: config.appear.ease,
481
+ stagger: 0.1,
482
+ });
483
+
484
+ activeAnimations.push({ timeline: tl, element: element });
563
485
  });
564
486
  },
565
487
  };
@@ -624,39 +546,40 @@ export async function init() {
624
546
  initAnimations();
625
547
  }
626
548
 
627
- // Set up resize handler with cleanup
549
+ // Safari optimization: Throttled resize handler using requestAnimationFrame
550
+ let resizeScheduled = false;
628
551
  let lastWidth = window.innerWidth;
552
+
629
553
  resizeHandler = () => {
630
554
  const currentWidth = window.innerWidth;
631
- if (currentWidth !== lastWidth) {
555
+ if (currentWidth !== lastWidth && !resizeScheduled) {
632
556
  lastWidth = currentWidth;
633
- clearTimeout(resizeTimeout);
634
- resizeTimeout = setTimeout(() => {
557
+ resizeScheduled = true;
558
+
559
+ // Use requestAnimationFrame instead of setTimeout for better Safari performance
560
+ requestAnimationFrame(() => {
635
561
  try {
636
562
  ScrollTrigger.refresh();
637
563
  } catch (error) {
638
564
  console.warn("Error refreshing ScrollTrigger:", error);
565
+ } finally {
566
+ resizeScheduled = false;
639
567
  }
640
- }, 300);
568
+ });
641
569
  }
642
570
  };
643
- window.addEventListener("resize", resizeHandler);
571
+ window.addEventListener("resize", resizeHandler, { passive: true });
644
572
 
645
- // Add page load handler for proper ScrollTrigger refresh timing
646
- const handlePageLoad = () => {
647
- setTimeout(() => {
573
+ // Safari optimization: Simplified page load handling
574
+ // Complex event handlers and multiple timeouts cause Safari lag
575
+ if (document.readyState === 'complete') {
576
+ requestAnimationFrame(() => {
648
577
  try {
649
578
  ScrollTrigger.refresh();
650
579
  } catch (error) {
651
- console.warn("Error refreshing ScrollTrigger on page load:", error);
580
+ console.warn("Error refreshing ScrollTrigger:", error);
652
581
  }
653
- }, 100);
654
- };
655
-
656
- if (document.readyState === 'complete') {
657
- handlePageLoad();
658
- } else {
659
- window.addEventListener('load', handlePageLoad);
582
+ });
660
583
  }
661
584
 
662
585
  // Initialize API with proper checks
@@ -677,7 +600,6 @@ export async function init() {
677
600
  cleanup: () => {
678
601
  killTextAnimations();
679
602
  window.removeEventListener("resize", resizeHandler);
680
- clearTimeout(resizeTimeout);
681
603
  },
682
604
  };
683
605
 
@@ -691,11 +613,6 @@ export async function init() {
691
613
  window.removeEventListener("resize", resizeHandler);
692
614
  }
693
615
 
694
- // Clear timeout
695
- if (resizeTimeout) {
696
- clearTimeout(resizeTimeout);
697
- }
698
-
699
616
  // Remove initialized markers
700
617
  document.querySelectorAll("[data-initialized]").forEach((el) => {
701
618
  el.removeAttribute("data-initialized");
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version:1.4.3
1
+ // Version:1.4.5
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.3",
3
+ "version": "1.4.5",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",