Skip to content

Commit 2c8b86e

Browse files
authored
Merge pull request #682 from DavidHDev/copilot/enhance-logoloop-flexibility
Add hoverSpeed, renderItem props and vertical direction support to LogoLoop
2 parents dab207e + 095f68c commit 2c8b86e

File tree

12 files changed

+497
-162
lines changed

12 files changed

+497
-162
lines changed

public/r/LogoLoop-JS-CSS.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

public/r/LogoLoop-JS-TW.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/r/LogoLoop-TS-CSS.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

public/r/LogoLoop-TS-TW.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/constants/code/Animations/logoLoopCode.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,30 @@ const imageLogos = [
2525
function App() {
2626
return (
2727
<div style={{ height: '200px', position: 'relative', overflow: 'hidden'}}>
28+
{/* Basic horizontal loop */}
2829
<LogoLoop
2930
logos={techLogos}
3031
speed={120}
3132
direction="left"
3233
logoHeight={48}
3334
gap={40}
34-
pauseOnHover
35+
hoverSpeed={0}
3536
scaleOnHover
3637
fadeOut
3738
fadeOutColor="#ffffff"
3839
ariaLabel="Technology partners"
3940
/>
41+
42+
{/* Vertical loop with deceleration on hover */}
43+
<LogoLoop
44+
logos={techLogos}
45+
speed={80}
46+
direction="up"
47+
logoHeight={48}
48+
gap={40}
49+
hoverSpeed={20}
50+
fadeOut
51+
/>
4052
</div>
4153
);
4254
}`,

src/content/Animations/LogoLoop/LogoLoop.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
--logoloop-fadeColorAuto: #ffffff;
88
}
99

10+
.logoloop--vertical {
11+
overflow-x: visible;
12+
overflow-y: hidden;
13+
}
14+
1015
.logoloop--scale-hover {
1116
padding-top: calc(var(--logoloop-logoHeight) * 0.1);
1217
padding-bottom: calc(var(--logoloop-logoHeight) * 0.1);
@@ -25,22 +30,42 @@
2530
user-select: none;
2631
}
2732

33+
.logoloop--vertical .logoloop__track {
34+
flex-direction: column;
35+
height: max-content;
36+
width: auto;
37+
}
38+
2839
.logoloop__list {
2940
display: flex;
3041
align-items: center;
3142
}
3243

44+
.logoloop--vertical .logoloop__list {
45+
flex-direction: column;
46+
}
47+
3348
.logoloop__item {
3449
flex: 0 0 auto;
3550
margin-right: var(--logoloop-gap);
3651
font-size: var(--logoloop-logoHeight);
3752
line-height: 1;
3853
}
3954

55+
.logoloop--vertical .logoloop__item {
56+
margin-right: 0;
57+
margin-bottom: var(--logoloop-gap);
58+
}
59+
4060
.logoloop__item:last-child {
4161
margin-right: var(--logoloop-gap);
4262
}
4363

64+
.logoloop--vertical .logoloop__item:last-child {
65+
margin-right: 0;
66+
margin-bottom: var(--logoloop-gap);
67+
}
68+
4469
.logoloop__node {
4570
display: inline-flex;
4671
align-items: center;

src/content/Animations/LogoLoop/LogoLoop.jsx

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const useImageLoader = (seqRef, onLoad, dependencies) => {
7171
}, dependencies);
7272
};
7373

74-
const useAnimationLoop = (trackRef, targetVelocity, seqWidth, isHovered, pauseOnHover) => {
74+
const useAnimationLoop = (trackRef, targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical) => {
7575
const rafRef = useRef(null);
7676
const lastTimestampRef = useRef(null);
7777
const offsetRef = useRef(0);
@@ -81,9 +81,14 @@ const useAnimationLoop = (trackRef, targetVelocity, seqWidth, isHovered, pauseOn
8181
const track = trackRef.current;
8282
if (!track) return;
8383

84-
if (seqWidth > 0) {
85-
offsetRef.current = ((offsetRef.current % seqWidth) + seqWidth) % seqWidth;
86-
track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
84+
const seqSize = isVertical ? seqHeight : seqWidth;
85+
86+
if (seqSize > 0) {
87+
offsetRef.current = ((offsetRef.current % seqSize) + seqSize) % seqSize;
88+
const transformValue = isVertical
89+
? `translate3d(0, ${-offsetRef.current}px, 0)`
90+
: `translate3d(${-offsetRef.current}px, 0, 0)`;
91+
track.style.transform = transformValue;
8792
}
8893

8994
const animate = timestamp => {
@@ -94,18 +99,20 @@ const useAnimationLoop = (trackRef, targetVelocity, seqWidth, isHovered, pauseOn
9499
const deltaTime = Math.max(0, timestamp - lastTimestampRef.current) / 1000;
95100
lastTimestampRef.current = timestamp;
96101

97-
const target = pauseOnHover && isHovered ? 0 : targetVelocity;
102+
const target = isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity;
98103

99104
const easingFactor = 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);
100105
velocityRef.current += (target - velocityRef.current) * easingFactor;
101106

102-
if (seqWidth > 0) {
107+
if (seqSize > 0) {
103108
let nextOffset = offsetRef.current + velocityRef.current * deltaTime;
104-
nextOffset = ((nextOffset % seqWidth) + seqWidth) % seqWidth;
109+
nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;
105110
offsetRef.current = nextOffset;
106111

107-
const translateX = -offsetRef.current;
108-
track.style.transform = `translate3d(${translateX}px, 0, 0)`;
112+
const transformValue = isVertical
113+
? `translate3d(0, ${-offsetRef.current}px, 0)`
114+
: `translate3d(${-offsetRef.current}px, 0, 0)`;
115+
track.style.transform = transformValue;
109116
}
110117

111118
rafRef.current = requestAnimationFrame(animate);
@@ -120,7 +127,7 @@ const useAnimationLoop = (trackRef, targetVelocity, seqWidth, isHovered, pauseOn
120127
}
121128
lastTimestampRef.current = null;
122129
};
123-
}, [targetVelocity, seqWidth, isHovered, pauseOnHover, trackRef]);
130+
}, [targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical, trackRef]);
124131
};
125132

126133
export const LogoLoop = memo(
@@ -131,10 +138,12 @@ export const LogoLoop = memo(
131138
width = '100%',
132139
logoHeight = 28,
133140
gap = 32,
134-
pauseOnHover = true,
141+
pauseOnHover,
142+
hoverSpeed,
135143
fadeOut = false,
136144
fadeOutColor,
137145
scaleOnHover = false,
146+
renderItem,
138147
ariaLabel = 'Partner logos',
139148
className,
140149
style
@@ -144,32 +153,60 @@ export const LogoLoop = memo(
144153
const seqRef = useRef(null);
145154

146155
const [seqWidth, setSeqWidth] = useState(0);
156+
const [seqHeight, setSeqHeight] = useState(0);
147157
const [copyCount, setCopyCount] = useState(ANIMATION_CONFIG.MIN_COPIES);
148158
const [isHovered, setIsHovered] = useState(false);
149159

160+
// Determine the effective hover speed (support backward compatibility)
161+
const effectiveHoverSpeed = useMemo(() => {
162+
if (hoverSpeed !== undefined) return hoverSpeed;
163+
if (pauseOnHover === true) return 0;
164+
if (pauseOnHover === false) return undefined;
165+
// Default behavior: pause on hover
166+
return 0;
167+
}, [hoverSpeed, pauseOnHover]);
168+
169+
const isVertical = direction === 'up' || direction === 'down';
170+
150171
const targetVelocity = useMemo(() => {
151172
const magnitude = Math.abs(speed);
152-
const directionMultiplier = direction === 'left' ? 1 : -1;
173+
let directionMultiplier;
174+
if (isVertical) {
175+
directionMultiplier = direction === 'up' ? 1 : -1;
176+
} else {
177+
directionMultiplier = direction === 'left' ? 1 : -1;
178+
}
153179
const speedMultiplier = speed < 0 ? -1 : 1;
154180
return magnitude * directionMultiplier * speedMultiplier;
155-
}, [speed, direction]);
181+
}, [speed, direction, isVertical]);
156182

157183
const updateDimensions = useCallback(() => {
158184
const containerWidth = containerRef.current?.clientWidth ?? 0;
159-
const sequenceWidth = seqRef.current?.getBoundingClientRect?.()?.width ?? 0;
160-
161-
if (sequenceWidth > 0) {
162-
setSeqWidth(Math.ceil(sequenceWidth));
163-
const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;
164-
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
185+
const containerHeight = containerRef.current?.clientHeight ?? 0;
186+
const sequenceRect = seqRef.current?.getBoundingClientRect?.();
187+
const sequenceWidth = sequenceRect?.width ?? 0;
188+
const sequenceHeight = sequenceRect?.height ?? 0;
189+
190+
if (isVertical) {
191+
if (sequenceHeight > 0) {
192+
setSeqHeight(Math.ceil(sequenceHeight));
193+
const copiesNeeded = Math.ceil(containerHeight / sequenceHeight) + ANIMATION_CONFIG.COPY_HEADROOM;
194+
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
195+
}
196+
} else {
197+
if (sequenceWidth > 0) {
198+
setSeqWidth(Math.ceil(sequenceWidth));
199+
const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;
200+
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
201+
}
165202
}
166-
}, []);
203+
}, [isVertical]);
167204

168-
useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight]);
205+
useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight, isVertical]);
169206

170-
useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight]);
207+
useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight, isVertical]);
171208

172-
useAnimationLoop(trackRef, targetVelocity, seqWidth, isHovered, pauseOnHover);
209+
useAnimationLoop(trackRef, targetVelocity, seqWidth, seqHeight, isHovered, effectiveHoverSpeed, isVertical);
173210

174211
const cssVariables = useMemo(
175212
() => ({
@@ -182,21 +219,37 @@ export const LogoLoop = memo(
182219

183220
const rootClassName = useMemo(
184221
() =>
185-
['logoloop', fadeOut && 'logoloop--fade', scaleOnHover && 'logoloop--scale-hover', className]
222+
[
223+
'logoloop',
224+
isVertical ? 'logoloop--vertical' : 'logoloop--horizontal',
225+
fadeOut && 'logoloop--fade',
226+
scaleOnHover && 'logoloop--scale-hover',
227+
className
228+
]
186229
.filter(Boolean)
187230
.join(' '),
188-
[fadeOut, scaleOnHover, className]
231+
[isVertical, fadeOut, scaleOnHover, className]
189232
);
190233

191234
const handleMouseEnter = useCallback(() => {
192-
if (pauseOnHover) setIsHovered(true);
193-
}, [pauseOnHover]);
235+
if (effectiveHoverSpeed !== undefined) setIsHovered(true);
236+
}, [effectiveHoverSpeed]);
194237

195238
const handleMouseLeave = useCallback(() => {
196-
if (pauseOnHover) setIsHovered(false);
197-
}, [pauseOnHover]);
239+
if (effectiveHoverSpeed !== undefined) setIsHovered(false);
240+
}, [effectiveHoverSpeed]);
198241

199242
const renderLogoItem = useCallback((item, key) => {
243+
// If renderItem prop is provided, use it
244+
if (renderItem) {
245+
return (
246+
<li className="logoloop__item" key={key} role="listitem">
247+
{renderItem(item, key)}
248+
</li>
249+
);
250+
}
251+
252+
// Default rendering logic
200253
const isNodeItem = 'node' in item;
201254

202255
const content = isNodeItem ? (
@@ -239,7 +292,7 @@ export const LogoLoop = memo(
239292
{itemContent}
240293
</li>
241294
);
242-
}, []);
295+
}, [renderItem]);
243296

244297
const logoLists = useMemo(
245298
() =>

0 commit comments

Comments
 (0)