diff --git a/README.md b/README.md index c7f8869..82b6d41 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,36 @@ An optional `color`, `style`, and `snowflakeCount` property can be passed in to /> ``` -## Positioning +## Using Images + +Instead of rendering colored circles you can instead pass in an array of image elements +that will be randomly selected and used as the snowflake instead. + +> _NOTE_: If the images provided are not square they will be stretched into a 1:1 aspect ratio. + +```tsx +const snowflake1 = document.createElement('img') +snowflake1.src = '/assets/snowflake-1.png' +const snowflake2 = document.createElement('img') +snowflake2.src = '/assets/snowflake-2.jpg' + +const images = [snowflake1, snowflake2] + +const Demo = () => { + return ( + + ) +} +``` +## Positioning The snowfall container is absolute positioned and has the following default styles (see [the definition](https://github.com/cahilfoley/react-snowfall/blob/a8e865e82cac3221930773cdfd6b90eeb0b34247/src/config.ts#L4-L10)): @@ -77,7 +105,7 @@ If you want the component to cover the entire screen then you can change the pos style={{ position: 'fixed', width: '100vw', - height: '100vh' + height: '100vh', }} /> ``` diff --git a/package-lock.json b/package-lock.json index 57a4a4c..caf7c83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21810,6 +21810,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -22913,6 +22921,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.1.1.tgz", + "integrity": "sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "packages/demo": { "name": "react-snowfall-demo", "version": "1.0.0", @@ -22930,7 +22961,8 @@ "react-dom": "^18.2.0", "react-scripts": "^5.0.1", "react-snowfall": "*", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "zustand": "^4.1.1" }, "devDependencies": { "@types/react-color": "^3.0.6" @@ -36938,7 +36970,8 @@ "react-dom": "^18.2.0", "react-scripts": "^5.0.1", "react-snowfall": "*", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "zustand": "*" } }, "react-transition-group": { @@ -38981,6 +39014,12 @@ "requires-port": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -39855,6 +39894,14 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zustand": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.1.1.tgz", + "integrity": "sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/packages/demo/package.json b/packages/demo/package.json index 3d47ceb..9456515 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -17,7 +17,8 @@ "react-dom": "^18.2.0", "react-scripts": "^5.0.1", "react-snowfall": "*", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "zustand": "^4.1.1" }, "scripts": { "predeploy": "npm run build", @@ -39,4 +40,4 @@ "devDependencies": { "@types/react-color": "^3.0.6" } -} \ No newline at end of file +} diff --git a/packages/demo/src/App.tsx b/packages/demo/src/App.tsx index c547b7e..defbdeb 100644 --- a/packages/demo/src/App.tsx +++ b/packages/demo/src/App.tsx @@ -1,33 +1,32 @@ -import { useContext } from 'react' import Snowfall from 'react-snowfall' import GithubLink from './components/GithubLink/GithubLink' import Settings from './components/Settings' -import { SettingsContext } from './context/settings' +import { useSettingsStore } from './settings' import logo from './logo.png' import './App.css' const githubURL = process.env.REACT_APP_GITHUB_URL as string const packageName = process.env.REACT_APP_PACKAGE_NAME as string -const imageElement = document.createElement('img') -imageElement.src = logo +const snowflake = document.createElement('img') +snowflake.src = logo -const images = [imageElement] +const images = [snowflake] const App = () => { - const settings = useContext(SettingsContext) + const { color, snowflakeCount, radius, speed, wind, useImages } = useSettingsStore() return (
- + Snowflake Logo

{packageName}

diff --git a/packages/demo/src/components/Settings/Settings.tsx b/packages/demo/src/components/Settings/Settings.tsx index 4903cfd..0259b24 100644 --- a/packages/demo/src/components/Settings/Settings.tsx +++ b/packages/demo/src/components/Settings/Settings.tsx @@ -1,14 +1,21 @@ -import { useContext } from 'react' -import { CirclePicker } from 'react-color' import Box from '@mui/material/Box' +import Checkbox from '@mui/material/Checkbox' +import Chip, { ChipProps } from '@mui/material/Chip' +import FormControlLabel from '@mui/material/FormControlLabel' import Paper from '@mui/material/Paper' -import Typography from '@mui/material/Typography' import Slider from '@mui/material/Slider' -import { SettingsContext, SnowfallSettings } from '../../context/settings' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import { CirclePicker } from 'react-color' +import { useSettingsStore } from '../../settings' import { ThemeProvider } from './theme' import './Settings.css' +const ValueChip = (props: ChipProps) => { + return +} + const colors = [ '#dee4fd', '#e91e63', @@ -31,58 +38,104 @@ const colors = [ ] const Settings = () => { - const settings = useContext(SettingsContext) as SnowfallSettings + const settings = useSettingsStore() return ( - Snowflake Count - {settings.snowflakeCount} - settings.setSnowflakeCount(value as number)} - /> - - Speed - Min {settings?.speed?.[0]} Max {settings?.speed?.[1]} - - settings.setSpeed(value as [number, number])} - /> - - Wind - Min {settings?.wind?.[0]} Max {settings?.wind?.[1]} - - settings.setWind(value as [number, number])} - /> - - Radius - Min {settings?.radius?.[0]} Max {settings?.radius?.[1]} - - settings.setRadius(value as [number, number])} - /> - - Color - {settings.color} - settings.setColor(value.hex)} - /> - + +
+ + Snowflake Count + + settings.update({ snowflakeCount: value as number })} + /> +
+
+ + Speed + + + settings.update({ speed: value as [number, number] })} + /> +
+
+ + Wind {' '} + + + settings.update({ wind: value as [number, number] })} + /> +
+
+ + Radius + + + settings.update({ radius: value as [number, number] })} + /> +
+
+ settings.setUseImages(event.target.checked)} + /> + } + label="Use Images" + /> +
+ {settings.useImages ? ( +
+ + Rotation Speed + + + + settings.update({ rotationSpeed: value as [number, number] }) + } + /> +
+ ) : ( + + + Color + + settings.update({ color: value.hex })} + /> + + )} +
) diff --git a/packages/demo/src/context/settings.tsx b/packages/demo/src/context/settings.tsx deleted file mode 100644 index ab07987..0000000 --- a/packages/demo/src/context/settings.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { createContext, ReactNode, useState } from 'react' -import { SnowfallProps } from 'react-snowfall' - -export interface SnowfallSettings extends SnowfallProps { - setColor: (color: string) => void - setSnowflakeCount: (count: number) => void - setSpeed: (speed: [number, number]) => void - setWind: (wind: [number, number]) => void - setRadius: (radius: [number, number]) => void -} - -export const SettingsContext = createContext>({}) - -export const StateProvider = ({ children }: { children: ReactNode }) => { - const [color, setColor] = useState('#dee4fd') - const [snowflakeCount, setSnowflakeCount] = useState(200) - const [radius, setRadius] = useState<[number, number]>([0.5, 3.0]) - const [speed, setSpeed] = useState<[number, number]>([0.5, 3.0]) - const [wind, setWind] = useState<[number, number]>([-0.5, 2.0]) - - return ( - - {children} - - ) -} diff --git a/packages/demo/src/index.tsx b/packages/demo/src/index.tsx index 2aa3de3..d375184 100644 --- a/packages/demo/src/index.tsx +++ b/packages/demo/src/index.tsx @@ -1,15 +1,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' -import { StateProvider } from './context/settings' import * as serviceWorkerRegistration from './serviceWorkerRegistration' const root = createRoot(document.getElementById('root') as HTMLDivElement) root.render( - - - + ) diff --git a/packages/demo/src/settings.tsx b/packages/demo/src/settings.tsx new file mode 100644 index 0000000..8cb9231 --- /dev/null +++ b/packages/demo/src/settings.tsx @@ -0,0 +1,26 @@ +import { SnowfallProps } from 'react-snowfall' +import create from 'zustand' + +export interface SnowfallSettings extends SnowfallProps { + update: (settings: Partial) => void + useImages: boolean + setUseImages: (useImages: boolean) => void +} + +export const useSettingsStore = create((set, get) => ({ + color: '#dee4fd', + snowflakeCount: 200, + radius: [0.5, 3.0], + speed: [0.5, 3.0], + wind: [-0.5, 2.0], + rotationSpeed: [-1.0, 1.0], + useImages: false, + update: (changes) => set(changes), + setUseImages: (useImages) => { + if (useImages) { + return set({ useImages, radius: [5, 20] }) + } else { + return set({ useImages, radius: [0.5, 3] }) + } + } +})) diff --git a/packages/react-snowfall/lib/Snowfall.d.ts b/packages/react-snowfall/lib/Snowfall.d.ts index 4d7cded..5cc1552 100644 --- a/packages/react-snowfall/lib/Snowfall.d.ts +++ b/packages/react-snowfall/lib/Snowfall.d.ts @@ -12,5 +12,5 @@ export interface SnowfallProps extends Partial { */ style?: React.CSSProperties; } -declare const Snowfall: ({ color, changeFrequency, radius, speed, wind, snowflakeCount, images, style, }?: SnowfallProps) => JSX.Element; +declare const Snowfall: ({ color, changeFrequency, radius, speed, wind, rotationSpeed, snowflakeCount, images, style, }?: SnowfallProps) => JSX.Element; export default Snowfall; diff --git a/packages/react-snowfall/lib/Snowfall.js b/packages/react-snowfall/lib/Snowfall.js index 2036d21..979db31 100644 --- a/packages/react-snowfall/lib/Snowfall.js +++ b/packages/react-snowfall/lib/Snowfall.js @@ -31,6 +31,8 @@ var Snowfall = function Snowfall() { speed = _ref$speed === void 0 ? _Snowflake.defaultConfig.speed : _ref$speed, _ref$wind = _ref.wind, wind = _ref$wind === void 0 ? _Snowflake.defaultConfig.wind : _ref$wind, + _ref$rotationSpeed = _ref.rotationSpeed, + rotationSpeed = _ref$rotationSpeed === void 0 ? _Snowflake.defaultConfig.rotationSpeed : _ref$rotationSpeed, _ref$snowflakeCount = _ref.snowflakeCount, snowflakeCount = _ref$snowflakeCount === void 0 ? 150 : _ref$snowflakeCount, images = _ref.images, @@ -47,6 +49,7 @@ var Snowfall = function Snowfall() { radius: radius, speed: speed, wind: wind, + rotationSpeed: rotationSpeed, images: images }); var snowflakes = (0, _hooks.useSnowflakes)(canvasRef, snowflakeCount, config); @@ -63,6 +66,7 @@ var Snowfall = function Snowfall() { var ctx = canvas.getContext('2d'); if (ctx) { + ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight); snowflakes.forEach(function (snowflake) { return snowflake.draw(ctx); diff --git a/packages/react-snowfall/lib/Snowfall.js.map b/packages/react-snowfall/lib/Snowfall.js.map index ffb573d..a406310 100644 --- a/packages/react-snowfall/lib/Snowfall.js.map +++ b/packages/react-snowfall/lib/Snowfall.js.map @@ -1 +1 @@ -{"version":3,"file":"Snowfall.js","names":["Snowfall","color","defaultConfig","changeFrequency","radius","speed","wind","snowflakeCount","images","style","mergedStyle","useSnowfallStyle","canvasRef","useRef","canvasSize","useComponentSize","animationFrame","lastUpdate","Date","now","config","useDeepMemo","snowflakes","useSnowflakes","render","useCallback","framesPassed","canvas","current","forEach","snowflake","update","ctx","getContext","clearRect","offsetWidth","offsetHeight","draw","loop","msPassed","targetFrameTime","requestAnimationFrame","useEffect","cancelAnimationFrame","height","width"],"sources":["../src/Snowfall.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useRef } from 'react'\r\nimport { targetFrameTime } from './config'\r\nimport { useComponentSize, useSnowfallStyle, useSnowflakes, useDeepMemo } from './hooks'\r\nimport { SnowflakeProps, defaultConfig } from './Snowflake'\r\n\r\nexport interface SnowfallProps extends Partial {\r\n /**\r\n * The number of snowflakes to be rendered.\r\n *\r\n * The default value is 150.\r\n */\r\n snowflakeCount?: number\r\n /**\r\n * Any style properties that will be passed to the canvas element.\r\n */\r\n style?: React.CSSProperties\r\n}\r\n\r\nconst Snowfall = ({\r\n color = defaultConfig.color,\r\n changeFrequency = defaultConfig.changeFrequency,\r\n radius = defaultConfig.radius,\r\n speed = defaultConfig.speed,\r\n wind = defaultConfig.wind,\r\n snowflakeCount = 150,\r\n images,\r\n style,\r\n}: SnowfallProps = {}): JSX.Element => {\r\n const mergedStyle = useSnowfallStyle(style)\r\n\r\n const canvasRef = useRef(null)\r\n const canvasSize = useComponentSize(canvasRef)\r\n const animationFrame = useRef(0)\r\n\r\n const lastUpdate = useRef(Date.now())\r\n const config = useDeepMemo({ color, changeFrequency, radius, speed, wind, images })\r\n const snowflakes = useSnowflakes(canvasRef, snowflakeCount, config)\r\n\r\n const render = useCallback(\r\n (framesPassed = 1) => {\r\n const canvas = canvasRef.current\r\n if (canvas) {\r\n // Update the positions of the snowflakes\r\n snowflakes.forEach((snowflake) => snowflake.update(canvas, framesPassed))\r\n\r\n // Render them if the canvas is available\r\n const ctx = canvas.getContext('2d')\r\n if (ctx) {\r\n ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight)\r\n\r\n snowflakes.forEach((snowflake) => snowflake.draw(ctx))\r\n }\r\n }\r\n },\r\n [snowflakes],\r\n )\r\n\r\n const loop = useCallback(() => {\r\n // Update based on time passed so that a slow frame rate won't slow down the snowflake\r\n const now = Date.now()\r\n const msPassed = Date.now() - lastUpdate.current\r\n lastUpdate.current = now\r\n\r\n // Frames that would have passed if running at 60 fps\r\n const framesPassed = msPassed / targetFrameTime\r\n\r\n render(framesPassed)\r\n\r\n animationFrame.current = requestAnimationFrame(loop)\r\n }, [render])\r\n\r\n useEffect(() => {\r\n loop()\r\n return () => cancelAnimationFrame(animationFrame.current)\r\n }, [loop])\r\n\r\n return (\r\n \r\n )\r\n}\r\n\r\nexport default Snowfall\r\n"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"Snowfall.js","names":["Snowfall","color","defaultConfig","changeFrequency","radius","speed","wind","rotationSpeed","snowflakeCount","images","style","mergedStyle","useSnowfallStyle","canvasRef","useRef","canvasSize","useComponentSize","animationFrame","lastUpdate","Date","now","config","useDeepMemo","snowflakes","useSnowflakes","render","useCallback","framesPassed","canvas","current","forEach","snowflake","update","ctx","getContext","setTransform","clearRect","offsetWidth","offsetHeight","draw","loop","msPassed","targetFrameTime","requestAnimationFrame","useEffect","cancelAnimationFrame","height","width"],"sources":["../src/Snowfall.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useRef } from 'react'\r\nimport { targetFrameTime } from './config'\r\nimport { useComponentSize, useSnowfallStyle, useSnowflakes, useDeepMemo } from './hooks'\r\nimport { SnowflakeProps, defaultConfig } from './Snowflake'\r\n\r\nexport interface SnowfallProps extends Partial {\r\n /**\r\n * The number of snowflakes to be rendered.\r\n *\r\n * The default value is 150.\r\n */\r\n snowflakeCount?: number\r\n /**\r\n * Any style properties that will be passed to the canvas element.\r\n */\r\n style?: React.CSSProperties\r\n}\r\n\r\nconst Snowfall = ({\r\n color = defaultConfig.color,\r\n changeFrequency = defaultConfig.changeFrequency,\r\n radius = defaultConfig.radius,\r\n speed = defaultConfig.speed,\r\n wind = defaultConfig.wind,\r\n rotationSpeed = defaultConfig.rotationSpeed,\r\n snowflakeCount = 150,\r\n images,\r\n style,\r\n}: SnowfallProps = {}): JSX.Element => {\r\n const mergedStyle = useSnowfallStyle(style)\r\n\r\n const canvasRef = useRef(null)\r\n const canvasSize = useComponentSize(canvasRef)\r\n const animationFrame = useRef(0)\r\n\r\n const lastUpdate = useRef(Date.now())\r\n const config = useDeepMemo({ color, changeFrequency, radius, speed, wind, rotationSpeed, images })\r\n const snowflakes = useSnowflakes(canvasRef, snowflakeCount, config)\r\n\r\n const render = useCallback(\r\n (framesPassed = 1) => {\r\n const canvas = canvasRef.current\r\n if (canvas) {\r\n // Update the positions of the snowflakes\r\n snowflakes.forEach((snowflake) => snowflake.update(canvas, framesPassed))\r\n\r\n // Render them if the canvas is available\r\n const ctx = canvas.getContext('2d')\r\n if (ctx) {\r\n ctx.setTransform(1, 0, 0, 1, 0, 0)\r\n ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight)\r\n\r\n snowflakes.forEach((snowflake) => snowflake.draw(ctx))\r\n }\r\n }\r\n },\r\n [snowflakes],\r\n )\r\n\r\n const loop = useCallback(() => {\r\n // Update based on time passed so that a slow frame rate won't slow down the snowflake\r\n const now = Date.now()\r\n const msPassed = Date.now() - lastUpdate.current\r\n lastUpdate.current = now\r\n\r\n // Frames that would have passed if running at 60 fps\r\n const framesPassed = msPassed / targetFrameTime\r\n\r\n render(framesPassed)\r\n\r\n animationFrame.current = requestAnimationFrame(loop)\r\n }, [render])\r\n\r\n useEffect(() => {\r\n loop()\r\n return () => cancelAnimationFrame(animationFrame.current)\r\n }, [loop])\r\n\r\n return (\r\n \r\n )\r\n}\r\n\r\nexport default Snowfall\r\n"],"mappings":"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"} \ No newline at end of file diff --git a/packages/react-snowfall/lib/Snowflake.d.ts b/packages/react-snowfall/lib/Snowflake.d.ts index 8f02882..3781892 100644 --- a/packages/react-snowfall/lib/Snowflake.d.ts +++ b/packages/react-snowfall/lib/Snowflake.d.ts @@ -1,4 +1,3 @@ -export declare type SnowflakeImageInput = HTMLImageElement | SVGImageElement | HTMLVideoElement | HTMLCanvasElement | ImageBitmap; export interface SnowflakeProps { /** The color of the snowflake, can be any valid CSS color. */ color: string; @@ -42,7 +41,19 @@ export interface SnowflakeProps { * An array of images that will be rendered as the snowflakes instead * of the default circle shapes. */ - images?: SnowflakeImageInput[]; + images?: CanvasImageSource[]; + /** + * The minimum and maximum rotation speed of the snowflake (in degrees of + * rotation per frame). + * + * The rotation speed determines how quickly the snowflake rotates when + * an image is being rendered. + * + * The values will be randomly selected within this range. + * + * The default value is `[-1.0, 1.0]`. + */ + rotationSpeed: [number, number]; } export declare type SnowflakeConfig = Partial; export declare const defaultConfig: SnowflakeProps; @@ -51,13 +62,17 @@ export declare const defaultConfig: SnowflakeProps; * and draw itself to the canvas every call to `draw`. */ declare class Snowflake { + static offscreenCanvases: WeakMap>; private config; private params; private framesSinceLastUpdate; + private image?; constructor(canvas: HTMLCanvasElement, config?: SnowflakeConfig); + private selectImage; updateConfig(config: SnowflakeConfig): void; private updateTargetParams; update(canvas: HTMLCanvasElement, framesPassed?: number): void; + private getImageOffscreenCanvas; draw(ctx: CanvasRenderingContext2D): void; } export default Snowflake; diff --git a/packages/react-snowfall/lib/Snowflake.js b/packages/react-snowfall/lib/Snowflake.js index b721604..361f759 100644 --- a/packages/react-snowfall/lib/Snowflake.js +++ b/packages/react-snowfall/lib/Snowflake.js @@ -40,7 +40,8 @@ var defaultConfig = { radius: [0.5, 3.0], speed: [1.0, 3.0], wind: [-0.5, 2.0], - changeFrequency: 200 + changeFrequency: 200, + rotationSpeed: [-1.0, 1.0] }; exports.defaultConfig = defaultConfig; @@ -69,7 +70,7 @@ var Snowflake = /*#__PURE__*/function () { radius = _this$config.radius, wind = _this$config.wind, speed = _this$config.speed, - images = _this$config.images; + rotationSpeed = _this$config.rotationSpeed; this.params = { x: (0, _utils.random)(0, canvas.offsetWidth), y: (0, _utils.random)(-canvas.offsetHeight, 0), @@ -77,9 +78,10 @@ var Snowflake = /*#__PURE__*/function () { radius: _utils.random.apply(void 0, _toConsumableArray(radius)), speed: _utils.random.apply(void 0, _toConsumableArray(speed)), wind: _utils.random.apply(void 0, _toConsumableArray(wind)), + rotationSpeed: _utils.random.apply(void 0, _toConsumableArray(rotationSpeed)), nextSpeed: _utils.random.apply(void 0, _toConsumableArray(wind)), nextWind: _utils.random.apply(void 0, _toConsumableArray(speed)), - nextRotation: (0, _utils.random)(0, 360) + nextRotationSpeed: _utils.random.apply(void 0, _toConsumableArray(rotationSpeed)) }; this.framesSinceLastUpdate = 0; } @@ -97,7 +99,8 @@ var Snowflake = /*#__PURE__*/function () { key: "updateConfig", value: function updateConfig(config) { var previousConfig = this.config; - this.config = _objectSpread(_objectSpread({}, defaultConfig), config); // Update the radius if the config has changed, it won't gradually update on it's own + this.config = _objectSpread(_objectSpread({}, defaultConfig), config); + this.config.changeFrequency = (0, _utils.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 && !(0, _reactFastCompare["default"])(this.config.radius, previousConfig === null || previousConfig === void 0 ? void 0 : previousConfig.radius)) { this.params.radius = _utils.random.apply(void 0, _toConsumableArray(this.config.radius)); @@ -112,7 +115,10 @@ var Snowflake = /*#__PURE__*/function () { value: function updateTargetParams() { this.params.nextSpeed = _utils.random.apply(void 0, _toConsumableArray(this.config.speed)); this.params.nextWind = _utils.random.apply(void 0, _toConsumableArray(this.config.wind)); - this.params.nextRotation = (0, _utils.random)(0, 360); + + if (this.image) { + this.params.nextRotationSpeed = _utils.random.apply(void 0, _toConsumableArray(this.config.rotationSpeed)); + } } }, { key: "update", @@ -122,48 +128,83 @@ var Snowflake = /*#__PURE__*/function () { x = _this$params.x, y = _this$params.y, rotation = _this$params.rotation, - nextRotation = _this$params.nextRotation, + rotationSpeed = _this$params.rotationSpeed, + nextRotationSpeed = _this$params.nextRotationSpeed, wind = _this$params.wind, speed = _this$params.speed, nextWind = _this$params.nextWind, - nextSpeed = _this$params.nextSpeed; // Update current location, wrapping around if going off the canvas + nextSpeed = _this$params.nextSpeed, + radius = _this$params.radius; // Update current location, wrapping around if going off the canvas + + this.params.x = (x + wind * framesPassed) % (canvas.offsetWidth + radius * 2); + if (this.params.x > canvas.offsetWidth + radius) this.params.x = -radius; + this.params.y = (y + speed * framesPassed) % (canvas.offsetHeight + radius * 2); + if (this.params.y > canvas.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.x = (x + wind * framesPassed) % canvas.offsetWidth; - this.params.y = (y + speed * framesPassed) % canvas.offsetHeight; // Update the wind and speed towards the desired values this.params.speed = (0, _utils.lerp)(speed, nextSpeed, 0.01); this.params.wind = (0, _utils.lerp)(wind, nextWind, 0.01); - this.params.rotation = (0, _utils.lerp)(rotation, nextRotation, 0.01); + this.params.rotationSpeed = (0, _utils.lerp)(rotationSpeed, nextRotationSpeed, 0.01); if (this.framesSinceLastUpdate++ > this.config.changeFrequency) { this.updateTargetParams(); this.framesSinceLastUpdate = 0; } } + }, { + key: "getImageOffscreenCanvas", + value: function getImageOffscreenCanvas(image, size) { + var _sizes$size; + + if (image instanceof HTMLImageElement && image.loading) return image; + var sizes = Snowflake.offscreenCanvases.get(image); + + if (!sizes) { + sizes = {}; + Snowflake.offscreenCanvases.set(image, sizes); + } + + if (!(size in sizes)) { + var _canvas$getContext; + + var canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + (_canvas$getContext = canvas.getContext('2d')) === null || _canvas$getContext === void 0 ? void 0 : _canvas$getContext.drawImage(image, 0, 0, size, size); + sizes[size] = canvas; + } + + return (_sizes$size = sizes[size]) !== null && _sizes$size !== void 0 ? _sizes$size : image; + } }, { key: "draw", value: function draw(ctx) { - ctx.save(); - ctx.translate(this.params.x, this.params.y); - if (this.image) { + // ctx.save() + // ctx.translate(this.params.x, this.params.y) + ctx.setTransform(1, 0, 0, 1, this.params.x, this.params.y); + var radius = Math.ceil(this.params.radius); ctx.rotate(this.params.rotation * Math.PI / 180); - ctx.drawImage(this.image, -this.params.radius / 2, -this.params.radius / 2, this.params.radius, this.params.radius); + ctx.drawImage(this.getImageOffscreenCanvas(this.image, radius), -Math.ceil(radius / 2), -Math.ceil(radius / 2), radius, radius); // ctx.restore() } else { ctx.beginPath(); - ctx.arc(0, 0, this.params.radius, 0, 2 * Math.PI); + ctx.arc(this.params.x, this.params.y, this.params.radius, 0, 2 * Math.PI); ctx.fillStyle = this.config.color; ctx.closePath(); ctx.fill(); } - - ctx.restore(); } }]); return Snowflake; }(); +_defineProperty(Snowflake, "offscreenCanvases", new WeakMap()); + var _default = Snowflake; exports["default"] = _default; //# sourceMappingURL=Snowflake.js.map \ No newline at end of file diff --git a/packages/react-snowfall/lib/Snowflake.js.map b/packages/react-snowfall/lib/Snowflake.js.map index 6472566..3e2e872 100644 --- a/packages/react-snowfall/lib/Snowflake.js.map +++ b/packages/react-snowfall/lib/Snowflake.js.map @@ -1 +1 @@ -{"version":3,"file":"Snowflake.js","names":["defaultConfig","color","radius","speed","wind","changeFrequency","Snowflake","canvas","config","updateConfig","images","params","x","random","offsetWidth","y","offsetHeight","rotation","nextSpeed","nextWind","nextRotation","framesSinceLastUpdate","length","image","randomElement","undefined","previousConfig","isEqual","selectImage","framesPassed","lerp","updateTargetParams","ctx","save","translate","rotate","Math","PI","drawImage","beginPath","arc","fillStyle","closePath","fill","restore"],"sources":["../src/Snowflake.ts"],"sourcesContent":["import isEqual from 'react-fast-compare'\r\nimport { lerp, random, randomElement } from './utils'\r\n\r\nexport type SnowflakeImageInput =\r\n | HTMLImageElement\r\n | SVGImageElement\r\n | HTMLVideoElement\r\n | HTMLCanvasElement\r\n | ImageBitmap\r\n\r\nexport interface SnowflakeProps {\r\n /** The color of the snowflake, can be any valid CSS color. */\r\n color: string\r\n /**\r\n * The minimum and maximum radius of the snowflake, will be\r\n * randomly selected within this range.\r\n *\r\n * The default value is `[0.5, 3.0]`.\r\n */\r\n radius: [number, number]\r\n /**\r\n * The minimum and maximum speed of the snowflake.\r\n *\r\n * The speed determines how quickly the snowflake moves\r\n * along the y axis (vertical speed).\r\n *\r\n * The values will be randomly selected within this range.\r\n *\r\n * The default value is `[1.0, 3.0]`.\r\n */\r\n speed: [number, number]\r\n /**\r\n * The minimum and maximum wind of the snowflake.\r\n *\r\n * The wind determines how quickly the snowflake moves\r\n * along the x axis (horizontal speed).\r\n *\r\n * The values will be randomly selected within this range.\r\n *\r\n * The default value is `[-0.5, 2.0]`.\r\n */\r\n wind: [number, number]\r\n /**\r\n * The frequency in frames that the wind and speed values\r\n * will update.\r\n *\r\n * The default value is 200.\r\n */\r\n changeFrequency: number\r\n /**\r\n * An array of images that will be rendered as the snowflakes instead\r\n * of the default circle shapes.\r\n */\r\n images?: SnowflakeImageInput[]\r\n}\r\n\r\nexport type SnowflakeConfig = Partial\r\n\r\nexport const defaultConfig: SnowflakeProps = {\r\n color: '#dee4fd',\r\n radius: [0.5, 3.0],\r\n speed: [1.0, 3.0],\r\n wind: [-0.5, 2.0],\r\n changeFrequency: 200,\r\n}\r\n\r\ninterface SnowflakeParams {\r\n x: number\r\n y: number\r\n radius: number\r\n rotation: number\r\n speed: number\r\n wind: number\r\n nextSpeed: number\r\n nextWind: number\r\n nextRotation: number\r\n}\r\n\r\n/**\r\n * An individual snowflake that will update it's location every call to `update`\r\n * and draw itself to the canvas every call to `draw`.\r\n */\r\nclass Snowflake {\r\n private config!: SnowflakeProps\r\n private params: SnowflakeParams\r\n private framesSinceLastUpdate: number\r\n private image?: SnowflakeImageInput\r\n\r\n public constructor(canvas: HTMLCanvasElement, config: SnowflakeConfig = {}) {\r\n // Set custom config\r\n this.updateConfig(config)\r\n\r\n // Setting initial parameters\r\n const { radius, wind, speed, images } = this.config\r\n\r\n this.params = {\r\n x: random(0, canvas.offsetWidth),\r\n y: random(-canvas.offsetHeight, 0),\r\n rotation: random(0, 360),\r\n radius: random(...radius),\r\n speed: random(...speed),\r\n wind: random(...wind),\r\n nextSpeed: random(...wind),\r\n nextWind: random(...speed),\r\n nextRotation: random(0, 360),\r\n }\r\n\r\n this.framesSinceLastUpdate = 0\r\n }\r\n\r\n private selectImage() {\r\n if (this.config.images && this.config.images.length > 0) {\r\n this.image = randomElement(this.config.images)\r\n } else {\r\n this.image = undefined\r\n }\r\n }\r\n\r\n public updateConfig(config: SnowflakeConfig): void {\r\n const previousConfig = this.config\r\n this.config = { ...defaultConfig, ...config }\r\n\r\n // Update the radius if the config has changed, it won't gradually update on it's own\r\n if (this.params && !isEqual(this.config.radius, previousConfig?.radius)) {\r\n this.params.radius = random(...this.config.radius)\r\n }\r\n\r\n if (!isEqual(this.config.images, previousConfig?.images)) {\r\n this.selectImage()\r\n }\r\n }\r\n\r\n private updateTargetParams(): void {\r\n this.params.nextSpeed = random(...this.config.speed)\r\n this.params.nextWind = random(...this.config.wind)\r\n this.params.nextRotation = random(0, 360)\r\n }\r\n\r\n public update(canvas: HTMLCanvasElement, framesPassed = 1): void {\r\n const { x, y, rotation, nextRotation, wind, speed, nextWind, nextSpeed } = this.params\r\n\r\n // Update current location, wrapping around if going off the canvas\r\n this.params.x = (x + wind * framesPassed) % canvas.offsetWidth\r\n this.params.y = (y + speed * framesPassed) % canvas.offsetHeight\r\n\r\n // Update the wind and speed towards the desired values\r\n this.params.speed = lerp(speed, nextSpeed, 0.01)\r\n this.params.wind = lerp(wind, nextWind, 0.01)\r\n this.params.rotation = lerp(rotation, nextRotation, 0.01)\r\n\r\n if (this.framesSinceLastUpdate++ > this.config.changeFrequency) {\r\n this.updateTargetParams()\r\n this.framesSinceLastUpdate = 0\r\n }\r\n }\r\n\r\n public draw(ctx: CanvasRenderingContext2D): void {\r\n ctx.save()\r\n ctx.translate(this.params.x, this.params.y)\r\n\r\n if (this.image) {\r\n ctx.rotate((this.params.rotation * Math.PI) / 180)\r\n ctx.drawImage(\r\n this.image,\r\n -this.params.radius / 2,\r\n -this.params.radius / 2,\r\n this.params.radius,\r\n this.params.radius,\r\n )\r\n } else {\r\n ctx.beginPath()\r\n ctx.arc(0, 0, this.params.radius, 0, 2 * Math.PI)\r\n ctx.fillStyle = this.config.color\r\n ctx.closePath()\r\n ctx.fill()\r\n }\r\n\r\n ctx.restore()\r\n }\r\n}\r\n\r\nexport default Snowflake\r\n"],"mappings":";;;;;;;AAAA;;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDO,IAAMA,aAA6B,GAAG;EAC3CC,KAAK,EAAE,SADoC;EAE3CC,MAAM,EAAE,CAAC,GAAD,EAAM,GAAN,CAFmC;EAG3CC,KAAK,EAAE,CAAC,GAAD,EAAM,GAAN,CAHoC;EAI3CC,IAAI,EAAE,CAAC,CAAC,GAAF,EAAO,GAAP,CAJqC;EAK3CC,eAAe,EAAE;AAL0B,CAAtC;;;AAoBP;AACA;AACA;AACA;IACMC,S;EAMJ,mBAAmBC,MAAnB,EAA4E;IAAA,IAA9BC,MAA8B,uEAAJ,EAAI;;IAAA;;IAAA;;IAAA;;IAAA;;IAAA;;IAC1E;IACA,KAAKC,YAAL,CAAkBD,MAAlB,EAF0E,CAI1E;;IACA,mBAAwC,KAAKA,MAA7C;IAAA,IAAQN,MAAR,gBAAQA,MAAR;IAAA,IAAgBE,IAAhB,gBAAgBA,IAAhB;IAAA,IAAsBD,KAAtB,gBAAsBA,KAAtB;IAAA,IAA6BO,MAA7B,gBAA6BA,MAA7B;IAEA,KAAKC,MAAL,GAAc;MACZC,CAAC,EAAE,IAAAC,aAAA,EAAO,CAAP,EAAUN,MAAM,CAACO,WAAjB,CADS;MAEZC,CAAC,EAAE,IAAAF,aAAA,EAAO,CAACN,MAAM,CAACS,YAAf,EAA6B,CAA7B,CAFS;MAGZC,QAAQ,EAAE,IAAAJ,aAAA,EAAO,CAAP,EAAU,GAAV,CAHE;MAIZX,MAAM,EAAEW,aAAA,kCAAUX,MAAV,EAJI;MAKZC,KAAK,EAAEU,aAAA,kCAAUV,KAAV,EALK;MAMZC,IAAI,EAAES,aAAA,kCAAUT,IAAV,EANM;MAOZc,SAAS,EAAEL,aAAA,kCAAUT,IAAV,EAPC;MAQZe,QAAQ,EAAEN,aAAA,kCAAUV,KAAV,EARE;MASZiB,YAAY,EAAE,IAAAP,aAAA,EAAO,CAAP,EAAU,GAAV;IATF,CAAd;IAYA,KAAKQ,qBAAL,GAA6B,CAA7B;EACD;;;;WAED,uBAAsB;MACpB,IAAI,KAAKb,MAAL,CAAYE,MAAZ,IAAsB,KAAKF,MAAL,CAAYE,MAAZ,CAAmBY,MAAnB,GAA4B,CAAtD,EAAyD;QACvD,KAAKC,KAAL,GAAa,IAAAC,oBAAA,EAAc,KAAKhB,MAAL,CAAYE,MAA1B,CAAb;MACD,CAFD,MAEO;QACL,KAAKa,KAAL,GAAaE,SAAb;MACD;IACF;;;WAED,sBAAoBjB,MAApB,EAAmD;MACjD,IAAMkB,cAAc,GAAG,KAAKlB,MAA5B;MACA,KAAKA,MAAL,mCAAmBR,aAAnB,GAAqCQ,MAArC,EAFiD,CAIjD;;MACA,IAAI,KAAKG,MAAL,IAAe,CAAC,IAAAgB,4BAAA,EAAQ,KAAKnB,MAAL,CAAYN,MAApB,EAA4BwB,cAA5B,aAA4BA,cAA5B,uBAA4BA,cAAc,CAAExB,MAA5C,CAApB,EAAyE;QACvE,KAAKS,MAAL,CAAYT,MAAZ,GAAqBW,aAAA,kCAAU,KAAKL,MAAL,CAAYN,MAAtB,EAArB;MACD;;MAED,IAAI,CAAC,IAAAyB,4BAAA,EAAQ,KAAKnB,MAAL,CAAYE,MAApB,EAA4BgB,cAA5B,aAA4BA,cAA5B,uBAA4BA,cAAc,CAAEhB,MAA5C,CAAL,EAA0D;QACxD,KAAKkB,WAAL;MACD;IACF;;;WAED,8BAAmC;MACjC,KAAKjB,MAAL,CAAYO,SAAZ,GAAwBL,aAAA,kCAAU,KAAKL,MAAL,CAAYL,KAAtB,EAAxB;MACA,KAAKQ,MAAL,CAAYQ,QAAZ,GAAuBN,aAAA,kCAAU,KAAKL,MAAL,CAAYJ,IAAtB,EAAvB;MACA,KAAKO,MAAL,CAAYS,YAAZ,GAA2B,IAAAP,aAAA,EAAO,CAAP,EAAU,GAAV,CAA3B;IACD;;;WAED,gBAAcN,MAAd,EAAiE;MAAA,IAAxBsB,YAAwB,uEAAT,CAAS;MAC/D,mBAA2E,KAAKlB,MAAhF;MAAA,IAAQC,CAAR,gBAAQA,CAAR;MAAA,IAAWG,CAAX,gBAAWA,CAAX;MAAA,IAAcE,QAAd,gBAAcA,QAAd;MAAA,IAAwBG,YAAxB,gBAAwBA,YAAxB;MAAA,IAAsChB,IAAtC,gBAAsCA,IAAtC;MAAA,IAA4CD,KAA5C,gBAA4CA,KAA5C;MAAA,IAAmDgB,QAAnD,gBAAmDA,QAAnD;MAAA,IAA6DD,SAA7D,gBAA6DA,SAA7D,CAD+D,CAG/D;;MACA,KAAKP,MAAL,CAAYC,CAAZ,GAAgB,CAACA,CAAC,GAAGR,IAAI,GAAGyB,YAAZ,IAA4BtB,MAAM,CAACO,WAAnD;MACA,KAAKH,MAAL,CAAYI,CAAZ,GAAgB,CAACA,CAAC,GAAGZ,KAAK,GAAG0B,YAAb,IAA6BtB,MAAM,CAACS,YAApD,CAL+D,CAO/D;;MACA,KAAKL,MAAL,CAAYR,KAAZ,GAAoB,IAAA2B,WAAA,EAAK3B,KAAL,EAAYe,SAAZ,EAAuB,IAAvB,CAApB;MACA,KAAKP,MAAL,CAAYP,IAAZ,GAAmB,IAAA0B,WAAA,EAAK1B,IAAL,EAAWe,QAAX,EAAqB,IAArB,CAAnB;MACA,KAAKR,MAAL,CAAYM,QAAZ,GAAuB,IAAAa,WAAA,EAAKb,QAAL,EAAeG,YAAf,EAA6B,IAA7B,CAAvB;;MAEA,IAAI,KAAKC,qBAAL,KAA+B,KAAKb,MAAL,CAAYH,eAA/C,EAAgE;QAC9D,KAAK0B,kBAAL;QACA,KAAKV,qBAAL,GAA6B,CAA7B;MACD;IACF;;;WAED,cAAYW,GAAZ,EAAiD;MAC/CA,GAAG,CAACC,IAAJ;MACAD,GAAG,CAACE,SAAJ,CAAc,KAAKvB,MAAL,CAAYC,CAA1B,EAA6B,KAAKD,MAAL,CAAYI,CAAzC;;MAEA,IAAI,KAAKQ,KAAT,EAAgB;QACdS,GAAG,CAACG,MAAJ,CAAY,KAAKxB,MAAL,CAAYM,QAAZ,GAAuBmB,IAAI,CAACC,EAA7B,GAAmC,GAA9C;QACAL,GAAG,CAACM,SAAJ,CACE,KAAKf,KADP,EAEE,CAAC,KAAKZ,MAAL,CAAYT,MAAb,GAAsB,CAFxB,EAGE,CAAC,KAAKS,MAAL,CAAYT,MAAb,GAAsB,CAHxB,EAIE,KAAKS,MAAL,CAAYT,MAJd,EAKE,KAAKS,MAAL,CAAYT,MALd;MAOD,CATD,MASO;QACL8B,GAAG,CAACO,SAAJ;QACAP,GAAG,CAACQ,GAAJ,CAAQ,CAAR,EAAW,CAAX,EAAc,KAAK7B,MAAL,CAAYT,MAA1B,EAAkC,CAAlC,EAAqC,IAAIkC,IAAI,CAACC,EAA9C;QACAL,GAAG,CAACS,SAAJ,GAAgB,KAAKjC,MAAL,CAAYP,KAA5B;QACA+B,GAAG,CAACU,SAAJ;QACAV,GAAG,CAACW,IAAJ;MACD;;MAEDX,GAAG,CAACY,OAAJ;IACD;;;;;;eAGYtC,S"} \ No newline at end of file +{"version":3,"file":"Snowflake.js","names":["defaultConfig","color","radius","speed","wind","changeFrequency","rotationSpeed","Snowflake","canvas","config","updateConfig","params","x","random","offsetWidth","y","offsetHeight","rotation","nextSpeed","nextWind","nextRotationSpeed","framesSinceLastUpdate","images","length","image","randomElement","undefined","previousConfig","isEqual","selectImage","framesPassed","lerp","updateTargetParams","size","HTMLImageElement","loading","sizes","offscreenCanvases","get","set","document","createElement","width","height","getContext","drawImage","ctx","setTransform","Math","ceil","rotate","PI","getImageOffscreenCanvas","beginPath","arc","fillStyle","closePath","fill","WeakMap"],"sources":["../src/Snowflake.ts"],"sourcesContent":["import isEqual from 'react-fast-compare'\r\nimport { lerp, random, randomElement } from './utils'\r\n\r\nexport interface SnowflakeProps {\r\n /** The color of the snowflake, can be any valid CSS color. */\r\n color: string\r\n /**\r\n * The minimum and maximum radius of the snowflake, will be\r\n * randomly selected within this range.\r\n *\r\n * The default value is `[0.5, 3.0]`.\r\n */\r\n radius: [number, number]\r\n /**\r\n * The minimum and maximum speed of the snowflake.\r\n *\r\n * The speed determines how quickly the snowflake moves\r\n * along the y axis (vertical speed).\r\n *\r\n * The values will be randomly selected within this range.\r\n *\r\n * The default value is `[1.0, 3.0]`.\r\n */\r\n speed: [number, number]\r\n /**\r\n * The minimum and maximum wind of the snowflake.\r\n *\r\n * The wind determines how quickly the snowflake moves\r\n * along the x axis (horizontal speed).\r\n *\r\n * The values will be randomly selected within this range.\r\n *\r\n * The default value is `[-0.5, 2.0]`.\r\n */\r\n wind: [number, number]\r\n /**\r\n * The frequency in frames that the wind and speed values\r\n * will update.\r\n *\r\n * The default value is 200.\r\n */\r\n changeFrequency: number\r\n /**\r\n * An array of images that will be rendered as the snowflakes instead\r\n * of the default circle shapes.\r\n */\r\n images?: CanvasImageSource[]\r\n /**\r\n * The minimum and maximum rotation speed of the snowflake (in degrees of\r\n * rotation per frame).\r\n *\r\n * The rotation speed determines how quickly the snowflake rotates when\r\n * an image is being rendered.\r\n *\r\n * The values will be randomly selected within this range.\r\n *\r\n * The default value is `[-1.0, 1.0]`.\r\n */\r\n rotationSpeed: [number, number]\r\n}\r\n\r\nexport type SnowflakeConfig = Partial\r\n\r\nexport const defaultConfig: SnowflakeProps = {\r\n color: '#dee4fd',\r\n radius: [0.5, 3.0],\r\n speed: [1.0, 3.0],\r\n wind: [-0.5, 2.0],\r\n changeFrequency: 200,\r\n rotationSpeed: [-1.0, 1.0],\r\n}\r\n\r\ninterface SnowflakeParams {\r\n x: number\r\n y: number\r\n radius: number\r\n rotation: number\r\n rotationSpeed: number\r\n speed: number\r\n wind: number\r\n nextSpeed: number\r\n nextWind: number\r\n nextRotationSpeed: number\r\n}\r\n\r\n/**\r\n * An individual snowflake that will update it's location every call to `update`\r\n * and draw itself to the canvas every call to `draw`.\r\n */\r\nclass Snowflake {\r\n static offscreenCanvases = new WeakMap>()\r\n\r\n private config!: SnowflakeProps\r\n private params: SnowflakeParams\r\n private framesSinceLastUpdate: number\r\n private image?: CanvasImageSource\r\n\r\n public constructor(canvas: HTMLCanvasElement, config: SnowflakeConfig = {}) {\r\n // Set custom config\r\n this.updateConfig(config)\r\n\r\n // Setting initial parameters\r\n const { radius, wind, speed, rotationSpeed } = this.config\r\n\r\n this.params = {\r\n x: random(0, canvas.offsetWidth),\r\n y: random(-canvas.offsetHeight, 0),\r\n rotation: random(0, 360),\r\n radius: random(...radius),\r\n speed: random(...speed),\r\n wind: random(...wind),\r\n rotationSpeed: random(...rotationSpeed),\r\n nextSpeed: random(...wind),\r\n nextWind: random(...speed),\r\n nextRotationSpeed: random(...rotationSpeed),\r\n }\r\n\r\n this.framesSinceLastUpdate = 0\r\n }\r\n\r\n private selectImage() {\r\n if (this.config.images && this.config.images.length > 0) {\r\n this.image = randomElement(this.config.images)\r\n } else {\r\n this.image = undefined\r\n }\r\n }\r\n\r\n public updateConfig(config: SnowflakeConfig): void {\r\n const previousConfig = this.config\r\n this.config = { ...defaultConfig, ...config }\r\n this.config.changeFrequency = random(this.config.changeFrequency, this.config.changeFrequency * 1.5)\r\n\r\n // Update the radius if the config has changed, it won't gradually update on it's own\r\n if (this.params && !isEqual(this.config.radius, previousConfig?.radius)) {\r\n this.params.radius = random(...this.config.radius)\r\n }\r\n\r\n if (!isEqual(this.config.images, previousConfig?.images)) {\r\n this.selectImage()\r\n }\r\n }\r\n\r\n private updateTargetParams(): void {\r\n this.params.nextSpeed = random(...this.config.speed)\r\n this.params.nextWind = random(...this.config.wind)\r\n if (this.image) {\r\n this.params.nextRotationSpeed = random(...this.config.rotationSpeed)\r\n }\r\n }\r\n\r\n public update(canvas: HTMLCanvasElement, framesPassed = 1): void {\r\n const { x, y, rotation, rotationSpeed, nextRotationSpeed, wind, speed, nextWind, nextSpeed, radius } = this.params\r\n\r\n // Update current location, wrapping around if going off the canvas\r\n this.params.x = (x + wind * framesPassed) % (canvas.offsetWidth + radius * 2)\r\n if (this.params.x > canvas.offsetWidth + radius) this.params.x = -radius\r\n this.params.y = (y + speed * framesPassed) % (canvas.offsetHeight + radius * 2)\r\n if (this.params.y > canvas.offsetHeight + radius) this.params.y = -radius\r\n\r\n // Apply rotation\r\n if (this.image) {\r\n this.params.rotation = (rotation + rotationSpeed) % 360\r\n }\r\n\r\n // Update the wind, speed and rotation towards the desired values\r\n this.params.speed = lerp(speed, nextSpeed, 0.01)\r\n this.params.wind = lerp(wind, nextWind, 0.01)\r\n this.params.rotationSpeed = lerp(rotationSpeed, nextRotationSpeed, 0.01)\r\n\r\n if (this.framesSinceLastUpdate++ > this.config.changeFrequency) {\r\n this.updateTargetParams()\r\n this.framesSinceLastUpdate = 0\r\n }\r\n }\r\n\r\n private getImageOffscreenCanvas(image: CanvasImageSource, size: number): CanvasImageSource {\r\n if (image instanceof HTMLImageElement && image.loading) return image\r\n let sizes = Snowflake.offscreenCanvases.get(image)\r\n\r\n if (!sizes) {\r\n sizes = {}\r\n Snowflake.offscreenCanvases.set(image, sizes)\r\n }\r\n\r\n if (!(size in sizes)) {\r\n const canvas = document.createElement('canvas')\r\n canvas.width = size\r\n canvas.height = size\r\n canvas.getContext('2d')?.drawImage(image, 0, 0, size, size)\r\n sizes[size] = canvas\r\n }\r\n\r\n return sizes[size] ?? image\r\n }\r\n\r\n public draw(ctx: CanvasRenderingContext2D): void {\r\n if (this.image) {\r\n // ctx.save()\r\n // ctx.translate(this.params.x, this.params.y)\r\n ctx.setTransform(1, 0, 0, 1, this.params.x, this.params.y)\r\n\r\n const radius = Math.ceil(this.params.radius)\r\n ctx.rotate((this.params.rotation * Math.PI) / 180)\r\n ctx.drawImage(\r\n this.getImageOffscreenCanvas(this.image, radius),\r\n -Math.ceil(radius / 2),\r\n -Math.ceil(radius / 2),\r\n radius,\r\n radius,\r\n )\r\n\r\n // ctx.restore()\r\n } else {\r\n ctx.beginPath()\r\n ctx.arc(this.params.x, this.params.y, this.params.radius, 0, 2 * Math.PI)\r\n ctx.fillStyle = this.config.color\r\n ctx.closePath()\r\n ctx.fill()\r\n }\r\n }\r\n}\r\n\r\nexport default Snowflake\r\n"],"mappings":"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"} \ No newline at end of file diff --git a/packages/react-snowfall/package.json b/packages/react-snowfall/package.json index aa7c086..c26163d 100644 --- a/packages/react-snowfall/package.json +++ b/packages/react-snowfall/package.json @@ -7,7 +7,7 @@ "scripts": { "generate-types": "tsc --emitDeclarationOnly --declarationDir ./lib", "build": "npm run generate-types && babel ./src --out-dir lib --extensions \".ts,.tsx\" --ignore \"src/**/*.test.tsx\",\"src/**/*.test.ts\",\"src/__mocks__/**/*\" --source-maps", - "watch": "npm run build -- --watch", + "start": "npm run build -- --watch", "test": "jest", "release": "standard-version" }, @@ -67,4 +67,4 @@ "dependencies": { "react-fast-compare": "^3.2.0" } -} \ No newline at end of file +} diff --git a/packages/react-snowfall/src/Snowfall.tsx b/packages/react-snowfall/src/Snowfall.tsx index ef91605..1701ffa 100644 --- a/packages/react-snowfall/src/Snowfall.tsx +++ b/packages/react-snowfall/src/Snowfall.tsx @@ -22,6 +22,7 @@ const Snowfall = ({ radius = defaultConfig.radius, speed = defaultConfig.speed, wind = defaultConfig.wind, + rotationSpeed = defaultConfig.rotationSpeed, snowflakeCount = 150, images, style, @@ -33,7 +34,7 @@ const Snowfall = ({ const animationFrame = useRef(0) const lastUpdate = useRef(Date.now()) - const config = useDeepMemo({ color, changeFrequency, radius, speed, wind, images }) + const config = useDeepMemo({ color, changeFrequency, radius, speed, wind, rotationSpeed, images }) const snowflakes = useSnowflakes(canvasRef, snowflakeCount, config) const render = useCallback( @@ -46,6 +47,7 @@ const Snowfall = ({ // Render them if the canvas is available const ctx = canvas.getContext('2d') if (ctx) { + ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight) snowflakes.forEach((snowflake) => snowflake.draw(ctx)) diff --git a/packages/react-snowfall/src/Snowflake.ts b/packages/react-snowfall/src/Snowflake.ts index 3aac48d..1830277 100644 --- a/packages/react-snowfall/src/Snowflake.ts +++ b/packages/react-snowfall/src/Snowflake.ts @@ -1,13 +1,6 @@ import isEqual from 'react-fast-compare' import { lerp, random, randomElement } from './utils' -export type SnowflakeImageInput = - | HTMLImageElement - | SVGImageElement - | HTMLVideoElement - | HTMLCanvasElement - | ImageBitmap - export interface SnowflakeProps { /** The color of the snowflake, can be any valid CSS color. */ color: string @@ -51,7 +44,19 @@ export interface SnowflakeProps { * An array of images that will be rendered as the snowflakes instead * of the default circle shapes. */ - images?: SnowflakeImageInput[] + images?: CanvasImageSource[] + /** + * The minimum and maximum rotation speed of the snowflake (in degrees of + * rotation per frame). + * + * The rotation speed determines how quickly the snowflake rotates when + * an image is being rendered. + * + * The values will be randomly selected within this range. + * + * The default value is `[-1.0, 1.0]`. + */ + rotationSpeed: [number, number] } export type SnowflakeConfig = Partial @@ -62,6 +67,7 @@ export const defaultConfig: SnowflakeProps = { speed: [1.0, 3.0], wind: [-0.5, 2.0], changeFrequency: 200, + rotationSpeed: [-1.0, 1.0], } interface SnowflakeParams { @@ -69,11 +75,12 @@ interface SnowflakeParams { y: number radius: number rotation: number + rotationSpeed: number speed: number wind: number nextSpeed: number nextWind: number - nextRotation: number + nextRotationSpeed: number } /** @@ -81,17 +88,19 @@ interface SnowflakeParams { * and draw itself to the canvas every call to `draw`. */ class Snowflake { + static offscreenCanvases = new WeakMap>() + private config!: SnowflakeProps private params: SnowflakeParams private framesSinceLastUpdate: number - private image?: SnowflakeImageInput + private image?: CanvasImageSource public constructor(canvas: HTMLCanvasElement, config: SnowflakeConfig = {}) { // Set custom config this.updateConfig(config) // Setting initial parameters - const { radius, wind, speed, images } = this.config + const { radius, wind, speed, rotationSpeed } = this.config this.params = { x: random(0, canvas.offsetWidth), @@ -100,9 +109,10 @@ class Snowflake { radius: random(...radius), speed: random(...speed), wind: random(...wind), + rotationSpeed: random(...rotationSpeed), nextSpeed: random(...wind), nextWind: random(...speed), - nextRotation: random(0, 360), + nextRotationSpeed: random(...rotationSpeed), } this.framesSinceLastUpdate = 0 @@ -119,6 +129,7 @@ class Snowflake { public updateConfig(config: SnowflakeConfig): void { 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?.radius)) { @@ -133,20 +144,29 @@ class Snowflake { private updateTargetParams(): void { this.params.nextSpeed = random(...this.config.speed) this.params.nextWind = random(...this.config.wind) - this.params.nextRotation = random(0, 360) + if (this.image) { + this.params.nextRotationSpeed = random(...this.config.rotationSpeed) + } } public update(canvas: HTMLCanvasElement, framesPassed = 1): void { - const { x, y, rotation, nextRotation, wind, speed, nextWind, nextSpeed } = this.params + 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) % canvas.offsetWidth - this.params.y = (y + speed * framesPassed) % canvas.offsetHeight + this.params.x = (x + wind * framesPassed) % (canvas.offsetWidth + radius * 2) + if (this.params.x > canvas.offsetWidth + radius) this.params.x = -radius + this.params.y = (y + speed * framesPassed) % (canvas.offsetHeight + radius * 2) + if (this.params.y > canvas.offsetHeight + radius) this.params.y = -radius + + // Apply rotation + if (this.image) { + this.params.rotation = (rotation + rotationSpeed) % 360 + } - // Update the wind and speed towards the desired values + // 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.rotation = lerp(rotation, nextRotation, 0.01) + this.params.rotationSpeed = lerp(rotationSpeed, nextRotationSpeed, 0.01) if (this.framesSinceLastUpdate++ > this.config.changeFrequency) { this.updateTargetParams() @@ -154,28 +174,50 @@ class Snowflake { } } - public draw(ctx: CanvasRenderingContext2D): void { - ctx.save() - ctx.translate(this.params.x, this.params.y) + private getImageOffscreenCanvas(image: CanvasImageSource, size: number): CanvasImageSource { + 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 + canvas.getContext('2d')?.drawImage(image, 0, 0, size, size) + sizes[size] = canvas + } + return sizes[size] ?? image + } + + public draw(ctx: CanvasRenderingContext2D): void { if (this.image) { + // ctx.save() + // ctx.translate(this.params.x, this.params.y) + ctx.setTransform(1, 0, 0, 1, this.params.x, this.params.y) + + const radius = Math.ceil(this.params.radius) ctx.rotate((this.params.rotation * Math.PI) / 180) ctx.drawImage( - this.image, - -this.params.radius / 2, - -this.params.radius / 2, - this.params.radius, - this.params.radius, + this.getImageOffscreenCanvas(this.image, radius), + -Math.ceil(radius / 2), + -Math.ceil(radius / 2), + radius, + radius, ) + + // ctx.restore() } else { ctx.beginPath() - ctx.arc(0, 0, this.params.radius, 0, 2 * Math.PI) + ctx.arc(this.params.x, this.params.y, this.params.radius, 0, 2 * Math.PI) ctx.fillStyle = this.config.color ctx.closePath() ctx.fill() } - - ctx.restore() } }