diff --git a/website/index.html b/website/index.html index d39c245..8bfab7b 100644 --- a/website/index.html +++ b/website/index.html @@ -10,7 +10,7 @@ diff --git a/website/src/App.css b/website/src/App.css deleted file mode 100644 index e69de29..0000000 diff --git a/website/src/App.jsx b/website/src/App.jsx index f29971e..5b7b58f 100644 --- a/website/src/App.jsx +++ b/website/src/App.jsx @@ -1,6 +1,4 @@ -import { useState } from 'react' -import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom"; -import './App.css' +import { Routes, Route, Navigate } from "react-router-dom"; import Menu from './Menu' import Homepage from './Homepage'; import Bird_CNN from './Bird_CNN'; diff --git a/website/src/Bird_CNN.jsx b/website/src/Bird_CNN.jsx index 193713f..fd323e0 100644 --- a/website/src/Bird_CNN.jsx +++ b/website/src/Bird_CNN.jsx @@ -86,16 +86,9 @@ function Bird_CNN() { <>

Bird Species Expert

-
+
-

Explanation

+

Explanation

Select and upload an image to our bird classification model. @@ -218,7 +211,7 @@ function Bird_CNN() {
-

Model Architecture

+

Model Architecture

diff --git a/website/src/Bird_CNN.module.css b/website/src/Bird_CNN.module.css index 5ecd503..e286a81 100644 --- a/website/src/Bird_CNN.module.css +++ b/website/src/Bird_CNN.module.css @@ -1,3 +1,11 @@ +.species-list { + padding-left: 24px; + margin: 12px 0 0 0; + display: flex; + flex-direction: column; + gap: 6px; +} + .species-item { list-style: none; position: relative; @@ -26,19 +34,31 @@ a:hover { border-color: #c59eff; } +.content-box { + width: 100%; + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: center; + gap: 30px; +} .explanation-box { flex: 1; - min-width: 300px; + min-width: 280px; display: flex; flex-direction: column; - flex-wrap: wrap; - overflow: hidden; - border-radius: 12px; - margin: 50px 50px 50px 50px; + gap: 4px; + padding: 10px 0; +} + +.explanation-heading { + margin-top: 0; + color: var(--surface); } .image-box { + width: 100%; height: fit-content; margin-top: 40px; margin-bottom: 40px; @@ -58,7 +78,7 @@ a:hover { user-select: none; height: auto; max-height: 400px; - max-width: min(700px, 80vw); + max-width: 100%; } .image-box img:hover { @@ -124,12 +144,13 @@ a:hover { .request-box { height: fit-content; - width: min(700px, 80%); + width: min(500px, 100%); + min-width: 0; + padding: 10px 0; display: flex; flex-direction: column; align-items: center; - flex: 0 0 auto; - margin: 0px 50px 0px 50px; + flex: 0 1 auto; } .request-box * { @@ -148,9 +169,9 @@ a:hover { } .response-block { - width: 80%; + width: 100%; height: 100px; - margin: 40px 40px 100px 40px; + margin: clamp(20px, 5vw, 40px) 0 clamp(20px, 6vw, 60px) 0; display: flex; flex-direction: column; align-items: center; diff --git a/website/src/Homepage.jsx b/website/src/Homepage.jsx index af346ee..c1fbf85 100644 --- a/website/src/Homepage.jsx +++ b/website/src/Homepage.jsx @@ -1,9 +1,50 @@ +import { Link } from 'react-router-dom'; +import styles from './Homepage.module.css'; + +const projects = [ + { + to: "/bird_cnn", + title: "Bird Species Expert", + description: "A convolutional neural network that classifies bird species from a photo, with a live confidence score.", + tag: "CNN · PyTorch", + }, + { + to: "/object_detection", + title: "Face Detection", + description: "A YOLO object detection model running on a live webcam feed, streamed to the server over a websocket.", + tag: "YOLO · Realtime", + }, + { + to: "/reviews", + title: "Reviews", + description: "Leave a short, anonymous review of this site or my portfolio.", + tag: "Feedback", + }, +]; + function Homepage() { return ( - <> -

Work in Progress, ignore this page and visit other Pages

- +
+

Marvin Krausser

+ +

+ I build and deploy small machine learning projects end to end — + from training the model to serving it behind a live API. Below are a + couple of interactive demos, running on real models. +

+ +
+ {projects.map(({ to, title, description, tag }) => ( + + {tag} +

{title}

+

{description}

+ Try it → + + ))} +
+
); } -export default Homepage; \ No newline at end of file +export default Homepage; diff --git a/website/src/Homepage.module.css b/website/src/Homepage.module.css new file mode 100644 index 0000000..f386d89 --- /dev/null +++ b/website/src/Homepage.module.css @@ -0,0 +1,53 @@ +.intro { + max-width: 65ch; + font-size: 1.1rem; + margin: -10px 0 50px 0; +} + +.grid { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 24px; +} + +.tile { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + text-decoration: none; + transition: border-color 0.15s, transform 0.15s, background-color 0.15s; +} + +.tile:hover { + border-color: var(--surface); + background: rgba(47, 168, 208, 0.08); + transform: translateY(-3px); +} + +.tag { + font-family: "Space Mono", monospace; + font-size: 0.75rem; + letter-spacing: 0.03em; + color: var(--surface); + text-transform: uppercase; +} + +.tileTitle { + margin: 0; + font-size: 1.7rem; + color: var(--text); +} + +.tileText { + margin: 0; + color: var(--text-dim); + flex-grow: 1; +} + +.cta { + margin-top: 8px; + font-weight: bold; + color: var(--surface); +} diff --git a/website/src/Menu.css b/website/src/Menu.css index b4a1963..aa30cc2 100644 --- a/website/src/Menu.css +++ b/website/src/Menu.css @@ -1,29 +1,138 @@ #navbar-main { width: 100%; - height: 80px; - display: flex; - gap: 20px; - justify-content: center; - align-items: center; - border-bottom: 1px solid rgb(47, 168, 208); + height: var(--nav-height); position: fixed; top: 0; left: 0; - z-index: 10; - background: rgb(4, 0, 10); + z-index: 100; + background: rgba(4, 0, 10, 0.75); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--surface-line); +} + +#navbar-inner { + max-width: 1200px; + height: 100%; + margin: 0 auto; + padding: 0 clamp(16px, 4vw, 40px); + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.navbar-brand { + font-family: 'Inter', system-ui, sans-serif; + font-weight: 600; + letter-spacing: -0.01em; + font-size: 1.2rem; + text-decoration: none; + color: var(--text); + white-space: nowrap; + flex-shrink: 0; +} + +.navbar-brand:hover { + color: var(--surface); +} + +#navbar-links { + display: flex; + align-items: center; + gap: 8px; } .navbar-item { text-decoration: none; - height: 80px; - width: 120px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.1rem; + color: var(--text-dim); + padding: 8px 16px; + border-radius: 20px; + font-size: 1rem; font-weight: bold; + white-space: nowrap; + transition: background-color 0.15s, color 0.15s; } .navbar-item:hover { - background-color: rgb(47, 168, 208); -} \ No newline at end of file + color: var(--text); + background-color: var(--surface-dim); +} + +.navbar-item.active { + color: black; + background-color: var(--surface); +} + +#navbar-toggle { + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 5px; + width: 40px; + height: 40px; + padding: 0; + background: transparent; + border: none; + cursor: pointer; + flex-shrink: 0; + z-index: 101; +} + +#navbar-toggle .bar { + width: 22px; + height: 2px; + background: var(--text); + border-radius: 2px; + transition: transform 0.2s ease, opacity 0.2s ease; +} + +#navbar-toggle .bar-1.open { + transform: translateY(7px) rotate(45deg); +} + +#navbar-toggle .bar-2.open { + opacity: 0; +} + +#navbar-toggle .bar-3.open { + transform: translateY(-7px) rotate(-45deg); +} + +@media (max-width: 720px) { + #navbar-toggle { + display: flex; + } + + #navbar-links { + position: fixed; + top: var(--nav-height); + left: 0; + width: 100%; + flex-direction: column; + align-items: stretch; + gap: 0; + background: rgba(4, 0, 10, 0.97); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--surface-line); + max-height: 0; + overflow: hidden; + transition: max-height 0.25s ease; + } + + #navbar-links.open { + max-height: 320px; + } + + .navbar-item { + border-radius: 0; + padding: 16px 24px; + text-align: center; + } + + .navbar-item.active { + background-color: var(--surface-dim); + color: var(--surface); + } +} diff --git a/website/src/Menu.jsx b/website/src/Menu.jsx index 1eb35a0..e436c28 100644 --- a/website/src/Menu.jsx +++ b/website/src/Menu.jsx @@ -1,17 +1,54 @@ -import { Link } from 'react-router-dom'; +import { useState } from 'react'; +import { NavLink } from 'react-router-dom'; import './Menu.css' +const links = [ + { to: "/", label: "Home" }, + { to: "/bird_cnn", label: "Birds" }, + { to: "/object_detection", label: "YOLO" }, + { to: "/reviews", label: "Reviews" }, +]; + function Menu() { + const [open, setOpen] = useState(false); + + const linkClass = ({ isActive }) => + `navbar-item${isActive ? " active" : ""}`; + return ( - <> - - + ); } -export default Menu; \ No newline at end of file +export default Menu; diff --git a/website/src/Object_Detection.jsx b/website/src/Object_Detection.jsx index 72ddfc0..c6f3875 100644 --- a/website/src/Object_Detection.jsx +++ b/website/src/Object_Detection.jsx @@ -174,9 +174,9 @@ function Object_Detection() { return (

Face Detection

-
+

- The model’s performance has been limited to 2 FPS due to low-end server hardware constraints. + The model's performance has been limited to 2 FPS due to low-end server hardware constraints.

The webcam video is transmitted to a server for processing. The server does not store, train on, or use the data for any purpose other than face detection. @@ -196,13 +196,13 @@ function Object_Detection() { {running ? "End Detection" : "Start Detection"}

-
+
-
+

This Project is an implementation of the YOLO (You Only Look Once) algorithm. It was implemented with Pytorch and trained on a Dataset from Roboflow.

diff --git a/website/src/Object_Detection.module.css b/website/src/Object_Detection.module.css index 7dd390a..efe7c6f 100644 --- a/website/src/Object_Detection.module.css +++ b/website/src/Object_Detection.module.css @@ -1,36 +1,40 @@ -#video-container { - margin: 0px; - width: min(70vh, 90vw); - height: min(70vh, 90vw); +.video-container { + margin: clamp(20px, 5vh, 50px) 0; + width: min(70vh, 100%); + max-width: 600px; + aspect-ratio: 1 / 1; background-color: black; + border-radius: var(--radius); + overflow: hidden; + align-self: center; + position: relative; + border: 1px solid var(--surface-line); +} + +.button-div { + width: fit-content; + align-self: center; + margin: clamp(24px, 6vw, 50px) 0; +} + +.text-container-explanation { + margin: clamp(30px, 6vw, 70px) 0 0 0; + width: 100%; + max-width: 800px; align-self: center; position: relative; } -.button-div{ - width: 200px; - align-self: center; - margin: 50px; -} - -#text-container-explanation { - margin: 70px 0 0 0; - width: 80vw; - height: 300px; +.text-container-introduction { + margin: clamp(20px, 5vw, 50px) 0 0 0; + width: 100%; + max-width: 800px; align-self: center; position: relative; } -#text-container-introduction { - margin: 50px 0 20px 0; - width: 70vw; - height: fit-content; - align-self: center; - position: relative; - left: -10vw; -} - -#text-container-introduction p { - margin: 40px 0 40px 0; +.text-container-introduction p, +.text-container-explanation p { + margin: 20px 0; font-size: 1.1rem; -} \ No newline at end of file +} diff --git a/website/src/Reviews.module.css b/website/src/Reviews.module.css index 434cd9d..69f8379 100644 --- a/website/src/Reviews.module.css +++ b/website/src/Reviews.module.css @@ -1,17 +1,15 @@ .review-description { - margin: 20px 70px 70px 70px; + margin: 0 0 clamp(30px, 6vw, 60px) 0; font-size: 1.1rem; - width: min(800px, 70vw); + max-width: 65ch; height: fit-content; } .input-box { display: grid; - gap: 10px; - column-gap: 10vw; - width: 98vw; - max-width: calc(3 * 200px + 10vw * 2); - min-height: 100vh; + gap: 30px 60px; + width: 100%; + max-width: 800px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-auto-rows: fit-content(300px); @@ -35,6 +33,8 @@ .input-component-box.text { grid-column: 1 / -1; + width: 100%; + justify-items: center; } .description { @@ -167,7 +167,7 @@ .review-text { resize: none; overflow:auto; - width: min(600px, 98vw); + width: min(600px, 100%); height: 400px; border-radius: 8px; border: 2px solid var(--bg); diff --git a/website/src/index.css b/website/src/index.css index 14bc47d..d450801 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -1,43 +1,98 @@ +:root { + --surface: rgb(47, 168, 208); + --surface-dim: rgba(47, 168, 208, 0.15); + --surface-line: rgba(47, 168, 208, 0.35); + --bg: rgb(4, 0, 10); + --bg-elevated: rgb(13, 10, 22); + --text: rgb(255, 255, 255); + --text-dim: rgba(255, 255, 255, 0.68); + --accent-2: #c59eff; + --radius: 16px; + --nav-height: 70px; + --page-pad: clamp(20px, 6vw, 100px); +} + html { - font-family: system-ui, sans-serif; - color: rgb(255, 255, 255); + font-family: system-ui, -apple-system, sans-serif; + color: var(--text); font-size: 1rem; line-height: 1.7; - + -webkit-text-size-adjust: 100%; } body { background: var(--bg); + background-image: + radial-gradient(ellipse 900px 500px at 15% -10%, var(--surface-dim), transparent 60%), + radial-gradient(ellipse 700px 500px at 100% 10%, rgba(197, 158, 255, 0.08), transparent 55%); + background-attachment: fixed; margin: 0; padding: 0; - --surface: rgb(47, 168, 208); - --bg: rgb(4, 0, 10); + min-height: 100vh; + overflow-x: hidden; } button { font-size: 1rem; + font-family: inherit; } a { - color: rgb(255, 255, 255); + color: var(--text); } h1, h2, h3, h4 { - font-family: 'Times New Roman', serif; + font-family: 'Inter', system-ui, sans-serif; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.15; +} + +p { + color: var(--text-dim); } * { box-sizing: border-box; } +*:focus-visible { + outline: 2px solid var(--accent-2); + outline-offset: 3px; +} + +::selection { + background: var(--surface); + color: black; +} + +/* scrollbar theming */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--surface); +} + .site-headline { - margin: 50px min(100px, 10vw) 50px min(100px, 10vw); + margin: 0 0 clamp(24px, 5vw, 50px) 0; color: var(--surface); font-weight: bold; - font-size: clamp(1rem, min(8vw, 8vh), 4rem); + font-size: clamp(2.2rem, 8vw, 4rem); position: relative; width: fit-content; } @@ -45,8 +100,8 @@ h4 { .site-headline::before { content: ""; position: absolute; - bottom: 0; - right: 0; + bottom: -6px; + left: 0; height: 4px; width: 100%; border-radius: 20px; @@ -54,11 +109,30 @@ h4 { } .site-box { - margin-top: 150px; + margin-top: calc(var(--nav-height) + clamp(30px, 8vh, 80px)); + margin-bottom: clamp(40px, 8vh, 100px); width: 100%; + max-width: 1200px; + padding: 0 var(--page-pad); height: fit-content; display: flex; flex-direction: column; + margin-left: auto; + margin-right: auto; +} + +.page-lead { + color: var(--text-dim); + font-size: 1.05rem; + max-width: 70ch; + margin: -10px 0 40px 0; +} + +.card { + background: var(--bg-elevated); + border: 1px solid var(--surface-line); + border-radius: var(--radius); + padding: clamp(20px, 3vw, 32px); } .custom-button { @@ -75,6 +149,7 @@ h4 { display: flex; align-items: center; justify-content: center; + transition: border-color 0.15s, color 0.15s, background-color 0.15s; } .custom-button:hover { @@ -90,4 +165,5 @@ h4 { .custom-button.inactive:hover { border-color: var(--surface); -} \ No newline at end of file + color: black; +}