これは MIERUNE AdventCalendar 2022 5日目の記事です。
昨日は @satoshi7190 さんによる Maplibre GL JSで地図上に植林してみた でした。
30DayMapChallengeのMusicのお題で作ったものを紹介します
なぜこれを作ろうと思ったのか
昔から、なんとなく地図から音楽生み出せないかなあとはぼんやり思っていたのですが、実践することにしてみました
- 地形の断面を波形とみなして音を生成
- 標高線の数値を利用して音を生成
- 地図上の地物の情報を利用して音を生成
上記のようなアイデアがありましたが、なかなか実現には骨が折れる感じだったので、今回の作品に落ちつきました
地図を用意
まず、ベースとなる地図を用意します
今回はMaplibreGLJSで、ベースフレームワークはSvelteKitを使いました
SvelteKitは今回のケースだと不要ですが、社内LT(MIERUNEには定期的にLTを行う文化があります)で紹介されて、気になっていたので使いました
こちらを参考に用意しました
(これ、日本語でも用意したほうが良いなあと思いつつ。。)
地図を走査する
次に、地図上のポイントを走査する線を作ります
色々考えた結果、GeoJsonで線引いて動かす方向で行ってみようと思い、以下実装を入れます
// 地図の4隅の座標を取得
const bounds = map.getBounds();
// 縦に線を引くgeojson
const lineAnimationData = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'LineString',
'coordinates': [[bounds._sw.lng, bounds._ne.lat], [bounds._sw.lng, bounds._sw.lat]]
}
}
]
};
map.on('load', function () {
// 縦に線を引くgeojsonを読み込み
map.addSource('lineAnimation', {
'type': 'geojson',
'data': lineAnimationData
});
// 線をレイヤーとして追加
map.addLayer({
'id': 'lineAnimation',
'type': 'line',
'source': 'lineAnimation',
'paint': {
'line-color': '#ed6498',
'line-width': 8,
'line-opacity': 0.8
}
});
});
この線を時間が進むにつれて横に動かせば良いので、setIntervalを使って徐々に経度を追加していくような実装にしました
const interval = setInterval(() => {
// フレーム毎に追加される経度
addLng += 0.0001;
// 右端まで行ったら最初に戻る
if (bounds._sw.lng + addLng > bounds._ne.lng) {
addLng = 0;
};
// 線のgeojson書き換え
lineAnimationData.features[0].geometry.coordinates = [[bounds._sw.lng + addLng, bounds._ne.lat], [bounds._sw.lng + addLng, bounds._sw.lat]];
// 一応sourceの存在チェック
if (map.getSource('lineAnimation')) {
// sourceの値入れ替え
map.getSource('lineAnimation').setData(lineAnimationData);
};
// インターバルの間隔
}, 50);
return() => clearInterval(interval);
これで動く走査線が出来ましたので、あとはポイントを載せて衝突判定させれば良さそうです
ポイントを載せる
なんか適当にデータ載せれればよかったのですが、ランダムにしました
ランダムポイントの生成ですが、Turf.jsを使います
import { randomPoint } from '@turf/random';
// 省略
const randomPoints = randomPoint(50, {bbox:[bounds._sw.lng,bounds._sw.lat,bounds._ne.lng,bounds._ne.lat]});
map.addSource('randomPoints', {
'type': 'geojson',
'data': randomPoints
});
map.addLayer({
'id': 'randomPoints',
'type': 'circle',
'source': 'randomPoints',
'paint': {
'circle-color': '#0ff',
'circle-opacity': 0.7,
'circle-radius': 10
}
});
交差の判定を行う前に
ポイントとの交差判定だと、すり抜ける事がありそうだなーと思い、交差判定はメッシュグリッドを生成して判定しようと思いました
ポイントが存在するメッシュに走査線が入ると音が鳴るという事です
メッシュグリッドの生成も、Turf.jsを使います
透明なメッシュを作ります
import squareGrid from '@turf/square-grid';
// 省略
const squareGrids = squareGrid([bounds._sw.lng,bounds._sw.lat,bounds._ne.lng,bounds._ne.lat], 0.1);
map.addSource('squareGrids', {
'type': 'geojson',
'data': squareGrids
});
map.addLayer({
'id': 'squareGrids',
'type': 'fill',
'source': 'squareGrids',
'paint': {
'fill-color': '#666',
'fill-opacity': 0.0
}
});
交差判定
Turf.jsのbooleanIntersectsで交差判定します
愚直にforEachでチェックして判定しています
import {lineString, polygon, point} from '@turf/helpers';
// 省略
//グリッドとlineの交差チェック
const checkLine = lineString([[bounds._sw.lng + addLng, bounds._ne.lat], [bounds._sw.lng + addLng, bounds._sw.lat]]);
squareGrids.features.forEach(gridElement => {
const checkPolygon = polygon(gridElement.geometry.coordinates);
// ポイントとグリッドの交差チェック
if (booleanIntersects(checkLine, checkPolygon) && !toneGrid.includes(gridElement.geometry.coordinates)){
randomPoints.features.forEach(pointElement => {
if (booleanIntersects(point(pointElement.geometry.coordinates), checkPolygon)){
// ここで音を鳴らしたい
}
});
};
});
音を鳴らす
音はtone.jsを使いました
ここではtone.jsについての詳細は割愛しますが、ブラウザは何らかのボタンアクションが無いと音を鳴らせないようになっているので、実はSoundONのボタンを用意していました
実装は以下のような感じ
グリッドがどのあたりにあるかで音の階層をつけてあげるという実装です
(今回、グリッドを抜けるまでは音を鳴らさないよう改良しています)
import * as Tone from 'tone'
// 省略
// 音
const synth = new Tone.PolySynth().toDestination();
// 今鳴っているグリッド
const toneGrid: Array<any>= [];
// 省略
// 鳴らす音を格納
const toneArray: Array<String> = [];
const noteArray: Array<String> = ['C4','D4','E4','F4','G4','A4','B4','C5','D5','E5','F5','G5','A5','B5','C6','D6','E6'];
//グリッドとlineの交差チェック
const checkLine = lineString([[bounds._sw.lng + addLng, bounds._ne.lat], [bounds._sw.lng + addLng, bounds._sw.lat]]);
squareGrids.features.forEach(gridElement => {
const checkPolygon = polygon(gridElement.geometry.coordinates);
// ポイントとグリッドの交差チェック
if (booleanIntersects(checkLine, checkPolygon) && !toneGrid.includes(gridElement.geometry.coordinates)){
randomPoints.features.forEach(pointElement => {
if (booleanIntersects(point(pointElement.geometry.coordinates), checkPolygon)){
// ここで音を鳴らしたい
const posY: number = map.project(pointElement.geometry.coordinates).y;
const notePosition: number = Math.floor((posY / 50));
if (notePosition < noteArray.length) {
toneArray.push(noteArray[notePosition]);
} else {
toneArray.push(noteArray[noteArray.length]);
}
// チェック用の配列に入れる
toneGrid.push(gridElement.geometry.coordinates);
}
});
};
});
if (toneArray.length > 0) {
synth.triggerAttackRelease(toneArray, "8n", Tone.now());
}
// 省略
出来たもの
左下のSound ONボタンを押すと音が出ます
ソースコードは少々お待ちを。。。
感想
- 何か不思議なものが出来たなあと思います
- Turf.jsはとても便利だと再認識
- SvelteKitはもう少し使い勝手を調べてみたい
- Tone.jsは趣味で使っていきたい
明日は@patrickyuen00さんです!お楽しみにー