|
| 1 | +<template> |
| 2 | +<div ref="container" :class="$style.root"> |
| 3 | +</div> |
| 4 | +</template> |
| 5 | + |
| 6 | +<script lang="ts" setup> |
| 7 | +import { onMounted, onUnmounted, shallowRef, nextTick, ref } from 'vue'; |
| 8 | +import * as Misskey from 'misskey-js'; |
| 9 | +import * as THREE from 'three'; |
| 10 | +import vertexShader from '../../node_modules/webgl-audiovisualizer/audiovisial-vertex.shader?raw'; |
| 11 | +import fragmentShader from '../../node_modules/webgl-audiovisualizer/audiovisial-fragment.shader?raw'; |
| 12 | +import circleMask from '../../node_modules/webgl-audiovisualizer/circlemask.png'; |
| 13 | +
|
| 14 | +const props = defineProps<{ |
| 15 | + user: Misskey.entities.UserLite; |
| 16 | + audioEl: HTMLAudioElement | undefined; |
| 17 | + analyser: AnalyserNode | undefined; |
| 18 | +}>(); |
| 19 | +
|
| 20 | +const container = shallowRef<HTMLDivElement>(); |
| 21 | +
|
| 22 | +const isPlaying = ref(false); |
| 23 | +const fftSize = 2048; |
| 24 | +
|
| 25 | +let prevTime = 0; |
| 26 | +let angle1 = 0; |
| 27 | +let angle2 = 0; |
| 28 | +
|
| 29 | +let scene, camera, renderer, width, height, uniforms, texture, maskTexture, dataArray1, dataArray2, dataArrayOrigin, bufferLength: number; |
| 30 | +
|
| 31 | +const init = () => { |
| 32 | + const parent = container.value ?? { offsetWidth: 0 }; |
| 33 | + width = parent.offsetWidth; |
| 34 | + height = Math.floor(width * 9 / 16); |
| 35 | +
|
| 36 | + scene = new THREE.Scene(); |
| 37 | + camera = new THREE.OrthographicCamera(); |
| 38 | + camera.left = width / -2; |
| 39 | + camera.right = width / 2; |
| 40 | + camera.top = height / 2; |
| 41 | + camera.bottom = height / -2; |
| 42 | + camera.updateProjectionMatrix(); |
| 43 | +
|
| 44 | + renderer = new THREE.WebGLRenderer({ antialias: true }); |
| 45 | + renderer.setSize(width, height); |
| 46 | +
|
| 47 | + if (container.value) { |
| 48 | + container.value.appendChild(renderer.domElement); |
| 49 | + } |
| 50 | +
|
| 51 | + const loader = new THREE.TextureLoader(); |
| 52 | + texture = loader.load(props.user.avatarUrl ?? ''); |
| 53 | + maskTexture = loader.load(circleMask); |
| 54 | + uniforms = { |
| 55 | + enableAudio: { |
| 56 | + value: 0, |
| 57 | + }, |
| 58 | + uTex: { value: texture }, |
| 59 | + uMask: { value: maskTexture }, |
| 60 | + time: { |
| 61 | + value: 0, |
| 62 | + }, |
| 63 | + resolution: { |
| 64 | + value: new THREE.Vector2(width, height), |
| 65 | + }, |
| 66 | + }; |
| 67 | +
|
| 68 | + const material = new THREE.ShaderMaterial({ |
| 69 | + uniforms: uniforms, |
| 70 | + vertexShader: vertexShader, |
| 71 | + fragmentShader: fragmentShader, |
| 72 | + }); |
| 73 | +
|
| 74 | + const geometry = new THREE.PlaneGeometry(2, 2); |
| 75 | +
|
| 76 | + const mesh = new THREE.Mesh(geometry, material); |
| 77 | + scene.add(mesh); |
| 78 | +
|
| 79 | + renderer.setAnimationLoop(animate); |
| 80 | +}; |
| 81 | +
|
| 82 | +const play = () => { |
| 83 | + if (props.analyser) { |
| 84 | + bufferLength = props.analyser.frequencyBinCount; |
| 85 | + dataArrayOrigin = new Uint8Array(bufferLength); |
| 86 | + dataArray1 = new Uint8Array(bufferLength); |
| 87 | + dataArray2 = new Uint8Array(bufferLength); |
| 88 | + uniforms = { |
| 89 | + enableAudio: { |
| 90 | + value: 1, |
| 91 | + }, |
| 92 | + tAudioData1: { value: new THREE.DataTexture(dataArray1, fftSize / 2, 1, THREE.RedFormat) }, |
| 93 | + tAudioData2: { value: new THREE.DataTexture(dataArray2, fftSize / 2, 1, THREE.RedFormat) }, |
| 94 | + uTex: { value: texture }, |
| 95 | + uMask: { value: maskTexture }, |
| 96 | + time: { |
| 97 | + value: 0, |
| 98 | + }, |
| 99 | + resolution: { |
| 100 | + value: new THREE.Vector2(width, height), |
| 101 | + }, |
| 102 | + }; |
| 103 | + const material = new THREE.ShaderMaterial({ |
| 104 | + uniforms: uniforms, |
| 105 | + vertexShader: vertexShader, |
| 106 | + fragmentShader: fragmentShader, |
| 107 | + }); |
| 108 | +
|
| 109 | + const geometry = new THREE.PlaneGeometry(2, 2); |
| 110 | +
|
| 111 | + const mesh = new THREE.Mesh(geometry, material); |
| 112 | + scene.add(mesh); |
| 113 | +
|
| 114 | + renderer.setAnimationLoop(animate); |
| 115 | + } |
| 116 | +}; |
| 117 | +
|
| 118 | +const pauseAnimation = () => { |
| 119 | + isPlaying.value = false; |
| 120 | +}; |
| 121 | +
|
| 122 | +const resumeAnimation = () => { |
| 123 | + isPlaying.value = true; |
| 124 | + renderer.setAnimationLoop(play); |
| 125 | +}; |
| 126 | +
|
| 127 | +const animate = (time) => { |
| 128 | + if (angle1 >= bufferLength) { |
| 129 | + angle1 = 0; |
| 130 | + } |
| 131 | + if (angle2 >= bufferLength) { |
| 132 | + angle2 = 0; |
| 133 | + } |
| 134 | +
|
| 135 | + if (props.analyser) { |
| 136 | + if ((time - prevTime) > 10) { |
| 137 | + for (let i = 0; i < bufferLength; i++) { |
| 138 | + let n1 = (i + angle1) % bufferLength; |
| 139 | + let n2 = (i + angle2) % bufferLength; |
| 140 | + if (dataArrayOrigin[n1] > dataArray1[i]) { |
| 141 | + dataArray1[i] = dataArray1[i] < 255 ? (dataArrayOrigin[i] + dataArray1[i]) / 2 : 255; |
| 142 | + } |
| 143 | + if (dataArrayOrigin[n2] > dataArray2[i]) { |
| 144 | + dataArray2[i] = dataArray2[i] < 255 ? (dataArrayOrigin[i] + dataArray2[i]) / 2 : 255; |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + if ((time - prevTime) > 20) { |
| 149 | + for (let i = 0; i < bufferLength; i++) { |
| 150 | + let n1 = (i + angle1) % bufferLength; |
| 151 | + let n2 = (i + angle2) % bufferLength; |
| 152 | + if (dataArrayOrigin[n1] < dataArray1[i]) { |
| 153 | + dataArray1[i] = dataArray1[i] > 0 ? dataArray1[i] - 1 : 0; |
| 154 | + } |
| 155 | + if (dataArrayOrigin[n2] < dataArray2[i]) { |
| 156 | + dataArray2[i] = dataArray2[i] > 0 ? dataArray2[i] - 1 : 0; |
| 157 | + } |
| 158 | + } |
| 159 | + uniforms.tAudioData1.value.needsUpdate = true; |
| 160 | + uniforms.tAudioData2.value.needsUpdate = true; |
| 161 | + prevTime = time; |
| 162 | + } |
| 163 | +
|
| 164 | + if (isPlaying.value) { |
| 165 | + props.analyser.getByteTimeDomainData(dataArrayOrigin); |
| 166 | + } else { |
| 167 | + for (let i = 0; i < bufferLength; i++) { |
| 168 | + dataArrayOrigin[i] = 0; |
| 169 | + } |
| 170 | + } |
| 171 | +
|
| 172 | + angle1 += parseInt(String(bufferLength / 360 * 2)); //こうしないと型エラー出てうざい |
| 173 | + angle2 += parseInt(String(bufferLength / 360 * 3)); |
| 174 | + } |
| 175 | + uniforms.time.value = time; |
| 176 | + renderer.render(scene, camera); |
| 177 | +}; |
| 178 | +
|
| 179 | +const onResize = () => { |
| 180 | + const parent = container.value ?? { offsetWidth: 0 }; |
| 181 | + width = parent.offsetWidth; |
| 182 | + height = Math.floor(width * 9 / 16); |
| 183 | + camera.left = width / -2; |
| 184 | + camera.right = width / 2; |
| 185 | + camera.top = height / 2; |
| 186 | + camera.bottom = height / -2; |
| 187 | + camera.updateProjectionMatrix(); |
| 188 | + renderer.setSize(width, height); |
| 189 | + uniforms.resolution.value.set(width, height); |
| 190 | +}; |
| 191 | +
|
| 192 | +onMounted(async () => { |
| 193 | + nextTick().then(() => { |
| 194 | + init(); |
| 195 | + window.addEventListener('resize', onResize); |
| 196 | + }); |
| 197 | +}); |
| 198 | +
|
| 199 | +onUnmounted(() => { |
| 200 | + if (renderer) { |
| 201 | + renderer.dispose(); |
| 202 | + } |
| 203 | +}); |
| 204 | +
|
| 205 | +defineExpose({ |
| 206 | + pauseAnimation, |
| 207 | + resumeAnimation, |
| 208 | +}); |
| 209 | +
|
| 210 | +</script> |
| 211 | + |
| 212 | +<style lang="scss" module> |
| 213 | +.root { |
| 214 | + position: relative; |
| 215 | + width: 100%; |
| 216 | + height: 100%; |
| 217 | + overflow: hidden; |
| 218 | +} |
| 219 | +</style> |
0 commit comments