@@ -10,31 +10,11 @@ import {
1010import { MacCursor , WindowsCursor } from "./cursors" ;
1111import { H_HERO , INK , MODE_THEME , type ModeKey } from "./theme" ;
1212
13- /**
14- * The hero headline is a three-slot machine: capture verb, middle beat,
15- * payoff. The slots keep their rhythm while the words are revised, so one
16- * sentence re-reads as a different mode.
17- *
18- * Record. Edit. Share. (at rest)
19- * Record. Stop. Instant Share. (Instant)
20- * Record. Edit. Export. (Studio)
21- * Screenshot. Beautify. Paste. (Screenshot)
22- *
23- * Nothing moves on its own: the mode bar sits above the line from the first
24- * paint with a cursor leaning in beside it, and you click a mode to watch
25- * the line get revised. The revision is staged like an edit on paper: a rule
26- * strikes through the word in the incoming mode's colour, the struck word
27- * lifts away, and the replacement rises into the gap while the slot width
28- * glides so the line re-centres. Words that survive the change are never
29- * struck, so you see exactly what the mode changes, which is why Studio only
30- * crosses out the last word.
31- */
32-
3313type SlideKey = ModeKey | "all" ;
3414
3515type Slide = {
3616 key : SlideKey ;
37- /** [capture, middle, payoff] */
17+
3818 words : [ string , string , string ] ;
3919} ;
4020
@@ -45,12 +25,11 @@ const SLIDES: Slide[] = [
4525 { key : "screenshot" , words : [ "Screenshot." , "Beautify." , "Paste." ] } ,
4626] ;
4727
48- /** The cursor's one line: it invites the click, then retires once you take it. */
4928const NUDGE = "Click me \u{1F440}" ;
5029const NUDGE_TOUCH = "Tap me \u{1F440}" ;
5130const TOUCH_QUERY = "(max-width: 767px)" ;
5231const TYPE_MS = 26 ;
53- /** The cursor leans in first, then starts talking. */
32+
5433const NUDGE_AT = 520 ;
5534const TYPE_AT = NUDGE_AT + 260 ;
5635
@@ -73,25 +52,20 @@ const PILL_ICON = {
7352 screenshot : ScreenshotIcon ,
7453} as const ;
7554
76- /* --------------------------------------------------------------- timing -- */
77-
78- /** Per-slot head start, so the revision runs left to right down the line. */
7955const SLOT_DELAY = [ 0 , 120 , 240 ] ;
80- /** Rule sweeps across the word. */
56+
8157const STRIKE_MS = 260 ;
82- /** Beat where the struck word just sits there, crossed out. */
58+
8359const HOLD_MS = 110 ;
84- /** Struck word (and its rule) lift away. */
60+
8561const LIFT_MS = 260 ;
86- /** Replacement rises in, overlapping the lift so the two read as one move. */
62+
8763const RISE_MS = 520 ;
8864
8965const LIFT_AT = STRIKE_MS + HOLD_MS ;
9066const RISE_AT = LIFT_AT + 150 ;
9167const WIDTH_AT = LIFT_AT + 60 ;
9268
93- /* ------------------------------------------------------------------ slot -- */
94-
9569type Departing = { text : string ; color : string ; id : number } ;
9670
9771type Swap = { text : string ; gen : number ; from : Departing | null } ;
@@ -104,9 +78,9 @@ const Slot = ({
10478 animate,
10579} : {
10680 text : string ;
107- /** Ink for most slots, the mode's colour for the payoff. */
81+
10882 color : string ;
109- /** Colour of the cross-out rule: the incoming mode's. */
83+
11084 strike : string ;
11185 delay : number ;
11286 animate : boolean ;
@@ -121,15 +95,14 @@ const Slot = ({
12195 gen : 0 ,
12296 from : null ,
12397 } ) ) ;
124- // Holds the colour the slot was painted in for the word now leaving.
98+
12599 const paintedColor = useRef ( color ) ;
126100
127101 if ( swap . text !== text ) {
128102 setSwap ( {
129103 text,
130104 gen : swap . gen + 1 ,
131- // A word that survives the mode change is never struck out: it just
132- // stays put while its neighbours are revised around it.
105+
133106 from : animate
134107 ? { text : swap . text , color : paintedColor . current , id : swap . gen + 1 }
135108 : null ,
@@ -183,7 +156,6 @@ const Slot = ({
183156 transitionDelay : `${ delay + WIDTH_AT } ms` ,
184157 } }
185158 >
186- { /* Sets the slot's height and its natural (target) width. */ }
187159 < span ref = { ghostRef } className = "ht-slot-ghost" aria-hidden = "true" >
188160 { text }
189161 </ span >
@@ -220,8 +192,6 @@ const Slot = ({
220192 ) ;
221193} ;
222194
223- /* -------------------------------------------------------------- headline -- */
224-
225195export const HeroHeadline = ( ) => {
226196 const { platform } = useDetectPlatform ( ) ;
227197 const [ index , setIndex ] = useState ( 0 ) ;
@@ -231,8 +201,6 @@ export const HeroHeadline = () => {
231201 const [ asked , setAsked ] = useState ( true ) ;
232202 const [ reduced , setReduced ] = useState ( false ) ;
233203
234- // Match the download button: assume macOS while detection resolves, so the
235- // arrow never swaps shape under the reader.
236204 const Arrow = platform === "windows" ? WindowsCursor : MacCursor ;
237205
238206 useEffect ( ( ) => {
@@ -243,8 +211,6 @@ export const HeroHeadline = () => {
243211 return ( ) => query . removeEventListener ( "change" , sync ) ;
244212 } , [ ] ) ;
245213
246- // The cursor leans in, then types its line. Reduced motion gets both at
247- // once, already finished.
248214 useEffect ( ( ) => {
249215 const text = window . matchMedia ( TOUCH_QUERY ) . matches ? NUDGE_TOUCH : NUDGE ;
250216 setNudgeText ( text ) ;
@@ -275,11 +241,9 @@ export const HeroHeadline = () => {
275241 const theme = slide . key === "all" ? null : MODE_THEME [ slide . key ] ;
276242 const accent = theme ? theme . glyph : INK ;
277243
278- // Clicking the mode you're already on puts the plain line back, so you can
279- // always see what Cap changed.
280244 const pick = ( target : number ) => {
281245 setIndex ( ( current ) => ( current === target ? 0 : target ) ) ;
282- // The cursor and its bubble have done their job the moment you click.
246+
283247 setAsked ( false ) ;
284248 } ;
285249
@@ -318,9 +282,6 @@ export const HeroHeadline = () => {
318282 } ) }
319283 </ div >
320284
321- { /* Nothing cycles on its own, so the cursor is what says the line is
322- yours to change. It retires as soon as you take it up on the
323- offer. */ }
324285 < span
325286 aria-hidden = "true"
326287 className = { `ht-nudge ${ nudge && asked ? "is-on" : "" } ` }
@@ -392,8 +353,7 @@ const CSS = `
392353}
393354.ht-layer > span { position: relative; display: inline-block; }
394355
395- /* The cross-out rule, drawn through the x-height and running a hair past
396- the word at both ends the way a pen would. */
356+
397357.ht-strike {
398358 position: absolute;
399359 left: -0.04em;
@@ -417,7 +377,7 @@ const CSS = `
417377.ht-out { animation: ht-out ${ LIFT_MS } ms cubic-bezier(0.4, 0, 0.75, 0.2) both; }
418378.ht-in { animation: ht-in ${ RISE_MS } ms cubic-bezier(0.18, 0.9, 0.22, 1) both; }
419379
420- /* Caret for the nudge as it types itself out. */
380+
421381.ht-caret {
422382 display: inline-block;
423383 width: 1.5px;
@@ -429,8 +389,7 @@ const CSS = `
429389}
430390@keyframes ht-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
431391
432- /* The cursor leaning in beside the mode bar. Hidden on phones, where there
433- is no room beside a wrapped bar and no cursor to speak of on touch. */
392+
434393.ht-nudge {
435394 display: block;
436395 position: absolute;
@@ -449,8 +408,7 @@ const CSS = `
449408 opacity: 1;
450409 transform: translate3d(0, 0, 0) scale(1);
451410}
452- /* Bubble and cursor sit side by side, the tail on the bubble's right edge
453- reaching across the gap to the arrow. */
411+
454412.ht-nudge-float {
455413 display: flex;
456414 align-items: flex-start;
@@ -471,10 +429,7 @@ const CSS = `
471429 height: auto;
472430 filter: drop-shadow(0 2px 5px rgba(17, 17, 17, 0.28));
473431}
474- /* An iMessage bubble sitting to the left of the cursor with its tail aimed
475- back at it. The mode bar and the page behind it are all white, so the chip
476- needs its own colour to land, and it stays a size under the mode toggles
477- (13px) so it reads as an aside rather than a fourth control. */
432+
478433.ht-nudge-chip {
479434 position: relative;
480435 display: block;
0 commit comments