fix(website): accurate Iran border on homepage map + slow on/off marker blink
CI/CD / CI · API (dotnet build + test) (pull_request) Successful in 46s
CI/CD / CI · Admin API (dotnet build) (pull_request) Successful in 31s
CI/CD / CI · Dashboard (tsc) (pull_request) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (pull_request) Successful in 35s
CI/CD / CI · Website (tsc) (pull_request) Successful in 45s
CI/CD / CI · Koja (tsc) (pull_request) Successful in 51s
CI/CD / Deploy · all services (pull_request) Has been skipped

Replaced the rough 40-point hand-drawn polygon with the real national border (74 vertices, Natural Earth via world.geo.json) and fitted the projection bounding box to Iran's true extent, so the silhouette is recognisable and café markers stay aligned. Reworked the marker animation from a radar-style expanding ring into a slow 3.6s ease-in-out lamp fade (opacity 1->0.2->1) with a halo that glows on and off in sync. Verified via the SVG timeline: opacity 1.0 at 0s, 0.2 at 1.8s, 1.0 at 3.6s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-01 21:38:25 +03:30
parent f813cc4854
commit b5a6b1b68d
@@ -22,11 +22,14 @@ type MarkersApiResponse = {
// ── Coordinate transform ──────────────────────────────────────────────────────
// Iran bounding box (degrees)
const MIN_LNG = 44;
const MAX_LNG = 64;
const MIN_LAT = 24;
const MAX_LAT = 41;
// Iran bounding box (degrees) — fitted to the real border extent
// (lng 44.1163.32, lat 25.0839.71) with a small margin so the
// silhouette fills the viewBox. Markers reproject with the same box,
// so they stay aligned with the outline.
const MIN_LNG = 43.6;
const MAX_LNG = 63.8;
const MIN_LAT = 24.6;
const MAX_LAT = 40.2;
const SVG_W = 600;
const SVG_H = 500;
@@ -41,34 +44,24 @@ function toPt([lng, lat]: [number, number]) {
}
// ── Iran silhouette ────────────────────────────────────────────────────────────
// Simplified 40-point polygon; approximate but recognisable.
// Coordinates are [longitude, latitude] going clockwise from NW.
// Real national border, simplified to 74 vertices (source: Natural Earth via
// world.geo.json). Coordinates are [longitude, latitude]; the ring starts on
// the Caspian (NE) and runs clockwise. Projected through toX/toY below, the
// same transform used for the café markers, so dots land in the right place.
const IRAN_OUTLINE: [number, number][] = [
// NW corner / Turkey-Armenia-Azerbaijan
[44.8, 39.6], [45.5, 39.2], [46.2, 38.9],
[46.8, 39.1], [47.6, 38.9],
// Caspian coast (the concave notch heading south then north again)
[48.3, 38.4], [49.0, 37.5], [49.9, 37.2],
[51.0, 36.9], [52.2, 36.8], [53.0, 36.7],
[54.0, 37.1], [54.7, 37.5],
// NE / Turkmenistan
[55.6, 37.4], [56.9, 37.1], [57.7, 36.8],
[58.7, 37.5], [59.4, 36.8], [60.1, 36.7],
// East / Afghanistan
[61.2, 36.5], [61.3, 35.7], [62.0, 35.5],
[62.5, 34.0], [63.0, 33.0], [63.2, 31.5],
// SE / Pakistan Oman Sea
[61.8, 29.8], [60.9, 29.5], [60.0, 27.5],
[59.0, 25.9], [58.5, 25.4],
// South coast (Persian Gulf, west-bound)
[57.5, 25.3], [56.4, 25.9], [55.6, 26.0],
[54.5, 27.0], [53.4, 27.3], [52.4, 28.0],
[51.1, 28.4], [50.4, 29.1], [49.0, 29.6],
[48.5, 30.2], [48.2, 30.8],
// West / Iraq border
[47.7, 31.0], [47.2, 32.0], [46.8, 33.2],
[46.2, 34.4], [45.5, 36.0], [45.0, 37.0],
[44.8, 38.1], [44.5, 38.9], [44.8, 39.6],
[53.92, 37.20], [54.80, 37.39], [55.51, 37.96], [56.18, 37.94], [56.62, 38.12], [57.33, 38.03],
[58.44, 37.52], [59.23, 37.41], [60.38, 36.53], [61.12, 36.49], [61.21, 35.65], [60.80, 34.40],
[60.53, 33.68], [60.96, 33.53], [60.54, 32.98], [60.86, 32.18], [60.94, 31.55], [61.70, 31.38],
[61.78, 30.74], [60.87, 29.83], [61.37, 29.30], [61.77, 28.70], [62.73, 28.26], [62.76, 27.38],
[63.23, 27.22], [63.32, 26.76], [61.87, 26.24], [61.50, 25.08], [59.62, 25.38], [58.53, 25.61],
[57.40, 25.74], [56.97, 26.97], [56.49, 27.14], [55.72, 26.96], [54.72, 26.48], [53.49, 26.81],
[52.48, 27.58], [51.52, 27.87], [50.85, 28.81], [50.12, 30.15], [49.58, 29.99], [48.94, 30.32],
[48.57, 29.93], [48.01, 30.45], [48.00, 30.99], [47.69, 30.98], [47.85, 31.71], [47.33, 32.47],
[46.11, 33.02], [45.42, 33.97], [45.65, 34.75], [46.15, 35.09], [46.08, 35.68], [45.42, 35.98],
[44.77, 37.17], [44.23, 37.97], [44.42, 38.28], [44.11, 39.43], [44.79, 39.71], [44.95, 39.34],
[45.46, 38.87], [46.14, 38.74], [46.51, 38.77], [47.69, 39.51], [48.06, 39.58], [48.36, 39.29],
[48.01, 38.79], [48.63, 38.27], [48.88, 38.32], [49.20, 37.58], [50.15, 37.37], [50.84, 36.87],
[52.26, 36.70], [53.83, 36.97],
];
const IRAN_PATH =
@@ -153,42 +146,50 @@ async function IranMapSvg() {
</g>
))}
{/* Café blinking dots */}
{/* Café markers — each glows slowly on and off like a small lamp.
Halo and core brighten/dim together (ease-in-out), staggered so the
map twinkles organically rather than pulsing in unison. */}
{markers.map((m, idx) => {
const cx = toX(m.longitude);
const cy = toY(m.latitude);
// Stagger animation delay so dots don't all pulse in sync
const delay = `${(idx * 0.4) % 2}s`;
const delay = `${((idx * 0.7) % 3.6).toFixed(2)}s`;
const dur = "3.6s";
// ease-in-out for a smooth lamp-like fade
const ease = "0.4 0 0.6 1; 0.4 0 0.6 1";
return (
<g key={m.id} filter="url(#glow)">
{/* Outer pulse ring */}
<circle cx={cx} cy={cy} r={10} fill="#0F6E56" opacity={0.2}>
<animate
attributeName="r"
values="8;16;8"
dur="2.4s"
begin={delay}
repeatCount="indefinite"
/>
{/* Soft halo */}
<circle cx={cx} cy={cy} r={9} fill="#0F6E56">
<animate
attributeName="opacity"
values="0.25;0;0.25"
dur="2.4s"
values="0.45;0.04;0.45"
keyTimes="0;0.5;1"
calcMode="spline"
keySplines={ease}
dur={dur}
begin={delay}
repeatCount="indefinite"
/>
</circle>
{/* Core dot */}
<circle
cx={cx}
cy={cy}
r={5}
fill="#0F6E56"
>
{/* Core dot — turns on (bright, slightly larger) and off (dim) */}
<circle cx={cx} cy={cy} r={4.5} fill="#0F6E56">
<animate
attributeName="opacity"
values="1;0.5;1"
dur="2.4s"
values="1;0.2;1"
keyTimes="0;0.5;1"
calcMode="spline"
keySplines={ease}
dur={dur}
begin={delay}
repeatCount="indefinite"
/>
<animate
attributeName="r"
values="4.5;5.6;4.5"
keyTimes="0;0.5;1"
calcMode="spline"
keySplines={ease}
dur={dur}
begin={delay}
repeatCount="indefinite"
/>