Skip to content

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) et particles (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

TypeCreation rapide (locale a l'app)Utilisation
BundleComposez votre loadAppBundle(engine) et appelez les chargeurs internesAppelez await loadAppBundle(tsParticles) avant tsParticles.load(...)
EffectEnregistrez avec pluginManager.addEffect("app-*", drawer)Definissez particles.effect.type avec l'id de votre effect
InteractionEnregistrez avec pluginManager.addInteractor("app-*", interactor)Activez dans interactivity.events / verifications optionnelles de mode personnalise
PaletteEnregistrez avec pluginManager.addPalette("app-*", palette)Definissez particles.palette avec l'id de votre palette
PathEnregistrez avec pluginManager.addPathGenerator("app-*", generator)Definissez particles.move.path.generator avec l'id de votre path
PluginCreez IPlugin + IContainerPlugin et appelez engine.addPlugin(...)Activez via les options du plugin et les hooks du cycle de vie
PresetEnregistrez avec tsParticles.addPreset("app-*", options)Definissez le preset racine
ShapeEnregistrez avec tsParticles.addShape("app-*", drawer) ou chargez tous les packages shape officielsDefinissez particles.shape.type et les options par shape dans particles.shape.options
UpdaterEnregistrez 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 :

ts
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.

ts
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

ts
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)

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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 true

Cela suffit pour prototyper localement chaque type d'extension, puis extraire ensuite vers des packages dedies.

Strategie de composition

  • Commencez avec un seul bundle (slim est 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