Skip to content

Commit

Permalink
feat: wip map mega animation
Browse files Browse the repository at this point in the history
  • Loading branch information
eddieferrer committed Nov 16, 2023
1 parent ee5a977 commit 0f14e09
Show file tree
Hide file tree
Showing 3 changed files with 358 additions and 0 deletions.
246 changes: 246 additions & 0 deletions @kiva/kv-components/utils/mapAnimation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/* eslint-disable import/prefer-default-export */

export function generateMapMarkers(mapInstance, borrowerPoints) {
const geojson = {
type: 'FeatureCollection',
};

geojson.features = borrowerPoints.map((borrower) => ({
type: 'Feature',
properties: {
message: 'test',
image: borrower.image,
iconSize: [80, 80],
},
geometry: {
type: 'Point',
coordinates: borrower.location,
},
}));
// add markers to map
geojson.features.forEach((marker) => {
// create a DOM element for the marker
const el = document.createElement('div');
el.className = 'marker';
el.style.backgroundImage = `url(${marker.properties.image})`;
el.style.width = `${marker.properties.iconSize[0]}px`;
el.style.height = `${marker.properties.iconSize[1]}px`;

// el.addEventListener('click', () => {
// window.alert(marker.properties.message);
// });

// add marker to map
// eslint-disable-next-line no-undef
new maplibregl.Marker({ element: el })
.setLngLat(marker.geometry.coordinates)
.addTo(mapInstance);
});
}
/**
* Code to generate random coordinates
* */
function getRandomInRange(from, to, fixed) {
return (Math.random() * (to - from) + from).toFixed(fixed) * 1;
// .toFixed() returns string, so ' * 1' is a trick to convert to number
}
const randomCoordinates = Array.from(
{ length: 20 },
() => [getRandomInRange(-180, 180, 3), getRandomInRange(-90, 90, 3)],
);

/**
* Given 2 coordinates and the number of steps return an array of coordinates in between
* @param {Array} startCoordinates - starting coordinates in the format [latitude, longitude]
* @param {Array} endCoordinates - ending coordinates in the format [latitude, longitude]
* @param {Number} numberOfSteps - number of steps to take between the start and end coordinates
* @returns {Array} - array of coordinates in the format [[latitude, longitude], [latitude, longitude]]
*/
function getCoordinatesBetween(startCoordinates, endCoordinates, numberOfSteps) {
const diffX = endCoordinates[0] - startCoordinates[0];
const diffY = endCoordinates[1] - startCoordinates[1];

const sfX = diffX / numberOfSteps;
const sfY = diffY / numberOfSteps;

let i = 0;
let j = 0;

const lineCoordinates = [];

while (Math.abs(i) < Math.abs(diffX) || Math.abs(j) < Math.abs(diffY)) {
lineCoordinates.push([startCoordinates[0] + i, startCoordinates[1] + j]);

if (Math.abs(i) < Math.abs(diffX)) {
i += sfX;
}

if (Math.abs(j) < Math.abs(diffY)) {
j += sfY;
}
}
// Because of rounding errors, lets push the exact end coordinates
// as the last item in the array to make sure the line ends precisely
lineCoordinates.push([endCoordinates[0], endCoordinates[1]]);
return lineCoordinates;
}

/**
* This function animates a series of lines from an array of starting coordinates to a single end point
* returns a promise when the animation is complete
* @param {Map Instance} mapInstance - the map instance
* @param {Array} originPoints - array of starting coordinates in the format [[latitude, longitude], [latitude, longitude]]
* @param {Array} endPoint - single end point in the format [latitude, longitude]
* @returns {Promise} - promise that resolves when the animation is complete
*/
function animateLines(mapInstance, originPoints, endPoint) {
const speedFactor = 100; // number of frames per degree, controls animation speed
return new Promise((resolve) => {
// EndPoint
mapInstance.addSource('endpoint', {
type: 'geojson',
data: {
type: 'Point',
coordinates: [
endPoint[0], endPoint[1],
],
},
});

const lineFlight = (startCoordinates, endCoordinates, index, lastLine = false) => {
const lineCoordinates = getCoordinatesBetween(startCoordinates, endCoordinates, speedFactor);
let animationCounter = 0;

// Create a GeoJSON source with an empty lineString.
const geojson = {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [],
},
}],
};

// Start Point
mapInstance.addSource(`startPoint${index}`, {
type: 'geojson',
data: {
type: 'Point',
coordinates: [
startCoordinates[0], startCoordinates[1],
],
},
});

// Line
mapInstance.addLayer({
id: `line-animation${index}`,
type: 'line',
source: {
type: 'geojson',
data: geojson,
},
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#277056',
'line-width': 2,
},
});

const animateLine = () => {
if (animationCounter < lineCoordinates.length) {
geojson.features[0].geometry.coordinates.push(lineCoordinates[animationCounter]);
mapInstance.getSource(`line-animation${index}`).setData(geojson);

requestAnimationFrame(animateLine);
// eslint-disable-next-line no-plusplus
animationCounter++;
} else {
// This else block is for "removing the line from point1 to 2"
const coord = geojson.features[0].geometry.coordinates;
// remove 2 points at a time
coord.shift();
coord.shift();

if (coord.length > 0) {
geojson.features[0].geometry.coordinates = coord;
mapInstance.getSource(`line-animation${index}`).setData(geojson);
requestAnimationFrame(animateLine);
} else {
// remove all sources to allow for new animation
mapInstance.removeLayer(`line-animation${index}`);
mapInstance.removeSource(`line-animation${index}`);
mapInstance.removeSource(`startPoint${index}`);

if (lastLine) {
mapInstance.removeSource('endpoint');
resolve();
}
}
}
};

animateLine();
};

originPoints.forEach((coordinate, index) => {
lineFlight(coordinate, endPoint, index, index === originPoints.length - 1);
});
});
}

export function animationCoordinator(mapInstance, borrowerPoints) {
return new Promise((resolve) => {
const destinationPoints = borrowerPoints.map((borrower) => borrower.location);
const totalNumberOfPoints = destinationPoints.length;
let currentPointIndex = 0;

const flyToPoint = (index) => {
mapInstance.flyTo({
// These options control the ending camera position: centered at
// the target, at zoom level 9, and north up.
center: destinationPoints[index],
zoom: 4,
bearing: 0,

// These options control the flight curve, making it move
// slowly and zoom out almost completely before starting
// to pan.
speed: 0.9, // make the flying slow
curve: 1, // change the speed at which it zooms out

// This can be any easing function: it takes a number between
// 0 and 1 and returns another number between 0 and 1.
easing(t) {
return t;
},

// this animation is considered essential with respect to prefers-reduced-motion
essential: true,
}, { flyEnd: true });
};

// This will trigger the next steps in the animation chain
mapInstance.on('moveend', (event) => {
if (event.flyEnd === true) {
animateLines(mapInstance, randomCoordinates, destinationPoints[currentPointIndex])
.then(() => {
if (currentPointIndex < totalNumberOfPoints - 1) {
currentPointIndex += 1;
flyToPoint(currentPointIndex);
} else {
resolve();
}
});
}
});

// fly to point 1
flyToPoint(currentPointIndex);
});
}
81 changes: 81 additions & 0 deletions @kiva/kv-components/vue/KvMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
</template>

<script>
import { animationCoordinator, generateMapMarkers } from '../utils/mapAnimation';
export default {
name: 'KvMap',
props: {
Expand Down Expand Up @@ -87,6 +89,31 @@ export default {
type: Number,
default: 4,
},
/**
* Size properties of the default image to use as a fallback.
* Sample object:
* {
borrowerPoints: [
{
image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/e60a3d61ff052d60991c5d6bbf4a45d3.jpg',
location: [-77.032, 38.913],
},
{
image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/6101929097c6e5de48232a4d1ae3b71c.jpg',
location: [41.402, 7.160],
},
{
image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/11e018ee3d8b9c5adee459c16a29d264.jpg',
location: [-73.356596, 3.501],
},
],
* }
*/
advancedAnimation: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
Expand Down Expand Up @@ -298,6 +325,24 @@ export default {
if (this.initialZoom !== null) {
this.createWrapperObserver();
}
this.mapInstance.on('load', () => {
// If this object exist in props, fire animation
if (this.advancedAnimation?.borrowerPoints) {
// remove country labels
this.mapInstance.style.stylesheet.layers.forEach((layer) => {
if (layer.type === 'symbol') {
this.mapInstance.removeLayer(layer.id);
}
});
generateMapMarkers(this.mapInstance, this.advancedAnimation.borrowerPoints);
animationCoordinator(this.mapInstance, this.advancedAnimation.borrowerPoints).then(() => {
console.log('animation complete');
this.mapInstance.dragPan.enable();
this.mapInstance.scrollZoom.enable();
});
}
});
},
checkIntersectionObserverSupport() {
if (typeof window === 'undefined'
Expand Down Expand Up @@ -348,3 +393,39 @@ export default {
},
};
</script>
<style>
.marker {
margin-top: -77px;
margin-left: 35px;
display: block;
border: none;
border-radius: 50%;
cursor: pointer;
padding: 0;
}
.marker::after {
content: '';
position: absolute;
top: -8px;
left: -8px;
right: -8px;
bottom: -8px;
border-radius: 50%;
border: 4px solid #000;
}
.marker::before {
content: "";
width: 0;
height: 0;
left: -13px;
bottom: -32px;
border: 9px solid transparent;
border-left: 40px solid #000;
transform: rotate(114deg);
position: absolute;
z-index: -1;
}
</style>
Loading

0 comments on commit 0f14e09

Please sign in to comment.