Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions public/r/LogoLoop-JS-CSS.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/r/LogoLoop-JS-TW.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/r/LogoLoop-TS-CSS.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/r/LogoLoop-TS-TW.json

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion src/constants/code/Animations/logoLoopCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,41 @@ const imageLogos = [
function App() {
return (
<div style={{ height: '200px', position: 'relative', overflow: 'hidden'}}>
{/* Basic horizontal loop */}
<LogoLoop
logos={techLogos}
speed={120}
direction="left"
logoHeight={48}
gap={40}
pauseOnHover
hoverSpeed={0}
scaleOnHover
fadeOut
fadeOutColor="#ffffff"
ariaLabel="Technology partners"
/>

{/* Vertical loop with deceleration on hover */}
<LogoLoop
logos={techLogos}
speed={80}
direction="up"
logoHeight={48}
gap={40}
hoverSpeed={20}
fadeOut
/>

{/* Custom rendering with renderItem */}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot remove this example from the usage code, it's not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the renderItem example from the usage code as requested. (ec44449)

<LogoLoop
logos={techLogos}
speed={100}
renderItem={(item, key) => (
<div className="custom-logo-wrapper">
{'node' in item ? item.node : <img src={item.src} alt={item.alt} />}
</div>
)}
/>
</div>
);
}`,
Expand Down
25 changes: 25 additions & 0 deletions src/content/Animations/LogoLoop/LogoLoop.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
--logoloop-fadeColorAuto: #ffffff;
}

.logoloop--vertical {
overflow-x: visible;
overflow-y: hidden;
}

.logoloop--scale-hover {
padding-top: calc(var(--logoloop-logoHeight) * 0.1);
padding-bottom: calc(var(--logoloop-logoHeight) * 0.1);
Expand All @@ -25,22 +30,42 @@
user-select: none;
}

.logoloop--vertical .logoloop__track {
flex-direction: column;
height: max-content;
width: auto;
}

.logoloop__list {
display: flex;
align-items: center;
}

.logoloop--vertical .logoloop__list {
flex-direction: column;
}

.logoloop__item {
flex: 0 0 auto;
margin-right: var(--logoloop-gap);
font-size: var(--logoloop-logoHeight);
line-height: 1;
}

.logoloop--vertical .logoloop__item {
margin-right: 0;
margin-bottom: var(--logoloop-gap);
}

.logoloop__item:last-child {
margin-right: var(--logoloop-gap);
}

.logoloop--vertical .logoloop__item:last-child {
margin-right: 0;
margin-bottom: var(--logoloop-gap);
}

.logoloop__node {
display: inline-flex;
align-items: center;
Expand Down
113 changes: 83 additions & 30 deletions src/content/Animations/LogoLoop/LogoLoop.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const useImageLoader = (seqRef, onLoad, dependencies) => {
}, dependencies);
};

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

if (seqWidth > 0) {
offsetRef.current = ((offsetRef.current % seqWidth) + seqWidth) % seqWidth;
track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
const seqSize = isVertical ? seqHeight : seqWidth;

if (seqSize > 0) {
offsetRef.current = ((offsetRef.current % seqSize) + seqSize) % seqSize;
const transformValue = isVertical
? `translate3d(0, ${-offsetRef.current}px, 0)`
: `translate3d(${-offsetRef.current}px, 0, 0)`;
track.style.transform = transformValue;
}

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

const target = pauseOnHover && isHovered ? 0 : targetVelocity;
const target = isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity;

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

if (seqWidth > 0) {
if (seqSize > 0) {
let nextOffset = offsetRef.current + velocityRef.current * deltaTime;
nextOffset = ((nextOffset % seqWidth) + seqWidth) % seqWidth;
nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;
offsetRef.current = nextOffset;

const translateX = -offsetRef.current;
track.style.transform = `translate3d(${translateX}px, 0, 0)`;
const transformValue = isVertical
? `translate3d(0, ${-offsetRef.current}px, 0)`
: `translate3d(${-offsetRef.current}px, 0, 0)`;
track.style.transform = transformValue;
}

rafRef.current = requestAnimationFrame(animate);
Expand All @@ -120,7 +127,7 @@ const useAnimationLoop = (trackRef, targetVelocity, seqWidth, isHovered, pauseOn
}
lastTimestampRef.current = null;
};
}, [targetVelocity, seqWidth, isHovered, pauseOnHover, trackRef]);
}, [targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical, trackRef]);
};

export const LogoLoop = memo(
Expand All @@ -131,10 +138,12 @@ export const LogoLoop = memo(
width = '100%',
logoHeight = 28,
gap = 32,
pauseOnHover = true,
pauseOnHover,
hoverSpeed,
fadeOut = false,
fadeOutColor,
scaleOnHover = false,
renderItem,
ariaLabel = 'Partner logos',
className,
style
Expand All @@ -144,32 +153,60 @@ export const LogoLoop = memo(
const seqRef = useRef(null);

const [seqWidth, setSeqWidth] = useState(0);
const [seqHeight, setSeqHeight] = useState(0);
const [copyCount, setCopyCount] = useState(ANIMATION_CONFIG.MIN_COPIES);
const [isHovered, setIsHovered] = useState(false);

// Determine the effective hover speed (support backward compatibility)
const effectiveHoverSpeed = useMemo(() => {
if (hoverSpeed !== undefined) return hoverSpeed;
if (pauseOnHover === true) return 0;
if (pauseOnHover === false) return undefined;
// Default behavior: pause on hover
return 0;
}, [hoverSpeed, pauseOnHover]);

const isVertical = direction === 'up' || direction === 'down';

const targetVelocity = useMemo(() => {
const magnitude = Math.abs(speed);
const directionMultiplier = direction === 'left' ? 1 : -1;
let directionMultiplier;
if (isVertical) {
directionMultiplier = direction === 'up' ? 1 : -1;
} else {
directionMultiplier = direction === 'left' ? 1 : -1;
}
const speedMultiplier = speed < 0 ? -1 : 1;
return magnitude * directionMultiplier * speedMultiplier;
}, [speed, direction]);
}, [speed, direction, isVertical]);

const updateDimensions = useCallback(() => {
const containerWidth = containerRef.current?.clientWidth ?? 0;
const sequenceWidth = seqRef.current?.getBoundingClientRect?.()?.width ?? 0;

if (sequenceWidth > 0) {
setSeqWidth(Math.ceil(sequenceWidth));
const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
const containerHeight = containerRef.current?.clientHeight ?? 0;
const sequenceRect = seqRef.current?.getBoundingClientRect?.();
const sequenceWidth = sequenceRect?.width ?? 0;
const sequenceHeight = sequenceRect?.height ?? 0;

if (isVertical) {
if (sequenceHeight > 0) {
setSeqHeight(Math.ceil(sequenceHeight));
const copiesNeeded = Math.ceil(containerHeight / sequenceHeight) + ANIMATION_CONFIG.COPY_HEADROOM;
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
}
} else {
if (sequenceWidth > 0) {
setSeqWidth(Math.ceil(sequenceWidth));
const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
}
}
}, []);
}, [isVertical]);

useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight]);
useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight, isVertical]);

useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight]);
useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight, isVertical]);

useAnimationLoop(trackRef, targetVelocity, seqWidth, isHovered, pauseOnHover);
useAnimationLoop(trackRef, targetVelocity, seqWidth, seqHeight, isHovered, effectiveHoverSpeed, isVertical);

const cssVariables = useMemo(
() => ({
Expand All @@ -182,21 +219,37 @@ export const LogoLoop = memo(

const rootClassName = useMemo(
() =>
['logoloop', fadeOut && 'logoloop--fade', scaleOnHover && 'logoloop--scale-hover', className]
[
'logoloop',
isVertical ? 'logoloop--vertical' : 'logoloop--horizontal',
fadeOut && 'logoloop--fade',
scaleOnHover && 'logoloop--scale-hover',
className
]
.filter(Boolean)
.join(' '),
[fadeOut, scaleOnHover, className]
[isVertical, fadeOut, scaleOnHover, className]
);

const handleMouseEnter = useCallback(() => {
if (pauseOnHover) setIsHovered(true);
}, [pauseOnHover]);
if (effectiveHoverSpeed !== undefined) setIsHovered(true);
}, [effectiveHoverSpeed]);

const handleMouseLeave = useCallback(() => {
if (pauseOnHover) setIsHovered(false);
}, [pauseOnHover]);
if (effectiveHoverSpeed !== undefined) setIsHovered(false);
}, [effectiveHoverSpeed]);

const renderLogoItem = useCallback((item, key) => {
// If renderItem prop is provided, use it
if (renderItem) {
return (
<li className="logoloop__item" key={key} role="listitem">
{renderItem(item, key)}
</li>
);
}

// Default rendering logic
const isNodeItem = 'node' in item;

const content = isNodeItem ? (
Expand Down Expand Up @@ -239,7 +292,7 @@ export const LogoLoop = memo(
{itemContent}
</li>
);
}, []);
}, [renderItem]);

const logoLists = useMemo(
() =>
Expand Down
Loading