mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: visualizer overhaul (#1575)
This commit is contained in:
parent
4854e56fdb
commit
5e283ef539
27 changed files with 902 additions and 102 deletions
|
@ -31,6 +31,7 @@
|
|||
"select": "^1.1.2",
|
||||
"sketch-js": "^1.1.3",
|
||||
"slugify": "^1.0.2",
|
||||
"three": "^0.146.0",
|
||||
"vue": "^3.2.32",
|
||||
"vue-global-events": "^2.1.1",
|
||||
"youtube-player": "^3.0.4"
|
||||
|
@ -49,6 +50,7 @@
|
|||
"@types/lodash": "^4.14.150",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/pusher-js": "^4.2.2",
|
||||
"@types/three": "^0.144.0",
|
||||
"@types/youtube-player": "^5.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
||||
"@typescript-eslint/parser": "^4.11.1",
|
||||
|
|
|
@ -14,7 +14,7 @@ new class extends UnitTestCase {
|
|||
Volume: this.stub('Volume')
|
||||
},
|
||||
provide: {
|
||||
[CurrentSongKey]: factory<Song>('song', {
|
||||
[<symbol>CurrentSongKey]: factory<Song>('song', {
|
||||
playback_state: 'Playing'
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
<template>
|
||||
<div class="extra-controls" data-testid="other-controls">
|
||||
<div class="wrapper">
|
||||
<button
|
||||
v-if="song?.playback_state === 'Playing'"
|
||||
<a
|
||||
v-koel-tooltip.top
|
||||
class="visualizer-btn"
|
||||
data-testid="toggle-visualizer-btn"
|
||||
title="Toggle the visualizer"
|
||||
type="button"
|
||||
@click.prevent="toggleVisualizer"
|
||||
href="/#/visualizer"
|
||||
title="Show the visualizer"
|
||||
>
|
||||
<icon :icon="faBolt"/>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<button
|
||||
v-if="useEqualizer"
|
||||
|
@ -41,7 +39,6 @@ import Volume from '@/components/ui/Volume.vue'
|
|||
const song = requireInjection(CurrentSongKey, ref(null))
|
||||
|
||||
const showEqualizer = () => eventBus.emit('MODAL_SHOW_EQUALIZER')
|
||||
const toggleVisualizer = () => eventBus.emit('TOGGLE_VISUALIZER')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`renders 1`] = `
|
||||
<div class="extra-controls" data-testid="other-controls" data-v-8bf5fe81="">
|
||||
<div class="wrapper" data-v-8bf5fe81=""><button class="visualizer-btn" data-testid="toggle-visualizer-btn" title="Toggle the visualizer" type="button" data-v-8bf5fe81=""><br data-testid="icon" icon="[object Object]" data-v-8bf5fe81=""></button>
|
||||
<div class="wrapper" data-v-8bf5fe81=""><a class="visualizer-btn" data-testid="toggle-visualizer-btn" href="/#/visualizer" title="Show the visualizer" data-v-8bf5fe81=""><br data-testid="icon" icon="[object Object]" data-v-8bf5fe81=""></a>
|
||||
<!--v-if--><br data-testid="Volume" data-v-8bf5fe81="">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { ref } from 'vue'
|
|||
import { waitFor } from '@testing-library/vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { eventBus } from '@/utils'
|
||||
import { albumStore, preferenceStore } from '@/stores'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { CurrentSongKey } from '@/symbols'
|
||||
|
@ -10,17 +9,17 @@ import AlbumArtOverlay from '@/components/ui/AlbumArtOverlay.vue'
|
|||
import MainContent from './MainContent.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (hasCurrentSong = false) {
|
||||
private renderComponent () {
|
||||
return this.render(MainContent, {
|
||||
global: {
|
||||
provide: {
|
||||
[CurrentSongKey]: ref(factory<Song>('song'))
|
||||
[<symbol>CurrentSongKey]: ref(factory<Song>('song'))
|
||||
},
|
||||
stubs: {
|
||||
AlbumArtOverlay,
|
||||
HomeScreen: this.stub(), // so that home overview requests are not made
|
||||
Visualizer: this.stub('visualizer'),
|
||||
},
|
||||
Visualizer: this.stub('visualizer')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -41,15 +40,5 @@ new class extends UnitTestCase {
|
|||
|
||||
await waitFor(() => expect(queryByTestId('album-art-overlay')).toBeNull())
|
||||
})
|
||||
|
||||
it('toggles visualizer', async () => {
|
||||
const { getByTestId, queryByTestId } = this.renderComponent()
|
||||
|
||||
eventBus.emit('TOGGLE_VISUALIZER')
|
||||
await waitFor(() => getByTestId('visualizer'))
|
||||
|
||||
eventBus.emit('TOGGLE_VISUALIZER')
|
||||
await waitFor(() => expect(queryByTestId('visualizer')).toBeNull())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
lists), so we use v-show.
|
||||
For those that don't need to maintain their own UI state, we use v-if and enjoy some code-splitting juice.
|
||||
-->
|
||||
<Visualizer v-if="showingVisualizer"/>
|
||||
<VisualizerScreen v-if="screen === 'Visualizer'"/>
|
||||
<AlbumArtOverlay v-if="showAlbumArtOverlay && currentSong" :album="currentSong?.album_id"/>
|
||||
|
||||
<HomeScreen v-show="screen === 'Home'"/>
|
||||
|
@ -34,7 +34,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref, toRef } from 'vue'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { requireInjection } from '@/utils'
|
||||
import { preferenceStore } from '@/stores'
|
||||
import { useThirdPartyServices } from '@/composables'
|
||||
import { CurrentSongKey, RouterKey } from '@/symbols'
|
||||
|
@ -61,7 +61,7 @@ const ProfileScreen = defineAsyncComponent(() => import('@/components/screens/Pr
|
|||
const YoutubeScreen = defineAsyncComponent(() => import('@/components/screens/YouTubeScreen.vue'))
|
||||
const SearchSongResultsScreen = defineAsyncComponent(() => import('@/components/screens/search/SearchSongResultsScreen.vue'))
|
||||
const NotFoundScreen = defineAsyncComponent(() => import('@/components/screens/NotFoundScreen.vue'))
|
||||
const Visualizer = defineAsyncComponent(() => import('@/components/ui/Visualizer.vue'))
|
||||
const VisualizerScreen = defineAsyncComponent(() => import('@/components/screens/VisualizerScreen.vue'))
|
||||
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
|
||||
|
@ -69,13 +69,10 @@ const router = requireInjection(RouterKey)
|
|||
const currentSong = requireInjection(CurrentSongKey, ref(null))
|
||||
|
||||
const showAlbumArtOverlay = toRef(preferenceStore.state, 'showAlbumArtOverlay')
|
||||
const showingVisualizer = ref(false)
|
||||
const screen = ref<ScreenName>('Home')
|
||||
|
||||
router.onRouteChanged(route => (screen.value = route.screen))
|
||||
|
||||
eventBus.on('TOGGLE_VISUALIZER', () => (showingVisualizer.value = !showingVisualizer.value))
|
||||
|
||||
onMounted(() => router.resolve())
|
||||
</script>
|
||||
|
||||
|
|
148
resources/assets/js/components/screens/VisualizerScreen.vue
Normal file
148
resources/assets/js/components/screens/VisualizerScreen.vue
Normal file
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<section id="vizContainer" :class="{ fullscreen: isFullscreen }" @dblclick="toggleFullscreen">
|
||||
<div class="artifacts">
|
||||
<div class="credits" v-if="selectedVisualizer">
|
||||
<h3>{{ selectedVisualizer.name }}</h3>
|
||||
<p class="text-secondary" v-if="selectedVisualizer.credits">
|
||||
by {{ selectedVisualizer.credits.author }}
|
||||
<a :href="selectedVisualizer.credits.url" target="_blank">
|
||||
<icon :icon="faUpRightFromSquare"/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<select v-model="selectedId">
|
||||
<option disabled value="-1">Pick a visualizer</option>
|
||||
<option v-for="v in visualizers" :key="v.id" :value="v.id">{{ v.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div ref="el" class="viz"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { logger } from '@/utils'
|
||||
import { preferenceStore as preferences, visualizerStore } from '@/stores'
|
||||
|
||||
const visualizers = visualizerStore.all
|
||||
let destroyVisualizer: () => void
|
||||
|
||||
const el = ref<HTMLElement>()
|
||||
const selectedId = ref<Visualizer['id']>()
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const render = async (viz: Visualizer) => {
|
||||
if (!el.value) {
|
||||
await nextTick()
|
||||
await render(viz)
|
||||
}
|
||||
|
||||
freeUp()
|
||||
|
||||
try {
|
||||
destroyVisualizer = await viz.init(el.value!)
|
||||
} catch (e) {
|
||||
// in e.g., DOM testing, the call will fail due to the lack of proper API support
|
||||
logger.warn('Failed to initialize visualizer', e)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedVisualizer = ref<Visualizer>()
|
||||
|
||||
watch(selectedId, id => {
|
||||
preferences.visualizer = id
|
||||
selectedVisualizer.value = visualizerStore.getVisualizerById(id || 'default')!
|
||||
render(selectedVisualizer.value)
|
||||
})
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value ? document.exitFullscreen() : el.value?.requestFullscreen()
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectedId.value = preferences.visualizer || 'default'
|
||||
|
||||
if (!visualizerStore.getVisualizerById(selectedId.value)) {
|
||||
selectedId.value = 'default'
|
||||
}
|
||||
})
|
||||
|
||||
const freeUp = () => {
|
||||
destroyVisualizer?.()
|
||||
el.value && (el.value.innerHTML = '')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => freeUp())
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#vizContainer {
|
||||
.viz {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
positioN: absolute;
|
||||
z-index: 0;
|
||||
|
||||
canvas {
|
||||
transition: opacity 0.3s;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.artifacts {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
padding: 24px;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.artifacts {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.credits {
|
||||
padding: 14px 28px 14px 14px;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: .5rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
// :fullscreen pseudo support is kind of buggy, so we use a class instead.
|
||||
background: var(--color-bg-primary);
|
||||
|
||||
.close {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
id="vizContainer"
|
||||
ref="el"
|
||||
:class="{ fullscreen: isFullscreen }"
|
||||
data-testid="visualizer"
|
||||
@dblclick="toggleFullscreen"
|
||||
>
|
||||
<CloseModalBtn class="close" @click="hide"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import initVisualizer from '@/utils/visualizer'
|
||||
import { eventBus, logger } from '@/utils'
|
||||
|
||||
import CloseModalBtn from '@/components/ui/BtnCloseModal.vue'
|
||||
|
||||
const el = ref<HTMLElement>()
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value ? document.exitFullscreen() : el.value?.requestFullscreen()
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
const hide = () => eventBus.emit('TOGGLE_VISUALIZER')
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
initVisualizer(el.value!)
|
||||
} catch (e) {
|
||||
logger.warn('Failed to initialize visualizer', e)
|
||||
// in e.g., DOM testing, the call will fail due to the lack of proper API support
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#vizContainer {
|
||||
position: relative;
|
||||
|
||||
&.fullscreen {
|
||||
// :fullscreen pseudo support is kind of buggy, so we use a class instead.
|
||||
background: var(--color-bg-primary);
|
||||
|
||||
.close {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.close {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -6,7 +6,6 @@ export type EventName =
|
|||
| 'FOCUS_SEARCH_FIELD'
|
||||
| 'PLAY_YOUTUBE_VIDEO'
|
||||
| 'INIT_EQUALIZER'
|
||||
| 'TOGGLE_VISUALIZER'
|
||||
| 'SEARCH_KEYWORDS_CHANGED'
|
||||
|
||||
| 'SONG_CONTEXT_MENU_REQUESTED'
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from './acceptedMediaTypes'
|
|||
export * from './genres'
|
||||
export * from './routes'
|
||||
export * from './audio'
|
||||
export * from './visualizers'
|
||||
|
|
|
@ -90,6 +90,10 @@ export const routes: Route[] = [
|
|||
path: '/genres/(?<name>\.+)',
|
||||
screen: 'Genre'
|
||||
},
|
||||
{
|
||||
path: '/visualizer',
|
||||
screen: 'Visualizer'
|
||||
},
|
||||
{
|
||||
path: '/song/(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
|
||||
screen: 'Queue',
|
||||
|
|
38
resources/assets/js/config/visualizers.ts
Normal file
38
resources/assets/js/config/visualizers.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
export const visualizers: Visualizer[] = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Color Pills',
|
||||
init: async (container) => (await import('@/visualizers/default')).init(container),
|
||||
credits: {
|
||||
author: 'Justin Windle (@soulwire)',
|
||||
url: 'https://codepen.io/soulwire/pen/Dscga'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'plane-mesh',
|
||||
name: 'Plane Mesh',
|
||||
init: async (container) => (await import('@/visualizers/plane-mesh')).init(container),
|
||||
credits: {
|
||||
author: 'Steven Marelly (@l1ve4code)',
|
||||
url: 'https://github.com/l1ve4code/3d-music-visualizer'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'waveform',
|
||||
name: 'Waveform',
|
||||
init: async (container) => (await import('@/visualizers/waveform')).init(container),
|
||||
credits: {
|
||||
author: 'Suboptimal Engineer (@SuboptimalEng)',
|
||||
url: 'https://github.com/SuboptimalEng/gamedex/tree/main/audio-visualizer'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'fluid-cube',
|
||||
name: 'Fluid Cube',
|
||||
init: async (container) => (await import('@/visualizers/fluid-cube')).init(container),
|
||||
credits: {
|
||||
author: 'Radik (@H2xDev)',
|
||||
url: 'https://codepen.io/H2xDev/pen/rRRGbv'
|
||||
}
|
||||
}
|
||||
]
|
|
@ -15,6 +15,7 @@ export const audioService = {
|
|||
source: null as unknown as MediaElementAudioSourceNode,
|
||||
element: null as unknown as HTMLMediaElement,
|
||||
preampGainNode: null as unknown as GainNode,
|
||||
analyzer: null as unknown as AnalyserNode,
|
||||
|
||||
bands: [] as Band[],
|
||||
|
||||
|
@ -24,7 +25,10 @@ export const audioService = {
|
|||
this.context = new AudioContext()
|
||||
this.preampGainNode = this.context.createGain()
|
||||
this.source = this.context.createMediaElementSource(this.element)
|
||||
this.source.connect(this.preampGainNode)
|
||||
this.analyzer = this.context.createAnalyser()
|
||||
|
||||
this.source.connect(this.analyzer)
|
||||
this.analyzer.connect(this.preampGainNode)
|
||||
|
||||
const config = equalizerStore.getConfig()
|
||||
|
||||
|
|
|
@ -15,3 +15,4 @@ export * from './songStore'
|
|||
export * from './themeStore'
|
||||
export * from './userStore'
|
||||
export * from './genreStore'
|
||||
export * from './visualizerStore'
|
||||
|
|
|
@ -14,6 +14,7 @@ interface Preferences extends Record<string, any> {
|
|||
showAlbumArtOverlay: boolean
|
||||
lyricsZoomLevel: number | null
|
||||
theme?: Theme['id'] | null
|
||||
visualizer?: Visualizer['id'] | null
|
||||
}
|
||||
|
||||
const preferenceStore = {
|
||||
|
@ -37,7 +38,8 @@ const preferenceStore = {
|
|||
supportBarNoBugging: false,
|
||||
showAlbumArtOverlay: true,
|
||||
lyricsZoomLevel: 1,
|
||||
theme: null
|
||||
theme: null,
|
||||
visualizer: 'default'
|
||||
}),
|
||||
|
||||
init (user: User): void {
|
||||
|
|
12
resources/assets/js/stores/visualizerStore.ts
Normal file
12
resources/assets/js/stores/visualizerStore.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { visualizers } from '@/config'
|
||||
|
||||
export const visualizerStore = {
|
||||
get all () {
|
||||
return visualizers
|
||||
},
|
||||
|
||||
getVisualizerById (id: string) {
|
||||
return visualizers.find(visualizer => visualizer.id === id)
|
||||
}
|
||||
}
|
||||
|
10
resources/assets/js/types.d.ts
vendored
10
resources/assets/js/types.d.ts
vendored
|
@ -389,3 +389,13 @@ type Genre = {
|
|||
}
|
||||
|
||||
type ExtraPanelTab = 'Lyrics' | 'Artist' | 'Album' | 'YouTube'
|
||||
|
||||
type Visualizer = {
|
||||
init: (container: HTMLElement) => Promise<Closure>
|
||||
id: string
|
||||
name: string
|
||||
credits?: {
|
||||
author: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import Sketch from 'sketch-js'
|
||||
import { audioService } from '@/services'
|
||||
import { random, sample } from 'lodash'
|
||||
|
||||
// Audio visualization originally created by Justin Windle (@soulwire)
|
||||
// as seen on https://codepen.io/soulwire/pen/Dscga
|
||||
import { noop } from '@/utils'
|
||||
|
||||
const NUM_PARTICLES = 128
|
||||
const NUM_BANDS = 128
|
||||
|
@ -48,13 +46,12 @@ class AudioAnalyser {
|
|||
this.audio = audioService.element
|
||||
this.source = audioService.source
|
||||
|
||||
this.analyser = audioService.context.createAnalyser()
|
||||
this.analyser = audioService.analyzer
|
||||
this.analyser.smoothingTimeConstant = this.smoothing
|
||||
this.analyser.fftSize = this.bandCount * 2
|
||||
|
||||
this.bands = new Uint8Array(this.analyser.frequencyBinCount)
|
||||
|
||||
this.source.connect(this.analyser)
|
||||
this.update()
|
||||
}
|
||||
|
||||
|
@ -149,7 +146,7 @@ class Particle {
|
|||
}
|
||||
}
|
||||
|
||||
export default (container: HTMLElement) => {
|
||||
export const init = (container: HTMLElement) => {
|
||||
const particles: Particle[] = []
|
||||
|
||||
Sketch.create({
|
||||
|
@ -181,4 +178,6 @@ export default (container: HTMLElement) => {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
return noop
|
||||
}
|
116
resources/assets/js/visualizers/fluid-cube/index.ts
Normal file
116
resources/assets/js/visualizers/fluid-cube/index.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { audioService } from '@/services'
|
||||
import { logger } from '@/utils'
|
||||
import shaders from './shaders'
|
||||
|
||||
export const init = async (container: HTMLElement) => {
|
||||
const gl = document.createElement('canvas').getContext('webgl')!
|
||||
const postctx = container.appendChild(document.createElement('canvas')).getContext('2d')!
|
||||
const postprocess = postctx.canvas
|
||||
const canvas = gl.canvas
|
||||
const cubeSize = 15
|
||||
|
||||
const analyzer = audioService.analyzer
|
||||
analyzer.smoothingTimeConstant = 0.2
|
||||
analyzer.fftSize = 128
|
||||
|
||||
let spectrumData = new Uint8Array(analyzer.frequencyBinCount)
|
||||
|
||||
const compileShader = (type, source) => {
|
||||
const shader = gl.createShader(type)!
|
||||
gl.shaderSource(shader, source)
|
||||
gl.compileShader(shader)
|
||||
|
||||
const status = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
|
||||
if (status) return shader
|
||||
|
||||
logger.error('shader compile error', gl.getShaderInfoLog(shader))
|
||||
gl.deleteShader(shader)
|
||||
}
|
||||
|
||||
const createProgram = function (vertexShader, fragmentShader) {
|
||||
const program = gl.createProgram()!
|
||||
gl.attachShader(program, vertexShader)
|
||||
gl.attachShader(program, fragmentShader)
|
||||
gl.linkProgram(program)
|
||||
|
||||
const status = gl.getProgramParameter(program, gl.LINK_STATUS)
|
||||
if (status) return program
|
||||
|
||||
logger.error('program link error', gl.getProgramInfoLog(program))
|
||||
gl.deleteProgram(program)
|
||||
}
|
||||
|
||||
const vertexShader = compileShader(gl.VERTEX_SHADER, shaders.vertex(cubeSize))
|
||||
const fragmentShader = compileShader(gl.FRAGMENT_SHADER, shaders.fragment())
|
||||
|
||||
const program = createProgram(vertexShader, fragmentShader)!
|
||||
|
||||
const aPosition = gl.getAttribLocation(program, 'a_pos')
|
||||
const uResolution = gl.getUniformLocation(program, 'u_res')
|
||||
const uFrame = gl.getUniformLocation(program, 'u_frame')
|
||||
const uSpectrumValue = gl.getUniformLocation(program, 'u_spectrumValue')
|
||||
|
||||
const vertices: number[] = []
|
||||
const vertexBuffer = gl.createBuffer()
|
||||
let frame = 0
|
||||
|
||||
const render = () => {
|
||||
frame++
|
||||
|
||||
analyzer.getByteFrequencyData(spectrumData)
|
||||
|
||||
// Transfer spectrum data to shader program
|
||||
gl.uniform1iv(uSpectrumValue, spectrumData)
|
||||
|
||||
if (postprocess.width !== postprocess.offsetWidth || postprocess.height !== postprocess.offsetHeight) {
|
||||
postprocess.width = postprocess.offsetWidth
|
||||
postprocess.height = postprocess.offsetHeight
|
||||
canvas.width = postprocess.width
|
||||
canvas.height = postprocess.height
|
||||
gl.uniform2fv(uResolution, [canvas.width, canvas.height])
|
||||
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
gl.uniform1f(uFrame, frame)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.drawArrays(gl.POINTS, 0, vertices.length / 3)
|
||||
|
||||
// Make Bloom
|
||||
postctx.globalAlpha = 1
|
||||
postctx.drawImage(canvas, 0, 0)
|
||||
postctx.filter = 'blur(4px)'
|
||||
postctx.globalCompositeOperation = 'screen'
|
||||
postctx.drawImage(canvas, 0, 0)
|
||||
postctx.globalCompositeOperation = 'source-over'
|
||||
postctx.filter = 'blur(0)'
|
||||
|
||||
requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
gl.clearColor(0, 0, 0, 1)
|
||||
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||
gl.useProgram(program)
|
||||
gl.uniform2fv(uResolution, new Float32Array([canvas.width, canvas.height]))
|
||||
|
||||
for (let i = 0; i < cubeSize ** 3; i++) {
|
||||
let x = (i % cubeSize)
|
||||
let y = Math.floor(i / cubeSize) % cubeSize
|
||||
let z = Math.floor(i / cubeSize ** 2)
|
||||
x -= cubeSize / 2 - 0.5
|
||||
y -= cubeSize / 2 - 0.5
|
||||
z -= cubeSize / 2 - 0.5
|
||||
|
||||
vertices.push(x)
|
||||
vertices.push(y)
|
||||
vertices.push(z)
|
||||
}
|
||||
|
||||
gl.enableVertexAttribArray(aPosition)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
|
||||
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)
|
||||
|
||||
render()
|
||||
|
||||
return () => gl?.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
110
resources/assets/js/visualizers/fluid-cube/shaders.ts
Normal file
110
resources/assets/js/visualizers/fluid-cube/shaders.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
export default {
|
||||
vertex: (cubeSize: number) => `
|
||||
attribute vec3 a_pos;
|
||||
uniform vec2 u_res;
|
||||
uniform float u_frame;
|
||||
uniform int u_spectrumValue[128];
|
||||
varying float v_frame;
|
||||
varying vec3 vv_pos;
|
||||
void main () {
|
||||
v_frame = u_frame;
|
||||
float pi = 3.141592653589793;
|
||||
float rad = u_frame / 2.0 / 180.0 * pi;
|
||||
int spectrumIndex = 12 + int(mod(a_pos.x + ${Math.floor(cubeSize / 2)}.0, ${cubeSize}.0) + mod(a_pos.y + ${Math.floor(cubeSize / 2)}.0, ${cubeSize ** 2}.0) + (a_pos.z + ${Math.floor(cubeSize / 2)}.0) / ${cubeSize ** 2}.0);
|
||||
float value = float(u_spectrumValue[spectrumIndex]) / 100.0;
|
||||
vec3 v_pos = a_pos;
|
||||
vec3 t = vec3(1, 1, 1);
|
||||
|
||||
vv_pos = v_pos;
|
||||
float dist = abs(${Math.floor(cubeSize / 2)}.0 - sqrt(vv_pos.x * vv_pos.x + vv_pos.y * vv_pos.y + vv_pos.z * vv_pos.z));
|
||||
|
||||
t.x = v_pos.x * cos(rad) + v_pos.z * sin(rad);
|
||||
t.y = v_pos.y;
|
||||
t.z = - v_pos.x * sin(rad) + v_pos.z * cos(rad);
|
||||
|
||||
v_pos = t;
|
||||
|
||||
|
||||
t.x = v_pos.x * cos(rad) - v_pos.y * sin(rad);
|
||||
t.y = v_pos.x * sin(rad) + v_pos.y * cos(rad);
|
||||
t.z = v_pos.z;
|
||||
|
||||
v_pos = t;
|
||||
|
||||
t.x = v_pos.x;
|
||||
t.y = v_pos.y * cos(rad) - v_pos.z * sin(rad);
|
||||
t.z = v_pos.y * sin(rad) + v_pos.z * cos(rad);
|
||||
|
||||
v_pos = t;
|
||||
|
||||
v_pos.z -= 20.0;
|
||||
|
||||
// Make reaction on spectrum
|
||||
v_pos.z += value * dist;
|
||||
v_pos.y += value / 100.0;
|
||||
|
||||
v_pos.x += sin(u_frame / 30.0 + v_pos.y / 4.0) * 1.2;
|
||||
v_pos.y += cos(u_frame / 20.0 + v_pos.z / 5.0) * 1.0;
|
||||
|
||||
|
||||
v_pos.x /= v_pos.z;
|
||||
v_pos.y /= v_pos.z;
|
||||
|
||||
v_pos.x /= u_res.x / u_res.y;
|
||||
|
||||
gl_Position = vec4(v_pos.xy, 0.0, 1.0);
|
||||
gl_PointSize = dist;
|
||||
}`,
|
||||
fragment: () => `
|
||||
precision mediump float;
|
||||
uniform vec4 u_color;
|
||||
varying float v_frame;
|
||||
varying vec3 vv_pos;
|
||||
float hue2rgb(float f1, float f2, float hue) {
|
||||
if (hue < 0.0)
|
||||
hue += 1.0;
|
||||
else if (hue > 1.0)
|
||||
hue -= 1.0;
|
||||
float res;
|
||||
if ((6.0 * hue) < 1.0)
|
||||
res = f1 + (f2 - f1) * 6.0 * hue;
|
||||
else if ((2.0 * hue) < 1.0)
|
||||
res = f2;
|
||||
else if ((3.0 * hue) < 2.0)
|
||||
res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;
|
||||
else
|
||||
res = f1;
|
||||
return res;
|
||||
}
|
||||
|
||||
vec3 hsl2rgb(vec3 hsl) {
|
||||
vec3 rgb;
|
||||
|
||||
if (hsl.y == 0.0) {
|
||||
rgb = vec3(hsl.z); // Luminance
|
||||
} else {
|
||||
float f2;
|
||||
|
||||
if (hsl.z < 0.5)
|
||||
f2 = hsl.z * (1.0 + hsl.y);
|
||||
else
|
||||
f2 = hsl.z + hsl.y - hsl.y * hsl.z;
|
||||
|
||||
float f1 = 2.0 * hsl.z - f2;
|
||||
|
||||
rgb.r = hue2rgb(f1, f2, hsl.x + (1.0/3.0));
|
||||
rgb.g = hue2rgb(f1, f2, hsl.x);
|
||||
rgb.b = hue2rgb(f1, f2, hsl.x - (1.0/3.0));
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
vec3 hsl2rgb(float h, float s, float l) {
|
||||
return hsl2rgb(vec3(h, s, l));
|
||||
}
|
||||
void main () {
|
||||
float dist = sqrt(vv_pos.x * vv_pos.x + vv_pos.y * vv_pos.y + vv_pos.z * vv_pos.z);
|
||||
float i_frame = mod(v_frame + dist * 20.0, 360.0);
|
||||
gl_FragColor = vec4(hsl2rgb((i_frame) / 360.0, 1.0, .5), 1.0);
|
||||
}`
|
||||
}
|
121
resources/assets/js/visualizers/plane-mesh/index.ts
Normal file
121
resources/assets/js/visualizers/plane-mesh/index.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import * as THREE from 'three'
|
||||
import shaders from './shaders'
|
||||
import planeMeshParameters from './planeMeshParameters'
|
||||
import { audioService } from '@/services'
|
||||
|
||||
export const init = (container: HTMLElement) => {
|
||||
const uniforms = {
|
||||
u_time: {
|
||||
type: 'f',
|
||||
value: 2.0
|
||||
},
|
||||
u_amplitude: {
|
||||
type: 'f',
|
||||
value: 4.0
|
||||
},
|
||||
u_data_arr: {
|
||||
type: 'float[64]',
|
||||
value: new Uint8Array()
|
||||
}
|
||||
}
|
||||
|
||||
const analyser = audioService.analyzer
|
||||
|
||||
analyser.fftSize = 1024
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount)
|
||||
|
||||
const width = container.clientWidth
|
||||
const height = container.clientHeight
|
||||
const scene = new THREE.Scene()
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xaaaaaa)
|
||||
ambientLight.castShadow = false
|
||||
|
||||
const spotLight = new THREE.SpotLight(0xffffff)
|
||||
spotLight.intensity = 0.9
|
||||
spotLight.position.set(-10, 40, 20)
|
||||
spotLight.castShadow = true
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
85,
|
||||
width / height,
|
||||
1,
|
||||
1000
|
||||
)
|
||||
camera.position.z = 80
|
||||
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
renderer.setSize(width, height)
|
||||
renderer.setClearAlpha(0)
|
||||
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
const planeGeometry = new THREE.PlaneGeometry(64, 64, 64, 64)
|
||||
const planeMaterial = new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: shaders.vertex,
|
||||
fragmentShader: shaders.fragment,
|
||||
wireframe: true
|
||||
})
|
||||
|
||||
planeMeshParameters.forEach(item => {
|
||||
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial)
|
||||
|
||||
if (item.rotation.x == undefined) {
|
||||
planeMesh.rotation.y = item.rotation.y
|
||||
} else {
|
||||
planeMesh.rotation.x = item.rotation.x
|
||||
}
|
||||
|
||||
planeMesh.scale.x = item.scale
|
||||
planeMesh.scale.y = item.scale
|
||||
planeMesh.scale.z = item.scale
|
||||
planeMesh.position.x = item.position.x
|
||||
planeMesh.position.y = item.position.y
|
||||
planeMesh.position.z = item.position.z
|
||||
|
||||
scene.add(planeMesh)
|
||||
})
|
||||
|
||||
scene.add(ambientLight)
|
||||
scene.add(spotLight)
|
||||
|
||||
const render = () => {
|
||||
analyser.getByteFrequencyData(dataArray)
|
||||
uniforms.u_data_arr.value = dataArray
|
||||
camera.rotation.z += 0.001
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate)
|
||||
render()
|
||||
}
|
||||
|
||||
const windowResizeHandler = () => {
|
||||
const width = container.clientWidth
|
||||
const height = container.clientHeight
|
||||
renderer.setSize(width, height)
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.domElement.width = width
|
||||
renderer.domElement.height = height
|
||||
}
|
||||
|
||||
const wheelHandler = event => {
|
||||
const val = camera.position.z + event.deltaY / 100
|
||||
camera.position.z = Math.min(Math.max(val, 0), 256)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', windowResizeHandler)
|
||||
document.addEventListener('wheel', wheelHandler)
|
||||
|
||||
animate()
|
||||
|
||||
return () => {
|
||||
renderer.domElement.remove()
|
||||
renderer.dispose()
|
||||
window.removeEventListener('resize', windowResizeHandler)
|
||||
document.removeEventListener('wheel', wheelHandler)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
const planeMeshParameters = [
|
||||
{
|
||||
rotation: {
|
||||
x: -Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 40,
|
||||
z: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
x: Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: 0,
|
||||
y: -40,
|
||||
z: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
y: Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: 40,
|
||||
y: 0,
|
||||
z: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
y: -Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: -40,
|
||||
y: 0,
|
||||
z: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
x: -Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 40,
|
||||
z: -118
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
x: Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: 0,
|
||||
y: -40,
|
||||
z: -118
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
y: Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: 40,
|
||||
y: 0,
|
||||
z: -118
|
||||
}
|
||||
},
|
||||
{
|
||||
rotation: {
|
||||
y: -Math.PI / 2
|
||||
},
|
||||
scale: 2,
|
||||
position: {
|
||||
x: -40,
|
||||
y: 0,
|
||||
z: -118
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default planeMeshParameters
|
36
resources/assets/js/visualizers/plane-mesh/shaders.ts
Normal file
36
resources/assets/js/visualizers/plane-mesh/shaders.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
const shaders = {
|
||||
vertex: `
|
||||
varying float x;
|
||||
varying float y;
|
||||
varying float z;
|
||||
varying vec3 vUv;
|
||||
uniform float u_time;
|
||||
uniform float u_amplitude;
|
||||
uniform float[64] u_data_arr;
|
||||
void main() {
|
||||
vUv = position;
|
||||
x = abs(position.x);
|
||||
y = abs(position.y);
|
||||
float floor_x = round(x);
|
||||
float floor_y = round(y);
|
||||
float x_multiplier = (64.0 - x) / 4.0;
|
||||
float y_multiplier = (64.0 - y) / 4.0;
|
||||
z = sin(u_data_arr[int(floor_x)] / 40.0 + u_data_arr[int(floor_y)] / 40.0) * u_amplitude;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, z, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying float x;
|
||||
varying float y;
|
||||
varying float z;
|
||||
varying vec3 vUv;
|
||||
uniform float u_time;
|
||||
uniform float[64] u_data_arr;
|
||||
void main() {
|
||||
// gl_FragColor = vec4((u_data_arr[32])/205.0, 0, (u_data_arr[8])/205.0, 1.0);
|
||||
gl_FragColor = vec4((64.0 - abs(x)) / 32.0, (32.0 - abs(y)) / 32.0, (abs(x + y) / 2.0) / 32.0, 1.0);
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export default shaders
|
55
resources/assets/js/visualizers/waveform/index.ts
Normal file
55
resources/assets/js/visualizers/waveform/index.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import * as THREE from 'three'
|
||||
import SceneInit from './lib/SceneInit'
|
||||
import shaders from './lib/shaders'
|
||||
import { audioService } from '@/services'
|
||||
|
||||
export const init = (container: HTMLElement) => {
|
||||
const sceneInit = new SceneInit(container)
|
||||
|
||||
const analyser = audioService.analyzer
|
||||
analyser.fftSize = 512
|
||||
let dataArray = new Uint8Array(analyser.frequencyBinCount)
|
||||
|
||||
const uniforms = {
|
||||
u_time: {
|
||||
type: 'f',
|
||||
value: 1.0
|
||||
},
|
||||
u_amplitude: {
|
||||
type: 'f',
|
||||
value: 3.0
|
||||
},
|
||||
u_data_arr: {
|
||||
type: 'float[64]',
|
||||
value: dataArray
|
||||
}
|
||||
}
|
||||
|
||||
const planeGeometry = new THREE.PlaneGeometry(64, 64, 64, 64)
|
||||
|
||||
const planeCustomMaterial = new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: shaders.vertex,
|
||||
fragmentShader: shaders.fragment,
|
||||
wireframe: true
|
||||
})
|
||||
|
||||
const planeMesh = new THREE.Mesh(planeGeometry, planeCustomMaterial)
|
||||
planeMesh.rotation.x = -Math.PI / 2 + Math.PI / 4
|
||||
planeMesh.scale.x = 2
|
||||
planeMesh.scale.y = 2
|
||||
planeMesh.scale.z = 2
|
||||
planeMesh.position.y = 8
|
||||
sceneInit.scene.add(planeMesh)
|
||||
|
||||
const getByteFrequency = () => {
|
||||
analyser.getByteFrequencyData(dataArray)
|
||||
requestAnimationFrame(getByteFrequency)
|
||||
}
|
||||
|
||||
getByteFrequency()
|
||||
|
||||
sceneInit.animate()
|
||||
|
||||
return () => sceneInit.destroy()
|
||||
}
|
83
resources/assets/js/visualizers/waveform/lib/SceneInit.ts
Normal file
83
resources/assets/js/visualizers/waveform/lib/SceneInit.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
export default class SceneInit {
|
||||
private readonly fov: number
|
||||
private container: HTMLElement
|
||||
private readonly camera: THREE.PerspectiveCamera
|
||||
private clock: THREE.Clock
|
||||
public scene: THREE.Scene
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private uniforms: any
|
||||
private readonly onWindowResize: () => void
|
||||
private readonly onDocumentWheel: (e: WheelEvent) => void
|
||||
|
||||
constructor (container: HTMLElement, fov = 36) {
|
||||
this.container = container
|
||||
this.fov = fov
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
this.fov,
|
||||
this.container.clientWidth / this.container.clientHeight,
|
||||
1,
|
||||
1000
|
||||
)
|
||||
|
||||
this.camera.position.z = 128
|
||||
|
||||
this.clock = new THREE.Clock()
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.uniforms = {
|
||||
u_time: { type: 'f', value: 1.0 },
|
||||
colorB: { type: 'vec3', value: new THREE.Color(0xfff000) },
|
||||
colorA: { type: 'vec3', value: new THREE.Color(0xffffff) }
|
||||
}
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
antialias: true
|
||||
})
|
||||
|
||||
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
|
||||
container.appendChild(this.renderer.domElement)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
|
||||
ambientLight.castShadow = false
|
||||
this.scene.add(ambientLight)
|
||||
|
||||
const spotLight = new THREE.SpotLight(0xffffff, 0.55)
|
||||
spotLight.castShadow = true
|
||||
spotLight.position.set(0, 80, 10)
|
||||
this.scene.add(spotLight)
|
||||
|
||||
this.onWindowResize = () => {
|
||||
this.camera.aspect = this.container.clientWidth / this.container.clientHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
|
||||
}
|
||||
|
||||
this.onDocumentWheel = (event: WheelEvent) => {
|
||||
const val = this.camera.position.z + event.deltaY / 100
|
||||
this.camera.position.z = Math.min(Math.max(val, 84), 256)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onWindowResize, false)
|
||||
document.addEventListener('wheel', this.onDocumentWheel, false)
|
||||
}
|
||||
|
||||
animate () {
|
||||
requestAnimationFrame(this.animate.bind(this))
|
||||
this.render()
|
||||
}
|
||||
|
||||
render () {
|
||||
this.uniforms.u_time.value += this.clock.getDelta()
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
window.removeEventListener('resize', this.onWindowResize, false)
|
||||
document.removeEventListener('wheel', this.onDocumentWheel, false)
|
||||
this.renderer.domElement.remove()
|
||||
this.renderer.dispose()
|
||||
}
|
||||
}
|
30
resources/assets/js/visualizers/waveform/lib/shaders.ts
Normal file
30
resources/assets/js/visualizers/waveform/lib/shaders.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export default {
|
||||
vertex: `
|
||||
varying float x;
|
||||
varying float y;
|
||||
varying float z;
|
||||
varying vec3 vUv;
|
||||
uniform float u_time;
|
||||
uniform float u_amplitude;
|
||||
uniform float[64] u_data_arr;
|
||||
void main() {
|
||||
vUv = position;
|
||||
x = abs(position.x);
|
||||
y = abs(position.y);
|
||||
float floor_x = round(x);
|
||||
float floor_y = round(y);
|
||||
z = sin(u_data_arr[int(floor_x)] / 50.0 + u_data_arr[int(floor_y)] / 20.0) * u_amplitude * 2.0;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, z, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying float x;
|
||||
varying float y;
|
||||
varying float z;
|
||||
varying vec3 vUv;
|
||||
uniform float u_time;
|
||||
void main() {
|
||||
gl_FragColor = vec4((32.0 - abs(x)) / 32.0, (32.0 - abs(y)) / 32.0, (abs(x + y) / 2.0) / 32.0, 1.0);
|
||||
}
|
||||
`
|
||||
}
|
17
yarn.lock
17
yarn.lock
|
@ -1291,6 +1291,18 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
|
||||
integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
|
||||
|
||||
"@types/three@^0.144.0":
|
||||
version "0.144.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/three/-/three-0.144.0.tgz#a154f40122dbc3668c5424a5373f3965c6564557"
|
||||
integrity sha512-psvEs6q5rLN50jUYZ3D4pZMfxTbdt3A243blt0my7/NcL6chaCZpHe2csbCtx0SOD9fI/XnF3wnVUAYZGqCSYg==
|
||||
dependencies:
|
||||
"@types/webxr" "*"
|
||||
|
||||
"@types/webxr@*":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.0.tgz#aae1cef3210d88fd4204f8c33385a0bbc4da07c9"
|
||||
integrity sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
|
||||
|
@ -6003,6 +6015,11 @@ text-table@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
|
||||
three@^0.146.0:
|
||||
version "0.146.0"
|
||||
resolved "https://registry.yarnpkg.com/three/-/three-0.146.0.tgz#fd80f0d128ab4bb821a02191ae241e4e6326f17a"
|
||||
integrity sha512-1lvNfLezN6OJ9NaFAhfX4sm5e9YCzHtaRgZ1+B4C+Hv6TibRMsuBAM5/wVKzxjpYIlMymvgsHEFrrigEfXnb2A==
|
||||
|
||||
throttleit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
|
|
Loading…
Reference in a new issue