react-snowfall
Version:
A react component that creates a snowfall effect
118 lines (94 loc) • 3.43 kB
text/typescript
import Snowflake, { SnowflakeConfig, defaultConfig } from './Snowflake'
import { targetFrameTime } from './config'
export interface SnowfallCanvasConfig extends SnowflakeConfig {
/**
* The number of snowflakes to be rendered.
*
* The default value is 150.
*/
snowflakeCount: number
}
export class SnowfallCanvas {
private lastUpdate = Date.now()
private snowflakes: Snowflake[] = []
private config: SnowfallCanvasConfig
#ctx: CanvasRenderingContext2D | null
get ctx() {
return this.#ctx
}
#canvas: HTMLCanvasElement
get canvas() {
return this.#canvas
}
set canvas(canvas: HTMLCanvasElement) {
this.#canvas = canvas
this.#ctx = canvas.getContext('2d')
}
constructor(canvas: HTMLCanvasElement, config: Partial<SnowfallCanvasConfig>) {
this.#canvas = canvas
this.#ctx = canvas.getContext('2d')
this.config = { snowflakeCount: 150, ...defaultConfig, ...config }
this.snowflakes = []
this.snowflakes = Snowflake.createSnowflakes(canvas, config.snowflakeCount || 150, config)
this.play()
}
/**
* Updates the config used for the snowfall animation, if the number of snowflakes
* has changed then this will create new or remove existing snowflakes gracefully
* to retain the position of as many existing snowflakes as possible.
*/
updateConfig(config: Partial<SnowfallCanvasConfig>) {
this.config = { ...this.config, ...config }
const sizeDifference = this.config.snowflakeCount - this.snowflakes.length
if (sizeDifference > 0) {
this.snowflakes = [...this.snowflakes, ...Snowflake.createSnowflakes(this.canvas, sizeDifference, config)]
}
if (sizeDifference < 0) {
this.snowflakes = this.snowflakes.slice(0, this.config.snowflakeCount)
}
this.snowflakes.forEach((snowflake) => snowflake.updateConfig(this.config))
}
/**
* Updates the location of each snowflake based on the number of frames passed then
* clears the canvas and draws each snowflake.
*/
private render(framesPassed = 1) {
const { ctx, canvas, snowflakes } = this
const { offsetWidth, offsetHeight } = canvas
// Update the position of each snowflake
snowflakes.forEach((snowflake) => snowflake.update(offsetWidth, offsetHeight, framesPassed))
// Render them if the canvas is available
if (ctx) {
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, offsetWidth, offsetHeight)
snowflakes.forEach((snowflake) => snowflake.draw(ctx))
}
}
private animationFrame: number | undefined
/**
* The animation loop, will calculate the time since the last render and update
* the position of the snowflakes appropriately before queueing another frame.
*/
private loop() {
// Update based on time passed so that a slow frame rate won't slow down the snowflake
const now = Date.now()
const msPassed = Date.now() - this.lastUpdate
this.lastUpdate = now
// Frames that would have passed if running at 60 fps
const framesPassed = msPassed / targetFrameTime
this.render(framesPassed)
this.animationFrame = requestAnimationFrame(() => this.loop())
}
/** Start the animation playing. */
play() {
this.loop()
}
/** Pause the animation. */
pause() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame)
this.animationFrame = undefined
}
}
}
export default SnowfallCanvas