UNPKG

number-rolling

Version:

用原生 js 实现的数字滚动插件

235 lines (224 loc) 8.16 kB
const maxLenNum = (aNum: string | number, bNum: string | number) => { return (aNum > bNum ? aNum : bNum).toString().length } const num2PadNumArr = (num: {toString: () => any}, len: any) => { const padLeftStr = (rawStr: string, lenNum: number): string => rawStr.length < lenNum ? padLeftStr('0' + rawStr, lenNum) : rawStr const str2NumArr = (rawStr: string) => rawStr.split('').map(Number) return str2NumArr(padLeftStr(num.toString(), len)).reverse() } const isstr = (any: any): any is string => Object.prototype.toString.call(any) === '[object String]' interface RollOptions { node: HTMLElement, from: number, easeFn?: (pos: number) => number, systemArr?: Array<string | number>, duration?: number, to?: number, direct?: boolean, separator?: string | string[], separateOnly?: number, separateEvery?: number, delay?: number, letterMargin?: number } export class Roll { private node: HTMLElement; // 最大位数就是根据这个 from 来定的 private from: number; private easeFn: (pos: number) => number; private systemArr: Array<string | number>; private beforeArr: Array<number>; private afterArr: Array<number>; private numberColumnNodeArr: Array<HTMLElement>; private duration: number; private to: number; private direct: boolean; private separator: string | string[]; private separateOnly: number; private separateEvery: number; private height?: number; private letterMargin?: number; constructor({ node, from = 0, easeFn = (pos: number) => ((pos /= 0.5) < 1 ? 0.5 * Math.pow(pos, 3) : 0.5 * (Math.pow(pos - 2, 3) + 2)), systemArr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], duration = 0.5, to, direct = true, delay, separator, separateOnly = 0, separateEvery = 3, letterMargin = 0 }: RollOptions) { // 翻转前的数组 this.beforeArr = [] // 翻转后的数组 this.afterArr = [] // 一列数字的容器数组 this.numberColumnNodeArr = [] // 花多长时间完成数字滚动 this.duration = duration * 1000 // 用户传入的转换字符串 this.systemArr = systemArr // 用户传入的移动函数 this.easeFn = easeFn // 用户最开始传入的数字 this.from = from // 用户要到达的数字 this.to = to || 0 // 数字滚动的容器 this.node = node // 数字的每位是否直接滚动到对应数字 this.direct = direct this.separator = separator this.separateOnly = separateOnly this.separateEvery = separateOnly ? 0 : separateEvery // this.letterMargin = letterMargin // 初始化 this._initHTML(maxLenNum(this.from, this.to)) // 无关紧要这个方法 this.setSelect(this.from) if (to === undefined) return if (delay) setTimeout(() => this.rollTo({to: this.to}), delay) else this.rollTo({to: this.to}) } _initHTML(digits: number) { while (this.node.lastChild) { this.node.removeChild(this.node.lastChild); } this.node.classList.add('number-roll') this.node.style.position = 'relative' this.node.style.overflow = 'hidden' this.node.style.display = 'block' this.node for(let i = 0; i < digits; i ++) { const numberColumnNode = document.createElement('div') numberColumnNode.classList.add(`ncn${i}`) numberColumnNode.classList.add(`ncn`) numberColumnNode.style.position = 'relative' numberColumnNode.style.display = 'inline-block' numberColumnNode.style.verticalAlign = 'top'; numberColumnNode.style.margin = `0 ${this.letterMargin}px`; [...this.systemArr, this.systemArr[0]].forEach(i => { const child = document.createElement('div') child.className = 'digit' child.innerHTML = `${i}` // appendChild 方法,把参数追加到指定父节点的子节点列表末尾。 // 如果接收的参数在 dom 树中存在,那它将会被剪切 numberColumnNode.appendChild(child) }) // 这里是添加到数组开头 this.numberColumnNodeArr.unshift(numberColumnNode) // 这里是添加到末尾 this.node.appendChild(numberColumnNode) this.beforeArr.push(0) /** 如果没有设置分隔符,则不需要添加分隔符。 如果设置了分隔符,那么只有设置了separateOnly或separateEvery其中之一才会生效,不能同时设置。 如果当前数字已经是最高位,即个位数,则不需要添加分隔符。 如果当前数字不是需要分隔符分割的数字,根据separateEvery和separateOnly的值判断是否需要添加分隔符。 其中separateOnly优先级较高,如果设置了separateOnly,则在滚动过程中只会添加一次分隔符。 否则,按照separateEvery的值决定每几个数字添加一个分隔符。 */ if ( !this.separator || (!this.separateOnly && !this.separateEvery) || i === digits - 1 || ((digits - i) % this.separateEvery != 1 && digits - i -this.separateOnly != 1) ) continue const sprtrStr = isstr(this.separator) ? this.separator : this.separator.shift() as string const separator = document.createElement('div') separator.className = 'sprtr' separator.style.display = 'inline-block' separator.innerHTML = sprtrStr this.node.appendChild(separator) } const resize = () => { this.height = this.numberColumnNodeArr[0].clientHeight / (this.systemArr.length + 1) this.node.style.height = this.height + 'px' if(this.afterArr.length) { // this.frame(1) } else { for (let d = 0, len = this.numberColumnNodeArr.length; d < len; d ++) { this._draw({ digit: d, per: 1, alter: ~~(this.from / Math.pow(10, d)) }) } } } resize() window.addEventListener('resize', resize) } _draw({per, alter, digit}: {per: number; alter: number; digit: number}) { const newHeight = this.numberColumnNodeArr[0].clientHeight / (this.systemArr.length + 1) if(this.height !== newHeight) this.height = newHeight const from = this.beforeArr[digit] // 有可能是负数,所以要两次取余。一定在 0 到 9 之间。 // 翻滚前的数字再来加上差值 * 百分比 const modNum = (((per * alter + from) % 10) + 10) % 10 const translateY = `translateY(${- modNum * (this.height || 0)}px) ` this.numberColumnNodeArr[digit].style.webkitTransform = translateY this.numberColumnNodeArr[digit].style.transform = translateY } frame(per: number) { let temp = 0; for(let d = this.numberColumnNodeArr.length - 1; d >= 0; d --) { const alter = this.afterArr[d] - this.beforeArr[d] temp += alter this._draw({ digit: d, per: this.easeFn(per), alter: this.direct ? alter: temp }) // 这里必须乘10 temp *= 10 } } rollTo ({ to, duration = 0, easeFn, direct }: { to: number; duration?: number; easeFn?: () => any; direct?: boolean }) { if(easeFn) this.easeFn = easeFn if(direct !== undefined) this.direct = direct this.setSelect(to) const len = this.numberColumnNodeArr.length this.beforeArr = num2PadNumArr(this.from, len) this.afterArr = num2PadNumArr(to, len) const start = Date.now() const dur = duration * 1000 || this.duration const tick = () => { const elapsed = Date.now() - start this.frame(elapsed / dur) if(elapsed < dur) requestAnimationFrame(tick) else { this.from = to; this.frame(1) } } requestAnimationFrame(tick); } // 无关紧要这个方法 setSelect (num: number) { const len = this.numberColumnNodeArr.length num2PadNumArr(num, len).forEach((n: number, digit: number) => { for (let i = 0; i < this.numberColumnNodeArr[digit].childNodes.length; i ++) { const el = this.numberColumnNodeArr[digit].childNodes[i] as HTMLElement el.style.userSelect = i === n ? 'auto' : 'none' } }) } }