timered-counter
Version:
Make the value change more vivid and natural
248 lines • 9.5 kB
JavaScript
import { __decorate } from "tslib";
import { html, LitElement } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { intersection, isEmpty, isNullish, isString, merge, omit, range, } from 'remeda';
import { classMap } from 'lit/directives/class-map.js';
import { property, query, state } from 'lit/decorators.js';
import { rollerDigitStyles } from './styles.js';
import * as EasingFunctions from '../../easing/penner-easing-functions.js';
import { graceDefineCustomElement } from '../../utils/grace-define-custom-element.js';
class RollerDigitAnimationEvent extends Event {
}
export class TimeredCounterRollerDigit extends LitElement {
constructor() {
super();
this.digit = { data: [], place: 0 };
this.preprocessData = {
animate: true,
cancelPrevAnimation: false,
earlyReturn: '',
index: 0,
partIndex: 0,
digitIndex: 0,
};
this.direction = 'up';
this.textStyle = {};
this.cellStyle = [];
this.animationOptions = {};
this.keyframes = {};
this.digitWidth = 0;
this.resizeObserver = new ResizeObserver(() => {
this.digitWidth = this.clonedRollDigitList
? this.clonedRollDigitList.offsetWidth
: 0;
});
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
if (this.clonedRollDigitList) {
this.resizeObserver.observe(this.clonedRollDigitList);
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver.disconnect();
}
updated(_changedProperties) {
super.updated(_changedProperties);
/**
* 仅当影响滚动列表 dom 结构的属性发生变化时才会触发动画.
*/
if (intersection(Array.from(_changedProperties.keys()), [
'digit',
'preprocessData',
'direction',
'animationOptions',
'keyframes',
]).length > 0) {
if (this.shouldAnimate()) {
this.startAnimation().then();
}
}
}
render() {
const shadowCellStyle = this.cellStyle.map(style => omit(style, ['position']));
return html `<span class="roller-part-digit">
<!-- 占位 -->
<span
class="placeholder"
style=${styleMap({ width: `${Math.round(this.digitWidth)}px` })}
>0</span
>
<!-- 一个不可见的滚动列表的复制, 用于计算该列表的最大宽度. -->
<span class="roll-list__shadow">
${repeat(this.digit.data, (_, index) => index, (digit, index) => html `<span style=${styleMap(shadowCellStyle[index])}
>${digit}</span
>`)}
</span>
<span
class=${classMap({
/**
* 向上(up)滚动时, 使滚动列表顶部对齐以便于应用 `translationY(-100%)` 实现向上滚动效果
* 向下同理
*/
'roll-list__up': this.direction === 'up',
'roll-list__down': this.direction === 'down',
'roll-list': true,
})}
>
${this.digit.data.length > 1
? repeat(this.digit.data, (_, index) => index, (digit, index) => {
if (this.direction === 'up' &&
index === this.digit.data.length - 1) {
return html `<span
part="cell"
class="roll-item roll-item__head"
style=${styleMap(this.cellStyle[index])}
><span style=${styleMap(this.textStyle)}
>${digit}</span
></span
>`;
}
if (this.direction === 'down' && index === 0) {
return html `
<span
part="cell"
class="roll-item roll-item__tail"
style=${styleMap(this.cellStyle[index])}
><span style=${styleMap(this.textStyle)}
>${digit}</span
></span
>
`;
}
return html `<span
part="cell"
class="roll-item"
style=${styleMap(this.cellStyle[index])}
><span style=${styleMap(this.textStyle)}
>${digit}</span
></span
>`;
})
: html `<span
part="cell"
class="roll-item"
style=${styleMap(this.cellStyle[0])}
><span style=${styleMap(this.textStyle)}
>${this.digit.data[0]}</span
></span
>`}</span
></span
> `;
}
shouldAnimate() {
const { cancelPrevAnimation, animate } = this.preprocessData;
if (cancelPrevAnimation) {
if (this.animation) {
this.animation.cancel();
}
if (this.rollList && this.rollList.style && this.rollList.style.cssText) {
this.rollList.style.cssText = '';
}
}
if (!animate)
return false;
return true;
}
async startAnimation() {
if (isNullish(this.rollList))
return;
this.__emitAnimationStart();
const anmOptions = merge({ duration: 1000, iterations: 1, easing: 'ease-out', fill: 'forwards' }, this.animationOptions);
const keyframes = isEmpty(this.keyframes)
? {
up: { transform: ['translateY(0)', 'translateY(-100%)'] },
down: { transform: ['translateY(0)', 'translateY(100%)'] },
}[this.direction]
: this.keyframes;
/**
* 尝试从 `PennerEasingFunctions` 中获取对应的 easing 函数
*/
// {
if (isString(anmOptions.easing) &&
EasingFunctions[anmOptions.easing]) {
const func = EasingFunctions[anmOptions.easing];
anmOptions.easing = `linear(${range(0, 64)
.map((_, i) => func(i / 63))
.join(',')})`;
}
// }
try {
this.clearAnimation();
this.animation = this.rollList.animate(keyframes, anmOptions);
this.animation.addEventListener('finish', () => this.__emitAnimationEnd(), {
once: true,
});
// /**
// * 动画播放完成或被其他动画中断都会使得 `finished` resolve.
// * 只有当动画顺利播放完成的情况下, 才能调用 `cancel` 取消动画. 在其他情况下调用, 会抛出异常[1].
// *
// * 因此, 提前检查 `playState` 的值. 当 `playState` 不是 `finished` 时, 说明动画被其他 `Animation` 实例中断.
// * 因为已经有其他 `Animation` 实例的存在, 我们可以直接丢弃这个 `Animation` 实例, 而不用担心无动画可用.
// *
// * [1]: https://developer.mozilla.org/en-US/docs/Web/API/Animation/cancel#exceptions
// */
// await this.animation.finished;
// if (
// this.animation.playState === 'finished' &&
// this.rollList.checkVisibility()
// ) {
// // this.animation.commitStyles();
// // this.animation.cancel();
// }
}
catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
clearAnimation() {
this.animation?.removeEventListener('finish', this.__emitAnimationEnd);
this.animation?.cancel();
}
__emitAnimationStart() {
if (!this.isConnected)
return;
this.dispatchEvent(new RollerDigitAnimationEvent('roller-digit-animation-start'));
}
__emitAnimationEnd() {
if (!this.isConnected)
return;
this.dispatchEvent(new RollerDigitAnimationEvent('roller-digit-animation-end'));
}
}
TimeredCounterRollerDigit.styles = [rollerDigitStyles];
__decorate([
property({ type: Object })
], TimeredCounterRollerDigit.prototype, "digit", void 0);
__decorate([
property({ type: Object })
], TimeredCounterRollerDigit.prototype, "preprocessData", void 0);
__decorate([
property({ type: String })
], TimeredCounterRollerDigit.prototype, "direction", void 0);
__decorate([
property({ type: Object })
], TimeredCounterRollerDigit.prototype, "textStyle", void 0);
__decorate([
property({ type: Array })
], TimeredCounterRollerDigit.prototype, "cellStyle", void 0);
__decorate([
property({ type: Object })
], TimeredCounterRollerDigit.prototype, "animationOptions", void 0);
__decorate([
property({ type: Object })
], TimeredCounterRollerDigit.prototype, "keyframes", void 0);
__decorate([
query('.roll-list__shadow')
], TimeredCounterRollerDigit.prototype, "clonedRollDigitList", void 0);
__decorate([
query('.roll-list')
], TimeredCounterRollerDigit.prototype, "rollList", void 0);
__decorate([
state()
], TimeredCounterRollerDigit.prototype, "digitWidth", void 0);
graceDefineCustomElement('timered-counter-roller-digit', TimeredCounterRollerDigit);
//# sourceMappingURL=roller-digit.js.map