はじめに
Nodejsでytdl-coreを使って取ってきたYoutube音声を再生して自動でAudioSpectrumを作るアプリを作ろうとしたときPixiJSでSpectrumを描画しようとしたら詰まったのでメモ。
環境構築(Preact)
自分が行ったとおり(Codespacesでbunをつかって)やっていきますので所々違うかもしれませんがまあそこは自分で調整して下さい
npm i -g bun
bun create preact
bun add pixi.js
解説 読みたい人向け
npm i -g bun
普通にbunのinstall
bun create preact
bunでpreactプロジェクトの作成
bunのところをnpmに変えても動くのかな?しらんけど
bun add pixi.js
pixijsをinstall
<本題>Spectrum Componentの作成
Componentはclass componentを使用します。そっちの方がそのまま書けるので
簡単にcanvasを操るテンプレ?がこちら
import { Component, createRef } from "preact";
export class Spectrum extends Component{
canv = createRef<HTMLCanvasElement>();
componentDidMount() {
}
render(){
return <canvas ref={this.canv} />
}
}
これにPixi Applicationなりを定義していくのですがv7とv8でPixiJS使用が大幅に変わっているらしい(性能も大幅によくなっているらしい)
PixiJS v8 Launches! 🎉
ということでドキュメントを読みながら書いてみたSpectrumがこれ
import { Component, createRef } from "preact";
import { Application, Graphics } from "pixi.js";
import Music from "../assets/music.mp3";
export class Spectrum extends Component {
config = {
size: 800,
circleMode: false
}
state = {
playing: false,
}
audio: HTMLAudioElement;
audioContext: AudioContext;
nodeAnalyser: AnalyserNode;
nodeSource: MediaElementAudioSourceNode;
canv = createRef<HTMLCanvasElement>();
app: Application;
bars: Graphics[] = [];
playAudio = () => {
this.audioContext = new AudioContext();
this.nodeAnalyser = this.audioContext.createAnalyser();
this.nodeAnalyser.fftSize = 1024; // ここの値を大きくすると細かくなります 範囲 32~32768
this.nodeAnalyser.smoothingTimeConstant = 0.85;
this.nodeAnalyser.connect(this.audioContext.destination);
this.setState({ playing: true });
this.audio = new Audio(Music);
this.nodeSource = this.audioContext.createMediaElementSource(this.audio);
this.nodeSource.connect(this.nodeAnalyser);
this.audio.play();
(async () => {
this.app = new Application();
await this.app.init({
canvas: this.canv.current,
width: this.config.size,
height: this.config.size,
backgroundColor: 0x000000,
antialias: true,
});
for (let i = 0; i < this.nodeAnalyser.frequencyBinCount; i++) {
const bar = new Graphics();
bar.pivot.set(this.config.size / this.nodeAnalyser.frequencyBinCount / 2, 0);
bar.rect(0, 0, this.config.size / this.nodeAnalyser.frequencyBinCount, 10);
bar.fill(0xffffff/* * Math.random()*/); // Math.random()を掛けることで色がランダムになる
this.bars.push(bar);
}
this.bars.forEach((bar, i) => {
if (!this.config.circleMode) {
bar.x = (i + 0.5) * this.config.size / this.nodeAnalyser.frequencyBinCount;
bar.y = this.config.size / 2;
bar.rotation = Math.PI;
} else {
bar.x = 200 + 100 * Math.cos(2 * Math.PI * i / this.nodeAnalyser.frequencyBinCount);
bar.y = 200 + 100 * Math.sin(2 * Math.PI * i / this.nodeAnalyser.frequencyBinCount);
bar.rotation = 2 * Math.PI * i / this.nodeAnalyser.frequencyBinCount - Math.PI / 2;
}
this.app.stage.addChild(bar);
});
this.audio.play();
this.app.ticker.add(() => {
this.update();
});
})()
}
update = () => {
const data = new Uint8Array(this.nodeAnalyser.frequencyBinCount);
this.nodeAnalyser.getByteFrequencyData(data);
this.bars.forEach((bar, i) => {
bar.height = data[i];
});
}
render() {
return (
<div style={{ position: 'relative' }}>
<button style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, margin: 'auto', height: 30, width: 120, display: this.state.playing && 'none' }} onClick={this.playAudio}>
Play
</button>
<canvas ref={this.canv} />
</div>
)
}
}
css読み込むのがめんどっちかったので直書きstyle
Applicationの定義などの以前にasyncを使った非同期関数でくくらなきゃいけないらしい。もちろんいくつかは呼び出すときawaitを付けなきゃいけない。
以前はApplication classを呼び出す際の引数でoptionをいれていたが呼び出した後initの中にoptionを入れたり名前が少し変わったりしてる。たとえば以前は
const app = new Application({
view: this.canv.current
})
だったものが、viewが非推奨になって
const app = new Application();
await app.init({
canvas: this.canv.current
});
になったりしている。うーんv7からv8への移行が大変そう...
で開発陣も言っているv8へのアップデートで一番変わったのがGraphics
PixiJS v8 移行ガイドを読んで変更点を簡単にまとめた
PIXI.Graphics変更点まとめ
- fillが先ではなく後に
- 図形を描画するメゾットの名前が変更
2つしかないじゃんって思った人もいると思うがこの2つが結構でかい...
今までだと
// red rect
const graphics = new Graphics()
.beginFill(0xFF0000)
.drawRect(50, 50, 100, 100)
.endFill();
// blur rect with stroke
const graphics2 = new Graphics()
.lineStyle(2, 'white')
.beginFill('blue')
.circle(530, 50, 140, 100)
.endFill();
まあこんな感じ。
これが
// red rect
const graphics = new Graphics()
.rect(50, 50, 100, 100)
.fill(0xFF0000);
// blur rect with stroke
const graphics2 = new Graphics()
.rect(50, 50, 100, 100)
.fill('blue')
.stroke({width:2, color:'white'});
こうなる。
beginFillで色を指定して図形を描画しendFillしていたものが先に図形を指定してfillで塗りつぶすなりstrokeで枠線を付けるようになってる。なんかいいコンバーターとかないかな...
つくってみるのもありかなと思ったけどそれほど需要はなさそうなので却下
まあGraphicsの変更はこんな感じであとは
- DisplayObjectが削除されてContainerがすべての要素の標準classに
- updateTransformが削除
- Assetsの追加方法の変更
- Texture.fromがurlから直接は出来なくなった <= 何気にめんどくさい
- app.ticker.add((d) => void)のdがdeltaTimeからTickerを返すようになった
- container.getBounds()で取得できていたものがcontainer.getBounds().rectangle
ちょっと多すぎるのでここまでにします詳しくは公式移行ガイドを自分で見てもらった方が早いかなと思うけど変更オオスギィ!