@@ -17,12 +17,152 @@ const EASINGS = [
1717 [ "circIn" , "circOut" , "circInOut" ] ,
1818] ;
1919
20+ /* Easing math — mirrors flixel's FlxEase (Penner equations) so previews match in-game. */
21+ const EASE = ( ( ) => {
22+ const PI = Math . PI ;
23+ const c1 = 1.70158 , c2 = c1 * 1.525 , c3 = c1 + 1 , c4 = ( 2 * PI ) / 3 , c5 = ( 2 * PI ) / 4.5 ;
24+ const bo = ( t ) => {
25+ const n = 7.5625 , d = 2.75 ;
26+ if ( t < 1 / d ) return n * t * t ;
27+ if ( t < 2 / d ) return n * ( t -= 1.5 / d ) * t + 0.75 ;
28+ if ( t < 2.5 / d ) return n * ( t -= 2.25 / d ) * t + 0.9375 ;
29+ return n * ( t -= 2.625 / d ) * t + 0.984375 ;
30+ } ;
31+ return {
32+ linear : ( t ) => t ,
33+ sineIn : ( t ) => 1 - Math . cos ( ( t * PI ) / 2 ) ,
34+ sineOut : ( t ) => Math . sin ( ( t * PI ) / 2 ) ,
35+ sineInOut : ( t ) => - ( Math . cos ( PI * t ) - 1 ) / 2 ,
36+ quadIn : ( t ) => t * t ,
37+ quadOut : ( t ) => 1 - ( 1 - t ) * ( 1 - t ) ,
38+ quadInOut : ( t ) => ( t < 0.5 ? 2 * t * t : 1 - Math . pow ( - 2 * t + 2 , 2 ) / 2 ) ,
39+ cubeIn : ( t ) => t * t * t ,
40+ cubeOut : ( t ) => 1 - Math . pow ( 1 - t , 3 ) ,
41+ cubeInOut : ( t ) => ( t < 0.5 ? 4 * t * t * t : 1 - Math . pow ( - 2 * t + 2 , 3 ) / 2 ) ,
42+ quartIn : ( t ) => t * t * t * t ,
43+ quartOut : ( t ) => 1 - Math . pow ( 1 - t , 4 ) ,
44+ quartInOut : ( t ) => ( t < 0.5 ? 8 * t * t * t * t : 1 - Math . pow ( - 2 * t + 2 , 4 ) / 2 ) ,
45+ quintIn : ( t ) => t * t * t * t * t ,
46+ quintOut : ( t ) => 1 - Math . pow ( 1 - t , 5 ) ,
47+ quintInOut : ( t ) => ( t < 0.5 ? 16 * t * t * t * t * t : 1 - Math . pow ( - 2 * t + 2 , 5 ) / 2 ) ,
48+ expoIn : ( t ) => ( t === 0 ? 0 : Math . pow ( 2 , 10 * t - 10 ) ) ,
49+ expoOut : ( t ) => ( t === 1 ? 1 : 1 - Math . pow ( 2 , - 10 * t ) ) ,
50+ expoInOut : ( t ) => ( t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math . pow ( 2 , 20 * t - 10 ) / 2 : ( 2 - Math . pow ( 2 , - 20 * t + 10 ) ) / 2 ) ,
51+ backIn : ( t ) => c3 * t * t * t - c1 * t * t ,
52+ backOut : ( t ) => 1 + c3 * Math . pow ( t - 1 , 3 ) + c1 * Math . pow ( t - 1 , 2 ) ,
53+ backInOut : ( t ) => ( t < 0.5 ? ( Math . pow ( 2 * t , 2 ) * ( ( c2 + 1 ) * 2 * t - c2 ) ) / 2 : ( Math . pow ( 2 * t - 2 , 2 ) * ( ( c2 + 1 ) * ( t * 2 - 2 ) + c2 ) + 2 ) / 2 ) ,
54+ elasticIn : ( t ) => ( t === 0 ? 0 : t === 1 ? 1 : - Math . pow ( 2 , 10 * t - 10 ) * Math . sin ( ( t * 10 - 10.75 ) * c4 ) ) ,
55+ elasticOut : ( t ) => ( t === 0 ? 0 : t === 1 ? 1 : Math . pow ( 2 , - 10 * t ) * Math . sin ( ( t * 10 - 0.75 ) * c4 ) + 1 ) ,
56+ elasticInOut : ( t ) => ( t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? - ( Math . pow ( 2 , 20 * t - 10 ) * Math . sin ( ( 20 * t - 11.125 ) * c5 ) ) / 2 : ( Math . pow ( 2 , - 20 * t + 10 ) * Math . sin ( ( 20 * t - 11.125 ) * c5 ) ) / 2 + 1 ) ,
57+ bounceIn : ( t ) => 1 - bo ( 1 - t ) ,
58+ bounceOut : bo ,
59+ bounceInOut : ( t ) => ( t < 0.5 ? ( 1 - bo ( 1 - 2 * t ) ) / 2 : ( 1 + bo ( 2 * t - 1 ) ) / 2 ) ,
60+ circIn : ( t ) => 1 - Math . sqrt ( 1 - t * t ) ,
61+ circOut : ( t ) => Math . sqrt ( 1 - ( t - 1 ) * ( t - 1 ) ) ,
62+ circInOut : ( t ) => ( t < 0.5 ? ( 1 - Math . sqrt ( 1 - Math . pow ( 2 * t , 2 ) ) ) / 2 : ( Math . sqrt ( 1 - Math . pow ( - 2 * t + 2 , 2 ) ) + 1 ) / 2 ) ,
63+ } ;
64+ } ) ( ) ;
65+
66+ /* Animated curve + motion preview for a single easing. */
67+ function EasingPreview ( { name, compact } ) {
68+ const fn = EASE [ name ] || EASE . linear ;
69+ const W = 280 , H = 220 , X0 = 36 , X1 = 262 , Y0 = 180 , Y1 = 40 ; // Y0 = value 0, Y1 = value 1
70+ const tx = ( t ) => X0 + t * ( X1 - X0 ) ;
71+ const vy = ( v ) => Y0 - v * ( Y0 - Y1 ) ;
72+ // static curve path for the current easing
73+ let d = "" ;
74+ const N = 96 ;
75+ for ( let i = 0 ; i <= N ; i ++ ) {
76+ const t = i / N ;
77+ d += ( i ? " L" : "M" ) + tx ( t ) . toFixed ( 1 ) + " " + vy ( fn ( t ) ) . toFixed ( 1 ) ;
78+ }
79+ // compress motion track to [-0.3, 1.3] so overshoot stays visible
80+ const puckPos = ( v ) => ( ( v + 0.3 ) / 1.6 ) * 100 ;
81+
82+ const dot = useRef ( null ) , gx = useRef ( null ) , gy = useRef ( null ) , puck = useRef ( null ) ;
83+ useEffect ( ( ) => {
84+ let raf , start = null ;
85+ const RUN = 1500 , HOLD = 450 , TOTAL = RUN + HOLD ;
86+ const tick = ( now ) => {
87+ if ( start == null ) start = now ;
88+ let t = ( ( now - start ) % TOTAL ) / RUN ;
89+ if ( t > 1 ) t = 1 ;
90+ const v = fn ( t ) , x = tx ( t ) , y = vy ( v ) ;
91+ if ( dot . current ) { dot . current . setAttribute ( "cx" , x ) ; dot . current . setAttribute ( "cy" , y ) ; }
92+ if ( gx . current ) { gx . current . setAttribute ( "x2" , x ) ; gx . current . setAttribute ( "y1" , y ) ; gx . current . setAttribute ( "y2" , y ) ; }
93+ if ( gy . current ) { gy . current . setAttribute ( "x1" , x ) ; gy . current . setAttribute ( "x2" , x ) ; gy . current . setAttribute ( "y1" , y ) ; }
94+ if ( puck . current ) puck . current . style . left = puckPos ( v ) + "%" ;
95+ raf = requestAnimationFrame ( tick ) ;
96+ } ;
97+ raf = requestAnimationFrame ( tick ) ;
98+ return ( ) => cancelAnimationFrame ( raf ) ;
99+ } , [ name ] ) ;
100+
101+ return (
102+ < React . Fragment >
103+ < svg className = "ease-graph" viewBox = { `0 0 ${ W } ${ H } ` } role = "img" aria-label = { `${ name } easing curve` } >
104+ < line className = "ease-grid" x1 = { X0 } y1 = { Y1 } x2 = { X1 } y2 = { Y1 } />
105+ < line className = "ease-axis" x1 = { X0 } y1 = { Y1 - 18 } x2 = { X0 } y2 = { Y0 + 18 } />
106+ < line className = "ease-axis" x1 = { X0 } y1 = { Y0 } x2 = { X1 } y2 = { Y0 } />
107+ < text className = "ease-tick" x = { X0 - 9 } y = { Y0 + 4 } textAnchor = "end" > 0</ text >
108+ < text className = "ease-tick" x = { X0 - 9 } y = { Y1 + 4 } textAnchor = "end" > 1</ text >
109+ < text className = "ease-tick" x = { X0 } y = { Y0 + 30 } textAnchor = "middle" > t=0</ text >
110+ < text className = "ease-tick" x = { X1 } y = { Y0 + 30 } textAnchor = "middle" > t=1</ text >
111+ < line className = "ease-linear" x1 = { X0 } y1 = { Y0 } x2 = { X1 } y2 = { Y1 } />
112+ < line ref = { gy } className = "ease-guide" x1 = { X0 } y1 = { Y0 } x2 = { X0 } y2 = { Y0 } />
113+ < line ref = { gx } className = "ease-guide" x1 = { X0 } y1 = { Y0 } x2 = { X0 } y2 = { Y0 } />
114+ < path className = "ease-curve" d = { d } />
115+ < circle ref = { dot } className = "ease-dot" cx = { X0 } cy = { Y0 } r = "4.5" />
116+ </ svg >
117+ { compact ? (
118+ < div className = "ease-active" > { name } </ div >
119+ ) : (
120+ < div className = "ease-demo" >
121+ < div className = "ease-active" > { name } </ div >
122+ < div className = "ease-demo-cap" > value 0 → 1 over time</ div >
123+ < div className = "ease-track" >
124+ < span className = "ease-tick-mark" style = { { left : puckPos ( 0 ) + "%" } } > </ span >
125+ < span className = "ease-tick-mark" style = { { left : puckPos ( 1 ) + "%" } } > </ span >
126+ < div ref = { puck } className = "ease-puck" style = { { left : puckPos ( 0 ) + "%" } } > </ div >
127+ </ div >
128+ </ div >
129+ ) }
130+ </ React . Fragment >
131+ ) ;
132+ }
133+
20134function EasingsSection ( ) {
21135 const [ hover , setHover ] = useState ( null ) ;
136+ const [ selected , setSelected ] = useState ( "elasticOut" ) ;
137+ const [ docked , setDocked ] = useState ( false ) ;
138+ const cardRef = useRef ( null ) ;
139+ const active = hover || selected ;
140+
141+ // When the inline preview scrolls up out of view, show a compact docked copy
142+ // so the curve stays visible while hovering rows further down the table.
143+ useEffect ( ( ) => {
144+ const el = cardRef . current ;
145+ if ( ! el || typeof IntersectionObserver === "undefined" ) return ;
146+ const io = new IntersectionObserver (
147+ ( [ e ] ) => setDocked ( ! e . isIntersecting && e . boundingClientRect . top < 60 ) ,
148+ { threshold : 0 , rootMargin : "-56px 0px 0px 0px" }
149+ ) ;
150+ io . observe ( el ) ;
151+ return ( ) => io . disconnect ( ) ;
152+ } , [ ] ) ;
153+
22154 return (
23155 < div >
24156 < h2 id = "list" > Available easings</ h2 >
25- < p > Unknown names fall back to < code > linear</ code > . Click an easing to preview its curve.</ p >
157+ < p > Hover or click an easing to preview its curve. Unknown names fall back to < code > linear</ code > .</ p >
158+ < div className = "ease-card" ref = { cardRef } >
159+ < EasingPreview name = { active } />
160+ </ div >
161+ { docked && (
162+ < div className = "ease-float" aria-hidden = "true" >
163+ < EasingPreview name = { active } compact />
164+ </ div >
165+ ) }
26166 < table className = "tbl" >
27167 < thead >
28168 < tr > < th > Family</ th > < th > In</ th > < th > Out</ th > < th > InOut</ th > </ tr >
@@ -40,10 +180,12 @@ function EasingsSection() {
40180 < td key = { idx }
41181 onMouseEnter = { ( ) => setHover ( name ) }
42182 onMouseLeave = { ( ) => setHover ( null ) }
183+ onClick = { ( ) => setSelected ( name ) }
43184 style = { {
44- cursor : "default" ,
45- color : hover === name ? "var(--violet-strong)" : "var(--violet)" ,
46- background : hover === name ? "var(--violet-soft)" : "transparent" ,
185+ cursor : "pointer" ,
186+ color : active === name ? "var(--violet-strong)" : "var(--violet)" ,
187+ background : active === name ? "var(--violet-soft)" : "transparent" ,
188+ fontWeight : selected === name ? 700 : 400 ,
47189 transition : "background 0.12s" ,
48190 } }
49191 >
@@ -59,7 +201,7 @@ function EasingsSection() {
59201 < CodeBlock lang = "lua" filename = "usage.lua" source = { `startTween('flourish', 'logo',
60202 { angle = 360, alpha = 0.5 },
61203 2.0,
62- { ease = '${ hover || "elasticOut" } ' })` } />
204+ { ease = '${ active } ' })` } />
63205 </ div >
64206 ) ;
65207}
0 commit comments