update the map

This commit is contained in:
Jesse Winton
2025-05-21 15:11:04 -04:00
parent e3dfff7130
commit ad5d030d10
5 changed files with 96 additions and 78 deletions

View File

@@ -0,0 +1,40 @@
import { hover } from 'motion';
import { writable } from 'svelte/store';
export interface Position {
x: number;
y: number;
}
export const useMousePosition = () => {
let position = $state<Position>({
x: 0,
y: 0
});
const handleMouseMove = (event: MouseEvent) => {
position = {
x: event.offsetX,
y: event.offsetY
};
};
const action = (node: HTMLElement | SVGSVGElement) => {
hover(node, () => {
document.addEventListener('mousemove', handleMouseMove);
});
return {
destroy() {
document.removeEventListener('mousemove', handleMouseMove);
}
};
};
return {
action,
position: () => {
return position;
}
};
};

View File

@@ -1,34 +0,0 @@
import { hover } from 'motion';
import { writable } from 'svelte/store';
export const useMousePosition = () => {
let position = writable<{ x: number; y: number }>({
x: 0,
y: 0
});
const action = (node: HTMLElement) => {
const handleMouseMove = (event: MouseEvent) => {
// Get the bounding rectangle of the element
const rect = node.getBoundingClientRect();
// Calculate position relative to the element
position.set({
x: event.clientX - rect.left,
y: event.clientY - rect.top
});
};
hover(node, () => {
node.addEventListener('mousemove', handleMouseMove);
});
return {
destroy() {
node.removeEventListener('mousemove', handleMouseMove);
}
};
};
return { action, position };
};

View File

@@ -68,18 +68,20 @@
}); });
</script> </script>
<div class="pointer-events-none absolute z-10 hidden md:block"> <div class="pointer-events-none absolute z-100 hidden md:block">
{#if tooltipData.city} {#if tooltipData.city}
<div <div
class={classNames( class={classNames(
'border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]' 'border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]'
)} )}
style:transform={`translateX(${x + 125}px) translateY(${y + 200}px)`} style:transform={`translateX(${x + 250}px) translateY(${y - 620}px)`}
> >
<span class="text-primary text-caption w-fit"> {#key tooltipData.city}
<span class="text-primary text-caption w-fit" bind:this={city}>
{tooltipData.city} {tooltipData.city}
({tooltipData.code}) ({tooltipData.code})
</span> </span>
{/key}
{#if tooltipData.available} {#if tooltipData.available}
<div <div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]" class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"

View File

@@ -2,7 +2,7 @@
import { slugify } from '$lib/utils/slugify'; import { slugify } from '$lib/utils/slugify';
import { classNames } from '$lib/utils/classnames'; import { classNames } from '$lib/utils/classnames';
import MapNav from './map-nav.svelte'; import MapNav from './map-nav.svelte';
import { useMousePosition } from '$lib/actions/mouse-position'; import { useMousePosition } from '$lib/actions/mouse-position.svelte';
import { useAnimateInView } from '$lib/actions/animate-in-view'; import { useAnimateInView } from '$lib/actions/animate-in-view';
import { pins, type PinSegment } from './data/pins'; import { pins, type PinSegment } from './data/pins';
import MapTooltip, { import MapTooltip, {
@@ -75,17 +75,13 @@
height, height,
markers: getMarkers(), markers: getMarkers(),
skew: 1, skew: 1,
baseColor: '#dadadd', baseColor: 'rgba(255,255,255,.1)',
markerColor: 'var(--color-accent)' markerColor: 'var(--color-accent)'
}); });
}); });
type Props = { theme: 'light' | 'dark' };
const { theme = 'dark' }: Props = $props();
</script> </script>
<div class="-mt-8 w-full overflow-x-scroll [scrollbar-width:none] md:overflow-x-hidden"> <div class="relative -mt-8 w-full overflow-x-scroll [scrollbar-width:none]">
<div <div
class="sticky left-0 mx-auto block max-w-[calc(100vw_-_calc(var(--spacing)_*-2))] md:hidden" class="sticky left-0 mx-auto block max-w-[calc(100vw_-_calc(var(--spacing)_*-2))] md:hidden"
> >
@@ -99,19 +95,15 @@
</select> </select>
</div> </div>
<div class="relative mx-auto h-full w-[250vw] [scrollbar-width:none] md:w-full" use:inView>
<div <div
class="relative mx-auto h-full w-[250vw] [scrollbar-width:none] md:w-full" class="relative mx-auto h-fit w-full max-w-5xl origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.4,_1.4,_1)] transition-all [scrollbar-width:none]"
use:inView
use:mousePosition
> >
<div <svg viewBox={`0 0 ${height * 2} ${height}`} use:mousePosition>
class="relative w-full origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.4,_1.4,_1)] transition-all [scrollbar-width:none]"
>
<svg viewBox={`0 0 ${height * 2} ${height}`}>
{#each map.points as point} {#each map.points as point}
<circle cx={point.x} cy={point.y} r={point.size} fill={point.color} /> <circle cx={point.x} cy={point.y} r={point.size} fill={point.color} />
{/each} {/each}
<!-- {#each map.markers as marker} {#each map.markers as marker}
<g <g
role="tooltip" role="tooltip"
class="animate-fade-in outline-none" class="animate-fade-in outline-none"
@@ -123,15 +115,25 @@
onmouseout={() => handleResetActiveTooltip(250)} onmouseout={() => handleResetActiveTooltip(250)}
data-region={slugify(marker.city)} data-region={slugify(marker.city)}
> >
<circle cx={marker.x} cy={marker.y} r={marker.size} fill={marker.color} /> <circle
<circle cx={marker.x} cy={marker.y} fill="white" /> cx={marker.x}
cy={marker.y}
r={marker.size * 1.5}
fill={marker.color}
/>
<circle cx={marker.x} cy={marker.y} r={marker.size * 0.5} fill="white" />
<circle
cx={marker.x}
cy={marker.y}
r={marker.size * 4}
fill="transparent"
/>
</g> </g>
{/each} --> {/each}
</svg> </svg>
</div> </div>
</div> </div>
<MapTooltip {...position()} />
</div> </div>
<MapTooltip {...$position} /> <MapNav onValueChange={(value) => (activeSegment = value)} />
<MapNav {theme} onValueChange={(value) => (activeSegment = value)} />

View File

@@ -24,31 +24,39 @@ export const createMap = ({
const defaultRadius = 0.35; const defaultRadius = 0.35;
// Use the same exact constants as getMapPoints
const TARGET_POINTS = 6000;
const aspect = width / height;
const NUM_ROWS = Math.round(Math.sqrt(TARGET_POINTS / aspect));
const NUM_COLS = Math.round(NUM_ROWS * aspect);
const markerPoints = markers.map((marker) => { const markerPoints = markers.map((marker) => {
const { lat, lng, size, ...markerData } = marker; const { lat, lng, size, ...markerData } = marker;
const [googleX, googleY] = proj4('GOOGLE', [lng, lat]); const [googleX, googleY] = proj4('GOOGLE', [lng, lat]);
const rawY = (mapHeight * (Y_MAX - googleY)) / Y_RANGE;
const rawX = (mapWidth * (googleX - X_MIN)) / X_RANGE;
const y = Math.round(rawY / ystep);
const x = Math.round(rawX);
const localy = Math.round(y) * ystep;
const localx = x;
const key = [localx, localy].join(';'); // Map projected coordinates to pixel space (same as getMapPoints)
if (!points[key]) { const localx = ((googleX - X_MIN) / X_RANGE) * width;
const [localLng, localLat] = proj4('GOOGLE', 'WGS84', [ const localy = ((Y_MAX - googleY) / Y_RANGE) * height;
(localx * X_RANGE) / mapWidth + X_MIN,
Y_MAX - (localy * Y_RANGE) / mapHeight // Round exactly as getMapPoints does
]); const roundedX = Math.round(localx);
points[key] = { const roundedY = Math.round(localy);
x: localx,
y: localy // Create a key for this point
const exactKey = `${roundedX};${roundedY}`;
// Create the point if it doesn't exist
if (!points[exactKey]) {
points[exactKey] = {
x: roundedX,
y: roundedY
}; };
} }
// Use these exact coordinates directly
return { return {
x: localx, x: roundedX,
y: localy, y: roundedY,
color: markerColor, color: markerColor,
size: size ?? defaultRadius, size: size ?? defaultRadius,
...markerData ...markerData