jparticles
Version:
A succinct, efficient, and lightweight Canvas plugin library for building some cool particle effects.
284 lines (230 loc) • 8 kB
JavaScript
const {utils, Base} = JParticles;
const {random, abs, PI, sin, ceil} = Math;
const twicePI = PI * 2;
const {
pInt, limitRandom, calcSpeed,
scaleValue, randomColor, isArray,
isFunction, isPlainObject, isUndefined,
resize, defineReadOnlyProperty,
registerListener
} = utils;
class WaveLoading extends Base {
static defaultConfig = {
// [font style][font weight][font size][font family]
// 文本样式,同css一样,必须包含 [font size] 和 [font family]
font: 'normal 900 20px Arial',
// 小字体样式,如:“%”
smallFont: 'normal 900 14px Arial',
// 小字体相对于中点向下的偏移值,
// 细节的处理,为了让显示更好看。
smallFontOffsetTop: 1,
// 文本颜色
color: '#333',
// 填充的背景色
fillColor: '#27C9E5',
// 线条横向偏移值,距离canvas画布左边的偏移值
// (0, 1)表示容器宽度的倍数,0 & [1, +∞)表示具体数值
offsetLeft: 0,
// 波峰高度,(0, 1)表示容器高度的倍数,0 & [1, +∞)表示具体数值
crestHeight: 4,
// 波纹个数,即正弦周期个数
rippleNum: 1,
// 波浪的运动速度
speed: .3,
// 加载到 99% 的时长,单位毫秒(ms)
// 用时越久,越慢加载到 99%。
duration: 5000,
// 加载过程的运动效果,
// 目前支持匀速(linear),先加速再减速(swing),两种
easing: 'swing'
};
get version() {
return '2.0.0';
}
constructor(selector, options) {
super(WaveLoading, selector, options);
}
init() {
this.c.style.borderRadius = '50%';
this.progress = 0;
this.set.offsetTop = this.ch;
this.halfCH = this.ch / 2;
this.progressListeners = [];
this.finishedListeners = [];
this.attrNormalize();
this.createDots();
this.draw();
}
attrNormalize() {
['offsetLeft', 'crestHeight'].forEach(attr => {
this.set[attr] = scaleValue(
this.set[attr],
attr === 'offsetLeft' ? this.cw : this.ch
);
});
}
createDots() {
const {cw} = this;
const dots = this.dots = [];
// 线条波长,每个周期(2π)在canvas上的实际长度
const rippleLength = cw / this.set.rippleNum;
// 点的y轴步进
const step = twicePI / rippleLength;
// 一条线段所需的点
for (let i = 0; i <= cw; i++) {
dots.push({
x: i,
y: i * step
});
}
}
draw() {
this.calcOffsetTop();
this.drawWave();
this.drawText();
this.calcProgress();
if (this.progress <= 100) {
this.requestAnimationFrame();
} else {
this.progress = 100;
this.calcOffsetTop();
this.drawWave();
this.drawText();
this.finishedListeners.forEach(cb => cb());
}
}
drawWave() {
const {cxt, cw, ch} = this;
const {
opacity, crestHeight, offsetLeft,
offsetTop, fillColor, speed
} = this.set;
cxt.clearRect(0, 0, cw, ch);
cxt.globalAlpha = opacity;
cxt.save();
cxt.beginPath();
this.dots.forEach((dot, i) => {
cxt[i ? 'lineTo' : 'moveTo'](
dot.x,
// y = A sin ( ωx + φ ) + h
crestHeight * sin(dot.y + offsetLeft) + offsetTop
);
dot.y -= speed;
});
cxt.lineTo(cw, ch);
cxt.lineTo(0, ch);
cxt.closePath();
cxt.fillStyle = fillColor;
cxt.fill();
cxt.restore();
}
drawText() {
const {cxt, cw, halfCH, progress} = this;
let {
font, smallFont, color,
smallFontOffsetTop
} = this.set;
let percentText = '%';
let progressText = ceil(progress);
this.progressListeners.forEach(callback => {
const res = callback(this.progress);
if (!isUndefined(res)) {
if (isPlainObject(res)) {
progressText = res.text;
percentText = res.smallText || '';
} else {
progressText = res;
percentText = '';
}
}
});
cxt.font = font;
const progressWidth = cxt.measureText(progressText).width;
cxt.font = smallFont;
const percentWidth = cxt.measureText(percentText).width;
const x = (cw - progressWidth - percentWidth) / 2;
cxt.textBaseline = 'middle';
cxt.fillStyle = color;
cxt.font = font;
cxt.fillText(progressText, x, halfCH);
cxt.font = smallFont;
cxt.fillText(
percentText,
x + progressWidth,
halfCH + smallFontOffsetTop
);
}
calcProgress() {
if (this.immediatelyComplete) {
this.progress += this.immediatelyComplete;
this.immediatelyComplete += 0.5;
return;
}
if (this.progress >= 99) return;
const {easing, duration} = this.set;
if (!this.startTime) {
this.startTime = Date.now();
}
// x: percent Complete percent Complete: elapsedTime / duration
// t: current time elapsed time: currentTime - startTime
// b: beginning value start value
// c: change in value finish value
// d: duration duration
const time = Date.now() - this.startTime;
const percent = time / duration;
if (percent <= 1) {
this.progress = JParticles.easing[easing](
// x, t, b, c, d
percent, time, 0, 100, duration
);
if (this.progress >= 99) {
this.progress = 99;
}
}
}
calcOffsetTop() {
// enhance performance when the loading progress continue for 99%
if (!this.immediatelyComplete && this.progress === 99) return;
if (this.progress === 100) {
this.set.offsetTop = -this.set.crestHeight;
} else {
this.set.offsetTop = ceil(
(100 - this.progress) / 100 * this.ch + this.set.crestHeight
);
}
}
resize() {
resize(this, (scaleX, scaleY) => {
['offsetLeft', 'offsetTop', 'crestHeight'].forEach(option => {
this.set[option] *= option === 'offsetLeft' ? scaleX : scaleY;
});
this.halfCH = this.ch / 2;
if (this.progress === 100) {
this.draw();
}
});
}
setOptions(newOptions) {
if (this.set && isPlainObject(newOptions)) {
for (const name in newOptions) {
if (name !== 'offsetTop' && (name in this.set)) {
this.set[name] = newOptions[name];
}
}
}
}
done() {
if (this.set && !this.immediatelyComplete) {
this.immediatelyComplete = 1;
}
}
onProgress() {
return registerListener(this, this.progressListeners, ...arguments);
}
onFinished() {
return registerListener(this, this.finishedListeners, ...arguments);
}
}
delete WaveLoading.prototype.pause;
delete WaveLoading.prototype.open;
defineReadOnlyProperty(WaveLoading, 'waveLoading');