preface

Now almost all video websites have the function of bullet screen, so today we use native JavaScript to encapsulate a bullet screen class. This class wants the following attributes and instance methods:

attribute

  • elContainer node selector, container node should be absolutely positioned, set width and height
  • heightThe height of each barrage
  • modeIn barrage mode, half is half of the container height, top is one-third, and full is full
  • speedThe amount of time a bullet screen moves
  • gapWidthThe distance between the last barrage and the previous barrage

methods

  • pushDataAdd barrage metadata
  • addDataKeep adding barrage
  • startStart dispatch of barrage
  • stopStop the barrage
  • restartRestart the barrage
  • clearDataEmpty the barrage
  • closeShut down
  • openRedisplay barrage

PS: There are some self-encapsulated tool functions will not be posted, probably know the meaning of good

Initialize the

Once the JavaScript files are introduced, we want to use them as follows, starting with the default configuration.

let barrage = new Barrage({
    el: '#container'
})
Copy the code

Parameter initialization:

function Barrage(options) {
    let {
        el,
        height,
        mode,
        speed,
        gapWidth,
    } = options
    this.container = document.querySelector(el)
    this.height = height || 30
    this.speed = speed || 15000 //2000ms
    this.gapWidth = gapWidth || 20
    this.list = []
    this.mode = mode || 'half'
    this.boxSize = getBoxSize(this.container)
    this.perSpeed = Math.round(this.boxSize.width / this.speed)
    this.rows = initRows(this.boxSize, this.mode, this.height)
    this.timeoutFuncs = []
    this.indexs = []
    this.idMap = []
}
Copy the code

Take the arguments and initialize them. Let’s look at getBoxSize and initRows

function getBoxSize(box) {
    let {
        height,
        width
    } = window.getComputedStyle(box)
    return {
        height: px2num(height),
        width: px2num(width)
    }

    function px2num(str) {
        return Number(str.substring(0, str.indexOf('p')))}}Copy the code

The width and height of the box is calculated using the getComputedStyleapi, which is used to calculate the width and height of the container and will be used later.

function initRows(box, mode, height) {
    let divisor = getDivisor(mode)
    rows = Math.ceil(box.height * divisor / height)
    return rows
}

function getDivisor(mode) {
    let divisor = . 5
    switch (mode) {
        case 'half':
            divisor = . 5
            break
        case 'top':
            divisor = 1 / 3
            break;
        case 'full':
            divisor = 1;
            break
        default:
            break;
    }
    return divisor
}
Copy the code

Figure out how many lines the barrage should have based on its height. The number of lines will be used somewhere below.

Insert data

There are two ways to insert data, one is to add source data, and the other is to continuously add data. Let’s start with adding source data:

this.pushData = function (data) {

    this.initDom()
    if (getType(data) == '[object Object]') {
        // Insert a single bar
        this.pushOne(data)
    }
    if (getType(data) == '[object Array]') {
        // Insert multiple inserts
        this.pushArr(data)
    }
}

this.initDom = function () {
    if (!document.querySelector(`${el} .barrage-list`)) {
        // Register the DOM node
        for (let i = 0; i < this.rows; i++) {
            let div = document.createElement('div')
            div.classList = `barrage-list barrage-list-${i}`
            div.style.height = `The ${this.boxSize.height*getDivisor(this.mode)/this.rows}px`
            this.container.appendChild(div)
        }
    }
}
Copy the code
this.pushOne = function (data) {
    for (let i = 0; i < this.rows; i++) {
        if (!this.list[i]) this.list[i] = []

    }

    let leastRow = getLeastRow(this.list) // Get the smallest column in the barrage list, which is a two-dimensional array
    this.list[leastRow].push(data)
}
this.pushArr = function (data) {
    let list = sliceRowList(this.rows, data)
    list.forEach((item, index) = > {
        if (this.list[index]) {
            this.list[index] = this.list[index].concat(... item) }else {
            this.list[index] = item
        }
    })
}
// Cut the one-dimensional bullet-screen list into a two-dimensional array of rows based on the number of rows
function sliceRowList(rows, list) {
    let sliceList = [],
        perNum = Math.round(list.length / rows)
    for (let i = 0; i < rows; i++) {
        let arr = []
        if (i == rows - 1) {
            arr = list.slice(i * perNum)
        } else {
            i == 0 ? arr = list.slice(0, perNum) : arr = list.slice(i * perNum, (i + 1) * perNum)
        }
        sliceList.push(arr)
    }
    return sliceList
}
Copy the code

The method of continuously adding data simply calls the method of adding source data and starts scheduling

this.addData = function (data) {
    this.pushData(data)
    this.start()
}
Copy the code

Fired barrage

Let’s look at the logic of the barrage

this.start = function () {
    // Start scheduling list
    this.dispatchList(this.list)
}

this.dispatchList = function (list) {
    for (let i = 0; i < list.length; i++) {
        this.dispatchRow(list[i], i)
    }
}

this.dispatchRow = function (row, i) {
    if (!this.indexs[i] && this.indexs[i] ! = =0) {
        this.indexs[i] = 0
    }
    // The actual scheduling starts here, with an instance variable that stores the current scheduling subscript.
    if (row[this.indexs[i]]) {
        this.dispatchItem(row[this.indexs[i]], i, this.indexs[i])
    }
}
Copy the code
this.dispatchItem = function (item, i) {
    // A barrage that has been scheduled once will not be needed the next time
    if(! item ||this.idMap[item.id]) {
        return
    }
    let index = this.indexs[i]
    this.idMap[item.id] = item.id
    let div = document.createElement('div'),
        parent = document.querySelector(`${el} .barrage-list-${i}`),
        width,
        pastTime
    div.innerHTML = item.content
    div.className = 'barrage-item'
    parent.appendChild(div)
    width = getBoxSize(div).width
    div.style = `width:${width}px; display:none`
    pastTime = this.computeTime(width) // Calculate when the next barrage should appear
    // The barrage will fly for a while
    this.run(div)
    if (index > this.list[i].length - 1) {
        return
    }
    let len = this.timeoutFuncs.length
    // Record the timer and empty it later
    this.timeoutFuncs[len] = setTimeout(() = > {
        this.indexs[i] = index + 1
        // Call the next item recursively
        this.dispatchItem(this.list[i][index + 1], i, index + 1)
    }, pastTime);
}
Copy the code
// Use CSS animation, the overall is relatively smooth
this.run = function (item) {
    item.classList += ' running'
    item.style.left = "left:100%"
    item.style.display = ' '
    item.style.animation = `run The ${this.speed/1000}s linear`
    // Put a mark on the finished item
    setTimeout(() = > {
        item.classList+=' done'
    }, this.speed);
}
Copy the code
// According to the width of the barrage and gapWth, calculate the time when the next barrage should appear
this.computeTime = function (width) {
    let length = width + this.gapWidth
    let time = Math.round(length / this.boxSize.width * this.speed/2)
    return time
}
Copy the code

Animation CSS details are as follows

@keyframes run {
    0% {
        left: 100%;
    }

    50% {
        left: 0
    }

    100% {
        left: -100%; }}.run {
    animation-name: run;
}
Copy the code

The rest of the way

stop

Paused using the animation’s attribute

this.stop = function () {
    let items = document.querySelectorAll(`${el} .barrage-item`);
    [...items].forEach(item= > {
        item.className += ' pause'})}Copy the code
.pause {
    animation-play-state: paused ! important;
}
Copy the code

Start all over again

Remove the pause class

this.restart = function () {
    let items = document.querySelectorAll(`${el} .barrage-item`);
    [...items].forEach(item= > {
        removeClassName(item, 'pause')})}Copy the code

Opening and closing

Do a show hide logic

this.close = function () {
    this.container.style.display = 'none'
}
this.open = function () {
    this.container.style.display = ' '
}

Copy the code

Clean up the barrage

this.clearData = function () {
    / / remove the list
    this.list = []
    / / remove the dom
    document.querySelector(`${el}`).innerHTML = ' '
    / / remove the timeout
    this.timeoutFuncs.forEach(fun= > clearTimeout(fun))
}
Copy the code

Finally, use a timer to clear expired shells:

setInterval(() = > {
    let items = document.querySelectorAll(`${el} .done`);
    [...items].forEach(item= >{
        item.parentNode.removeChild(item)
    })
}, this.speed*5);
Copy the code

The last

If you were designing such a class, how would you design it?