The source code for our eny.social landing page, which is mirrored in a different repository as part of the CI setup. eny.social
social-network eny local-first
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(RememberWhen): refine crossfade transitions and timing

+35 -13
+35 -13
app/components/RememberWhen.tsx
··· 7 7 8 8 // Active GIF duration per slot (must match `items` order). 9 9 // Adjust these values to control how long each GIF stays active. 10 - const GIF_INTERVALS_MS = [3000, 3000, 3000] as const; 11 - const FADE_MS = 600; 10 + const GIF_INTERVALS_MS = [4000, 4000, 4000] as const; 11 + const FADE_IN_MS = 1000; 12 + const FADE_OUT_MS = 1000; 13 + const OVERLAP_MS = 100; 14 + const FADE_EASING = "linear"; 12 15 13 16 export default function RememberWhen() { 14 17 const items = useMemo( ··· 37 40 38 41 const [activeIndex, setActiveIndex] = useState(0); 39 42 const [overlayOpacity, setOverlayOpacity] = useState(1); 43 + const [fadeMs, setFadeMs] = useState(FADE_IN_MS); 40 44 const [staticFrameByIndex, setStaticFrameByIndex] = useState< 41 45 Record<number, string> 42 46 >({}); ··· 45 49 const durationMs = 46 50 GIF_INTERVALS_MS[activeIndex] ?? GIF_INTERVALS_MS[0] ?? 6000; 47 51 48 - // Fade in at start, fade out near the end for smooth transitions. 49 - // Use timeouts so state updates happen asynchronously (avoids cascading render warning). 50 - const fadeInStartId = window.setTimeout(() => setOverlayOpacity(0), 0); 51 - const fadeInId = window.setTimeout(() => setOverlayOpacity(1), 30); 52 - const fadeOutAt = Math.max(0, durationMs - FADE_MS); 53 - const fadeOutId = window.setTimeout(() => setOverlayOpacity(0), fadeOutAt); 52 + // Fade in, fade out 53 + const fadeInStartId = window.setTimeout(() => { 54 + setFadeMs(FADE_IN_MS); 55 + setOverlayOpacity(0); 56 + }, 0); 57 + const fadeInId = window.setTimeout(() => { 58 + setFadeMs(FADE_IN_MS); 59 + setOverlayOpacity(1); 60 + }, 30); 61 + 62 + const fadeOutAt = Math.max(0, durationMs - FADE_OUT_MS); 63 + const fadeOutId = window.setTimeout(() => { 64 + setFadeMs(FADE_OUT_MS); 65 + setOverlayOpacity(0); 66 + }, fadeOutAt); 54 67 55 - const nextId = window.setTimeout(() => { 56 - setActiveIndex((prev) => (prev + 1) % items.length); 57 - }, durationMs); 68 + const nextId = window.setTimeout( 69 + () => { 70 + setActiveIndex((prev) => (prev + 1) % items.length); 71 + }, 72 + Math.max(0, durationMs - OVERLAP_MS), 73 + ); 58 74 59 75 return () => { 60 76 window.clearTimeout(fadeInStartId); ··· 160 176 src={staticFrameByIndex[i] ?? item.staticSrc} 161 177 alt="" 162 178 className="absolute inset-0 h-full w-full object-cover" 163 - style={{ objectPosition: item.position }} 179 + style={{ 180 + objectPosition: item.position, 181 + // Crossfade: as the animated overlay fades out, fade the static base in. 182 + opacity: activeIndex === i ? 1 - overlayOpacity : 1, 183 + transition: `opacity ${fadeMs}ms ${FADE_EASING}`, 184 + willChange: "opacity", 185 + }} 164 186 draggable={false} 165 187 /> 166 188 ··· 173 195 style={{ 174 196 objectPosition: item.position, 175 197 opacity: overlayOpacity, 176 - transition: `opacity ${FADE_MS}ms ease-in-out`, 198 + transition: `opacity ${fadeMs}ms ${FADE_EASING}`, 177 199 willChange: "opacity", 178 200 }} 179 201 draggable={false}