Skip to content

Commit a9802f6

Browse files
committed
Pause/play landing page carousels on mousein/mouseout
1 parent 51814d7 commit a9802f6

2 files changed

Lines changed: 45 additions & 18 deletions

File tree

src/components/QuoteTicker.astro

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
/**
33
* Infinite horizontally scrolling ticker using CSS animation.
44
* Two copies of the content sit side-by-side and translate continuously.
5-
* Hover pause is pure CSS (:hover) to avoid JS-induced animation jumps.
6-
* JS handles: touch pause, per-card colors, and reshuffling on each cycle.
5+
* JS smoothly ramps playbackRate on hover for gentle pause/resume.
76
*/
87
interface Props {
98
direction?: 'left' | 'right';
@@ -14,7 +13,6 @@ interface Props {
1413
1514
const { direction = 'left', variant = 'press', duration = 60 } = Astro.props;
1615
const animName = direction === 'right' ? 'ticker-scroll-right' : 'ticker-scroll-left';
17-
// Add ±10% random jitter so tickers don't appear perfectly synchronized
1816
const jitter = duration * (0.9 + Math.random() * 0.2);
1917
const actualDuration = Math.round(jitter);
2018
---
@@ -38,10 +36,11 @@ const actualDuration = Math.round(jitter);
3836
* SPDX-License-Identifier: Apache-2.0
3937
*/
4038
(function() {
41-
if (window.__tickerV5) return;
42-
window.__tickerV5 = true;
39+
if (window.__tickerV6) return;
40+
window.__tickerV6 = true;
4341

4442
var TOUCH_PAUSE_MS = 10000;
43+
var RAMP_MS = 600;
4544
var PALETTE = [
4645
{h:207,s:70},{h:220,s:65},{h:190,s:60},{h:260,s:55},{h:340,s:60},
4746
{h:15,s:65},{h:170,s:50},{h:280,s:50},{h:45,s:55},{h:150,s:45}
@@ -52,7 +51,6 @@ const actualDuration = Math.round(jitter);
5251
else window.addEventListener('load', fn);
5352
}
5453

55-
// Fisher-Yates shuffle for DOM children
5654
function shuffleChildren(container) {
5755
var children = Array.from(container.children);
5856
for (var i = children.length - 1; i > 0; i--) {
@@ -62,6 +60,25 @@ const actualDuration = Math.round(jitter);
6260
}
6361
}
6462

63+
// Smoothly ramp playbackRate from current to target over RAMP_MS
64+
function rampRate(anim, target, ms) {
65+
if (!anim) return;
66+
var start = anim.playbackRate;
67+
var startTime = null;
68+
function step(ts) {
69+
if (!startTime) startTime = ts;
70+
var elapsed = ts - startTime;
71+
var progress = Math.min(elapsed / ms, 1);
72+
// Ease-in-out cubic
73+
var ease = progress < 0.5
74+
? 4 * progress * progress * progress
75+
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
76+
anim.playbackRate = start + (target - start) * ease;
77+
if (progress < 1) requestAnimationFrame(step);
78+
}
79+
requestAnimationFrame(step);
80+
}
81+
6582
ready(function() {
6683
// Assign per-card colors
6784
document.querySelectorAll('.ticker-card').forEach(function(card) {
@@ -83,18 +100,33 @@ const actualDuration = Math.round(jitter);
83100
var halves = track.querySelectorAll('.ticker-half');
84101
var touchTimer = null;
85102

86-
// Touch-only pause (mouse hover is handled by pure CSS)
103+
// Get the CSS animation object from the track element
104+
function getAnim() {
105+
var anims = track.getAnimations();
106+
return anims.length > 0 ? anims[0] : null;
107+
}
108+
109+
// Mouse hover: gently ramp to 0 on enter, back to 1 on leave
110+
el.addEventListener('pointerenter', function(e) {
111+
if (e.pointerType !== 'mouse') return;
112+
rampRate(getAnim(), 0, RAMP_MS);
113+
});
114+
el.addEventListener('pointerleave', function(e) {
115+
if (e.pointerType !== 'mouse') return;
116+
rampRate(getAnim(), 1, RAMP_MS);
117+
});
118+
119+
// Touch: instant pause, resume after timeout
87120
el.addEventListener('touchstart', function() {
88-
track.style.animationPlayState = 'paused';
121+
var anim = getAnim();
122+
if (anim) anim.playbackRate = 0;
89123
if (touchTimer) clearTimeout(touchTimer);
90124
touchTimer = setTimeout(function() {
91-
track.style.animationPlayState = '';
125+
rampRate(getAnim(), 1, RAMP_MS);
92126
}, TOUCH_PAUSE_MS);
93127
}, {passive: true});
94128

95-
// Reshuffle both halves when the animation completes one full cycle.
96-
// The shuffle happens at the exact loop point where the two halves
97-
// are visually identical, so the reorder is invisible to the user.
129+
// Reshuffle on each animation cycle
98130
track.addEventListener('animationiteration', function() {
99131
for (var i = 0; i < halves.length; i++) {
100132
shuffleChildren(halves[i]);

src/styles/landing.css

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -643,12 +643,7 @@
643643
direction: initial;
644644
}
645645

646-
/* Pure CSS hover pause — no JS needed, no animation jump */
647-
@media (hover: hover) {
648-
.ticker:hover .ticker-track {
649-
animation-play-state: paused;
650-
}
651-
}
646+
/* Hover pause is handled by JS (smooth playbackRate ramp) */
652647

653648
.ticker-half {
654649
display: flex;

0 commit comments

Comments
 (0)