Ce POC trie les pixels d’une image par couleur, comme l’application Attracteurs étranges déformait une image en suivant la trajectoire d’un attracteur mathématique. Mais l’important ici n’est pas l’application. C’est l’environnement, minimal, et la méthode pour construire ce genre de projet avec l’IA, même sans bagage en code.
Le principe du POC
Un fichier HTML unique, sans dépendance externe. Il charge une image, la lit pixel par pixel dans un canvas caché, trie ces pixels par teinte puis par luminosité, et affiche le résultat à côté de l’image d’origine. Tout se passe dans le navigateur, aucune image n’est envoyée à un serveur.
C’est volontairement un projet minimal, pas une application finie. Le but est de montrer qu’on peut obtenir un résultat fonctionnel, rapide et sécurisé, en une poignée d’échanges avec l’IA.
Ci-dessous sur un écran en mode portrait :

ou en mode paysage, avec une autre image

Le cahier des charges avant de coder
Avant d’écrire la moindre ligne de code, la première chose à faire est de poser un cahier des charges, même court. Trois questions suffisent pour ce genre de petit projet :
- Que doit faire l’application, précisément ?
- Quelles contraintes techniques et de sécurité s’appliquent (aucune donnée envoyée au serveur, un seul fichier autonome) ?
- Sur quels supports doit-elle fonctionner (ordinateur, téléphone) ?
Ce dernier point vient d’une leçon apprise sur le projet Attracteurs : l’adaptation au téléphone n’avait pas été anticipée dans le cahier des charges initial, et n’a été détectée qu’en testant sur téléphone après coup. Pour ce projet, la contrainte responsive a été posée dès le départ.
Autre principe utile : ne pas réinventer ce qui existe déjà. Le chargement sécurisé d’image et la lecture des pixels via un canvas caché avaient déjà été validés sur le projet Attracteurs. Il n’y avait aucune raison de repartir de zéro sur ce point, seule la logique de tri était vraiment nouvelle.
Discuter avec l’IA pour jauger la complexité
Une fois le tri de base posé, une idée est venue en cours de route : permettre de survoler l’image triée à la souris pour afficher la couleur du pixel pointé, en RGB et en HSL. Une bonne question à se poser à ce moment-là : est-ce simple à ajouter, ou est-ce que ça mérite d’être traité à part ?
La réponse, discutée avec l’IA, a été de séparer le projet en deux étapes. L’étape A, le tri lui-même, suffisante pour valider le principe. L’étape B, le survol interactif, repoussée à une session suivante. Deux raisons à ce choix : tester une chose à la fois évite de devoir déboguer plusieurs nouveautés en même temps, et une fonctionnalité tactile (le survol au doigt pose des questions différentes de la souris) mérite sa propre réflexion plutôt que d’être ajoutée dans la précipitation.
C’est ce genre d’arbitrage qui fait la différence entre un prompt unique qui tente de tout faire d’un coup, et une conversation qui avance par étapes validées.
Le code complet
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tri de pixels par couleur</title>
<style>
:root {
--bg: #0b0d12;
--panel: rgba(20, 22, 30, 0.9);
--line: rgba(255, 255, 255, 0.10);
--txt: #e4e5ea;
--dim: #9497a3;
--accent: #2979ff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
body {
background: var(--bg);
color: var(--txt);
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
font-size: 14px;
padding: 16px;
min-height: 100vh;
}
h1 {
font-size: 15px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--dim);
margin-bottom: 14px;
}
h1 b { color: var(--accent); font-weight: 600; }
#panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
padding: 14px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
input[type=file] { color: var(--dim); max-width: 100%; }
.btn {
padding: 8px 14px;
border: 1px solid var(--line);
border-radius: 7px;
background: rgba(255,255,255,0.06);
color: var(--txt);
font-size: 13px;
cursor: pointer;
}
.btn:hover { border-color: var(--accent); }
#status {
color: var(--dim);
font-size: 13px;
}
#resultats {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
figure {
flex: 1 1 380px;
min-width: 0;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px;
}
figure canvas {
display: block;
width: 100%;
height: auto;
border-radius: 6px;
background: #000;
}
figcaption {
color: var(--dim);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 8px;
}
@media (max-width: 820px) {
#resultats { flex-direction: column; }
}
</style>
</head>
<body>
<h1>Tri de pixels par <b>couleur</b></h1>
<div id="panel">
<input type="file" id="file" accept="image/*">
<button type="button" class="btn" id="demo" hidden>Image de démonstration</button>
<span id="status">Choisis une image pour commencer.</span>
</div>
<div id="resultats" hidden>
<figure>
<figcaption>Image d'origine</figcaption>
<canvas id="cvOriginal"></canvas>
</figure>
<figure>
<figcaption>Pixels triés par teinte, puis luminosité</figcaption>
<canvas id="cvTrie"></canvas>
</figure>
</div>
<script>
const ui = {
file: document.getElementById("file"),
demo: document.getElementById("demo"),
status: document.getElementById("status"),
resultats: document.getElementById("resultats"),
cvOriginal: document.getElementById("cvOriginal"),
cvTrie: document.getElementById("cvTrie"),
};
const TAILLE_MAX = 1200; // plus grande dimension autorisée pour la taille de travail
const DEMO_DIR = "demo/";
/* ============================================================
Chargement d'une image choisie par l'utilisateur
============================================================ */
ui.file.addEventListener("change", () => {
const f = ui.file.files && ui.file.files[0];
if (!f) return;
const url = URL.createObjectURL(f);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
traiterImage(img);
};
img.onerror = () => {
URL.revokeObjectURL(url);
ui.status.textContent = "Impossible de lire ce fichier image.";
};
img.src = url;
});
/* ============================================================
Images de démonstration
Le manifeste n'est lu que si l'application est servie en
http(s). En ouverture directe du fichier (file://), l'appel
échoue silencieusement et le bouton reste caché : rien ne
bloque, l'utilisateur charge simplement sa propre image.
============================================================ */
let demoListe = [];
fetch(DEMO_DIR + "manifest.json")
.then(r => r.ok ? r.json() : [])
.then(liste => {
if (Array.isArray(liste) && liste.length) {
demoListe = liste;
ui.demo.hidden = false;
}
})
.catch(() => {});
ui.demo.addEventListener("click", () => {
if (!demoListe.length) return;
const nom = demoListe[Math.floor(Math.random() * demoListe.length)];
const img = new Image();
img.onload = () => traiterImage(img);
img.onerror = () => { ui.status.textContent = "Impossible de charger l'image de démonstration."; };
img.src = DEMO_DIR + nom;
});
/* ============================================================
Redimensionnement interne
La plus grande dimension de l'image est ramenée à TAILLE_MAX
si besoin, pour garder le tri fluide sur tous les appareils.
Le rapport largeur/hauteur d'origine est conservé.
============================================================ */
function calculerTailleTravail(largeur, hauteur) {
const plusGrand = Math.max(largeur, hauteur);
if (plusGrand <= TAILLE_MAX) return { w: largeur, h: hauteur };
const ratio = TAILLE_MAX / plusGrand;
return {
w: Math.max(1, Math.round(largeur * ratio)),
h: Math.max(1, Math.round(hauteur * ratio)),
};
}
/* ============================================================
Conversion RGB vers HSL
Seules la teinte (h, 0-360) et la luminosité (l, 0-1) sont
utilisées ici comme critères de tri.
============================================================ */
function rgbVersHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const l = (max + min) / 2;
if (max === min) return { h: 0, l };
const d = max - min;
let h;
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)); break;
case g: h = ((b - r) / d + 2); break;
default: h = ((r - g) / d + 4);
}
h *= 60;
return { h, l };
}
/* ============================================================
Traitement principal : lecture, tri, reconstruction, affichage
============================================================ */
function traiterImage(img) {
ui.status.textContent = "Traitement en cours...";
ui.resultats.hidden = true;
// laisse le navigateur afficher le message de statut avant
// de démarrer le calcul, potentiellement bloquant
requestAnimationFrame(() => {
const { w, h } = calculerTailleTravail(img.naturalWidth, img.naturalHeight);
const n = w * h;
// canvas caché pour la lecture des pixels, image redimensionnée
const cvLecture = document.createElement("canvas");
cvLecture.width = w;
cvLecture.height = h;
const ctxLecture = cvLecture.getContext("2d", { willReadFrequently: true });
ctxLecture.drawImage(img, 0, 0, w, h);
const donnees = ctxLecture.getImageData(0, 0, w, h).data;
// teinte et luminosité de chaque pixel, calculées une seule fois
const teintes = new Float32Array(n);
const luminosites = new Float32Array(n);
for (let i = 0; i < n; i++) {
const j = i * 4;
const { h: teinte, l } = rgbVersHsl(donnees[j], donnees[j + 1], donnees[j + 2]);
teintes[i] = teinte;
luminosites[i] = l;
}
// indices des pixels triés par teinte, puis par luminosité
const indices = new Uint32Array(n);
for (let i = 0; i < n; i++) indices[i] = i;
const indicesTries = Array.from(indices).sort((a, b) => {
if (teintes[a] !== teintes[b]) return teintes[a] - teintes[b];
return luminosites[a] - luminosites[b];
});
// reconstruction de l'image triée : les pixels triés sont replacés
// dans l'ordre de lecture habituel (ligne par ligne), en gardant
// leur couleur exacte d'origine
const imageTrie = ctxLecture.createImageData(w, h);
const sortie = imageTrie.data;
for (let i = 0; i < n; i++) {
const source = indicesTries[i] * 4;
const cible = i * 4;
sortie[cible] = donnees[source];
sortie[cible + 1] = donnees[source + 1];
sortie[cible + 2] = donnees[source + 2];
sortie[cible + 3] = donnees[source + 3];
}
// affichage : image d'origine
ui.cvOriginal.width = w;
ui.cvOriginal.height = h;
ui.cvOriginal.getContext("2d").drawImage(img, 0, 0, w, h);
// affichage : image triée
ui.cvTrie.width = w;
ui.cvTrie.height = h;
ui.cvTrie.getContext("2d").putImageData(imageTrie, 0, 0);
ui.resultats.hidden = false;
ui.status.textContent = `Image traitée : ${w} x ${h} px (${n.toLocaleString("fr-FR")} pixels).`;
});
}
</script>
</body>
</html>
Pour les images de démonstration, un dossier demo/ contient un fichier manifest.json listant simplement leurs noms :
json
["falaise.jpg", "port-breton.png"]
S’il est absent, rien ne bloque, le bouton « Image de démonstration » reste caché et le visiteur charge sa propre image.
Tester en local, sans rien connaître
Trois façons de servir ce fichier avant de le mettre en ligne, du plus accessible au plus technique.
La solution VSCodium, ou VSCode (non testée), avec l’extension Live Server, est la plus simple si tu ne veux rien installer d’autre qu’un éditeur de code. Un clic droit sur le fichier HTML, « Open with Live Server », et la page s’ouvre dans le navigateur avec rechargement automatique à chaque modification.
Un serveur Python local, sur une machine où Python est déjà installé. Dans le dossier contenant le fichier :
python3 -m http.server 8000
Puis ouvrir http://localhost:8000/tri-pixels-couleur.html dans le navigateur. Si le serveur tourne sur une autre machine du même réseau local (un boîtier Linux par exemple), remplacer localhost par l’adresse IP locale de cette machine, du type http://192.168.x.x:8000/tri-pixels-couleur.html.
Sous Windows, la même commande peut nécessiter d’installer Python au préalable (depuis python.org, pas le Microsoft Store), cette option n’a pas été testée dans le cadre de ce projet.
Mettre en ligne, gratuitement
J’ai testé et validé deux options pour ce projet.
option Page Github
GitHub Pages, pour un partage public et gratuit, indépendant de tout hébergement personnel :
- Créer un compte GitHub si besoin (github.com, gratuit)
- Créer un dépôt public, par exemple
tri-pixels-couleur, sans rien cocher (pas de README, pas de licence) - Dans le dépôt, « Add file » puis « Upload files », déposer le fichier HTML renommé en
index.html, et si besoin le dossierdemo/avec son manifeste et ses images - Dans « Settings » puis « Pages », section « Build and deployment », choisir « Deploy from a branch », branche
main, dossier/ (root) - L’URL publique apparaît en haut de cette même page après une à deux minutes, du type
https://ton-pseudo.github.io/tri-pixels-couleur/.
L’application testée tourne sur une page github de mon dépôt : https://aldelpech.github.io/tri-pixels-couleur/

option sous-répertoire d’un site web existant
Un sous-répertoire de son propre site, avec un vrai hébergement web. Même principe que l’application Attracteurs étranges : déposer le fichier HTML renommé en index.html dans un dossier dédié à la racine du site (via FTP ou le gestionnaire de fichiers de l’hébergeur), avec le dossier demo/ à côté si besoin. L’URL devient propre automatiquement, sans conflit avec les permaliens WordPress. Pour l’application Attracteurs étranges, ça fonctionne parfaitement ici : https://knowledge.parcours-performance.com/attracteurs/

C’est quoi le vibe coding ?
Le terme désigne une façon de coder en dialoguant avec une IA en langage naturel, plutôt qu’en écrivant soi-même chaque ligne. On décrit ce qu’on veut, l’IA propose du code, on teste, on ajuste, on recommence.
Ce projet en est un exemple assez pur : aucune ligne de ce fichier n’a été tapée à la main, tout est passé par une conversation structurée, cahier des charges d’abord, questions une par une, validation avant chaque étape de code.
C’est aussi une bonne méthode pour construire un POC (proof of concept, ou preuve de concept en français. On pourrait aussi dire prototype), c’est-à-dire une version minimale d’une idée, juste assez fonctionnelle pour vérifier qu’elle tient debout, avant d’envisager d’aller plus loin. Cette expérimentation en est un exemple : elle ne visait pas à produire un outil fini, seulement à valider rapidement plusieurs options sécurisées pour mettre en ligne des applications en html et javascript.
Ce principe s’applique à beaucoup d’autres petits projets, créatifs ou utiles. Quelques idées pour continuer à s’entraîner :
- Un générateur de palette de couleurs à partir d’une photo
- Une page de calcul simple pour une TPE, devis, marge, conversion d’unités
- Un petit générateur de mots de passe ou de QR codes, entièrement local
- Une visualisation ludique de données personnelles, dépenses, lectures, trajets
Commentaires récents