UNPKG

react-snowfall

Version:

A react component that creates a snowfall effect

171 lines 7.42 kB
import isEqual from 'react-fast-compare'; import { lerp, random, randomElement, twoPi } from './utils.js'; export const defaultConfig = { color: '#dee4fd', radius: [0.5, 3.0], speed: [1.0, 3.0], wind: [-0.5, 2.0], changeFrequency: 200, rotationSpeed: [-1.0, 1.0], opacity: [1, 1], }; /** * An individual snowflake that will update it's location every call to `update` * and draw itself to the canvas every call to `draw`. */ class Snowflake { /** * A utility function to create a collection of snowflakes * @param canvas The canvas element * @param amount The number of snowflakes * @param config The configuration for each snowflake */ static createSnowflakes(canvas, amount, config) { if (!canvas) return []; const snowflakes = []; for (let i = 0; i < amount; i++) { snowflakes.push(new Snowflake(canvas, config)); } return snowflakes; } constructor(canvas, config = {}) { // Set custom config this.updateConfig(config); // Setting initial parameters const { radius, wind, speed, rotationSpeed, opacity } = this.config; this.params = { x: random(0, canvas.offsetWidth), y: random(-canvas.offsetHeight, 0), rotation: random(0, 360), radius: random(...radius), speed: random(...speed), wind: random(...wind), rotationSpeed: random(...rotationSpeed), nextSpeed: random(...speed), nextWind: random(...wind), nextRotationSpeed: random(...rotationSpeed), opacity: random(...opacity), }; this.framesSinceLastUpdate = 0; } selectImage() { if (this.config.images && this.config.images.length > 0) { this.image = randomElement(this.config.images); } else { this.image = undefined; } } updateConfig(config) { const previousConfig = this.config; this.config = { ...defaultConfig, ...config }; this.config.changeFrequency = random(this.config.changeFrequency, this.config.changeFrequency * 1.5); // Update the radius if the config has changed, it won't gradually update on it's own if (this.params && !isEqual(this.config.radius, previousConfig === null || previousConfig === void 0 ? void 0 : previousConfig.radius)) { this.params.radius = random(...this.config.radius); } if (!isEqual(this.config.images, previousConfig === null || previousConfig === void 0 ? void 0 : previousConfig.images)) { this.selectImage(); } } updateTargetParams() { this.params.nextSpeed = random(...this.config.speed); this.params.nextWind = random(...this.config.wind); if (this.image) { this.params.nextRotationSpeed = random(...this.config.rotationSpeed); } } update(offsetWidth, offsetHeight, framesPassed = 1) { const { x, y, rotation, rotationSpeed, nextRotationSpeed, wind, speed, nextWind, nextSpeed, radius } = this.params; // Update current location, wrapping around if going off the canvas this.params.x = (x + wind * framesPassed) % (offsetWidth + radius * 2); if (this.params.x > offsetWidth + radius) this.params.x = -radius; this.params.y = (y + speed * framesPassed) % (offsetHeight + radius * 2); if (this.params.y > offsetHeight + radius) this.params.y = -radius; // Apply rotation if (this.image) { this.params.rotation = (rotation + rotationSpeed) % 360; } // Update the wind, speed and rotation towards the desired values this.params.speed = lerp(speed, nextSpeed, 0.01); this.params.wind = lerp(wind, nextWind, 0.01); this.params.rotationSpeed = lerp(rotationSpeed, nextRotationSpeed, 0.01); if (this.framesSinceLastUpdate++ > this.config.changeFrequency) { this.updateTargetParams(); this.framesSinceLastUpdate = 0; } } getImageOffscreenCanvas(image, size) { var _a, _b; if (image instanceof HTMLImageElement && image.loading) return image; let sizes = Snowflake.offscreenCanvases.get(image); if (!sizes) { sizes = {}; Snowflake.offscreenCanvases.set(image, sizes); } if (!(size in sizes)) { const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; (_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.drawImage(image, 0, 0, size, size); sizes[size] = canvas; } return (_b = sizes[size]) !== null && _b !== void 0 ? _b : image; } /** * Draws a circular snowflake to the canvas. * * This method should only be called if our config does not have images. * * This method assumes that a path has already been started on the canvas. * `ctx.beginPath()` should be called before calling this method. * * After calling this method, the fillStyle should be set to the snowflake's * color and `ctx.fill()` should be called to fill the snowflake. * * Calling `ctx.fill()` after multiple snowflakes have had `drawCircle` called * will render all of the snowflakes since the last call to `ctx.beginPath()`. * * @param ctx The canvas context to draw to */ drawCircle(ctx) { ctx.moveTo(this.params.x, this.params.y); ctx.arc(this.params.x, this.params.y, this.params.radius, 0, twoPi); } /** * Draws an image-based snowflake to the canvas. * * This method should only be called if our config has images. * * @param ctx The canvas context to draw to */ drawImage(ctx) { const { x, y, rotation, radius } = this.params; const radian = (rotation * Math.PI) / 180; const cos = Math.cos(radian); const sin = Math.sin(radian); // Save the current state to avoid affecting other drawings if changing the opacity if (this.params.opacity !== 1) { ctx.save(); ctx.globalAlpha = this.params.opacity; // Set the global alpha to the snowflake's opacity } // Translate to the location that we will be drawing the snowflake, including any rotation that needs to be applied // The arguments for setTransform are: a, b, c, d, e, f // a (scaleX), b (skewY), c (skewX), d (scaleY), e (translateX), f (translateY) ctx.setTransform(cos, sin, -sin, cos, x, y); // Draw the image with the center of the image at the center of the current location const image = this.getImageOffscreenCanvas(this.image, radius); ctx.drawImage(image, -(radius / 2), -(radius / 2), radius, radius); // Reset the transform to avoid affecting other drawings if we were changing the opacity if (this.params.opacity !== 1) { ctx.restore(); } } } Snowflake.offscreenCanvases = new WeakMap(); export default Snowflake; //# sourceMappingURL=Snowflake.js.map