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.
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
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");
}
});
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.