How can I create a javascript animation that recognizes when it hits things, in other words a hit box?

46 Views Asked by At

I am currently building a piano with visualizer in React. The users can record their song and play it back. This recording records a list of notes + start/end timestamps. I currently have note visualizations that start from the top of the screen and expand their height for end timestamp - start timestamp. Then the note falls down from the top until it is offscreen and is deleted. Both of these are done using animations with fill forward.

My problem is: I need the note sound to play when the visualizations hit the keys, and currently, basing this off of a timeout is causing me issues. It would make a lot more sense to me to base this sound off of the animated visualization hitting the key div but I am not sure how to do this.

Additionally, storing the animations for being able to pause and resume is causing me a lot of issues and I was wondering if there was a more concrete method of doing this. Pausing and resuming the animations is really finicky. I do not want to save it as a video.

Piano

Animation

export const attribute_animation = (object, attribute, start, end, duration, easing) => {
    return object.animate(
        [{[attribute]: start}, { [attribute]: end}],
        {duration: duration, fill: 'forwards', easing: easing}
    )
}

I animate first expanding the height attr, then the top attr. How can I have a specific div with ref recognize if it is hitting another div? (visualization hitting key)

Piano Component

  let [playing, set_playing] = useState(null)
  let start_note_index = useRef(0)
  let end_note_index = useRef(0)
  let song_player = useRef(null)
  let sp_start_time = useRef(null)
  let sp_anim_frameid = useRef(null)
function song_playback() {
  
      function animate(timestamp) {
        if (!sp_start_time.current) {
          sp_start_time.current = timestamp
        }
        if (start_note_index.current < song.length && 
          song[start_note_index.current]['note']['start_timestamp'] <= timestamp - sp_start_time.current) {
          
          let pkey_index = pkey_to_pkeyind[song[start_note_index.current]['note']['note']]
          set_piano(prev_state => {
            const keys = [...prev_state]
            keys[pkey_index] = React.cloneElement(keys[pkey_index], { pb_visual_mode: 'expand_down' })
            return keys
          })
          start_note_index.current += 1
        }
        if (end_note_index.current < song.length && 
          song[end_note_index.current]['note']['end_timestamp'] <= timestamp - sp_start_time.current) {
          
          let pkey_index = pkey_to_pkeyind[song[end_note_index.current]['note']['note']]
          set_piano(prev_state => {
            const keys = [...prev_state]
            keys[pkey_index] = React.cloneElement(keys[pkey_index], { pb_visual_mode: 'move_down' })
            return keys
          })
          end_note_index.current += 1
          }
          if (start_note_index.current >= song.length && end_note_index.current >= song.length) {
            set_playing(null)
            cancelAnimationFrame(sp_anim_frameid.current)
          } else {
            if (playing === 'playing') {
              sp_anim_frameid.current = requestAnimationFrame(animate)
            }
          }
        
      }
    
      function pause() {
        cancelAnimationFrame(sp_anim_frameid.current)
        set_piano(prev_state => {
          const keys = [...prev_state]
          for (let i = 0; i < keys.length; i++) {
            keys[i] = React.cloneElement(keys[i], { pb_visual_mode: 'pause' })
          }
          return keys
        })
      }
    
      function resume() {
        requestAnimationFrame(animate)
        set_piano(prev_state => {
          const keys = [...prev_state]
          for (let i = 0; i < keys.length; i++) {
            keys[i] = React.cloneElement(keys[i], { pb_visual_mode: 'resume' })
          }
          return keys
        })
      }
    
      function reset() {
        sp_start_time.current = null
        start_note_index.current = 0
        end_note_index.current = 0
      }

      return { pause, resume, reset }
    }
    song_player.current = song_playback()
  }, [song, playing])

Piano Key Component

  let [playback_visuals, set_playback_visuals] = useState([])
  let pb_counter = useRef(0)
  let curr_pb_anim = useRef([[], true])
  let playback_visual_refs = useRef({})
  let timeouts = useRef({})
  let timeouts_counter = useRef(0)
useEffect(() => {
    if (playback_visuals[pb_counter.current] && curr_pb_anim.current[1]) {
      curr_pb_anim.current[0].push(attribute_animation(playback_visual_refs.current[pb_counter.current], 'height', '0', '300000px', 1000000))
      curr_pb_anim.current[1] = false
    }
    
  }, [playback_visuals])

  useEffect(() => {
    if (pb_visual_mode === 'expand_down') {

      let curr_counter = timeouts_counter.current
      const timeout_id = new Timer((curr_counter) => {
        audio.current.play()
        delete timeouts.current[curr_counter]
      }, 2000, curr_counter)

      timeouts.current[timeouts_counter.current] = timeout_id
      timeouts_counter.current += 1

      set_playback_visuals(prev_state => {
        curr_pb_anim.current[1] = true

        return ({...prev_state,
          [pb_counter.current]: (
          <div key={`${pb_counter.current}`} ref={ref => playback_visual_refs.current[pb_counter.current] = ref} className='pb-visualizer-instance'></div>
          )
        })
      })
    } else if (pb_visual_mode === 'move_down' && curr_pb_anim.current[0]) {

      console.log(curr_pb_anim.current, note)
      curr_pb_anim.current[0][curr_pb_anim.current[0].length-1].pause()
      
      curr_pb_anim.current[0][curr_pb_anim.current[0].length-1] = attribute_animation(playback_visual_refs.current[pb_counter.current], 'top', '0', '300000px', 1000000, 'linear')
      curr_pb_anim.current[1] = true


      let curr_t_counter = timeouts_counter.current
      let curr_pb_counter = pb_counter.current
      const timeout_id = new Timer((curr_t_counter, curr_pb_counter) => {
        delete timeouts.current[curr_t_counter]
        delete playback_visual_refs.current[curr_pb_counter]
        delete curr_pb_anim.current[curr_pb_counter]
        set_playback_visuals(prev_state => {
          const new_state = Object.keys(prev_state).filter(key => key !== curr_pb_counter).reduce((acc, key) => {
            acc[key] = prev_state[key]
            return acc
          }, {})
          return new_state
        })
      }, 3000, curr_t_counter, curr_pb_counter)

      timeouts.current[timeouts_counter.current] = timeout_id
      timeouts_counter.current += 1

      pb_counter.current += 1

    } else if (pb_visual_mode === 'pause') {
      
      for (let i = 0; i < curr_pb_anim.current[0].length; i++) {
        console.log("pause", curr_pb_anim.current, note)
        curr_pb_anim.current[0][i].pause()
      }
    
      for (let timer_key in timeouts.current) {
        timeouts.current[timer_key].pause()
      }

      
      
    } else if (pb_visual_mode == 'resume') {
      for (let i = 0; i < curr_pb_anim.current[0].length; i++) {
        console.log("resume", curr_pb_anim.current, note)
        curr_pb_anim.current[0][i].play()
      }

      for (let timer_key in timeouts.current) {
        timeouts.current[timer_key].resume()
      }
    }
  }, [pb_visual_mode])
1

There are 1 best solutions below

0
The Canadian On

I experimented with observer API and setting the rootMargin to line up with the piano keys, but couldnt get it working. What I ended up doing was just doing the math to scale with the window height. Here is my code in the case anyone ends up coding something like this:

useEffect(() => {
    
    if (playback_visuals[pb_counter.current] && curr_pb_anim.current[1]) {
      const computed_style = window.getComputedStyle(playback_visual_refs.current[pb_counter.current])
      const height = parseFloat(computed_style.getPropertyValue('height'))
      console.log(key_wrapper.current.clientHeight)
      const duration = 3000 + ((key_wrapper.current.clientHeight / 300) * height)

      console.log(duration)
      const f_timeout_id = new Timer(() => {
        audio.current.play()
      }, (3000 * .75))

      let curr_pb_counter = pb_counter.current
      let curr_timeout_counter = timeouts_counter.current
      const s_timeout_id = new Timer((curr_pb_counter, curr_timeout_counter) => {
        set_playback_visuals(prev_state => {
          const new_state = Object.keys(prev_state).filter(key => key !== curr_pb_counter).reduce((acc, key) => {
            acc[key] = prev_state[key]
            return acc
          }, {})
          return new_state
        })
        delete timeouts.current[curr_timeout_counter]

        if (end_song !== null) {
          end_song(null)
        }
      }, 3000, curr_pb_counter, curr_timeout_counter)

      timeouts.current[timeouts_counter.current] = f_timeout_id
      timeouts_counter.current += 1
      timeouts.current[timeouts_counter.current] = s_timeout_id
      timeouts_counter.current += 1

      console.log(.75 * key_wrapper.current.clientHeight / duration)
      curr_pb_anim.current[0].push(attribute_animation(playback_visual_refs.current[pb_counter.current], 'top', 
      `-${height}px`,
       `${(key_wrapper.current.clientHeight * .75)}px`, duration))
      curr_pb_anim.current[1] = false
    }
    
  }, [playback_visuals])