1
+ import { useState , useEffect , useRef } from "react" ;
1
2
import { useColorMode } from "@/context/ColorMode" ;
2
3
import styles from "./Elements.module.css" ;
3
4
@@ -29,13 +30,52 @@ const borderOpacities = [0, 1] as const;
29
30
const eyeOpacities = [ 1 , 0 , 0 ] as const ;
30
31
const eyeScales = [ 1 , 0 , 0 ] as const ;
31
32
33
+ function getEyeOffset (
34
+ mousePos : { x : number ; y : number } | undefined ,
35
+ leftEyeBox : DOMRect | undefined ,
36
+ rightEyeBox : DOMRect | undefined ,
37
+ svgBox : DOMRect | undefined ,
38
+ ) : { x : number ; y : number } {
39
+ if ( ! mousePos || ! leftEyeBox || ! rightEyeBox || ! svgBox )
40
+ return { x : 0 , y : 0 } ;
41
+ // Assume the eyes are nicely symmetrical
42
+ const center = {
43
+ x : ( leftEyeBox . right + rightEyeBox . left ) / 2 ,
44
+ y : leftEyeBox . top + leftEyeBox . height / 2 ,
45
+ } ;
46
+ // Let cx = half-width of the eye * 0.9, cy = half-height of the eye * 0.9
47
+ // (ex, ey) = lookDir.
48
+ // We want the pupil to move on the ellipse
49
+ // (x / cx)^2 + (y / cy)^2 = 1 (w.r.t. the eye's center)
50
+ // substitute y = k * x where k = ey / ex, then
51
+ // ((1 / cx^2) + (k^2 / cy^2)) * x^2 = 1
52
+ // Solve for x.
53
+ const cx = ( leftEyeBox . width / 2 ) * 0.9 ;
54
+ const cy = ( leftEyeBox . height / 2 ) * 0.9 ;
55
+ const k = ( mousePos . y - center . y ) / ( mousePos . x - center . x ) ;
56
+ const x =
57
+ Math . sqrt ( 1 / ( 1 / cx ** 2 + k ** 2 / cy ** 2 ) ) *
58
+ ( mousePos . x < center . x ? - 1 : 1 ) ;
59
+ const y = k * x ;
60
+ const svgScale = Math . max ( 400 / svgBox . width , 200 / svgBox . height ) ;
61
+ // The leftmost point has zero offset;
62
+ // Also restore the coordinate system to the SVG's
63
+ return { x : ( x + cx ) * svgScale , y : y * svgScale } ;
64
+ }
65
+
32
66
export default function ScrollyElements ( {
33
67
className,
34
68
getVal,
35
69
} : {
36
70
readonly className ?: string ;
37
71
readonly getVal : ( keyframes : readonly number [ ] ) => number ;
38
72
} ) : JSX . Element {
73
+ const [ mousePos , setMousePos ] = useState <
74
+ { x : number ; y : number } | undefined
75
+ > ( undefined ) ;
76
+ const svgRef = useRef < SVGSVGElement > ( null ) ;
77
+ const leftEyeRef = useRef < SVGPathElement > ( null ) ;
78
+ const rightEyeRef = useRef < SVGPathElement > ( null ) ;
39
79
const gScale = getVal ( gScales ) ;
40
80
const jSizeW = getVal ( jSizeWs ) ;
41
81
const jSizeH = getVal ( jSizeHs ) ;
@@ -45,11 +85,25 @@ export default function ScrollyElements({
45
85
const eyeScale = getVal ( eyeScales ) ;
46
86
const { colorMode } = useColorMode ( ) ;
47
87
const jColors = colorMode === "dark" ? [ 255 , 255 ] : [ 0 , 255 ] ;
88
+ useEffect ( ( ) => {
89
+ function onMouseMove ( event : MouseEvent ) {
90
+ setMousePos ( { x : event . clientX , y : event . clientY } ) ;
91
+ }
92
+ window . addEventListener ( "mousemove" , onMouseMove ) ;
93
+ return ( ) => window . removeEventListener ( "mousemove" , onMouseMove ) ;
94
+ } , [ ] ) ;
95
+ const eyeOffset = getEyeOffset (
96
+ mousePos ,
97
+ leftEyeRef . current ?. getBoundingClientRect ( ) ,
98
+ rightEyeRef . current ?. getBoundingClientRect ( ) ,
99
+ svgRef . current ?. getBoundingClientRect ( ) ,
100
+ ) ;
48
101
return (
49
102
< svg
50
103
xmlns = "http://www.w3.org/2000/svg"
51
104
viewBox = "-100 0 400 200"
52
105
className = { className }
106
+ ref = { svgRef }
53
107
preserveAspectRatio = "xMinYMin slice" >
54
108
< g
55
109
data-color-mode = "dark"
@@ -104,6 +158,7 @@ export default function ScrollyElements({
104
158
d = "m 130.03906,35.265625 c -6.57096,0.37252 -13.07243,3.355468 -17.28515,8.46875 0.88932,0.650391 1.77865,1.300781 2.66797,1.951172 4.43368,-5.340543 11.71756,-7.687776 18.51336,-7.130448 7.67453,0.605406 14.78248,4.099282 21.12921,8.26912 0.60807,-0.91862 1.21615,-1.83724 1.82422,-2.75586 -7.38373,-4.862754 -15.85747,-8.764558 -24.84934,-8.859205 -0.66714,-0.004 -1.33444,0.01439 -2.00027,0.05647 z"
105
159
/>
106
160
< path
161
+ ref = { leftEyeRef }
107
162
style = { {
108
163
fill : "white" ,
109
164
stroke : "black" ,
@@ -116,7 +171,9 @@ export default function ScrollyElements({
116
171
d = "m 153.86903,55.368874 c -0.10519,2.366721 -2.40555,3.806331 -4.30783,4.756214 -5.76944,2.601433 -12.276,3.051559 -18.52206,2.704715 -4.72103,-0.402854 -9.66795,-1.168747 -13.69218,-3.840952 -1.57182,-1.014048 -3.00432,-3.044071 -2.06513,-4.940148 1.38193,-2.600204 4.39224,-3.709345 7.0221,-4.603408 7.07564,-2.055388 14.68311,-2.140198 21.87419,-0.631329 3.21502,0.807417 6.74549,1.805406 8.93804,4.466841 0.44661,0.601486 0.75658,1.329592 0.75287,2.088067 z"
117
172
/>
118
173
< path
119
- className = { styles . eye }
174
+ style = { {
175
+ transform : `translate(${ eyeOffset . x / eyeScale / 2 } px, ${ eyeOffset . y / eyeScale / 2 } px)` ,
176
+ } }
120
177
fill = "black"
121
178
d = "m 127.75874,54.998494 c 0.0205,1.2573 -1.00865,2.3469 -2.15068,2.4722 -1.20933,0.2018 -2.54385,-0.659 -2.79539,-1.9736 -0.26724,-1.1948 0.49672,-2.4748 1.57136,-2.8371 1.20571,-0.4801 2.74703,0.1524 3.2175,1.4672 0.10312,0.2763 0.15748,0.5737 0.15721,0.8713 z"
122
179
/>
@@ -125,6 +182,7 @@ export default function ScrollyElements({
125
182
d = "m 67.189453,34.892578 c -6.570618,0.37531 -13.07235,3.357143 -17.285156,8.470703 0.889323,0.650391 1.778646,1.300781 2.667969,1.951172 4.433686,-5.34054 11.717567,-7.687777 18.513365,-7.130448 7.674528,0.605406 14.782484,4.099283 21.129213,8.26912 0.608073,-0.91862 1.216145,-1.837239 1.824218,-2.755859 -7.383909,-4.862683 -15.857256,-8.765788 -24.849343,-8.861111 -0.667137,-0.004 -1.334436,0.01434 -2.000266,0.05642 z"
126
183
/>
127
184
< path
185
+ ref = { rightEyeRef }
128
186
style = { {
129
187
fill : "white" ,
130
188
stroke : "black" ,
@@ -137,10 +195,11 @@ export default function ScrollyElements({
137
195
d = "m 91.019508,54.997169 c -0.105185,2.366722 -2.405551,3.806332 -4.307834,4.756215 -5.769431,2.601433 -12.275998,3.051559 -18.52205,2.704715 -4.721032,-0.402854 -9.66795,-1.168747 -13.692187,-3.840952 -1.571817,-1.014048 -3.004316,-3.044071 -2.065128,-4.940148 1.381929,-2.600204 4.392238,-3.709345 7.022107,-4.603408 7.075633,-2.055388 14.6831,-2.140198 21.874179,-0.631329 3.215024,0.807417 6.745497,1.805406 8.938038,4.466841 0.446612,0.601486 0.756586,1.329591 0.752875,2.088066 z"
138
196
/>
139
197
< path
140
- className = { styles . eye }
198
+ style = { {
199
+ transform : `translate(${ eyeOffset . x / eyeScale / 2 } px, ${ eyeOffset . y / eyeScale / 2 } px)` ,
200
+ } }
141
201
fill = "black"
142
202
d = "m 64.199753,54.998493 c 0.01817,1.240801 -0.970846,2.309985 -2.094058,2.463637 -1.211091,0.228208 -2.55671,-0.595823 -2.839702,-1.909945 -0.289738,-1.167921 0.407551,-2.434405 1.446127,-2.845906 1.189665,-0.542318 2.744057,0.01649 3.283233,1.303159 0.133651,0.308771 0.204696,0.648627 0.2044,0.989055 z"
143
- style = { { strokeWidth : 1.33 } }
144
203
/>
145
204
</ g >
146
205
< g opacity = { getVal ( borderOpacities ) } >
0 commit comments