From 5d7b3f53bc64f7724c77afcaba2c1f224611bc0a Mon Sep 17 00:00:00 2001 From: Cahil Foley Date: Wed, 24 Feb 2021 00:10:50 +0800 Subject: [PATCH] feat(SnowflakeConfig): allow additional snowflake properties to be overridden via the Snowfall props --- src/Snowfall.tsx | 28 +++++++--- src/Snowflake.ts | 45 +++++++++++++--- src/hooks.ts | 51 ++++++++++++++++++- ...{ResizeObserver.d.ts => ResizeObserver.ts} | 6 ++- 4 files changed, 116 insertions(+), 14 deletions(-) rename src/typings/{ResizeObserver.d.ts => ResizeObserver.ts} (87%) diff --git a/src/Snowfall.tsx b/src/Snowfall.tsx index 38fc0ac..7b9b5ca 100644 --- a/src/Snowfall.tsx +++ b/src/Snowfall.tsx @@ -1,14 +1,30 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react' +import React, { useCallback, useEffect, useRef } from 'react' import { targetFrameTime } from './config' -import { useComponentSize, useSnowfallStyle, useSnowflakes } from './hooks' +import { useComponentSize, useSnowfallStyle, useSnowflakes, useDeepMemo } from './hooks' +import { SnowflakeProps, defaultConfig } from './Snowflake' -export interface SnowfallProps { - color?: string +export interface SnowfallProps extends Partial { + /** + * The number of snowflakes to be rendered. + * + * The default value is 150. + */ snowflakeCount?: number + /** + * Any style properties that will be passed to the canvas element. + */ style?: React.CSSProperties } -const Snowfall = ({ color = '#dee4fd', snowflakeCount = 150, style }: SnowfallProps = {}) => { +const Snowfall = ({ + color = defaultConfig.color, + changeFrequency = defaultConfig.changeFrequency, + radius = defaultConfig.radius, + speed = defaultConfig.speed, + wind = defaultConfig.wind, + snowflakeCount = 150, + style, +}: SnowfallProps = {}) => { const mergedStyle = useSnowfallStyle(style) const canvasRef = useRef() @@ -16,7 +32,7 @@ const Snowfall = ({ color = '#dee4fd', snowflakeCount = 150, style }: SnowfallPr const animationFrame = useRef(0) const lastUpdate = useRef(Date.now()) - const config = useMemo(() => ({ color }), [color]) + const config = useDeepMemo({ color, changeFrequency, radius, speed, wind }) const snowflakes = useSnowflakes(canvasRef, snowflakeCount, config) const updateCanvasRef = (element: HTMLCanvasElement) => { diff --git a/src/Snowflake.ts b/src/Snowflake.ts index 115bc9d..436c417 100644 --- a/src/Snowflake.ts +++ b/src/Snowflake.ts @@ -1,20 +1,53 @@ import { lerp, random } from './utils' export interface SnowflakeProps { + /** The color of the snowflake, can be any valid CSS color. */ color: string - radius: [number, number] - speed: [number, number] - wind: [number, number] + /** + * The minimum and maximum radius of the snowflake, will be + * randomly selected within this range. + * + * The default value is `[0.5, 3.0]`. + */ + radius: [minimumRadius: number, maximumRadius: number] + /** + * The minimum and maximum speed of the snowflake. + * + * The speed determines how quickly the snowflake moves + * along the y axis (vertical speed). + * + * The values will be randomly selected within this range. + * + * The default value is `[1.0, 3.0]`. + */ + speed: [minimumSpeed: number, maximumSpeed: number] + /** + * The minimum and maximum wind of the snowflake. + * + * The wind determines how quickly the snowflake moves + * along the x axis (horizontal speed). + * + * The values will be randomly selected within this range. + * + * The default value is `[-0.5, 2.0]`. + */ + wind: [minimumWind: number, maximumWind: number] + /** + * The frequency in frames that the wind and speed values + * will update. + * + * The default value is 200. + */ changeFrequency: number } export type SnowflakeConfig = Partial -const defaultConfig: SnowflakeProps = { +export const defaultConfig: SnowflakeProps = { color: '#dee4fd', radius: [0.5, 3.0], - speed: [1, 3], - wind: [-0.5, 2], + speed: [1.0, 3.0], + wind: [-0.5, 2.0], changeFrequency: 200, } diff --git a/src/hooks.ts b/src/hooks.ts index be2083f..9c5a4a8 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,4 +1,16 @@ -import { useCallback, useLayoutEffect, useEffect, useState, MutableRefObject, CSSProperties, useMemo } from 'react' +import { + DependencyList, + EffectCallback, + useCallback, + useLayoutEffect, + useEffect, + useRef, + useState, + MutableRefObject, + CSSProperties, + useMemo, +} from 'react' +import isEqual from 'react-fast-compare' import Snowflake, { SnowflakeConfig } from './Snowflake' import { snowfallBaseStyle } from './config' import { getSize } from './utils' @@ -116,3 +128,40 @@ export const useSnowfallStyle = (overrides?: CSSProperties) => { return styles } + +/** + * Same as `React.useEffect` but uses a deep comparison on the dependency array. This should only + * be used when working with non-primitive dependencies. + * + * @param effect Effect callback to run + * @param deps Effect dependencies + */ +export function useDeepCompareEffect(effect: EffectCallback, deps: DependencyList) { + const ref = useRef(deps) + + // Only update the current dependencies if they are not deep equal + if (!isEqual(deps, ref.current)) { + ref.current = deps + } + + useEffect(effect, ref.current) +} + +/** + * Utility hook to stabilize a reference to a value, the returned value will always match the input value + * but (unlike an inline object) will maintain [SameValueZero](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * equality until a change is made. + * + * @example + * + * const obj = useDeepMemo({ foo: 'bar', bar: 'baz' }) // <- inline object creation + * const prevValue = usePrevious(obj) // <- value from the previous render + * console.log(obj === prevValue) // <- always logs true until value changes + */ +export function useDeepMemo(value: T): T { + const [state, setState] = useState(value) + + useDeepCompareEffect(() => setState(value), [value]) + + return state +} diff --git a/src/typings/ResizeObserver.d.ts b/src/typings/ResizeObserver.ts similarity index 87% rename from src/typings/ResizeObserver.d.ts rename to src/typings/ResizeObserver.ts index b880dab..089abf2 100644 --- a/src/typings/ResizeObserver.d.ts +++ b/src/typings/ResizeObserver.ts @@ -4,6 +4,10 @@ interface Window { ResizeObserver: ResizeObserver } +interface ResizeObserverOptions { + box?: 'border-box' | 'content-box' | 'device-pixel-content-box' +} + /** * The ResizeObserver interface is used to observe changes to Element's content * rect. @@ -16,7 +20,7 @@ interface ResizeObserver { /** * Adds target to the list of observed elements. */ - observe: (target: Element) => void + observe: (target: Element, options?: ResizeObserverOptions) => void /** * Removes target from the list of observed elements.