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>
<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}
<div
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]'
)}
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">
{tooltipData.city}
({tooltipData.code})
</span>
{#key tooltipData.city}
<span class="text-primary text-caption w-fit" bind:this={city}>
{tooltipData.city}
({tooltipData.code})
</span>
{/key}
{#if tooltipData.available}
<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]"

View File

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

View File

@@ -24,31 +24,39 @@ export const createMap = ({
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 { lat, lng, size, ...markerData } = marker;
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(';');
if (!points[key]) {
const [localLng, localLat] = proj4('GOOGLE', 'WGS84', [
(localx * X_RANGE) / mapWidth + X_MIN,
Y_MAX - (localy * Y_RANGE) / mapHeight
]);
points[key] = {
x: localx,
y: localy
// Map projected coordinates to pixel space (same as getMapPoints)
const localx = ((googleX - X_MIN) / X_RANGE) * width;
const localy = ((Y_MAX - googleY) / Y_RANGE) * height;
// Round exactly as getMapPoints does
const roundedX = Math.round(localx);
const roundedY = Math.round(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 {
x: localx,
y: localy,
x: roundedX,
y: roundedY,
color: markerColor,
size: size ?? defaultRadius,
...markerData