I. Choix de Solution: Trois Approches Principales
| Approche | Principe | Avantages | Inconvénients | Cas d'usage |
|---|---|---|---|---|
| Canvas Natif | Appel direct aux Canvas API côté client | Flexible, aperçu en temps réel, pas de serveur nécessaire | Code complexe, problèmes de compatibilité | Posters hautement personnalisés |
| Composant Painter | Piloté par configuraiton JSON, encapsulation Canvas | Simple à prendre en main, syntaxe CSS-like | Personnalisation limitée, maintenance réduite | Modèles de posters standardisés |
| Génération Serveur | Backend utilise des bibliothèques comme Node-canvas | Style parfaitement cohérent, pas de charge client | Coût serveur, pas d'aperçu en temps réel | Campagnes marketing à fort trafic |
Pour les modèles simples, utilisez Painter ; pour les personnalisations copmlexes, Canvas natif ; pour les hauts volumes, génération côté serveur.
II. Dessin avec Canvas Natif: Processus en Trois Étapes
2.1 Lier le Nœud Canvas
La nouvelle interface Canvas 2D (bibliothèque 2.9.0+) nécessite d'utiliser la méthode node :
<canvas type="2d" id="afficheCanvas" style="width: 300px; height: 400px;"></canvas>
Page({
data: {
largeurCanvas: 300,
hauteurCanvas: 400,
ratioPixel: 1
},
onLoad() {
// Obtenir le ratio de pixels pour un affichage haute définition
const sysInfo = wx.getSystemInfoSync();
this.setData({ ratioPixel: sysInfo.ratioPixel });
// Initialiser le Canvas
this.initialiserCanvas();
},
initialiserCanvas() {
const query = wx.createSelectorQuery();
query.select('#afficheCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// Définir les dimensions haute définition
const dpr = this.data.ratioPixel;
canvas.width = this.data.largeurCanvas * dpr;
canvas.height = this.data.hauteurCanvas * dpr;
// Mettre à l'échelle pour les écrans HD
ctx.scale(dpr, dpr);
this.canvas = canvas;
this.ctx = ctx;
// Commencer le dessin
this.dessinerAffiche();
});
}
});
2.2 Dessiner le Contenu
Les images réseau doivent être téléchargées localement avant d'être utilisées dans Canvas :
async dessinerAffiche() {
const ctx = this.ctx;
const { largeurCanvas, hauteurCanvas } = this.data;
// 1. Dessiner l'arrière-plan
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, largeurCanvas, hauteurCanvas);
// 2. Dessiner l'image de fond (nécessite un téléchargement préalable)
await this.ajouterImage('https://exemple.com/fond.jpg');
// 3. Dessiner l'avatar utilisateur (découpage circulaire)
await this.ajouterImageRonde('https://exemple.com/avatar.jpg', 20, 20, 60);
// 4. Dessiner le texte
ctx.fillStyle = '#333333';
ctx.font = '16px "PingFang SC", sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Pseudo utilisateur', 90, 50);
// 5. Dessiner un texte multiligne
this.dessinerTexteMultiligne('Contenu du texte qui doit être automatiquement renvoyé à la ligne en fonction de la largeur.', 20, 100, largeurCanvas - 40, 24);
// 6. Dessiner le QR code
await this.ajouterImage('https://exemple.com/qrcode.jpg', 230, 320, 60, 60);
}
// Méthode utilitaire: ajouter une image standard
ajouterImage(src, x, y, w, h) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: (res) => {
const img = this.canvas.createImage();
img.src = res.path;
img.onload = () => {
this.ctx.drawImage(img, x, y, w, h);
resolve();
};
img.onerror = reject;
},
fail: reject
});
});
}
// Méthode utilitaire: ajouter une image circulaire
ajouterImageRonde(src, x, y, taille) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: (res) => {
const img = this.canvas.createImage();
img.src = res.path;
img.onload = () => {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x + taille/2, y + taille/2, taille/2, 0, Math.PI * 2);
this.ctx.clip();
this.ctx.drawImage(img, x, y, taille, taille);
this.ctx.restore();
resolve();
};
img.onerror = reject;
},
fail: reject
});
});
}
// Méthode utilitaire: dessiner un texte avec retour automatique à la ligne
dessinerTexteMultiligne(texte, x, y, largeurMax, hauteurLigne) {
const ctx = this.ctx;
let ligne = '';
let yActuel = y;
for (let i = 0; i < texte.length; i++) {
const testLigne = ligne + texte[i];
const metrics = ctx.measureText(testLigne);
if (metrics.width > largeurMax && i > 0) {
ctx.fillText(ligne, x, yActuel);
ligne = texte[i];
yActuel += hauteurLigne;
} else {
ligne = testLigne;
}
}
ctx.fillText(ligne, x, yActuel);
}
2.3 Exporter en Image
Une fois le dessin terminé, exporter en fichier temporaire :
exporterAffiche() {
wx.canvasToTempFilePath({
canvas: this.canvas, // Pour la nouvelle API, passer l'instance canvas
fileType: 'jpg',
quality: 0.9,
success: (res) => {
this.setData({ cheminAffiche: res.tempFilePath });
console.log('Affiche générée avec succès:', res.tempFilePath);
},
fail: (err) => {
console.error('Échec de l\'export:', err);
}
});
}
III. Sauvegarde dans la Galerie: La Gestion des Permissions
La sauvegarde d'images nécessite la permission scope.writePhotosAlbum. Le traitement du refus par l'utilisateur est un point crucial :
sauvegarderDansGalerie() {
const cheminAffiche = this.data.cheminAffiche;
if (!cheminAffiche) {
wx.showToast({ title: 'Veuillez d\'abord générer l\'affiche', icon: 'none' });
return;
}
// Vérifier d'abord l'état des permissions
wx.getSetting({
success: (res) => {
// Déjà autorisé: sauvegarder directement
if (res.authSetting['scope.writePhotosAlbum']) {
this.effectuerSauvegarde(cheminAffiche);
return;
}
// Non autorisé: demander la permission
wx.authorize({
scope: 'scope.writePhotosAlbum',
success: () => {
this.effectuerSauvegarde(cheminAffiche);
},
fail: () => {
// Après un refus, authorize ne réaffichera pas la popup
// Il faut guider manuellement vers les paramètres
this.afficherModalAutorisation();
}
});
}
});
}
effectuerSauvegarde(cheminFichier) {
wx.saveImageToPhotosAlbum({
filePath: cheminFichier,
success: () => {
wx.showToast({ title: 'Sauvegarde réussie', icon: 'success' });
},
fail: (err) => {
// Gérer différents cas d'échec
if (err.errMsg.includes('auth deny')) {
this.afficherModalAutorisation();
}
}
});
}
afficherModalAutorisation() {
wx.showModal({
title: 'Permission de galerie requise',
content: 'Veuillez activer la permission de sauvegarder des images dans la galerie depuis les paramètres',
confirmText: 'Aller aux paramètres',
success: (res) => {
if (res.confirm) {
// Ouvrir la page des paramètres
wx.openSetting({
success: (setRes) => {
if (setRes.authSetting['scope.writePhotosAlbum']) {
this.effectuerSauvegarde(this.data.cheminAffiche);
}
}
});
}
}
});
}
IV. Partage sur Moments WeChat: Configuration et Guide
Les mini-programmes ne peuvent pas déclencher le partage sur Moments WeChat par programmation - l'utilisateur doit cliquer manuellement sur le menu en haut à droite.
4.1 Activer le menu de partage
onLoad() {
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'] // Doit inclure shareTimeline
});
}
4.2 Définir le contenu de partage
// Partager avec des amis
onShareAppMessage() {
return {
title: 'Ce mini-programme est génial',
path: '/pages/index/index',
imageUrl: '/assets/share.png'
};
}
// Partager sur Moments
onShareTimeline() {
return {
title: 'Je recommande ce mini-programme',
query: 'from=timeline', // Pour le suivi des données, n'affecte pas le routage
imageUrl: '/assets/timeline.png' // Format recommandé 5:4, max 1MB
};
}
4.3 Ajouter un bouton pour guider l'utilisateur
<button class="btn-partage" open-type="share">Partager avec des amis</button>
V. Conseils d'Évitage: Expériences et Pièges Courants
Piège 1: Export de Canvas vide
Cause: draw() est asynchrone, il faut attendre la fin du dessin avant d'exporter.
// ❌ Incorrect: appeler l'export immédiatement
ctx.draw();
wx.canvasToTempFilePath({ ... });
// ✅ Correct: utiliser callback ou setTimeout
ctx.draw();
setTimeout(() => {
wx.canvasToTempFilePath({ ... });
}, 300);
Piège 2: Images floues sur écrans HD
Cause: Le pixel réel du Canvas est le pixel logique × ratioPixel, mais la taille de style est la taille logique.
// ❌ Incorrect: définir directement la taille logique
canvas.width = 300;
canvas.height = 400;
// ✅ Correct: mettre à l'échelle selon le ratio de pixels
const dpr = wx.getSystemInfoSync().ratioPixel;
canvas.width = 300 * dpr;
canvas.height = 400 * dpr;
ctx.scale(dpr, dpr); // Également mettre à l'échelle le dessin
Piège 3: Échec du téléchargement d'images réseau
Cause: Le domaine n'est pas dans la liste blanche des téléchargements.
Solution: Se connecter à la plateforme WeChat → Gestion du développement → Domaines serveur → Ajouter le domaine d'images à downloadFile legal domain.
Piège 4: Extension de fichier incorrecte sur Android
Cause: Certains appareils Android retournent un tempFilePath sans extension dans downloadFile.
// ✅ Spécifier filePath avec extension forcée
const filePath = wx.env.USER_DATA_PATH + '/affiche.jpg';
wx.downloadFile({
url: urlAffiche,
filePath, // Spécifier le chemin local et l'extension
success: (res) => {
wx.saveImageToPhotosAlbum({ filePath: res.filePath });
}
});
Piège 5: Positionnement de retour à la ligne incorrect sur iOS
Cause: ctx.measureText() se comporte différemment sur iOS et Android.
Solution: Laisser une marge dans le calcul de retour à la ligne, par exemple largeur * 0.95.
Piège 6: Après un refus, la permission ne demande plus
Cause: Conception de WeChat, après un refus, authorize échoue silencieusement.
Solution: Il faut utiliser <button open-type="openSetting"> ou wx.openSetting() pour guider manuellement.
Piège 7: Limitations du mode page unique Moments
Après partage sur Moments, l'ouvre dans un "mode page unique" avec plusieurs restrictions:
- Interfaces de connexion
wx.login() - Sélection d'images
wx.chooseImage() - Sauvegarde d'images
wx.saveImageToPhotosAlbum() - Navigation entre pages
wx.navigateTo() - Composants tabBar
Solution: Vérifier via le scénario scene === 1154 et masquer les fonctionnalités indisponibles en mode page unique.
VI. Exemple Complet: Génération + Sauvegarde + Partage
// pages/affiche/affiche.js
Page({
data: {
cheminAffiche: '',
largeurCanvas: 300,
hauteurCanvas: 420,
ratioPixel: 1
},
onLoad() {
this.setData({ ratioPixel: wx.getSystemInfoSync().ratioPixel });
this.initialiserCanvas();
// Activer le menu de partage
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
});
},
async initialiserCanvas() {
const query = wx.createSelectorQuery();
query.select('#affiche')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = this.data.ratioPixel;
canvas.width = this.data.largeurCanvas * dpr;
canvas.height = this.data.hauteurCanvas * dpr;
ctx.scale(dpr, dpr);
this.canvas = canvas;
this.ctx = ctx;
this.genererAffiche();
});
},
async genererAffiche() {
wx.showLoading({ title: 'Génération en cours...' });
try {
// Arrière-plan
this.ctx.fillStyle = '#f5f5f5';
this.ctx.fillRect(0, 0, this.data.largeurCanvas, this.data.hauteurCanvas);
// Télécharger et dessiner l'image de fond
await this.telechargerEtAjouterImage('https://exemple.com/fond.jpg', 0, 0, 300, 200);
// Dessiner l'avatar
await this.telechargerEtAjouterImageRonde('https://exemple.com/avatar.jpg', 20, 220, 50);
// Dessiner le texte
this.ctx.fillStyle = '#333';
this.ctx.font = '14px sans-serif';
this.ctx.fillText('Pseudo utilisateur', 80, 245);
// Dessiner le texte descriptif
this.dessinerTexteMultiligne('Générez votre affiche personnalisée et invitez vos amis à rejoindre', 20, 290, 260, 20);
// Dessiner le QR code
await this.telechargerEtAjouterImage('https://exemple.com/qr.jpg', 110, 320, 80, 80);
wx.hideLoading();
this.exporterAffiche();
} catch (err) {
wx.hideLoading();
wx.showToast({ title: 'Échec de la génération', icon: 'none' });
}
},
telechargerEtAjouterImage(src, x, y, w, h) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: (res) => {
const img = this.canvas.createImage();
img.src = res.path;
img.onload = () => {
this.ctx.drawImage(img, x, y, w, h);
resolve();
};
img.onerror = reject;
},
fail: reject
});
});
},
telechargerEtAjouterImageRonde(src, x, y, taille) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: (res) => {
const img = this.canvas.createImage();
img.src = res.path;
img.onload = () => {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x + taille/2, y + taille/2, taille/2, 0, Math.PI * 2);
this.ctx.clip();
this.ctx.drawImage(img, x, y, taille, taille);
this.ctx.restore();
resolve();
};
img.onerror = reject;
},
fail: reject
});
});
},
dessinerTexteMultiligne(texte, x, y, largeurMax, hauteurLigne) {
const ctx = this.ctx;
let ligne = '';
let yActuel = y;
for (let i = 0; i < texte.length; i++) {
const testLigne = ligne + texte[i];
if (ctx.measureText(testLigne).width > largeurMax && i > 0) {
ctx.fillText(ligne, x, yActuel);
ligne = texte[i];
yActuel += hauteurLigne;
} else {
ligne = testLigne;
}
}
ctx.fillText(ligne, x, yActuel);
},
exporterAffiche() {
wx.canvasToTempFilePath({
canvas: this.canvas,
fileType: 'jpg',
quality: 0.9,
success: (res) => {
this.setData({ cheminAffiche: res.tempFilePath });
}
});
},
sauvegarderAffiche() {
const chemin = this.data.cheminAffiche;
if (!chemin) return;
wx.getSetting({
success: (res) => {
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({
scope: 'scope.writePhotosAlbum',
success: () => this._effectuerSauvegarde(chemin),
fail: () => this._afficherModalAutorisation()
});
} else {
this._effectuerSauvegarde(chemin);
}
}
});
},
_effectuerSauvegarde(cheminFichier) {
wx.saveImageToPhotosAlbum({
filePath: cheminFichier,
success: () => wx.showToast({ title: 'Sauvegardé avec succès', icon: 'success' }),
fail: (err) => err.errMsg.includes('deny') && this._afficherModalAutorisation()
});
},
_afficherModalAutorisation() {
wx.showModal({
title: 'Permission de galerie requise',
content: 'Veuillez activer la permission de sauvegarder des images dans la galerie',
confirmText: 'Aller aux paramètres',
success: (res) => res.confirm && wx.openSetting()
});
},
guiderPartageTimeline() {
wx.showToast({ title: 'Veuillez cliquer sur ··· en haut à droite pour partager sur Moments', icon: 'none' });
},
onShareAppMessage() {
return {
title: 'Générez votre affiche personnalisée',
path: '/pages/affiche/affiche',
imageUrl: this.data.cheminAffiche
};
},
onShareTimeline() {
return {
title: 'Générez votre affiche personnalisée',
query: 'from=timeline',
imageUrl: this.data.cheminAffiche
};
}
});