Skip to content

Commit d8cf968

Browse files
committed
New front page implementation
Fixes #3
1 parent cb175eb commit d8cf968

File tree

6 files changed

+81
-67
lines changed

6 files changed

+81
-67
lines changed

src/client-entry.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { BrowserRouter } from "react-router-dom";
44
import * as H from "react-helmet-async";
55
import App from "./App";
66

7-
const { HelmetProvider } = (H as unknown as { default: typeof H }).default;
7+
// The types are extremely messed up
8+
const { HelmetProvider } =
9+
(H as unknown as { default?: typeof H }).default ?? H;
810

911
ReactDOM.hydrateRoot(
1012
document.getElementById("app")!,

src/components/Scrolly/Elements.module.css

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,12 @@
22
filter: drop-shadow(0 0 2px white);
33
}
44

5-
@keyframes rolling {
6-
from {
7-
translate: 0;
8-
animation-timing-function: ease-in-out;
9-
}
10-
11-
14% {
12-
translate: 20px;
13-
}
14-
15-
50% {
16-
translate: 20px;
17-
animation-timing-function: ease-in-out;
18-
}
19-
20-
64% {
21-
translate: 0;
22-
}
23-
24-
to {
25-
translate: 0;
26-
}
27-
}
28-
295
@keyframes raising {
306
from {
317
translate: 0;
328
}
339

34-
64% {
10+
60% {
3511
translate: 0;
3612
animation-timing-function: ease-in;
3713
}
@@ -41,7 +17,7 @@
4117
animation-timing-function: ease-out;
4218
}
4319

44-
72% {
20+
76% {
4521
translate: 0;
4622
}
4723

@@ -50,11 +26,7 @@
5026
}
5127
}
5228

53-
.eye {
54-
animation: rolling 10s infinite;
55-
}
56-
5729
.eyebrow {
58-
animation: raising 10s infinite;
30+
animation: raising 5s infinite;
5931
fill: var(--color-text);
6032
}

src/components/Scrolly/Elements.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState, useEffect, useRef } from "react";
12
import { useColorMode } from "@/context/ColorMode";
23
import styles from "./Elements.module.css";
34

@@ -29,13 +30,52 @@ const borderOpacities = [0, 1] as const;
2930
const eyeOpacities = [1, 0, 0] as const;
3031
const eyeScales = [1, 0, 0] as const;
3132

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+
3266
export default function ScrollyElements({
3367
className,
3468
getVal,
3569
}: {
3670
readonly className?: string;
3771
readonly getVal: (keyframes: readonly number[]) => number;
3872
}): 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);
3979
const gScale = getVal(gScales);
4080
const jSizeW = getVal(jSizeWs);
4181
const jSizeH = getVal(jSizeHs);
@@ -45,11 +85,25 @@ export default function ScrollyElements({
4585
const eyeScale = getVal(eyeScales);
4686
const { colorMode } = useColorMode();
4787
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+
);
48101
return (
49102
<svg
50103
xmlns="http://www.w3.org/2000/svg"
51104
viewBox="-100 0 400 200"
52105
className={className}
106+
ref={svgRef}
53107
preserveAspectRatio="xMinYMin slice">
54108
<g
55109
data-color-mode="dark"
@@ -104,6 +158,7 @@ export default function ScrollyElements({
104158
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"
105159
/>
106160
<path
161+
ref={leftEyeRef}
107162
style={{
108163
fill: "white",
109164
stroke: "black",
@@ -116,7 +171,9 @@ export default function ScrollyElements({
116171
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"
117172
/>
118173
<path
119-
className={styles.eye}
174+
style={{
175+
transform: `translate(${eyeOffset.x / eyeScale / 2}px, ${eyeOffset.y / eyeScale / 2}px)`,
176+
}}
120177
fill="black"
121178
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"
122179
/>
@@ -125,6 +182,7 @@ export default function ScrollyElements({
125182
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"
126183
/>
127184
<path
185+
ref={rightEyeRef}
128186
style={{
129187
fill: "white",
130188
stroke: "black",
@@ -137,10 +195,11 @@ export default function ScrollyElements({
137195
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"
138196
/>
139197
<path
140-
className={styles.eye}
198+
style={{
199+
transform: `translate(${eyeOffset.x / eyeScale / 2}px, ${eyeOffset.y / eyeScale / 2}px)`,
200+
}}
141201
fill="black"
142202
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 }}
144203
/>
145204
</g>
146205
<g opacity={getVal(borderOpacities)}>

src/components/Scrolly/index.tsx

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,16 @@ function ScrollyClient() {
2222
const [percentage, setPercentage] = useState(0);
2323
const getVal = linearInterpolate(percentage);
2424

25-
const observer = useRef(
26-
new IntersectionObserver(
27-
(entries) => {
28-
const p = entries[0]!.intersectionRatio;
29-
if (
30-
p >= 0 &&
31-
p <= 1 &&
32-
p !== percentage &&
33-
entries[0]!.intersectionRect.top !== entries[0]!.rootBounds!.top
34-
)
35-
setPercentage(p);
36-
},
37-
{ threshold: Array.from({ length: 1000 }, (_, i) => i / 1000) },
38-
),
39-
);
4025
useEffect(() => {
41-
observer.current.observe(frameRef.current!);
42-
const onScroll = () => {
43-
// Intersection observer doesn't reliably fire for the boundaries, so we
44-
// add another handler (but IO still has better performance). It's also
45-
// useful for the initial render
46-
if (window.scrollY < 20) setPercentage(0);
47-
else if (window.scrollY > frameRef.current!.offsetTop) setPercentage(1);
48-
};
49-
onScroll();
26+
function onScroll() {
27+
if (!frameRef.current) return;
28+
const { top, height } = frameRef.current.getBoundingClientRect();
29+
const newPercentage = Math.min(1, Math.max(0, (height - top) / height));
30+
console.log(newPercentage);
31+
setPercentage(newPercentage);
32+
}
5033
window.addEventListener("scroll", onScroll);
51-
return () => {
52-
// Observer ref never changes
53-
// eslint-disable-next-line react-hooks/exhaustive-deps
54-
observer.current.disconnect();
55-
window.removeEventListener("scroll", onScroll);
56-
};
34+
return () => window.removeEventListener("scroll", onScroll);
5735
}, []);
5836
return (
5937
<div className={styles.container}>

src/routes.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import * as H from "react-helmet-async";
33

4-
const { Helmet } = (H as unknown as { default: typeof H }).default;
4+
// The types are extremely messed up
5+
const { Helmet } = (H as unknown as { default?: typeof H }).default ?? H;
56

67
// Auto generates routes from files under ./pages
78
// https://vitejs.dev/guide/features.html#glob-import

src/server-entry.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import * as H from "react-helmet-async";
66
import App from "./App";
77
import { SSRContextProvider, type SSRContextValue } from "./context/SSRContext";
88

9-
const { HelmetProvider } = (H as unknown as { default: typeof H }).default;
9+
// The types are extremely messed up
10+
const { HelmetProvider } =
11+
(H as unknown as { default?: typeof H }).default ?? H;
1012

1113
// Inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js
1214
class WritableAsPromise extends Writable {

0 commit comments

Comments
 (0)