Chapitre 08

Canvas et Interactions Visuelles

Le Canvas est le coeur graphique de FoundryVTT. Apprenez a etendre les Tokens, MeasuredTemplates et autres elements visuels pour personnaliser l'experience de jeu.

Avance ~30 min de lecture

Introduction

Le Canvas est le composant central de l'interface de FoundryVTT. C'est la zone de jeu ou les joueurs interagissent avec la scene, deplacent leurs personnages et visualisent les effets. Techniquement, Foundry utilise PIXI.js, une bibliotheque de rendu 2D performante basee sur WebGL.

💡
PIXI.js dans FoundryVTT

Foundry encapsule PIXI.js pour fournir une API de plus haut niveau. Vous n'avez pas besoin de connaitre PIXI en detail, mais comprendre les concepts de base (Sprites, Containers, Graphics) est utile pour les extensions avancees.

Le canvas contient differents layers organises hierarchiquement, chacun gerant un type d'element visuel specifique. Les elements visuels sont appeles Placeables et sont lies a des Documents qui stockent leurs donnees.

Architecture du Canvas

Le canvas est organise en couches (layers) qui se superposent pour creer l'affichage final.

Hierarchie des Layers

📜
Ordre des layers (de bas en haut)
1. BackgroundLayer     --> Images de fond, tiles statiques
2. DrawingsLayer       --> Dessins vectoriels (cercles, rectangles...)
3. TemplateLayer       --> Zones d'effet (MeasuredTemplates)
4. TokenLayer          --> Tokens des acteurs
5. LightingLayer       --> Effets de lumiere
6. SightLayer          --> Vision et brouillard de guerre
7. EffectsLayer        --> Effets visuels (animations)
8. ControlsLayer       --> Controles d'interface (rulers, etc.)
9. HUDLayer            --> Interface utilisateur (barres de vie, etc.)

Acces aux Layers

// Acceder au canvas principal
const canvas = game.canvas;

// Acceder a un layer specifique
const tokenLayer = canvas.tokens;
const templateLayer = canvas.templates;
const drawingsLayer = canvas.drawings;

// Verifier si un layer est actif
if (canvas.tokens.active) {
  console.log("Le layer des tokens est actif");
}

// Activer/desactiver un layer
canvas.tokens.activate();
canvas.templates.deactivate();

Structure d'un Layer

Chaque layer herite de PlaceablesLayer et contient :

  • placeables : Collection des objets visuels (Tokens, Templates, etc.)
  • objects : Container PIXI pour le rendu
  • interactiveChildren : Enfants interactifs (clics, drag)
// Exemple avec TokenLayer
const tokenLayer = canvas.tokens;

// Collection des tokens places
tokenLayer.placeables.forEach(token => {
  console.log(`Token: ${token.name}`);
});

// Container PIXI pour les effets visuels
const effectsContainer = tokenLayer.objects;

// Rendre un token interactif
token.interactive = true;
token.buttonMode = true;

Relation Document/Placeable

Chaque element visuel (Placeable) est lie a un Document qui stocke ses donnees persistantes.

TokenDocument ↔ Token

Document (TokenDocument) Placeable (Token)
Stockage Donnees persistantes en base Representation visuelle temporaire
Classe TokenDocument Token
Acces actor.prototypeToken canvas.tokens.get(tokenId)
Modification document.update() token.draw() ou token.refresh()

Cycle de vie

// 1. Creation d'un token depuis un document
const tokenDoc = actor.prototypeToken;
const token = new Token(tokenDoc);

// 2. Ajout au canvas
canvas.tokens.addChild(token);

// 3. Modification des donnees
await tokenDoc.update({ 
  "x": 1000, 
  "y": 800,
  "system.hp.value": 15 
});

// 4. Le token se met a jour automatiquement
token.refresh();

Synchronisation

Lorsque les donnees du Document changent, le Placeable correspondant se met a jour automatiquement.

// Ecouter les changements de position
Hooks.on("updateToken", (tokenDoc, changes, options, userId) => {
  if (foundry.utils.hasProperty(changes, "x") || 
      foundry.utils.hasProperty(changes, "y")) {
    
    const token = canvas.tokens.get(tokenDoc.id);
    if (token) {
      // Le token s'est deja deplace automatiquement
      console.log(`Token deplace vers ${token.x}, ${token.y}`);
    }
  }
});

Personnalisation des Tokens

Les tokens peuvent etre personnalises de nombreuses facons : apparence, comportements, effets visuels.

Etendre la classe Token

// Creer une classe Token personnalisee
class MonToken extends Token {
  
  // Methode appelee lors du rendu initial
  async _draw() {
    await super._draw();
    
    // Ajouter des elements personnalises
    this._drawAura();
    this._drawStatusEffects();
  }
  
  // Methode appelee lors des mises a jour
  _refresh() {
    super._refresh();
    
    // Mettre a jour les elements personnalises
    this._refreshAura();
  }
  
  // Dessiner une aura personnalisee
  _drawAura() {
    if (!this.auraGraphics) {
      this.auraGraphics = this.addChild(new PIXI.Graphics());
    }
    
    const aura = this.document.getFlag("mon-systeme", "aura");
    if (!aura) return;
    
    this.auraGraphics.clear();
    this.auraGraphics.lineStyle(2, aura.color, 0.8);
    this.auraGraphics.drawCircle(0, 0, aura.radius);
    this.auraGraphics.endFill();
  }
  
  // Mettre a jour l'aura
  _refreshAura() {
    if (this.auraGraphics) {
      this._drawAura();
    }
  }
}

// Enregistrer la classe personnalisee
Hooks.once("init", () => {
  CONFIG.Token.objectClass = MonToken;
});

Barres de ressources personnalisees

// Ajouter une barre personnalisee
class MonToken extends Token {
  
  _drawBar(number, bar, data) {
    // Barre standard (HP)
    if (number === 0) {
      return super._drawBar(number, bar, data);
    }
    
    // Barre personnalisee (Mana)
    if (number === 1) {
      return this._drawManaBar(bar, data);
    }
  }
  
  _drawManaBar(bar, data) {
    const {bg, border} = bar;
    
    // Fond
    bg.clear()
       .beginFill(0x000000, 0.5)
       .drawRoundedRect(0, 0, this.w, 8, 2);
    
    // Bordure
    border.clear()
          .lineStyle(1, 0xFFFFFF, 1)
          .drawRoundedRect(0, 0, this.w, 8, 2);
    
    // Remplissage (bleu pour mana)
    const pct = data.value / data.max;
    bg.beginFill(0x0066CC, 0.8)
       .drawRoundedRect(1, 1, (this.w - 2) * pct, 6, 1);
    
    return true;
  }
}

Animations et effets

// Ajouter une animation de degats
Hooks.on("updateActor", (actor, changes, options, userId) => {
  if (foundry.utils.hasProperty(changes, "system.hp.value")) {
    const token = canvas.tokens.placeables.find(t => t.actor === actor);
    if (!token) return;
    
    const oldHp = foundry.utils.getProperty(changes, "system.hp.value");
    const newHp = actor.system.hp.value;
    const damage = oldHp - newHp;
    
    if (damage > 0) {
      // Animation de degats
      token._playDamageAnimation(damage);
    }
  }
});

// Methode d'animation
MonToken.prototype._playDamageAnimation = function(damage) {
  // Creer un texte flottant
  const text = new PIXI.Text(`-${damage}`, {
    fontFamily: 'Arial',
    fontSize: 24,
    fill: 0xFF0000,
    stroke: 0x000000,
    strokeThickness: 2
  });
  
  text.anchor.set(0.5);
  text.x = this.w / 2;
  text.y = -20;
  
  this.addChild(text);
  
  // Animation
  gsap.to(text, {
    y: -60,
    alpha: 0,
    duration: 1,
    ease: "power2.out",
    onComplete: () => {
      this.removeChild(text);
    }
  });
};

MeasuredTemplates

Les MeasuredTemplates representent les zones d'effet : cones, cercles, rayons, etc.

Types de templates

  • circle : Zone circulaire
  • cone : Cone d'effet
  • rect : Rectangle
  • ray : Ligne droite

Creer un template personnalise

// Creer un template de zone d'effet
const templateData = {
  t: "circle",           // Type: circle, cone, rect, ray
  x: 1000,              // Position X
  y: 800,               // Position Y
  distance: 20,         // Rayon/Distance
  direction: 0,         // Direction (pour cone/ray)
  angle: 90,            // Angle (pour cone)
  fillColor: "#FF0000", // Couleur de remplissage
  fillAlpha: 0.3,       // Transparence
  texture: null         // Texture optionnelle
};

// Creer le document
const templateDoc = await MeasuredTemplateDocument.create(templateData, {
  parent: canvas.scene
});

// Creer le placeable
const template = new MeasuredTemplate(templateDoc);
canvas.templates.addChild(template);

Etendre MeasuredTemplate

// Classe personnalisee pour templates avances
class MonMeasuredTemplate extends MeasuredTemplate {
  
  // Personnaliser le rendu
  _drawShape() {
    super._drawShape();
    
    // Ajouter des effets personnalises
    this._drawCustomBorder();
    this._drawParticles();
  }
  
  // Bordure animee
  _drawCustomBorder() {
    if (!this.borderAnimation) {
      this.borderAnimation = this.addChild(new PIXI.Graphics());
    }
    
    const time = Date.now() * 0.001; // Temps pour animation
    const alpha = 0.5 + 0.3 * Math.sin(time * 2);
    
    this.borderAnimation.clear();
    this.borderAnimation.lineStyle(3, 0x00FF00, alpha);
    this.borderAnimation.drawCircle(0, 0, this.distance * canvas.grid.size);
  }
  
  // Particules dans la zone
  _drawParticles() {
    if (!this.particles) {
      this.particles = [];
      for (let i = 0; i < 10; i++) {
        const particle = new PIXI.Graphics();
        particle.beginFill(0xFFFF00, 0.6);
        particle.drawCircle(0, 0, 2);
        particle.endFill();
        this.addChild(particle);
        this.particles.push(particle);
      }
    }
    
    // Animer les particules
    this.particles.forEach((particle, i) => {
      const angle = (Date.now() * 0.001 + i * 0.5) % (Math.PI * 2);
      const radius = this.distance * canvas.grid.size * 0.8;
      particle.x = Math.cos(angle) * radius;
      particle.y = Math.sin(angle) * radius;
    });
  }
  
  // Mise a jour periodique
  _refresh() {
    super._refresh();
    this._drawCustomBorder();
    this._drawParticles();
  }
}

// Enregistrer la classe
Hooks.once("init", () => {
  CONFIG.MeasuredTemplate.objectClass = MonMeasuredTemplate;
});

Exercice pratique : Aura visuelle autour d'un token

Implementons un systeme d'aura personnalisable autour des tokens.

1. Configuration de l'aura

// module/aura.mjs

/**
 * Configuration des auras
 */
export const AURA_TYPES = {
  divine: {
    color: 0xFFD700,    // Or
    radius: 3,          // 3 cases
    alpha: 0.4,
    animated: true
  },
  necrotic: {
    color: 0x800080,    // Violet
    radius: 2,
    alpha: 0.6,
    animated: false
  },
  fire: {
    color: 0xFF4500,    // Orange rouge
    radius: 1,
    alpha: 0.8,
    animated: true
  }
};

/**
 * Classe Token avec aura
 */
export class AuraToken extends Token {
  
  async _draw() {
    await super._draw();
    this._drawAura();
  }
  
  _refresh() {
    super._refresh();
    this._refreshAura();
  }
  
  _drawAura() {
    // Supprimer l'aura precedente
    if (this.auraContainer) {
      this.removeChild(this.auraContainer);
    }
    
    const auraType = this.document.getFlag("mon-systeme", "aura");
    if (!auraType || !AURA_TYPES[auraType]) return;
    
    const config = AURA_TYPES[auraType];
    this.auraContainer = this.addChild(new PIXI.Container());
    
    // Cercle principal
    const circle = new PIXI.Graphics();
    circle.lineStyle(2, config.color, config.alpha);
    circle.beginFill(config.color, config.alpha * 0.3);
    circle.drawCircle(0, 0, config.radius * canvas.grid.size);
    circle.endFill();
    
    this.auraContainer.addChild(circle);
    
    // Cercle anime si configure
    if (config.animated) {
      this.animatedCircle = new PIXI.Graphics();
      this.auraContainer.addChild(this.animatedCircle);
    }
  }
  
  _refreshAura() {
    if (!this.auraContainer) return;
    
    const auraType = this.document.getFlag("mon-systeme", "aura");
    if (!auraType || !AURA_TYPES[auraType]) {
      this.removeChild(this.auraContainer);
      this.auraContainer = null;
      return;
    }
    
    // Animation du cercle secondaire
    if (this.animatedCircle) {
      const config = AURA_TYPES[auraType];
      const time = Date.now() * 0.002;
      const pulseRadius = config.radius * canvas.grid.size * (1 + 0.1 * Math.sin(time));
      
      this.animatedCircle.clear();
      this.animatedCircle.lineStyle(1, config.color, config.alpha * 0.5);
      this.animatedCircle.drawCircle(0, 0, pulseRadius);
    }
  }
}

/**
 * Fonction pour appliquer une aura
 */
export async function setTokenAura(tokenId, auraType) {
  const tokenDoc = canvas.scene.tokens.get(tokenId);
  if (!tokenDoc) return;
  
  if (auraType && !AURA_TYPES[auraType]) {
    ui.notifications.error(`Type d'aura inconnu: ${auraType}`);
    return;
  }
  
  await tokenDoc.setFlag("mon-systeme", "aura", auraType);
  
  // Rafraichir le token visuel
  const token = canvas.tokens.get(tokenId);
  if (token) {
    token.refresh();
  }
}

/**
 * Hook pour initialiser
 */
Hooks.once("init", () => {
  CONFIG.Token.objectClass = AuraToken;
});

2. Interface utilisateur

// Ajouter un bouton dans le menu contextuel des tokens
Hooks.on("getActorDirectoryEntryContext", (html, options) => {
  options.push({
    name: "Set Divine Aura",
    icon: '',
    condition: game.user.isGM,
    callback: li => {
      const actor = game.actors.get(li.data("actorId"));
      const token = canvas.tokens.placeables.find(t => t.actor === actor);
      if (token) {
        setTokenAura(token.id, "divine");
      }
    }
  });
  
  options.push({
    name: "Remove Aura",
    icon: '',
    condition: game.user.isGM,
    callback: li => {
      const actor = game.actors.get(li.data("actorId"));
      const token = canvas.tokens.placeables.find(t => t.actor === actor);
      if (token) {
        setTokenAura(token.id, null);
      }
    }
  });
});

3. Effets d'aura

// Effets automatiques bases sur l'aura
Hooks.on("updateToken", (tokenDoc, changes, options, userId) => {
  const auraType = tokenDoc.getFlag("mon-systeme", "aura");
  if (!auraType) return;
  
  // Appliquer des effets selon le type d'aura
  if (auraType === "divine") {
    // Aura divine : bonus de sauvegarde
    console.log("Aura divine activee");
  } else if (auraType === "necrotic") {
    // Aura necrotique : zone de degats
    console.log("Aura necrotique activee");
  }
});
Felicitations !

Vous maitrisez maintenant l'extension du Canvas FoundryVTT. Vous pouvez personnaliser les tokens, creer des effets visuels avances et etendre les MeasuredTemplates pour enrichir l'experience de jeu.