Skip to content

Commit 5d7b3f5

Browse files
committed
feat(SnowflakeConfig): allow additional snowflake properties to be overridden via the Snowfall props
1 parent c47c36f commit 5d7b3f5

File tree

4 files changed

+116
-14
lines changed

4 files changed

+116
-14
lines changed

src/Snowfall.tsx

+22-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
1-
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
1+
import React, { useCallback, useEffect, useRef } from 'react'
22
import { targetFrameTime } from './config'
3-
import { useComponentSize, useSnowfallStyle, useSnowflakes } from './hooks'
3+
import { useComponentSize, useSnowfallStyle, useSnowflakes, useDeepMemo } from './hooks'
4+
import { SnowflakeProps, defaultConfig } from './Snowflake'
45

5-
export interface SnowfallProps {
6-
color?: string
6+
export interface SnowfallProps extends Partial<SnowflakeProps> {
7+
/**
8+
* The number of snowflakes to be rendered.
9+
*
10+
* The default value is 150.
11+
*/
712
snowflakeCount?: number
13+
/**
14+
* Any style properties that will be passed to the canvas element.
15+
*/
816
style?: React.CSSProperties
917
}
1018

11-
const Snowfall = ({ color = '#dee4fd', snowflakeCount = 150, style }: SnowfallProps = {}) => {
19+
const Snowfall = ({
20+
color = defaultConfig.color,
21+
changeFrequency = defaultConfig.changeFrequency,
22+
radius = defaultConfig.radius,
23+
speed = defaultConfig.speed,
24+
wind = defaultConfig.wind,
25+
snowflakeCount = 150,
26+
style,
27+
}: SnowfallProps = {}) => {
1228
const mergedStyle = useSnowfallStyle(style)
1329

1430
const canvasRef = useRef<HTMLCanvasElement>()
1531
const canvasSize = useComponentSize(canvasRef)
1632
const animationFrame = useRef(0)
1733

1834
const lastUpdate = useRef(Date.now())
19-
const config = useMemo(() => ({ color }), [color])
35+
const config = useDeepMemo<SnowflakeProps>({ color, changeFrequency, radius, speed, wind })
2036
const snowflakes = useSnowflakes(canvasRef, snowflakeCount, config)
2137

2238
const updateCanvasRef = (element: HTMLCanvasElement) => {

src/Snowflake.ts

+39-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,53 @@
11
import { lerp, random } from './utils'
22

33
export interface SnowflakeProps {
4+
/** The color of the snowflake, can be any valid CSS color. */
45
color: string
5-
radius: [number, number]
6-
speed: [number, number]
7-
wind: [number, number]
6+
/**
7+
* The minimum and maximum radius of the snowflake, will be
8+
* randomly selected within this range.
9+
*
10+
* The default value is `[0.5, 3.0]`.
11+
*/
12+
radius: [minimumRadius: number, maximumRadius: number]
13+
/**
14+
* The minimum and maximum speed of the snowflake.
15+
*
16+
* The speed determines how quickly the snowflake moves
17+
* along the y axis (vertical speed).
18+
*
19+
* The values will be randomly selected within this range.
20+
*
21+
* The default value is `[1.0, 3.0]`.
22+
*/
23+
speed: [minimumSpeed: number, maximumSpeed: number]
24+
/**
25+
* The minimum and maximum wind of the snowflake.
26+
*
27+
* The wind determines how quickly the snowflake moves
28+
* along the x axis (horizontal speed).
29+
*
30+
* The values will be randomly selected within this range.
31+
*
32+
* The default value is `[-0.5, 2.0]`.
33+
*/
34+
wind: [minimumWind: number, maximumWind: number]
35+
/**
36+
* The frequency in frames that the wind and speed values
37+
* will update.
38+
*
39+
* The default value is 200.
40+
*/
841
changeFrequency: number
942
}
1043

1144
export type SnowflakeConfig = Partial<SnowflakeProps>
1245

13-
const defaultConfig: SnowflakeProps = {
46+
export const defaultConfig: SnowflakeProps = {
1447
color: '#dee4fd',
1548
radius: [0.5, 3.0],
16-
speed: [1, 3],
17-
wind: [-0.5, 2],
49+
speed: [1.0, 3.0],
50+
wind: [-0.5, 2.0],
1851
changeFrequency: 200,
1952
}
2053

src/hooks.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import { useCallback, useLayoutEffect, useEffect, useState, MutableRefObject, CSSProperties, useMemo } from 'react'
1+
import {
2+
DependencyList,
3+
EffectCallback,
4+
useCallback,
5+
useLayoutEffect,
6+
useEffect,
7+
useRef,
8+
useState,
9+
MutableRefObject,
10+
CSSProperties,
11+
useMemo,
12+
} from 'react'
13+
import isEqual from 'react-fast-compare'
214
import Snowflake, { SnowflakeConfig } from './Snowflake'
315
import { snowfallBaseStyle } from './config'
416
import { getSize } from './utils'
@@ -116,3 +128,40 @@ export const useSnowfallStyle = (overrides?: CSSProperties) => {
116128

117129
return styles
118130
}
131+
132+
/**
133+
* Same as `React.useEffect` but uses a deep comparison on the dependency array. This should only
134+
* be used when working with non-primitive dependencies.
135+
*
136+
* @param effect Effect callback to run
137+
* @param deps Effect dependencies
138+
*/
139+
export function useDeepCompareEffect(effect: EffectCallback, deps: DependencyList) {
140+
const ref = useRef<DependencyList>(deps)
141+
142+
// Only update the current dependencies if they are not deep equal
143+
if (!isEqual(deps, ref.current)) {
144+
ref.current = deps
145+
}
146+
147+
useEffect(effect, ref.current)
148+
}
149+
150+
/**
151+
* Utility hook to stabilize a reference to a value, the returned value will always match the input value
152+
* but (unlike an inline object) will maintain [SameValueZero](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
153+
* equality until a change is made.
154+
*
155+
* @example
156+
*
157+
* const obj = useDeepMemo({ foo: 'bar', bar: 'baz' }) // <- inline object creation
158+
* const prevValue = usePrevious(obj) // <- value from the previous render
159+
* console.log(obj === prevValue) // <- always logs true until value changes
160+
*/
161+
export function useDeepMemo<T>(value: T): T {
162+
const [state, setState] = useState(value)
163+
164+
useDeepCompareEffect(() => setState(value), [value])
165+
166+
return state
167+
}

src/typings/ResizeObserver.d.ts src/typings/ResizeObserver.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ interface Window {
44
ResizeObserver: ResizeObserver
55
}
66

7+
interface ResizeObserverOptions {
8+
box?: 'border-box' | 'content-box' | 'device-pixel-content-box'
9+
}
10+
711
/**
812
* The ResizeObserver interface is used to observe changes to Element's content
913
* rect.
@@ -16,7 +20,7 @@ interface ResizeObserver {
1620
/**
1721
* Adds target to the list of observed elements.
1822
*/
19-
observe: (target: Element) => void
23+
observe: (target: Element, options?: ResizeObserverOptions) => void
2024

2125
/**
2226
* Removes target from the list of observed elements.

0 commit comments

Comments
 (0)