@@ -19,6 +19,9 @@ const ASTERISK_THRESHOLD = 0.65;
1919 * The video keeps scrubbing underneath. */
2020const UNPIN_THRESHOLD = 0.8 ;
2121
22+ /** Clamp a value to 0–1. */
23+ const clamp01 = ( v : number ) => Math . min ( 1 , Math . max ( 0 , v ) ) ;
24+
2225const PILL =
2326 "inline-block px-4 py-1.5 rounded-md border border-[var(--color-caramel)]/30 text-[var(--color-caramel)] text-sm font-display hover:bg-[var(--color-caramel)]/10 hover:border-[var(--color-caramel)]/60 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150" ;
2427
@@ -99,13 +102,10 @@ function Home() {
99102 const runwayScroll = - rect . top ;
100103 const runwayHeight = runway . offsetHeight - window . innerHeight ;
101104 const fraction = runwayHeight > 0
102- ? Math . min ( 1 , Math . max ( 0 , runwayScroll / runwayHeight ) )
105+ ? clamp01 ( runwayScroll / runwayHeight )
103106 : 0 ;
104107
105- // Compute rendered video content height (object-contain fits within container
106- // while preserving the video's native aspect ratio). This is the icon's actual
107- // on-screen size and drives the icon rise distance, the hook fade timing, and
108- // the video scrub start point.
108+ // Rendered icon height (object-contain preserves aspect ratio within container).
109109 const naturalAspect = video . videoWidth && video . videoHeight
110110 ? video . videoWidth / video . videoHeight
111111 : 1.22 ; // fallback before metadata loads
@@ -115,16 +115,13 @@ function Home() {
115115 : video . offsetHeight ; // height-limited
116116 const initialOffset = iconHeight * ICON_INITIAL_HIDE_FRAC ;
117117
118- // Scrub video: hold on frame 0 while the icon is still rising into position.
119- // Once the icon reaches its static position (runwayScroll >= initialOffset),
120- // scrub the remaining scroll range across the video's duration.
118+ // Scrub video: hold frame 0 during icon rise, then scrub remaining range.
121119 if ( video . duration && isFinite ( video . duration ) ) {
122120 if ( runwayScroll < initialOffset ) {
123121 video . currentTime = 0 ;
124122 } else {
125- const videoRunway = runwayHeight - initialOffset ;
126- const videoProgress = videoRunway > 0
127- ? Math . min ( 1 , ( runwayScroll - initialOffset ) / videoRunway )
123+ const videoProgress = ( runwayHeight - initialOffset ) > 0
124+ ? clamp01 ( ( runwayScroll - initialOffset ) / ( runwayHeight - initialOffset ) )
128125 : 0 ;
129126 video . currentTime = videoProgress * video . duration ;
130127 }
@@ -134,26 +131,24 @@ function Home() {
134131 for ( let i = 0 ; i < WORD_THRESHOLDS . length ; i ++ ) {
135132 const el = wordRefs [ i ] . current ;
136133 if ( ! el ) continue ;
137- const progress = Math . min ( 1 , Math . max ( 0 ,
134+ const progress = clamp01 (
138135 ( fraction - WORD_THRESHOLDS [ i ] ) / 0.08
139- ) ) ;
136+ ) ;
140137 el . style . opacity = String ( progress ) ;
141138 el . style . transform = `translateY(${ ( 1 - progress ) * 12 } px)` ;
142139 }
143140
144141 // Asterisk + footnote
145- const astProgress = Math . min ( 1 , Math . max ( 0 ,
142+ const astProgress = clamp01 (
146143 ( fraction - ASTERISK_THRESHOLD ) / 0.08
147- ) ) ;
144+ ) ;
148145 if ( asteriskRef . current ) asteriskRef . current . style . opacity = String ( astProgress ) ;
149146 if ( footnoteRef . current ) footnoteRef . current . style . opacity = String ( astProgress * 0.7 ) ;
150147
151- // (Hook fade handled below, after iconHeight is computed for the icon rise.)
152-
153148 // Header: reveal brand + background at unpin threshold
154- const headerProgress = Math . min ( 1 , Math . max ( 0 ,
149+ const headerProgress = clamp01 (
155150 ( fraction - UNPIN_THRESHOLD ) / 0.08
156- ) ) ;
151+ ) ;
157152 if ( headerBrandRef . current ) {
158153 headerBrandRef . current . style . opacity = String ( headerProgress ) ;
159154 }
@@ -169,35 +164,23 @@ function Home() {
169164 const slideAmount = Math . max ( 0 , runwayScroll - contentEnterScroll ) ;
170165
171166 // Video transform combines two behaviors:
172- // 1. Icon-rise (runwayScroll 0 → initialOffset px): 1:1 with scroll.
173- // At load the icon is translated +initialOffset so only the top third
174- // is visible; each pixel of scroll lifts it by one pixel until fully
175- // in view.
176- // 2. Existing unpin slide (fraction > UNPIN_THRESHOLD): translate up with
177- // content as hero unpins.
178- let videoTranslateY = 0 ;
179- let iconCurrentOffset = 0 ;
180- if ( runwayScroll < initialOffset ) {
181- iconCurrentOffset = initialOffset - runwayScroll ;
182- videoTranslateY = iconCurrentOffset ;
183- } else if ( slideAmount > 0 ) {
184- videoTranslateY = - Math . round ( slideAmount ) ;
185- }
167+ // 1. Icon-rise (runwayScroll 0 → initialOffset): translate down so only
168+ // the top third is visible; scroll lifts it 1:1 until fully in view.
169+ // 2. Unpin slide (fraction > UNPIN_THRESHOLD): translate up with content.
170+ const iconCurrentOffset = Math . max ( 0 , initialOffset - runwayScroll ) ;
171+ const videoTranslateY = iconCurrentOffset > 0
172+ ? iconCurrentOffset
173+ : slideAmount > 0 ? - Math . round ( slideAmount ) : 0 ;
186174 video . style . transform = videoTranslateY !== 0
187175 ? `translateY(${ Math . round ( videoTranslateY ) } px)`
188176 : '' ;
189177
190- // Hook text: holds visible until the bottom ~10% of the icon enters the
191- // viewport, then fades out as the icon completes its rise. Tied to icon
192- // position (not scroll fraction) so the timing matches 1:1 icon motion.
178+ // Hook text: visible until the icon nearly finishes rising, then fades out.
193179 if ( hookRef . current ) {
194180 const remainingHidden = iconHeight > 0 ? iconCurrentOffset / iconHeight : 0 ;
195- let fadeProgress = 0 ;
196- if ( runwayScroll >= initialOffset ) {
197- fadeProgress = 1 ;
198- } else if ( remainingHidden < HOOK_FADE_REMAINING ) {
199- fadeProgress = 1 - remainingHidden / HOOK_FADE_REMAINING ;
200- }
181+ const fadeProgress = iconCurrentOffset === 0
182+ ? 1
183+ : clamp01 ( 1 - remainingHidden / HOOK_FADE_REMAINING ) ;
201184 hookRef . current . style . opacity = String ( 1 - fadeProgress ) ;
202185 hookRef . current . style . transform = `translateY(${ - fadeProgress * 24 } px)` ;
203186 }
0 commit comments