diff --git a/apps/hype/index.html b/apps/hype/index.html index e1fe2246..3af27017 100644 --- a/apps/hype/index.html +++ b/apps/hype/index.html @@ -7,7 +7,10 @@ content="width=device-width, initial-scale=1.0,viewport-fit=cover" /> - + @@ -23,6 +26,7 @@ +
diff --git a/apps/hype/package.json b/apps/hype/package.json index b49901fe..b3b2ee71 100644 --- a/apps/hype/package.json +++ b/apps/hype/package.json @@ -11,17 +11,7 @@ "verify": "yarn build && tsc --noEmit && yarn lint" }, "dependencies": { - "@chakra-ui/icons": "^2.1.1", - "@chakra-ui/react": "^2.8.2", - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.0", - "@fontsource/nunito": "^5.2.5", - "@fontsource/roboto-slab": "^5.2.5", "@rp/shared": "*", - "axios": "^1.6.8", - "chakra-react-select": "^5.0.5", - "formik": "^2.4.6", - "framer-motion": "11.3.23", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "7.8.1" diff --git a/apps/hype/public/fonts/Ethnocentric-Regular.otf b/apps/hype/public/fonts/Ethnocentric-Regular.otf new file mode 100644 index 00000000..c1dadc89 Binary files /dev/null and b/apps/hype/public/fonts/Ethnocentric-Regular.otf differ diff --git a/apps/hype/public/rp_logo.svg b/apps/hype/public/rp_logo.svg index ab7735e1..561e9c08 100644 --- a/apps/hype/public/rp_logo.svg +++ b/apps/hype/public/rp_logo.svg @@ -1,22 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/hype/src/App.css b/apps/hype/src/App.css index a392d4d8..043b9c63 100644 --- a/apps/hype/src/App.css +++ b/apps/hype/src/App.css @@ -1,158 +1,740 @@ -/* Global Styles */ -body { - background-color: black; - color: white; +@font-face { + font-family: "Ethnocentric Rg"; + src: url("/fonts/Ethnocentric-Regular.otf") format("opentype"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +:root { + --black: #0a0a0a; + --pink: #ff2d75; + --purple: #b829dd; + --pink-dim: rgba(255, 45, 117, 0.3); + --purple-dim: rgba(184, 41, 221, 0.25); + --train-duration: 4s; + --train2-duration: 1.6s; +} + +*, +*::before, +*::after { margin: 0; padding: 0; - overflow-x: hidden; - font-family: "Nunito", sans-serif; + box-sizing: border-box; +} + +html, +body, +#root { + width: 100%; + height: 100%; + overflow: hidden; + background: var(--black); + color: #fff; + font-family: + "Ethnocentric Rg", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Helvetica, + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji"; } -.app-wrapper { +#app { position: relative; - min-height: 100vh; - background-color: black; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; overflow: hidden; } -/* Background Grid with a Stronger, Closer-Spaced Pattern */ -.background-grid { - min-width: 100vw; - min-height: 100vh; +.vignette { position: fixed; inset: 0; pointer-events: none; + z-index: 99; + background: radial-gradient( + ellipse at center, + transparent 50%, + rgba(10, 10, 10, 0.7) 100% + ); +} - /* Increase line opacity, decrease spacing to 50px for a denser grid */ - background-image: - linear-gradient(to right, rgba(255, 255, 255, 0.3) 2px, transparent 1px), - linear-gradient(to bottom, rgba(255, 255, 255, 0.3) 2px, transparent 1px); - background-size: 100px 100px; +.logo-link { + position: fixed; + top: 40px; + left: 40px; + z-index: 101; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + opacity: 0.4; + transition: opacity 0.3s ease; +} - background-position: center; +.logo-link:hover { + opacity: 1; +} - /* Larger spotlight radius (250px) and more abrupt fade */ - mask-image: radial-gradient( - circle 250px at var(--mouse-x, 50vw) var(--mouse-y, 50vh), - white 0%, - transparent 70% - ); - -webkit-mask-image: radial-gradient( - circle 250px at var(--mouse-x, 50vw) var(--mouse-y, 50vh), - white 0%, - transparent 70% - ); +.rp-logo { + width: 64px; + height: 64px; +} + +.train { + position: absolute; + top: 50%; + left: 0; + transform: translate(-100%, -50%); + z-index: 50; + display: flex; + align-items: flex-end; + gap: 0; + opacity: 0; + will-change: transform; +} + +.animate .train { + animation: trainPass var(--train-duration) cubic-bezier(0.1, 0.7, 0.3, 1) 0.2s + forwards; +} + +.train--second { + top: calc(100% - 100px); + transform: translate(calc(100vw + 100%), -50%) scale(1.1); + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.animate2 .train--second.active { + opacity: 1; + visibility: visible; + animation: trainPassReverse var(--train2-duration) + cubic-bezier(0.1, 0.7, 0.3, 1) 0s forwards; +} + +@keyframes trainPass { + 0% { + transform: translate(-100%, -50%); + opacity: 1; + } + + 5% { + opacity: 1; + } + + 100% { + transform: translate(calc(100vw + 100%), -50%); + opacity: 1; + } +} + +@keyframes trainPassReverse { + 0% { + transform: translate(calc(100vw + 100%), -50%) scale(1.1); + } + + 100% { + transform: translate(-150%, -50%) scale(1.1); + } +} + +.train-engine { + width: 320px; + height: 200px; + background: var(--black); + border: 2px solid var(--purple); + border-radius: 4px 120px 10px 4px; + position: relative; +} + +.train-engine--front { + border-radius: 120px 4px 4px 10px; + transform: scaleX(-1); +} - z-index: 2; +.train-engine--front-reverse { + border-radius: 120px 4px 4px 10px; } -/* Main Container */ -.app-container { - background-color: black; - color: white; +.train-engine--alt { + border-color: var(--pink); +} + +.train-engine--alt .train-engine-cockpit { + background: var(--pink-dim); + border-color: var(--pink); +} + +.train-engine--alt .train-engine-headlight { + border-color: var(--pink); + box-shadow: + 0 0 20px 10px var(--pink-dim), + 10px 0 15px 3px var(--pink); +} + +.train-engine--alt .train-engine-grill { + border-color: var(--pink); +} + +.train-engine--alt .grill-line { + background: var(--pink); +} + +.train-engine-cockpit { + position: absolute; + right: 50px; + top: 50px; + width: 90px; + height: 35px; + background: var(--purple-dim); + border: 1px solid var(--purple); + border-radius: 20px 4px 4px 4px; + transform: skewX(-30deg); +} + +.train-engine-headlight { + position: absolute; + right: -6px; + bottom: 80px; + width: 12px; + height: 25px; + background: #fff; + border: 2px solid var(--purple); + border-radius: 4px; + box-shadow: + 0 0 20px 10px var(--purple-dim), + 10px 0 15px 3px var(--purple); +} + +.train-engine-grill { + position: absolute; + right: 0px; + bottom: 15px; + width: 40px; + height: 50px; + background: var(--black); + border-left: 2px solid var(--purple); + border-top: 2px solid var(--purple); + border-radius: 10px 0 0 0; display: flex; flex-direction: column; - align-items: center; - min-height: 100vh; + justify-content: space-evenly; + padding: 4px 0 4px 8px; +} + +.grill-line { width: 100%; + height: 4px; + background: var(--purple); + border-radius: 2px 0 0 2px; +} + +.train-body { + width: 520px; + height: 180px; + background: var(--black); + border: 2px solid var(--purple); + border-radius: 4px; + position: relative; + display: flex; + align-items: center; + gap: 24px; + padding: 0 36px; +} + +.train-body--alt { + border-color: var(--pink); + width: 420px; + height: 160px; +} + +.train-connector { + width: 20px; + height: 40px; + bottom: 20px; + position: relative; + background: var(--purple); + border-radius: 2px; +} + +.train-car { + width: 400px; + height: 155px; + background: var(--black); + border: 2px solid var(--purple); + border-radius: 4px; position: relative; - z-index: 1; + display: flex; + align-items: center; + gap: 24px; + padding: 0 36px; +} + +.train-car--alt { + border-color: var(--pink); + width: 320px; + height: 140px; } -.gif-container { +.train-car-half { + width: 320px; + height: 90px; + background: var(--black); + border: 2px solid var(--purple); + border-radius: 4px; position: relative; - width: 60vh; display: flex; + align-items: center; + gap: 24px; + padding: 0 36px; +} + +.train-window { + width: 56px; + height: 44px; + background: var(--purple-dim); + border: 1px solid var(--purple); + border-radius: 4px; +} + +.train-window--half { + width: 48px; + height: 28px; + background: var(--purple-dim); + border: 1px solid var(--purple); + border-radius: 4px; +} + +.train-car .train-window, +.train-car-half .train-window--half, +.train-body .train-window, +.train-engine .train-window { + border-color: var(--purple); +} + +.train-stripe { + position: absolute; + bottom: 16px; + left: 24px; + right: 24px; + height: 3px; + background: var(--purple); + border-radius: 2px; +} + +.train-car-half .train-stripe { + bottom: 10px; +} + +.train-wheel { + width: 36px; + height: 36px; + background: var(--black); + border: 3px solid var(--purple); + border-radius: 50%; + position: absolute; + bottom: -18px; +} + +.animate .train-wheel { + animation: wheelSpin 0.2s linear infinite; +} + +@keyframes wheelSpin { + to { + transform: rotate(360deg); + } +} + +.train-wheel--front { + left: 40px; +} + +.train-wheel--rear { + right: 40px; +} + +.bottom-buttons { + position: fixed; + bottom: 40px; + left: 0; + right: 0; + z-index: 30; + display: flex; + flex-direction: row; + align-items: center; justify-content: center; - top: 0; - overflow: hidden; + gap: 1.5em; + opacity: 0; + pointer-events: none; +} + +.bottom-buttons.visible { + opacity: 1; + pointer-events: auto; +} + +.social-links { + display: flex; + align-items: center; + gap: 1em; +} + +.social-item { + display: flex; + align-items: center; + justify-content: center; + width: 54px; + height: 54px; + border-radius: 50%; + border: 1px solid var(--purple); + background: rgba(10, 10, 10, 0.6); + backdrop-filter: blur(4px); + color: #fff; + transition: + border-color 0.3s ease, + background-color 0.3s ease; } -.centered-gif { - /* width: 80%; */ - max-width: 60vh; - height: auto; - object-position: center 15%; - object-fit: cover; +.social-item svg { + width: 24px; + height: 24px; +} + +.social-item:hover { + border-color: #fff; + color: #fff; +} + +@media (max-width: 768px) { + .bottom-buttons { + bottom: 25px; + gap: 1em; + } } .title-container { - transform: translateY(-5vh); + position: relative; + z-index: 30; text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + clip-path: inset(-200px 100% -200px -200px); + will-change: clip-path; width: 100%; - color: #b9b8b8; - z-index: 3; } -@keyframes fadeIn { - 0% { - opacity: 0; - transform: translateY(10px); - } - 100% { - opacity: 1; - transform: translateY(0); - } +.title { + font-size: clamp(1.2rem, 3vw, 2.2rem); + font-weight: 400; + letter-spacing: 0.08em; + text-transform: lowercase; + color: #fff; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 0.3em; + white-space: nowrap; + width: 100%; } -@keyframes glowSweep { - 0% { - background-position: -200%; - } - 100% { - background-position: 200%; - } +.title span:first-child { + text-align: right; } -.fade-in { - animation: fadeIn 1.8s ease-out both; +.title span:last-child { + text-align: left; } -.glow-text { - background: linear-gradient( - 90deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.8) 50%, - rgba(255, 255, 255, 0) 100% - ); - background-size: 200% auto; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; - animation: glowSweep 14s linear infinite; +.title-sep { + color: #fff; + font-weight: 400; +} + +.subtitle { + font-size: clamp(0.7rem, 1.5vw, 1rem); + font-weight: 400; + letter-spacing: 0.35em; + padding-left: 0.35em; + + text-transform: uppercase; + color: var(--pink); + margin-top: 1.5em; + opacity: 0; +} + +.subtitle.visible { + opacity: 1; +} + +#skyline-canvas { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + z-index: 10; + opacity: 0; + image-rendering: pixelated; +} + +#skyline-canvas.visible { + opacity: 1; +} + +.button-group { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 1.5em; + margin-top: 2em; + opacity: 0; + z-index: 40; +} + +.button-group.visible { + opacity: 1; +} + +.cta-button { + position: relative; + font-family: + "Ethnocentric Rg", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Helvetica, + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji"; + font-size: clamp(0.6rem, 1vw, 0.8rem); + font-weight: 400; + letter-spacing: 0.2em; + text-transform: uppercase; + text-decoration: none; + color: var(--pink); + border: 1px solid var(--pink); + padding: 1.04em 0; + width: 18em; + text-align: center; + text-indent: 0.2em; + border-radius: 2px; + background: var(--black); display: inline-block; + cursor: pointer; + transition: + background 0.3s ease, + color 0.3s ease, + box-shadow 0.3s ease, + border-color 0.3s ease; +} + +.cta-button:hover { + background: var(--pink); + color: var(--black); + box-shadow: + 0 0 15px var(--pink-dim), + 0 0 40px rgba(255, 45, 117, 0.15); +} + +.about-button { + margin-left: 0; +} + +.about-button:hover { + box-shadow: + 0 0 15px var(--pink-dim), + 0 0 40px rgba(255, 45, 117, 0.15); } -/* Mobile Styles */ @media (max-width: 768px) { - .gif-container { - top: 10vh; + .logo-link { + top: 25px; + left: 25px; + } + + .rp-logo { + width: 48px; + height: 48px; + } + + .train-engine { + width: 220px; + height: 140px; + } + + .train-engine-cockpit { + right: 30px; + top: 30px; + width: 60px; + height: 25px; + } + + .train-engine-headlight { + right: -4px; + bottom: 50px; + width: 8px; + height: 15px; + } + + .train-engine-grill { + bottom: 10px; + width: 25px; + height: 35px; + } + + .train-body { + width: 340px; + height: 130px; + } + + .train-car { + width: 260px; + height: 110px; + } + + .train-car-half { + width: 210px; + height: 75px; + } + + .train-window { + width: 40px; + height: 30px; + } + + .train-window--half { + width: 32px; + height: 22px; + } + + .train-wheel { + width: 28px; + height: 28px; + bottom: -14px; + } + + .train-wheel--front { + left: 20px; } - .centered-gif { - max-width: 90%; - object-position: center 20%; + .train-wheel--rear { + right: 20px; } - .title-container { - margin-top: 8vh; + .title { + gap: 0.2em; } } -@media (max-height: 600px) { - .gif-container { - top: 0vh; - width: 80vh; +@media (max-width: 480px) { + .logo-link { + top: 15px; + left: 15px; + } + + .rp-logo { + width: 40px; + height: 40px; + } + + .train-engine { + width: 160px; + height: 100px; + border-radius: 4px 60px 5px 4px; + } + + .train-engine-cockpit { + right: 20px; + top: 20px; + width: 40px; + height: 15px; + } + + .train-engine-headlight { + right: -3px; + bottom: 35px; + width: 6px; + height: 10px; + } + + .train-engine-grill { + bottom: 5px; + width: 15px; + height: 25px; + } + + .train-body { + width: 240px; + height: 95px; + padding: 0 16px; + gap: 14px; + } + + .train-car { + width: 190px; + height: 80px; + padding: 0 16px; + gap: 14px; + } + + .train-car-half { + width: 150px; + height: 60px; + padding: 0 16px; + gap: 14px; + } + + .train-window { + width: 28px; + height: 20px; + } + + .train-window--half { + width: 24px; + height: 16px; + } + + .train-connector { + width: 14px; + height: 26px; + bottom: 10px; + } + + .train-wheel { + width: 20px; + height: 20px; + bottom: -10px; + } + + .train-wheel--front { + left: 16px; + } + + .train-wheel--rear { + right: 16px; } - .centered-gif { - max-width: 90%; - object-position: center 20%; + .title { + gap: 0.2em; } - .title-container { - margin-top: 0vh; - transform: translateY(-50px); + .title-sep { + display: inline; } } diff --git a/apps/hype/src/App.tsx b/apps/hype/src/App.tsx index 7e737d21..bfae1475 100644 --- a/apps/hype/src/App.tsx +++ b/apps/hype/src/App.tsx @@ -1,138 +1,472 @@ -import { useEffect } from "react"; -import "./App.css"; -import "@fontsource/roboto-slab"; -import "@fontsource/nunito"; +import { useEffect, useRef } from "react"; + import { - ChakraProvider, - HStack, - VStack, - Text, - useMediaQuery, - Button, - Box, - Link -} from "@chakra-ui/react"; -import yourGif from "./assets/RPREC.gif"; // adjust path as needed + buildSkylineData, + drawSkyline, + animateWindowsOn, + resizeSkylineCanvas, + BuildingData +} from "./skyline"; function App() { - const [isSmall] = useMediaQuery("(max-width: 480px)"); - const [isMobile] = useMediaQuery("(max-width: 768px)"); + const appRef = useRef(null); + const trainRef = useRef(null); + const train2Ref = useRef(null); + const titleContainerRef = useRef(null); + const subtitleRef = useRef(null); + const ctaButtonRef = useRef(null); + const bottomButtonsRef = useRef(null); + const skylineCanvasRef = useRef(null); + const titleSepRef = useRef(null); + const buttonGroupRef = useRef(null); useEffect(() => { - const handleMouseMove = (e: { clientX: number; clientY: number }) => { - document.documentElement.style.setProperty("--mouse-x", `${e.clientX}px`); - document.documentElement.style.setProperty("--mouse-y", `${e.clientY}px`); - }; + const app = appRef.current; + const train = trainRef.current; + const titleContainer = titleContainerRef.current; + const subtitle = subtitleRef.current; + const ctaButton = ctaButtonRef.current; + const skylineCanvas = skylineCanvasRef.current; + const titleSep = titleSepRef.current; + + const train2 = train2Ref.current; + const bottomButtons = bottomButtonsRef.current; + const buttonGroup = buttonGroupRef.current; + + if ( + !app || + !train || + !train2 || + !titleContainer || + !subtitle || + !ctaButton || + !bottomButtons || + !skylineCanvas || + !titleSep || + !buttonGroup + ) + return; + + let timeout1: ReturnType; + let timeout2: ReturnType; + let cleanupReveal: (() => void) | undefined; + let cleanupReveal2: (() => void) | undefined; - window.addEventListener("mousemove", handleMouseMove); - return () => window.removeEventListener("mousemove", handleMouseMove); + const skylineData: BuildingData[] = buildSkylineData(); + resizeSkylineCanvas(skylineCanvas); + drawSkyline(skylineCanvas, skylineData); + + const rafId = requestAnimationFrame(() => { + timeout1 = setTimeout(() => { + app.classList.add("animate"); + cleanupReveal = runRevealLoop( + train, + titleContainer, + subtitle, + buttonGroup, + skylineCanvas, + skylineData + ); + timeout2 = setTimeout(() => { + app.classList.add("animate2"); + train2.classList.add("active"); + cleanupReveal2 = runRevealLoop2(train2, bottomButtons); + }, 1200); + }, 200); + }); + + return () => { + cancelAnimationFrame(rafId); + clearTimeout(timeout1); + clearTimeout(timeout2); + if (cleanupReveal) cleanupReveal(); + if (cleanupReveal2) cleanupReveal2(); + }; }, []); + function runRevealLoop( + train: HTMLDivElement, + titleContainer: HTMLDivElement, + subtitle: HTMLDivElement, + buttonGroup: HTMLDivElement, + skylineCanvas: HTMLCanvasElement, + skylineData: BuildingData[] + ) { + let revealed = false; + let skylineShown = false; + let windowsTriggered = false; + let titleLeft = 0; + let titleWidth = 0; + let cached = false; + let rafId: number; + let cleanupWindows: (() => void) | undefined; + const vw = window.innerWidth; + + function tick() { + const trainRect = train.getBoundingClientRect(); + + if (!cached) { + const titleRect = titleContainer.getBoundingClientRect(); + titleLeft = titleRect.left; + titleWidth = titleRect.width; + if (titleWidth > 0) cached = true; + } + + if (cached) { + const revealPx = trainRect.left - titleLeft; + const revealPercent = Math.max( + 0, + Math.min(100, (revealPx / titleWidth) * 100) + ); + const clipRight = 100 - revealPercent; + titleContainer.style.clipPath = `inset(-200px ${clipRight}% -200px -200px)`; + + if (revealPercent >= 10 && !revealed) { + revealed = true; + subtitle.classList.add("visible"); + buttonGroup.classList.add("visible"); + } + + if (revealPercent >= 100) { + titleContainer.style.clipPath = "inset(-200px -200px -200px -200px)"; + } + } + + if (!skylineShown && trainRect.left > vw * 0.15) { + skylineShown = true; + skylineCanvas.classList.add("visible"); + } + + if (skylineShown && !windowsTriggered) { + windowsTriggered = true; + cleanupWindows = animateWindowsOn(skylineCanvas, skylineData); + } + + if (trainRect.left > vw) { + return; + } + + rafId = requestAnimationFrame(tick); + } + + rafId = requestAnimationFrame(tick); + return () => { + cancelAnimationFrame(rafId); + if (cleanupWindows) cleanupWindows(); + }; + } + + function runRevealLoop2( + train2: HTMLDivElement, + bottomButtons: HTMLDivElement + ) { + let revealed = false; + let rafId: number; + const vw = window.innerWidth; + + function tick2() { + const trainRect = train2.getBoundingClientRect(); + + if (!revealed && trainRect.left < vw * 0.6) { + revealed = true; + bottomButtons.classList.add("visible"); + } + + if (trainRect.right < 0) return; + rafId = requestAnimationFrame(tick2); + } + + rafId = requestAnimationFrame(tick2); + return () => cancelAnimationFrame(rafId); + } + return ( - -
- {/* Background grid layer with spotlight mask */} -
-
-
- Centered GIF +
+
+ + + RP Logo + + +
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
-
- - - reflections - - - | - - - projections - - - - - 2025 - - - - coming soon - - - - - - +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+

+ reflections + + | + + projections +

+

+ coming soon 2026 +

+ +
+ + - + + +
); } diff --git a/apps/hype/src/assets/RPREC.gif b/apps/hype/src/assets/RPREC.gif deleted file mode 100644 index a62f3f1c..00000000 Binary files a/apps/hype/src/assets/RPREC.gif and /dev/null differ diff --git a/apps/hype/src/assets/rplogo.svg b/apps/hype/src/assets/rplogo.svg new file mode 100644 index 00000000..43ef9a30 --- /dev/null +++ b/apps/hype/src/assets/rplogo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/hype/src/customTheme.ts b/apps/hype/src/customTheme.ts deleted file mode 100644 index 11272564..00000000 --- a/apps/hype/src/customTheme.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { extendTheme, ThemeConfig } from "@chakra-ui/react"; - -const config: ThemeConfig = { - initialColorMode: "light", - useSystemColorMode: false -}; - -export const customTheme = extendTheme({ config }); diff --git a/apps/hype/src/index.css b/apps/hype/src/index.css deleted file mode 100644 index 2ce62ef9..00000000 --- a/apps/hype/src/index.css +++ /dev/null @@ -1,69 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; - background-color: black; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/apps/hype/src/main.tsx b/apps/hype/src/main.tsx index 0869d9d9..b1d48616 100644 --- a/apps/hype/src/main.tsx +++ b/apps/hype/src/main.tsx @@ -1,14 +1,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { ChakraProvider } from "@chakra-ui/react"; import App from "./App.tsx"; - -import { customTheme } from "./customTheme.ts"; +import "./App.css"; ReactDOM.createRoot(document.getElementById("root")!).render( - - - + ); diff --git a/apps/hype/src/skyline.ts b/apps/hype/src/skyline.ts new file mode 100644 index 00000000..3cb78c92 --- /dev/null +++ b/apps/hype/src/skyline.ts @@ -0,0 +1,265 @@ +export interface WindowData { + ox: number; + oy: number; + size: number; + on: boolean; + brightness: number; + burstGroup: number; + isStraggler: boolean; + stragglerDelay: number; + sparkleSpeed: number; + sparkleOffset: number; +} + +export interface BuildingData { + x: number; + w: number; + h: number; + windows: WindowData[]; +} + +let _seed = 2026; +export function seededRandom() { + _seed |= 0; + _seed = (_seed + 0x6d2b79f5) | 0; + let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; +} + +export function rand(min: number, max: number) { + return seededRandom() * (max - min) + min; +} + +export function buildSkylineData(): BuildingData[] { + _seed = 2026; + const vw = window.innerWidth; + const isMobile = vw < 768; + const isSmall = vw < 480; + + const maxH = isSmall ? 400 : isMobile ? 600 : 900; + const minH = isSmall ? 80 : isMobile ? 120 : 160; + const maxW = isSmall ? 35 : isMobile ? 50 : 75; + const minW = isSmall ? 18 : isMobile ? 25 : 35; + const gap = isSmall ? 3 : 4; + + const avgWidth = (maxW + minW) / 2 + gap; + const count = Math.ceil(vw / avgWidth) + 2; + + const winSize = isSmall ? 4 : 6; + const winGapX = isSmall ? 12 : 14; + const winGapY = isSmall ? 18 : 22; + + const buildings: BuildingData[] = []; + const totalEstWidth = count * avgWidth; + const startX = Math.max(0, (vw - totalEstWidth) / 2); + let x = startX; + + for (let i = 0; i < count; i++) { + const normalizedPos = i / (count - 1); + const distFromCenter = Math.abs(normalizedPos - 0.5) * 2; + + const heightCap = 0.25 + distFromCenter * 0.75; + const effectiveMaxH = maxH * heightCap; + + const h = Math.floor(rand(minH, Math.max(minH, effectiveMaxH))); + const w = Math.floor(rand(minW, maxW)); + + const windows: WindowData[] = []; + const cols = Math.floor((w - 8) / winGapX); + const rows = Math.floor((h - 14) / winGapY); + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + if (seededRandom() > 0.3) continue; + + const burstGroup = Math.floor(seededRandom() * 3); + const isStraggler = seededRandom() < 0.04; + + windows.push({ + ox: 5 + c * winGapX, + oy: 10 + r * winGapY, + size: winSize, + on: false, + brightness: rand(0.3, 0.8), + burstGroup, + isStraggler, + stragglerDelay: isStraggler ? rand(1.5, 4.0) : 0, + sparkleSpeed: rand(0.8, 2.0), + sparkleOffset: rand(0, Math.PI * 2) + }); + } + } + + buildings.push({ x, w, h, windows }); + x += w + gap; + } + + return buildings; +} + +function interpolateColor(t: number) { + const r = Math.floor(0 + t * (184 - 0)); + const g = Math.floor(100 + t * (41 - 100)); + const b = Math.floor(255 + t * (221 - 255)); + return `${r}, ${g}, ${b}`; +} + +export function resizeSkylineCanvas(canvas: HTMLCanvasElement) { + const vw = window.innerWidth; + const vh = window.innerHeight; + const canvasH = Math.floor(vh * 0.9); + const dpr = Math.min(window.devicePixelRatio || 1, 2); + + const targetW = Math.floor(vw * dpr); + const targetH = Math.floor(canvasH * dpr); + + if (canvas.width !== targetW || canvas.height !== targetH) { + canvas.width = targetW; + canvas.height = targetH; + canvas.style.width = vw + "px"; + canvas.style.height = canvasH + "px"; + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); + } + } +} + +export function drawSkyline( + canvas: HTMLCanvasElement, + buildings: BuildingData[], + elapsed: number = 0 +) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const vw = canvas.clientWidth; + const vh = canvas.clientHeight; + + ctx.clearRect(0, 0, vw, vh); + + const bgColor = "#0a0a0a"; + + for (const b of buildings) { + const by = vh - b.h; + + ctx.fillStyle = bgColor; + ctx.fillRect(b.x, by, b.w, b.h); + + ctx.strokeStyle = "rgba(158, 25, 191, 0.24)"; + ctx.lineWidth = 1; + + ctx.beginPath(); + ctx.moveTo(b.x + 0.5, by); + ctx.lineTo(b.x + 0.5, vh); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(b.x + b.w - 0.5, by); + ctx.lineTo(b.x + b.w - 0.5, vh); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(b.x, by + 0.5); + ctx.lineTo(b.x + b.w, by + 0.5); + ctx.stroke(); + + for (const win of b.windows) { + if (!win.on) continue; + const wx = b.x + win.ox; + const wy = by + win.oy; + + const sinVal = + (Math.sin(elapsed * win.sparkleSpeed + win.sparkleOffset) + 1) / 2; + const sparkle = Math.pow(sinVal, 10); + const t = (Math.sin(elapsed * 0.3 + win.sparkleOffset) + 1) / 2; + const rgb = interpolateColor(t); + const alpha = win.brightness * (0.1 + sparkle * 0.9); + + ctx.fillStyle = `rgba(${rgb}, ${alpha})`; + ctx.fillRect(wx, wy, win.size, win.size); + + if (alpha > 0.8) { + ctx.fillStyle = `rgba(${rgb}, 0.2)`; + ctx.fillRect(wx - 1, wy - 1, win.size + 2, win.size + 2); + } + } + } +} + +export function animateWindowsOn( + canvas: HTMLCanvasElement, + buildings: BuildingData[] +) { + const burstStartTimes = [0.3, 0.8, 1.4]; + const flickerDuration = 0.2; + const startTime = performance.now(); + const burstState = [0, 0, 0]; + let rafId: number; + + function frame() { + resizeSkylineCanvas(canvas); + const now = performance.now(); + const elapsed = (now - startTime) / 1000; + + for (let g = 0; g < 3; g++) { + if (burstState[g] === 2) continue; + + if (burstState[g] === 0 && elapsed >= burstStartTimes[g]) { + burstState[g] = 1; + } + + if (burstState[g] === 1) { + const flickerElapsed = elapsed - burstStartTimes[g]; + const flickerProgress = flickerElapsed / flickerDuration; + + if (flickerProgress >= 1) { + burstState[g] = 2; + for (const b of buildings) { + for (const win of b.windows) { + if (!win.isStraggler && win.burstGroup === g) win.on = true; + } + } + } else { + const step = Math.floor(flickerProgress * 5); + const isOn = step % 2 === 0; + for (const b of buildings) { + for (const win of b.windows) { + if (!win.isStraggler && win.burstGroup === g) win.on = isOn; + } + } + } + } + } + + for (const b of buildings) { + for (const win of b.windows) { + if (!win.isStraggler) continue; + if (win.on) continue; + + if (elapsed >= win.stragglerDelay) { + const flickerElapsed = elapsed - win.stragglerDelay; + const flickerProgress = flickerElapsed / 0.12; + + if (flickerProgress >= 1) { + win.on = true; + } else { + const step = Math.floor(flickerProgress * 3); + win.on = step % 2 === 0; + } + } + } + } + + drawSkyline(canvas, buildings, elapsed); + + rafId = requestAnimationFrame(frame); + } + + rafId = requestAnimationFrame(frame); + return () => { + if (rafId) cancelAnimationFrame(rafId); + }; +} diff --git a/apps/hype/src/vite-env.d.ts b/apps/hype/src/vite-env.d.ts index 11f02fe2..e69de29b 100644 --- a/apps/hype/src/vite-env.d.ts +++ b/apps/hype/src/vite-env.d.ts @@ -1 +0,0 @@ -/// diff --git a/apps/hype/vite.config.ts b/apps/hype/vite.config.ts index 2a288a53..51719cb0 100644 --- a/apps/hype/vite.config.ts +++ b/apps/hype/vite.config.ts @@ -2,10 +2,10 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import { resolve } from "path"; -// https://vitejs.dev/config/ export default defineConfig({ server: { - port: 3004 + port: 3004, + allowedHosts: true }, plugins: [react()], envDir: resolve(__dirname, "../.."), diff --git a/yarn.lock b/yarn.lock index fc48b8bb..5fcaca3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -941,14 +941,14 @@ __metadata: languageName: node linkType: hard -"@fontsource/nunito@npm:^5.0.18, @fontsource/nunito@npm:^5.2.5": +"@fontsource/nunito@npm:^5.0.18": version: 5.2.6 resolution: "@fontsource/nunito@npm:5.2.6" checksum: 10c0/ddda7b88529ebae5922749970c16acc755352624c07ffa8ea7dcacdf4ed680a1e67c4b79c758083134e2197f1dd1933125fead28903f235b37d6c8b1232b317c languageName: node linkType: hard -"@fontsource/roboto-slab@npm:^5.0.20, @fontsource/roboto-slab@npm:^5.2.5": +"@fontsource/roboto-slab@npm:^5.0.20": version: 5.2.7 resolution: "@fontsource/roboto-slab@npm:5.2.7" checksum: 10c0/072184b265fe78091980fbd67ba93c0e587aa533677f66be692d9128bc53532ca54fec13bc41d436af166ee424399886f5c8785dbabbfdb39f557bd8806cc16e @@ -1371,12 +1371,6 @@ __metadata: version: 0.0.0-use.local resolution: "@rp/hype@workspace:apps/hype" dependencies: - "@chakra-ui/icons": "npm:^2.1.1" - "@chakra-ui/react": "npm:^2.8.2" - "@emotion/react": "npm:^11.11.4" - "@emotion/styled": "npm:^11.11.0" - "@fontsource/nunito": "npm:^5.2.5" - "@fontsource/roboto-slab": "npm:^5.2.5" "@rp/shared": "npm:*" "@types/node": "npm:^20.12.7" "@types/react": "npm:^18.2.66" @@ -1384,10 +1378,6 @@ __metadata: "@types/react-dom": "npm:^18.2.22" "@vitejs/plugin-react": "npm:^4.3.4" "@vitejs/plugin-react-swc": "npm:^3.5.0" - axios: "npm:^1.6.8" - chakra-react-select: "npm:^5.0.5" - formik: "npm:^2.4.6" - framer-motion: "npm:11.3.23" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-router-dom: "npm:7.8.1"