Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/components/map/DistrictBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ function seededUnit(seed: number) {
return value - Math.floor(value);
}

const HERO_LABEL_OFFSETS: Record<string, [number, number]> = {
"21010": [0.05, -0.12],
"21020": [-0.16, -0.08],
"21030": [0.08, 0.12],
"21040": [0.18, -0.05],
"21050": [0.12, 0.08],
"21060": [0.02, 0.12],
"21070": [0.16, -0.06],
"21080": [-0.12, 0.08],
"21090": [0.08, 0.04],
"21100": [-0.04, -0.12],
"21120": [1.45, 1.6],
"21130": [0.12, -0.04],
"21140": [0.2, -0.06],
"21150": [-0.12, 0.02],
};

function makeGeo(polygons: Polygon[], bbox: BBox, heroStyle: boolean): THREE.ExtrudeGeometry {
const shapes = polygons.map((poly) => {
const shape = new THREE.Shape(
Expand Down Expand Up @@ -74,6 +91,7 @@ export function DistrictBlock({
index,
phase,
heroStyle = false,
showHeroLabel = false,
onAssemblyComplete,
onSelect,
selected,
Expand All @@ -86,6 +104,7 @@ export function DistrictBlock({
index: number;
phase: string;
heroStyle?: boolean;
showHeroLabel?: boolean;
onAssemblyComplete?: () => void;
onSelect: (code: string, name: string) => void;
selected: boolean;
Expand Down Expand Up @@ -134,6 +153,7 @@ export function DistrictBlock({
const nextColor = new THREE.Color(selected ? "#3b82f6" : color);
return hovered ? nextColor.lerp(new THREE.Color("#8be6ff"), 0.24) : nextColor;
}, [color, hovered, selected]);
const [heroLabelDx, heroLabelDy] = HERO_LABEL_OFFSETS[district.code] ?? [0, 0];

useCursor(heroStyle && hovered);

Expand Down Expand Up @@ -299,6 +319,45 @@ export function DistrictBlock({
</div>
</Html>
)}

{heroStyle && (
<Html
position={[
district.centroid[0] + heroLabelDx,
district.centroid[1] + heroLabelDy,
0.98,
]}
center
distanceFactor={8.2}
zIndexRange={[8, 0]}
occlude={false}
style={{
pointerEvents: "none",
opacity: showHeroLabel ? 1 : 0,
transition: `opacity 280ms ease ${index * 28}ms`,
}}
>
<span
aria-hidden="true"
style={{
display: "block",
padding: "1px 3px",
color: "#f8fdff",
fontSize: "10px",
fontWeight: 850,
lineHeight: 1.1,
letterSpacing: "-0.05em",
whiteSpace: "nowrap",
textShadow: "0 1px 2px rgba(5, 39, 77, 0.95), 0 0 5px rgba(9, 64, 113, 0.7)",
transform: showHeroLabel ? "translateY(0)" : "translateY(4px)",
transition: `transform 360ms ease ${index * 28}ms`,
userSelect: "none",
}}
>
{district.name}
</span>
</Html>
)}
</group>
);
}
63 changes: 57 additions & 6 deletions src/components/map/IntroScene2.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -153,29 +153,77 @@
}

.routeShadow,
.routeRail,
.routePath {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}

.routeShadow {
stroke: rgb(14 63 115 / 20%);
stroke: rgb(14 63 115 / 24%);
stroke-width: 11;
transform: translateY(3px);
}

.routeRail,
.routePath {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}

.routeRail {
stroke: #2e7df2;
stroke-width: 9;
transform: translateY(4px);
filter: drop-shadow(0 2px 5px rgb(25 99 168 / 24%));
}

.routePath {
stroke: white;
stroke-width: 6;
stroke-dasharray: 390;
stroke-dashoffset: 390;
filter: drop-shadow(0 2px 5px rgb(25 99 168 / 25%));
stroke-width: 5.5;
}

.routeVisible .routeRail,
.routeVisible .routePath {
animation: drawRoute 850ms ease-out 120ms forwards;
}

.routeStop {
fill: white;
stroke: #56a5ee;
stroke-width: 2.5;
opacity: 0;
transform-box: fill-box;
transform-origin: center;
}

.routePin {
opacity: 0;
transform-box: fill-box;
transform-origin: center;
}

.routePin path {
fill: #2e7df2;
stroke: white;
stroke-width: 3.5;
paint-order: stroke;
filter: drop-shadow(0 3px 4px rgb(14 63 115 / 30%));
}

.routePin circle {
fill: white;
}

.routeVisible .routeStop,
.routeVisible .routePin {
animation: popIn 420ms cubic-bezier(0.34, 1.56, 0.64, 1) 760ms forwards;
}

.routeVisible .routePinEnd {
animation-delay: 920ms;
}

.routeDot {
fill: #2e7df2;
stroke: white;
Expand Down Expand Up @@ -462,6 +510,9 @@
}

.routePath,
.routeRail,
.routeStop,
.routePin,
.routeDot,
.busBadge {
animation-duration: 1ms !important;
Expand Down
52 changes: 42 additions & 10 deletions src/components/map/IntroScene2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export default function IntroScene2({ onEnter }: { onEnter: () => void }) {
phase={scenePhase}
colorPalette={LOGIN_DISTRICT_COLORS}
heroStyle
showHeroLabels={panelVisible}
onAssemblyComplete={handleAssemblyComplete}
onSelect={() => {}}
selected={null}
Expand All @@ -279,16 +280,47 @@ export default function IntroScene2({ onEnter }: { onEnter: () => void }) {
viewBox="0 0 360 170"
aria-hidden="true"
>
<path className={styles.routeShadow} d="M38 126 C90 159 95 80 158 98 S235 129 306 42" />
<path className={styles.routePath} d="M38 126 C90 159 95 80 158 98 S235 129 306 42" />
<circle cx="38" cy="126" r="8" className={styles.routeDot} />
<circle cx="306" cy="42" r="8" className={styles.routeDot} />
<g className={styles.busBadge} transform="translate(155 83)">
<circle r="23" />
<rect x="-8" y="-10" width="16" height="20" rx="4" />
<path d="M-5 -5h10M-5 2h10" />
<circle cx="-5" cy="13" r="2" />
<circle cx="5" cy="13" r="2" />
<path
className={styles.routeShadow}
d="M38 126 C82 151 102 96 142 101 C180 106 201 126 232 104 C260 84 280 62 306 42"
/>
<path
pathLength="1"
className={styles.routeRail}
d="M38 126 C82 151 102 96 142 101 C180 106 201 126 232 104 C260 84 280 62 306 42"
/>
<path
pathLength="1"
className={styles.routePath}
d="M38 126 C82 151 102 96 142 101 C180 106 201 126 232 104 C260 84 280 62 306 42"
/>

<g transform="translate(38 126)">
<g className={styles.routePin}>
<path d="M0 9c-7-8-10-12-10-18a10 10 0 0 1 20 0C10-3 7 1 0 9Z" />
<circle cy="-9" r="3.7" />
</g>
</g>
<g transform="translate(306 42)">
<g className={`${styles.routePin} ${styles.routePinEnd}`}>
<path d="M0 9c-7-8-10-12-10-18a10 10 0 0 1 20 0C10-3 7 1 0 9Z" />
<circle cy="-9" r="3.7" />
</g>
</g>

<circle cx="72" cy="132" r="4.5" className={styles.routeStop} style={{ animationDelay: "700ms" }} />
<circle cx="111" cy="104" r="4.5" className={styles.routeStop} style={{ animationDelay: "760ms" }} />
<circle cx="151" cy="103" r="4.5" className={styles.routeStop} style={{ animationDelay: "820ms" }} />
<circle cx="194" cy="113" r="4.5" className={styles.routeStop} style={{ animationDelay: "880ms" }} />
<circle cx="237" cy="100" r="4.5" className={styles.routeStop} style={{ animationDelay: "940ms" }} />
<circle cx="273" cy="70" r="4.5" className={styles.routeStop} style={{ animationDelay: "1000ms" }} />

<g className={styles.busBadge} transform="translate(108 104)">
<circle r="19" />
<rect x="-7" y="-9" width="14" height="18" rx="3.5" />
<path d="M-4.5 -4.5h9M-4.5 1.5h9" />
<circle cx="-4.5" cy="11" r="1.8" />
<circle cx="4.5" cy="11" r="1.8" />
</g>
</svg>
</section>
Expand Down
3 changes: 3 additions & 0 deletions src/components/map/Scene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function Scene({
controlsRef,
colorPalette,
heroStyle = false,
showHeroLabels = false,
onAssemblyComplete,
onTap,
tappedName,
Expand All @@ -29,6 +30,7 @@ export function Scene({
controlsRef: React.RefObject<OrbitControlsImpl | null>;
colorPalette?: readonly string[];
heroStyle?: boolean;
showHeroLabels?: boolean;
onAssemblyComplete?: () => void;
onTap?: (name: string) => void;
tappedName?: string | null;
Expand Down Expand Up @@ -85,6 +87,7 @@ export function Scene({
index={i}
phase={phase}
heroStyle={heroStyle}
showHeroLabel={showHeroLabels}
onAssemblyComplete={i === districts.length - 1 ? onAssemblyComplete : undefined}
onSelect={onSelect}
selected={selected === d.code}
Expand Down
Loading