Skip to content

Commit a8e7e1e

Browse files
committed
🗺️ Add scrollytelling CV map component using maplibre-gl & scrollama
1 parent 6c3b92a commit a8e7e1e

File tree

4 files changed

+370
-46
lines changed

4 files changed

+370
-46
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"preview": "vitepress preview pages"
66
},
77
"dependencies": {
8+
"maplibre-gl": "^5.12.0",
9+
"scrollama": "^3.2.0",
810
"vitepress": "^1.6.4"
911
}
1012
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<template>
2+
<div id="map-page">
3+
<div id="map"></div>
4+
<div id="steps">
5+
<div class="step"></div>
6+
<div class="step"></div>
7+
</div>
8+
</div>
9+
</template>
10+
11+
<script setup>
12+
import { onMounted, onUnmounted } from 'vue'
13+
import maplibregl from 'maplibre-gl'
14+
import scrollama from 'scrollama'
15+
16+
onMounted(() => {
17+
const locParliament = [19.04556, 47.50704]
18+
const locCastle = [19.03945, 47.49621]
19+
const zoomStart = 15
20+
const zoomEnd = 17
21+
22+
const map = new maplibregl.Map({
23+
container: 'map',
24+
style: 'https://tiles.openfreemap.org/styles/liberty',
25+
center: locParliament,
26+
zoom: zoomStart
27+
})
28+
29+
// Disable map interactions in order not to interfere with scrollytelling
30+
map.scrollZoom.disable()
31+
map.boxZoom.disable()
32+
map.dragRotate.disable()
33+
map.dragPan.disable()
34+
map.keyboard.disable()
35+
map.doubleClickZoom.disable()
36+
map.touchZoomRotate.disable()
37+
38+
map.once('idle', () => {
39+
map.setCenter(locParliament)
40+
map.setZoom(zoomStart)
41+
42+
const card1 = document.createElement('div')
43+
card1.className = 'marker-card'
44+
card1.innerHTML = '<img src="" alt="Parliament"><p>Hungarian Parliament</p>'
45+
46+
const card2 = document.createElement('div')
47+
card2.className = 'marker-card'
48+
card2.innerHTML = '<img src="" alt="Castle"><p>Buda Castle</p>'
49+
50+
const marker1 = new maplibregl.Marker(card1, { offset: [0, -75] })
51+
.setLngLat(locParliament)
52+
.addTo(map)
53+
const marker2 = new maplibregl.Marker(card2, { offset: [0, -75] })
54+
.setLngLat(locCastle)
55+
.addTo(map)
56+
57+
card2.style.opacity = 0
58+
59+
const scroller = scrollama()
60+
scroller
61+
.setup({ step: '.step', offset: 0.5, progress: true })
62+
.onStepProgress(resp => {
63+
if (resp.index === 0) {
64+
const t = Math.max(0, Math.min(1, resp.progress))
65+
const lng = locParliament[0] + (locCastle[0] - locParliament[0]) * t
66+
const lat = locParliament[1] + (locCastle[1] - locParliament[1]) * t
67+
const zoom = zoomStart + (zoomEnd - zoomStart) * t
68+
69+
map.easeTo({
70+
center: [lng, lat],
71+
zoom,
72+
duration: 100,
73+
easing: n => n
74+
})
75+
76+
card1.style.opacity = 1 - t
77+
card2.style.opacity = t
78+
}
79+
})
80+
.onStepEnter(resp => {
81+
if (resp.index === 1) {
82+
map.easeTo({
83+
center: locCastle,
84+
zoom: zoomEnd,
85+
duration: 800,
86+
easing: n => n
87+
})
88+
card1.style.opacity = 0
89+
card2.style.opacity = 1
90+
}
91+
})
92+
93+
window.addEventListener('resize', scroller.resize)
94+
})
95+
96+
onUnmounted(() => {
97+
map.remove()
98+
})
99+
})
100+
</script>
101+
102+
<style>
103+
#map-page {
104+
margin: 0;
105+
padding: 0;
106+
}
107+
#map {
108+
position: fixed;
109+
inset: 0;
110+
width: 100%;
111+
height: 100%;
112+
z-index: 0;
113+
}
114+
#steps {
115+
position: relative;
116+
z-index: 1;
117+
}
118+
.step {
119+
height: 100vh;
120+
}
121+
.marker-card {
122+
background: white;
123+
border: 1px solid #666;
124+
border-radius: 4px;
125+
padding: 8px;
126+
font-family: sans-serif;
127+
font-size: 14px;
128+
max-width: 200px;
129+
text-align: center;
130+
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
131+
transition: opacity 0.4s ease;
132+
}
133+
.marker-card img {
134+
width: 100%;
135+
height: auto;
136+
display: block;
137+
margin-bottom: 5px;
138+
}
139+
</style>

pages/cv.md

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,5 @@
1-
---
2-
outline: deep
3-
---
4-
5-
# Runtime API Examples
6-
7-
This page demonstrates usage of some of the runtime APIs provided by VitePress.
8-
9-
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
10-
11-
```md
12-
<script setup>
13-
import { useData } from 'vitepress'
14-
15-
const { theme, page, frontmatter } = useData()
16-
</script>
17-
18-
## Results
19-
20-
### Theme Data
21-
<pre>{{ theme }}</pre>
22-
23-
### Page Data
24-
<pre>{{ page }}</pre>
25-
26-
### Page Frontmatter
27-
<pre>{{ frontmatter }}</pre>
28-
```
29-
301
<script setup>
31-
import { useData } from 'vitepress'
32-
33-
const { site, theme, page, frontmatter } = useData()
2+
import CvMap from '.vitepress/components/CvMap.vue'
343
</script>
354

36-
## Results
37-
38-
### Theme Data
39-
<pre>{{ theme }}</pre>
40-
41-
### Page Data
42-
<pre>{{ page }}</pre>
43-
44-
### Page Frontmatter
45-
<pre>{{ frontmatter }}</pre>
46-
47-
## More
48-
49-
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
5+
<CvMap />

0 commit comments

Comments
 (0)