diff --git a/package-lock.json b/package-lock.json index 7653b45..0b2f1c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "simulation-website", "version": "0.0.0", "dependencies": { + "@fontsource/michroma": "^5.2.8", "lucide-react": "^1.21.0", "react": "^19.2.6", "react-dom": "^19.2.6" @@ -429,6 +430,15 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@fontsource/michroma": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/michroma/-/michroma-5.2.8.tgz", + "integrity": "sha512-52Ml9TU/yC5YluqHxXqLK/AVUWbIeI3lCTZcTRnIkvfKHPZ0GSvSFi49ti872oCdo+YAMUk8RIfxVxXa6Cjigw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", diff --git a/package.json b/package.json index bf0a078..fe94661 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource/michroma": "^5.2.8", "lucide-react": "^1.21.0", "react": "^19.2.6", "react-dom": "^19.2.6" diff --git a/src/App.css b/src/App.css index 3b6e942..175b122 100644 --- a/src/App.css +++ b/src/App.css @@ -14,13 +14,12 @@ .topbar { display: grid; - grid-template-columns: minmax(420px, 1.35fr) 230px minmax(300px, 360px) minmax(220px, 0.7fr); + grid-template-columns: minmax(610px, 1.45fr) 230px minmax(300px, 360px) minmax(220px, 0.7fr); gap: 28px; align-items: center; margin-bottom: 16px; } -.brand-lockup, .mode-card, .weather, .runtime-status { @@ -28,20 +27,39 @@ align-items: center; } +.brand-lockup { + display: grid; + grid-template-columns: auto 1px minmax(230px, 1fr); + gap: 28px; + align-items: center; +} + .brand { - font-size: clamp(54px, 4vw, 76px); + display: flex; + align-items: center; + gap: 14px; line-height: 0.82; +} + +.brand img { + width: 66px; + height: 66px; + object-fit: contain; + filter: drop-shadow(0 0 16px rgba(32, 232, 255, 0.18)); +} + +.brand span { + font-family: Michroma, Eurostile, "Arial Black", sans-serif; + font-size: clamp(38px, 2.55vw, 48px); font-weight: 700; - letter-spacing: -0.04em; - background: linear-gradient(100deg, #3f79ff 5%, #00d9ff 62%, #22efc6 100%); - -webkit-background-clip: text; - color: transparent; + letter-spacing: 0.015em; + color: #f4f8ff; + text-shadow: 0 0 22px rgba(30, 226, 255, 0.16); } .divider { width: 1px; height: 48px; - margin: 0 28px; background: linear-gradient(transparent, rgba(95, 201, 255, 0.42), transparent); } @@ -392,13 +410,27 @@ h1 { transition: transform 0.6s ease, filter 0.6s ease; } +.floorplan-wrap::before { + content: ''; + position: absolute; + inset: 5% 3% 3%; + z-index: 0; + background: + radial-gradient(circle at 28% 44%, rgba(35, 134, 255, 0.18), transparent 22%), + radial-gradient(circle at 67% 48%, rgba(46, 238, 188, 0.12), transparent 19%), + radial-gradient(circle at 75% 72%, rgba(245, 163, 83, 0.14), transparent 17%); + filter: blur(20px); + opacity: 0.9; + pointer-events: none; +} + .floorplan-wrap.zoomed { transform: scale(1.035); } .view-tab { position: absolute; - z-index: 4; + z-index: 7; top: -9px; left: 50%; transform: translateX(-50%); @@ -416,11 +448,24 @@ h1 { } .floorplan { + position: relative; + z-index: 2; width: 100%; height: 600px; overflow: visible; } +.airflow-canvas { + position: absolute; + inset: 0; + z-index: 4; + width: 100%; + height: 100%; + pointer-events: none; + mix-blend-mode: screen; + opacity: 0.94; +} + .foundation { fill: rgba(166, 188, 216, 0.24); stroke: rgba(201, 221, 245, 0.38); @@ -431,11 +476,12 @@ h1 { fill: url(#planDepth); stroke: rgba(185, 214, 247, 0.56); stroke-width: 0.45; + filter: drop-shadow(0 12px 12px rgba(0, 0, 0, 0.45)); } .room rect { - fill: rgba(44, 72, 103, 0.48); - stroke: rgba(154, 187, 222, 0.26); + fill: rgba(44, 72, 103, 0.54); + stroke: rgba(185, 214, 247, 0.31); stroke-width: 0.45; } @@ -450,9 +496,10 @@ h1 { .wall { fill: none; - stroke: rgba(196, 215, 238, 0.72); - stroke-width: 1.6; + stroke: rgba(213, 232, 255, 0.78); + stroke-width: 1.9; stroke-linecap: square; + filter: drop-shadow(0 0 3px rgba(142, 201, 255, 0.2)); } .hub { @@ -471,19 +518,20 @@ h1 { .flow { fill: none; - stroke: rgba(42, 137, 255, 0.52); - stroke-width: 3.6; + stroke: rgba(42, 137, 255, 0.24); + stroke-width: 2.2; stroke-linecap: round; - opacity: 0.92; + opacity: 0.68; } .flow-tracer { fill: none; stroke: #25e9ff; - stroke-width: 1.2; + stroke-width: 0.8; stroke-linecap: round; - stroke-dasharray: 9 17; + stroke-dasharray: 3 13; animation: flowRun 2.4s linear infinite; + opacity: 0.58; } .flow-2, @@ -519,15 +567,17 @@ h1 { .room-badge { position: absolute; - z-index: 5; + z-index: 8; transform: translate(-50%, -50%); - min-width: 118px; - padding: 8px 12px; + min-width: 112px; + padding: 8px 11px 7px; border-radius: 8px; - background: linear-gradient(180deg, rgba(5, 22, 35, 0.72), rgba(2, 12, 20, 0.42)); + background: linear-gradient(180deg, rgba(5, 22, 35, 0.84), rgba(2, 12, 20, 0.58)); + border: 1px solid rgba(76, 177, 238, 0.1); color: #f2f8ff; text-shadow: 0 2px 6px rgba(0, 0, 0, 0.75); pointer-events: none; + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18); } .room-badge strong, @@ -539,12 +589,13 @@ h1 { } .room-badge strong { - font-size: 13px; + font-size: 12px; + letter-spacing: 0.015em; } .room-badge span { display: block; - font-size: 29px; + font-size: 28px; line-height: 1.05; font-weight: 800; } @@ -565,7 +616,7 @@ h1 { .vent-callout { position: absolute; - z-index: 6; + z-index: 8; display: grid; place-items: center; min-width: 55px; @@ -596,13 +647,14 @@ h1 { font-size: 12px; } -.callout-0 { left: 2%; top: 9%; } -.callout-1 { left: 48%; top: 5%; } -.callout-2 { right: 1%; top: 9%; } -.callout-3 { left: -1%; top: 40%; } +.callout-0 { left: 2%; top: 10%; } +.callout-1 { left: 50%; top: 6%; } +.callout-2 { right: 1%; top: 10%; } +.callout-3 { left: -1%; top: 41%; } .callout-4 { left: -2%; top: 68%; } -.callout-5 { left: 49%; bottom: 0; } -.callout-6 { right: 0; top: 67%; } +.callout-5 { left: 50%; bottom: 0; } +.callout-6 { right: 0; top: 41%; } +.callout-7 { right: 0; top: 68%; } .callout-0::after, .callout-3::after, @@ -616,7 +668,8 @@ h1 { } .callout-2::after, -.callout-6::after { right: 100%; } +.callout-6::after, +.callout-7::after { right: 100%; } .vent-panel { padding-bottom: 11px; @@ -629,7 +682,7 @@ h1 { .vent-head, .vent-row { display: grid; - grid-template-columns: 1.35fr 54px 68px 74px 20px; + grid-template-columns: minmax(112px, 1.25fr) 48px 72px 74px 20px; align-items: center; column-gap: 8px; } @@ -649,6 +702,12 @@ h1 { font-size: 13px; } +.vent-row span, +.vent-row b, +.vent-row small { + white-space: nowrap; +} + .vent-row b, .vent-row small { font-weight: 600; @@ -674,8 +733,8 @@ h1 { .distribution { display: grid; - grid-template-columns: 142px 1fr; - gap: 10px; + grid-template-columns: 138px 1fr; + gap: 12px; align-items: center; padding: 14px 16px 18px; } @@ -724,7 +783,7 @@ h1 { .distribution li { display: grid; - grid-template-columns: 10px 1fr auto; + grid-template-columns: 10px minmax(80px, 1fr) minmax(74px, auto); gap: 7px; align-items: center; } @@ -991,19 +1050,36 @@ h1 { @media (max-width: 760px) { .simulation-shell { padding: 20px 14px 24px; - overflow: visible; + overflow-x: clip; } .brand-lockup { + grid-template-columns: 1fr; + gap: 8px; align-items: flex-start; } .brand { - font-size: 45px; + gap: 8px; + } + + .brand img { + width: 50px; + height: 50px; + } + + .brand span { + font-size: 28px; } .divider { - margin: 0 14px; + display: none; + } + + .mode-card, + .panel, + .demo-strip { + max-width: calc(100vw - 28px); } h1 { diff --git a/src/App.tsx b/src/App.tsx index 2f9ce3a..cae7a22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,8 @@ import { Wind, Zap, } from 'lucide-react' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' +import vynteMark from './assets/vynte-mark.png' import './App.css' type Room = { @@ -74,6 +75,28 @@ const flowPaths = [ 'M50 53 C50 43 49 37 49 29 C48 19 50 14 52 11', ] +const flowCurves = [ + [[50, 53], [44, 51], [38, 45], [31, 39], [24, 31], [18, 19]], + [[50, 53], [54, 46], [55, 37], [54, 27], [53, 18], [54, 11]], + [[50, 53], [58, 51], [65, 47], [72, 39], [80, 29], [84, 18]], + [[50, 53], [58, 54], [66, 52], [71, 45], [79, 40], [86, 42]], + [[50, 53], [43, 55], [35, 54], [29, 50], [22, 45], [14, 43]], + [[50, 53], [44, 59], [38, 63], [31, 68], [23, 75], [17, 80]], + [[50, 53], [51, 61], [52, 70], [52, 79], [52, 87]], + [[50, 53], [58, 58], [66, 63], [72, 69], [79, 74], [86, 75]], +] as const + +const ventCallouts = [ + { value: 65, label: 'Open' }, + { value: 50, label: 'Open' }, + { value: 55, label: 'Open' }, + { value: 75, label: 'Open' }, + { value: 60, label: 'Open' }, + { value: 50, label: 'Open' }, + { value: 70, label: 'Open' }, + { value: 45, label: 'Open' }, +] + function PanelTitle({ title, icon }: { title: string; icon?: React.ReactNode }) { return