UNPKG

react-confetti

Version:

React component to draw confetti for your party.

414 lines (404 loc) 16.2 kB
var ReactConfetti = (function (jsxRuntime, React, tweens) { 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var tweens__namespace = /*#__PURE__*/_interopNamespaceDefault(tweens); function degreesToRads(degrees) { return (degrees * Math.PI) / 180; } function randomRange(min, max) { return min + Math.random() * (max - min); } function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); } var ParticleShape; (function (ParticleShape) { ParticleShape[ParticleShape["Circle"] = 0] = "Circle"; ParticleShape[ParticleShape["Square"] = 1] = "Square"; ParticleShape[ParticleShape["Strip"] = 2] = "Strip"; })(ParticleShape || (ParticleShape = {})); var RotationDirection; (function (RotationDirection) { RotationDirection[RotationDirection["Positive"] = 1] = "Positive"; RotationDirection[RotationDirection["Negative"] = -1] = "Negative"; })(RotationDirection || (RotationDirection = {})); class Particle { constructor(context, getOptions, x, y) { this.getOptions = getOptions; const { colors, initialVelocityX, initialVelocityY } = this.getOptions(); this.context = context; this.x = x; this.y = y; this.w = randomRange(5, 20); this.h = randomRange(5, 20); this.radius = randomRange(5, 10); this.vx = typeof initialVelocityX === 'number' ? randomRange(-initialVelocityX, initialVelocityX) : randomRange(initialVelocityX.min, initialVelocityX.max); this.vy = typeof initialVelocityY === 'number' ? randomRange(-initialVelocityY, 0) : randomRange(initialVelocityY.min, initialVelocityY.max); this.shape = randomInt(0, 2); this.angle = degreesToRads(randomRange(0, 360)); this.angularSpin = randomRange(-0.2, 0.2); this.color = colors[Math.floor(Math.random() * colors.length)]; this.rotateY = randomRange(0, 1); this.rotationDirection = randomRange(0, 1) ? RotationDirection.Positive : RotationDirection.Negative; } update() { const { gravity, wind, friction, opacity, drawShape } = this.getOptions(); this.x += this.vx; this.y += this.vy; this.vy += gravity; this.vx += wind; this.vx *= friction; this.vy *= friction; if (this.rotateY >= 1 && this.rotationDirection === RotationDirection.Positive) { this.rotationDirection = RotationDirection.Negative; } else if (this.rotateY <= -1 && this.rotationDirection === RotationDirection.Negative) { this.rotationDirection = RotationDirection.Positive; } const rotateDelta = 0.1 * this.rotationDirection; this.rotateY += rotateDelta; this.angle += this.angularSpin; this.context.save(); this.context.translate(this.x, this.y); this.context.rotate(this.angle); this.context.scale(1, this.rotateY); this.context.rotate(this.angle); this.context.beginPath(); this.context.fillStyle = this.color; this.context.strokeStyle = this.color; this.context.globalAlpha = opacity; this.context.lineCap = 'round'; this.context.lineWidth = 2; if (drawShape && typeof drawShape === 'function') { drawShape.call(this, this.context); } else { switch (this.shape) { case ParticleShape.Circle: { this.context.beginPath(); this.context.arc(0, 0, this.radius, 0, 2 * Math.PI); this.context.fill(); break; } case ParticleShape.Square: { this.context.fillRect(-this.w / 2, -this.h / 2, this.w, this.h); break; } case ParticleShape.Strip: { this.context.fillRect(-this.w / 6, -this.h / 2, this.w / 3, this.h); break; } } } this.context.closePath(); this.context.restore(); } } class ParticleGenerator { constructor(canvas, getOptions) { this.x = 0; this.y = 0; this.w = 0; this.h = 0; this.lastNumberOfPieces = 0; this.tweenInitTime = Date.now(); this.particles = []; this.particlesGenerated = 0; this.removeParticleAt = (i) => { this.particles.splice(i, 1); }; this.getParticle = () => { const newParticleX = randomRange(this.x, this.w + this.x); const newParticleY = randomRange(this.y, this.h + this.y); return new Particle(this.context, this.getOptions, newParticleX, newParticleY); }; this.animate = () => { const { canvas, context, particlesGenerated, lastNumberOfPieces } = this; const { run, recycle, numberOfPieces, debug, tweenFunction, tweenDuration, } = this.getOptions(); if (!run) { return false; } const nP = this.particles.length; const activeCount = recycle ? nP : particlesGenerated; const now = Date.now(); // Initial population if (activeCount < numberOfPieces) { // Use the numberOfPieces prop as a key to reset the easing timing if (lastNumberOfPieces !== numberOfPieces) { this.tweenInitTime = now; this.lastNumberOfPieces = numberOfPieces; } const { tweenInitTime } = this; // Add more than one piece per loop, otherwise the number of pieces would // be limitted by the RAF framerate const progressTime = now - tweenInitTime > tweenDuration ? tweenDuration : Math.max(0, now - tweenInitTime); const tweenedVal = tweenFunction(progressTime, activeCount, numberOfPieces, tweenDuration); const numToAdd = Math.round(tweenedVal - activeCount); for (let i = 0; i < numToAdd; i++) { this.particles.push(this.getParticle()); } this.particlesGenerated += numToAdd; } if (debug) { // Draw debug text context.font = '12px sans-serif'; context.fillStyle = '#333'; context.textAlign = 'right'; context.fillText(`Particles: ${nP}`, canvas.width - 10, canvas.height - 20); } // Maintain the population this.particles.forEach((p, i) => { // Update each particle's position p.update(); // Prune the off-canvas particles if (p.y > canvas.height || p.y < -100 || p.x > canvas.width + 100 || p.x < -100) { if (recycle && activeCount <= numberOfPieces) { // Replace the particle with a brand new one this.particles[i] = this.getParticle(); } else { this.removeParticleAt(i); } } }); return nP > 0 || activeCount < numberOfPieces; }; this.canvas = canvas; const ctx = this.canvas.getContext('2d'); if (!ctx) { throw new Error('Could not get canvas context'); } this.context = ctx; this.getOptions = getOptions; } } const confettiDefaults = { width: typeof window !== 'undefined' ? window.innerWidth : 300, height: typeof window !== 'undefined' ? window.innerHeight : 200, numberOfPieces: 200, friction: 0.99, wind: 0, gravity: 0.1, initialVelocityX: 4, initialVelocityY: 10, colors: [ '#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', ], opacity: 1.0, debug: false, tweenFunction: tweens__namespace.easeInOutQuad, tweenDuration: 5000, recycle: true, run: true, }; class Confetti { constructor(canvas, opts) { this.lastFrameTime = Date.now(); this.setOptionsWithDefaults = (opts) => { const computedConfettiDefaults = { confettiSource: { x: 0, y: 0, w: this.canvas.width, h: 0, }, }; this._options = { ...computedConfettiDefaults, ...confettiDefaults, ...opts, }; Object.assign(this, opts.confettiSource); }; this.update = () => { const { options: { run, onConfettiComplete, frameRate }, canvas, context, } = this; // Throttle the frame rate if set if (frameRate) { const now = Date.now(); const elapsed = now - this.lastFrameTime; if (elapsed < 1000 / frameRate) { this.rafId = requestAnimationFrame(this.update); return; } this.lastFrameTime = now - (elapsed % frameRate); } if (run) { context.fillStyle = 'white'; context.clearRect(0, 0, canvas.width, canvas.height); } if (this.generator.animate()) { this.rafId = requestAnimationFrame(this.update); } else { if (onConfettiComplete && typeof onConfettiComplete === 'function' && this.generator.particlesGenerated > 0) { onConfettiComplete.call(this, this); } this._options.run = false; } }; this.reset = () => { if (this.generator && this.generator.particlesGenerated > 0) { this.generator.particlesGenerated = 0; this.generator.particles = []; this.generator.lastNumberOfPieces = 0; } }; this.stop = () => { this.options = { run: false }; if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = undefined; } }; this.canvas = canvas; const ctx = this.canvas.getContext('2d'); if (!ctx) { throw new Error('Could not get canvas context'); } this.context = ctx; this.generator = new ParticleGenerator(this.canvas, () => this.options); this.options = opts; this.update(); } get options() { return this._options; } set options(opts) { const lastRunState = this._options?.run; const lastRecycleState = this._options?.recycle; this.setOptionsWithDefaults(opts); if (this.generator) { Object.assign(this.generator, this.options.confettiSource); if (typeof opts.recycle === 'boolean' && opts.recycle && lastRecycleState === false) { this.generator.lastNumberOfPieces = this.generator.particles.length; } } if (typeof opts.run === 'boolean' && opts.run && lastRunState === false) { this.update(); } } } const ref = React.createRef(); class ReactConfettiInternal extends React.Component { constructor(props) { super(props); this.canvas = React.createRef(); this.canvas = props.canvasRef || ref; } componentDidMount() { if (this.canvas.current) { const opts = extractCanvasProps(this.props)[0]; this.confetti = new Confetti(this.canvas.current, opts); } } componentDidUpdate() { const confettiOptions = extractCanvasProps(this.props)[0]; if (this.confetti) { this.confetti.options = confettiOptions; } } componentWillUnmount() { if (this.confetti) { this.confetti.stop(); } this.confetti = undefined; } render() { const [confettiOptions, passedProps] = extractCanvasProps(this.props); const canvasStyles = { zIndex: 2, position: 'absolute', pointerEvents: 'none', top: 0, left: 0, bottom: 0, right: 0, ...passedProps.style, }; return (jsxRuntime.jsx("canvas", { width: confettiOptions.width, height: confettiOptions.height, ref: this.canvas, ...passedProps, style: canvasStyles })); } } ReactConfettiInternal.defaultProps = { ...confettiDefaults, }; ReactConfettiInternal.displayName = 'ReactConfetti'; function extractCanvasProps(props) { const confettiOptions = {}; const refs = {}; const rest = {}; const confettiOptionKeys = [ ...Object.keys(confettiDefaults), 'confettiSource', 'drawShape', 'onConfettiComplete', 'frameRate', ]; const refProps = ['canvasRef']; for (const prop in props) { const val = props[prop]; if (confettiOptionKeys.includes(prop)) { confettiOptions[prop] = val; } else if (refProps.includes(prop)) { refProps[prop] = val; } else { rest[prop] = val; } } return [confettiOptions, rest, refs]; } const ReactConfetti = React.forwardRef((props, ref) => jsxRuntime.jsx(ReactConfettiInternal, { canvasRef: ref, ...props })); return ReactConfetti; })(jsxRuntime, React, tweens); //# sourceMappingURL=react-confetti.iife.js.map