PageSourceSearch

https://treefortmusicfest.com/wp-content/themes/tmf27/assets/js/home.js?ver=1789148835.1

js treefortmusicfest.com collected 2026-09-24 10:15:03 UTC 14,750 bytes, 398 lines download raw bytes

1/**
2 * Homepage interactions.
3 *
4 * 1. Parallax: moves star layers at different rates on scroll, per scene
5 *    (.tmf-space and .tmf-forts both have parallax backgrounds).
6 * 2. Hover-GIF swap: any [data-hover] image (with [data-static]) swaps to its
7 *    animated GIF on hover — lander, astronaut, amp.
8 * 3. Forts carousel: cycles fort "planets" (prev / active / next) via arrows.
9 *
10 * All are progressive enhancement; motion features respect prefers-reduced-
11 * motion, and parallax is disabled on small screens. Vanilla JS, no deps.
12 */
13( function () {
14    'use strict';
15
16    var reduceMotion = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
17    var smallScreen = window.matchMedia( '(max-width: 700px)' );
18
19    /* ---- Parallax (per scene) ----------------------------------------- */
20
21    function initParallax() {
22        if ( reduceMotion ) {
23            return;
24        }
25
26        var scenes = Array.prototype.slice
27            .call( document.querySelectorAll( '.tmf-space, .tmf-forts' ) )
28            .map( function ( el ) {
29                return {
30                    root: el,
31                    layers: Array.prototype.slice.call( el.querySelectorAll( '[data-parallax]' ) ),
32                };
33            } )
34            .filter( function ( s ) {
35                return s.layers.length;
36            } );
37
38        if ( ! scenes.length ) {
39            return;
40        }
41
42        var ticking = false;
43
44        var apply = function () {
45            ticking = false;
46            var small = smallScreen.matches;
47
48            scenes.forEach( function ( scene ) {
49                var rect = scene.root.getBoundingClientRect();
50                var offset = ( rect.top + rect.height / 2 ) - ( window.innerHeight / 2 );
51
52                scene.layers.forEach( function ( l ) {
53                    if ( small ) {
54                        l.style.transform = '';
55                        return;
56                    }
57                    var speed = parseFloat( l.getAttribute( 'data-parallax' ) ) || 0;
58                    l.style.transform = 'translate3d(0,' + ( offset * speed * -0.38 ).toFixed( 1 ) + 'px,0)';
59                } );
60            } );
61        };
62
63        var onScroll = function () {
64            if ( ! ticking ) {
65                ticking = true;
66                window.requestAnimationFrame( apply );
67            }
68        };
69
70        window.addEventListener( 'scroll', onScroll, { passive: true } );
71        window.addEventListener( 'resize', onScroll );
72        apply();
73    }
74
75    /* ---- Generic hover-GIF swap --------------------------------------- */
76
77    function initHoverGifs() {
78        if ( reduceMotion ) {
79            return;
80        }
81
82        var imgs = document.querySelectorAll( 'img[data-hover][data-static]' );
83
84        Array.prototype.forEach.call( imgs, function ( img ) {
85            var staticSrc = img.getAttribute( 'data-static' );
86            var hoverSrc = img.getAttribute( 'data-hover' );
87            if ( ! staticSrc || ! hoverSrc ) {
88                return;
89            }
90
91            img.addEventListener( 'mouseenter', function () {
92                // Cache-bust so the GIF replays from the first frame each time.
93                img.src = hoverSrc + ( hoverSrc.indexOf( '?' ) === -1 ? '?' : '&' ) + 't=' + Date.now();
94            } );
95            img.addEventListener( 'mouseleave', function () {
96                img.src = staticSrc;
97            } );
98        } );
99    }
100
101    /* ---- Forts carousel ----------------------------------------------- */
102
103    function initFortsCarousel() {
104        var stage = document.querySelector( '[data-forts-carousel]' );
105        if ( ! stage ) {
106            return;
107        }
108
109        var items = Array.prototype.slice.call( stage.querySelectorAll( '.tmf-forts__item' ) );
110        var prevBtn = stage.querySelector( '[data-forts-prev]' );
111        var nextBtn = stage.querySelector( '[data-forts-next]' );
112        var count = items.length;
113
114        if ( count < 2 ) {
115            return; // Nothing to cycle.
116        }
117
118        var current = 0;
119        var rels = new Array( count ); // each item's last position (for teleport detection)
120
121        var render = function () {
122            var half = count / 2;
123
124            items.forEach( function ( item, i ) {
125                // Signed distance from the centre, wrapped to the shortest way
126                // around the ring: 0 = centre, -1 = left, +1 = right, and
127                // anything further out is off-stage on that side.
128                var rel = i - current;
129                while ( rel > half ) { rel -= count; }
130                while ( rel < -half ) { rel += count; }
131
132                var zone;
133                if ( rel === 0 ) {
134                    zone = 'is-active';
135                } else if ( rel === -1 ) {
136                    zone = 'is-prev';
137                } else if ( rel === 1 ) {
138                    zone = 'is-next';
139                } else if ( rel < 0 ) {
140                    zone = 'is-offstage-left';
141                } else {
142                    zone = 'is-offstage-right';
143                }
144
145                // The one item that crosses the ring seam jumps more than one
146                // position — reposition it instantly (off-stage, so invisible)
147                // rather than letting it slide across the stage.
148                var teleport = ( typeof rels[ i ] === 'number' ) && Math.abs( rel - rels[ i ] ) > 1;
149                if ( teleport ) {
150                    item.style.transition = 'none';
151                }
152
153                item.classList.remove( 'is-active', 'is-prev', 'is-next', 'is-offstage-left', 'is-offstage-right' );
154                item.classList.add( zone );
155
156                if ( teleport ) {
157                    void item.offsetWidth; // commit the jump before restoring transitions
158                    item.style.transition = '';
159                }
160
161                rels[ i ] = rel;
162
163                // Only the active planet is interactive / in the tab order.
164                var link = item.querySelector( '.tmf-forts__planet' );
165                if ( link ) {
166                    if ( 0 === rel ) {
167                        link.removeAttribute( 'tabindex' );
168                        link.removeAttribute( 'aria-hidden' );
169                    } else {
170                        link.setAttribute( 'tabindex', '-1' );
171                        link.setAttribute( 'aria-hidden', 'true' );
172                    }
173                }
174            } );
175        };
176
177        var go = function ( dir ) {
178            current = ( current + dir + count ) % count;
179            render();
180        };
181
182        if ( prevBtn ) {
183            prevBtn.addEventListener( 'click', function () {
184                go( -1 );
185            } );
186        }
187        if ( nextBtn ) {
188            nextBtn.addEventListener( 'click', function () {
189                go( 1 );
190            } );
191        }
192
193        // Left/right arrow keys when the carousel has focus.
194        stage.addEventListener( 'keydown', function ( e ) {
195            if ( e.key === 'ArrowLeft' ) {
196                go( -1 );
197            } else if ( e.key === 'ArrowRight' ) {
198                go( 1 );
199            }
200        } );
201
202        render();
203    }
204
205    /* ---- Photo carousel (center-emphasis, auto-scroll left) ----------- */
206
207    function initPhotoCarousel() {
208        var track = document.querySelector( '[data-photo-carousel]' );
209        if ( ! track ) {
210            return;
211        }
212
213        var originals = Array.prototype.slice.call( track.children );
214        var n = originals.length;
215        if ( n < 1 ) {
216            return;
217        }
218
219        // For 2+ images, clone a full set before and after so the active slide
220        // always has neighbours to bleed past the edges and the loop is seamless.
221        var looping = n > 1;
222        if ( looping ) {
223            // Keep a reference to the first real slide so the prepended clones
224            // are inserted *in order* before it (inserting before firstChild in
225            // a loop would reverse them and duplicate a neighbour).
226            var firstReal = track.firstChild;
227            originals.forEach( function ( li ) {
228                var c = li.cloneNode( true );
229                c.setAttribute( 'aria-hidden', 'true' );
230                track.insertBefore( c, firstReal );
231            } );
232            originals.forEach( function ( li ) {
233                var c = li.cloneNode( true );
234                c.setAttribute( 'aria-hidden', 'true' );
235                track.appendChild( c );
236            } );
237        }
238
239        var slides = Array.prototype.slice.call( track.children );
240        var index = looping ? n : 0; // first "real" slide
241        var sideW = 0;
242        var centerW = 0;
243
244        // Read the final (post-transition) side/center widths with transitions
245        // disabled, so centering math never sees an in-progress width.
246        function measure() {
247            track.classList.add( 'is-measuring' );
248            slides.forEach( function ( s ) {
249                s.classList.remove( 'is-center' );
250            } );
251            var probe = slides[ index ];
252            sideW = probe.offsetWidth;
253            probe.classList.add( 'is-center' );
254            centerW = probe.offsetWidth;
255            track.classList.remove( 'is-measuring' );
256            // measure() leaves the active slide centered (no animation, since
257            // transitions were disabled during the pass).
258        }
259
260        // Center the active slide. Positions are deterministic — every slide
261        // before the active one is at side width — so we don't read layout
262        // mid-transition.
263        function center( animate ) {
264            // Guard against an out-of-range index from any caller so we never
265            // dereference an undefined slide.
266            if ( ! slides[ index ] ) {
267                index = ( ( index % slides.length ) + slides.length ) % slides.length;
268            }
269
270            // For an instant reposition, freeze transitions on the track AND the
271            // slides *before* changing anything, so neither the transform nor the
272            // width animates; a forced reflow commits the new values, then
273            // transitions are restored.
274            if ( ! animate ) {
275                track.classList.add( 'is-snapping' );
276            }
277
278            slides.forEach( function ( s ) {
279                s.classList.remove( 'is-center' );
280            } );
281            slides[ index ].classList.add( 'is-center' );
282
283            var x = ( window.innerWidth / 2 ) - ( index * sideW + centerW / 2 );
284            track.style.transform = 'translateX(' + x + 'px)';
285
286            if ( ! animate ) {
287                void track.offsetWidth;       // flush the jump
288                track.classList.remove( 'is-snapping' );
289            }
290        }
291
292        measure();
293        center( false );
294        track.classList.add( 'is-ready' );
295
296        if ( ! looping || reduceMotion ) {
297            return; // Single image, or motion is off: leave it centered/static.
298        }
299
300        // When we've advanced one full set past the originals, jump back by a
301        // set with no animation — the slides repeat, so it's invisible.
302        track.addEventListener( 'transitionend', function ( e ) {
303            if ( e.propertyName === 'transform' && index >= 2 * n ) {
304                index -= n;
305                center( false );
306            }
307        } );
308
309        var timer = null;
310        function start() {
311            stop();
312            timer = window.setInterval( function () {
313                // Safety net: the transitionend handler normally snaps index
314                // back by a set once it reaches the trailing clones. That event
315                // can be missed (e.g. a backgrounded tab suppresses transition
316                // events while this interval keeps firing), which would let
317                // index climb until slides[index] is undefined. Re-normalise
318                // here — the clone sets are identical, so an untransitioned
319                // snap-back is invisible — so index can never leave range.
320                if ( index >= 2 * n ) {
321                    index -= n;
322                    center( false );
323                }
324                index++;
325                center( true );
326            }, 2500 );
327        }
328        function stop() {
329            if ( timer ) {
330                window.clearInterval( timer );
331                timer = null;
332            }
333        }
334
335        start();
336        // Deliberately NO pause-on-hover: on touch devices `mouseenter` fires on
337        // tap but `mouseleave` frequently doesn't, which left the carousel stuck.
338        // It now scrolls continuously no matter what.
339
340        // Widths are vw-based, so re-measure and re-center on resize.
341        var rt = null;
342        window.addEventListener( 'resize', function () {
343            window.clearTimeout( rt );
344            rt = window.setTimeout( function () {
345                measure();
346                center( false );
347            }, 150 );
348        } );
349    }
350
351    /* ---- Hero background video ---------------------------------------- */
352
353    function initHeroVideo() {
354        var video = document.querySelector( '.tmf-hero__video' );
355        if ( ! video ) {
356            return;
357        }
358
359        // Reduced motion: don't play a moving background. The hero image (poster
360        // + section background) stays as the still hero, and we drop the source
361        // so nothing downloads.
362        if ( reduceMotion ) {
363            video.removeAttribute( 'autoplay' );
364            while ( video.firstChild ) {
365                video.removeChild( video.firstChild );
366            }
367            video.load();
368            video.remove();
369            return;
370        }
371
372        // Fade the video in only once it's actually rendering frames, so the
373        // hero image carries the load and there's no still→motion "pop". CSS
374        // starts it at opacity 0 (.tmf-hero--has-video .tmf-hero__video).
375        var reveal = function () {
376            video.classList.add( 'is-playing' );
377        };
378        if ( ! video.paused && video.readyState >= 3 ) {
379            reveal(); // already playing (e.g. cached)
380        } else {
381            video.addEventListener( 'playing', reveal, { once: true } );
382        }
383
384        // Some browsers won't honour the autoplay attribute until asked; nudge
385        // it, and ignore the promise rejection if the policy still blocks it
386        // (the poster/image simply remains).
387        var attempt = video.play();
388        if ( attempt && typeof attempt.catch === 'function' ) {
389            attempt.catch( function () {} );
390        }
391    }
392
393    initParallax();
394    initHoverGifs();
395    initFortsCarousel();
396    initPhotoCarousel();
397    initHeroVideo();
398}() );

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.