@@ -57,6 +57,8 @@ function useSmoothScrollProgress(
5757) {
5858 const progressRef = useRef ( 0 ) ;
5959 const targetRef = useRef ( 0 ) ;
60+ const lastTargetRef = useRef ( 0 ) ;
61+ const lastScrollAtRef = useRef ( 0 ) ;
6062 const rafRef = useRef < number | null > ( null ) ;
6163 const onUpdateRef = useRef < typeof onUpdate > ( onUpdate ) ;
6264
@@ -67,56 +69,97 @@ function useSmoothScrollProgress(
6769 useEffect ( ( ) => {
6870 let active = true ;
6971 let resizeObserver : ResizeObserver | null = null ;
72+ let running = false ;
7073
7174 const computeTarget = ( ) => {
7275 if ( ! ref . current ) {
7376 targetRef . current = 0 ;
7477 return ;
7578 }
7679 const rect = ref . current . getBoundingClientRect ( ) ;
80+ if ( ! Number . isFinite ( rect . top ) || rect . height <= 1 ) {
81+ return ;
82+ }
7783 const total = rect . height - window . innerHeight ;
7884 const raw = total > 0 ? Math . min ( Math . max ( - rect . top / total , 0 ) , 1 ) : 0 ;
85+ const now =
86+ typeof performance !== "undefined" ? performance . now ( ) : Date . now ( ) ;
87+ const sinceScroll = now - lastScrollAtRef . current ;
88+ if ( sinceScroll > 140 && Math . abs ( raw - lastTargetRef . current ) > 0.35 ) {
89+ targetRef . current = lastTargetRef . current ;
90+ return ;
91+ }
92+ lastTargetRef . current = raw ;
7993 targetRef . current = raw ;
8094 } ;
8195
8296 const epsilon = 0.0008 ;
8397 const smoothing = 0.12 ;
8498
8599 const tick = ( ) => {
86- if ( ! active ) {
100+ if ( ! active || document . hidden ) {
101+ running = false ;
102+ rafRef . current = null ;
87103 return ;
88104 }
89105 const diff = targetRef . current - progressRef . current ;
90106 if ( Math . abs ( diff ) < epsilon ) {
91107 progressRef . current = targetRef . current ;
92- } else {
93- progressRef . current += diff * smoothing ;
108+ onUpdateRef . current ?.( progressRef . current ) ;
109+ running = false ;
110+ rafRef . current = null ;
111+ return ;
94112 }
113+ progressRef . current += diff * smoothing ;
95114 onUpdateRef . current ?.( progressRef . current ) ;
96115 rafRef . current = window . requestAnimationFrame ( tick ) ;
97116 } ;
98117
118+ const startLoop = ( ) => {
119+ if ( running ) {
120+ return ;
121+ }
122+ running = true ;
123+ rafRef . current = window . requestAnimationFrame ( tick ) ;
124+ } ;
125+
99126 computeTarget ( ) ;
100- rafRef . current = window . requestAnimationFrame ( tick ) ;
127+ startLoop ( ) ;
101128
102- const schedule = ( ) => {
129+ const schedule = ( fromScroll = false ) => {
130+ if ( fromScroll ) {
131+ lastScrollAtRef . current =
132+ typeof performance !== "undefined" ? performance . now ( ) : Date . now ( ) ;
133+ }
103134 computeTarget ( ) ;
135+ startLoop ( ) ;
104136 } ;
105137
106- window . addEventListener ( "scroll" , schedule , { passive : true } ) ;
107- window . addEventListener ( "resize" , schedule ) ;
138+ const handleScroll : EventListener = ( ) => schedule ( true ) ;
139+ const handleResize : EventListener = ( ) => schedule ( false ) ;
140+ window . addEventListener ( "scroll" , handleScroll , { passive : true } ) ;
141+ window . addEventListener ( "resize" , handleResize ) ;
142+ const handleVisibility = ( ) => {
143+ if ( ! document . hidden ) {
144+ schedule ( ) ;
145+ }
146+ } ;
147+ document . addEventListener ( "visibilitychange" , handleVisibility ) ;
108148
109149 if ( typeof ResizeObserver !== "undefined" ) {
110- resizeObserver = new ResizeObserver ( schedule ) ;
150+ const handleResizeObserver : ResizeObserverCallback = ( ) =>
151+ schedule ( false ) ;
152+ resizeObserver = new ResizeObserver ( handleResizeObserver ) ;
111153 if ( ref . current ) {
112154 resizeObserver . observe ( ref . current ) ;
113155 }
114156 }
115157
116158 return ( ) => {
117159 active = false ;
118- window . removeEventListener ( "scroll" , schedule ) ;
119- window . removeEventListener ( "resize" , schedule ) ;
160+ window . removeEventListener ( "scroll" , handleScroll ) ;
161+ window . removeEventListener ( "resize" , handleResize ) ;
162+ document . removeEventListener ( "visibilitychange" , handleVisibility ) ;
120163 resizeObserver ?. disconnect ( ) ;
121164 resizeObserver = null ;
122165 if ( rafRef . current !== null ) {
@@ -459,6 +502,8 @@ export default function HomeClient({
459502 heroActive : true ,
460503 } ) ;
461504 const debugLastRef = useRef ( 0 ) ;
505+ const revealRef = useRef ( 0 ) ;
506+ const fadeRef = useRef ( 1 ) ;
462507 const featuredIndex = useMemo (
463508 ( ) => profile . sections . indexOf ( "featured" ) ,
464509 [ profile . sections ] ,
@@ -488,9 +533,12 @@ export default function HomeClient({
488533 if ( ! shellRef . current ) {
489534 return ;
490535 }
536+ if ( typeof document !== "undefined" && document . hidden ) {
537+ return ;
538+ }
491539 const activeId = activeSectionRef . current ?? "home" ;
492540 const activeIndex = profile . sections . indexOf ( activeId ) ;
493- const forceOpaque = featuredIndex >= 0 && activeIndex > featuredIndex ;
541+ const forceOpaque = featuredIndex >= 0 && activeIndex >= featuredIndex ;
494542 const clamp = ( val : number , min : number , max : number ) =>
495543 Math . min ( max , Math . max ( min , val ) ) ;
496544 let reveal = 0 ;
@@ -500,22 +548,37 @@ export default function HomeClient({
500548 featuredRef . current ;
501549 if ( featuredEl ) {
502550 const rect = featuredEl . getBoundingClientRect ( ) ;
503- const elementCenter = rect . top + rect . height / 2 ;
504- const offsetCenter = elementCenter - window . innerHeight ;
505- const triggerStart = window . innerHeight * 0.8 ;
506- const triggerEnd = window . innerHeight * 0.5 ;
507- if ( triggerStart !== triggerEnd ) {
508- reveal = clamp (
509- ( triggerStart - offsetCenter ) / ( triggerStart - triggerEnd ) ,
510- 0 ,
511- 1 ,
512- ) ;
551+ if ( Number . isFinite ( rect . top ) && rect . height > 1 ) {
552+ const elementCenter = rect . top + rect . height / 2 ;
553+ const offsetCenter = elementCenter - window . innerHeight ;
554+ const triggerStart = window . innerHeight * 0.8 ;
555+ const triggerEnd = window . innerHeight * 0.5 ;
556+ if ( triggerStart !== triggerEnd ) {
557+ reveal = clamp (
558+ ( triggerStart - offsetCenter ) / ( triggerStart - triggerEnd ) ,
559+ 0 ,
560+ 1 ,
561+ ) ;
562+ }
563+ if ( rect . top <= window . innerHeight * 0.1 ) {
564+ reveal = 1 ;
565+ }
566+ } else {
567+ reveal = revealRef . current ;
513568 }
514569 } else {
515570 reveal = clamp ( ( value - 0.85 ) / 0.12 , 0 , 1 ) ;
516571 }
517572 reveal = forceOpaque ? 1 : reveal ;
518- const fade = forceOpaque ? 0 : 1 - reveal ;
573+ let fade = forceOpaque ? 0 : 1 - reveal ;
574+ if ( ! Number . isFinite ( reveal ) ) {
575+ reveal = revealRef . current ;
576+ }
577+ if ( ! Number . isFinite ( fade ) ) {
578+ fade = fadeRef . current ;
579+ }
580+ revealRef . current = reveal ;
581+ fadeRef . current = fade ;
519582 shellRef . current . style . setProperty ( "--hero-fade" , String ( fade ) ) ;
520583 shellRef . current . style . setProperty ( "--content-reveal" , String ( reveal ) ) ;
521584 const nextHeroActive = reveal < 0.98 ;
@@ -1488,9 +1551,9 @@ export default function HomeClient({
14881551 "# Kinin Portfolio\n\nStatus: active\nStack: Next.js, Three.js, WebGL\n" ,
14891552 } ,
14901553 {
1491- path : `${ projectsDir } /retro -os/README.md` ,
1554+ path : `${ projectsDir } /kinin -os/README.md` ,
14921555 content :
1493- "# Retro OS\n\nExperimenting with terminal UX and CRT shaders.\n" ,
1556+ "# Kinin OS\n\nExperimenting with terminal UX and CRT shaders.\n" ,
14941557 } ,
14951558 {
14961559 path : `${ homeDir } /.bashrc` ,
0 commit comments