我目前正在用 React 构建一架带有可视化工具的钢琴。用户可以录制他们的歌曲并播放。该录音记录了笔记列表+开始/结束时间戳。我目前有从屏幕顶部开始并扩展结束时间戳 - 开始时间戳的高度的注释可视化。然后注释从顶部落下,直到超出屏幕并被删除。这两者都是使用带有前向填充的动画来完成的。
我的问题是:当可视化按下按键时,我需要播放音符声音,而目前,基于超时的这一点给我带来了问题。对我来说,将这种声音基于击中关键 div 的动画可视化更有意义,但我不知道如何做到这一点。
此外,存储动画以便能够暂停和恢复给我带来了很多问题,我想知道是否有更具体的方法来做到这一点。暂停和恢复动画确实很挑剔。我不想将其另存为视频。
动画
export const attribute_animation = (object, attribute, start, end, duration, easing) => {
return object.animate(
[{[attribute]: start}, { [attribute]: end}],
{duration: duration, fill: 'forwards', easing: easing}
)
}
我首先对高度属性进行动画处理,然后对顶部属性进行动画处理。如果某个特定的 div 碰到了另一个 div,如何通过 ref 识别它? (可视化击键)
钢琴组件
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])
钢琴琴键部件
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])
我尝试了观察者 API 并将 rootMargin 设置为与钢琴键对齐,但无法使其工作。我最终所做的只是进行数学计算以随窗口高度缩放。这是我的代码,以防有人最终编写这样的代码:
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])