react-weather-effects 0.0.0-stage → 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,418 @@
1
- # Temporary Holding Version
1
+ # react-weather-effects
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Composable, high-performance rain, snow, and fog visual effects for React, rendered on a single lightweight 2D canvas.
4
+
5
+ ## Installation
6
+
7
+ Install `react-weather-effects` using your package manager of choice:
8
+
9
+ ```sh
10
+ # npm
11
+ npm install react-weather-effects
12
+
13
+ # pnpm
14
+ pnpm add react-weather-effects
15
+
16
+ # Yarn
17
+ yarn add react-weather-effects
18
+ ```
19
+
20
+ > **Beta Prerelease:** To install the current beta version:
21
+ > ```sh
22
+ > npm install react-weather-effects@beta
23
+ > # or pin specifically:
24
+ > npm install react-weather-effects@0.1.0-beta.0
25
+ > ```
26
+
27
+ ### Requirements
28
+
29
+ - **React 18** or later (React 18 and 19 supported as peer dependencies).
30
+ - A modern browser with 2D `<canvas>` support. Contained sizing uses `ResizeObserver`.
31
+ - Full TypeScript declarations, ESM (`.mjs`), and CommonJS (`.cjs`) entry points included.
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ Wrap any effect component inside `<WeatherCanvas>`:
38
+
39
+ ```tsx
40
+ import { WeatherCanvas, Rain } from 'react-weather-effects';
41
+
42
+ export default function App() {
43
+ return (
44
+ <main style={{ minHeight: '100vh', background: '#0f172a', color: '#fff' }}>
45
+ <h1>My Weather App</h1>
46
+
47
+ <WeatherCanvas>
48
+ <Rain intensity={0.6} />
49
+ </WeatherCanvas>
50
+ </main>
51
+ );
52
+ }
53
+ ```
54
+
55
+ By default, `<WeatherCanvas>` runs in `mode="fullscreen"`, covering the viewport with a transparent background. It ignores pointer events (`pointer-events: none`), so all underlying interactive elements remain clickable.
56
+
57
+ ---
58
+
59
+ ## Features
60
+
61
+ - **Single Canvas Engine:** One 2D `<canvas>` manages all effects, batching rendering at 60 FPS without DOM bloat.
62
+ - **Composable:** Combine multiple effects (`Rain`, `Snow`, `Fog`) inside a single canvas.
63
+ - **Configurable:** Adjust intensity, speed, wind angle, colors, and custom snowflake sprite artwork.
64
+ - **Motion-Aware & Accessible:** Automatically respects `prefers-reduced-motion` and pauses when hidden or offscreen.
65
+ - **Contained & Fullscreen:** Place inside specific cards/heroes (`mode="contained"`) or cover the whole screen.
66
+ - **SSR & Next.js Safe:** Safe for React Server Components (RSC) and Next.js App Router (runs cleanly with `'use client'`).
67
+
68
+ ---
69
+
70
+ ## Contents
71
+
72
+ - [Usage examples](#usage-examples)
73
+ - [API reference](#api-reference)
74
+ - [Quality and performance](#quality-and-performance)
75
+ - [Motion, accessibility, and lifecycle](#motion-accessibility-and-lifecycle)
76
+ - [Server rendering and Next.js](#server-rendering-and-nextjs)
77
+ - [Troubleshooting](#troubleshooting)
78
+ - [Local development](#local-development)
79
+ - [License](#license)
80
+
81
+ ## Usage examples
82
+
83
+ ### Keep an effect inside a container
84
+
85
+ Use `mode="contained"` and give the parent a positioning context and a nonzero height. Add `overflow: 'hidden'` when the effect should be clipped to the container's edges.
86
+
87
+ ```tsx
88
+ import { WeatherCanvas, Snow } from 'react-weather-effects';
89
+
90
+ export function SnowCard() {
91
+ return (
92
+ <section
93
+ style={{
94
+ position: 'relative',
95
+ height: 320,
96
+ overflow: 'hidden',
97
+ borderRadius: 12,
98
+ background: '#0f172a',
99
+ }}
100
+ >
101
+ <WeatherCanvas mode="contained" wind={{ x: -0.15, y: 0 }}>
102
+ <Snow intensity={0.5} speed={0.8} />
103
+ </WeatherCanvas>
104
+
105
+ <div style={{ position: 'relative', zIndex: 2, padding: 24, color: '#fff' }}>
106
+ <h2>Winter forecast</h2>
107
+ <p>Snow remains behind this content.</p>
108
+ </div>
109
+ </section>
110
+ );
111
+ }
112
+ ```
113
+
114
+ Size and style the parent rather than passing `width`, `height`, or `style` to `WeatherCanvas`; those are not part of its public API.
115
+
116
+ ### Combine effects
117
+
118
+ ```tsx
119
+ import { WeatherCanvas, Rain, Fog } from 'react-weather-effects';
120
+
121
+ export function RainyScene() {
122
+ return (
123
+ <WeatherCanvas quality="medium" wind={{ x: 0.3, y: 0 }}>
124
+ <Fog density={0.25} layers={2} />
125
+ <Rain intensity={0.75} angle={12} speed={1.1} />
126
+ </WeatherCanvas>
127
+ );
128
+ }
129
+ ```
130
+
131
+ Fog draws behind rain and snow. Rain and snow share a drawing priority; instances at that priority draw in registration order. Each effect has its own settings and can be mounted or removed independently.
132
+
133
+ ### Use custom snow artwork
134
+
135
+ Add a static image to your application's public assets, for example `public/snowflakes/crystal.svg`, and pass its served URL:
136
+
137
+ ```tsx
138
+ import { WeatherCanvas, Snow } from 'react-weather-effects';
139
+
140
+ export function CustomSnow() {
141
+ return (
142
+ <WeatherCanvas wind={{ x: 0.15, y: 0 }}>
143
+ <Snow
144
+ sprite="/snowflakes/crystal.svg"
145
+ intensity={0.5}
146
+ minSize={8}
147
+ maxSize={24}
148
+ speed={0.8}
149
+ wobble
150
+ />
151
+ </WeatherCanvas>
152
+ );
153
+ }
154
+ ```
155
+
156
+ Pass a **URL string**, not an SVG React component or raw SVG markup. PNG, SVG, and other browser-supported image formats can be used. Demo artwork is not bundled into the installed library.
157
+
158
+ #### Sprite sizing and caching
159
+
160
+ - `minSize` and `maxSize` are **radii in CSS pixels**, not image widths. A sprite with radius `24` has a longest side of `48` CSS pixels; its original aspect ratio is preserved.
161
+ - Sprites keep their original colors and transparency. The `color` prop applies only to built-in flakes and the loading/error fallback.
162
+ - Each mounted `Snow` instance loads its sprite once, unless the `sprite` URL changes. Built-in flakes appear while loading and remain if loading or rasterization fails.
163
+ - The package rasterizes artwork into a reusable texture, so SVG paths and filters are not rendered separately for every particle on every frame.
164
+ - Texture resolution accounts for flake size and device pixel ratio. It grows in size buckets when needed and is capped at `1024` pixels per side. Very large flakes can therefore look softer.
165
+ - The maximum RGBA pixel storage for one cached texture is approximately **4 MiB**, excluding the source image and browser overhead. This is not a limit on download size or decoded source-image memory.
166
+ - Animated images are used as **static snapshots**. Sprite animation is not supported; particle movement and gentle rotation still work.
167
+
168
+ For remote images, the image server must allow anonymous cross-origin requests with an appropriate `Access-Control-Allow-Origin` header. Serving artwork from your own application is the simplest option.
169
+
170
+ `wobble={false}` disables the animated sway and gentle sprite rotation. Wind can still move the flakes. There is no separate `rotation` prop in the current API.
171
+
172
+ ### Pause and resume a scene
173
+
174
+ ```tsx
175
+ import { useState } from 'react';
176
+ import { WeatherCanvas, Snow } from 'react-weather-effects';
177
+
178
+ export function PausableSnow() {
179
+ const [paused, setPaused] = useState(false);
180
+
181
+ return (
182
+ <>
183
+ <button type="button" onClick={() => setPaused((value) => !value)}>
184
+ {paused ? 'Resume weather' : 'Pause weather'}
185
+ </button>
186
+
187
+ <WeatherCanvas paused={paused}>
188
+ <Snow intensity={0.5} />
189
+ </WeatherCanvas>
190
+ </>
191
+ );
192
+ }
193
+ ```
194
+
195
+ Changing effect props updates the current scene without remounting its particle pool. Increasing intensity or quality initializes additional particles as needed; lowering them reduces the active count.
196
+
197
+ ## API reference
198
+
199
+ All props are optional. `intensity` and `density` are fractions from `0` to `1`, not percentages from `0` to `100`; values outside that interval are clamped.
200
+
201
+ ### `WeatherCanvas`
202
+
203
+ | Prop | Type | Default | Description |
204
+ | --- | --- | --- | --- |
205
+ | `children` | `ReactNode` | None | Effect components that share this canvas. |
206
+ | `mode` | `'fullscreen' \| 'contained'` | `'fullscreen'` | Cover the viewport or fill the positioned parent. |
207
+ | `wind` | `{ x: number; y: number }` | `{ x: 0, y: 0 }` | Relative wind input. Positive `x` points right; positive `y` points down. |
208
+ | `quality` | `'low' \| 'medium' \| 'high'` | `'medium'` | Particle budget and maximum device pixel ratio. |
209
+ | `dpr` | `'light' \| 'normal' \| 'heavy'` | `'normal'` | Additional device pixel ratio cap; see [quality and performance](#quality-and-performance). |
210
+ | `reducedMotion` | `'auto' \| 'animated' \| 'static' \| 'off'` | `'auto'` | How to handle motion and system preferences. |
211
+ | `paused` | `boolean` | `false` | Freeze simulation while keeping the scene visible. |
212
+ | `seed` | `string \| number` | Per-instance seed | Seed for reproducible random initialization. |
213
+ | `zIndex` | `number` | `1` | CSS stacking order of the overlay within its ancestor stacking context. |
214
+ | `onContextLost` | `() => void` | None | Called when the 2D context cannot be acquired or is lost. |
215
+
216
+ Wind is an effect-specific input, not a velocity in pixels per second. Rain and snow use both components; fog currently uses horizontal wind only.
217
+
218
+ ### `Rain`
219
+
220
+ | Prop | Type | Default | Description |
221
+ | --- | --- | --- | --- |
222
+ | `intensity` | `number` | `0.7` | Fraction of this instance's particle budget to draw. `0` draws no rain. |
223
+ | `angle` | `number` | `10` | Degrees from vertical. Positive values lean right; negative values lean left. |
224
+ | `speed` | `number` | `1` | Multiplier for intrinsic falling speed. |
225
+ | `color` | `string` | `'rgba(190, 215, 245, 0.8)'` | CSS color for rain trails. |
226
+
227
+ ### `Snow`
228
+
229
+ | Prop | Type | Default | Description |
230
+ | --- | --- | --- | --- |
231
+ | `intensity` | `number` | `0.5` | Fraction of this instance's particle budget to draw. `0` draws no snow. |
232
+ | `speed` | `number` | `1` | Multiplier for intrinsic falling speed and wobble progression. |
233
+ | `color` | `string` | `'rgba(255, 255, 255, 0.85)'` | CSS color for built-in flakes and the sprite fallback. Does not tint artwork. |
234
+ | `sprite` | `string` | None | Static image URL. Omitting it uses soft, built-in snowflakes. |
235
+ | `minSize` | `number` | `1.5`, or `6` with a sprite | Minimum flake radius in CSS pixels. |
236
+ | `maxSize` | `number` | `4`, or `18` with a sprite | Maximum flake radius in CSS pixels. |
237
+ | `wobble` | `boolean` | `true` | Enable animated sway and gentle sprite rotation. |
238
+
239
+ Use nonnegative sizes, with `minSize <= maxSize`. Built-in flakes have a feathered glow that extends beyond their nominal radius.
240
+
241
+ ### `Fog`
242
+
243
+ | Prop | Type | Default | Description |
244
+ | --- | --- | --- | --- |
245
+ | `density` | `number` | `0.4` | Opacity multiplier. `0` draws no fog. |
246
+ | `speed` | `number` | `1` | Multiplier for intrinsic drift and shape movement. |
247
+ | `color` | `string` | `'rgba(220, 225, 235, 0.35)'` | CSS color used to tint the cached fog texture. |
248
+ | `layers` | `number` | `3` | Layer count, rounded and clamped between `1` and `6`. |
249
+
250
+ Use nonnegative speed multipliers for the usual falling and drifting behavior. Setting `speed={0}` does not necessarily stop wind-driven movement; use `WeatherCanvas paused` to freeze the entire scene.
251
+
252
+ ### TypeScript exports
253
+
254
+ The public entry point exports these types in addition to the components:
255
+
256
+ - `WeatherCanvasProps`, `RainProps`, `SnowProps`, `FogProps`
257
+ - `Quality`, `DPRWeight`, `RenderBudget`
258
+ - `ReducedMotionProp`, `MotionMode`
259
+
260
+ `MotionMode` describes the resolved state (`'animated'`, `'static'`, or `'off'`). The `reducedMotion` prop also accepts `'auto'` through `ReducedMotionProp`.
261
+
262
+ ## Quality and performance
263
+
264
+ Each `WeatherCanvas` owns its rendering budget:
265
+
266
+ | Quality | Maximum particles per rain or snow instance | DPR cap |
267
+ | --- | ---: | ---: |
268
+ | `'low'` | 150 | 1 |
269
+ | `'medium'` | 300 | 2 |
270
+ | `'high'` | 600 | 2 |
271
+
272
+ The particle cap is **per effect instance**, not a whole-scene limit. For example, medium quality with `Snow intensity={0.5}` draws 150 flakes. A second identical `Snow` instance can draw another 150. Fog uses a bounded layer count and cached textures instead of this particle count.
273
+
274
+ The `dpr` prop applies another resolution cap:
275
+
276
+ | DPR setting | Additional cap |
277
+ | --- | ---: |
278
+ | `'light'` | 2 |
279
+ | `'normal'` | 2 |
280
+ | `'heavy'` | 1.5 |
281
+
282
+ The effective ratio is the minimum of the device DPR, the quality cap, and the additional DPR cap. `light` and `normal` currently have the same cap. Size props remain in CSS pixels regardless of DPR.
283
+
284
+ For lower rendering cost:
285
+
286
+ 1. Start with `quality="medium"`; use `'low'` on constrained devices or large canvases.
287
+ 2. Reduce intensity, fog density, or fog layers before adding more effect instances.
288
+ 3. Prefer contained scenes when the effect only needs to cover a small part of the page.
289
+ 4. Keep sprite files reasonably sized. Caching reduces repeated rendering, but downloading, decoding, and the first rasterization still take work.
290
+ 5. Measure the complete scene on target browsers and devices. Large translucent areas and overlapping effects can dominate rendering cost.
291
+
292
+ There is no fixed FPS guarantee or automatic quality downgrade. The engine reuses particle pools and textures, but hardware, canvas area, DPR, and composition still matter.
293
+
294
+ The repository's size check enforces a **5 kB minified-and-gzipped core-only budget**, excluding React. Rain and combined Rain/Snow/Fog sizes are reported separately; the core budget is not a size claim for all effects together.
295
+
296
+ ## Motion, accessibility, and lifecycle
297
+
298
+ The default `reducedMotion="auto"` follows the user's `prefers-reduced-motion` setting:
299
+
300
+ | Setting | Behavior |
301
+ | --- | --- |
302
+ | `'auto'` | Animate when the system allows motion; otherwise draw a static scene. The unknown initial preference is treated as static. |
303
+ | `'animated'` | Animate regardless of the system preference. Use only when an explicit application choice is appropriate. |
304
+ | `'static'` | Draw a seeded scene without advancing simulation. |
305
+ | `'off'` | Clear the canvas and suppress effect drawing. |
306
+
307
+ `paused={true}` freezes the existing simulation rather than hiding the canvas. Static and paused scenes can still redraw after prop changes, resizing, or sprite loading, without advancing particle motion.
308
+
309
+ Each active canvas uses one shared animation loop. Scheduling stops when the page is hidden, the canvas is outside the viewport, its size is zero, it is paused, motion is disabled/static, or no effects are mounted. Resuming continues from the retained scene state rather than starting over.
310
+
311
+ The canvas is marked `aria-hidden="true"` and does not receive pointer events. Treat the effects as decorative: provide readable text or other accessible content when weather information itself matters.
312
+
313
+ ### Seeds and scene resets
314
+
315
+ A fixed `seed` makes random initialization reproducible, but does not fix frame timing or guarantee identical screenshots across different rendering environments. Repeatable captures also need matching props, viewport, DPR, and simulation steps.
316
+
317
+ Changing `seed` resets the random generator but does not rebuild already initialized particles. To intentionally reset the whole scene, change the canvas's React `key` as well as its `seed`.
318
+
319
+ ## Server rendering and Next.js
320
+
321
+ Importing and initially rendering the public components is SSR-safe. Canvas drawing and browser-dependent setup happen after the component mounts on the client.
322
+
323
+ In Next.js App Router or another React Server Components framework, put the weather scene behind a client-component boundary:
324
+
325
+ ```tsx
326
+ 'use client';
327
+
328
+ import { WeatherCanvas, Snow } from 'react-weather-effects';
329
+
330
+ export default function WeatherOverlay() {
331
+ return (
332
+ <WeatherCanvas>
333
+ <Snow intensity={0.4} />
334
+ </WeatherCanvas>
335
+ );
336
+ }
337
+ ```
338
+
339
+ You can then render `WeatherOverlay` from your layout or page. No animation runs on the server, and a sprite may initially show the built-in fallback until its image loads.
340
+
341
+ ## Troubleshooting
342
+
343
+ | Symptom | What to check |
344
+ | --- | --- |
345
+ | An effect throws “must be used inside WeatherCanvas.” | Render it as a descendant of `WeatherCanvas`, not as a standalone sibling. |
346
+ | Nothing appears in contained mode. | Give the parent a nonzero height and `position: relative`. Check that it is visible and that `ResizeObserver` is available. |
347
+ | The scene is visible but does not animate. | Check `paused`, `reducedMotion`, system motion preferences, tab visibility, and whether the canvas is in the viewport. |
348
+ | Custom artwork is replaced by soft snowflakes. | Check the image URL, network response, and CORS headers. A loading or failed sprite uses the built-in fallback. |
349
+ | `color` does not change a custom sprite. | Artwork intentionally retains its original colors. Edit the source image to recolor it. |
350
+ | The overlay appears behind another element. | Check `zIndex` and ancestor stacking contexts. A higher value cannot escape its parent's stacking context. |
351
+ | Rendering is slow. | Lower quality and intensity, reduce fog layers, limit the covered area, and profile all active effects together. |
352
+
353
+ If the browser cannot provide a 2D rendering context, the engine stops drawing and scheduling. Use `onContextLost` if your application needs to respond to that condition.
354
+
355
+ ## Local development
356
+
357
+ This section is for contributors working from a source checkout. It is not required when using the installed npm package. Use a maintained Node.js LTS release and pnpm.
358
+
359
+ From the repository root, install dependencies, build the library, and start the demo:
360
+
361
+ ```sh
362
+ pnpm install
363
+ pnpm build
364
+ pnpm demo
365
+ ```
366
+
367
+ The demo runs at `http://localhost:5173` by default. Vite may select another port if that one is occupied; use the address printed in the terminal.
368
+
369
+ The repository is a pnpm workspace:
370
+
371
+ ```text
372
+ packages/
373
+ core/ Internal canvas engine, scheduler, budgets, and pools
374
+ effects/ Internal Rain, Snow, and Fog implementations
375
+ react-weather-effects/ Public entry point and distributable bundle
376
+ apps/
377
+ demo/ Vite demonstration application
378
+ test/ Vitest rendering and lifecycle tests
379
+ ```
380
+
381
+ The public package bundles the private workspaces. Consumers should not need to install or resolve those private package names.
382
+
383
+ Run these commands from the repository root:
384
+
385
+ | Command | Purpose |
386
+ | --- | --- |
387
+ | `pnpm install` | Install workspace dependencies. |
388
+ | `pnpm build` | Build core, effects, and the public package in dependency order. |
389
+ | `pnpm demo` | Start the local Vite demo. Build the packages first. |
390
+ | `pnpm test` | Run the test suite once. |
391
+ | `pnpm test:watch` | Run tests in watch mode. |
392
+ | `pnpm typecheck` | Check TypeScript project references; writes generated files into `dist`. |
393
+ | `pnpm size` | Measure bundle sizes after a package build. |
394
+ | `pnpm --filter weather-effects-demo build` | Build the demo for production. |
395
+
396
+ Run `pnpm typecheck` and `pnpm build` sequentially, not at the same time: both write to the package `dist` directories. Run `pnpm build` after typechecking and before packing a release so the distributable declarations are bundled correctly.
397
+
398
+ The demo consumes built workspace packages. After changing library source, rebuild the packages and reload the demo; restart its server if it retains cached output.
399
+
400
+ ### Test a local package build
401
+
402
+ After running `pnpm build`, create a tarball of the public package:
403
+
404
+ ```sh
405
+ pnpm --dir packages/react-weather-effects pack --pack-destination ../..
406
+ ```
407
+
408
+ Install the generated tarball in a separate application using its actual path. For the current beta version:
409
+
410
+ ```sh
411
+ npm install /path/to/react-weather-effects-0.1.0-beta.0.tgz
412
+ ```
413
+
414
+ For implementation contracts, package boundaries, and future plans, see `ARCHITECTURE.md` in the source repository. Keep changes focused, cover rendering and lifecycle behavior with tests, and verify the build, types, and size budget before submitting changes.
415
+
416
+ ## License
417
+
418
+ Licensed under the MIT license.