Le projet : transformer du texte en vraie image
Septième projet, et une nouvelle corde à ton arc : le canvas. Jusqu'ici on assemblait des balises HTML ; cette fois on va dessiner, pixel par pixel, dans une zone dédiée. Le but : tu écris un message, il apparaît sur une jolie carte en dégradé, et un bouton te la télécharge en image PNG — prête à poster.
Pourquoi c'est un bon palier ? Parce que dessiner, c'est plus bas niveau qu'empiler du HTML : le canvas ne sait rien faire « tout seul ». Pas de retour à la ligne, pas de texte sélectionnable, rien pour un lecteur d'écran. C'est très exactement le terrain où l'IA produit un résultat joli sur la capture d'écran, mais bancal dès qu'on sort du cas idéal. On code vite, on relit lentement.
Un <canvas>, c'est une surface de dessin pilotée en JavaScript via un « contexte » (ctx). On lui donne des ordres : remplis ce rectangle, écris ce texte ici. À la fin, on peut exporter le tout en fichier image.
Prompt 1 : poser le cadre
On cadre : un champ de message, un canvas qui dessine une carte avec un dégradé, et un bouton pour télécharger l'image.
Crée une mini-app web autonome, un seul fichier HTML (zéro dépendance). Un champ texte ; le message s'affiche sur une carte dessinée dans un <canvas> avec un fond en dégradé et du texte blanc centré. Un bouton télécharge la carte en PNG. Bilingue FR/EN. Code simple et lisible.
L'IA dessine la carte. Mais son code de dessin cache trois manques :
canvas.width = 800; canvas.height = 450; // ⚠️ taille fixe : image floue sur écran Retina
var ctx = canvas.getContext('2d');
ctx.fillStyle = grad; ctx.fillRect(0, 0, 800, 450);
ctx.fillStyle = '#fff'; ctx.font = '40px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(message, 400, 225); // ⚠️ une seule ligne : un long message déborde
Tape un mot, c'est parfait. Tape une phrase, elle sort de la carte par les côtés. Et l'image téléchargée est curieusement floue sur un téléphone récent.
Ma relecture humaine : 3 trucs que l'IA a laissés passer
Le canvas, c'est puissant mais primitif : il fait exactement ce qu'on lui dit, rien de plus. Tout ce que le HTML offrait gratuitement (retour à la ligne, accessibilité), il faut ici le rajouter à la main. Voici les trois points.
1. Le canvas ne va pas à la ligne tout seul
fillText écrit le texte sur une seule ligne, point. Un message un peu long déborde tout simplement de la carte. Il faut découper le texte soi-même : on mesure les mots un par un avec ctx.measureText(), et on passe à la ligne dès qu'on dépasse la largeur disponible. C'est une petite boucle, mais c'est à toi de l'écrire — le canvas ne le fera jamais.
2. L'image est floue sur les écrans Retina
Un canvas de 800×450 « pixels » sur un écran haute densité (téléphone, Mac récent), c'est étiré sur deux fois plus de pixels réels : le résultat est flou. La parade : multiplier la résolution interne par window.devicePixelRatio, puis appliquer la même échelle au dessin (ctx.setTransform). On dessine toujours en « points » logiques, mais l'image exportée est nette.
3. Un canvas est muet pour un lecteur d'écran
Le texte dessiné dans un canvas n'existe pas vraiment : c'est de la peinture, pas du texte. Un lecteur d'écran ne « voit » qu'une image vide. On corrige en deux temps : le vrai champ reste un <textarea> (accessible, c'est la source), et on donne au canvas role="img" + un aria-label qui reflète le message courant. L'aperçu redevient compréhensible pour tout le monde.
Prompt 2 : durcir après relecture
Trois manques, trois corrections demandées précisément :
Corrige : (1) découpe le texte en plusieurs lignes avec measureText pour qu'un long message ne déborde pas. (2) Tiens compte de window.devicePixelRatio (canvas.width = W * dpr, puis ctx.setTransform) pour une image nette sur écran Retina. (3) Garde un <textarea> comme champ accessible, et mets role="img" + un aria-label reflétant le texte sur le canvas.
// 2. netteté Retina : on dessine en plus haute résolution
var dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr; canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // on raisonne ensuite en points logiques
// 1. retour à la ligne : on mesure mot par mot
function wrapText(text, maxWidth) {
var words = text.split(' '), lines = [], line = '';
for (var i = 0; i < words.length; i++) {
var test = line ? line + ' ' + words[i] : words[i];
if (ctx.measureText(test).width > maxWidth && line) { lines.push(line); line = words[i]; }
else line = test;
}
if (line) lines.push(line);
return lines;
}
// 3. accessibilité : le texte dessiné est aussi annoncé
canvas.setAttribute('aria-label', text);
La règle de fond, encore valable ici : l'outil ne fait que ce qu'on lui dit. Le HTML t'offrait des cadeaux (le navigateur gérait le texte et l'accessibilité). Le canvas, lui, te rend responsable de tout. Plus on descend en bas niveau, plus la relecture humaine compte.
Héberger et tester
Fichier statique, en ligne d'un dépôt. Côté tests, on garde les réflexes habituels, plus ceux propres au canvas :
- Tape une phrase longue : elle doit passer sur plusieurs lignes, jamais déborder.
- Télécharge l'image et ouvre-la : sur mobile/Retina, le texte doit être net, pas pixelisé.
- Change de fond plusieurs fois : le texte blanc reste lisible (les fonds sont volontairement sombres).
- Navigue au clavier jusqu'au champ ; vérifie la bascule FR / EN et la console (zéro erreur).
Le rendu final
Le code complet (et téléchargeable)
Le fichier entier, exactement celui qui tourne au-dessus.
Télécharger le code (.html · 236 lignes)
Voir le code complet
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Carte en image (Canvas)</title>
<meta name="description" content="Écris un message, génère une carte et télécharge-la en image. Mini-projet construit avec l'IA — web-developpeur.com">
<meta name="robots" content="noindex, follow">
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<!--
============================================================================
CARTE EN IMAGE — un seul fichier : HTML + CSS + JS, zéro dépendance.
1. <style> : l'apparence (le canvas, le champ texte, les boutons).
2. <body> : la structure (champ + aperçu canvas + actions).
3. <script> : dessiner sur le canvas et exporter l'image en PNG.
Nouveauté du projet : le <canvas>, une zone où l'on DESSINE en JavaScript
(au lieu d'empiler des balises HTML). Trois pièges classiques à corriger :
le retour à la ligne (le canvas ne le fait pas tout seul), la netteté sur
écran Retina, et l'accessibilité (un canvas est une image muette).
============================================================================
-->
<style>
:root { --ink:#1a1d24; --muted:#5a6270; --accent:#267d42; --border:#e2e6ea; }
* { box-sizing:border-box; }
html,body { margin:0; padding:0; }
body { font-family:'Segoe UI',system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif; background:#f4f6f8; color:var(--ink); min-height:100vh; display:flex; flex-direction:column; align-items:center; padding:32px 18px 48px; line-height:1.5; }
.app { width:100%; max-width:560px; }
header { text-align:center; margin-bottom:20px; }
h1 { font-size:1.5rem; margin:0 0 6px; letter-spacing:-0.01em; }
.sub { color:var(--muted); font-size:0.95rem; margin:0 0 16px; }
.lang-switch { display:inline-flex; border:1.5px solid var(--border); border-radius:999px; overflow:hidden; background:#fff; }
.lang-btn { border:0; background:transparent; padding:7px 18px; font:inherit; font-weight:700; font-size:0.8rem; color:var(--muted); cursor:pointer; }
.lang-btn[aria-pressed="true"] { background:var(--accent); color:#fff; }
.lang-btn:focus-visible { outline:3px solid rgba(38,125,66,0.4); outline-offset:2px; }
/* Le canvas s'affiche en pleine largeur ; sa résolution interne est plus haute (Retina). */
canvas { width:100%; height:auto; display:block; border-radius:16px; box-shadow:0 16px 38px rgba(15,25,35,0.16); }
textarea { width:100%; margin-top:18px; min-height:80px; padding:12px 14px; border:1.5px solid var(--border); border-radius:12px; font:inherit; font-size:1rem; color:var(--ink); resize:vertical; }
textarea:focus-visible { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(38,125,66,0.12); }
.actions { display:flex; gap:12px; margin-top:14px; flex-wrap:wrap; }
.btn { flex:1 1 auto; min-width:150px; min-height:50px; border-radius:12px; border:1.5px solid transparent; font:inherit; font-weight:700; font-size:0.98rem; cursor:pointer; transition:transform 0.12s ease, background 0.2s ease, border-color 0.2s ease; }
.btn-primary { background:var(--accent); color:#fff; box-shadow:0 6px 16px rgba(38,125,66,0.28); }
.btn-primary:hover { background:#1f6a37; transform:translateY(-1px); }
.btn-ghost { background:#fff; color:var(--ink); border-color:var(--border); }
.btn-ghost:hover { border-color:var(--accent); color:var(--accent); }
.btn:focus-visible { outline:3px solid rgba(38,125,66,0.4); outline-offset:2px; }
.credit { text-align:center; color:var(--muted); font-size:0.78rem; margin-top:26px; }
.credit a { color:var(--accent); }
@media (max-width:480px){ .btn { flex-basis:100%; } }
@media (prefers-reduced-motion: reduce){ .btn { transition:none; } }
</style>
</head>
<body>
<main class="app">
<header>
<h1 data-i18n="title">Carte en image</h1>
<p class="sub" data-i18n="sub">Écris un message, choisis un fond, télécharge la carte en image.</p>
<div class="lang-switch" role="group" aria-label="Langue / Language">
<button class="lang-btn" data-lang-btn="fr" aria-pressed="true" type="button">FR</button>
<button class="lang-btn" data-lang-btn="en" aria-pressed="false" type="button">EN</button>
</div>
</header>
<!-- role="img" + aria-label : un canvas est une image. On y mettra le texte courant
pour qu'un lecteur d'écran sache ce qui est dessiné dessus. -->
<canvas id="canvas" role="img" aria-label=""></canvas>
<!-- Le vrai champ accessible, c'est ce textarea (le canvas n'est qu'un aperçu). -->
<textarea id="text" maxlength="120" data-i18n-ph="ph" placeholder="Ton message (ex. Code. Café. Répète.)" aria-label="Message de la carte"></textarea>
<div class="actions">
<button class="btn btn-ghost" id="bg" type="button" data-i18n="bg">Changer le fond</button>
<button class="btn btn-primary" id="dl" type="button" data-i18n="dl">Télécharger en PNG</button>
</div>
<p class="credit" data-i18n="credit">Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com</p>
</main>
<script>
(function () {
'use strict';
/* ---------------------------------------------------------------------------
1. LES TEXTES (FR / EN) + les fonds disponibles.
--------------------------------------------------------------------------- */
var UI = {
fr: { title:"Carte en image", sub:"Écris un message, choisis un fond, télécharge la carte en image.",
ph:"Ton message (ex. Code. Café. Répète.)", bg:"Changer le fond", dl:"Télécharger en PNG",
placeholder:"Ton message ici",
credit:'Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com' },
en: { title:"Image card", sub:"Write a message, pick a background, download the card as an image.",
ph:"Your message (e.g. Code. Coffee. Repeat.)", bg:"Change background", dl:"Download as PNG",
placeholder:"Your message here",
credit:'A mini-project from the <a href="/apprendre/projets/">Applied projects</a> course — web-developpeur.com' }
};
// Fonds CHOISIS, tous assez sombres pour que le texte blanc reste lisible.
var GRADIENTS = [
['#0f1923', '#267d42'], ['#1a1a2e', '#16213e'], ['#2d1b4e', '#5b2a86'],
['#0b3d2e', '#1e6f5c'], ['#3a1c40', '#7d2d52'], ['#102a43', '#334e68']
];
/* ---------------------------------------------------------------------------
2. LES ÉLÉMENTS + L'ÉTAT.
W et H = la taille LOGIQUE de la carte (en points). La résolution réelle
de l'image sera plus grande sur écran Retina (voir draw()).
--------------------------------------------------------------------------- */
var W = 800, H = 450;
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var textInput = document.getElementById('text');
var bgBtn = document.getElementById('bg');
var dlBtn = document.getElementById('dl');
var lang = 'fr';
try { lang = localStorage.getItem('card-lang') || 'fr'; } catch (e) {}
if (lang !== 'fr' && lang !== 'en') lang = 'fr';
var bgIndex = 0; // quel fond est utilisé
/* ---------------------------------------------------------------------------
3. DÉCOUPER LE TEXTE EN LIGNES.
Le canvas n'a PAS de retour à la ligne automatique : si on écrit une longue
phrase, elle dépasse. On mesure les mots un par un (measureText) et on passe
à la ligne quand on dépasse la largeur disponible.
--------------------------------------------------------------------------- */
function wrapText(text, maxWidth) {
var words = text.split(' ');
var lines = [], line = '';
for (var i = 0; i < words.length; i++) {
var test = line ? line + ' ' + words[i] : words[i];
if (ctx.measureText(test).width > maxWidth && line) {
lines.push(line); // la ligne est pleine : on la garde
line = words[i]; // et on commence la suivante
} else {
line = test;
}
}
if (line) lines.push(line);
return lines;
}
/* ---------------------------------------------------------------------------
4. DESSINER la carte (fond + texte + signature).
--------------------------------------------------------------------------- */
function draw() {
// Retina : on dessine à une résolution multipliée par devicePixelRatio,
// sinon l'image téléchargée serait floue sur les écrans haute densité.
var dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // on continue à raisonner en points logiques
// Le fond : un dégradé en diagonale.
var grad = GRADIENTS[bgIndex];
var g = ctx.createLinearGradient(0, 0, W, H);
g.addColorStop(0, grad[0]); g.addColorStop(1, grad[1]);
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// Le message, centré et découpé en lignes.
var text = textInput.value.trim() || UI[lang].placeholder;
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = "700 40px 'Segoe UI', system-ui, sans-serif";
var lines = wrapText(text, W - 140);
var lineHeight = 52;
var startY = H / 2 - (lines.length - 1) * lineHeight / 2;
lines.forEach(function (l, i) { ctx.fillText(l, W / 2, startY + i * lineHeight); });
// Une petite signature en bas.
ctx.globalAlpha = 0.85;
ctx.font = "600 20px 'Segoe UI', system-ui, sans-serif";
ctx.fillText('</> dev.card', W / 2, H - 44);
ctx.globalAlpha = 1;
// Accessibilité : un canvas est une image MUETTE pour un lecteur d'écran.
// On y attache le texte courant via aria-label, pour qu'il sache ce qui est affiché.
canvas.setAttribute('aria-label', text);
}
/* ---------------------------------------------------------------------------
5. TÉLÉCHARGER l'image — on convertit le canvas en fichier PNG.
--------------------------------------------------------------------------- */
function download() {
canvas.toBlob(function (blob) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = 'carte.png';
a.click();
URL.revokeObjectURL(url); // on libère la mémoire une fois le téléchargement lancé
}, 'image/png');
}
/* ---------------------------------------------------------------------------
6. LES ACTIONS + LE DÉMARRAGE.
--------------------------------------------------------------------------- */
textInput.addEventListener('input', draw); // on redessine à chaque frappe
bgBtn.addEventListener('click', function () {
bgIndex = (bgIndex + 1) % GRADIENTS.length; draw(); // fond suivant (et on boucle)
});
dlBtn.addEventListener('click', download);
function setLang(next) {
lang = next;
try { localStorage.setItem('card-lang', lang); } catch (e) {}
document.documentElement.lang = lang;
var t = UI[lang];
document.querySelectorAll('[data-i18n]').forEach(function (el) {
var k = el.getAttribute('data-i18n'); if (!t[k]) return;
if (k === 'credit') el.innerHTML = t[k]; else el.textContent = t[k];
});
var ph = document.querySelector('[data-i18n-ph]'); if (ph) ph.placeholder = t.ph;
document.querySelectorAll('[data-lang-btn]').forEach(function (b) {
b.setAttribute('aria-pressed', b.getAttribute('data-lang-btn') === lang ? 'true' : 'false');
});
draw(); // le texte par défaut change avec la langue : on redessine
}
document.querySelectorAll('[data-lang-btn]').forEach(function (b) {
b.addEventListener('click', function () { setLang(b.getAttribute('data-lang-btn')); });
});
setLang(lang); // applique la langue et dessine la première carte
})();
</script>
</body>
</html>
À toi de jouer
Pousse la carte plus loin, en gardant les réflexes du canvas :
- Ajoute un choix de taille de police (et adapte le retour à la ligne).
- Permets de déposer une image de fond (attention : une image d'un autre site « contamine » le canvas et bloque le téléchargement — cherche « tainted canvas / CORS »).
- Un bouton format carré (1:1) pour Instagram en plus du format large.
À chaque ajout : le texte déborde-t-il ? l'image reste-t-elle nette ? l'aperçu est-il annoncé ?
The project: turning text into a real image
Seventh project, and a new string to your bow: the canvas. Until now we assembled HTML tags; this time we'll draw, pixel by pixel, in a dedicated area. The goal: you write a message, it appears on a nice gradient card, and a button downloads it as a PNG image — ready to post.
Why is this a good step up? Because drawing is lower-level than stacking HTML: the canvas does nothing "on its own". No line wrapping, no selectable text, nothing for a screen reader. It's exactly the ground where the AI produces a result that's pretty in the screenshot, but shaky as soon as you leave the ideal case. Code fast, review slowly.
A <canvas> is a drawing surface controlled in JavaScript via a "context" (ctx). You give it orders: fill this rectangle, write this text here. At the end, you can export the whole thing as an image file.
Prompt 1: set the frame
We frame it: a message field, a canvas that draws a card with a gradient, and a button to download the image.
Create a standalone web mini-app, a single HTML file (zero dependencies). A text field; the message shows on a card drawn in a <canvas> with a gradient background and centered white text. A button downloads the card as PNG. Bilingual FR/EN. Simple, readable code.
The AI draws the card. But its drawing code hides three gaps:
canvas.width = 800; canvas.height = 450; // ⚠️ fixed size: blurry image on Retina screens
var ctx = canvas.getContext('2d');
ctx.fillStyle = grad; ctx.fillRect(0, 0, 800, 450);
ctx.fillStyle = '#fff'; ctx.font = '40px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(message, 400, 225); // ⚠️ a single line: a long message overflows
Type a word, it's perfect. Type a sentence, it spills off the sides of the card. And the downloaded image is oddly blurry on a recent phone.
My human review: 3 things the AI let slip
The canvas is powerful but primitive: it does exactly what you tell it, nothing more. Everything HTML gave for free (wrapping, accessibility) you must add by hand here. Here are the three points.
1. The canvas doesn't wrap text on its own
fillText writes the text on a single line, period. A slightly long message simply overflows the card. You must split the text yourself: measure the words one by one with ctx.measureText(), and break to a new line as soon as you exceed the available width. It's a small loop, but it's yours to write — the canvas never will.
2. The image is blurry on Retina screens
An 800×450 "pixel" canvas on a high-density screen (phone, recent Mac) is stretched over twice as many real pixels: the result is blurry. The fix: multiply the internal resolution by window.devicePixelRatio, then apply the same scale to the drawing (ctx.setTransform). You still draw in logical "points", but the exported image is sharp.
3. A canvas is mute to a screen reader
Text drawn in a canvas doesn't really exist: it's paint, not text. A screen reader only "sees" an empty image. We fix it in two moves: the real field stays a <textarea> (accessible, it's the source), and we give the canvas role="img" + an aria-label reflecting the current message. The preview becomes understandable for everyone again.
Prompt 2: harden after review
Three gaps, three precisely-requested fixes:
Fix: (1) wrap the text over several lines with measureText so a long message doesn't overflow. (2) Account for window.devicePixelRatio (canvas.width = W * dpr, then ctx.setTransform) for a sharp image on Retina screens. (3) Keep a <textarea> as the accessible field, and set role="img" + an aria-label reflecting the text on the canvas.
// 2. Retina sharpness: we draw at a higher resolution
var dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr; canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // we then reason in logical points
// 1. line wrapping: we measure word by word
function wrapText(text, maxWidth) {
var words = text.split(' '), lines = [], line = '';
for (var i = 0; i < words.length; i++) {
var test = line ? line + ' ' + words[i] : words[i];
if (ctx.measureText(test).width > maxWidth && line) { lines.push(line); line = words[i]; }
else line = test;
}
if (line) lines.push(line);
return lines;
}
// 3. accessibility: the drawn text is announced too
canvas.setAttribute('aria-label', text);
The underlying rule, still true here: the tool only does what you tell it. HTML gave you gifts (the browser handled text and accessibility). The canvas makes you responsible for everything. The lower-level you go, the more human review matters.
Host and test
Static file, online in one drop. For testing, keep the usual reflexes, plus the canvas-specific ones:
- Type a long sentence: it must wrap over several lines, never overflow.
- Download the image and open it: on mobile/Retina, the text must be sharp, not pixelated.
- Change the background a few times: the white text stays readable (backgrounds are deliberately dark).
- Tab to the field; check the FR / EN toggle and the console (zero error).
The finished result
The full code (and downloadable)
The entire file, exactly the one running above.
Download the code (.html · 236 lines)
View the full code
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Carte en image (Canvas)</title>
<meta name="description" content="Écris un message, génère une carte et télécharge-la en image. Mini-projet construit avec l'IA — web-developpeur.com">
<meta name="robots" content="noindex, follow">
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<!--
============================================================================
CARTE EN IMAGE — un seul fichier : HTML + CSS + JS, zéro dépendance.
1. <style> : l'apparence (le canvas, le champ texte, les boutons).
2. <body> : la structure (champ + aperçu canvas + actions).
3. <script> : dessiner sur le canvas et exporter l'image en PNG.
Nouveauté du projet : le <canvas>, une zone où l'on DESSINE en JavaScript
(au lieu d'empiler des balises HTML). Trois pièges classiques à corriger :
le retour à la ligne (le canvas ne le fait pas tout seul), la netteté sur
écran Retina, et l'accessibilité (un canvas est une image muette).
============================================================================
-->
<style>
:root { --ink:#1a1d24; --muted:#5a6270; --accent:#267d42; --border:#e2e6ea; }
* { box-sizing:border-box; }
html,body { margin:0; padding:0; }
body { font-family:'Segoe UI',system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif; background:#f4f6f8; color:var(--ink); min-height:100vh; display:flex; flex-direction:column; align-items:center; padding:32px 18px 48px; line-height:1.5; }
.app { width:100%; max-width:560px; }
header { text-align:center; margin-bottom:20px; }
h1 { font-size:1.5rem; margin:0 0 6px; letter-spacing:-0.01em; }
.sub { color:var(--muted); font-size:0.95rem; margin:0 0 16px; }
.lang-switch { display:inline-flex; border:1.5px solid var(--border); border-radius:999px; overflow:hidden; background:#fff; }
.lang-btn { border:0; background:transparent; padding:7px 18px; font:inherit; font-weight:700; font-size:0.8rem; color:var(--muted); cursor:pointer; }
.lang-btn[aria-pressed="true"] { background:var(--accent); color:#fff; }
.lang-btn:focus-visible { outline:3px solid rgba(38,125,66,0.4); outline-offset:2px; }
/* Le canvas s'affiche en pleine largeur ; sa résolution interne est plus haute (Retina). */
canvas { width:100%; height:auto; display:block; border-radius:16px; box-shadow:0 16px 38px rgba(15,25,35,0.16); }
textarea { width:100%; margin-top:18px; min-height:80px; padding:12px 14px; border:1.5px solid var(--border); border-radius:12px; font:inherit; font-size:1rem; color:var(--ink); resize:vertical; }
textarea:focus-visible { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(38,125,66,0.12); }
.actions { display:flex; gap:12px; margin-top:14px; flex-wrap:wrap; }
.btn { flex:1 1 auto; min-width:150px; min-height:50px; border-radius:12px; border:1.5px solid transparent; font:inherit; font-weight:700; font-size:0.98rem; cursor:pointer; transition:transform 0.12s ease, background 0.2s ease, border-color 0.2s ease; }
.btn-primary { background:var(--accent); color:#fff; box-shadow:0 6px 16px rgba(38,125,66,0.28); }
.btn-primary:hover { background:#1f6a37; transform:translateY(-1px); }
.btn-ghost { background:#fff; color:var(--ink); border-color:var(--border); }
.btn-ghost:hover { border-color:var(--accent); color:var(--accent); }
.btn:focus-visible { outline:3px solid rgba(38,125,66,0.4); outline-offset:2px; }
.credit { text-align:center; color:var(--muted); font-size:0.78rem; margin-top:26px; }
.credit a { color:var(--accent); }
@media (max-width:480px){ .btn { flex-basis:100%; } }
@media (prefers-reduced-motion: reduce){ .btn { transition:none; } }
</style>
</head>
<body>
<main class="app">
<header>
<h1 data-i18n="title">Carte en image</h1>
<p class="sub" data-i18n="sub">Écris un message, choisis un fond, télécharge la carte en image.</p>
<div class="lang-switch" role="group" aria-label="Langue / Language">
<button class="lang-btn" data-lang-btn="fr" aria-pressed="true" type="button">FR</button>
<button class="lang-btn" data-lang-btn="en" aria-pressed="false" type="button">EN</button>
</div>
</header>
<!-- role="img" + aria-label : un canvas est une image. On y mettra le texte courant
pour qu'un lecteur d'écran sache ce qui est dessiné dessus. -->
<canvas id="canvas" role="img" aria-label=""></canvas>
<!-- Le vrai champ accessible, c'est ce textarea (le canvas n'est qu'un aperçu). -->
<textarea id="text" maxlength="120" data-i18n-ph="ph" placeholder="Ton message (ex. Code. Café. Répète.)" aria-label="Message de la carte"></textarea>
<div class="actions">
<button class="btn btn-ghost" id="bg" type="button" data-i18n="bg">Changer le fond</button>
<button class="btn btn-primary" id="dl" type="button" data-i18n="dl">Télécharger en PNG</button>
</div>
<p class="credit" data-i18n="credit">Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com</p>
</main>
<script>
(function () {
'use strict';
/* ---------------------------------------------------------------------------
1. LES TEXTES (FR / EN) + les fonds disponibles.
--------------------------------------------------------------------------- */
var UI = {
fr: { title:"Carte en image", sub:"Écris un message, choisis un fond, télécharge la carte en image.",
ph:"Ton message (ex. Code. Café. Répète.)", bg:"Changer le fond", dl:"Télécharger en PNG",
placeholder:"Ton message ici",
credit:'Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com' },
en: { title:"Image card", sub:"Write a message, pick a background, download the card as an image.",
ph:"Your message (e.g. Code. Coffee. Repeat.)", bg:"Change background", dl:"Download as PNG",
placeholder:"Your message here",
credit:'A mini-project from the <a href="/apprendre/projets/">Applied projects</a> course — web-developpeur.com' }
};
// Fonds CHOISIS, tous assez sombres pour que le texte blanc reste lisible.
var GRADIENTS = [
['#0f1923', '#267d42'], ['#1a1a2e', '#16213e'], ['#2d1b4e', '#5b2a86'],
['#0b3d2e', '#1e6f5c'], ['#3a1c40', '#7d2d52'], ['#102a43', '#334e68']
];
/* ---------------------------------------------------------------------------
2. LES ÉLÉMENTS + L'ÉTAT.
W et H = la taille LOGIQUE de la carte (en points). La résolution réelle
de l'image sera plus grande sur écran Retina (voir draw()).
--------------------------------------------------------------------------- */
var W = 800, H = 450;
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var textInput = document.getElementById('text');
var bgBtn = document.getElementById('bg');
var dlBtn = document.getElementById('dl');
var lang = 'fr';
try { lang = localStorage.getItem('card-lang') || 'fr'; } catch (e) {}
if (lang !== 'fr' && lang !== 'en') lang = 'fr';
var bgIndex = 0; // quel fond est utilisé
/* ---------------------------------------------------------------------------
3. DÉCOUPER LE TEXTE EN LIGNES.
Le canvas n'a PAS de retour à la ligne automatique : si on écrit une longue
phrase, elle dépasse. On mesure les mots un par un (measureText) et on passe
à la ligne quand on dépasse la largeur disponible.
--------------------------------------------------------------------------- */
function wrapText(text, maxWidth) {
var words = text.split(' ');
var lines = [], line = '';
for (var i = 0; i < words.length; i++) {
var test = line ? line + ' ' + words[i] : words[i];
if (ctx.measureText(test).width > maxWidth && line) {
lines.push(line); // la ligne est pleine : on la garde
line = words[i]; // et on commence la suivante
} else {
line = test;
}
}
if (line) lines.push(line);
return lines;
}
/* ---------------------------------------------------------------------------
4. DESSINER la carte (fond + texte + signature).
--------------------------------------------------------------------------- */
function draw() {
// Retina : on dessine à une résolution multipliée par devicePixelRatio,
// sinon l'image téléchargée serait floue sur les écrans haute densité.
var dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // on continue à raisonner en points logiques
// Le fond : un dégradé en diagonale.
var grad = GRADIENTS[bgIndex];
var g = ctx.createLinearGradient(0, 0, W, H);
g.addColorStop(0, grad[0]); g.addColorStop(1, grad[1]);
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// Le message, centré et découpé en lignes.
var text = textInput.value.trim() || UI[lang].placeholder;
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = "700 40px 'Segoe UI', system-ui, sans-serif";
var lines = wrapText(text, W - 140);
var lineHeight = 52;
var startY = H / 2 - (lines.length - 1) * lineHeight / 2;
lines.forEach(function (l, i) { ctx.fillText(l, W / 2, startY + i * lineHeight); });
// Une petite signature en bas.
ctx.globalAlpha = 0.85;
ctx.font = "600 20px 'Segoe UI', system-ui, sans-serif";
ctx.fillText('</> dev.card', W / 2, H - 44);
ctx.globalAlpha = 1;
// Accessibilité : un canvas est une image MUETTE pour un lecteur d'écran.
// On y attache le texte courant via aria-label, pour qu'il sache ce qui est affiché.
canvas.setAttribute('aria-label', text);
}
/* ---------------------------------------------------------------------------
5. TÉLÉCHARGER l'image — on convertit le canvas en fichier PNG.
--------------------------------------------------------------------------- */
function download() {
canvas.toBlob(function (blob) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = 'carte.png';
a.click();
URL.revokeObjectURL(url); // on libère la mémoire une fois le téléchargement lancé
}, 'image/png');
}
/* ---------------------------------------------------------------------------
6. LES ACTIONS + LE DÉMARRAGE.
--------------------------------------------------------------------------- */
textInput.addEventListener('input', draw); // on redessine à chaque frappe
bgBtn.addEventListener('click', function () {
bgIndex = (bgIndex + 1) % GRADIENTS.length; draw(); // fond suivant (et on boucle)
});
dlBtn.addEventListener('click', download);
function setLang(next) {
lang = next;
try { localStorage.setItem('card-lang', lang); } catch (e) {}
document.documentElement.lang = lang;
var t = UI[lang];
document.querySelectorAll('[data-i18n]').forEach(function (el) {
var k = el.getAttribute('data-i18n'); if (!t[k]) return;
if (k === 'credit') el.innerHTML = t[k]; else el.textContent = t[k];
});
var ph = document.querySelector('[data-i18n-ph]'); if (ph) ph.placeholder = t.ph;
document.querySelectorAll('[data-lang-btn]').forEach(function (b) {
b.setAttribute('aria-pressed', b.getAttribute('data-lang-btn') === lang ? 'true' : 'false');
});
draw(); // le texte par défaut change avec la langue : on redessine
}
document.querySelectorAll('[data-lang-btn]').forEach(function (b) {
b.addEventListener('click', function () { setLang(b.getAttribute('data-lang-btn')); });
});
setLang(lang); // applique la langue et dessine la première carte
})();
</script>
</body>
</html>
Your turn
Push the card further, keeping the canvas reflexes:
- Add a font size choice (and adapt the wrapping).
- Let the user drop a background image (careful: an image from another site "taints" the canvas and blocks the download — look up "tainted canvas / CORS").
- A square format (1:1) button for Instagram, in addition to the wide format.
On every addition: does the text overflow? does the image stay sharp? is the preview announced?
You can now draw a shareable image card right in the browser. Next up, you build your first organization tool: a Kanban board where you drag your cards from one column to the next.
Lesson 8: Kanban board →