jparticles
Version:
A succinct, efficient, and lightweight Canvas plugin library for building some cool particle effects.
573 lines (494 loc) • 15.6 kB
JavaScript
/**
* 规定:
* defaultConfig:默认配置项,需挂载到构造函数对象上
*
* 对象的属性
* set: 参数配置
* set.opacity 透明度
* set.color: 颜色
* set.resize: 自适应
*
* c: canvas对象
* cw: canvas宽度
* ch: canvas高度
* cxt: canvas 2d 绘图环境
* container: {DOM Element} 包裹canvas的容器
* dots: {array} 通过arc绘制的粒子对象集
* [dot].x: 通过arc绘制的粒子的x值
* [dot].y: 通过arc绘制的粒子的y值
* paused: {boolean} 是否暂停
* canvasRemoved: {boolean} canvas从DOM中被移除
*
* 对象的方法
* color:返回随机或设定好的粒子颜色
*
* 子类原型对象的方法
* init: 初始化配置或方法调用
* draw: 绘图函数
*
* 继承 Base 父类的方法
* pause: 暂停粒子运动
* open: 开启粒子运动
* resize: 自适应窗口,需手动调用
*
* 继承 Base 父类的事件
* onDestroy: 动画被销毁后执行的事件
*/
// 编译构建时通过 package.json 的 version 生成版本
const version = null;
const win = window;
const doc = document;
const {random, floor} = Math;
const {isArray} = Array;
const orientationSupport = !!win.DeviceOrientationEvent;
const defaultCanvasWidth = 485;
const defaultCanvasHeight = 300;
const regExp = {
trimAll: /\s/g,
styleValue: /^\d+(\.\d+)?[a-z]+$/i
};
function pInt(str) {
return parseInt(str, 10);
}
function trimAll(str) {
return str.replace(regExp.trimAll, '');
}
function randomColor() {
// http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript
return '#' + random().toString(16).slice(-6);
}
/**
* 限制随机数的范围
* @param max {number}
* @param min {number}
* @returns {number}
*/
function limitRandom(max, min) {
return max === min ? max : (random() * (max - min) + min);
}
/**
* 对象的复制,跟jQuery extend方法一致(站在jQuery的肩膀之上)。
* extend( target [, object1 ] [, objectN ] )
* extend( [ deep ,] target, object1 [, objectN ] )
* @returns {object}
*/
function extend() {
let arg = arguments,
target = arg[0] || {},
deep = false,
length = arg.length,
i = 1,
value, attr,
copyIsArray;
if (isBoolean(target)) {
deep = target;
target = arg[1] || {};
i++;
}
for (; i < length; i++) {
for (attr in arg[i]) {
value = arg[i][attr];
if (deep && (isPlainObject(value) || (copyIsArray = isArray(value)))) {
let src = target[attr];
if (copyIsArray) {
copyIsArray = false;
src = isArray(src) ? src : [];
} else {
src = isPlainObject(src) ? src : {};
}
target[attr] = extend(deep, src, value);
} else {
target[attr] = value;
}
}
}
return target;
}
/**
* 对象的检测
* @param obj {*} 需要检测的对象
* @param type {string} 对象所属类型
* @returns {boolean}
*/
function typeChecking(obj, type) {
// 直接使用 toString.call(obj) 在 ie 会下报错
return Object.prototype.toString.call(obj) === type;
}
function isFunction(obj) {
return typeChecking(obj, '[object Function]');
}
function isPlainObject(obj) {
return typeChecking(obj, '[object Object]');
}
function isString(val) {
return typeof val === 'string';
}
function isBoolean(val) {
return typeof val === 'boolean';
}
function isUndefined(val) {
return typeof val === 'undefined';
}
function isNull(val) {
return val === null;
}
function isElement(obj) {
// document(nodeType===9)不能是element,因为它没有很多element该有的属性
// 如用getComputedStyle获取不到它的宽高,就会报错
// 当传入0的时候,不加!!会返回0,而不是Boolean值
return !!(obj && obj.nodeType === 1);
}
/**
* 获取对象的css属性值
* @param elem {element}
* @param attr {string}
* @returns {string|number}
*/
function getCss(elem, attr) {
const val = win.getComputedStyle(elem)[attr];
// 对于属性值是 200px 这样的形式,返回 200 这样的数字值
return regExp.styleValue.test(val) ? pInt(val) : val;
}
/**
* 获取对象距离页面的top、left值
* @param elem {element}
* @returns {{left: (number), top: (number)}}
*/
function offset(elem) {
let left = elem.offsetLeft || 0;
let top = elem.offsetTop || 0;
while (elem = elem.offsetParent) {
left += elem.offsetLeft;
top += elem.offsetTop;
}
return {
left: left,
top: top
};
}
function on(elem, evtName, handler) {
elem.addEventListener(evtName, handler);
}
function off(elem, evtName, handler) {
elem.removeEventListener(evtName, handler);
}
function setCanvasWH(context) {
context.cw = context.c.width = getCss(context.container, 'width') || defaultCanvasWidth;
context.ch = context.c.height = getCss(context.container, 'height') || defaultCanvasHeight;
}
/**
* 计算刻度值
* @param val {number} 乘数,(0, 1)表示被乘数的倍数,0 & [1, +∞)表示具体数值
* @param scale {number} 被乘数
* @returns {number}
*/
function scaleValue(val, scale) {
return val > 0 && val < 1 ? scale * val : val;
}
/**
* 计算速度值
* @param max {number}
* @param min {number}
* @returns {number}
*/
function calcSpeed(max, min) {
return (limitRandom(max, min) || max) * (random() > .5 ? 1 : -1);
}
/**
* 为插件事件添加增强的监听器
*
* eg:
* onDestroy() {
* registerListener(this.destructionListeners, ...arguments);
* }
*
* @param listener {array} 监听器集合
* @param arg {function} 回调函数
*/
function registerListener(context, listener, ...arg) {
if (context.set) {
for (let i = 0; i < arg.length; i++) {
if (isFunction(arg[i])) {
listener.push(arg[i]);
}
}
}
return context;
}
/**
* 生成 color 函数,用于给粒子赋予颜色
* @param color {string|array} 颜色数组
* @returns {function}
*/
function generateColor(color) {
const colorLength = isArray(color) ? color.length : false;
const recolor = () => color[floor(random() * colorLength)];
return isString(color) ? () => color : colorLength ? recolor : randomColor;
}
// 暂停粒子运动
function pause(context, callback) {
// 没有set表示实例创建失败,防止错误调用报错
if (!context.canvasRemoved && context.set && !context.paused) {
// 传递 pause 关键字供特殊使用
isFunction(callback) && callback.call(context, 'pause');
context.paused = true;
}
}
// 开启粒子运动
function open(context, callback) {
if (!context.canvasRemoved && context.set && context.paused) {
isFunction(callback) && callback.call(context, 'open');
context.paused = false;
context.draw();
}
}
// 自适应窗口,重新计算粒子坐标
function resize(context, callback) {
if (context.set.resize) {
context._resizeHandler = function () {
const oldCW = context.cw;
const oldCH = context.ch;
// 重新设置canvas宽高
setCanvasWH(context);
// 计算比例
const scaleX = context.cw / oldCW;
const scaleY = context.ch / oldCH;
// 重新赋值
if (isArray(context.dots)) {
context.dots.forEach(v => {
if (isPlainObject(v)) {
v.x *= scaleX;
v.y *= scaleY;
}
});
}
if (isFunction(callback)) {
callback.call(context, scaleX, scaleY);
}
context.paused && context.draw();
};
on(win, 'resize', context._resizeHandler);
}
}
/**
* 修改插件原型上的方法
* 使用:modifyPrototype(Particle.prototype, 'pause', function(){})
* @param prototype {Object} 原型对象
* @param names {string} 方法名,多个方法名用逗号隔开
* @param callback {function} 回调函数
*/
function modifyPrototype(prototype, names, callback) {
trimAll(names).split(',').forEach(name => {
prototype[name] = function () {
utils[name](this, callback);
};
});
}
/**
* 使用此方法挂载插件到 JParticles 对象上,防止被修改。
* function.name 的兼容性并不高,所以插件需手动传递 name 值。
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name
*
* eg:
* defineReadOnlyProperty(Particle, 'particle')
* defineReadOnlyProperty(regExp, 'regExp', utils)
*
* @param value {Class|*} 插件类或其他值
* @param name {string} 属性名称
* @param target {object} 要在其上定义属性的对象
*/
function defineReadOnlyProperty(value, name, target = JParticles) {
Object.defineProperty(target, name, {
value,
writable: false,
enumerable: true,
configurable: false
});
}
class Base {
constructor(constructor, selector, options) {
// 构建任务自动添加 runSupport 变量,
// 即不支持 Object.defineProperty 的浏览器和 IE8 将不支持创建特效
if (runSupport && (this.container = isElement(selector) ? selector : doc.querySelector(selector))) {
this.set = extend(true, {}, JParticles.commonConfig, constructor.defaultConfig, options);
this.c = doc.createElement('canvas');
this.cxt = this.c.getContext('2d');
this.paused = false;
setCanvasWH(this);
this.container.innerHTML = '';
this.container.appendChild(this.c);
this.color = generateColor(this.set.color);
this.observeCanvasRemoved();
this.init();
this.resize();
}
}
requestAnimationFrame() {
if (!this.canvasRemoved && !this.paused) {
win.requestAnimationFrame(this.draw.bind(this));
}
}
observeCanvasRemoved() {
this.destructionListeners = [];
observeElementRemoved(this.c, () => {
// canvas 从DOM中被移除
// 1、停止 requestAnimationFrame,避免性能损耗
this.canvasRemoved = true;
// 2、移除外在事件
if (this._resizeHandler) {
off(win, 'resize', this._resizeHandler);
}
// 3、触发销毁回调事件
this.destructionListeners.forEach(callback => {
callback();
});
});
}
onDestroy() {
// 让事件支持链式操作
return registerListener(this, this.destructionListeners, ...arguments);
}
pause() {
pause(this);
}
open() {
open(this);
}
resize() {
resize(this);
}
}
// requestAnimationFrame 兼容处理
win.requestAnimationFrame = (win => {
return win.requestAnimationFrame
|| win.webkitRequestAnimationFrame
|| win.mozRequestAnimationFrame
|| function (fn) {
win.setTimeout(fn, 1000 / 60);
};
})(win);
// 不管是 MutationObserver 还是 DOMNodeRemoved,
// 当监听某个具体的元素时,如果父祖级被删除了,并不会触发该元素被移除的事件,
// 所以要监听整个文档,每次移除事件都得递归遍历要监听的元素是否被删除。
const observeElementRemoved = (() => {
const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
const checkElementRemoved = (node, element) => {
if (node === element) {
return true;
} else if (isElement(node)) {
const children = node.children;
let length = children.length;
while (length--) {
if (checkElementRemoved(children[length], element)) {
return true;
}
}
}
return false;
};
const useMutation = (element, callback) => {
const observer = new MutationObserver((mutations, observer) => {
let i = mutations.length;
while (i--) {
const removeNodes = mutations[i].removedNodes;
let j = removeNodes.length;
while (j--) {
if (checkElementRemoved(removeNodes[j], element)) {
observer.disconnect();
return callback();
}
}
}
});
observer.observe(document, {
childList: true,
subtree: true
});
};
const useDOMNodeRemoved = (element, callback) => {
const DOMNodeRemoved = e => {
if (checkElementRemoved(e.target, element)) {
document.removeEventListener('DOMNodeRemoved', DOMNodeRemoved);
callback();
}
};
document.addEventListener('DOMNodeRemoved', DOMNodeRemoved);
};
return MutationObserver ? useMutation : useDOMNodeRemoved;
})();
// 工具箱
const utils = {
orientationSupport,
regExp,
pInt,
trimAll,
randomColor,
limitRandom,
extend,
typeChecking,
isPlainObject,
isFunction,
isArray,
isString,
isBoolean,
isUndefined,
isNull,
isElement,
observeElementRemoved,
getCss,
offset,
on,
off,
scaleValue,
calcSpeed,
pause,
open,
resize,
modifyPrototype,
defineReadOnlyProperty,
registerListener
};
const commonConfig = {
// 画布全局透明度 {number}
// 取值范围:[0-1]
opacity: 1,
// 粒子颜色 {string|array}
// 1、空数组表示随机取色。
// 2、在特定颜色的数组里随机取色,如:['red', 'blue', 'green']。
// 3、当为 string 类型时,如:'red',则表示粒子都填充为红色。
color: [],
// 自适应窗口尺寸变化 {boolean}
resize: true
};
// http://easings.net
const easing = {
linear(x, t, b, c, d) {
return b + (c - b) * x;
},
swing() {
return easing.easeInOutQuad(...arguments);
},
easeInOutQuad(x, t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
};
const JParticles = {
version,
utils,
Base,
easing
};
// 设置对外提供的对象的属性及方法
// 为只读,可枚举,不允许修改及删除。
(function defineProperties(object) {
for (const name in object) {
const value = object[name];
defineReadOnlyProperty(value, name, object);
if (isPlainObject(value)) {
defineProperties(value);
}
}
})(JParticles);
JParticles.commonConfig = commonConfig;
win.JParticles = JParticles;