|
88 | 88 | } |
89 | 89 | }); |
90 | 90 |
|
91 | | - // Measure tig character widths after fonts load, so the swap animation is pixel-perfect |
| 91 | + // Measure tig character widths after fonts load, so the swap animation is pixel-perfect. |
| 92 | + // Also listens for animationend to hand off from the initial CSS animation to JS control. |
92 | 93 | $effect(() => { |
93 | 94 | if (!tigGroupEl || !tigTEl || !tigIEl || !tigGEl) return; |
94 | 95 | const group = tigGroupEl; |
|
104 | 105 | group.style.setProperty('--tig-dg', `${(wt + wi) / fontSize}em`); |
105 | 106 | group.style.setProperty('--tig-di', `${(wg - wt) / fontSize}em`); |
106 | 107 | }); |
| 108 | + group.addEventListener('animationend', handleTigAnimationEnd); |
| 109 | + return () => group.removeEventListener('animationend', handleTigAnimationEnd); |
| 110 | + }); |
| 111 | +
|
| 112 | + // --- Tig↔git interactive hover swap --- |
| 113 | + type SwapPhase = |
| 114 | + | 'initial' |
| 115 | + | 'showing-git' |
| 116 | + | 'hover-pending' |
| 117 | + | 'animating-to-tig' |
| 118 | + | 'showing-tig' |
| 119 | + | 'animating-to-git'; |
| 120 | +
|
| 121 | + let swapPhase: SwapPhase = 'initial'; |
| 122 | + let swapNeedsFreshEnter = true; |
| 123 | + let swapIsHovering = false; |
| 124 | + let swapHoverTimer: ReturnType<typeof setTimeout> | undefined; |
| 125 | + let swapRevertTimer: ReturnType<typeof setTimeout> | undefined; |
| 126 | + let swapAnim: Animation | null = null; |
| 127 | +
|
| 128 | + const swapAnimate = (from: number, to: number, onDone: () => void) => { |
| 129 | + swapAnim?.cancel(); |
| 130 | + swapAnim = null; |
| 131 | + if (!tigGroupEl) return; |
| 132 | + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| 133 | + tigGroupEl.style.setProperty('--tig-swap', String(from)); |
| 134 | + swapAnim = tigGroupEl.animate([{ '--tig-swap': String(from) }, { '--tig-swap': String(to) }], { |
| 135 | + duration: reducedMotion ? 1 : 2000, |
| 136 | + easing: 'linear', |
| 137 | + fill: 'forwards' |
| 138 | + }); |
| 139 | + swapAnim.addEventListener( |
| 140 | + 'finish', |
| 141 | + () => { |
| 142 | + tigGroupEl?.style.setProperty('--tig-swap', String(to)); |
| 143 | + swapAnim?.cancel(); |
| 144 | + swapAnim = null; |
| 145 | + onDone(); |
| 146 | + }, |
| 147 | + { once: true } |
| 148 | + ); |
| 149 | + }; |
| 150 | +
|
| 151 | + const swapStartRevertTimer = () => { |
| 152 | + if (swapRevertTimer) clearTimeout(swapRevertTimer); |
| 153 | + swapRevertTimer = setTimeout(() => { |
| 154 | + swapRevertTimer = undefined; |
| 155 | + swapPhase = 'animating-to-git'; |
| 156 | + swapAnimate(0, 1, () => { |
| 157 | + swapPhase = 'showing-git'; |
| 158 | + swapNeedsFreshEnter = swapIsHovering; |
| 159 | + }); |
| 160 | + }, 5000); |
| 161 | + }; |
| 162 | +
|
| 163 | + const handleTigAnimationEnd = (e: AnimationEvent) => { |
| 164 | + if (e.animationName !== 'tig-swap-anim' || swapPhase !== 'initial') return; |
| 165 | + if (!tigGroupEl) return; |
| 166 | + tigGroupEl.style.animation = 'none'; |
| 167 | + tigGroupEl.style.setProperty('--tig-swap', '1'); |
| 168 | + swapPhase = 'showing-git'; |
| 169 | + swapNeedsFreshEnter = swapIsHovering; |
| 170 | + }; |
| 171 | +
|
| 172 | + const handleTigMouseEnter = () => { |
| 173 | + swapIsHovering = true; |
| 174 | + if (swapPhase === 'showing-git' && !swapNeedsFreshEnter) { |
| 175 | + swapPhase = 'hover-pending'; |
| 176 | + swapHoverTimer = setTimeout(() => { |
| 177 | + swapHoverTimer = undefined; |
| 178 | + swapPhase = 'animating-to-tig'; |
| 179 | + swapAnimate(1, 0, () => { |
| 180 | + swapPhase = 'showing-tig'; |
| 181 | + if (!swapIsHovering) swapStartRevertTimer(); |
| 182 | + }); |
| 183 | + }, 500); |
| 184 | + } else if (swapPhase === 'showing-tig') { |
| 185 | + if (swapRevertTimer) { |
| 186 | + clearTimeout(swapRevertTimer); |
| 187 | + swapRevertTimer = undefined; |
| 188 | + } |
| 189 | + } |
| 190 | + }; |
| 191 | +
|
| 192 | + const handleTigMouseLeave = () => { |
| 193 | + swapIsHovering = false; |
| 194 | + if (swapPhase === 'showing-git') { |
| 195 | + swapNeedsFreshEnter = false; |
| 196 | + } else if (swapPhase === 'hover-pending') { |
| 197 | + if (swapHoverTimer) clearTimeout(swapHoverTimer); |
| 198 | + swapHoverTimer = undefined; |
| 199 | + swapPhase = 'showing-git'; |
| 200 | + } else if (swapPhase === 'showing-tig') { |
| 201 | + swapStartRevertTimer(); |
| 202 | + } |
| 203 | + }; |
| 204 | +
|
| 205 | + // Cleanup swap timers/animation on unmount |
| 206 | + $effect(() => { |
| 207 | + return () => { |
| 208 | + if (swapHoverTimer) clearTimeout(swapHoverTimer); |
| 209 | + if (swapRevertTimer) clearTimeout(swapRevertTimer); |
| 210 | + swapAnim?.cancel(); |
| 211 | + }; |
107 | 212 | }); |
108 | 213 |
|
109 | 214 | const startTimer = (kind: 'clone' | 'process') => { |
|
366 | 471 | <div class="relative py-4 text-center sm:py-6"> |
367 | 472 | <div class="strata-hero-lines"></div> |
368 | 473 | <div class="relative"> |
| 474 | + <!-- Decorative tig↔git animation easter egg, not an interactive control --> |
| 475 | + <!-- svelte-ignore a11y_no_static_element_interactions --> |
369 | 476 | <!-- prettier-ignore --> |
370 | 477 | <h1 |
371 | 478 | class="text-3xl font-bold tracking-tight text-[var(--color-text)] sm:text-4xl lg:text-5xl" |
372 | 479 | style="font-family: var(--font-sans); letter-spacing: -0.025em;" |
373 | | - >Stra<span class="tig-group" bind:this={tigGroupEl}><span class="tig-char tig-t text-[var(--color-accent)]" bind:this={tigTEl}>t</span><span class="tig-char tig-i text-[var(--color-accent)]" bind:this={tigIEl}>i</span><span class="tig-char tig-g text-[var(--color-accent)]" bind:this={tigGEl}>g</span></span>raphy for your code</h1> |
| 480 | + >Stra<span |
| 481 | + class="tig-group" |
| 482 | + bind:this={tigGroupEl} |
| 483 | + onmouseenter={handleTigMouseEnter} |
| 484 | + onmouseleave={handleTigMouseLeave} |
| 485 | + ><span class="tig-char tig-t text-[var(--color-accent)]" bind:this={tigTEl}>t</span><span class="tig-char tig-i text-[var(--color-accent)]" bind:this={tigIEl}>i</span><span class="tig-char tig-g text-[var(--color-accent)]" bind:this={tigGEl}>g</span></span>raphy for your code</h1> |
374 | 486 | <p |
375 | 487 | class="mx-auto mt-4 max-w-lg text-sm leading-relaxed text-[var(--color-text-secondary)] sm:text-base" |
376 | 488 | style="font-family: var(--font-sans);" |
|
0 commit comments