UNPKG

duo-touch

Version:

a touch special effects

272 lines (262 loc) 10.4 kB
import anime from "animejs/lib/anime.es.js"; import { is, rand, transformString, setStyleByName, getCSSValue } from "../utils/index.js"; class Particles { constructor(el, options) { let defaults = { type: 'circle', style: 'fill', color: '#000', canvasPadding: 150, duration: 1000, easing: 'easeInOutCubic', direction: 'left', size: function () { return Math.floor((Math.random() * 13) + 1); }, speed: function () { return rand(4); }, amount: 3, frequency: 20 } this.el = el; this.isOnce = (options && options.once) || false this.isHidden = (options && options.hidden) || true this.options = this.extend(options, defaults); this.init(); } init() { this.particles = []; this.frame = null; this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d'); this.canvas.className = '__duo__particle_canvas'; setStyleByName(this.canvas, 'position', 'absolute') setStyleByName(this.canvas, 'pointer-events', 'none') setStyleByName(this.canvas, 'top', '50%') setStyleByName(this.canvas, 'left', '50%') setStyleByName(this.canvas, 'transform', 'translate3d(-50%, -50%, 0)') setStyleByName(this.canvas, 'display', 'none') this.wrapper = document.createElement('div'); this.wrapper.className = '__duo__particle_wrapper'; setStyleByName(this.wrapper, 'position', 'relative') setStyleByName(this.wrapper, 'display', 'inline-block') setStyleByName(this.wrapper, 'overflow', 'hidden') this.el.parentNode.insertBefore(this.wrapper, this.el); this.wrapper.appendChild(this.el); this.parentWrapper = document.createElement('div'); this.parentWrapper.className = '__duo__particle'; setStyleByName(this.parentWrapper, 'display', 'inline-block') setStyleByName(this.parentWrapper, 'position', 'relative') this.wrapper.parentNode.insertBefore(this.parentWrapper, this.wrapper); this.parentWrapper.appendChild(this.wrapper); this.parentWrapper.appendChild(this.canvas); this.bindEvent() } loop() { this.updateParticles(); this.renderParticles(); if (this.isAnimating()) { this.frame = requestAnimationFrame(this.loop.bind(this)); } } updateParticles() { let p; for (let i = 0; i < this.particles.length; i++) { p = this.particles[i]; if (p.life > p.death) { this.particles.splice(i, 1); } else { p.x += p.speed; p.y = this.o.frequency * Math.sin(p.counter * p.increase); p.life++; p.counter += this.disintegrating ? 1 : -1; } } if (!this.particles.length) { this.pause(); setStyleByName(this.canvas, 'display', 'none') if (is.fnc(this.o.complete)) { this.o.complete(); } } } renderParticles() { this.ctx.clearRect(0, 0, this.width, this.height); let p; for (let i = 0; i < this.particles.length; i++) { p = this.particles[i]; if (p.life < p.death) { this.ctx.translate(p.startX, p.startY); this.ctx.rotate(p.angle * Math.PI / 180); this.ctx.globalAlpha = this.disintegrating ? 1 - p.life / p.death : p.life / p.death; this.ctx.fillStyle = this.ctx.strokeStyle = this.o.color; this.ctx.beginPath(); switch (this.o.type) { case 'circle': this.ctx.arc(p.x, p.y, p.size, 0, 2 * Math.PI); break; case 'triangle': this.ctx.moveTo(p.x, p.y); this.ctx.lineTo(p.x + p.size, p.y + p.size); this.ctx.lineTo(p.x + p.size, p.y - p.size); break; case 'rectangle': this.ctx.rect(p.x, p.y, p.size, p.size); break; } if (this.o.style === 'fill') { this.ctx.fill(); } else if (this.o.style === 'stroke') { this.ctx.closePath(); this.ctx.stroke(); } this.ctx.globalAlpha = 1; this.ctx.rotate(-p.angle * Math.PI / 180); this.ctx.translate(-p.startX, -p.startY); } } } play() { this.frame = requestAnimationFrame(this.loop.bind(this)); } bindEvent() { this.el.addEventListener('click', () => { if (!this.isAnimating()) { this.disintegrate() } }); } pause() { cancelAnimationFrame(this.frame); this.frame = null; if (this.disintegrating && !this.isOnce) { this.integrate({ duration: 800, easing: 'easeOutSine' }); } } // add particle to particle list addParticle(options) { let frames = this.o.duration * 60 / 1000; let speed = is.fnc(this.o.speed) ? this.o.speed() : this.o.speed; this.particles.push({ startX: options.x, startY: options.y, x: this.disintegrating ? 0 : speed * -frames, y: 0, angle: rand(360), counter: this.disintegrating ? 0 : frames, increase: Math.PI * 2 / 100, life: 0, death: this.disintegrating ? (frames - 20) + Math.random() * 40 : frames, speed: speed, size: is.fnc(this.o.size) ? this.o.size() : this.o.size }); } addParticles(rect, progress) { let x = this.options.canvasPadding, y = this.options.canvasPadding, progressDiff = this.disintegrating ? progress - this.lastProgress : this.lastProgress - progress, progressValue = (this.isHorizontal() ? rect.width : rect.height) * progress + progressDiff * (this.disintegrating ? 100 : 220); this.lastProgress = progress; if (this.isHorizontal()) { x += this.o.direction === 'left' ? progressValue : rect.width - progressValue; } else { y += this.o.direction === 'top' ? progressValue : rect.height - progressValue; } let i = Math.floor(this.o.amount * (progressDiff * 100 + 1)); if (i > 0) { while (i--) { this.addParticle({ x: x + (this.isHorizontal() ? 0 : rect.width * Math.random()), y: y + (this.isHorizontal() ? rect.height * Math.random() : 0) }); } } if (!this.isAnimating()) { setStyleByName(this.canvas, 'display', 'block') this.play(); } } addTransforms(value) { let translateProperty = this.isHorizontal() ? 'translateX' : 'translateY', translateValue = this.o.direction === 'left' || this.o.direction === 'top' ? value : -value; this.isHidden && setStyleByName(this.wrapper, transformString, `${translateProperty}(${translateValue}%)`) this.isHidden && setStyleByName(this.el, transformString, `${translateProperty}(${-translateValue}%)`) } disintegrate(options) { if (!this.isAnimating()) { this.disintegrating = true; this.lastProgress = 0; this.setup(options); this.animate((anim) => { let _ = this, value = anim.animatables[0].target.value; _.addTransforms(value); if (_.o.duration) { _.addParticles(_.rect, value / 100, true); } }); } } integrate(options) { if (!this.isAnimating()) { this.disintegrating = false; this.lastProgress = 1; this.setup(options); let _ = this; this.animate(function (anim) { let value = anim.animatables[0].target.value; setTimeout(function () { _.addTransforms(value); }, _.o.duration); if (_.o.duration) { _.addParticles(_.rect, value / 100, true); } }); } } setup(options) { this.o = this.extend({}, this.options, options); this.isHidden && setStyleByName(this.wrapper, 'visibility', 'visible') if (this.o.duration) { this.rect = this.el.getBoundingClientRect(); // {bottom left right top x y} this.width = this.canvas.width = this.o.width || this.rect.width + this.o.canvasPadding * 2; this.height = this.canvas.height = this.o.height || this.rect.height + this.o.canvasPadding * 2; } } animate(update) { let _ = this; anime({ targets: { value: _.disintegrating ? 0 : 100 }, value: _.disintegrating ? 100 : 0, duration: _.o.duration, easing: _.o.easing, begin: _.o.begin, update: update, complete: function () { if (_.disintegrating) { this.isHidden && setStyleByName(_.wrapper, 'visibility', 'visible') } }, }); } isAnimating() { return !!this.frame; } isHorizontal() { return this.o.direction === 'left' || this.o.direction === 'right'; } extend(target, source) { !target && (target = {}) for (let key in source) { !target[key] && (target[key] = source[key]); } return target; } } export default (el, value) => { new Particles(el, value); }