background

Before I wrote a simple single bullet to achieve the article, for bilibili such a video site bullet, how to achieve it? How to push an input bullet screen into the current screen, select the appropriate position to push, ensure that the current bullet screen is not repeated with the bullet screen?

Problems that need to be solved

  • Multi-track bullet screen
  • Distribute the existing bullet lists evenly on multiple tracks
  • The bullet screens on each track remain non-overlapping as they move along the track
  • Some other functions, such as control of the style of the barrage, the speed of the barrage, the motion state of the barrage (whether to stop)

implementation

For the bullet screen component, it can be made into two forms: controlled component and uncontrolled component

  • Controlled components Components expose hooks that can change the internal state and behavior of control components
  • After initial properties are passed in from the outside of an uncontrolled component, the behavior and internal state of the component are managed entirely by itself

Depending on your business needs, you can choose how this component is implemented, and in this article, this component is implemented as an uncontrolled component

Component Property Definition

export interface IBarrageProps {
  // The default height of the bullet screen is 50 px
  trackHeight: number
  // The default number of bullet tracks is 4
  trackLines: number
  // Start to play the bullet callbackonStart? :() = > void
  // Finish playing the bullet screen callbackonEnd? :() = > void
  // Single bullet display time of each track. If it is an array, it means that each track defines its own bullet display time s
  duration: number | number[]
  // For the collection of bullet screens added to the bullet screen track
  barrageList: IBarrageItem[]
  TrackLineIndex: track number 1, 2, 3, 4renderItem? :(item: IBarrageItem, trackLineIndex) = >ReactNode className? :string
  // Control whether the barrage stopsautoScroll? :boolean
}
export interface IBarrageItem {
  text: stringavatar? :string
}
Copy the code

TrackHeight * trackLines is the height of the barrage container

Bullet screen movement mode

For a single bullet screen, there are two modes of motion after being pushed into a track

  • Fixed speed of the barrage

    With a preset speed of uniform motion, so that the speed of each track is the same, to be pushed into the bullet screen as long as the end of the last bullet screen in the orbit has been fully into the orbit after the push, can ensure that in the process of motion will not overlap with the last bullet screen;

  • Fixed time of the barrage

    The time between the beginning of the barrage entering the orbit and the end completely leaving the orbit is fixed for each barrageduration;

The first mode is relatively simple to implement, so we will focus on the second mode of motion

Fixed time of the barrage

After the content of each bullet screen on each track is determined, some properties of the bullet screen itself have been determined: container width + bullet screen width = distance that the bullet screen needs to move; bullet screen speed = distance that the bullet screen needs to move; bullet screen movement time: duration

The time when each bullet screen starts to be pushed is denoted as startTime

After each frame of the bullet screen where the position is determined

dt = Date.now() - startTime

The distance of the barrage movement until the current frame

distance = speed * dt

export interface IBarrageItemProps {
  // The unique id of the barrage
  key: string
  // Screen element width px
  width: number
  // The height of the barrage element is px
  height: number
  // The current offset is px
  distance: number
  // The display duration of the barrage element is s
  duration: number
  // This parameter is set after the width of the bullet screen is set
  speed: number
  // The timing of the barrage element relative to the start time ms
  startTime: number
  // Barrage list information
  item: IBarrageItem
}
Copy the code

So the problem becomes two

  • How do you know the width of a barrage before it is pushed in
  • Whether the bullet screen can be pushed into the current orbit and ensure that the bullet screen to be pushed and the existing bullet screen on the orbit do not overlap

Train of thought

How do you know the width of a barrage before it is pushed in

To know to push into the barrage of width, of course, to be rendered in the browser to get the position of the barrage size data, for the incoming barrage of tabular data, all render barrage if once, you can get all the style of the barrage data, but can bring a lot of rendering overhead, this is not acceptable, in order to solve this problem, can provide a temporary container, It is used to render the current bullet screen to be pushed, and obtain the size and position of the bullet screen. During each frame, the bullet screen to be pushed is placed in a temporary container to determine whether it can be pushed into a certain track. If so, the next frame continues the judgment of the next bullet screen; If not, the next frame continues to judge the barrage; const { width, height } = tempBarrageElementRef.current.getBoundingClientRect()

Whether the bullet screen can be pushed into the current orbit and ensure that the bullet screen to be pushed and the existing bullet screen on the orbit do not overlap

To judge whether the bullet screen can be pushed into the current orbit and ensure that the bullet screen to be pushed into the orbit and the existence of the bullet screen does not overlap, only need to judge the current bullet screen to be pushed into the current orbit and the latest bullet screen pushed into the current orbit comparison, if not overlapping judgment, then you can be pushed into the current orbit;

Nonoverlapping judgment

In fact, is the junior high school chase and problem

const checkBarrage = useCallback((trackLineIndex: number.waitPushBarrage: IBarrageItemProps): boolean= > {
    // Determine whether the bullet screen can be placed on the currently selected orbit
    // containerPos Barrage container property existBarrageList pushes the barrage data
    const currentBarrageTrack = existBarrageList[trackLineIndex]
    const lastBullet = currentBarrageTrack[currentBarrageTrack.length - 1]
    if (lastBullet && containerPos) {
      const { startTime, speed, width } = lastBullet
      const now = Date.now()
      const distance = (now - startTime) / 1000 * speed
      const rightPos = containerPos.right + width - distance
      const leftPos = containerPos.right - distance
      // The last element in the orbit is required to be in the display area with a free distance of 15px
      if (rightPos > containerPos.right - 10) {
        return false
      }

      // Basic formula: s = v * t
      const lastS = leftPos - containerPos.left + width
      const lastT = lastS / speed

      const newS = containerPos.width - 10 // Leave a space of 10px
      const newT = newS / waitPushBarrage.speed

      // Trace the problem
      if (speed < waitPushBarrage.speed && lastT > newT) {
        return false}}return true
  }, [containerPos, existBarrageList])
Copy the code
// Container properties
export interface ContainerPops {
  width: number
  height: number
  top: number
  left: number
  right: number
  bottom: number
}
Copy the code

The pushed barrage moves out of the container and should be removed

newExistBarrageList = newPositionExistBarrageList.map(trackBarrageList => trackBarrageList.filter(item => item.distance <= item.width + containerpos.width + 100)

animation

As far as animating each frame is concerned, it must be implemented with requestAnimationFrame