Change image sprite at runtime

I’m trying to change an image source at runtime, based on some audio levels. It’s meant to act as a video playhead controller. I got everything working except the sprite doesn’t update. I have a reference to the sprite, and I’m setting it on the image component, but the visuals never update.

import { AudioSource, Behaviour, serializable, Sprite, Image } from "@needle-tools/engine"
import { AudioAnalyser, Audio } from "three"

function lerp(start, end, amt) {
  return (1 - amt) * start + amt * end

export class AudioViz extends Behaviour {

  frames: Sprite[] = []
  image?: Image

  audio?: AudioSource

  private analyser?: AudioAnalyser
  private smoothLevel: number = 0
  private didInit: boolean = false

  initAnalyzer() {
    if (! return
    this.didInit = true
    const sound = as any as Audio
    this.analyser = new AudioAnalyser(sound, 64) // fft size

  update(): void {
    if ( && !this.didInit) this.initAnalyzer()

    const freq = this.analyser?.getAverageFrequency() || 0
    this.smoothLevel = lerp(this.smoothLevel, freq / 255, .5)

      this.image.sprite = this.frames[Math.floor(this.smoothLevel * 150) % 20]

Original Post on Discord

by user 401167615826984963

I think we never did that before so it doesnt automatically update the underlying objects. I’ll check. One thing i notice that is wrong/doenst work like that I think is the @serializable(Array<Sprite>) - it should be @serializable(Sprite) (declaring the type - not the array) - it will surely not be enough to make it work tho

Good note, thank you.

One more thing I noticed is that there’s no built in event for when an audio component starts playing, so I had to do this state checking in the update handler. Would be a nice quality of life addition to emit an event for that.

by user 401167615826984963

Good point - could you maybe access the audio node directly (via the Sound property on AudioSource) ? <audio>: The Embed Audio element - HTML: HyperText Markup Language | MDN to make that work?

For the sprite as a workaround: can you try changing this:

this.image.sprite = this.frames[Math.floor(this.smoothLevel * 150) % 20]


const sprite = this.frames[Math.floor(this.smoothLevel * 150) % 20];

Hey that works! Private method?

by user 401167615826984963

Yes not exposed. I’ll add support for setting the sprite directly now too and maybe also just expose this method

Just building a little test component ^^

will be in the next update (probably tomorrow)

Awesome, thank you!

by user 401167615826984963

Here’s what I ended up with

by user 401167615826984963

Wow the glb is huge!

Nice effect :slightly_smiling_face:

lol yeah it’s all textures

by user 401167615826984963

You should probably hook into the window.resize event too ^^

did you make a production build?

how many textures are those? what resolution?

Maybe there’s a better workflow for this

by user 401167615826984963

I think they are pretty big, 150x1920x1080

by user 401167615826984963

I did make a production build

by user 401167615826984963