周囲の音に基づき 3D 建物をアニメーション化
runtime styling API を Web Audio API と接続し、3D 建物が周囲環境に適応するマップを作成します。
<!DOCTYPE html><html><head><meta charset="utf-8"><title>周囲の音に基づき 3D 建物をアニメーション化</title><meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"><link href="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css" rel="stylesheet"><script src="https://api.mapbox.com/mapbox-gl-js/v2.14.1/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_MAPBOX_ACCESS_TOKEN';// Use a minimal variant of the Mapbox Dark style, with certain features removed.const map = new mapboxgl.Map({style: 'mapbox://styles/examples/cj68bstx01a3r2rndlud0pwpv',center: {lng: -74.0064,lat: 40.7081},zoom: 15,pitch: 55,container: 'map',antialias: true}); map.addControl(new mapboxgl.FullscreenControl()); map.on('load', () => {const bins = 16;const maxHeight = 200;const binWidth = maxHeight / bins; // Divide the buildings into 16 bins based on their true height, using a layer filter.for (let 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 firstif (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 = (constraints) => {// First get ahold of the legacy getUserMedia, if presentconst getUserMedia =navigator.webkitGetUserMedia || navigator.mozGetUserMedia; // Some browsers just don't implement it - return a rejected promise with an error// to keep a consistent interfaceif (!getUserMedia) {return Promise.reject(new Error('getUserMedia is not implemented in this browser'));} // Otherwise, wrap the call to the old navigator.getUserMedia with a Promisereturn new Promise((resolve, reject) => {getUserMedia.call(navigator, constraints, resolve, reject);});};} navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {// Set up a Web Audio AudioContext and AnalyzerNode, configured to return the// same number of bins of audio frequency data.const audioCtx = new (window.AudioContext ||window.webkitAudioContext)(); const analyser = audioCtx.createAnalyser();analyser.minDecibels = -90;analyser.maxDecibels = -10;analyser.smoothingTimeConstant = 0.85; const source = audioCtx.createMediaStreamSource(stream);source.connect(analyser); analyser.fftSize = bins * 2; const dataArray = new Uint8Array(bins); function draw(now) {analyser.getByteFrequencyData(dataArray); // Use that data to drive updates to the fill-extrusion-height property.let avg = 0;for (let 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);const hue = (now / 100) % 360;const saturation = Math.min(50 + avg / 4, 100);map.setLight({color: `hsl(${hue},${saturation}%,50%)`,intensity: Math.min(1, (avg / 256) * 10)}); requestAnimationFrame(draw);} requestAnimationFrame(draw);}).catch((err) => {console.log('The following gUM error occurred:', err);});});</script> </body></html>