Plugins et personnalisation
tsParticles peut etre etendu a l'execution avec des formes personnalisees, des presets et des plugins.
Ce guide se concentre sur la voie rapide : ajouter un comportement personnalise directement dans une application, sans creer d'abord un package standalone complet.
Carte de decision rapide
- Utilisez une shape personnalisee quand vous avez seulement besoin d'une nouvelle primitive de dessin.
- Utilisez un preset personnalise quand vous voulez reutiliser un objet d'options complet.
- Utilisez un plugin quand vous avez besoin de logique d'execution (cycle de vie du conteneur, comportement personnalise, analyse des options).
Tous les types d'extension en un coup d'oeil
La personnalisation de tsParticles est plus large que les seuls plugins personnalises.
- Bundle : chargeur groupe qui enregistre de nombreuses fonctionnalites d'un coup (
slim,basic,all). - Effect : effet de rendu des particules (
particles.effect). - Interaction : comportement entre particules et evenements ; separe en
external(souris/touch) etparticles(particule-particule). - Palette : profil reutilisable de style/couleurs (
particles.palette). - Path : generateur de trajectoire pour le mouvement des particules (
particles.move.path). - Plugin : module de fonctionnalite du conteneur/runtime (par exemple emitters, absorbers, polygon mask).
- Preset : profil complet d'options reutilisable (
preset). - Shape : primitive de dessin de particules (
particles.shape.type). - Updater : mise a jour par frame des proprietes des particules (tilt, roll, twinkle, opacity, size, etc.).
Si vous expliquez ces categories aux utilisateurs, ils comprennent immediatement jusqu'ou la personnalisation peut aller.
Tableau recapitulatif
| Type | Creation rapide (locale a l'app) | Utilisation |
|---|---|---|
| Bundle | Composez votre loadAppBundle(engine) et appelez les chargeurs internes | Appelez await loadAppBundle(tsParticles) avant tsParticles.load(...) |
| Effect | Enregistrez avec pluginManager.addEffect("app-*", drawer) | Definissez particles.effect.type avec l'id de votre effect |
| Interaction | Enregistrez avec pluginManager.addInteractor("app-*", interactor) | Activez dans interactivity.events / verifications optionnelles de mode personnalise |
| Palette | Enregistrez avec pluginManager.addPalette("app-*", palette) | Definissez particles.palette avec l'id de votre palette |
| Path | Enregistrez avec pluginManager.addPathGenerator("app-*", generator) | Definissez particles.move.path.generator avec l'id de votre path |
| Plugin | Creez IPlugin + IContainerPlugin et appelez engine.addPlugin(...) | Activez via les options du plugin et les hooks du cycle de vie |
| Preset | Enregistrez avec tsParticles.addPreset("app-*", options) | Definissez le preset racine |
| Shape | Enregistrez avec tsParticles.addShape("app-*", drawer) ou chargez tous les packages shape officiels | Definissez particles.shape.type et les options par shape dans particles.shape.options |
| Updater | Enregistrez avec pluginManager.addParticleUpdater("app-*", updater) | S'execute automatiquement sur les particules ou isEnabled(...) renvoie true |
Creation locale rapide + utilisation par type d'extension
Tous les extraits supposent cet ordre de configuration :
await loadSlim(tsParticles);
// register custom pieces
await tsParticles.load({ id: "tsparticles", options });Bundle
Creez un petit bundle d'application qui connecte exactement les elements souhaites.
import type { Engine } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim";
export async function loadAppBundle(engine: Engine): Promise<void> {
await loadSlim(engine);
await Promise.all([
loadAppShape(engine),
loadAppPreset(),
loadAppPalette(engine),
loadAppEffect(engine),
loadAppPath(engine),
loadAppUpdater(engine),
loadAppInteraction(engine),
loadAppPlugin(engine),
]);
}
await loadAppBundle(tsParticles);Effect
import type { Engine } from "@tsparticles/engine";
export async function loadAppEffect(engine: Engine): Promise<void> {
await engine.pluginManager.register((e) => {
e.pluginManager.addEffect("app-fade", () =>
Promise.resolve({
drawBefore: ({ context }) => {
context.save();
context.globalAlpha *= 0.85;
},
drawAfter: ({ context }) => {
context.restore();
},
}),
);
});
}
await loadAppEffect(tsParticles);
const options = {
particles: {
effect: {
type: "app-fade",
},
},
};Interactions (external et particles)
import {
ExternalInteractorBase,
loadInteractivityPlugin,
type IInteractivityData,
} from "@tsparticles/plugin-interactivity";
import type { Engine, IDelta } from "@tsparticles/engine";
class AppHoverPauseInteractor extends ExternalInteractorBase {
readonly maxDistance = 0;
clear(): void {}
init(): void {}
interact(interactivityData: IInteractivityData, _delta: IDelta): void {
if (interactivityData.pointer?.position) {
this.container.pause();
}
}
isEnabled(interactivityData: IInteractivityData): boolean {
return !!interactivityData.pointer?.position;
}
reset(): void {
this.container.play();
}
}
export async function loadAppInteraction(engine: Engine): Promise<void> {
await loadInteractivityPlugin(engine);
await engine.pluginManager.register((e) => {
e.pluginManager.addInteractor?.("app-hover-pause", (container) => {
return Promise.resolve(new AppHoverPauseInteractor(container));
});
});
}
await loadAppInteraction(tsParticles);
const options = {
interactivity: {
events: {
onHover: {
enable: true,
},
},
},
};Palette
import type { Engine, IPalette } from "@tsparticles/engine";
const appPalette: IPalette = {
name: "App Sunset",
blendMode: "multiply",
colors: {
fill: {
enable: true,
value: ["#ff6b6b", "#ffd166", "#4ecdc4"],
},
},
};
export async function loadAppPalette(engine: Engine): Promise<void> {
await engine.pluginManager.register((e) => {
e.pluginManager.addPalette("app-sunset", appPalette);
});
}
await loadAppPalette(tsParticles);
const options = {
particles: {
palette: "app-sunset",
},
};Path
import { loadMovePlugin } from "@tsparticles/plugin-move";
import { Vector, type Engine } from "@tsparticles/engine";
export async function loadAppPath(engine: Engine): Promise<void> {
await loadMovePlugin(engine);
await engine.pluginManager.register((e) => {
e.pluginManager.addPathGenerator?.("app-sway", () =>
Promise.resolve({
generate: (particle) => {
const wave = Math.sin(particle.position.y * 0.02);
return Vector.create(wave, 0);
},
init: () => {},
reset: () => {},
update: () => {},
}),
);
});
}
await loadAppPath(tsParticles);
const options = {
particles: {
move: {
enable: true,
path: {
enable: true,
generator: "app-sway",
},
},
},
};Plugin
import type { Container, Engine, IContainerPlugin, IPlugin, ISourceOptions, Options } from "@tsparticles/engine";
class AppPluginInstance implements IContainerPlugin {
private readonly container: Container;
constructor(container: Container) {
this.container = container;
}
async init(): Promise<void> {
this.container.retina.pixelRatio = Math.max(this.container.retina.pixelRatio, 1);
}
}
class AppPlugin implements IPlugin {
readonly id = "app-plugin";
async getPlugin(container: Container): Promise<IContainerPlugin> {
return new AppPluginInstance(container);
}
loadOptions(_options: Options, source?: ISourceOptions): void {
if (source?.appPlugin === false) {
return;
}
}
needsPlugin(source?: ISourceOptions): boolean {
return source?.appPlugin !== false;
}
}
export async function loadAppPlugin(engine: Engine): Promise<void> {
await engine.addPlugin(new AppPlugin());
}
await loadAppPlugin(tsParticles);
const options = {
appPlugin: true,
};Preset
import { tsParticles } from "@tsparticles/engine";
export async function loadAppPreset(): Promise<void> {
tsParticles.addPreset("app-hero", {
fpsLimit: 60,
particles: {
number: { value: 80 },
move: { enable: true, speed: 2 },
links: { enable: true, distance: 140 },
},
});
}
await loadAppPreset();
const options = {
preset: "app-hero",
};Shape
import type { Engine } from "@tsparticles/engine";
import { loadArrowShape } from "@tsparticles/shape-arrow";
import { loadCardsShape } from "@tsparticles/shape-cards";
import { loadCircleShape } from "@tsparticles/shape-circle";
import { loadCogShape } from "@tsparticles/shape-cog";
import { loadEmojiShape } from "@tsparticles/shape-emoji";
import { loadHeartShape } from "@tsparticles/shape-heart";
import { loadImageShape, type ImageEngine } from "@tsparticles/shape-image";
import { loadInfinityShape } from "@tsparticles/shape-infinity";
import { loadLineShape } from "@tsparticles/shape-line";
import { loadMatrixShape } from "@tsparticles/shape-matrix";
import { loadPathShape } from "@tsparticles/shape-path";
import { loadPolygonShape } from "@tsparticles/shape-polygon";
import { loadRoundedPolygonShape } from "@tsparticles/shape-rounded-polygon";
import { loadRoundedRectShape } from "@tsparticles/shape-rounded-rect";
import { loadSpiralShape } from "@tsparticles/shape-spiral";
import { loadSquareShape } from "@tsparticles/shape-square";
import { loadSquircleShape } from "@tsparticles/shape-squircle";
import { loadStarShape } from "@tsparticles/shape-star";
import { loadTextShape } from "@tsparticles/shape-text";
export async function loadAppShape(engine: Engine): Promise<void> {
await Promise.all([
loadArrowShape(engine),
loadCardsShape(engine),
loadCircleShape(engine),
loadCogShape(engine),
loadEmojiShape(engine),
loadHeartShape(engine),
loadImageShape(engine as ImageEngine),
loadInfinityShape(engine),
loadLineShape(engine),
loadMatrixShape(engine),
loadPathShape(engine),
loadPolygonShape(engine),
loadRoundedPolygonShape(engine),
loadRoundedRectShape(engine),
loadSpiralShape(engine),
loadSquareShape(engine),
loadSquircleShape(engine),
loadStarShape(engine),
loadTextShape(engine),
]);
}
await loadAppShape(tsParticles);
const options = {
particles: {
paint: {
stroke: {
width: 2,
},
},
shape: {
type: [
"arrow",
"card",
"circle",
"club",
"cog",
"diamond",
"emoji",
"heart",
"hearts",
"image",
"images",
"infinity",
"line",
"matrix",
"path",
"polygon",
"rounded-polygon",
"rounded-rect",
"spade",
"spades",
"spiral",
"edge",
"square",
"squircle",
"star",
"text",
"character",
"char",
"multiline-text",
"triangle",
"clubs",
"diamonds",
],
options: {
image: {
src: "https://particles.js.org/images/hdr/fruits/cherry.png",
width: 32,
height: 32,
replaceColor: false,
},
line: {
close: false,
fill: false,
},
path: {
close: true,
d: "M 0,-14 L 10,14 L -10,14 Z",
},
polygon: {
sides: 6,
},
"rounded-polygon": {
sides: 6,
radius: 0.25,
},
"rounded-rect": {
width: 20,
height: 14,
radius: 3,
},
spiral: {
innerRadius: 1,
lineSpacing: 1,
},
star: {
sides: 5,
inset: 2,
},
text: {
value: ["TS", "Particles"],
font: "Verdana",
},
},
},
},
};La shape line est pilotee par le stroke, donc gardez fill: false et configurez particles.paint.stroke.
L'URL image.src ci-dessus est reutilisee depuis les configurations existantes du projet (utils/configs).
Updater
import type { Engine, IDelta, Particle } from "@tsparticles/engine";
export async function loadAppUpdater(engine: Engine): Promise<void> {
await engine.pluginManager.register((e) => {
e.pluginManager.addParticleUpdater("app-drift", () =>
Promise.resolve({
init: (): void => {},
isEnabled: (): boolean => true,
update: (particle: Particle, delta: IDelta): void => {
particle.position.x += 0.02 * delta.factor;
},
}),
);
});
}
await loadAppUpdater(tsParticles);
// no extra options required: updater runs when isEnabled(...) is trueCela suffit pour prototyper localement chaque type d'extension, puis extraire ensuite vers des packages dedies.
Strategie de composition
- Commencez avec un seul bundle (
slimest generalement suffisant). - Ajoutez les capacites manquantes sous forme de petits modules cibles (interaction/updater/path/effect/shape).
- Utilisez preset pour reutiliser le comportement et palette pour reutiliser l'identite visuelle.
- Gardez d'abord les extensions personnalisees en local dans l'app, puis publiez seulement en cas de reutilisation entre projets.
Regles pratiques
- Gardez des noms d'extension uniques (par exemple
app-*ou un prefixe d'entreprise). - Commencez en local dans l'app, puis extrayez vers un package uniquement en cas de reutilisation dans plusieurs projets.
- Conservez une petite fixture de configuration pendant le developpement (verifications de regression plus rapides).
- Si une fonctionnalite manque, verifiez que le package requis est bien charge (shape, interaction, updater, plugin).
References
- Documentation de l'interface plugin : https://particles.js.org/docs/modules/Core_Interfaces_IPlugin.html
- Guide markdown etendu : https://github.com/tsparticles/tsparticles/blob/main/markdown/Plugins.md
