UNPKG

let-it-go

Version:

❄️ Let your website snow instantly

281 lines (214 loc) 6.97 kB
/* eslint-disable no-param-reassign */ /* eslint-disable no-underscore-dangle */ import { Vec2D, Snowflake, assert, assertIsAlphaRange, assertIsRadiusRange, assertIsRange, getRandom, setStyleProps, } from './utils'; import { DEFAULT_OPTIONS } from './constants'; import type { Range, Options } from './types'; export class LetItGo { readonly root = document.body; #isGo = false; #number = 0; get number(): number { return this.#number; } set number(number: number) { assert(number >= 0, 'Number must be positive'); this.#number = number; this.#createSnowflakes(); } #velocityXRange: Range; get velocityXRange(): Range { return this.#velocityXRange; } set velocityXRange(range: Range) { assertIsRange(range); const _range = range.sort(); this.#velocityXRange = _range; this.#snowflakes.forEach((snowflake) => { snowflake.v.x = getRandom(..._range); }); } #velocityYRange: Range; get velocityYRange(): Range { return this.#velocityYRange; } set velocityYRange(range: Range) { assertIsRange(range); const sortedRange = range.sort(); this.#velocityYRange = sortedRange; this.#snowflakes.forEach((snowflake) => { snowflake.v.y = getRandom(...sortedRange); }); } #radiusRange: Range; get radiusRange(): Range { return this.#radiusRange; } set radiusRange(range: Range) { assertIsRadiusRange(range); const _range = range.sort(); this.#radiusRange = _range; this.#snowflakes.forEach((snowflake) => { snowflake.r = getRandom(..._range); }); } #color: CanvasFillStrokeStyles['fillStyle']; get color(): CanvasFillStrokeStyles['fillStyle'] { return this.#color; } set color(color: CanvasFillStrokeStyles['fillStyle']) { this.#color = color; this.#snowflakes.forEach((snowflake) => { snowflake.color = color; }); } #alphaRange: Range; get alphaRange(): Range { return this.#alphaRange; } set alphaRange(range: Range) { assertIsAlphaRange(range); const sortedRange = range.sort(); this.#alphaRange = sortedRange; this.#snowflakes.forEach((snowflake) => { snowflake.alpha = getRandom(...sortedRange); }); } backgroundColor: CanvasFillStrokeStyles['fillStyle']; style = DEFAULT_OPTIONS.style; readonly canvas: HTMLCanvasElement = document.createElement('canvas'); readonly #ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; #snowflakes: Snowflake[] = []; #intervalID: number | null = null; #requestID: number | null = null; static readonly DEFAULT_OPTIONS = DEFAULT_OPTIONS; constructor({ root = DEFAULT_OPTIONS.root, number = DEFAULT_OPTIONS.number, velocityXRange = DEFAULT_OPTIONS.velocityXRange, velocityYRange = DEFAULT_OPTIONS.velocityYRange, radiusRange = DEFAULT_OPTIONS.radiusRange, color = DEFAULT_OPTIONS.color, alphaRange = DEFAULT_OPTIONS.alphaRange, backgroundColor = DEFAULT_OPTIONS.backgroundColor, style = DEFAULT_OPTIONS.style, }: Readonly<Options> = {}) { assertIsRange(velocityXRange); assertIsRange(velocityYRange); assertIsRadiusRange(radiusRange); assertIsAlphaRange(alphaRange); this.root = root; this.#number = number; this.#velocityXRange = velocityXRange.sort(); this.#velocityYRange = velocityYRange.sort(); this.#radiusRange = radiusRange.sort(); this.#color = color; this.#alphaRange = alphaRange.sort(); this.backgroundColor = backgroundColor; this.style = style; // TODO: use OffscreenCanvas when is possible // const ctx = this.canvas.transferControlToOffscreen().getContext('2d'); const ctx = this.canvas.getContext('2d'); if (!ctx) throw new Error('[let-it-go] The 2d context canvas is not supported.'); this.#ctx = ctx; this.#mountCanvas(); this.#createSnowflakes(); this.#startAnimate(); } #resizeObserver: ResizeObserver | null = null; #mountCanvas(): void { try { const resizeObserver = new ResizeObserver((entries) => { // eslint-disable-next-line no-restricted-syntax for (const entry of entries) { this.canvas.width = entry.contentRect.width; this.canvas.height = entry.contentRect.height; } }); resizeObserver.observe(this.root); this.#resizeObserver = resizeObserver; } catch (error) { // eslint-disable-next-line no-console console.warn('[let-it-go] ResizeObserver is not supported.', error); } this.canvas.width = this.root.clientWidth; this.canvas.height = this.root.clientHeight; setStyleProps(this.root, { position: 'relative' }); setStyleProps(this.canvas, { position: 'absolute', top: '0', left: '0', ...this.style, }); this.root.appendChild(this.canvas); } #createSnowflakes(): void { const { canvas } = this; this.#snowflakes = Array.from( { length: this.#number }, () => new Snowflake({ p: new Vec2D( getRandom(0, canvas.width), getRandom(0, -canvas.height), ), v: new Vec2D( getRandom(...this.#velocityXRange) || Number.MIN_VALUE, getRandom(...this.#velocityYRange) || Number.MIN_VALUE, ), r: getRandom(...this.#radiusRange) || Number.MIN_VALUE, color: this.#color, alpha: getRandom(...this.#alphaRange) || Number.MIN_VALUE, }), ); } #update = (): void => { this.#snowflakes.forEach( (snowflake) => snowflake.update(this.canvas), ); }; #draw = (): void => { if (!this.#isGo) return; const { width, height } = this.canvas; this.#ctx.clearRect(0, 0, width, height); this.#ctx.fillStyle = this.backgroundColor; this.#ctx.fillRect(0, 0, width, height); this.#snowflakes.forEach((snowflake) => snowflake.draw(this.#ctx)); this.#requestID = requestAnimationFrame(this.#draw); }; static readonly FRAME_RATE = 30; static readonly FRAME_INTERVAL = 1000 / LetItGo.FRAME_RATE; #startAnimate(): void { if (this.#isGo) return; this.#intervalID = setInterval(this.#update, LetItGo.FRAME_INTERVAL); this.#requestID = requestAnimationFrame(this.#draw); this.#isGo = true; } letItStop(): void { this.#isGo = false; if (this.#intervalID) { clearInterval(this.#intervalID); this.#intervalID = null; } if (this.#requestID) { cancelAnimationFrame(this.#requestID); this.#requestID = null; } } letItGoAgain(): void { this.#startAnimate(); } clear(): void { this.letItStop(); this.#snowflakes = []; if (this.#resizeObserver) { this.#resizeObserver.disconnect(); this.#resizeObserver = null; } if (this.canvas.parentNode) { this.root.removeChild(this.canvas); } this.#ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.#number = 0; } } export default LetItGo;