All docsMapbox GL JSExamplesAnimate 3D buildings based on ambient sounds

Animate 3D buildings based on ambient sounds

This example uses runtime styling with the Web Audio API to create a map where the 3D buildings dynamically change height to the rhythm of your ambient environment, giving the appearance of dancing.

It uses getUserMedia to access audio from the user's environment, then analyzes that sound for loudness and uses the loudness levels to adjust the fill-extrusion-height property of the fill-extrusion layer.

To optimize performance, it uses the ==, > and <= expression operators to filter buildings in the layer based on their height data.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Animate 3D buildings based on ambient sounds</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
// TO MAKE THE MAP APPEAR YOU MUST
// ADD YOUR ACCESS TOKEN FROM
// https://account.mapbox.com
mapboxgl.accessToken = '<your access token here>';
/* global Promise */
// Use a minimal variant of the Mapbox Dark style, with certain features removed.
var map = new mapboxgl.Map({
style: 'mapbox://styles/examples/cj68bstx01a3r2rndlud0pwpv',
center: {
lng: -74.00649562332922,
lat: 40.70811328605049
},
zoom: 15,
pitch: 55,
container: 'map',
antialias: true
});
map.addControl(new mapboxgl.FullscreenControl());
map.on('load', function () {
var bins = 16;
var maxHeight = 200;
var binWidth = maxHeight / bins;
// Divide the buildings into 16 bins based on their true height, using a layer filter.
for (var i = 0; i < bins; i++) {
map.addLayer({
'id': '3d-buildings-' + i,
'source': 'composite',
'source-layer': 'building',
'filter': [
'all',
['==', 'extrude', 'true'],
['>', 'height', i * binWidth],
['<=', 'height', (i + 1) * binWidth]
],
'type': 'fill-extrusion',
'minzoom': 15,
'paint': {
'fill-extrusion-color': '#aaa',
'fill-extrusion-height-transition': {
duration: 0,
delay: 0
},
'fill-extrusion-opacity': 0.6
}
});
}
// Older browsers might not implement mediaDevices at all, so we set an empty object first
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// Some browsers partially implement mediaDevices. We can't just assign an object
// with getUserMedia as it would overwrite existing properties.
// Here, we will just add the getUserMedia property if it's missing.
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints) {
// First get ahold of the legacy getUserMedia, if present
var getUserMedia =
navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// Some browsers just don't implement it - return a rejected promise with an error
// to keep a consistent interface
if (!getUserMedia) {
return Promise.reject(
new Error(
'getUserMedia is not implemented in this browser'
)
);
}
// Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(function (stream) {
// Set up a Web Audio AudioContext and AnalyzerNode, configured to return the
// same number of bins of audio frequency data.
var audioCtx = new (window.AudioContext ||
window.webkitAudioContext)();
var analyser = audioCtx.createAnalyser();
analyser.minDecibels = -90;
analyser.maxDecibels = -10;
analyser.smoothingTimeConstant = 0.85;
var source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.fftSize = bins * 2;
var dataArray = new Uint8Array(bins);
function draw(now) {
analyser.getByteFrequencyData(dataArray);
// Use that data to drive updates to the fill-extrusion-height property.
var avg = 0;
for (var i = 0; i < bins; i++) {
avg += dataArray[i];
map.setPaintProperty(
'3d-buildings-' + i,
'fill-extrusion-height',
10 + 4 * i + dataArray[i]
);
}
avg /= bins;
// Animate the map bearing and light color over time, and make the light more
// intense when the audio is louder.
map.setBearing(now / 500);
map.setLight({
color:
'hsl(' +
((now / 100) % 360) +
',' +
Math.min(50 + avg / 4, 100) +
'%,50%)',
intensity: Math.min(1, (avg / 256) * 10)
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
})
.catch(function (err) {
console.log('The following gUM error occured: ' + err);
});
});
</script>
</body>
</html>