diff --git a/.planning/handovers/4.3.0_PLAN.md b/.planning/handovers/4.3.0_PLAN.md new file mode 100644 index 00000000000..f5b3952186c --- /dev/null +++ b/.planning/handovers/4.3.0_PLAN.md @@ -0,0 +1,389 @@ +# tsParticles 4.3.0 — Master Coordination Plan + +## Overview + +This plan coordinates **3 independent features** targeting the `4.3.0` release. +Each feature has its own detailed handover plan. This document manages sequencing, dependencies, +conflict areas, and release readiness. + +--- + +## Hard Rule: Master Plan First, Feature Plans After + +All validation steps defined in **this** document (see "Step 0: Baseline Validation" below) **must be executed and passing before any feature-specific plan** (i.e. before `BACKGROUND_CANVAS_PLAN.md`, `FLUID_INTERACTION_PLAN.md`, `GIF_SHAPE_PLAN.md`) is worked on. Even if new feature plans are added to the 4.3.0 scope mid-cycle, run this master plan's checks first, then proceed to the feature plan's steps. + +This rule exists because the 4.2.0 release was burned by a `workspace:*` → hardcoded version regression discovered only at CI publish time. Running the master gate first establishes a baseline so feature work doesn't mask or introduce regressions. + +--- + +## Step 0: Baseline Validation (run BEFORE any feature work) + +Before touching any feature plan, execute these checks on the current `main` state: + +### 0.1 — Workspace `@tsparticles/` deps use `workspace:*` + +Run from repo root: +```bash +node -e " + const glob = require('fs').readdirSync; + const dirs = [...require('fs').readFileSync('pnpm-workspace.yaml','utf8').matchAll(/^ - (.+)/gm)].map(m=>m[1]); + const pkgs = dirs.flatMap(d => glob(d, {withFileTypes:true}).filter(e=>e.isDirectory()).map(e=>e.parentPath+'/'+e.name+'/package.json')); + pkgs.push('engine/package.json'); + let fail = false; + for (const f of pkgs) { + try { + const p = require(f); + const deps = {...p.dependencies, ...p.devDependencies}; + for (const [k,v] of Object.entries(deps)) + if (k.startsWith('@tsparticles/') && v !== 'workspace:*') + { console.log('❌', f, ':', k, v); fail = true; } + } catch { /* skip non-existent */ } + } + if (!fail) console.log('✅ all workspace:*'); +" +``` +Known root cause: version-bump scripts (e.g. `lerna version`, `nx release version`) sometimes rewrite `workspace:*` → hardcoded version in one or more `package.json` files. In the 4.2.0 release, `cli/utils/eslint-config/package.json` had `@tsparticles/prettier-config` changed to `"4.2.0"` while the lockfile still expected `workspace:*`. + +### 0.2 — `pnpm run build:ci` passes on clean state + +```bash +pnpm run build:ci +``` + +If either check fails, fix the regression first before starting any feature work. Log the fix commit hash in the dashboard. + +--- + +## Progress Dashboard + +Checklist riassuntiva di tutte le attività del piano. Da tenere aggiornata durante l'esecuzione. + +``` +## 4.3.0 Dashboard — [date] + +### A: Background Canvas +- [ ] Phase 0: hook points identified +- [ ] Phase 1: size delta measured, path chosen +- [ ] Phase 1b: release target decided +- [ ] Phase 2: API/types exported +- [ ] Phase 3: frame hook integrated +- [ ] Phase 4: context cache + fallback integrated +- [ ] Phase 5: warning/error policy integrated +- [ ] Phase 6: tests added and passing +- [ ] Phase 7: docs/examples updated +- [ ] Phase 8: validation commands passing + +### B: Fluid Interaction +- [ ] Step 0: conventions aligned, naming frozen +- [ ] Step 1: package skeleton + noop interactor +- [ ] Step 2: options contract + loaders + tests +- [ ] Step 3: baseline DDR solver +- [ ] Step 4: boundaries + caps + stability +- [ ] Step 5: viscosity + pointer stirring +- [ ] Step 6: interoperability validated +- [ ] Step 7: performance + soak passed +- [ ] Step 8: docs + presets finalized +- [ ] Step 9: (optional) plugin orchestration + +### C: GIF Shape +- [ ] Step 1: scaffold shapes/gif/ (17 files) +- [ ] Step 2: copy + adapt GifUtils (10 files) +- [ ] Step 3: remove GIF from shape-image (9 files) +- [ ] Step 4: add to @tsparticles/all (2 files) +- [ ] Step 5: build + verify +- [ ] Step 6: demo config updated + +### Release Gate +- [ ] Step 0.1 — Workspace `@tsparticles/` deps use `workspace:*` +- [ ] Step 0.2 — `pnpm run build:ci` passes +- [ ] Root `package.json` version matches packages (sincronizzazione automatica via script, non manuale) +- [ ] Integration tests pass +- [ ] Demo server smoke-tested +``` + +--- + +## Feature Inventory + +| # | Feature | Package(s) Affected | Type | Plan | +|---|---------|--------------------|------|------| +| A | Background Canvas programmable hook | `@tsparticles/engine` | Engine enhancement | `BACKGROUND_CANVAS_PLAN.md` | +| B | Fluid Particle Interaction | `@tsparticles/interaction-particles-fluid` | New package | `FLUID_INTERACTION_PLAN.md` | +| C | Separate GIF Shape from Image Shape | `@tsparticles/shape-gif` (new), `@tsparticles/shape-image` (modified), `@tsparticles/all` (modified) | Refactor + new package | `GIF_SHAPE_PLAN.md` | + +--- + +## Dependency Graph + +``` +A (Background Canvas) B (Fluid Interaction) C (GIF Shape) + engine only new package only shape-image refactor + no deps on B/C no deps on A/C shape-gif (new) + all bundle update + no deps on A/B + +All three are INDEPENDENT — no shared files, no conflicting changes. +They can be implemented in any order or in parallel. +``` + +### Conflict matrix + +| Area | A ∩ B | A ∩ C | B ∩ C | +|------|-------|-------|-------| +| Files | None | None | None | +| Types | None | None | None | +| Engine | A modifies engine; B, C only use engine types | Same | None | +| Bundle `all` | Not affected | C touches `bundles/all/` | Not affected | +| Risk | None isolated | None isolated | None isolated | + +**Verdict:** All three are fully parallelizable. No merge conflicts expected. +The only integration point is `@tsparticles/all` (feature C adds a dependency). + +--- + +## Sequencing Strategy + +Since all features are independent, the recommended execution is **parallel**: + +``` +Week 1-2: + Agent A: Background Canvas (Phases 0-4) + Agent B: Fluid Interaction (Steps 0-5) + Agent C: GIF Shape (Steps 1-4) + +Week 2-3: + Agent A: Background Canvas (Phases 5-8) + Agent B: Fluid Interaction (Steps 6-9) + Agent C: GIF Shape (Steps 5-6) + +Week 3-4: + Integration: build all, integration tests, soak tests + Release prep: changelog, docs, version bump to 4.3.0 +``` + +If resources are limited, priority order: +1. **Feature C (GIF Shape)** — simplest, lowest risk, unblocks the image shape cleanup +2. **Feature A (Background Canvas)** — moderate, needs bundle-size measurement gate +3. **Feature B (Fluid Interaction)** — most complex, new simulation code, needs tuning + +--- + +## Cross-feature Concerns + +### Per-Feature Verification (run during/after each feature implementation) + +- [ ] **A:** Bundle impact decision recorded (engine vs plugin path) and within accepted threshold +- [ ] **A:** Zero regressions on legacy background behavior +- [ ] **B:** No NaN/Infinity in standard presets after 20k+ frame soak +- [ ] **B:** Fluid visual effect clearly distinguishable from plain attract/repulse +- [ ] **C:** `@tsparticles/shape-image` builds with zero GIF references +- [ ] **C:** `@tsparticles/shape-gif` builds and registers as `type: "gif"` +- [ ] **C:** GIF demo config (`utils/configs/src/g/gifs.ts`) works with new shape + +### Pre-Release Gate (run before cutting release branch) + +- [ ] **ALL:** `pnpm run build:ci` passes +- [ ] **ALL:** No high-severity open issues targeting 4.3.0 +- [ ] **ALL:** Workspace `@tsparticles/` deps use `workspace:*` (re-run Step 0.1 check) +- [ ] **ALL:** Root `package.json` version matches packages — implementata una sincronizzazione automatica (es. script `scripts/sync-root-version.js` chiamato da `version` lifecycle hook o integrato nel comando di version bump) +- [ ] **ALL:** Integration tests pass (see below) +- [ ] **ALL:** Demo server smoke-tested (see below) +- [ ] **ALL:** Integration tests pass (see below) +- [ ] **ALL:** Demo server smoke-tested (see below) + +### Integration testing + +After individual feature verification, run combined scenarios: + +```bash +pnpm run build:ci +pnpm exec vitest run +``` + +Then smoke-test the demo server: + +```bash +cd demo/vanilla && pnpm start +``` + +### Changelog entries (draft) + +Each feature produces its own changelog section: + +``` +## 4.3.0 + +### New Features +- Background Canvas: programmable `background.draw` callback + `background.element` target (#A) +- Fluid Interaction: new `@tsparticles/interaction-particles-fluid` package with DDR-based + liquid simulation, pointer stirring, and presets (#B) +- GIF Shape: new `@tsparticles/shape-gif` shape extracted from image shape. + Use `type: "gif"` instead of `type: "image"` with `gif: true` (#C) + +### Breaking Changes +- `@tsparticles/shape-image`: removed animated GIF support (`gif` option no longer available). + Migrate to `@tsparticles/shape-gif` with `type: "gif"`. + +### Bundle Changes +- `@tsparticles/all`: now includes `@tsparticles/shape-gif` +``` + +--- + +## Feature A — Background Canvas (from `BACKGROUND_CANVAS_PLAN.md`) + +**Target:** `@tsparticles/engine` +**Load function:** N/A (built into engine) +**Plan phases:** 0-8 + +### Key decisions (from plan) + +1. **Bundle impact gate** (Phase 1): measure delta; if LOW/MEDIUM → keep in engine; if HIGH → extract plugin +2. **Release target gate** (Phase 1b): feature-gate passes → target 4.3.0 + +### Implementation phases + +| Phase | Description | Verification | +|-------|-------------|-------------| +| 0 | Baseline scan — identify insertion points | Short notes | +| 1 | Bundle impact measurement + decision | Size delta block | +| 1b | Release target decision | Go/No-go for 4.3.0 | +| 2 | Type contract — add `element`, `draw`, `BackgroundDrawContext` | Compile passes | +| 3 | Runtime hook in `RenderManager.drawParticles` after `clear()` | Callback runs in order | +| 4 | Target context resolution + caching | Deterministic selection | +| 5 | Error/warning handling (once-per-key, try/catch) | No log flood | +| 6 | Tests — load, precedence, fallback, resilience | Tests green | +| 7 | Docs/examples | No ambiguity | +| 8 | Validation gate | Final checklist complete | + +### Critical files + +| File | Change | +|------|--------| +| `engine/src/Options/Interfaces/Background/IBackground.ts` | Add `element`, `draw` | +| `engine/src/Options/Classes/Background/Background.ts` | Add properties + load | +| `engine/src/Core/Utils/RenderManager.ts` | Add callback call after `clear()` | +| `engine/src/export-types.ts` | Export `BackgroundDrawContext` | + +--- + +## Feature B — Fluid Interaction (from `FLUID_INTERACTION_PLAN.md`) + +**Target:** `@tsparticles/interaction-particles-fluid` +**Load function:** `loadParticlesFluidInteraction(engine)` +**Plan steps:** 0-9 + +### Execution governance + +- Agent must NOT create commits or PRs +- Agent must leave changes uncommitted for maintainer review + +### Implementation steps + +| Step | Description | Verification | +|------|-------------|-------------| +| 0 | Repository alignment — inspect conventions, freeze naming | Checklist | +| 1 | Package skeleton — scaffold + noop `FluidInteractor` | Build passes | +| 2 | Options contract — `IFluid` + classes + loaders + unit tests | Tests pass | +| 3 | Baseline DDR solver — density, pressure, displacement, velocity reconstruction | Visual blob, no NaN | +| 4 | Constraints — boundary clamp/bounce, maxNeighbors, stability | Stable at edges | +| 5 | Viscosity + pointer stirring | Local deformation + recovery | +| 6 | Interoperability — validate with move/emitters/repulse/collisions | Matrix validated | +| 7 | Performance + soak — benchmark 300/600/1000 particles | Meets thresholds | +| 8 | Docs + presets — README, 3 presets, changelog | No undocumented options | +| 9 | Optional advanced path (plugin orchestration) — only if ordering artifacts | Before/after evidence | + +### Critical options (defaults) + +```json +{ + "particles": { + "fluid": { + "enable": false, + "radius": 34, "stiffness": 0.45, "nearStiffness": 0.8, + "restDensity": 3, "viscosity": 0.08, "iterations": 2, + "maxForce": 2.5, "maxNeighbors": 64, + "boundaryBounce": 0.2, "boundaryClamp": true, + "gravity": { "enable": true, "acceleration": 0.01 }, + "pointer": { "enable": false, "radius": 90, "strength": 0.45, "mode": "push" } + } + } +} +``` + +--- + +## Feature C — GIF Shape (from `GIF_SHAPE_PLAN.md`) + +**Target:** `@tsparticles/shape-gif` (new), `@tsparticles/shape-image` (modified), `@tsparticles/all` (modified) +**Load function:** `loadGifShape(engine)` +**Plan substeps:** 1.1 through 6.1 + +### Implementation steps + +| Step | Description | Verification | +|------|-------------|-------------| +| 1 | Scaffold `shapes/gif/` — 17 new files | Directory exists | +| 2 | Copy `GifUtils/` from image shape — adapt `drawGif`, remove `loadGifImage` | Imports resolve | +| 3 | Remove GIF from `@tsparticles/shape-image` — 9 files modified | `grep` zero matches | +| 4 | Add `@tsparticles/shape-gif` to `@tsparticles/all` bundle | Build passes | +| 5 | Build + verify — `pnpm run build:ci` | `dist/` output OK | +| 6 | Update demo config `utils/configs/src/g/gifs.ts` | Demo works | + +### Critical files + +| File | Change | Type | +|------|--------|------| +| `shapes/gif/src/GifDrawer.ts` | New — IShapeDrawer with GIF cache | New | +| `shapes/gif/src/GifUtils/Utils.ts` | Copied + adapted (drawGif, no loadGifImage) | Copy+edit | +| `shapes/image/src/Utils.ts` | Remove 7 gif fields + import | Modify | +| `shapes/image/src/ImageDrawer.ts` | Remove drawGif branch, gif fields | Modify | +| `bundles/all/src/index.ts` | Add loadGifShape import + call | Modify | + +### User-facing config change + +```jsonc +// Before (4.2.x): +{ "shape": { "type": "image", "options": { "image": { "gif": true, "src": "...", "width": 200, "height": 200 } } } } + +// After (4.3.0): +{ "shape": { "type": "gif", "options": { "gif": { "src": "...", "width": 200, "height": 200 } } } } +``` + +--- + +## Release Infrastructure + +### Root `package.json` version sync + +Il root `package.json` ha un campo `"version"` che spesso rimane indietro rispetto ai package del workspace quando si lancia `lerna version` o un comando simile, perché questi tool aggiornano solo i package figli. + +**Task:** Creare uno script (es. `scripts/sync-root-version.js`) che legga la versione da un package del workspace (es. `engine/package.json`) e la scriva nel root `package.json`. Poi agganciarlo al comando di version bump in modo che finisca **nello stesso commit/tag**, ad esempio: + +- via `"version"` lifecycle hook nel root `package.json`: `"version": "node scripts/sync-root-version.js && git add package.json"` +- o integrato direttamente nello script che lancia il version bump + +Lo script deve: +1. Leggere `engine/package.json` → `version` +2. Leggere `package.json` root → se diverso, sovrascrivere +3. Salvare e loggare il cambio + +Il `git add package.json` nel lifecycle hook garantisce che la modifica sia inclusa nel commit di version bump, non in un commit separato. + +--- + +## Rollback Plan + +If any feature is not ready at the 4.3.0 freeze date: + +| Scenario | Action | +|----------|--------| +| A not ready | Ship B + C as 4.3.0, defer A to 4.4.0 | +| B not ready | Ship A + C as 4.3.0, defer B to 4.4.0 | +| C not ready | Ship A + B as 4.3.0, defer C to 4.4.0 | +| A+B not ready | Ship C alone as 4.3.0 | +| A+B+C not ready | No 4.3.0 — reassess scope | + +Each feature's branch must be independent so partial shipping is trivial. + + diff --git a/.planning/handovers/BACKGROUND_CANVAS_PLAN.md b/.planning/handovers/BACKGROUND_CANVAS_PLAN.md index 87f77823744..125477011b1 100644 --- a/.planning/handovers/BACKGROUND_CANVAS_PLAN.md +++ b/.planning/handovers/BACKGROUND_CANVAS_PLAN.md @@ -1,5 +1,30 @@ # Background Canvas Plan (v5.0 Agent-Ready) +## Summary + +| | | +|---|---| +| **Target** | `@tsparticles/engine` | +| **Phases** | 8 (0–8) | +| **Type** | Engine enhancement | +| **Load function** | N/A (built into engine) | +| **Key decision gate** | Phase 1: bundle impact → engine vs plugin; Phase 1b: 4.2.0 vs 4.3.0 | + +### Progress + +- [ ] Phase 0: Baseline scan +- [ ] Phase 1: Bundle impact + decision +- [ ] Phase 1b: Release target decision +- [ ] Phase 2: Type contract +- [ ] Phase 3: Runtime hook +- [ ] Phase 4: Target resolution/cache +- [ ] Phase 5: Error/warning handling +- [ ] Phase 6: Tests +- [ ] Phase 7: Docs/examples +- [ ] Phase 8: Validation gate + +--- + ## Context Current `background` support is CSS-style only and applied to the particles DOM canvas style (`color`, `image`, `position`, `repeat`, `size`, `opacity`). diff --git a/.planning/handovers/FLUID_INTERACTION_PLAN.md b/.planning/handovers/FLUID_INTERACTION_PLAN.md index b849a98dd75..7edcab52b29 100644 --- a/.planning/handovers/FLUID_INTERACTION_PLAN.md +++ b/.planning/handovers/FLUID_INTERACTION_PLAN.md @@ -1,5 +1,30 @@ # Fluid Particle Interaction - Deep Technical Specification (Draft v2) +## Summary + +| | | +|---|---| +| **Target** | `@tsparticles/interaction-particles-fluid` | +| **Steps** | 10 (0–9) | +| **Type** | New package | +| **Load function** | `loadParticlesFluidInteraction(engine)` | +| **Key constraints** | Don't touch engine; agent must NOT commit/PR | + +### Progress + +- [ ] Step 0: Repository alignment + constraints +- [ ] Step 1: Package skeleton (noop) +- [ ] Step 2: Options contract + loaders +- [ ] Step 3: Baseline DDR solver +- [ ] Step 4: Constraints + boundary behavior +- [ ] Step 5: Viscosity + pointer stirring +- [ ] Step 6: Interoperability hardening +- [ ] Step 7: Performance + soak validation +- [ ] Step 8: Docs, presets, release readiness +- [ ] Step 9: (optional) Advanced path + +--- + ## Document status - Scope: planning only (no implementation in this session) diff --git a/.planning/handovers/GIF_SHAPE_PLAN.md b/.planning/handovers/GIF_SHAPE_PLAN.md new file mode 100644 index 00000000000..d7aafafb5c2 --- /dev/null +++ b/.planning/handovers/GIF_SHAPE_PLAN.md @@ -0,0 +1,1146 @@ +# Plan: Separate Animated GIF from Image Shape + +## Summary + +| | | +|---|---| +| **Target** | `@tsparticles/shape-gif` (new), `@tsparticles/shape-image` (modified), `@tsparticles/all` (modified) | +| **Steps** | 6 (1–6) | +| **Type** | Refactor + new package | +| **Load function** | `loadGifShape(engine)` | +| **Files** | ~17 new (shape-gif), ~9 modified (shape-image), ~2 modified (all bundle) | + +### Progress + +- [ ] Step 1: Scaffold `shapes/gif/` (17 files) +- [ ] Step 2: Copy `GifUtils/` from image shape — adapt `drawGif`, remove `loadGifImage` +- [ ] Step 3: Remove GIF from `@tsparticles/shape-image` (9 files) +- [ ] Step 4: Add `@tsparticles/shape-gif` to `@tsparticles/all` bundle +- [ ] Step 5: Build + verify +- [ ] Step 6: Update demo config (`utils/configs/src/g/gifs.ts`) + +--- + +## Goal + +Extract all animated GIF handling from `@tsparticles/shape-image` into a new `@tsparticles/shape-gif` package. +`@tsparticles/shape-image` loses GIF support entirely (static images only — including `.gif` files loaded as static `` elements, without animation decoding). +`@tsparticles/shape-gif` is included in `@tsparticles/all` only (not in slim/full). + +## Architecture + +``` +@tsparticles/shape-image @tsparticles/shape-gif + src/ src/ + Utils.ts (IImage, IParticleImage, IGifShape.ts (shape options) + ImageParticle — NO gif*) types.ts (GifParticle) + ImageDrawer.ts (NO drawGif) GifDrawer.ts (with GIF cache Map) + ImagePreloader.ts GifUtils/ + index.ts (NO loadGifImage) Utils.ts (decodeGIF, drawGif, getGIFLoopAmount) + Options/Classes/Preload.ts ByteStream.ts + (NO gif flag) Constants.ts + Enums/DisposalMethod.ts + Types/GIF.ts, Frame.ts, ... + (*: all gif* fields removed) +``` + +**Key differences in `drawGif` in the copy:** +- Original (in shape-image): reads `particle.image.gifData`, `particle.image.gifLoopCount`, `particle.image.gif` +- New (in shape-gif): reads `particle.gifData` directly, writes `particle.gifFrame/gifTime/gifLoopCount` directly +- The `gifParticle` has `gifData` at the top level, not nested under `image` + +--- + +## Step 1 — Create `shapes/gif/` package scaffold + +### Sub-step 1.1 — Create directory structure + +```bash +mkdir -p shapes/gif/src +``` + +--- + +### Sub-step 1.2 — Create `shapes/gif/package.json` + +```json +{ + "name": "@tsparticles/shape-gif", + "version": "4.2.0", + "description": "tsParticles shape for rendering particles as animated GIFs", + "homepage": "https://particles.js.org", + "scripts": { + "build": "tsparticles-build", + "version": "tsparticles-build -d && git add package.dist.json && tsparticles-build -p -l && git add .", + "prepack": "pnpm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tsparticles/tsparticles.git", + "directory": "shapes/gif" + }, + "keywords": [ + "front-end", "frontend", "tsparticles", "particles", "particle", + "canvas", "animation", "web", "html5", "css3", "animated", + "background", "confetti", "fireworks", "typescript", "javascript", + "tsparticles-shape", "gif", "animated-gif" + ], + "author": "Matteo Bruni ", + "license": "MIT", + "bugs": { "url": "https://github.com/tsparticles/tsparticles/issues" }, + "prettier": "@tsparticles/prettier-config", + "files": ["dist"], + "sideEffects": false, + "browser": "dist/browser/index.js", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "default": "./dist/esm/index.js" + }, + "./lazy": { + "types": "./dist/types/index.lazy.d.ts", + "browser": "./dist/browser/index.lazy.js", + "import": "./dist/esm/index.lazy.js", + "require": "./dist/cjs/index.lazy.js", + "default": "./dist/esm/index.lazy.js" + }, + "./package.json": "./dist/package.json" + }, + "peerDependencies": { "@tsparticles/engine": "workspace:*" }, + "devDependencies": { + "@tsparticles/cli-build": "workspace:*", + "@tsparticles/engine": "workspace:*" + }, + "publishConfig": { "access": "public", "directory": "dist", "linkDirectory": true }, + "type": "module" +} +``` + +--- + +### Sub-step 1.3 — Create `shapes/gif/package.dist.json` + +```json +{ + "name": "@tsparticles/shape-gif", + "version": "4.2.0", + "description": "tsParticles gif shape", + "homepage": "https://particles.js.org", + "repository": { "type": "git", "url": "git+https://github.com/tsparticles/tsparticles.git", "directory": "shapes/gif" }, + "keywords": [ + "front-end", "frontend", "tsparticles", "particles", "particle", + "canvas", "jsparticles", "xparticles", "particles-js", "particles.js", + "particles-ts", "particles.ts", "typescript", "javascript", + "animation", "web", "html5", "web-design", "webdesign", + "css", "html", "css3", "animated", "background", "tsparticles-shape" + ], + "author": "Matteo Bruni ", + "license": "MIT", + "bugs": { "url": "https://github.com/tsparticles/tsparticles/issues" }, + "sideEffects": false, + "jsdelivr": "tsparticles.shape.gif.min.js", + "unpkg": "tsparticles.shape.gif.min.js", + "browser": "browser/index.js", + "main": "cjs/index.js", + "module": "esm/index.js", + "types": "types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", "browser": "./browser/index.js", + "import": "./esm/index.js", "require": "./cjs/index.js", + "default": "./esm/index.js" + }, + "./lazy": { + "types": "./types/index.lazy.d.ts", "browser": "./browser/index.lazy.js", + "import": "./esm/index.lazy.js", "require": "./cjs/index.lazy.js", + "default": "./esm/index.lazy.js" + }, + "./package.json": "./package.json" + }, + "peerDependencies": { "@tsparticles/engine": "4.2.0" }, + "publishConfig": { "access": "public" }, + "type": "module" +} +``` + +--- + +### Sub-step 1.4 — Create `shapes/gif/.browserslistrc` + +``` +extends @tsparticles/browserslist-config +``` + +--- + +### Sub-step 1.5 — Create `shapes/gif/eslint.config.js` + +```javascript +import tsParticlesESLintConfig from "@tsparticles/eslint-config"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + tsParticlesESLintConfig, +]); +``` + +--- + +### Sub-step 1.6 — Create `shapes/gif/rollup.config.js` + +```javascript +import { loadParticlesShape } from "@tsparticles/rollup-plugin"; +import { fileURLToPath } from "node:url"; +import fs from "fs-extra"; +import path from "node:path"; + +const __filename = fileURLToPath(import.meta.url), + __dirname = path.dirname(__filename), + rootPkgPath = path.join(__dirname, "package.json"), + pkg = await fs.readJson(rootPkgPath), + version = pkg.version; + +export default loadParticlesShape({ + moduleName: "gif", + shapeName: "Gif", + version, + dir: __dirname, + progress: false, +}); +``` + +--- + +### Sub-step 1.7 — Create TypeScript config files + +Create `shapes/gif/tsconfig.base.json`: +```json +{ + "extends": "@tsparticles/tsconfig/dist/tsconfig.base.json", + "compilerOptions": { "rootDir": "./src" }, + "include": ["./src"] +} +``` + +Create `shapes/gif/tsconfig.json`: +```json +{ + "extends": ["./tsconfig.base.json", "@tsparticles/tsconfig/dist/tsconfig.json"], + "compilerOptions": { "outDir": "./dist/cjs" } +} +``` + +Create `shapes/gif/tsconfig.browser.json`: +```json +{ + "extends": ["./tsconfig.base.json", "@tsparticles/tsconfig/dist/tsconfig.browser.json"], + "compilerOptions": { "outDir": "./dist/browser" } +} +``` + +Create `shapes/gif/tsconfig.module.json`: +```json +{ + "extends": ["./tsconfig.base.json", "@tsparticles/tsconfig/dist/tsconfig.module.json"], + "compilerOptions": { "outDir": "./dist/esm" } +} +``` + +Create `shapes/gif/tsconfig.types.json`: +```json +{ + "extends": ["./tsconfig.base.json", "@tsparticles/tsconfig/dist/tsconfig.types.json"], + "compilerOptions": { "outDir": "./dist/types" } +} +``` + +--- + +### Sub-step 1.8 — Create `shapes/gif/typedoc.json` + +```json +{ + "projectDocuments": ["../markdown/**/*.md"], + "entryPoints": ["./src/"], + "entryPointStrategy": "expand", + "name": "tsParticles GIF Shape", + "includeVersion": true, + "hideGenerator": true, + "out": "./docs", + "validation": { "invalidLink": true, "notDocumented": true } +} +``` + +--- + +### Sub-step 1.9 — Create `shapes/gif/src/IGifShape.ts` + +```typescript +import type { IShapeValues } from "@tsparticles/engine"; + +export interface IGifShape extends IShapeValues { + height: number; + loopCount?: number; + name?: string; + src: string; + width: number; +} +``` + +--- + +### Sub-step 1.10 — Create `shapes/gif/src/types.ts` + +```typescript +import type { Particle } from "@tsparticles/engine"; +import type { GIF } from "./GifUtils/Types/GIF.js"; + +export type GifParticle = Particle & { + gifData?: GIF; + gifFrame: number; + gifLoopCount: number; + gifTime: number; +}; +``` + +--- + +### Sub-step 1.11 — Create `shapes/gif/src/GifDrawer.ts` + +```typescript +import type { Container, IShapeDrawData, IShapeDrawer } from "@tsparticles/engine"; +import { getGIFLoopAmount } from "./GifUtils/Utils.js"; +import type { GifParticle } from "./types.js"; +import type { IGifShape } from "./IGifShape.js"; +import { decodeGIF, drawGif } from "./GifUtils/Utils.js"; +import type { GIF } from "./GifUtils/Types/GIF.js"; + +export class GifDrawer implements IShapeDrawer { + readonly #container: Container; + readonly #gifCache: Map; + + constructor(container: Container) { + this.#container = container; + this.#gifCache = new Map(); + } + + async draw(data: IShapeDrawData): Promise { + const { context, opacity } = data; + + if (!data.particle.gifData) return; + + context.globalAlpha = opacity; + await drawGif(data, this.#container.canvas.render.settings); + context.globalAlpha = 1; + } + + async particleInit(container: Container, particle: GifParticle): Promise { + const shapeData = particle.shapeData as IGifShape | undefined; + + if (!shapeData?.src) return; + + let gifData = this.#gifCache.get(shapeData.src); + + if (!gifData) { + particle.gifData = undefined; + particle.gifFrame = 0; + particle.gifTime = 0; + particle.gifLoopCount = 0; + + gifData = await decodeGIF(shapeData.src, { colorSpace: "srgb" }); + this.#gifCache.set(shapeData.src, gifData); + } + + particle.gifData = gifData; + particle.gifFrame = 0; + particle.gifTime = 0; + particle.gifLoopCount = getGIFLoopAmount(gifData); + + if (particle.gifLoopCount === 0) { + particle.gifLoopCount = Number.POSITIVE_INFINITY; + } + + if (shapeData.loopCount !== undefined) { + particle.gifLoopCount = shapeData.loopCount; + } + } + + destroy(): void { + this.#gifCache.clear(); + } +} +``` + +--- + +### Sub-step 1.12 — Create `shapes/gif/src/index.ts` + +```typescript +import type { Engine } from "@tsparticles/engine"; +import { GifDrawer } from "./GifDrawer.js"; + +export async function loadGifShape(engine: Engine): Promise { + engine.checkVersion("4.2.0"); + + await engine.pluginManager.register(e => { + e.pluginManager.addShape(["gif", "gifs"], container => + Promise.resolve(new GifDrawer(container)), + ); + }); +} +``` + +--- + +### Sub-step 1.13 — Create `shapes/gif/src/index.lazy.ts` + +```typescript +import type { Engine } from "@tsparticles/engine"; + +export async function loadGifShape(engine: Engine): Promise { + engine.checkVersion("4.2.0"); + + await engine.pluginManager.register(async e => { + const { GifDrawer } = await import("./GifDrawer.js"); + + e.pluginManager.addShape(["gif", "gifs"], container => + Promise.resolve(new GifDrawer(container)), + ); + }); +} +``` + +--- + +### Sub-step 1.14 — Create `shapes/gif/src/browser.ts` + +```typescript +import { loadGifShape } from "./index.js"; + +const globalObject = globalThis as typeof globalThis & { + __tsParticlesInternals?: Record; + loadGifShape?: typeof loadGifShape; +}; +globalObject.__tsParticlesInternals = globalObject.__tsParticlesInternals ?? {}; +globalObject.loadGifShape = loadGifShape; + +export * from "./index.js"; +``` + +--- + +## Step 2 — Copy `GifUtils/` from `shapes/image/src/GifUtils/` to `shapes/gif/src/GifUtils/` + +### Sub-step 2.1 — Copy all files (preserve directory tree) + +Copy the entire `GifUtils/` directory from `shapes/image/src/GifUtils/` to `shapes/gif/src/GifUtils/`: + +```bash +cp -r shapes/image/src/GifUtils/ shapes/gif/src/GifUtils/ +``` + +Files copied: +``` +GifUtils/ + ByteStream.ts + Constants.ts + Utils.ts + Enums/DisposalMethod.ts + Types/GIF.ts + Types/Frame.ts + Types/ApplicationExtension.ts + Types/GIFDataHeaders.ts + Types/GIFProgressCallbackFunction.ts + Types/PlainTextData.ts +``` + +--- + +### Sub-step 2.2 — Fix import in copied `GifUtils/Utils.ts` + +In `shapes/gif/src/GifUtils/Utils.ts`, replace the import at line 3: + +**Before:** +```typescript +import { type IImage, type ImageParticle, loadImage } from "../Utils.js"; +``` + +**After:** +```typescript +import type { GifParticle } from "../types.js"; +``` + +--- + +### Sub-step 2.3 — Remove `loadGifImage` function from copied `GifUtils/Utils.ts` + +Delete the entire `loadGifImage` function (the last function in the file, lines 755-777 in the original). The function signature is: + +```typescript +export async function loadGifImage(image: IImage, canvasSettings: CanvasRenderingContext2DSettings): Promise { +``` + +This function is replaced by the `GifDrawer.particleInit()` method. + +--- + +### Sub-step 2.4 — Replace `drawGif` function in copied `GifUtils/Utils.ts` + +Replace the entire `drawGif` function (line 641 onward in the original) with the adapted version below. + +The original function accesses GIF data via `particle.image.gifData`. The new version accesses it via `particle.gifData` directly. + +**Full replacement for `drawGif`:** + +```typescript +export function drawGif(data: IShapeDrawData, canvasSettings?: CanvasRenderingContext2DSettings): void { + const { context, radius, particle, delta } = data, + gifData = particle.gifData; + + if (!gifData) { + return; + } + + const offscreenCanvas = new OffscreenCanvas(gifData.width, gifData.height), + offscreenContext = offscreenCanvas.getContext("2d", canvasSettings); + + if (!offscreenContext) { + throw new Error("could not create offscreen canvas context"); + } + + offscreenContext.imageSmoothingQuality = "low"; + offscreenContext.imageSmoothingEnabled = false; + + offscreenContext.clearRect(originPoint.x, originPoint.y, offscreenCanvas.width, offscreenCanvas.height); + + let frameIndex = particle.gifFrame ?? defaultFrame; + + const pos = { x: -gifData.width * half, y: -gifData.height * half }, + frame = gifData.frames[frameIndex]!; + + particle.gifTime ??= initialTime; + + if (!frame.bitmap) { + return; + } + + context.scale(radius / gifData.width, radius / gifData.height); + + switch (frame.disposalMethod) { + case DisposalMethod.UndefinedA: + case DisposalMethod.UndefinedB: + case DisposalMethod.UndefinedC: + case DisposalMethod.UndefinedD: + case DisposalMethod.Replace: + offscreenContext.drawImage(frame.bitmap, frame.left, frame.top); + context.drawImage(offscreenCanvas, pos.x, pos.y); + offscreenContext.clearRect(originPoint.x, originPoint.y, offscreenCanvas.width, offscreenCanvas.height); + break; + case DisposalMethod.Combine: + offscreenContext.drawImage(frame.bitmap, frame.left, frame.top); + context.drawImage(offscreenCanvas, pos.x, pos.y); + break; + case DisposalMethod.RestoreBackground: + offscreenContext.drawImage(frame.bitmap, frame.left, frame.top); + context.drawImage(offscreenCanvas, pos.x, pos.y); + offscreenContext.clearRect(originPoint.x, originPoint.y, offscreenCanvas.width, offscreenCanvas.height); + if (!gifData.globalColorTable.length) { + offscreenContext.putImageData(gifData.frames[firstIndex]!.image, pos.x + frame.left, pos.y + frame.top); + } else { + offscreenContext.putImageData(gifData.backgroundImage, pos.x, pos.y); + } + break; + case DisposalMethod.RestorePrevious: + { + const previousImageData = offscreenContext.getImageData( + originPoint.x, originPoint.y, offscreenCanvas.width, offscreenCanvas.height, + ); + + offscreenContext.drawImage(frame.bitmap, frame.left, frame.top); + context.drawImage(offscreenCanvas, pos.x, pos.y); + offscreenContext.clearRect(originPoint.x, originPoint.y, offscreenCanvas.width, offscreenCanvas.height); + offscreenContext.putImageData(previousImageData, originPoint.x, originPoint.y); + } + break; + } + + particle.gifTime += delta.value; + + if (particle.gifTime > frame.delayTime) { + particle.gifTime -= frame.delayTime; + + if (++frameIndex >= gifData.frames.length) { + if (--particle.gifLoopCount <= defaultLoopCount) { + return; + } + + frameIndex = firstIndex; + offscreenContext.clearRect(originPoint.x, originPoint.y, offscreenCanvas.width, offscreenCanvas.height); + } + + particle.gifFrame = frameIndex; + } + + context.scale(gifData.width / radius, gifData.height / radius); +} +``` + +**Key differences from the original:** +1. `IShapeDrawData` instead of `IShapeDrawData` +2. `gifData = particle.gifData` instead of `image = particle.image` +3. `if (!gifData)` instead of `if (!image?.gifData || !image.gif)` +4. All `image.gifData.*` replaced with `gifData.*` +5. Removed `particle.gifLoopCount ??= image.gifLoopCount ?? defaultLoopCount` (loop count is set by `particleInit()`) +6. All `image.*` references removed + +--- + +### Sub-step 2.5 — Verify no broken imports in copied GifUtils files + +Check that all imported paths resolve correctly: + +| File | Imports | Status | +|---|---|---| +| `ByteStream.ts` | `./Types/GIFDataHeaders.js` | ✅ Internal | +| `Constants.ts` | none | ✅ | +| `Utils.ts` | `@tsparticles/engine`, `../types.js`, `./Constants.js`, `./ByteStream.js`, `./Enums/DisposalMethod.js`, `./Types/GIF.js`, `./Types/GIFDataHeaders.js`, `./Types/GIFProgressCallbackFunction.js` | ✅ Fixed in 2.2 | +| `Enums/DisposalMethod.ts` | none | ✅ | +| `Types/GIF.ts` | `@tsparticles/engine`, `./ApplicationExtension.js`, `./Frame.js` | ✅ | +| `Types/Frame.ts` | `@tsparticles/engine`, `../Enums/DisposalMethod.js`, `./PlainTextData.js` | ✅ | +| `Types/GIFProgressCallbackFunction.ts` | `@tsparticles/engine` | ✅ | +| `Types/PlainTextData.ts` | `@tsparticles/engine` | ✅ | +| Others | none | ✅ | + +--- + +## Step 3 — Remove GIF from `@tsparticles/shape-image` + +### Sub-step 3.1 — Delete `GifUtils/` directory from shape-image + +```bash +rm -rf shapes/image/src/GifUtils/ +``` + +--- + +### Sub-step 3.2 — Clean `shapes/image/src/Utils.ts` + +**Remove import (line 2):** +```typescript +import type { GIF } from "./GifUtils/Types/GIF.js"; +``` + +**Remove 3 fields from `IImage` interface (lines 18-20):** +```typescript +gif: boolean; +gifData?: GIF; +gifLoopCount?: number; +``` + +**Remove 3 fields from `IParticleImage` interface (lines 37-39):** +```typescript +gif: boolean; +gifData?: GIF; +gifLoopCount?: number; +``` + +**Remove 3 fields from `ImageParticle` type (lines 50-52):** +```typescript +gifFrame?: number; +gifLoopCount?: number; +gifTime?: number; +``` + +After removal, `ImageParticle` becomes: +```typescript +export type ImageParticle = Particle & { + image?: IParticleImage; +}; +``` + +**Remove `gif` field from `replaceImageColor` return value (line 166):** +```typescript +// REMOVE this line from the IParticleImage literal: +gif: imageData.gif, +``` + +--- + +### Sub-step 3.3 — Clean `shapes/image/src/IImageShape.ts` + +**Remove `gif: boolean;` (line 4).** + +After: +```typescript +export interface IImageShape extends IShapeValues { + height: number; + name: string; + replaceColor: boolean; + src: string; + width: number; +} +``` + +--- + +### Sub-step 3.4 — Clean `shapes/image/src/Options/Classes/Preload.ts` + +**Remove `gif` property (line 5):** +```typescript +gif = false; // REMOVE +``` + +**Remove `loadProperty(this, "gif", data.gif);` (line 17):** + +After: +```typescript +export class Preload implements IPreload, IOptionLoader { + height?: number; + name?: string; + replaceColor?: boolean; + src = ""; + width?: number; + + load(data?: RecursivePartial): void { + if (isNull(data)) { return; } + + loadProperty(this, "height", data.height); + loadProperty(this, "name", data.name); + loadProperty(this, "replaceColor", data.replaceColor); + loadProperty(this, "src", data.src); + loadProperty(this, "width", data.width); + } +} +``` + +--- + +### Sub-step 3.5 — Clean `shapes/image/src/Options/Interfaces/IPreload.ts` + +**Remove `gif: boolean` and its JSDoc (lines 3-5):** + +After: +```typescript +/** The preload image options */ +export interface IPreload { + /** The image height */ + height?: number; + /** The image name */ + name?: string; + /** Replace the color with the particle color */ + replaceColor?: boolean; + /** The image source */ + src: string; + /** The image width */ + width?: number; +} +``` + +--- + +### Sub-step 3.6 — Clean `shapes/image/src/ImageDrawer.ts` + +**Remove import (line 12):** +```typescript +import { drawGif } from "./GifUtils/Utils.js"; +``` + +**In `draw()` — remove GIF branch (lines 48-49):** + +Before: +```typescript + if (image.gif && image.gifData) { + drawGif(data, this.#container.canvas.render.settings); + } else if (element) { +``` + +After: +```typescript + if (element) { +``` + +**In `particleInit()` — remove gif fields from `imageRes` (lines 158-160):** + +Before: +```typescript + gif: image.gif, + gifData: image.gifData, + gifLoopCount: image.gifLoopCount, +``` + +After: (these 3 lines removed entirely) + +**In `#loadImageShape()` — remove `gif` from preload data (line 196):** + +Before: +```typescript + await this.#engine.loadImage(container, { + gif: imageShape.gif, + name: imageShape.name, +``` + +After: +```typescript + await this.#engine.loadImage(container, { + name: imageShape.name, +``` + +--- + +### Sub-step 3.7 — Clean `shapes/image/src/index.ts` + +**Remove import (line 6):** +```typescript +import { loadGifImage } from "./GifUtils/Utils.js"; +``` + +**Remove `gif` field from `IImage` literal (line 48):** +```typescript +gif: data.gif, // REMOVE +``` + +**Remove `data.gif` check and `loadGifImage` branch (lines 64-65):** + +Before: +```typescript + if (data.gif) { + imageFunc = (img): Promise => loadGifImage(img, { colorSpace: "srgb" }); + } else if (data.replaceColor) { +``` + +After: +```typescript + if (data.replaceColor) { +``` + +--- + +### Sub-step 3.8 — Clean `shapes/image/src/index.lazy.ts` + +**Remove `gif` field from `IImage` literal (line 45):** +```typescript +gif: data.gif, // REMOVE +``` + +**Remove `data.gif` check and dynamic `loadGifImage` import (lines 61-64):** + +Before: +```typescript + if (data.gif) { + const { loadGifImage } = await import("./GifUtils/Utils.js"); + + imageFunc = (img): Promise => loadGifImage(img, { colorSpace: "srgb" }); + } else if (data.replaceColor) { +``` + +After: +```typescript + if (data.replaceColor) { +``` + +--- + +### Sub-step 3.9 — Verify no GIF references remain in shape-image + +```bash +grep -rn "gif\|Gif\|GIF\|GifUtils" shapes/image/src/ --include="*.ts" | grep -v node_modules +``` + +Expected output: no matches. + +--- + +## Step 4 — Add `@tsparticles/shape-gif` to `@tsparticles/all` bundle + +### Sub-step 4.1 — Edit `bundles/all/package.json` + +Add to `dependencies` (in alphabetical order, after `"@tsparticles/shape-cog"`): +```json +"@tsparticles/shape-gif": "workspace:*", +``` + +--- + +### Sub-step 4.2 — Edit `bundles/all/package.dist.json` + +If `bundles/all/package.dist.json` exists, add the same dependency. If not, skip this substep. + +--- + +### Sub-step 4.3 — Edit `bundles/all/src/index.ts` + +**Add import** (in alphabetical order, after existing imports): +```typescript +import { loadGifShape } from "@tsparticles/shape-gif"; +``` + +**Add load call** inside the `Promise.all` shapes block (after `loadSquircleShape(e),`): +```typescript +loadGifShape(e), +``` + +The shapes block becomes: +```typescript + loadArrowShape(e), + loadCardsShape(e), + loadCogShape(e), + loadGifShape(e), + loadHeartShape(e), + loadInfinityShape(e), + loadMatrixShape(e), + loadPathShape(e), + loadRibbonShape(e), + loadRoundedPolygonShape(e), + loadRoundedRectShape(e), + loadSpiralShape(e), + loadSquircleShape(e), +``` + +--- + +### Sub-step 4.4 — Verify `bundles/all/src/bundle.ts` and `bundles/all/src/browser.ts` + +Both files re-export from `./index.js` which will now export `loadGifShape`. No changes needed. + +--- + +## Step 5 — Verify build + +### Sub-step 5.1 — Link workspace packages + +```bash +pnpm install +``` + +### Sub-step 5.2 — Build all packages + +```bash +pnpm run build:ci +``` + +### Sub-step 5.3 — Check for errors + +Look for: +- TypeScript errors in `shapes/gif/src/` +- TypeScript errors in `shapes/image/src/` (no more GIF references) +- TypeScript errors in `bundles/all/src/` (missing import) +- Rollup/webpack bundle errors + +### Sub-step 5.4 — If errors, build individually for debugging + +```bash +pnpm nx run shape-gif:build +pnpm nx run shape-image:build +pnpm nx run all:build +``` + +### Sub-step 5.5 — Verify bundle output + +Check that `shapes/gif/dist/` exists with this structure: +``` +shapes/gif/dist/ + browser/index.js + browser/index.lazy.js + browser/browser.js + cjs/index.js + cjs/index.lazy.js + esm/index.js + esm/index.lazy.js + types/index.d.ts + types/index.lazy.d.ts + types/GifDrawer.d.ts + types/IGifShape.d.ts + types/types.d.ts + types/GifUtils/... +``` + +--- + +## Step 6 — Update demo config + +### Sub-step 6.1 — Edit `utils/configs/src/g/gifs.ts` + +Change from old image+gif config to new gif shape config: + +**Before:** +```typescript +shape: { + options: { + image: { + gif: true, + height: 200, + src: "https://particles.js.org/images/mario.gif", + width: 200, + }, + }, + type: "image", +}, +``` + +**After:** +```typescript +shape: { + options: { + gif: { + height: 200, + src: "https://particles.js.org/images/mario.gif", + width: 200, + }, + }, + type: "gif", +}, +``` + +--- + +## Execution Order + +``` +Step 1 (create shapes/gif/ scaffold) + ├── 1.1 mkdir + ├── 1.2 package.json + ├── 1.3 package.dist.json + ├── 1.4 .browserslistrc + ├── 1.5 eslint.config.js + ├── 1.6 rollup.config.js + ├── 1.7 tsconfig files (5 files) + ├── 1.8 typedoc.json + ├── 1.9 IGifShape.ts + ├── 1.10 types.ts + ├── 1.11 GifDrawer.ts + ├── 1.12 index.ts + ├── 1.13 index.lazy.ts + └── 1.14 browser.ts + +Step 2 (copy GifUtils) + ├── 2.1 cp -r all files + ├── 2.2 fix import in Utils.ts + ├── 2.3 remove loadGifImage + ├── 2.4 replace drawGif + └── 2.5 verify imports + +Step 3 (remove GIF from shape-image) + ├── 3.1 rm -rf GifUtils/ + ├── 3.2 Utils.ts + ├── 3.3 IImageShape.ts + ├── 3.4 Preload.ts + ├── 3.5 IPreload.ts + ├── 3.6 ImageDrawer.ts + ├── 3.7 index.ts + ├── 3.8 index.lazy.ts + └── 3.9 grep verify + +Step 4 (add to @tsparticles/all) + ├── 4.1 package.json + ├── 4.2 package.dist.json + ├── 4.3 src/index.ts + └── 4.4 verify bundle/browser exports + +Step 5 (build & verify) + ├── 5.1 pnpm install + ├── 5.2 pnpm run build:ci + ├── 5.3 check errors + ├── 5.4 debug build per-package + └── 5.5 verify dist/ output + +Step 6 (demo config) + └── 6.1 utils/configs/src/g/gifs.ts +``` + +**Dependencies between steps:** +- Steps 1-3 can run in parallel (different package dirs) +- Step 4 requires Step 1 (package must exist for pnpm workspace resolution to work) +- Step 5 requires Steps 1-4 +- Step 6 can run anytime after Step 3 + +--- + +## Cheat sheet: Exact changes by file + +### New files (17) + +| # | File | Sub-step | +|---|---|---| +| 1 | `shapes/gif/package.json` | 1.2 | +| 2 | `shapes/gif/package.dist.json` | 1.3 | +| 3 | `shapes/gif/.browserslistrc` | 1.4 | +| 4 | `shapes/gif/eslint.config.js` | 1.5 | +| 5 | `shapes/gif/rollup.config.js` | 1.6 | +| 6 | `shapes/gif/tsconfig.base.json` | 1.7 | +| 7 | `shapes/gif/tsconfig.json` | 1.7 | +| 8 | `shapes/gif/tsconfig.browser.json` | 1.7 | +| 9 | `shapes/gif/tsconfig.module.json` | 1.7 | +| 10 | `shapes/gif/tsconfig.types.json` | 1.7 | +| 11 | `shapes/gif/typedoc.json` | 1.8 | +| 12 | `shapes/gif/src/IGifShape.ts` | 1.9 | +| 13 | `shapes/gif/src/types.ts` | 1.10 | +| 14 | `shapes/gif/src/GifDrawer.ts` | 1.11 | +| 15 | `shapes/gif/src/index.ts` | 1.12 | +| 16 | `shapes/gif/src/index.lazy.ts` | 1.13 | +| 17 | `shapes/gif/src/browser.ts` | 1.14 | + +### Copied files (10) + +| File | From | To | Edit needed | +|---|---|---|---| +| `GifUtils/ByteStream.ts` | `shapes/image/src/GifUtils/` | `shapes/gif/src/GifUtils/` | No | +| `GifUtils/Constants.ts` | same | same | No | +| `GifUtils/Utils.ts` | same | same | **Yes** — 2.2, 2.3, 2.4 | +| `GifUtils/Enums/DisposalMethod.ts` | same | same | No | +| `GifUtils/Types/GIF.ts` | same | same | No | +| `GifUtils/Types/Frame.ts` | same | same | No | +| `GifUtils/Types/ApplicationExtension.ts` | same | same | No | +| `GifUtils/Types/GIFDataHeaders.ts` | same | same | No | +| `GifUtils/Types/GIFProgressCallbackFunction.ts` | same | same | No | +| `GifUtils/Types/PlainTextData.ts` | same | same | No | + +### Modified files (10) + +| File | Change | Sub-step | +|---|---|---| +| `shapes/image/src/Utils.ts` | Remove GIF import + 7 gif fields + 1 `gif:` in literal | 3.2 | +| `shapes/image/src/IImageShape.ts` | Remove `gif: boolean` | 3.3 | +| `shapes/image/src/Options/Classes/Preload.ts` | Remove `gif` property + `loadProperty("gif")` | 3.4 | +| `shapes/image/src/Options/Interfaces/IPreload.ts` | Remove `gif: boolean` | 3.5 | +| `shapes/image/src/ImageDrawer.ts` | Remove drawGif import, if-gif branch, gif fields in particleInit, gif in loadImageShape | 3.6 | +| `shapes/image/src/index.ts` | Remove loadGifImage import, gif literal field, if(data.gif) branch | 3.7 | +| `shapes/image/src/index.lazy.ts` | Same as index.ts | 3.8 | +| `bundles/all/package.json` | Add `@tsparticles/shape-gif` dependency | 4.1 | +| `bundles/all/src/index.ts` | Add import + loadGifShape call | 4.3 | +| `utils/configs/src/g/gifs.ts` | Change `type: "image"` + gif opts → `type: "gif"` | 6.1 | + +### Deleted (1) + +| Path | Sub-step | +|---|---| +| `shapes/image/src/GifUtils/` | 3.1 | + +### No changes needed + +| Reason | | +|---|---| +| `pnpm-workspace.yaml` | Already includes `shapes/*` | +| `bundles/all/src/bundle.ts` | Re-exports from `./index.js` | +| `bundles/all/src/browser.ts` | Re-exports from `./index.js` | +| `shapes/image/src/types.ts` | No GIF-specific types | +| `shapes/image/src/ImagePreloader.ts` | No GIF-specific code | +| `shapes/image/src/ImagePreloaderInstance.ts` | No GIF-specific code | +| `shapes/image/src/browser.ts` | No GIF-specific code | + +--- + +## Testing + +Run the demo server after implementation: + +```bash +cd demo/vanilla && pnpm start +``` + +Navigate to the "Gifs" demo preset. Particles should render as animated GIFs using the new `type: "gif"` shape. diff --git a/.planning/handovers/WRAPPER_REACTIVITY_PLAN.md b/.planning/handovers/WRAPPER_REACTIVITY_PLAN.md deleted file mode 100644 index 963e0c184b9..00000000000 --- a/.planning/handovers/WRAPPER_REACTIVITY_PLAN.md +++ /dev/null @@ -1,2613 +0,0 @@ -# Wrapper Reactivity & Vue3 Docs Plan — Enriched - -> **Status**: Planning document (ready for agent execution) -> **Total wrappers in scope**: all wrappers in repository except `wordpress` -> **Previous version**: ~560 lines — this enriched version adds per-wrapper code analysis, type contracts, before/after code, and per-substep agent instructions. - ---- - -## Execution Summary (Supervisor View) - -Use this table as the single high-level progress board. Update it whenever a step changes state. - -| Step | Area | Status | Notes | -|------|---------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------| -| S1 | Vue 3 wrapper | Done | Wrapper + demo + template check + README + verification | -| S2 | Vue 2 wrapper | Done | Wrapper + demo + template check + README + verification | -| S3 | Angular wrapper | Done | Wrapper + demo + template check + README + verification | -| S4 | Solid wrapper | Done | Wrapper + demo + template check + README + verification | -| S5 | Qwik wrapper | Done | Wrapper + demo + template check + README + verification | -| S6 | Astro wrapper | Done | Wrapper + demo + template check + README + verification | -| S7 | Inferno wrapper | Done | Wrapper + demo/template check + README + verification | -| S8 | Lit wrapper | Done | Wrapper + demo/template check + README + verification | -| S9 | Riot wrapper | Done | Wrapper + demo/template check + README + verification | -| S10 | WebComponents wrapper | Done | Wrapper + demo/template check + README + verification | -| S11 | React docs | Done | README alignment + demo/template check | -| S12 | Preact wrapper | Done | Wrapper + demo/template check + README + verification | -| S13 | Svelte wrapper | Done | `theme` prop, selective `$:` reactivity, demo alignment, README | -| S14 | Stencil wrapper | Done | `theme` prop, `particlesLoaded` event, `@Watch("theme")`, demo with theme toggle + plugin-themes, README aligned | -| S15 | Ember wrapper | Done | `theme` prop, lifecycle fix, README alignment, build pass | -| S16 | jQuery wrapper | Done | `setTheme` method, `WeakMap` tracking, safe `loadTheme?.()`, README aligned, build pass | -| S17 | Angular-fireworks wrapper | Done | Implemented `OnChanges` + `#destroyed` guard + `#startFireworks`; N/A: no standalone demo or templates dir; README updated; build pass | -| S18 | EN docs | Done | Removed stale `:init`, `@particles-init`, `particlesInit`; removed Component-Level Init section; fixed `useParticles` → `useParticlesProvider`; added `theme` prop to API table with plugin caveat; added theme prop docs in Theme Switching section | -| S19 | 9 translations | Done | Mirrored all S18 changes across zh, ja, hi, ru, pt, fr, de, es, it — each file reduced from 622→~576 lines; zero stale patterns remaining | -| S20 | Validation | Done | Build + smoke + stale-pattern checks ✅ — all 13 wrappers build pass; stale patterns: zero; README gaps documented | -| S21 | Final handoff | Done | All artifacts aligned. README gaps fixed for preact, stencil, ember, jquery, angular-fireworks. Builds passing. Zero stale patterns. | -| S22 | EN wrapper guide audit | Done | Audit complete. All 16 guide files checked. Gap matrix produced: all 16 have missing `theme`/reactivity/cleanup docs. Nuxt has stale `@particles-init`/`@particles-destroy`. Solid/Svelte have misleading reactivity descriptions. Audit-only — no files edited. | -| S23 | Translation alignment | Done | All 16 EN guides fixed (theme prop, reactivity, cleanup, Container types, stale events, misleading descriptions). Mirrored to 9 translation directories (144 files). | - -Status legend: `Pending`, `In progress`, `Blocked`, `Partial`, `Done`, `N/A`. - ---- - -## ⚠️ CRITICAL: FILE TOUCH RULES FOR IMPLEMENTING AGENTS - -**You must NEVER modify files outside these allowed paths.** This is enforced by design — the engine and accessory libraries are stable and must not be changed by wrapper work. - -### ✅ ALLOWED (can create/edit) -| Path | Notes | -|-----------------------------------------------------------------------|------------------------------------------------------| -| `wrappers//**` | Wrapper package code, README, config | -| `demo//**` | Corresponding demo app | -| `websites/website/docs/guides/.md` | English docs (one per wrapper) | -| `websites/website/docs/{zh,ja,hi,ru,pt,fr,de,es,it}/guides/.md` | Translated docs (mirror EN) | -| `.planning/handovers/WRAPPER_REACTIVITY_PLAN.md` | This plan — update step statuses & execution records | -| `.planning/STATE.md` | Project state — update session outcome & next steps | - -### 🚫 FORBIDDEN (never touch) -| Path | Reason | -|-------------------|-----------------------------------------| -| `bundles/**` | Bundle assembly — stable, out of scope | -| `cli/**` | CLI — stable, out of scope | -| `effects/**` | Effects — stable, out of scope | -| `engine/**` | Core runtime — stable, out of scope | -| `integrations/**` | Integrations — stable, out of scope | -| `interactions/**` | Interactions — stable, out of scope | -| `palettes/**` | Palettes — stable, out of scope | -| `paths/**` | Path generators — stable, out of scope | -| `plugins/**` | Plugin packages — stable, out of scope | -| `presets/**` | Preset configs — stable, out of scope | -| `shapes/**` | Shape presets — stable, out of scope | -| `updaters/**` | Updaters — stable, out of scope | -| `utils/**` | Utility packages — stable, out of scope | - -**Violation policy**: If an agent touches a forbidden path, revert immediately and document the mistake. No exceptions. - ---- - -## Scope - -Implement reactive updates for wrapper components when `options`, `url`, or `theme` props change, and align Vue 3 guide docs with actual wrapper APIs. - -### In scope -- **Core wrappers**: `vue3`, `vue2`, `angular`, `solid`, `qwik`, `astro` -- **Website docs**: update all affected wrapper guide pages so behavior/docs match implemented v4 wrapper behavior (not only Vue 3). -- **Mandatory website set**: `websites/website/docs/guides/vue3.md` + all 9 translations under `websites/website/docs/{zh,ja,hi,ru,pt,fr,de,es,it}/guides/vue3.md`. -- **Wrapper READMEs**: update README files for all touched wrappers (`vue3`, `vue2`, `angular`, `solid`, `qwik`, `astro`) when behavior text is stale. - -### Extended alignment scope (mandatory audit + conditional implementation) -- Additional wrappers to audit and align when drift is found: `react`, `preact`, `svelte`, `stencil`, `lit`, `inferno`, `ember`, `riot`, `webcomponents`, `jquery`, `nextjs`, `nuxt2`, `nuxt3`, `nuxt4`, `angular-confetti`, `angular-fireworks`. -- These wrappers are not optional: each must be audited in execution, and if drift exists, implementation/demo/template/README alignment is required in-step. -- `wordpress` is explicitly excluded from this plan and is the only wrapper out of scope. - -### Out of scope -- New wrapper APIs beyond `theme` support already implied by runtime capability -- Engine-level typing changes for `Container` in this phase -- Unrelated guide files (React/Svelte/etc.) - ---- - -## Source of Truth (Critical) - -This project is on **v4** with breaking changes vs v3. - -Mandatory rules for implementing agents: -- **Do not rely on agent memory** for wrapper APIs or docs snippets. -- **Read the repository code first** (current branch state) before any change. -- Use tag **`v3.9.1` only as a comparison baseline** to understand what changed from v3 to v4. -- If memory and repository disagree, **repository wins**. - -**Critical finding from v3.9.1 diff**: The `wrappers/` directory does NOT exist in v3.9.1. All wrapper files are new in v4. There is no direct v3→v4 diff for wrapper files. The engine `Container.ts` was significantly refactored (`Canvas → CanvasManager`, `Particles → ParticlesManager`, new `ContainerParams`). Engine changes that affect wrappers: -- `tsParticles.load()` returns `Container | undefined` (v4) -- `Container` no longer has `loadTheme` (moved to optional plugin) -- Engine init now requires explicit `tsParticles.init()` call - ---- - -## Problem Statement - -Current wrapper behavior is inconsistent: -- Some wrappers only load particles on mount and ignore later prop changes -- `theme` support depends on optional plugin runtime injection (`@tsparticles/plugin-themes`) -- Vue 3 guide documents nonexistent component-level `:init` / `@particles-init` usage - -This creates broken expectations and stale examples. - ---- - -## Critical Constraints (Do Not Violate) - -1. **Theme plugin is optional (NON-NEGOTIABLE, MUST BE DOCUMENTED EVERYWHERE)** - - `container.loadTheme` exists **only** if `@tsparticles/plugin-themes` is loaded - - Without that plugin, `theme` must be a safe no-op (no crash, no throw) - - Must call safely with optional chaining + type narrowing/cast - - This caveat must be explicit in all updated docs/readmes touched by this work - -2. **`tsParticles.load()` can return `Container | undefined`** - - Must guard before emitting callbacks expecting `Container` - -3. **Vue 3 docs translation rule is mandatory** - - Any change in `websites/website/docs/guides/vue3.md` must be mirrored in all 9 translation files in the same PR - -4. **No fake APIs in docs** - - Remove `:init`, `@particles-init`, and any prose implying component-level `init` prop for Vue 3 wrapper - -5. **Wrapper behavior must be aligned (NON-NEGOTIABLE)** - - `id` change => destroy + reload particles - - `options` change => destroy + reload particles - - `url` change => destroy + reload particles - - This rule is mandatory for all 6 wrappers in scope; no wrapper-specific exceptions - -6. **Engine init must run once, components must wait** - - Engine/plugin init is one-time bootstrap work - - Particle components must wait until init is completed before calling `tsParticles.load` - -7. **Loaded callback timing is fixed** - - `particlesLoaded`/`loaded`/`onLoaded`/`onParticlesLoaded` must fire only **after** `tsParticles.load` resolves - - Callback must receive the resolved container (guarding `undefined` where needed) - -8. **Container cleanup on component destruction is mandatory** - - Component unmount/destroy must call container `destroy()` - - No orphan animations must remain active after component teardown - ---- - -## `loadTheme` Typing Contract (from Engine + Plugin) - -From `plugins/themes/src/types.ts`: -```ts -export type ThemesContainer = Container & { - actualOptions: ThemesOptions; - currentTheme?: string; - loadTheme?: (name?: string) => Promise; - // ... -}; -``` - -The `Container` class in `engine/src/Core/Container.ts` does NOT declare `loadTheme`. It is injected at runtime by `ThemesPluginInstance.init()` (line 55: `container.loadTheme = loadTheme`). - -**Safe invocation pattern (use in ALL wrappers):** -```ts -(container as unknown as { loadTheme?: (name?: string) => Promise }).loadTheme?.(theme); -``` - -Note: The `as unknown as` double-cast is needed because `Container` and `ThemesContainer` are in different packages. Do NOT use global declaration merging — it causes cross-package type pollution. - ---- - -## Status Overview - -| Wrapper | Current Behavior | Reload `id/options/url` | `theme` prop | Loaded Callback | Destroy on Teardown | Priority | -|-------------|----------------------------------------|-----------------------------------------------|----------------------------|-----------------------------------------------------------------|-----------------------------------|----------| -| **Vue 3** | Mount-only load via provider gate | **Missing** (`theme` prop exists but ignored) | Prop exists, never applied | Emits `particlesLoaded` (ok) | Yes (`onUnmounted`) | **HIGH** | -| **Vue 2** | Mount-only load via `mounted` | **Missing** | **Missing** prop entirely | `particlesLoaded` prop (ok) | Yes (`beforeDestroy`) | **HIGH** | -| **Angular** | `ngAfterViewInit` only, no `OnChanges` | **Missing** | **Missing** `@Input` prop | `EventEmitter` (ok, but `Container` not `Container\|undefined`) | Yes (`ngOnDestroy`) | **HIGH** | -| **Solid** | `onMount` + `createResource`, one-shot | **Missing** (effect not tracked on props) | **Missing** prop | `particlesLoaded` fires (ok) | `onCleanup` | **HIGH** | -| **Qwik** | `useVisibleTask$`, one-shot | **Missing** (no tracking) | **Missing** prop | `loaded` QRL fires (ok) | `cleanup` | **HIGH** | -| **Astro** | Constructor load, one-shot | **Missing** (no observer) | **Missing** attribute | Global function call (ok) | No `disconnectedCallback` cleanup | **HIGH** | - ---- - -## Current State: Per-Wrapper Code Analysis - -### Vue 3 (`wrappers/vue3/src/components/vue-particles.vue`) — 72 lines - -**Current code**: -```vue - - - -``` - -**Current Props interface** (explicit via `defineProps`): -```ts -{ - id: string; // required - options?: ISourceOptions; - url?: string; - theme?: string; // EXISTS but NEVER USED -} -``` - -**Current Emits**: -```ts -{ - (e: "particlesLoaded", container?: Container): void; -} -``` - -**Provider**: `wrappers/vue3/src/components/particles-provider.ts` (92 lines) -- Uses `globalThis.__tsparticles_vue3_init_state__` singleton -- `initParticlesProvider()` calls `tsParticles.init()` after optional plugin registration -- `useParticlesProvider()` returns `{ loaded: boolean }` via Vue `inject` - -**Package entry**: `wrappers/vue3/src/components/index.ts` -- Exports: `VueParticles` (default), `IParticlesProviderContext`, `IParticlesProviderOptions`, `ParticlesPluginRegistrar` -- Does NOT export `useParticlesProvider` - -**Gaps**: -1. No watcher on `props.id` — changing id does nothing -2. No watcher on `props.options` — changing options does nothing -3. No watcher on `props.url` — changing url does nothing -4. `theme` prop exists but is never applied via `loadTheme` -5. `IParticlesProps` is `ISourceOptions` (wrong name — it shadows the props type) - ---- - -### Vue 2 (`wrappers/vue2/src/Particles/vue-particles.vue`) — 62 lines - -**Current code**: -```vue - - - -``` - -**Current props**: `id` (required), `options?`, `url?`, `particlesLoaded?` -**Missing `theme` prop**: not declared -**Gaps**: -1. No `theme` prop -2. No watchers on `id`, `options`, `url` -3. `export type IParticlesProps = ISourceOptions` — this creates `TS2528` (duplicate default export) in SFC compiler context because the component itself is the default export. Must be removed or inlined. -4. `particlesLoaded` callback type is `(container: Container) => void` but `tsParticles.load()` returns `Container | undefined` — type mismatch - -**Event bus** (`wrappers/vue2/src/Particles/event-bus.ts`): -- `ensureParticlesInitialization(init?)` — runs optional init callback, sets `initialized = true` -- Does NOT call `tsParticles.init()` — this is a v3→v4 gap! In v4, `tsParticles.init()` must be called after plugin registration - ---- - -### Angular (`wrappers/angular/projects/ng-particles/src/lib/ng-particles.component.ts`) — 55 lines - -**Current code**: -```ts -import { AfterViewInit, Component, EventEmitter, Inject, Input, OnDestroy, Output, PLATFORM_ID } from "@angular/core"; -import { isPlatformServer } from "@angular/common"; -import { tsParticles } from "@tsparticles/engine"; -import type { Container } from "@tsparticles/engine"; -import { IParticlesProps } from "./ng-particles.module"; -import { NgParticlesService } from "./ng-particles.service"; - -@Component({ - selector: "ngx-particles", - standalone: false, - template: '
', -}) -export class NgxParticlesComponent implements AfterViewInit, OnDestroy { - @Input() options?: IParticlesProps; - @Input() url?: string; - @Input() id = "tsparticles"; - @Output() particlesLoaded: EventEmitter = new EventEmitter(); - - #container?: Container; - #loadingPromise?: Promise; - readonly #particlesService: NgParticlesService; - - constructor( - @Inject(PLATFORM_ID) protected platformId: string, - particlesService: NgParticlesService, - ) { - this.#particlesService = particlesService; - } - - public ngAfterViewInit(): void { - if (isPlatformServer(this.platformId)) { return; } - this.#loadingPromise = this.#loadParticles(); - } - - public ngOnDestroy(): void { - this.#container?.destroy(); - this.#loadingPromise = undefined; - } - - async #loadParticles(): Promise { - await this.#particlesService.waitForInitialization(); - this.#particlesService.assertInitialized(); - this.#container?.destroy(); - const container = await tsParticles.load({ id: this.id, options: this.options, url: this.url }); - this.#container = container; - this.particlesLoaded.emit(container); // BUG: emits Container | undefined as Container - } -} -``` - -**Current `@Input()`**: `options?`, `url?`, `id` (default `"tsparticles"`) -**Missing `@Input()`**: `theme?` -**Missing interface**: `OnChanges` — no `ngOnChanges` implemented -**Gaps**: -1. No `ngOnChanges` — prop changes silently ignored -2. No `theme` input -3. `particlesLoaded` emitter is typed `EventEmitter` but should be `EventEmitter` (or emit guard) -4. Service (`ng-particles.service.ts`) does NOT call `tsParticles.init()` — v4 gap - -**Service** (`ng-particles.service.ts`): -```ts -public async init(particlesInit?: ParticlesPluginRegistrar): Promise { - // calls particlesInit(tsParticles) - // sets initialized = true - // Does NOT call tsParticles.init() — MISSING in v4 -} -``` - ---- - -### Solid (`wrappers/solid/src/Particles.tsx`) — 53 lines - -**Current code**: -```tsx -import { tsParticles } from "@tsparticles/engine"; -import { createEffect, createResource, JSX, mergeProps, on, onCleanup, onMount } from "solid-js"; -import type { IParticlesProps } from "./IParticlesProps"; -import { isParticlesEngineInitialized, waitForParticlesEngineInitialization } from "./initParticlesEngine"; - -const Particles = (props: IParticlesProps): JSX.Element => { - const config = mergeProps({ id: "tsparticles" }, props); - - onMount(() => { - const [container] = createResource( - () => ({ - id: config.id, - options: config.params ?? config.options ?? {}, - url: config.url, - }), - async data => { - await waitForParticlesEngineInitialization(); - if (!isParticlesEngineInitialized()) { - throw new Error("..."); - } - return tsParticles.load(data); - }, - ); - - createEffect( - on(container, container => { - if (!container) return; - config.particlesLoaded?.(container); - onCleanup(() => container.destroy()); - }), - ); - }); - - return ( -
- -
- ); -}; -``` - -**Current props** (`IParticlesProps.ts`): -```ts -export interface IParticlesProps { - id?: string; - width?: string; - height?: string; - options?: ISourceOptions; - url?: string; - params?: ISourceOptions; - style?: JSX.CSSProperties; - class?: string; - canvasClass?: string; - container?: { current: Container }; - particlesLoaded?: (container: Container) => Promise; -} -``` - -**Gaps**: -1. `onMount` is one-shot — no reactivity to prop changes -2. `createResource` is not keyed on `props.id`, `props.options`, `props.url` — no reload -3. No `theme` prop in `IParticlesProps` -4. `particlesLoaded` type is `(container: Container) => Promise` should be `(container?: Container) => Promise` -5. Cleanup via `onCleanup` inside `createEffect` only fires when container resource changes, not on component unmount proper. Need explicit `onCleanup` at top level. - ---- - -### Qwik (`wrappers/qwik/src/components/particles/particles.tsx`) — 63 lines - -**Current code**: -```tsx -import { NoSerialize, component$, noSerialize, useVisibleTask$ } from "@builder.io/qwik"; -import { Container, tsParticles } from "@tsparticles/engine"; -import type { IParticlesProps } from "./IParticlesProps"; -import { isParticlesEngineInitialized, waitForParticlesEngineInitialization } from "./initParticlesEngine"; - -const Particles = component$(props => { - const librarySig: { value: NoSerialize } = { value: undefined }; - const id = props.id ?? "tsparticles"; - const { class: className, canvasClassName, height, width, loaded } = props; - - useVisibleTask$(function Initializer({ cleanup }) { - void (async () => { - await waitForParticlesEngineInitialization(); - if (!isParticlesEngineInitialized()) { - throw new Error("..."); - } - const container = await tsParticles.load({ - url: props.url, id, options: props.options ?? props.params, - }); - if (props.container) { - props.container.value = noSerialize(container); - } - librarySig.value = noSerialize(container); - if (loaded) { - await loaded(container); - } - })(); - - cleanup(() => { - if (librarySig.value) { - librarySig.value.destroy(); - librarySig.value = undefined; - } - }); - }); - - // render... -}); -``` - -**Current props** (`IParticlesProps.ts`): -```ts -export interface IParticlesProps { - id?: string; - width?: number | string; - height?: number | string; - options?: ISourceOptions; - url?: string; - params?: ISourceOptions; - style?: CSSProperties; - class?: ClassList | Signal; - canvasClassName?: ClassList | Signal; - container?: Signal>; - loaded?: QRL<(container?: Container) => Promise>; -} -``` - -**Gaps**: -1. `useVisibleTask$` runs once when element becomes visible — does NOT re-run on prop changes -2. No `theme` prop in `IParticlesProps` -3. No tracking mechanism for `props.id`, `props.options`, `props.url` changes -4. In Qwik, signals must be tracked explicitly. Need `useVisibleTask$` with tracker function or `$track` to observe prop changes - ---- - -### Astro (`wrappers/astro/src/Particles.astro`) — 79 lines - -**Current code**: -```astro ---- -import type { Container, ISourceOptions } from "@tsparticles/engine"; - -export interface IParticlesProps { - id: string; - loaded?: string; - options?: ISourceOptions; - url?: string; -} - -const { id, loaded, options, url } = Astro.props as IParticlesProps; ---- - - - - - - -``` - -**Gaps**: -1. Constructor-only load — no `connectedCallback` or `attributeChangedCallback` -2. No `observedAttributes` — Astro particles are rendered via data attributes but changes are not observed -3. No `disconnectedCallback` — no cleanup when element is removed from DOM -4. No `theme` support (no `data-theme` attribute, no `loadTheme` call) -5. `dataset.options` is a JSON string — must re-parse on every attribute change -6. Async race: multiple constructor calls could overlap (the custom element constructor runs once, but attribute changes later have no handler) -7. The `globalStore` pattern for `loaded` callback is fragile — uses `window` global - ---- - -## Extended Wrappers: Current State (Inferno/Lit/Riot/WebComponents/React) - -### Inferno (`wrappers/inferno/src/Particles.tsx`) — ALREADY has reload! - -**Current state** (actually already has reactive reload via `componentDidUpdate`): -```tsx -componentDidUpdate(prevProps: IParticlesProps) { - if ( - prevProps.id !== this.props.id || - prevProps.url !== this.props.url || - prevProps.options !== this.props.options - ) { - void this.reloadContainer(); - } -} -``` - -**But**: No `theme` prop. `loaded` callback exists in props but is NOT called after load (only `particlesLoaded` — actually neither is called in current code!). `loadContainer()` does NOT fire any callback. - -**Gaps**: -1. No `loaded`/`particlesLoaded` callback invocation after `tsParticles.load` -2. No `theme` prop -3. No safe `loadTheme` apply - -**Props type** (`IParticlesProps.ts`): -```ts -export interface IParticlesProps { - id?: string; width?: string; height?: string; - options?: ISourceOptions; url?: string; params?: ISourceOptions; - className?: string; canvasClassName?: string; style?: Record; - container?: { current?: Container }; - loaded?: (container?: Container) => Promise | void; - particlesLoaded?: (container?: Container) => Promise | void; -} -``` - ---- - -### Lit (`wrappers/lit/src/lit-tsparticles.ts`) — ALREADY has reload! - -**Current state** (reload via `update()` lifecycle): -```ts -update(changedProperties: PropertyValues) { - super.update(changedProperties); - void this.#loadParticles(++this.#renderId); -} - -async #loadParticles(currentRenderId: number): Promise { - // ... destroy old, load new, check renderId for stale race -} -``` - -**Gaps**: -1. No `theme` property -2. No `loadTheme` invocation -3. No loaded notification event/callback (container assigned but no event fired) - -**Current properties**: `id`, `options`, `url` -**Missing**: `theme`, loaded notification - ---- - -### Riot (`wrappers/riot/src/riot-particles.riot`) — Mount-only - -**Current state**: -```riot -export default { - async onMounted(props) { - // ... one-shot load - } -} -``` - -**Gaps**: -1. No reactivity (Riot doesn't have built-in watchers; would need manual attribute observer) -2. No `theme` prop -3. `particlesLoaded` callback uses `tsParticles.dom().find(...)` for old container cleanup (v3 pattern) -4. Teardown: no explicit container destroy on unmount - -**Props**: `id`, `options`, `url`, `particlesLoaded` - ---- - -### WebComponents (`wrappers/webcomponents/src/Particles.ts`) — Partial reload - -**Current state** (has observers but incomplete): -```ts -static get observedAttributes(): string[] { - return ["url", "options"]; -} - -// Has setters: this.url, this.options — but they don't reload! -// Missing: "id" in observedAttributes -// Missing: theme attribute -``` - -**Gaps**: -1. `id` changes are NOT observed (missing from `observedAttributes`) -2. No `theme` attribute support -3. `attributeChangedCallback` stores values but doesn't trigger reload directly for all paths — setters DO trigger reload via `#loadParticles`, but attribute changes don't always propagate -4. Constructor dispatches `particlesInit` event with engine (but that event doesn't exist in v4) -5. `connectedCallback` calls `#loadParticles` — reload works for initial render - ---- - -### React (`wrappers/react/lib/Particles.tsx`) — Already reactive - -**Current state** (reload via `useEffect` dependencies): -```tsx -useEffect(() => { - if (!loaded) return; - tsParticles.load({ id: particleId, url, options }).then(c => { /* ... */ }); - return () => { containerRef.current?.destroy(); }; -}, [id, loaded, options, particlesLoaded, url]); -``` - -**Gaps**: -1. No `theme` prop or `loadTheme` support -2. `particlesLoaded` type in `IParticlesProps.ts` is `(container?: Container) => Promise | void` — ok, but docs/README props table is incomplete (missing `particlesLoaded` entry) - ---- - -## Canonical Wrapper Contract (Target Behavior) - -All wrappers must implement this behavior (framework syntax differs): - -### 1. Init contract -- Engine/plugin init is triggered once at app/bootstrap level. -- Wrapper components wait for init completion before loading particles. - -### 2. Reactivity contract -- `id` change => destroy + reload -- `options` change => destroy + reload -- `url` change => destroy + reload -- Reload path is deterministic and idempotent - -### 3. Theme contract -- `theme` prop/attribute exists in wrappers. -- `loadTheme` is optional runtime capability requiring `@tsparticles/plugin-themes`. -- Without plugin, `theme` updates are safe no-op. -- Invocation: `(container as unknown as { loadTheme?: (name?: string) => Promise }).loadTheme?.(theme)` - -### 4. Loaded callback contract -- Event/callback (`particlesLoaded` / `loaded` / framework equivalent) is emitted after successful `tsParticles.load` resolution. -- Callback receives `Container | undefined`. - -### 5. Destroy contract -- On component teardown, wrapper calls `container.destroy()`. -- No orphan animations remain. - ---- - -## Implementation Steps + Status - -### Completion Rule (non-negotiable) - -Each wrapper step is considered **Done** only when all related artifacts are aligned together: - -1. Wrapper implementation (reactivity + theme + lifecycle) -2. Wrapper demo(s) aligned with final API and behavior -3. Wrapper template(s) checked and aligned (or explicitly marked N/A if none exist) -4. Wrapper README aligned with the implemented behavior - -This plan therefore uses **one wrapper per step**. No multi-wrapper implementation steps. - -Website docs are intentionally batched at the end for efficiency, while wrapper-specific context stays inside each wrapper step (README + demo + template checks are in-step requirements). - -### Per-step substep template (apply to every wrapper step S1-S17) - -- `a` Wrapper code changes (`id/options/url` reload, theme safety, teardown, init wait) -- `b` Demo alignment for that wrapper family -- `c` Template alignment check (or explicit N/A note) -- `d` Wrapper README alignment -- `e` Verification (`pnpm --filter build` + smoke behavior checks) - -No wrapper step can be marked complete unless all five substeps are complete. - -### A1 audit template (special step) - -`A1` is an upfront analysis step and uses this output contract: - -- Wrapper audited -- Drift found: `yes/no` -- Evidence (file refs + short rationale) -- Action: `queue X-step` or `no-change` -- If `queue X-step`: list exact files likely to change - -`A1` must complete before any `S12-S17` implementation step starts. - -### A1 Audit Decision Matrix (completed) - -| Wrapper | Drift | Evidence | Decision | -|---------|-------|----------|----------| -| preact | yes | `wrappers/preact/src/IParticlesProps.ts` callback typed `(container: Container)`; no `theme` prop | Activate `S12` | -| svelte | yes | `wrappers/svelte/src/lib/Particles.svelte` reloads on every `afterUpdate`; no `theme` handling | Done `S13` | -| stencil | yes | `wrappers/stencil/src/components/stencil-particles/stencil-particles.tsx` has no `theme` prop and no loaded callback/event | Activate `S14` | -| ember | yes | `wrappers/ember/addon/modifiers/particles.ts` no `theme`; repeated `modify()` may stack destructors without explicit pre-destroy | Activate `S15` | -| jquery | yes | `wrappers/jquery/src/particles.ts` has `load/ajax` only, no theme pathway nor theme docs alignment | Activate `S16` | -| nextjs | no | `wrappers/nextjs/lib/index.tsx` is thin client-only re-export over `@tsparticles/react` | Close `X6` as no-change | -| nuxt2 | no | `wrappers/nuxt2/lib/index.ts` only installs Vue3 plugin via module; no direct wrapper lifecycle | Close `X7` as no-change | -| nuxt3 | no | `wrappers/nuxt3/lib/index.ts` only installs Vue3 plugin via module; no direct wrapper lifecycle | Close `X8` as no-change | -| nuxt4 | no | `wrappers/nuxt4/lib/index.ts` only installs Vue3 plugin via module; no direct wrapper lifecycle | Close `X9` as no-change | -| angular-confetti | no | `wrappers/angular-confetti/projects/ng-confetti/src/lib/ng-confetti.component.ts` is event-like fire wrapper with explicit trigger semantics | Close `X10` as no-change | -| angular-fireworks | yes | `wrappers/angular-fireworks/projects/ng-fireworks/src/lib/ng-fireworks.component.ts` lacks `OnChanges`; input changes not reapplied | Activate `S17` | - -Implementation gate from A1: -- Execute: `S12`, `S13`, `S14`, `S15`, `S16`, `S17`. -- No-change (closed with evidence): `X6` (Next.js), `X7` (Nuxt 2), `X8` (Nuxt 3), `X9` (Nuxt 4), `X10` (Angular-confetti). - -## No-Change Summary (A1 Audit Closure) - -These wrappers were audited and require no implementation work. Evidence is documented in the A1 decision matrix above. - -| Wrapper | Evidence | -|---------|----------| -| Next.js (X6) | `wrappers/nextjs/lib/index.tsx` — thin client-only re-export of `@tsparticles/react`, no direct lifecycle | -| Nuxt 2 (X7) | `wrappers/nuxt2/lib/index.ts` — module only installs Vue3 plugin, no direct wrapper lifecycle | -| Nuxt 3 (X8) | Same pattern as Nuxt 2 | -| Nuxt 4 (X9) | Same pattern as Nuxt 2 | -| Angular-confetti (X10) | `wrappers/angular-confetti/projects/ng-confetti/src/lib/ng-confetti.component.ts` — event-like fire wrapper with explicit trigger semantics, not a generic particles component | - -These steps are **closed** and do not appear in any execution table. - ---- - -## Agent Execution Protocol (mandatory) - -This plan is designed for distributed execution by multiple agents. Follow this protocol to avoid drift and overlap. - -### 1) Ownership model -- One agent owns exactly one step at a time. -- No agent may edit files outside its owned step, except `S20` and `S21` owner agents. -- If cross-step edits are discovered as necessary, agent must report them as "Follow-up required" instead of editing outside ownership. - -### 2) Step completion contract -An agent can mark a step complete only if all are true: -- Wrapper implementation substep is complete. -- Demo alignment substep is complete (or explicit `N/A` with reason). -- Template alignment substep is complete (or explicit `N/A` with reason). -- README alignment substep is complete. -- Verification commands executed and passed (or failure documented with logs). - -### 3) Standard agent handoff output -Every agent must return results in this structure: - -```md -Step: Sx -Status: done | partial | blocked - -Files changed: -- path/to/file1 -- path/to/file2 - -Substep status: -- a Wrapper implementation: done/partial/blocked -- b Demo alignment: done/partial/blocked (or N/A: reason) -- c Template alignment: done/partial/blocked (or N/A: reason) -- d README alignment: done/partial/blocked -- e Verification: done/partial/blocked - -Verification executed: -- -- - -Behavior deltas: -- ... - -Known risks / follow-ups: -- ... -``` - -### 4) Parallelization waves (recommended) -- **Wave 1**: all wrapper implementation steps in parallel — `S1-S17`. -- **Wave 2**: `S18` then `S19` (strictly sequential — EN docs first, then 9 translations). -- **Wave 3**: `S20` validation. -- **Wave 4**: `S21` final handoff. - -### 5) Conflict avoidance -- `S18`/`S19` are the only steps allowed to edit `websites/website/docs/**/guides/vue3.md`. -- Each wrapper step edits only its wrapper package + mapped demo + mapped README. -- `S20` does not change implementation unless explicitly requested; it validates and reports. - -### 6) Failure policy -- If verification fails, step status is `partial` (not `done`). -- If blocked by external dependency, status is `blocked` with exact blocker and next action. -- Never silently skip demo/template/README substeps. - ---- - -## Per-Wrapper Implementation Details - ---- - -### S1 — Vue 3 wrapper ✅ COMPLETED - -**Files modified**: -| File | Change | -|------|--------| -| `wrappers/vue3/src/components/vue-particles.vue` | Added watchers for `id`/`options`/`url` → destroy+reload; added `theme` watcher + initial apply; removed stale `IParticlesProps` export | -| `demo/vue3/src/App.vue` | Rewrote with reactive config pills switching `:options` binding, `@particles-loaded` event, `:theme` prop binding | -| `wrappers/vue3/README.md` | Fully documented reactive behavior, theme plugin caveat, loaded callback, teardown | - -**Files unchanged (plan candidate, not needed)**: -- `wrappers/vue3/src/components/index.ts` — no export changes needed; `IParticlesProps` removal was sufficient - -**What was implemented**: - -| Requirement | Implementation | -|---|---| -| `id` change → destroy + reload | `watch(() => props.id, ...)` calls `loadParticles()` | -| `options` change → destroy + reload | `watch(() => props.options, ..., { deep: true })` calls `loadParticles()` | -| `url` change → destroy + reload | `watch(() => props.url, ...)` calls `loadParticles()` | -| `theme` change → safe `loadTheme` | `watch(() => props.theme, ...)` with `(container as unknown as { loadTheme?: ... }).loadTheme?.(newTheme)` | -| Initial theme applied after load | `if (container && props.theme) { ... loadTheme?.(props.theme) }` inside `loadParticles()` | -| `particlesLoaded` emit guard | Emit signature `(e: "particlesLoaded", container?: Container)` — already correct | -| Teardown destroys container | `onUnmounted(() => { container?.destroy(); container = undefined })` — already present, unchanged | - -**Substep verification**: -- a (wrapper code): ✅ — `vue-particles.vue` updated with all watchers + initial theme -- b (demo alignment): ✅ — `App.vue` rewritten with reactive pill-based config switching -- c (template check): ✅ — N/A (no Vue 3 templates exist in repo) -- d (README alignment): ✅ — full rewrite covering all contract points -- e (build): ✅ — `pnpm --filter @tsparticles/vue3 build` succeeds - ---- - -### S2 — Vue 2 wrapper (✅ DONE) - -**Files modified**: -- `wrappers/vue2/src/Particles/vue-particles.vue` -- `wrappers/vue2/src/Particles/event-bus.ts` - -**What was implemented**: - -| Requirement | Implementation | -|---|---| -| `id` change → destroy + reload | `@Watch("id")` calls `particlesInit(this)` when `this.container` exists | -| `options` change → destroy + reload | `@Watch("options")` calls `particlesInit(this)` when `this.container` exists | -| `url` change → destroy + reload | `@Watch("url")` calls `particlesInit(this)` when `this.container` exists | -| `theme` change → safe `loadTheme` | `@Watch("theme")` with `(this.container as unknown as { loadTheme?: ... }).loadTheme?.(newTheme)` | -| Initial theme applied after load | `if (container && component.theme) { ... loadTheme?.(component.theme) }` inside `particlesInit()` | -| `IParticlesProps` export removed | Removed `export type IParticlesProps = ISourceOptions;` — used `ISourceOptions` directly in prop type | -| `particlesLoaded` type guard | Changed prop type to `(container?: Container) => void`, cb guards `if (container && component.particlesLoaded)` | -| `tsParticles.init()` call | Added `.then(() => tsParticles.init())` in `ensureParticlesInitialization()` in event-bus.ts | -| Teardown destroys container | `beforeDestroy()` calls `this.container?.destroy()` — unchanged | - -**Substep verification**: -- a (wrapper code): ✅ — `vue-particles.vue` updated with all watchers + initial theme + event-bus init fix -- b (demo alignment): ✅ — `App.vue` rewritten with reactive pill-based config switching -- c (template check): ✅ — N/A (no Vue 2 templates exist in repo) -- d (README alignment): ✅ — full rewrite covering all contract points -- e (build): ✅ — `pnpm --filter @tsparticles/vue2 build` succeeds (2 pre-existing TS warnings) - ---- - -### S3 — Angular wrapper - -**Files to modify**: -- `wrappers/angular/projects/ng-particles/src/lib/ng-particles.component.ts` -- `wrappers/angular/projects/ng-particles/src/lib/ng-particles.service.ts` (add `tsParticles.init()`) -- `wrappers/angular/projects/ng-particles/src/lib/ng-particles.module.ts` (add `theme` to `IParticlesProps` if needed — it's `ISourceOptions` which doesn't have it) - -**Changes needed**: - -#### 4a: Implement `OnChanges` interface - -```ts -import { OnChanges, SimpleChanges } from "@angular/core"; - -export class NgxParticlesComponent implements AfterViewInit, OnDestroy, OnChanges { - // ... - public ngOnChanges(changes: SimpleChanges): void { - if (!this.#container) { - return; // not loaded yet; ngAfterViewInit will handle initial load - } - if (changes["id"] || changes["options"] || changes["url"]) { - void this.#loadParticles(); - } - if (changes["theme"]) { - (this.#container as unknown as { loadTheme?: (name?: string) => Promise }).loadTheme?.(changes["theme"].currentValue); - } - } -} -``` - -**Risk**: `ngOnChanges` fires before `ngAfterViewInit`. Guard with `this.#container` check — if `#container` is undefined, skip reload (initial load happens in `ngAfterViewInit`). - -**Risk**: `ngOnChanges` with `options` object — Angular `OnChanges` uses reference equality for objects. This is fine IF the user provides a new options object reference. Does not support deep mutation detection. - -#### 4b: Add `theme` input - -```ts -@Input() theme?: string; -``` - -#### 4c: Fix `particlesLoaded` emitter type - -```ts -@Output() particlesLoaded: EventEmitter = new EventEmitter(); -``` - -Or guard: -```ts -const container = await tsParticles.load({ id: this.id, options: this.options, url: this.url }); -this.#container = container; -if (container) { - this.particlesLoaded.emit(container); -} -``` - -#### 4d: Fix service — add `tsParticles.init()` - -```ts -initPromise = (async () => { - if (particlesInit) { - await particlesInit(tsParticles); - } - await tsParticles.init(); // ADD THIS for v4 - initialized = true; -})().catch(...); -``` - -**Verification**: -- `pnpm --filter @tsparticles/angular build` -- Manual: change options binding, verify reload - -#### S3.b: Demo alignment -- Align `demo/angular/src/app/app.component.html` and `demo/angular/src/app/app.component.ts` with final behavior. -- Keep `*ngIf` only as optional visibility control (not as reactivity workaround). - -#### S3.c: Template alignment -- Verify whether Angular templates/scaffolds exist for this wrapper family. -- If none exist, record explicit `N/A` in implementation notes. - -#### S3.d: README alignment -- Update `wrappers/angular/README.md` with `theme` input and reload contract. - -#### S3.e: Completion gate -- Mark S3 complete only when S3.a-S3.d are done and verification passes. - ---- - -### S4 — Solid wrapper - -**Files to modify**: -- `wrappers/solid/src/Particles.tsx` -- `wrappers/solid/src/IParticlesProps.ts` - -**Changes needed**: - -#### 5a: Track `id`, `options`, `url` reactively - -```tsx -import { createEffect, createMemo, onCleanup } from "solid-js"; - -// Replace onMount with createEffect that tracks prop signals: -const loadParams = createMemo(() => ({ - id: props.id ?? "tsparticles", - options: props.options ?? props.params, - url: props.url, - theme: props.theme, -})); - -createEffect(async () => { - const params = loadParams(); - - // Clean up previous container first - // (onCleanup runs on disposal, but we need to destroy before creating a new one) - // Use a manual approach: - - // Actually in Solid, we need to use onCleanup inside the tracking scope - // or manage the container lifecycle manually -}); -``` - -**Recommended pattern** — use `on` with a merged key to observe changes: - -```tsx -let container: Container | undefined; -let previousId: string | undefined; - -createEffect(on( - () => [props.id, props.options, props.url] as const, - async ([id, options, url]) => { - // destroy previous - container?.destroy(); - - await waitForParticlesEngineInitialization(); - if (!isParticlesEngineInitialized()) throw new Error("..."); - - container = await tsParticles.load({ - id: id ?? "tsparticles", - options: options ?? props.params, - url, - }); - - props.particlesLoaded?.(container); - }, - { defer: false }, -)); - -// Theme effect -createEffect(on( - () => props.theme, - (theme) => { - if (!container || !theme) return; - (container as unknown as { loadTheme?: (name?: string) => Promise }).loadTheme?.(theme); - }, -)); - -// Cleanup on unmount -onCleanup(() => { - container?.destroy(); -}); -``` - -#### 5b: Add `theme` to `IParticlesProps` - -```ts -export interface IParticlesProps { - // ... existing props - theme?: string; -} -``` - -#### 5c: Fix `particlesLoaded` type - -```ts -particlesLoaded?: (container?: Container) => Promise; -``` - -**Verification**: -- `pnpm --filter @tsparticles/solid build` - -#### S4.b: Demo alignment -- Align `demo/solid/src/App.tsx` by removing conditional mount workaround. -- Ensure demo shows runtime config switching via prop updates. - -#### S4.c: Template alignment -- Verify whether Solid templates exist for this wrapper family. -- If none exist, record explicit `N/A` in implementation notes. - -#### S4.d: README alignment -- Update `wrappers/solid/README.md` (including corrected `particlesLoaded` signature). - -#### S4.e: Completion gate -- Mark S4 complete only when S4.a-S4.d are done and verification passes. - ---- - -### S5 — Qwik wrapper - -**Files to modify**: -- `wrappers/qwik/src/components/particles/particles.tsx` -- `wrappers/qwik/src/components/particles/IParticlesProps.ts` - -**Changes needed**: - -#### 6a: Track `id`, `options`, `url` reactively in `useVisibleTask$` - -In Qwik, `useVisibleTask$` runs once. To track prop changes, use the `track` function or use `useTask$` from `@builder.io/qwik` (not `useVisibleTask$` only). - -```tsx -import { component$, useVisibleTask$, useTask$, $, noSerialize } from "@builder.io/qwik"; - -const Particles = component$(props => { - const librarySig = useSignal>(); - - // Use useTask$ to track prop changes (runs eagerly) - useTask$(({ track, cleanup }) => { - const id = track(() => props.id) ?? "tsparticles"; - const options = track(() => props.options); - const url = track(() => props.url); - - cleanup(() => { - const c = librarySig.value; - if (c) { - c.destroy(); - librarySig.value = undefined; - } - }); - - // Async loading... - const load = $(async () => { - await waitForParticlesEngineInitialization(); - if (!isParticlesEngineInitialized()) throw new Error("..."); - - // Destroy old before creating new - const old = librarySig.value; - if (old) { - old.destroy(); - librarySig.value = undefined; - } - - const container = await tsParticles.load({ - id, options: options ?? props.params, url, - }); - - librarySig.value = noSerialize(container); - if (props.loaded) { - await props.loaded(container); - } - }); - - void load(); - }); - - // Also track theme changes separately - useTask$(({ track }) => { - const theme = track(() => props.theme); - const container = librarySig.value; - - if (!container || !theme) return; - (container as unknown as { loadTheme?: (name?: string) => Promise }).loadTheme?.(theme); - }); - - // Also do cleanup on unmount via useVisibleTask$ - useVisibleTask$(function Cleanup({ cleanup }) { - cleanup(() => { - const c = librarySig.value; - if (c) { - c.destroy(); - librarySig.value = undefined; - } - }); - }); -}); -``` - -**Important Qwik detail**: `useTask$` runs on the server too! Guard with `useVisibleTask$` for client-only code OR use `useTask$` with `{ eagerness: 'visible' }`. Better pattern: use `useTask$` with `{ eagerness: 'load' }` or combine strategies. - -**Alternative**: `useVisibleTask$` with a tracker: - -```tsx -useVisibleTask$(function Initializer({ track, cleanup }) { - const id = track(() => props.id) ?? "tsparticles"; - const options = track(() => props.options); - const url = track(() => props.url); - // ... load -}); -``` - -**But**: `useVisibleTask$` with `track()` is the correct Qwik pattern for reactive re-execution. When tracked values change, the cleanup runs, then the task re-executes. - -#### 6b: Add `theme` to `IParticlesProps` - -```ts -export interface IParticlesProps { - // ... existing props - theme?: string; -} -``` - -**Verification**: -- `pnpm --filter @tsparticles/qwik build` - -#### S5.b: Demo alignment -- Align `demo/qwik/src/root.tsx` by removing conditional mount workaround. -- Ensure demo demonstrates reactive updates without remount. - -#### S5.c: Template alignment -- Verify whether Qwik templates exist for this wrapper family. -- If none exist, record explicit `N/A` in implementation notes. - -#### S5.d: README alignment -- Update `wrappers/qwik/README.md` with theme caveat and reload contract. - -#### S5.e: Completion gate -- Mark S5 complete only when S5.a-S5.d are done and verification passes. - ---- - -### S6 — Astro wrapper - -**Files to modify**: -- `wrappers/astro/src/Particles.astro` - -**Changes needed**: - -#### 7a: Move from constructor-only to `connectedCallback` + `attributeChangedCallback` - -```astro - -``` - -#### 7b: Add `data-theme` attribute to template - -```astro - - - -``` - -#### 7c: Add `theme` to `IParticlesProps` in frontmatter - -```ts -export interface IParticlesProps { - id: string; - loaded?: string; - options?: ISourceOptions; - url?: string; - theme?: string; // ADD -} - -const { id, loaded, options, url, theme } = Astro.props as IParticlesProps; -``` - -**Risks**: -- `attributeChangedCallback` fires before `connectedCallback` for initial attributes. Guard with `this.isConnected`. -- `JSON.parse` on every attribute change is expensive for large options. Consider caching the parsed object. - -**Verification**: -- `pnpm --filter @tsparticles/astro build` -- Smoke test: change data attributes on the custom element, verify reload - -#### S6.b: Demo alignment -- Align `demo/astro/src/pages/index.astro` with attribute-driven reactivity. -- Verify connect/disconnect lifecycle behavior in navigation. - -#### S6.c: Template alignment -- Verify whether Astro templates exist for this wrapper family. -- If none exist, record explicit `N/A` in implementation notes. - -#### S6.d: README alignment -- Update `wrappers/astro/README.md` with `theme` prop and lifecycle/reload contract. - -#### S6.e: Completion gate -- Mark S6 complete only when S6.a-S6.d are done and verification passes. - ---- - -### S7 — Inferno wrapper - -#### S7.a: Wrapper implementation - -**File**: `wrappers/inferno/src/Particles.tsx` - -Current `loadContainer` does NOT invoke any loaded callback after `tsParticles.load`. Fix: - -```ts -async loadContainer() { - try { - await waitForParticlesEngineInitialization(); - if (this._cancelled) return; - - const { id, url, options, loaded, particlesLoaded } = this.props; - this._container = await tsParticles.load({ id: id ?? "tsparticles", url, options }); - - // Invoke loaded callbacks - if (this._container) { - await loaded?.(this._container); - await particlesLoaded?.(this._container); - } - } catch (e) { - getLogger().error("Particles: error during load", e); - } -} -``` - -Also add `theme` handling: -- Add `@Prop() theme?: string` to props interface -- In `componentDidUpdate`, check for theme change -- Safe `loadTheme` call - -#### S7.b: Demo alignment -- Verify inferno demo behavior (if present) is consistent with wrapper contract. -- If no inferno demo exists, record explicit `N/A`. - -#### S7.c: Template alignment -- Verify whether Inferno templates exist. -- If none exist, record explicit `N/A`. - -#### S7.d: README alignment -- Update `wrappers/inferno/README.md` to reflect callback semantics, optional theme plugin, and reload/teardown behavior. - -#### S7.e: Completion gate -- Mark S7 complete only when S7.a-S7.d are done and verification passes. - -### S8 — Lit wrapper - -#### S8.a: Wrapper implementation - -**File**: `wrappers/lit/src/lit-tsparticles.ts` - -Current `#loadParticles` assigns `this.container = container` but fires no event. Fix: - -```ts -async #loadParticles(currentRenderId: number): Promise { - // ... existing load logic ... - - this.container = container; - - if (container) { - // Dispatch loaded event - this.dispatchEvent(new CustomEvent("particlesLoaded", { - detail: container, - bubbles: true, - composed: true, - })); - } -} -``` - -#### S8.b: Demo alignment -- Verify lit demo behavior (if present) is aligned with the event contract. -- If no lit demo exists, record explicit `N/A`. - -#### S8.c: Template alignment -- Verify whether Lit templates exist. -- If none exist, record explicit `N/A`. - -#### S8.d: README alignment -- Update `wrappers/lit/README.md` with `particlesLoaded` event behavior and optional theme caveat. - -#### S8.e: Completion gate -- Mark S8 complete only when S8.a-S8.d are done and verification passes. - -Also add `theme` property: -```ts -@property({ type: String }) -theme?: string; -``` - -And a theme watcher in `update()`: -```ts -update(changedProperties: PropertyValues) { - super.update(changedProperties); - - if (changedProperties.has("theme") && this.container) { - const theme = changedProperties.get("theme") as string | undefined; - (this.container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(theme); - } - - if (changedProperties.has("options") || changedProperties.has("url") || changedProperties.has("id")) { - void this.#loadParticles(++this.#renderId); - } -} -``` - -### S9 — Riot wrapper - -#### S9.a: Wrapper implementation - -**File**: `wrappers/riot/src/riot-particles.riot` - -Current: no reload, no destroy on unmount. - -Fix: Riot components use `onMounted`, `onUnmounted`, `onUpdated` lifecycle hooks. The `export default { onMounted(props) { ... } }` only has `onMounted`. Need to add `onUpdated` for prop changes: - -```riot -export default { - async onMounted(props) { - // ... initial load (unchanged, but fix teardown) - }, - onUnmounted(props) { - // Destroy container - if (oldId) { - const oldContainer = tsParticles.dom().find((c) => c.id === oldId); - if (oldContainer) { - oldContainer.destroy(); - } - } - }, - onUpdated(props) { - // Check for prop changes and reload - if (oldId && (props.options !== oldId._options || props.url !== oldId._url)) { - // ... reload - } - } -} -``` - -**Riot caveat**: Riot components don't have automatic deep comparison. Manual tracking needed. - -#### S9.b: Demo alignment -- Verify riot demo behavior (if present) is aligned with reload + teardown contract. -- If no riot demo exists, record explicit `N/A`. - -#### S9.c: Template alignment -- Verify whether Riot templates exist. -- If none exist, record explicit `N/A`. - -#### S9.d: README alignment -- Update `wrappers/riot/README.md` with reactive reload and teardown behavior. - -#### S9.e: Completion gate -- Mark S9 complete only when S9.a-S9.d are done and verification passes. - -### S10 — WebComponents wrapper - -#### S10.a: Wrapper implementation - -**File**: `wrappers/webcomponents/src/Particles.ts` - -Add `"data-id"` and `"data-theme"` to `observedAttributes`: - -```ts -static get observedAttributes(): string[] { - return ["url", "options", "data-id", "data-theme"]; -} -``` - -The setter for `id` should trigger reload: -```ts -set id(value: string) { - this.setAttribute("data-id", value); -} -``` - -Or handle in `attributeChangedCallback`: -```ts -attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void { - if (name === "data-id") { - this.container.current?.destroy(); - void this.#loadParticles(++this.#loadId); - } else if (name === "data-theme") { - const container = this.container.current; - if (container && newValue) { - (container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(newValue); - } - } - // ... existing options/url handling -} -``` - -Also remove the deprecated `particlesInit` custom event dispatch from constructor (v3 pattern). - -#### S10.b: Demo alignment -- Verify webcomponents demo behavior (if present) reflects observed attribute support (`id/options/url/theme`). -- If no webcomponents demo exists, record explicit `N/A`. - -#### S10.c: Template alignment -- Verify whether WebComponents templates exist. -- If none exist, record explicit `N/A`. - -#### S10.d: README alignment -- Update `wrappers/webcomponents/README.md` with observed attributes and theme caveat. - -#### S10.e: Completion gate -- Mark S10 complete only when S10.a-S10.d are done and verification passes. - -### S11 — React docs - -#### S11.a: README alignment - -**File**: `wrappers/react/README.md` - -Current props table lists only `id`, `options`, `url`, `style`, `className`. Missing `particlesLoaded`. - -Add to the table: -```markdown -| particlesLoaded | function | Callback invoked when the container is loaded, receives `(container?: Container)` | -``` - -Also note: React wrapper currently has NO `theme` prop — this remains out of scope for S11 (React is docs-only in this plan). Document existing behavior only. - -#### S11.b: Demo alignment check -- Verify `demo/react/src/App.jsx` remains aligned with documented API. - -#### S11.c: Template alignment -- Verify `wrappers/react/templates/` status and record `N/A` if still empty. - -#### S11.d: Completion gate -- Mark S11 complete only when S11.a-S11.c are done. - ---- - -### S12 — Preact wrapper - -**Files to modify**: -- `wrappers/preact/src/IParticlesProps.ts` -- `wrappers/preact/src/Particles.tsx` -- `wrappers/preact/src/IParticlesState.ts` (type fix for container) - -#### S12.a: Wrapper implementation - -**1. Add `theme` prop to `IParticlesProps`:** - -```ts -export interface IParticlesProps { - id?: string; - width?: string; - height?: string; - options?: ISourceOptions; - url?: string; - params?: ISourceOptions; - style?: CSSProperties; - className?: string; - canvasClassName?: string; - container?: RefObject; // FIX: add | undefined - particlesLoaded?: (container?: Container) => Promise; // FIX: add ? to container - theme?: string; // ADD -} -``` - -**2. Fix `container` ref type in state:** - -In `IParticlesState.ts`, the `library` type is `Container` — keep as-is (it's always assigned after successful load and is never `undefined` when set). The fix is in `IParticlesProps`. - -**3. Add `loadTheme` call in `loadParticles`:** - -```ts -private async loadParticles(): Promise { - if (!this.state.init) { return; } - - const cb = async (container?: Container) => { - if (this.props.container) { - (this.props.container as MutableRefObject).current = container; - } - - this.setState({ library: container }); - - // Apply theme after load if provided - if (container && this.props.theme) { - (container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(this.props.theme); - } - - if (this.props.particlesLoaded) { - await this.props.particlesLoaded(container); - } - }; - - const container = await tsParticles.load({ - url: this.props.url, - options: this.props.options, - id: this.props.id, - }); - - await cb(container); -} -``` - -**4. Handle theme changes in `componentDidUpdate`:** - -The existing `componentDidUpdate` calls `this.refresh()` which destroys and reloads. But for `theme` changes, we shouldn't destroy+reload — just call `loadTheme`. Add theme check: - -```ts -componentDidUpdate(prevProps: Readonly): void { - // Theme change: apply without reload - if (prevProps.theme !== this.props.theme && this.state.library) { - (this.state.library as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(this.props.theme); - } - // Other prop changes: full reload - this.refresh(); -} -``` - -**Verification**: -- `pnpm --filter @tsparticles/preact build` - -#### S12.b: Demo alignment -- **Remove** `{this.state.particlesInitialized && }` conditional in `demo/preact/src/components/app.js` -- Move `initParticlesEngine` to module level (outside component lifecycle) -- Verify `` works standalone after init - -#### S12.c: Template alignment -- Verify whether Preact templates exist (`wrappers/preact/templates/`). -- If none exist, record explicit `N/A`. - -#### S12.d: README alignment -- Update `wrappers/preact/README.md` with `theme` prop documentation and optional plugin caveat. - -#### S12.e: Completion gate -- Mark S12 complete only when S12.a-S12.d are done and verification passes. - ---- - -### S13 — Svelte wrapper - -**Files to modify**: -- `wrappers/svelte/src/lib/Particles.svelte` - -#### S13.a: Wrapper implementation - -**1. Add `theme` prop:** - -```svelte -export let theme: string | undefined = undefined; -``` - -**2. Replace `afterUpdate` with selective reactivity:** - -Current: `afterUpdate(async () => { await loadParticles(); })` — reloads on EVERY update (class, style, etc.). - -Fix: use Svelte reactive statements to track only relevant props: - -```svelte -$: if (mounted) { - void loadParticles(); -} -// Trigger only when id, options, url, or theme change: -$: void (id, options, url, theme, loadParticlesOnChange()); -// Actually Svelte doesn't have a great pattern for selective trigger. -// Better approach: use a reactive hash and compare. -``` - -**Recommended pattern** — use a reactive `loadKey` that changes only when id/options/url change, and a separate reactive for theme: - -```svelte -$: loadKey = `${id}|${url}|${JSON.stringify(options)}`; - -$: if (mounted && loadKey) { - void loadParticles(); -} - -$: if (mounted && theme && currentContainer) { - (currentContainer as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(theme); -} -``` - -Then remove `afterUpdate` entirely. - -**3. Apply initial theme after `tsParticles.load`:** - -```svelte -const container = await tsParticles.load({ id, options, url }); -currentContainer = container; - -// Apply theme if provided -if (container && theme) { - (container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(theme); -} - -cb(container); -``` - -**4. Add guard for container existence before dispatch:** - -```svelte -const cb = (container?: Container) => { - dispatch(particlesLoadedEvent, { - particles: container // already Container | undefined — correct - }); - oldId = id; -}; -``` - -**Verification**: -- `pnpm --filter @tsparticles/svelte build` - -#### S13.b: Demo alignment -- Rename `particlesInit` → `initParticlesEngine` in `demo/svelte/src/App.svelte` and `demo/svelte-kit/src/routes/+page.svelte` -- Verify reactive `options` changes work via prop binding - -#### S13.c: Template alignment -- Verify whether Svelte templates exist. -- If none exist, record explicit `N/A`. - -#### S13.d: README alignment -- Update `wrappers/svelte/README.md` with `theme` prop and reload contract. - -#### S13.e: Completion gate ✅ -- Completed: wrapper (`theme` prop, reactive `$:` statements, `loadTheme`), demos (Svelte 5 `mount()`, `initParticlesEngine` naming, SSR-safe dynamic imports in SvelteKit), README (props table, reactive behavior, theme docs). Build pass. User-verified both demos working. - ---- - -### S14 — Stencil wrapper - -**Files to modify**: -- `wrappers/stencil/src/components/stencil-particles/stencil-particles.tsx` - -#### S14.a: Wrapper implementation - -**1. Add `theme` prop and loaded event:** - -```ts -import { Component, type JSX, Prop, Watch, Event, EventEmitter, h } from "@stencil/core"; - -@Event() particlesLoaded!: EventEmitter; - -@Prop() theme?: string; -``` - -**2. Add `@Watch("theme")` for theme changes:** - -```ts -@Watch("theme") -protected async onThemeChange(): Promise { - if (!this.container) return; - (this.container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(this.theme); -} -``` - -**3. Emit loaded event after successful load:** - -```ts -private async loadParticles(currentRenderId: number): Promise { - // ... existing code ... - this.container = container; - - // Emit particlesLoaded event (NEW) - this.particlesLoaded.emit(container); - - // Apply theme if provided (NEW) - if (container && this.theme) { - (container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(this.theme); - } -} -``` - -**4. Ensure `@Watch("theme")` doesn't fire before component is loaded:** - -The `@Watch` on `theme` will fire when the prop is first set (before `componentDidLoad`). Guard with `this.container` check — already done in `onThemeChange`. - -**Verification**: -- `pnpm --filter @tsparticles/stencil build` - -#### S14.b: Demo alignment -- Verify stencil demo (if present) is aligned with the new `particlesLoaded` event and `theme` prop. -- If no stencil demo exists, record explicit `N/A`. - -#### S14.c: Template alignment -- Verify whether Stencil templates exist. -- If none exist, record explicit `N/A`. - -#### S14.d: README alignment -- Update `wrappers/stencil/README.md` with `theme` prop, `particlesLoaded` event, and optional plugin note. - -#### S14.e: Completion gate -- Mark S14 complete only when S14.a-S14.d are done and verification passes. - ---- - -### S15 — Ember wrapper - -**Files to modify**: -- `wrappers/ember/addon/modifiers/particles.ts` - -#### S15.a: Wrapper implementation - -**1. Add `theme` to modifier signature:** - -```ts -interface ParticlesModifierSignature { - Args: { - Positional: []; - Named: { - options: Options; - url: string; - particlesLoaded: (container?: Container) => void; // FIX: undefined allowed - theme?: string; // ADD - }; - }; -} -``` - -**2. Fix memory leak — destroy previous container before creating new:** - -The modifier's `modify()` can be called multiple times when named arguments change. Currently each call creates a new container and registers a new destructor without cleaning up the old one. - -Fix: track the current container at the instance level: - -```ts -export default class ParticlesModifier extends Modifier { - #container?: Container; - #cleanupRegistered = false; - - async modify( - element: Element, - _: PositionalArgs, - { options, url, particlesLoaded, theme }: NamedArgs, - ) { - if (!element.id) { - throw new Error('The specified element must have an id attribute.'); - } - - await waitForParticlesEngineInitialization(); - if (!isParticlesEngineInitialized()) { - throw new Error('...'); - } - - // Destroy previous container before creating new one (FIX: memory leak) - this.#container?.destroy(); - this.#container = undefined; - - let container = await tsParticles.load({ - id: element.id, - options: options ?? {}, - url, - }); - - this.#container = container; - - // Apply theme if provided - if (container && theme) { - (container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(theme); - } - - if (particlesLoaded && container) { - particlesLoaded(container); - } - - // Register destructor only once (FIX: avoid accumulation) - if (!this.#cleanupRegistered) { - registerDestructor(this, () => { - this.#container?.destroy(); - this.#container = undefined; - }); - this.#cleanupRegistered = true; - } - } -} -``` - -**3. Fix `particlesLoaded` type:** `(container: Container) => void` → `(container?: Container) => void` - -**Verification**: -- `pnpm --filter @tsparticles/ember build` - -#### S15.b: Demo alignment -- Verify Ember demo (if present) is aligned with the modified modifier API. -- If no Ember demo exists, record explicit `N/A`. - -#### S15.c: Template alignment -- Verify whether Ember templates exist. -- If none exist, record explicit `N/A`. - -#### S15.d: README alignment -- Update `wrappers/ember/README.md` with `theme` support, reload behavior, and optional plugin note. - -#### S15.e: Completion gate -- Mark S15 complete only when S15.a-S15.d are done and verification passes. - ---- - -### S16 — jQuery wrapper - -**Files to modify**: -- `wrappers/jquery/src/particles.ts` - -#### S16.a: Wrapper implementation - -**1. Add theme support to the jQuery API:** - -The current API has `load(options)` and `ajax(jsonUrl)`. Adding a separate `setTheme` method is cleaner than modifying the existing signatures: - -```ts -type ParticlesResult = { - load: (options: ISourceOptions) => Promise; - ajax: (jsonUrl: string) => Promise; - setTheme: (theme: string) => Promise; // ADD -}; -``` - -**2. Track containers per element for theme application:** - -```ts -// Add a WeakMap to track containers per element -const containers = new WeakMap(); - -// In the load function, after tsParticles.load(): -return await tsParticles.load({ id: element.id, options }).then(container => { - if (container) { - containers.set(element, container); - } - return container; -}); - -// Add setTheme method: -const setTheme = async (theme: string): Promise => { - for (const element of this) { - const container = containers.get(element); - if (container) { - await (container as unknown as { loadTheme?: (name?: string) => Promise }) - .loadTheme?.(theme); - } - } -}; -``` - -**3. Apply theme on initial load when passed via options?** No — jQuery's API doesn't have a `theme` parameter on `load()`/`ajax()`. The `setTheme` method is the appropriate API for theme control. - -**4. Update the return object:** - -```ts -return { load, ajax, setTheme }; -``` - -**Verification**: -- `pnpm --filter @tsparticles/jquery build` - -#### S16.b: Demo alignment -- Verify jQuery demo (if present) reflects the new `setTheme` API. -- If no jQuery demo exists, record explicit `N/A`. - -#### S16.c: Template alignment -- Verify whether jQuery templates exist. -- If none exist, record explicit `N/A`. - -#### S16.d: README alignment -- Update `wrappers/jquery/README.md` with `setTheme` method documentation and optional plugin note. - -#### S16.e: Completion gate -- Mark S16 complete only when S16.a-S16.d are done and verification passes. - ---- - -### S17 — Angular-fireworks wrapper - -**Files to modify**: -- `wrappers/angular-fireworks/projects/ng-fireworks/src/lib/ng-fireworks.component.ts` - -#### S17.a: Wrapper implementation - -**1. Implement `OnChanges` interface:** - -```ts -import { AfterViewInit, Component, Inject, Input, OnChanges, OnDestroy, PLATFORM_ID, SimpleChanges } from "@angular/core"; -import { FireworkOptions, fireworks } from "@tsparticles/fireworks"; - -@Component({ - selector: "ngx-fireworks", - standalone: false, - template: `
`, -}) -export class NgxFireworksComponent implements AfterViewInit, OnDestroy, OnChanges { - @Input() options?: FireworkOptions; - @Input() id = "tsparticles"; - - #fireworksInstance?: Awaited>; - #destroyed = false; - - constructor(@Inject(PLATFORM_ID) protected platformId: string) {} - - public ngAfterViewInit(): void { - if (isPlatformServer(this.platformId)) { return; } - void this.#startFireworks(); - } - - public ngOnChanges(changes: SimpleChanges): void { - if (isPlatformServer(this.platformId)) { return; } - if (this.#destroyed) { return; } - // Re-fire on options or id changes - if (changes["options"] || changes["id"]) { - void this.#startFireworks(); - } - // Note: theme is not applicable — fireworks uses @tsparticles/fireworks - // which is a standalone display, not the particles engine. - } - - public ngOnDestroy(): void { - this.#destroyed = true; - this.#fireworksInstance?.stop(); - this.#fireworksInstance = undefined; - } - - async #startFireworks(): Promise { - // Stop previous instance - this.#fireworksInstance?.stop(); - this.#fireworksInstance = undefined; - - this.#fireworksInstance = await fireworks(this.id, this.options); - } -} -``` - -**2. Note on destroy-guard**: The `#destroyed` flag prevents `ngOnChanges` from starting fireworks after the component is destroyed (since Angular can fire change detection after destroy in some edge cases). - -**Verification**: -- `pnpm --filter @tsparticles/angular-fireworks build` - -#### S17.b: Demo alignment -- Verify `demo/angular-fireworks/` demo reflects reactive input behavior. -- If no demo exists, record explicit `N/A`. - -#### S17.c: Template alignment -- Verify whether Angular-fireworks templates exist. -- If none exist, record explicit `N/A`. - -#### S17.d: README alignment -- Update `wrappers/angular-fireworks/README.md` with `OnChanges` behavior documentation. - -#### S17.e: Completion gate -- Mark S17 complete only when S17.a-S17.d are done and verification passes. - ---- - -## Docs Change Plan — Vue 3 Guide - -### Stale patterns found in `websites/website/docs/guides/vue3.md` (622 lines) - -| Pattern | Occurrences | Lines (EN) | Action | -|--------------------------|-------------|---------------------------------------------------------------------------|-------------------------------------------------------------------| -| `:init="particlesInit"` | 10 | 141, 166, 198, 232, 266, 442, 481, 482, 595 | Remove from ALL examples | -| `particlesInit` function | 15 | 126, 135, 141, 159, 166, 185, 198, 219, 232, 253, 266, 429, 442, 584, 595 | Remove function definitions (no longer needed at component level) | -| `@particles-init` event | 1 | 613 | Remove from API table | -| `:init` prose section | ~2 | 128-143, 477 | Remove "Using particlesInit with the Component" section | -| `useParticles()` | 1 | 492 | Wrong function name; should be `useParticlesProvider` | - -### Required changes to ALL 10 files (EN + 9 translations) - -1. **Remove lines 126-143**: Section "Using `particlesInit` with the Component" -2. **Remove `:init="particlesInit"`** from all code examples -3. **Remove `@particles-init`** from API reference table (line 613) -4. **Fix `useParticles()` → `useParticlesProvider()`** or remove if not publicly exported -5. **Remove `particlesInit` function definitions** from all code examples -6. **Ensure API table has `theme` prop** with note about optional plugin dependency -7. **Add note** about `@tsparticles/plugin-themes` requirement for `theme` prop - -### Exact file list -``` -websites/website/docs/guides/vue3.md (EN source) -websites/website/docs/zh/guides/vue3.md (Chinese) -websites/website/docs/ja/guides/vue3.md (Japanese) -websites/website/docs/hi/guides/vue3.md (Hindi) -websites/website/docs/ru/guides/vue3.md (Russian) -websites/website/docs/pt/guides/vue3.md (Portuguese) -websites/website/docs/fr/guides/vue3.md (French) -websites/website/docs/de/guides/vue3.md (German) -websites/website/docs/es/guides/vue3.md (Spanish) -websites/website/docs/it/guides/vue3.md (Italian) -``` - -### Agent execution order for S18+S19 -1. Edit EN file first (S18) -2. Mirror structural + code changes in all 9 translations (S19) -3. Preserve translated prose where possible; update only code blocks and section structure -4. Run stale-pattern grep to verify: no `:init`, no `@particles-init`, no `particlesInit` remains - ---- - -## README Changes Per Wrapper - -README work is **embedded in each wrapper step** (S1-S17) and is not a standalone late phase. - -For every wrapper step, the README must document: -1. `theme` support depends on optional `@tsparticles/plugin-themes` (if wrapper exposes `theme`) -2. Missing plugin ⇒ `theme` is safe no-op -3. `id`/`options`/`url` changes reload particles -4. Loaded callback/event fires only after `tsParticles.load` resolves -5. Component/element teardown destroys container - -Wrapper-specific known README gaps are listed in each wrapper section and must be closed inside that wrapper step. - ---- - -## Risk Register (Expanded) - -| Risk | Impact | Likelihood | Mitigation | -|---------------------------------------------------------|--------------------------------|:----------:|----------------------------------------------------------------------------------------------------------------------------------------| -| `loadTheme` type mismatch across 6 wrappers | Build failures | High | Use local cast at each call site: `(container as unknown as { loadTheme?: ... })` | -| `Container \| undefined` passed to strict callbacks | TS errors / runtime crashes | High | Guard before callback emit: `if (container) cb(container)` or type signature `(container?: Container) => void` | -| Over-triggered reload effects/watchers | Perf regression, flicker | Medium | Explicit dependency tracking + destroy-before-load | -| Vue 3 deep watcher on `options` firing too often | Performance | Medium | Use `{ deep: false }` and rely on reference comparison (user creates new object = good, mutation = not tracked — acceptable trade-off) | -| Angular `ngOnChanges` fires before `ngAfterViewInit` | Double load / error | High | Guard with `this.#container` check | -| Vue 2 `@Watch` decorator + SFC `export type` issue | Compile error | Medium | Remove `export type IParticlesProps = ISourceOptions` from SFC | -| Qwik `useTask$` running on server | SSR crash | High | Use `useVisibleTask$` instead, or guard with `isServer` | -| Astro attribute observer race (concurrent loads) | Wrong config shown | Medium | Use monotonic `#loadId` token (already planned) | -| Astro `JSON.parse` on every attribute change | Performance | Low | Cache parsed options, only re-parse when string changes | -| Agent applies v3 mental model on v4 code | Wrong API/docs | High | Mandatory repository-first audit + all wrappers are v4-only (no v3 history) | -| Translation edits break code blocks | Docs render issues | Medium | Manual spot-check after scripted edits | -| `IParticlesProps` naming in Vue3 shadows props type | Confusion / wrong usage | Medium | Fix: remove standalone `export type IParticlesProps = ISourceOptions` | -| Vue 2 event-bus missing `tsParticles.init()` | Runtime crash (v4 requirement) | High | Add `tsParticles.init()` call in `ensureParticlesInitialization` | -| Angular NgParticlesService missing `tsParticles.init()` | Runtime crash (v4 requirement) | High | Add `tsParticles.init()` call in `NgParticlesService.init()` | -| Solid `onCleanup` inside `createEffect` scope removal | Memory leak | Medium | Ensure top-level `onCleanup` for unmount, plus manual destroy in reactive effect | -| Riot `tsParticles.dom()` usage (v3 API) | Runtime error (v4) | Medium | Track container locally instead of querying `tsParticles.dom()` | - ---- - -## Validation Plan - -### Build validation -```bash -# Core wrappers -pnpm nx run-many -t build --projects=@tsparticles/vue3,@tsparticles/vue2,@tsparticles/angular,@tsparticles/solid,@tsparticles/qwik,@tsparticles/astro - -# Extended wrappers touched in S7-S10 -pnpm nx run-many -t build --projects=@tsparticles/inferno,@tsparticles/lit,@tsparticles/riot,@tsparticles/webcomponents -``` - -### Focused smoke checks (per wrapper) -1. Verify that changing `options` prop at runtime reloads particles without remounting component -2. Verify that changing `url` prop at runtime reloads particles -3. Verify that changing `id` prop at runtime destroys old container and creates new one with new id -4. Verify `theme` prop change is safe no-op (no crash) when `@tsparticles/plugin-themes` is NOT loaded -5. Verify `theme` prop change applies the theme when plugin IS loaded -6. Verify component unmount destroys the container (no orphan animations) -7. Verify `particlesLoaded`/`loaded` callback emits after successful load - -### Docs stale-pattern validation -```bash -# Check all Vue 3 guide files for stale patterns -grep -rn ":init" websites/website/docs/*/guides/vue3.md -grep -rn "particles-init" websites/website/docs/*/guides/vue3.md -grep -rn "particlesInit" websites/website/docs/*/guides/vue3.md - -# These should return NO results after S18+S19 -``` - -### README consistency scan -- For each updated README, verify: theme plugin note, reload contract, loaded callback, destroy behavior - ---- - -## Handoff Checklist for Implementing Agents - -### Before coding: -- [ ] Read the current repository files (do not trust memory) -- [ ] Read this document's per-wrapper analysis for the target wrapper(s) -- [ ] Confirm target files and prop type declarations -- [ ] Note that the entire `wrappers/` directory is v4-only (no v3 wrapper code existed) - -### During coding: -- [ ] Changes must be wrapper-local and minimal -- [ ] No engine-wide typing changes in this phase -- [ ] Safe `loadTheme` call: `(container as unknown as { loadTheme?: (name?: string) => Promise }).loadTheme?.(theme)` -- [ ] Guard `tsParticles.load()` result for `undefined` before using as `Container` -- [ ] Ensure `tsParticles.init()` is called in the provider/init function (v4 requirement that some wrappers miss) - -### Before handoff: -- [ ] Run wrapper build: `pnpm --filter @tsparticles/ build` -- [ ] Run stale-pattern checks for docs (if docs touched) -- [ ] Include list of touched files and exact behavior deltas - ---- - -## Execution Order (Recommended for Sub-Agents) - -Each step below is designed to be executed by a SEPARATE agent from scratch, using only this document as context. - -``` -S1 Vue 3 wrapper → Sub-agent A -S2 Vue 2 wrapper → Sub-agent B -S3 Angular wrapper → Sub-agent C -S4 Solid wrapper → Sub-agent D -S5 Qwik wrapper → Sub-agent E -S6 Astro wrapper → Sub-agent F ✅ - -S7 Inferno wrapper → Sub-agent G -S8 Lit wrapper → Sub-agent H -S9 Riot wrapper → Sub-agent I -S10 WebComponents wrapper → Sub-agent J -S11 React docs → Sub-agent K - -S12 Preact wrapper → Sub-agent L -S13 Svelte wrapper ✅ Done -S14 Stencil wrapper → Sub-agent N -S15 Ember wrapper → Sub-agent O -S16 jQuery wrapper → Sub-agent P -S17 Angular-fireworks wrapper → Sub-agent Q - -S18 EN docs (vue3.md) → Sub-agent X -S19 9 translations (vue3.md) → Sub-agent Y - -S20 Validation → Sub-agent Z (after all above) -S21 Final handoff → Sub-agent AA - -S22 EN wrapper guide audit → Sub-agent BB (audit only — no code changes) -S23 Translation alignment → Sub-agent CC (only if S22 finds gaps) -``` - -Note: use wave execution from the protocol section. `S18` must precede `S19`. `S22` must precede `S23`. S22 is audit-only: it produces a gap matrix but does NOT edit files. - ---- - -## Demo & Template Alignment - -All 29 demo projects live under `demo/` at the repo root (NOT inside wrapper packages). Each wrapper we modify has a corresponding demo. Additionally, some demos use patterns that are **workarounds for missing reactivity** — these should be simplified within the corresponding wrapper step (not deferred). - -Rule: if a wrapper has an associated demo/template, that wrapper step remains open until those artifacts are aligned. - -### Demo inventory: conditional mounting patterns (workarounds for no reactivity) - -| Demo | Pattern | Why it exists | After reactivity fix | -|---------------------------------------------------------|---------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------| -| **Solid** (`demo/solid/src/App.tsx`) | `{init() && }` via `` | `onMount` + `createResource` was one-shot; needed init gate | Move `initParticlesEngine` to app bootstrap; remove `` gate around `` | -| **Preact** (`demo/preact/src/app.js`) | `{this.state.particlesInitialized && }` | Constructor via `initParticlesEngine` returns promise, no reactivity | Move init to app bootstrap; remove conditional render | -| **Qwik** (`demo/qwik/src/root.tsx`) | `{particlesReady.value && }` | `useVisibleTask$` runs once | Remove conditional; component itself waits for init | -| **Angular** (`demo/angular/src/app/app.component.html`) | `*ngIf="particlesVisible"` on `` | Visibility toggle workaround (options change required re-mount) | Less needed now, but `*ngIf` remains valid for conditional display | -| **React** (`demo/react/src/App.jsx`) | No conditional — uses `ParticlesProvider` | Already has proper init gate | No change needed | -| **Vue 3** (`demo/vue3/src/App.vue`) | No conditional — uses `app.use(Particles, { init })` | Already has proper plugin init | No change needed | -| **Vue 2** (`demo/vue2/src/App.vue`) | No conditional — uses `Vue.use(Particles, { init })` | Already has proper plugin init | No change needed | - -### Demos that use stale `particlesInit` function name at component level -| Demo | Location | Pattern | Action | -|------|----------|---------|--------| -| **Svelte** (`demo/svelte/src/App.svelte`) | `particlesInit(async (engine) => { ... })` | Function named `particlesInit` | Rename to `initParticlesEngine` for clarity (not broken, just naming) | -| **Svelte Kit** (`demo/svelte-kit/src/routes/+page.svelte`) | `particlesInit(async (engine) => { ... })` | Same as above | Same action | - -### Demos with `*ngIf` visibility toggle on particles (could be simplified) -| Demo | Pattern | Notes | -|------|---------|-------| -| **Angular** (`demo/angular/src/app/app.component.html:840-841`) | `*ngIf="particlesVisible"` on `` | Used for toggle button. After reactivity, options can change without re-mount. `ngIf` is still valid for hide/show. | -| **Angular-confetti** | `*ngIf="confettiVisible"` | Separate sub-component | -| **Angular-fireworks** | `*ngIf="fireworksVisible"` | Separate sub-component | - -### Step-to-artifact mapping (authoritative) - -Use this map to decide ownership and avoid file collisions. - -| Step | Wrapper files | Demo files | README | -|------|---------------|------------|--------| -| S1 | `wrappers/vue3/**` | `demo/vue3/**` | `wrappers/vue3/README.md` | -| S2 | `wrappers/vue2/**` | `demo/vue2/**` | `wrappers/vue2/README.md` | -| S3 | `wrappers/angular/**` | `demo/angular/**` | `wrappers/angular/README.md` | -| S4 | `wrappers/solid/**` | `demo/solid/**` | `wrappers/solid/README.md` | -| S5 | `wrappers/qwik/**` | `demo/qwik/**` | `wrappers/qwik/README.md` | -| S6 | `wrappers/astro/**` | `demo/astro/**` | `wrappers/astro/README.md` | -| S7 | `wrappers/inferno/**` | `demo/inferno/**` (check + align if needed) | `wrappers/inferno/README.md` | -| S8 | `wrappers/lit/**` | lit demo if present | `wrappers/lit/README.md` | -| S9 | `wrappers/riot/**` | riot demo if present | `wrappers/riot/README.md` | -| S10 | `wrappers/webcomponents/**` | webcomponents demo if present | `wrappers/webcomponents/README.md` | -| S11 | `wrappers/react/README.md` | `demo/react/**` check only | `wrappers/react/README.md` | -| S12 | `wrappers/preact/**` | `demo/preact/**` | `wrappers/preact/README.md` | -| S13 | `wrappers/svelte/**` | `demo/svelte/**` + `demo/svelte-kit/**` | `wrappers/svelte/README.md` | -| S14 | `wrappers/stencil/**` | stencil demo if present | `wrappers/stencil/README.md` | -| S15 | `wrappers/ember/**` | ember demo if present | `wrappers/ember/README.md` | -| S16 | `wrappers/jquery/**` | jquery demo if present | `wrappers/jquery/README.md` | -| S17 | `wrappers/angular-fireworks/**` | `demo/angular-fireworks/**` if present | `wrappers/angular-fireworks/README.md` | -| S18 | `websites/website/docs/guides/vue3.md` | N/A | N/A | -| S19 | `websites/website/docs/{zh,ja,hi,ru,pt,fr,de,es,it}/guides/vue3.md` | N/A | N/A | - -If a mapped demo does not exist, record explicit `N/A` in the step output. - -#### S1-DEMO — Vue 3 demo -**Files**: `demo/vue3/src/App.vue` -**Actions**: -- No conditional render to remove (already clean) -- Verify `:options` binding works with reactive changes -- Optionally add a "switch config" button to demo reactivity (changing `:options` prop) -- Verify `@particles-loaded` event fires after each reload - -#### S2-DEMO — Vue 2 demo -**Files**: `demo/vue2/src/App.vue` -**Actions**: -- Same as Vue 3 — verify reactive options work -- Optionally add theme toggle demo - -#### S3-DEMO — Angular demo -**Files**: `demo/angular/src/app/app.component.html`, `demo/angular/src/app/app.component.ts` -**Actions**: -- The `*ngIf="particlesVisible"` pattern is a workaround that can remain for toggle functionality, but should be documented as optional -- Add `theme` input binding example if desired -- Verify `[options]` changes trigger reload via `OnChanges` -- The `particlesVisible` toggle should NOT be the only way to change config — add a "Switch config" button that changes `[options]` directly - -#### S4-DEMO — Solid demo -**Files**: `demo/solid/src/App.tsx` -**Actions**: -- **Remove** the `` conditional around `` -- Move `initParticlesEngine` call to module level (not inside `onMount`) -- After reactivity: `` works standalone -- Optionally add config switch button to demo reactive `options` change - -#### S5-DEMO — Qwik demo -**Files**: `demo/qwik/src/root.tsx` -**Actions**: -- **Remove** the `{particlesReady.value && }` conditional -- Move `initParticlesEngine` to module level (outside component) -- After reactivity: `` works standalone -- Optionally add theme/config switch - -#### S6-DEMO — Astro demo -**Files**: `demo/astro/src/pages/index.astro` -**Actions**: -- Verify `` works with reactive attribute changes -- Add `disconnectedCallback` test (navigate away and back) - -### Templates -- `wrappers/react/templates/` exists but is EMPTY — no CRA templates to update -- No other template directories found in the repo -- **Action**: keep explicit template check in every wrapper step and record `N/A` when applicable - -### Cross-wrapper README maintenance for demos -- After any demo change, verify the demo's code example matches the corresponding wrapper README's example -- If a demo demonstrates `:init` or `@particles-init` pattern, REMOVE it (stale API) - -### Add to Definition of Done -- [ ] Conditional rendering workarounds removed from Solid (S4), Preact (S12), Qwik (S5) demos -- [x] Svelte (S13) demos use `initParticlesEngine` naming (not `particlesInit`) -- [ ] Angular demo has config switch example (not only `*ngIf` toggle) -- [ ] All demos use correct wrapper API (no `:init`, no `@particles-init`) -- [ ] Each core wrapper demo verifiably demonstrates reactive prop changes - ---- - -## Definition of Done - -Done only if all are true: -- [ ] All 6 wrappers react to `id`, `options`, and `url` updates by reloading (within each framework's API model) -- [ ] All 6 wrappers safely handle `theme` updates without plugin hard dependency -- [ ] All 6 wrappers call `tsParticles.init()` during bootstrap (v4 fix) -- [ ] Every wrapper step (S1-S17) includes aligned demo + template check + README before closure -- [ ] All touched wrapper docs/readmes explicitly document the optional theme-plugin dependency and no-op behavior without plugin -- [ ] Website docs for touched wrappers are updated and aligned with README + implementation behavior -- [ ] All wrappers emit loaded callback/event only after `tsParticles.load` resolves -- [ ] All wrappers destroy container on component teardown -- [ ] TypeScript build passes for all affected wrapper packages -- [ ] Vue 3 English guide fixed and all 9 translations aligned -- [ ] No `:init` / `@particles-init` / stale `particlesInit` in Vue 3 guides -- [ ] Inferno calls loaded callback after load -- [ ] Lit dispatches `particlesLoaded` event -- [ ] Riot has proper teardown (container destroy on unmount) -- [x] WebComponents observes `id` attribute changes -- [x] A1 audit completed: `nextjs`, `nuxt2`, `nuxt3`, `nuxt4`, `angular-confetti` closed as no-change with evidence; `preact`, `svelte`, `stencil`, `ember`, `jquery`, `angular-fireworks` activated for implementation -- [x] S22 audit completed: all 16 wrapper guide pages checked against wrapper READMEs. Gap matrix produced. -- [x] S23 translations aligned (All 16 EN guides fixed, 144 translation files mirrored) - ---- - -### S22 — EN Wrapper Guide Audit (audit only — no file edits) - -**Goal**: Check every wrapper guide page under `websites/website/docs/guides/` (except `vue3.md`, `index.md`, `vanilla.md`, `wordpress.md`) against the actual wrapper implementation and README from S1-S17. - -**Files to audit** (15 EN files): - -| Wrapper | Guide file | Reference README | -|---------|-----------|------------------| -| Angular | `websites/website/docs/guides/angular.md` | `wrappers/angular/README.md` | -| Solid | `websites/website/docs/guides/solid.md` | `wrappers/solid/README.md` | -| Qwik | `websites/website/docs/guides/qwik.md` | `wrappers/qwik/README.md` | -| Astro | `websites/website/docs/guides/astro.md` | `wrappers/astro/README.md` | -| React | `websites/website/docs/guides/react.md` | `wrappers/react/README.md` | -| Preact | `websites/website/docs/guides/preact.md` | `wrappers/preact/README.md` | -| Svelte | `websites/website/docs/guides/svelte.md` | `wrappers/svelte/README.md` | -| Stencil | `websites/website/docs/guides/stencil.md` | `wrappers/stencil/README.md` | -| Ember | `websites/website/docs/guides/ember.md` | `wrappers/ember/README.md` | -| jQuery | `websites/website/docs/guides/jquery.md` | `wrappers/jquery/README.md` | -| Inferno | `websites/website/docs/guides/inferno.md` | `wrappers/inferno/README.md` | -| Lit | `websites/website/docs/guides/lit.md` | `wrappers/lit/README.md` | -| Riot | `websites/website/docs/guides/riot.md` | `wrappers/riot/README.md` | -| WebComponents | `websites/website/docs/guides/webcomponents.md` | `wrappers/webcomponents/README.md` | -| Next.js | `websites/website/docs/guides/nextjs.md` | `wrappers/nextjs/README.md` | - -Nuxt (`websites/website/docs/guides/nuxt.md`) also included — cross-check against `wrappers/nuxt3/README.md` + `wrappers/nuxt4/README.md`. - -**Audit checklist per file** (compare guide page vs wrapper README): - -1. **`theme` prop/attribute**: Does the guide document the `theme` prop? Does it mention `@tsparticles/plugin-themes` is optional? Is the safe no-op behavior documented? -2. **Reactivity contract**: Does the guide document that `id`/`options`/`url` changes trigger destroy+reload? -3. **`particlesLoaded` / loaded callback**: Does the guide show the callback? Is the type `(container?: Container)` or similar? -4. **Destroy on teardown**: Is cleanup documented? -5. **Stale patterns**: Any `:init`, `@particles-init`, `particlesInit` function names, or other stale v3 patterns? -6. **Init approach**: Does the guide show the correct v4 init (plugin/provider) rather than component-level init? -7. **Demo alignment**: Do code examples match what the actual wrapper API looks like? - -**Output contract** (per-file, return this structure): - -```md -## {wrapper name} - -**Guide file**: `websites/website/docs/guides/{name}.md` -**Reference README**: `wrappers/{name}/README.md` - -### Gap assessment -| Check | Status | Evidence | -|-------|--------|----------| -| `theme` prop documented | ✅/❌/Partial | ... | -| Reactivity contract | ✅/❌/Partial | ... | -| Loaded callback | ✅/❌/Partial | ... | -| Destroy on teardown | ✅/❌/Partial | ... | -| No stale patterns | ✅/❌/Partial | ... | -| Correct v4 init | ✅/❌/Partial | ... | -| Demo alignment | ✅/❌/N/A | ... | - -### Known gaps (if any) -- {gap description} → {fix needed} - -### Extent of changes needed -- {none / minor / significant} -- {estimated lines changed} -``` - -**No files should be edited by S22.** S22 only produces the gap matrix. - ---- - -### S23 — Translation Alignment (execution step, only if S22 finds gaps) - -**Goal**: Mirror S22 fixes across all 9 translation directories. - -**Files to edit**: For each wrapper guide with gaps from S22, edit `websites/website/docs/{zh,ja,hi,ru,pt,fr,de,es,it}/guides/{name}.md`. - -**Rule**: Preserve translated prose; update only code blocks and structural sections. - -**Only activate S23 if S22's gap matrix is non-empty.** If all guides are clean, mark S23 as `N/A`. diff --git a/package.json b/package.json index 694ba6e7a60..e4c2c49a5e5 100644 --- a/package.json +++ b/package.json @@ -77,5 +77,5 @@ "webpack-cli": "^7.0.3", "yargs": "^18.0.0" }, - "packageManager": "pnpm@11.7.0" + "packageManager": "pnpm@11.8.0" } diff --git a/websites/website/docs/de/migrations/from-v3.md b/websites/website/docs/de/migrations/from-v3.md index 8bc0157e60a..9b4465bd441 100644 --- a/websites/website/docs/de/migrations/from-v3.md +++ b/websites/website/docs/de/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/es/migrations/from-v3.md b/websites/website/docs/es/migrations/from-v3.md index 1d3a2bbed0b..b810ab74331 100644 --- a/websites/website/docs/es/migrations/from-v3.md +++ b/websites/website/docs/es/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/fr/migrations/from-v3.md b/websites/website/docs/fr/migrations/from-v3.md index fdb50bd3b1d..b01be5aa803 100644 --- a/websites/website/docs/fr/migrations/from-v3.md +++ b/websites/website/docs/fr/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/hi/migrations/from-v3.md b/websites/website/docs/hi/migrations/from-v3.md index d592c66d866..3f823d74bd4 100644 --- a/websites/website/docs/hi/migrations/from-v3.md +++ b/websites/website/docs/hi/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/it/migrations/from-v3.md b/websites/website/docs/it/migrations/from-v3.md index 238a6897620..ed3e4bbdede 100644 --- a/websites/website/docs/it/migrations/from-v3.md +++ b/websites/website/docs/it/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/ja/migrations/from-v3.md b/websites/website/docs/ja/migrations/from-v3.md index 953dd3cb97b..ced9bc1ecd8 100644 --- a/websites/website/docs/ja/migrations/from-v3.md +++ b/websites/website/docs/ja/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/migrations/from-v3.md b/websites/website/docs/migrations/from-v3.md index 3af8f0c8758..306c979d02a 100644 --- a/websites/website/docs/migrations/from-v3.md +++ b/websites/website/docs/migrations/from-v3.md @@ -35,7 +35,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/pt/migrations/from-v3.md b/websites/website/docs/pt/migrations/from-v3.md index 9e06ebc8190..93ff5d7052c 100644 --- a/websites/website/docs/pt/migrations/from-v3.md +++ b/websites/website/docs/pt/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/ru/migrations/from-v3.md b/websites/website/docs/ru/migrations/from-v3.md index 11a37040800..95fca4e70e4 100644 --- a/websites/website/docs/ru/migrations/from-v3.md +++ b/websites/website/docs/ru/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2, diff --git a/websites/website/docs/zh/migrations/from-v3.md b/websites/website/docs/zh/migrations/from-v3.md index 5515ba03561..427bca160ca 100644 --- a/websites/website/docs/zh/migrations/from-v3.md +++ b/websites/website/docs/zh/migrations/from-v3.md @@ -45,7 +45,7 @@ const options = { particles: { paint: { fill: { - value: "#ff0000", + color: "#ff0000", }, stroke: { width: 2,