Dans cet article
- L’API Canvas HTML5 permet de créer un paint JavaScript complet en moins de 200 lignes de code
- Les événements souris et tactiles couvrent 95 % des interactions nécessaires à un outil de dessin web
- JS Paint, le clone open source le plus abouti, reproduit 100 % des fonctionnalités de MS Paint classique
- Un paint JavaScript performant doit gérer le requestAnimationFrame pour fluidifier le rendu à 60 FPS
- L’export en PNG, JPEG ou SVG s’effectue nativement via canvas.toDataURL() sans librairie externe
- Les bibliothèques comme Fabric.js ou Paint.js réduisent le temps de développement de 60 à 80 %
Sommaire
- Comprendre l’API Canvas HTML5 pour le dessin
- Architecture d’un paint JavaScript from scratch
- Gestion des événements souris et tactile
- Implémenter les outils de dessin essentiels
- Optimisation des performances et du rendu
- Bibliothèques et alternatives pour un paint en ligne
- Export et sauvegarde du canvas
- Quel est l’équivalent de Paint en ligne ?
Après douze ans à développer des applications web sur mesure, je constate que la demande pour des outils de dessin intégrés aux navigateurs n’a jamais été aussi forte. Que ce soit pour annoter des images, créer des maquettes rapides ou proposer un espace créatif aux utilisateurs, le paint JavaScript est devenu un composant incontournable. Dans ce guide, je vous montre comment construire votre propre outil de dessin, étape par étape, en m’appuyant sur l’API Canvas HTML5 et les bonnes pratiques que j’applique au quotidien sur mes projets clients.
Comprendre l’API Canvas HTML5 pour le dessin
L’élément <canvas> est la fondation de tout paint JavaScript. Introduit avec HTML5, il offre une surface de dessin bitmap que l’on manipule exclusivement via JavaScript. Contrairement au SVG qui travaille en vectoriel, le canvas rasterise chaque pixel, ce qui le rend idéal pour un outil de type Paint.
Pour initialiser un canvas de dessin, voici la structure minimale que j’utilise systématiquement :
<canvas id="paintCanvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('paintCanvas');
const ctx = canvas.getContext('2d');
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 3;
ctx.strokeStyle = '#000000';
</script>
Le contexte 2D (getContext('2d')) expose toutes les méthodes de dessin : beginPath(), moveTo(), lineTo(), stroke(), fill(), et bien d’autres. C’est ce contexte qui transforme une simple balise HTML en véritable surface de peinture interactive.
Quelques propriétés essentielles à configurer dès le départ :
- lineCap : définit la forme des extrémités de ligne (round pour un rendu naturel)
- lineJoin : gère les jonctions entre segments (round évite les angles disgracieux)
- globalCompositeOperation : permet de créer un effet gomme avec ‘destination-out’
- imageSmoothingEnabled : active ou désactive l’anticrénelage
Je recommande de toujours dimensionner le canvas via ses attributs HTML (width et height) plutôt qu’en CSS. Utiliser le CSS pour redimensionner un canvas étire l’image sans modifier la résolution réelle, ce qui produit un rendu flou. Pour un canvas responsive, je recalcule les dimensions en JavaScript lors du redimensionnement de la fenêtre, comme je l’explique dans mon article sur la propriété length des arrays JavaScript où je détaille la manipulation dynamique des structures de données.

Architecture d’un paint JavaScript from scratch
Après avoir développé plusieurs outils de dessin pour mes clients, j’ai établi une architecture modulaire qui facilite la maintenance et l’ajout de fonctionnalités. Voici la structure que je préconise :
class PaintApp {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.isDrawing = false;
this.currentTool = 'brush';
this.history = [];
this.historyIndex = -1;
this.init();
}
init() {
this.setupCanvas();
this.bindEvents();
this.saveState();
}
setupCanvas() {
this.canvas.width = this.canvas.offsetWidth;
this.canvas.height = this.canvas.offsetHeight;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
}
saveState() {
this.historyIndex++;
this.history = this.history.slice(0, this.historyIndex);
this.history.push(this.canvas.toDataURL());
}
undo() {
if (this.historyIndex > 0) {
this.historyIndex--;
this.restoreState();
}
}
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.restoreState();
}
}
restoreState() {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
};
img.src = this.history[this.historyIndex];
}
}
Cette classe centralise la gestion de l'état, l'historique (undo/redo) et l'initialisation. Le pattern que j'utilise repose sur trois piliers :
- Séparation des responsabilités : chaque outil (pinceau, gomme, formes) est une classe distincte
- Historique immutable : chaque état est une copie complète du canvas, ce qui simplifie l'annulation
- Event-driven : les interactions utilisateur déclenchent des méthodes spécifiques selon l'outil actif
Pour les projets plus complexes, j'utilise un pattern Command où chaque action de dessin est un objet sérialisable. Cela permet non seulement l'undo/redo mais aussi la collaboration en temps réel via WebSocket. La boucle for en JavaScript s'avère particulièrement utile pour itérer sur l'historique des commandes lors de la reconstruction de l'état du canvas.
Gestion des événements souris et tactile
La fluidité d'un paint JavaScript dépend directement de la qualité de la gestion des événements. J'ai identifié trois catégories d'événements à gérer simultanément : souris, tactile et stylet (Pointer Events).
bindEvents() {
// Pointer Events (unifie souris, tactile et stylet)
this.canvas.addEventListener('pointerdown', (e) => this.startDrawing(e));
this.canvas.addEventListener('pointermove', (e) => this.draw(e));
this.canvas.addEventListener('pointerup', () => this.stopDrawing());
this.canvas.addEventListener('pointerleave', () => this.stopDrawing());
// Empêcher le scroll sur mobile pendant le dessin
this.canvas.addEventListener('touchstart', (e) => e.preventDefault());
this.canvas.addEventListener('touchmove', (e) => e.preventDefault());
}
startDrawing(e) {
this.isDrawing = true;
const pos = this.getPosition(e);
this.ctx.beginPath();
this.ctx.moveTo(pos.x, pos.y);
}
draw(e) {
if (!this.isDrawing) return;
const pos = this.getPosition(e);
this.ctx.lineTo(pos.x, pos.y);
this.ctx.stroke();
}
stopDrawing() {
if (this.isDrawing) {
this.isDrawing = false;
this.ctx.closePath();
this.saveState();
}
}
getPosition(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (this.canvas.width / rect.width),
y: (e.clientY - rect.top) * (this.canvas.height / rect.height)
};
}
Le calcul de position dans getPosition() est critique. Il compense à la fois le décalage du canvas dans la page et le ratio entre les dimensions CSS et les dimensions réelles du canvas. Sans cette correction, le trait apparaît décalé par rapport au curseur.
Pour la pression du stylet, les Pointer Events exposent la propriété e.pressure (valeur entre 0 et 1). Je l'utilise pour moduler l'épaisseur du trait :
draw(e) {
if (!this.isDrawing) return;
const pos = this.getPosition(e);
// Moduler l'épaisseur selon la pression
if (e.pressure > 0) {
this.ctx.lineWidth = this.baseWidth * e.pressure * 2;
}
this.ctx.lineTo(pos.x, pos.y);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(pos.x, pos.y);
}
Ce détail fait toute la différence entre un outil de dessin basique et une expérience professionnelle. Les tablettes graphiques Wacom et les iPad exploitent pleinement cette fonctionnalité.
Implémenter les outils de dessin essentiels
Un paint JavaScript complet propose au minimum cinq outils fondamentaux. Voici comment je les implémente dans mes projets :
Le pinceau libre
C'est l'outil par défaut. Le code présenté dans la section événements constitue déjà un pinceau fonctionnel. Pour un rendu plus lisse, j'applique un algorithme de lissage par interpolation quadratique :
drawSmooth(points) {
if (points.length < 3) return;
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length - 1; i++) {
const midX = (points[i].x + points[i + 1].x) / 2;
const midY = (points[i].y + points[i + 1].y) / 2;
this.ctx.quadraticCurveTo(points[i].x, points[i].y, midX, midY);
}
this.ctx.stroke();
}
La gomme
Deux approches possibles. La plus simple utilise globalCompositeOperation :
enableEraser() {
this.ctx.globalCompositeOperation = 'destination-out';
this.ctx.lineWidth = 20;
}
disableEraser() {
this.ctx.globalCompositeOperation = 'source-over';
}
Le remplissage (pot de peinture)
L'algorithme de flood fill parcourt les pixels adjacents de même couleur. C'est l'outil le plus gourmand en ressources :
floodFill(startX, startY, fillColor) {
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const data = imageData.data;
const targetColor = this.getPixelColor(data, startX, startY);
if (this.colorsMatch(targetColor, fillColor)) return;
const stack = [[startX, startY]];
const width = this.canvas.width;
const height = this.canvas.height;
while (stack.length > 0) {
const [x, y] = stack.pop();
const index = (y * width + x) * 4;
if (x < 0 || x >= width || y < 0 || y >= height) continue;
if (!this.colorsMatch(this.getPixelAt(data, index), targetColor)) continue;
data[index] = fillColor.r;
data[index + 1] = fillColor.g;
data[index + 2] = fillColor.b;
data[index + 3] = 255;
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
}
this.ctx.putImageData(imageData, 0, 0);
}
Les formes géométriques
Pour dessiner des rectangles, cercles et lignes droites, j'utilise une technique de canvas temporaire qui préserve le dessin existant pendant le tracé :
drawRectangle(startPos, endPos) {
// Restaurer l'état précédent
this.restoreState();
this.ctx.beginPath();
this.ctx.rect(
startPos.x,
startPos.y,
endPos.x - startPos.x,
endPos.y - startPos.y
);
this.ctx.stroke();
}

Le sélecteur de couleur (pipette)
pickColor(x, y) {
const pixel = this.ctx.getImageData(x, y, 1, 1).data;
return `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`;
}
| Outil | Complexité | Performance | Cas d'usage principal |
|---|---|---|---|
| Pinceau libre | Faible | Excellente | Dessin à main levée |
| Gomme | Faible | Excellente | Correction et effacement |
| Remplissage | Élevée | Variable (dépend de la surface) | Coloriage de zones fermées |
| Formes géométriques | Moyenne | Bonne | Schémas et annotations |
| Pipette | Faible | Excellente | Récupération de couleur existante |
| Texte | Moyenne | Bonne | Annotations et légendes |
| Spray | Moyenne | Bonne | Effets de texture et dispersion |
Optimisation des performances et du rendu
Un paint JavaScript mal optimisé devient inutilisable sur les grands canvas ou les appareils mobiles. Voici les techniques que j'applique systématiquement pour garantir un rendu fluide à 60 FPS :
requestAnimationFrame pour le rendu
Plutôt que de dessiner à chaque événement pointermove (qui peut se déclencher plus de 100 fois par seconde), je bufferise les points et dessine une seule fois par frame :
class OptimizedPaint {
constructor() {
this.pendingPoints = [];
this.rafId = null;
}
onPointerMove(e) {
this.pendingPoints.push(this.getPosition(e));
if (!this.rafId) {
this.rafId = requestAnimationFrame(() => this.renderFrame());
}
}
renderFrame() {
this.rafId = null;
if (this.pendingPoints.length === 0) return;
this.ctx.beginPath();
this.ctx.moveTo(this.lastPoint.x, this.lastPoint.y);
for (const point of this.pendingPoints) {
this.ctx.lineTo(point.x, point.y);
}
this.ctx.stroke();
this.lastPoint = this.pendingPoints[this.pendingPoints.length - 1];
this.pendingPoints = [];
}
}
Système de couches (layers)
Pour les applications complexes, j'empile plusieurs canvas transparents. Chaque couche est un <canvas> positionné en absolu. Cette technique évite de redessiner l'intégralité de l'image quand seule une couche est modifiée :
createLayer(name) {
const layer = document.createElement('canvas');
layer.width = this.canvas.width;
layer.height = this.canvas.height;
layer.style.position = 'absolute';
layer.style.top = '0';
layer.style.left = '0';
this.layers.set(name, layer);
this.container.appendChild(layer);
return layer.getContext('2d');
}
OffscreenCanvas pour les calculs lourds
Depuis 2020, l'API OffscreenCanvas permet de déporter les opérations de rendu dans un Web Worker. C'est particulièrement utile pour le flood fill ou l'application de filtres :
// main.js
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('paint-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// paint-worker.js
self.onmessage = function(e) {
const ctx = e.data.canvas.getContext('2d');
// Opérations lourdes sans bloquer le thread principal
};
D'après la documentation MDN sur l'API Canvas, l'OffscreenCanvas est supporté par tous les navigateurs modernes depuis 2023, ce qui en fait une solution fiable pour les applications de dessin exigeantes.
Gestion mémoire et historique optimisé
Stocker chaque état comme un dataURL complet consomme énormément de mémoire. Pour un canvas 1920x1080, chaque snapshot pèse environ 8 Mo. Je limite l'historique à 30 étapes et utilise une compression différentielle quand c'est possible. La méthode trimEnd en JavaScript illustre bien ce principe de nettoyage des données superflues que j'applique aussi à la gestion mémoire du canvas.
Bibliothèques et alternatives pour un paint en ligne
Développer un paint JavaScript from scratch n'est pas toujours pertinent. Selon le budget et les contraintes du projet, je recommande différentes approches :
JS Paint : le clone parfait de MS Paint
JS Paint (disponible sur GitHub) est la reproduction la plus fidèle de Microsoft Paint dans le navigateur. Open source et activement maintenu, il offre toutes les fonctionnalités du Paint classique : sélection, recadrage, retournement, étirement, et même les filtres. C'est la solution que je recommande quand un client souhaite un outil de dessin complet sans développement spécifique.
Fabric.js : le couteau suisse du canvas
Fabric.js abstrait la complexité du canvas en proposant un modèle objet. Chaque élément dessiné devient un objet manipulable (déplaçable, redimensionnable, rotatif). Idéal pour les éditeurs d'images et les outils d'annotation :
const fabricCanvas = new fabric.Canvas('paintCanvas');
fabricCanvas.isDrawingMode = true;
fabricCanvas.freeDrawingBrush.width = 5;
fabricCanvas.freeDrawingBrush.color = '#ff0000';
Paint.js : bibliothèque légère spécialisée
Paint.js est une bibliothèque open source conçue spécifiquement pour créer des applications de dessin. Plus légère que Fabric.js, elle se concentre sur l'essentiel : pinceaux, couleurs, calques et export.
Comparatif des solutions
| Solution | Taille | Courbe d'apprentissage | Personnalisation | Idéal pour |
|---|---|---|---|---|
| Canvas natif (from scratch) | 0 Ko | Élevée | Totale | Projets sur mesure, performance critique |
| JS Paint | ~500 Ko | Faible (clé en main) | Limitée | Remplacement direct de MS Paint |
| Fabric.js | ~300 Ko | Moyenne | Élevée | Éditeurs d'images, annotations |
| Paint.js | ~50 Ko | Faible | Moyenne | Applications de dessin simples |
| Konva.js | ~150 Ko | Moyenne | Élevée | Applications interactives complexes |
Le choix dépend du contexte. Pour un outil d'annotation intégré à un CMS WordPress, j'opte généralement pour Fabric.js. Pour une application de dessin autonome destinée aux enfants, un développement natif avec Canvas API reste mon choix préféré car il offre un contrôle total sur l'interface et les performances.

Export et sauvegarde du canvas
Un paint JavaScript sans fonction d'export est incomplet. L'API Canvas offre plusieurs méthodes natives pour sauvegarder le travail de l'utilisateur :
Export en image (PNG, JPEG, WebP)
// Export PNG (qualité maximale, transparence conservée)
const pngUrl = canvas.toDataURL('image/png');
// Export JPEG (compression paramétrable)
const jpegUrl = canvas.toDataURL('image/jpeg', 0.85);
// Export WebP (meilleur ratio qualité/poids)
const webpUrl = canvas.toDataURL('image/webp', 0.90);
// Déclencher le téléchargement
function downloadCanvas(format = 'png', quality = 1) {
const link = document.createElement('a');
link.download = `dessin.${format}`;
link.href = canvas.toDataURL(`image/${format}`, quality);
link.click();
}
Export en Blob pour upload serveur
canvas.toBlob((blob) => {
const formData = new FormData();
formData.append('drawing', blob, 'dessin.png');
fetch('/api/upload', {
method: 'POST',
body: formData
});
}, 'image/png');
Sauvegarde en localStorage
Pour permettre à l'utilisateur de reprendre son dessin plus tard sans backend :
// Sauvegarder
localStorage.setItem('paintSave', canvas.toDataURL());
// Restaurer
const saved = localStorage.getItem('paintSave');
if (saved) {
const img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.src = saved;
}
Attention : localStorage est limité à environ 5 Mo par domaine. Pour des canvas volumineux, je privilégie IndexedDB qui supporte des tailles bien supérieures. Cette contrainte de taille rappelle l'importance de bien connaître les méthodes de manipulation de chaînes, comme splice en JavaScript pour gérer efficacement les données en mémoire.
Export SVG pour le vectoriel
Si votre paint JavaScript utilise un système de commandes (et pas uniquement du bitmap), vous pouvez reconstruire un SVG à partir de l'historique des tracés. C'est plus complexe mais offre un résultat redimensionnable sans perte de qualité.
Quel est l'équivalent de Paint en ligne ?
La question revient fréquemment chez mes clients et dans les forums : quel est l'équivalent de Paint en ligne ? Depuis que Microsoft a intégré Paint 3D puis l'a progressivement abandonné au profit de solutions cloud, les alternatives web se sont multipliées.
JS Paint reste la réponse la plus directe. Ce projet open source reproduit pixel par pixel l'interface de Windows 98 Paint, avec toutes ses fonctionnalités : sélection libre, texte, aérographe, et même la palette de couleurs personnalisable. Il fonctionne directement dans le navigateur sans installation.
Pour les besoins plus avancés, Photopea offre une alternative gratuite à Photoshop directement dans le navigateur, tandis que Aggie.io et Magma proposent du dessin collaboratif en temps réel.
Comment activer Paint sur Windows ?
Sur Windows 10 et 11, Paint est préinstallé. Si vous ne le trouvez pas, tapez "Paint" dans la barre de recherche du menu Démarrer. En cas de désinstallation accidentelle, réinstallez-le depuis le Microsoft Store gratuitement. Paint 3D, quant à lui, n'est plus maintenu par Microsoft depuis novembre 2024 mais reste disponible au téléchargement.
Paint 3D est-il gratuit ?
Oui, Paint 3D est entièrement gratuit. Il est disponible sur le Microsoft Store sans abonnement ni achat intégré. Cependant, Microsoft a annoncé l'arrêt de son développement actif. Pour la modélisation 3D en ligne, des alternatives comme Tinkercad ou SculptGL prennent le relais, gratuitement également.
Comment obtenir Paint ?
Trois options selon votre plateforme :
- Windows : préinstallé, ou via le Microsoft Store
- macOS / Linux : utilisez JS Paint dans le navigateur ou installez des équivalents natifs (Pinta, KolourPaint)
- En ligne : accédez directement à jspaint.app sans aucune installation
Selon la page de support Microsoft, Paint a reçu une refonte majeure en 2023 avec l'intégration de l'IA générative (Cocreator) pour Windows 11, confirmant que l'outil historique reste activement développé.
Pour les développeurs souhaitant intégrer un équivalent de Paint dans leurs propres applications web, le développement d'un paint JavaScript personnalisé reste la meilleure approche. Cela permet de contrôler précisément les fonctionnalités exposées, l'interface utilisateur et l'intégration avec le reste de l'application. Un CMS WordPress peut par exemple embarquer un éditeur de dessin léger pour permettre aux utilisateurs d'annoter des images avant publication, une fonctionnalité que je déploie régulièrement dans mes projets de création de sites internet.
À retenir
- Initialisez votre canvas avec lineCap et lineJoin en 'round' pour un rendu naturel dès le premier trait
- Utilisez les Pointer Events plutôt que les Mouse Events pour supporter souris, tactile et stylet simultanément
- Bufferisez les points avec requestAnimationFrame pour maintenir 60 FPS même sur mobile
- Limitez l'historique undo/redo à 30 états maximum pour éviter la saturation mémoire
- Préférez canvas.toBlob() à toDataURL() pour les uploads serveur (meilleure performance mémoire)
Questions fréquentes
Quel est l'équivalent de Paint en ligne ?
JS Paint (jspaint.app) est l'équivalent le plus fidèle de Microsoft Paint en ligne. Open source et gratuit, il reproduit toutes les fonctionnalités du Paint classique directement dans le navigateur, sans installation. Pour des besoins plus avancés, Photopea offre des fonctionnalités proches de Photoshop, toujours gratuitement et en ligne.
Paint 3D est-il gratuit ?
Oui, Paint 3D est totalement gratuit et disponible sur le Microsoft Store. Aucun abonnement ni achat intégré n'est requis. Cependant, Microsoft a cessé son développement actif depuis fin 2024. Pour la modélisation 3D gratuite en ligne, Tinkercad et SculptGL sont les meilleures alternatives actuelles.
Comment activer Paint ?
Sur Windows 10 et 11, Paint est préinstallé. Recherchez "Paint" dans le menu Démarrer. S'il est absent, téléchargez-le gratuitement depuis le Microsoft Store. Sur macOS ou Linux, utilisez JS Paint dans votre navigateur ou installez Pinta, un équivalent open source multiplateforme.
Comment obtenir Paint ?
Sur Windows, Paint est inclus par défaut. Si désinstallé, rendez-vous sur le Microsoft Store. Sur les autres systèmes, accédez à jspaint.app dans n'importe quel navigateur pour une version web complète et gratuite, ou installez des alternatives natives comme Pinta (macOS, Linux) ou KolourPaint (Linux).
Quelle bibliothèque JavaScript utiliser pour créer un paint ?
Pour un projet rapide, Fabric.js (300 Ko) offre le meilleur équilibre entre puissance et simplicité. Pour un clone de MS Paint, JS Paint est disponible en open source. Pour un contrôle total et des performances optimales, le développement natif avec l'API Canvas HTML5 reste la meilleure approche, sans aucune dépendance externe.
Comment rendre un paint JavaScript compatible mobile ?
Utilisez les Pointer Events qui unifient souris, tactile et stylet. Ajoutez touch-action: none en CSS sur le canvas pour empêcher le scroll pendant le dessin. Bufferisez les points avec requestAnimationFrame et testez avec la propriété e.pressure pour supporter les stylets des tablettes.
Comment implémenter l'undo/redo dans un paint JavaScript ?
Sauvegardez l'état du canvas après chaque action avec canvas.toDataURL() dans un tableau. Maintenez un index de position dans l'historique. Pour annuler, décrémentez l'index et restaurez l'image correspondante avec drawImage(). Limitez le tableau à 30 entrées pour éviter la consommation excessive de mémoire.
Nathan Morel est développeur web freelance depuis 12 ans dans la Loire. Spécialisé WordPress et solutions sur mesure, il a accompagné plus de 200 PME et partage son expérience technique et entrepreneuriale sur NA Web.