UNPKG

jparticles

Version:

A succinct, efficient, and lightweight Canvas plugin library for building some cool particle effects.

383 lines (315 loc) 12 kB
const {utils, Base} = JParticles; const {random, abs, PI, floor} = Math; const twicePI = PI * 2; const { pInt, limitRandom, calcSpeed, scaleValue, getCss, offset, isElement, isFunction, isNull, on, off, orientationSupport, resize, defineReadOnlyProperty } = utils; /** * 检查元素或其祖先节点的属性是否等于预给值 * @param elem {element} 起始元素 * @param property {string} css属性 * @param value {string} css属性值 * @returns {boolean} */ function checkParentsProperty(elem, property, value) { while (elem = elem.offsetParent) { if (getCss(elem, property) === value) { return true; } } return false; } class Particle extends Base { static defaultConfig = { // 粒子个数,默认为容器宽度的 0.12 倍 // (0, 1) 显示为容器宽度相应倍数的个数,0 & [1, +∞) 显示具体个数 // 0 是没有意义的 num: .12, // 粒子最大半径(0, +∞) maxR: 2.4, // 粒子最小半径(0, +∞) minR: .6, // 粒子最大运动速度(0, +∞) maxSpeed: 1, // 粒子最小运动速度(0, +∞) minSpeed: .1, // 两点连线的最大值 // 在 range 范围内的两点距离小于 proximity,则两点之间连线 // (0, 1) 显示为容器宽度相应倍数的个数,0 & [1, +∞) 显示具体个数 proximity: .2, // 定位点的范围,范围越大连线越多 // 当 range 等于 0 时,不连线,相关值无效 // (0, 1) 显示为容器宽度相应倍数的个数,0 & [1, +∞) 显示具体个数 range: .2, // 线段的宽度 lineWidth: .2, // 连线的形状 // spider: 散开的蜘蛛状 // cube: 合拢的立方体状 lineShape: 'spider', // 改变定位点坐标的事件元素 // null 表示 canvas 画布,或传入原生元素对象,如 document 等 eventElem: null, // 视差效果 {boolean} parallax: false, // 定义粒子在视差图层里的层数及每层的层级大小,类似 css 里的 z-index。 // 取值范围: [0, +∞),值越小视差效果越强烈,0 则不动。 // 定义四层粒子示例:[1, 3, 5, 10] parallaxLayer: [1, 2, 3], // 视差强度,值越小视差效果越强烈 parallaxStrength: 3 }; get version() { return '2.0.0'; } constructor(selector, options) { super(Particle, selector, options); } init() { this.attrNormalize(); if (this.set.range > 0) { // 定位点坐标 this.positionX = random() * this.cw; this.positionY = random() * this.ch; this.defineLineShape(); this.positionEvent(); } // 初始化鼠标在视差上的坐标 this.mouseX = this.mouseY = 0; this.createDots(); this.draw(); this.parallaxEvent(); } attrNormalize() { const {cw, set} = this; ['num', 'proximity', 'range'].forEach(attr => { set[attr] = pInt(scaleValue(set[attr], cw)); }); // 设置触发事件的元素 if (!isElement(set.eventElem) && set.eventElem !== document) { set.eventElem = this.c; } } defineLineShape() { const {proximity, range, lineShape} = this.set; switch (lineShape) { case 'cube': this.lineShapeMaker = (x, y, sx, sy, cb) => { const {positionX, positionY} = this; if ( abs(x - sx) <= proximity && abs(y - sy) <= proximity && abs(x - positionX) <= range && abs(y - positionY) <= range && abs(sx - positionX) <= range && abs(sy - positionY) <= range ) { cb(); } }; break; default: this.lineShapeMaker = (x, y, sx, sy, cb) => { const {positionX, positionY} = this; if ( abs(x - sx) <= proximity && abs(y - sy) <= proximity && ( abs(x - positionX) <= range && abs(y - positionY) <= range || abs(sx - positionX) <= range && abs(sy - positionY) <= range ) ) { cb(); } }; } } createDots() { const {cw, ch, color} = this; let {num, maxR, minR, maxSpeed, minSpeed, parallaxLayer} = this.set; let layerLength = parallaxLayer.length; let dots = this.dots = []; while (num--) { let r = limitRandom(maxR, minR); dots.push({ r, x: limitRandom(cw - r, r), y: limitRandom(ch - r, r), vx: calcSpeed(maxSpeed, minSpeed), vy: calcSpeed(maxSpeed, minSpeed), color: color(), // 定义粒子在视差图层里的层数及每层的层级大小 parallaxLayer: parallaxLayer[floor(random() * layerLength)], // 定义粒子视差的偏移值 parallaxOffsetX: 0, parallaxOffsetY: 0 }); } } draw() { const {cw, ch, cxt} = this; const {range, lineWidth, opacity} = this.set; cxt.clearRect(0, 0, cw, ch); // 当canvas宽高改变的时候,全局属性需要重新设置 cxt.lineWidth = lineWidth; cxt.globalAlpha = opacity; // 更新粒子坐标 this.updateXY(); // 绘制粒子 this.dots.forEach(dot => { const {x, y, r, parallaxOffsetX, parallaxOffsetY} = dot; cxt.save(); cxt.beginPath(); cxt.arc( x + parallaxOffsetX, y + parallaxOffsetY, r, 0, twicePI ); cxt.fillStyle = dot.color; cxt.fill(); cxt.restore(); }); // 当连接范围小于 0 时,不连接线段 if (range > 0) { this.connectDots(); } this.requestAnimationFrame(); } connectDots() { const {dots, cxt, lineShapeMaker} = this; const length = dots.length; dots.forEach((dot, i) => { const x = dot.x + dot.parallaxOffsetX; const y = dot.y + dot.parallaxOffsetY; while (++i < length) { const sibDot = dots[i]; const sx = sibDot.x + sibDot.parallaxOffsetX; const sy = sibDot.y + sibDot.parallaxOffsetY; lineShapeMaker(x, y, sx, sy, () => { cxt.save(); cxt.beginPath(); cxt.moveTo(x, y); cxt.lineTo(sx, sy); cxt.strokeStyle = dot.color; cxt.stroke(); cxt.restore(); }); } }); } updateXY() { const {paused, mouseX, mouseY, cw, ch} = this; const {parallax, parallaxStrength} = this.set; this.dots.forEach(dot => { // 暂停的时候,vx 和 vy 保持不变, // 防止自适应窗口变化时出现粒子移动 if (!paused) { if (parallax) { // https://github.com/jnicol/particleground const divisor = parallaxStrength * dot.parallaxLayer; dot.parallaxOffsetX += (mouseX / divisor - dot.parallaxOffsetX) / 10; dot.parallaxOffsetY += (mouseY / divisor - dot.parallaxOffsetY) / 10; } dot.x += dot.vx; dot.y += dot.vy; let {x, y, r, parallaxOffsetX, parallaxOffsetY} = dot; x += parallaxOffsetX; y += parallaxOffsetY; // 自然碰撞反向,视差事件移动反向 if (x + r >= cw) { dot.vx = -abs(dot.vx); } else if (x - r <= 0) { dot.vx = abs(dot.vx); } if (y + r >= ch) { dot.vy = -abs(dot.vy); } else if (y - r <= 0) { dot.vy = abs(dot.vy); } } }); } setElemOffset() { return ( this.elemOffset = this.set.eventElem === document ? null : offset(this.set.eventElem) ); } proxyEvent(move, rientation) { const {eventElem} = this.set; let orientationHandler = null; if (orientationSupport) { orientationHandler = e => { if (this.paused || isNull(e.beta)) return; // 转换 beta 范围 [-180, 180] 成 [-90, 90] rientation(Math.min(Math.max(e.beta, -90), 90), e.gamma); }; on(window, 'deviceorientation', orientationHandler); } const moveHandler = e => { if (this.paused) return; let left = e.pageX; let top = e.pageY; if (this.setElemOffset()) { // 动态判断祖先节点是否具有固定定位,有则使用 client 计算 if (checkParentsProperty(eventElem, 'position', 'fixed')) { left = e.clientX; top = e.clientY; } left -= this.elemOffset.left; top -= this.elemOffset.top; } move(left, top); }; on(eventElem, 'mousemove', moveHandler); this.onDestroy(() => { off(eventElem, 'mousemove', moveHandler); off(window, 'deviceorientation', orientationHandler); }); } positionEvent() { const {range} = this.set; // 性能优化 if (range > this.cw && range > this.ch) return; // 鼠标移动事件 this.proxyEvent((left, top) => { this.positionX = left; this.positionY = top; // 陀螺仪事件 }, (beta, gamma) => { this.positionY = -(beta - 90) / 180 * this.ch; this.positionX = -(gamma - 90) / 180 * this.cw; }); } parallaxEvent() { if (!this.set.parallax) return; this.proxyEvent((left, top) => { this.mouseX = left - this.cw / 2; this.mouseY = top - this.ch / 2; }, (beta, gamma) => { // 一半高度或宽度的对应比例值 // mouseX: - gamma / 90 * cw / 2; // mouseY: - beta / 90 * ch / 2; this.mouseX = -gamma * this.cw / 180; this.mouseY = -beta * this.ch / 180; }); } resize() { resize(this, (scaleX, scaleY) => { if (this.set.range > 0) { this.positionX *= scaleX; this.positionY *= scaleY; this.mouseX *= scaleX; this.mouseY *= scaleY; } }); } } // 挂载插件到 JParticles 对象上。 defineReadOnlyProperty(Particle, 'particle');