Skip to content

Commit efde2b0

Browse files
Samuel Volchenboumclaude
authored andcommitted
feat: natural voice, small-country circles, hints, version tag (v1.2.0)
Voice: prioritise Samantha/Karen/Daniel over robotic default; load via voiceschanged event so correct voice is picked on all browsers. Circles: 33 small countries (Isle of Man, Andorra, San Marino, etc.) now show a clickable dot at their centroid so they're easy to target. Dot size scales automatically with zoom level. Hints: appear after 2 wrong guesses. Hint 1 — directional text ("It's in the northern, western part of Europe") spoken aloud + shown in quiz panel. Hint 2 — pulsing amber ring on the map pointing at the country. Both hints reset when the question advances. Version: v1.2.0 shown in bottom-right corner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b8824e6 commit efde2b0

File tree

6 files changed

+239
-73
lines changed

6 files changed

+239
-73
lines changed

scripts/process-data.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ for (const [name, data] of Object.entries(landsRaw.data)) {
174174
landsInfo[name] = {
175175
fillColorNumber: data.fillColorNumber ?? 0,
176176
countryCode: data.countryCode ?? '',
177+
showCircle: data.showCircle === true,
177178
...(label ?? {})
178179
};
179180
}

src/lib/components/WorldMap.svelte

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
landsInfo: LandsInfo;
88
viewBox: string;
99
regionIds: string[];
10-
/** Countries already correctly found — show their names permanently */
1110
foundIds?: string[];
12-
/** Country the user just clicked wrongly — flash its name briefly */
1311
wrongFlashId?: string;
14-
/** Country just answered correctly — highlight green briefly */
1512
correctFlashId?: string;
13+
/** Pulsing hint ring shown at this country's centroid */
14+
hintPulseId?: string;
1615
quizActive?: boolean;
1716
onLandClick?: (id: string) => void;
1817
}
@@ -25,6 +24,7 @@
2524
foundIds = [],
2625
wrongFlashId = '',
2726
correctFlashId = '',
27+
hintPulseId = '',
2828
quizActive = false,
2929
onLandClick = () => {}
3030
}: Props = $props();
@@ -35,6 +35,13 @@
3535
'#87be70', '#c8a070', '#9880c0', '#70be88'
3636
];
3737
38+
// Parse viewBox to get width for scaling circles
39+
let vbWidth = $derived(
40+
(() => { const p = viewBox.split(' ').map(Number); return p[2] ?? 10968; })()
41+
);
42+
// Circle radius ≈ 8–10px on screen at any zoom level
43+
let circleR = $derived(Math.max(12, vbWidth / 115));
44+
3845
function getFill(id: string): string {
3946
if (id === correctFlashId) return '#51cf66';
4047
if (id === wrongFlashId) return '#ff6b6b';
@@ -49,13 +56,17 @@
4956
return 'rgba(255,255,255,0.25)';
5057
}
5158
52-
// Labels to render: found countries (permanent) + wrong flash (temporary)
5359
type LabelEntry = { id: string; color: string };
5460
let labels = $derived<LabelEntry[]>([
5561
...foundIds.map(id => ({ id, color: '#ffffff' })),
56-
...(wrongFlashId ? [{ id: wrongFlashId, color: '#ffcc66' }] : []),
62+
...(wrongFlashId ? [{ id: wrongFlashId, color: '#ffcc66' }] : []),
5763
...(correctFlashId ? [{ id: correctFlashId, color: '#ffffff' }] : [])
5864
]);
65+
66+
// Countries that need a dot because they're too small to click reliably
67+
let circleIds = $derived(
68+
regionIds.filter(id => landsInfo[id]?.showCircle && landsInfo[id]?.cx !== undefined)
69+
);
5970
</script>
6071

6172
<svg
@@ -71,21 +82,54 @@
7182
{@const stroke = getStroke(id)}
7283
{#each [...(geometry[id].land ?? []), ...(geometry[id].island ?? [])] as d, i (i)}
7384
<path
74-
{d}
75-
{fill}
76-
{stroke}
85+
{d} {fill} {stroke}
7786
stroke-width="1.5"
7887
vector-effect="non-scaling-stroke"
7988
class:clickable={quizActive}
8089
onclick={() => quizActive && onLandClick(id)}
8190
role={quizActive ? 'button' : undefined}
82-
aria-label={quizActive ? id : undefined}
8391
/>
8492
{/each}
8593
{/if}
8694
{/each}
8795

88-
<!-- Country name labels -->
96+
<!-- Small-country click circles -->
97+
{#each circleIds as id (id + '-circle')}
98+
{@const info = landsInfo[id]}
99+
{@const fill = getFill(id)}
100+
<circle
101+
cx={info.cx}
102+
cy={info.cy}
103+
r={circleR}
104+
{fill}
105+
stroke="rgba(255,255,255,0.6)"
106+
stroke-width="1.5"
107+
vector-effect="non-scaling-stroke"
108+
class:clickable={quizActive}
109+
onclick={() => quizActive && onLandClick(id)}
110+
role={quizActive ? 'button' : undefined}
111+
/>
112+
{/each}
113+
114+
<!-- Hint pulse ring -->
115+
{#if hintPulseId}
116+
{@const info = landsInfo[hintPulseId]}
117+
{#if info?.cx !== undefined}
118+
<circle
119+
cx={info.cx}
120+
cy={info.cy}
121+
r={circleR * 5}
122+
fill="none"
123+
stroke="#ffd43b"
124+
stroke-width="3"
125+
vector-effect="non-scaling-stroke"
126+
class="pulse-ring"
127+
pointer-events="none"
128+
/>
129+
{/if}
130+
{/if}
131+
132+
<!-- Country name labels (found + flash) -->
89133
{#each labels as { id, color } (id + color)}
90134
{@const info = landsInfo[id]}
91135
{#if info?.cx !== undefined && info.cy !== undefined}
@@ -117,7 +161,7 @@
117161
background: #0d1b2e;
118162
}
119163
120-
path {
164+
path, circle {
121165
transition: fill 0.12s ease;
122166
}
123167
@@ -128,4 +172,13 @@
128172
.clickable:hover {
129173
opacity: 0.75;
130174
}
175+
176+
.pulse-ring {
177+
animation: pulse 1.4s ease-in-out infinite;
178+
}
179+
180+
@keyframes pulse {
181+
0%, 100% { opacity: 1; }
182+
50% { opacity: 0.15; }
183+
}
131184
</style>

src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Geometry = Record<string, ShapePaths>;
99
export interface LandInfo {
1010
fillColorNumber: number;
1111
countryCode: string;
12+
showCircle?: boolean;
1213
cx?: number;
1314
cy?: number;
1415
lw?: number;

src/lib/version.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const VERSION = '1.2.0';

0 commit comments

Comments
 (0)