Teinter les photos du site : trois solutions comparées

Document de travail, 29 et 30 septembre 2026. Il rassemble la proposition de Romain, les remarques de Sébastien, ce qu'on a mesuré, les trois solutions possibles et ce qui a été décidé. Chaque décision porte son état : validé fait proposé en cours en attente à trancher.

Mis à jour le 30/09 au soir.

Décision d'Emmanuel : pas de filtre sur le site. Les photos actuelles de la frise sont teintées à l'avance (solution A), avec les couleurs mesurées dans les fichiers de la maquette FINALE, et enregistrées en WebP : 33 fichiers, 6,3 Mo. Le détail, cadre par cadre : Les photos de la frise.

La solution B (filtre SVG), retenue le matin, n'est donc pas appliquée. Pour les photos que l'Alliance ajoutera plus tard, le choix est reporté : teinte par le serveur à l'envoi (A) ou filtre (B). Le reste de la page est gardé tel quel, pour la comparaison et les mesures.

1. En bref

2. Les mots techniques

LUT
Table de correspondance des couleurs : pour chaque couleur d'entrée, elle donne la couleur de sortie. Les photographes et les étalonneurs de cinéma s'en servent pour donner une ambiance à une image.
WebGL
Technique qui permet à une page web de faire calculer une image par la carte graphique (le GPU).
Shader
Petit programme exécuté par la carte graphique, ici une fois pour chaque pixel de la photo.
Canvas
Zone de dessin d'une page web (balise <canvas>). Contrairement à une balise <img>, elle n'a pas de texte alternatif.
Contexte WebGL
L'accès d'un canvas à la carte graphique. Chrome en garde 16 actifs au maximum par page.
Mémoire graphique
La mémoire de la carte graphique, où sont rangées les images à afficher (les « textures »).
Filtre SVG
Transformation d'image écrite en SVG, que le CSS peut appliquer à n'importe quel élément avec filter: url(#nom).
Delta E
Mesure de l'écart entre deux couleurs. Sous 1, aucune différence visible ; jusqu'à 2, à peine perceptible en comparant côte à côte.
WebP
Format d'image du web, bien plus léger que le JPEG ou le PNG, pour une qualité équivalente.
Premier écran
Temps pour que les photos visibles sans défiler soient toutes affichées.
GD, Imagick
Les deux bibliothèques de traitement d'images que PHP peut utiliser sur le serveur.

3. Le besoin

Teinte des photos par chapitre
ChapitreTeinte dans la FINALETeinte dans le prototype actuel
01 Contextebrunrouge (1 photo à changer)
02 Histoirebrunbrun
03 Contestationrougejaune, et une brune (5 photos à changer)
04 Le Quartierbleu (à vérifier)bleu
05 Aujourd'huijaunerouge (7 photos à changer)

4. La proposition de Romain

Sa démarche

  1. Dans Photoshop, des calques de réglage (courbes, balance des couleurs, teinte et saturation) créent l'ambiance voulue sur une image témoin.
  2. Ce réglage est exporté en LUT (format .cube, 64 niveaux par couleur), puis converti en une image PNG de 512 × 512 px que le navigateur sait lire.
  3. Un script WebGL (filtre-bleu.js, etc.) fait calculer par la carte graphique, pour chaque pixel de la photo, sa nouvelle couleur lue dans la LUT. La photo est remplacée par un canvas qui affiche le résultat.

C'est la méthode des professionnels de l'image, et le travail est soigné et bien documenté (README complet). Romain a pris comme référence les photos déjà teintées de l'ancienne maquette « Marais 2.0 », préparées par Imane : sa LUT devait reproduire ce rendu, et elle y parvient.

Le calcul est exact

Écart entre la LUT de Romain et l'image de référence
TeinteÉcart moyenVerdict
rouge0,10invisible
bleu0,67invisible
brun0,77invisible
jaune1,83 (44 % des pixels au-dessus de 2)visible par endroits, dans les sombres. Cause probable, d'après la date des fichiers, non confirmée : la LUT a été retouchée après coup. À clarifier avec Romain : laquelle des deux, la LUT ou l'image, est la bonne ?

Pourquoi le script ne peut pas aller tel quel sur le site

Testé dans Chrome sur la vraie frise (34 photos). Quatre problèmes bloquants, tous confirmés par une deuxième vérification :

  1. Un contexte WebGL par image, alors que Chrome en garde 16 au maximum. Au-delà, il supprime les plus anciens : sur 34 photos, 18 deviennent des rectangles blancs. Lesquelles dépend de l'ordre des scripts ; dans un essai, tout le début de la frise (Contexte et Histoire) avait disparu.
  2. Le canvas remplace la photo sans reprendre ses classes CSS. Il perd sa position : les photos s'empilent, et le contenu de la frise passe de 900 à 10 308 px de haut (la partie qui déborde est coupée).
  3. Le chemin de la LUT est lu à partir de la page, pas du script ('../lut_bleu.png'). À la racine du serveur, « ../ » ne peut pas remonter plus haut et le fichier est trouvé ; depuis prototype/, il pointe vers un dossier où la LUT n'existe pas. Le script ne signale rien : seule la console affiche une erreur 404.
  4. La photo est masquée avant le calcul. À la moindre erreur (image absente, image d'un autre site, page ouverte en double-clic quand la LUT est trouvée), elle disparaît.

Autres points relevés : près de 1 Go de mémoire graphique sur une page de test de 16 photos (77 Mo sans filtre) ; le texte alternatif (alt) disparaît pour les lecteurs d'écran ; les images haute densité (srcset) sont ignorées ; la teinte n'apparaît qu'après le chargement complet de la page, avec un flash de la photo d'origine ; quatre scripts identiques à trois lignes près (nom de la fonction, chemin de la LUT, sélecteur).

Grille de test de 34 photos : 16 photos teintées en rouge et en jaune, 18 rectangles blancs avec une icône d'image cassée.
Page de test, 34 photos avec le script tel quel : 18 contextes WebGL perdus, 18 rectangles blancs. Les teintes bleue et brune, créées en premier, ont toutes disparu. (Fond magenta : celui de la page de test.)

5. Les remarques de Sébastien

Sébastien a relu le script et envoyé dix remarques sur ses performances. Nous les avons toutes vérifiées, par une mesure ou par la lecture du code : cinq se confirment telles quelles, cinq se précisent.

Les dix remarques et leur vérification
RemarqueVérification
Chaque image crée son canvas, son contexte GPU et ses textures.Confirmé. 34 contextes et 68 textures pour 34 photos.
Une image de 4000 × 3000 pèse environ 48 Mo en mémoire, plus de 100 Mo avec les tampons.Confirmé, et même prudent : avec les réglages par défaut, jusqu'à 10 fois la taille de l'image. La photo 34 (5472 × 3648) ajoute plus de 1 Go à elle seule.
La LUT est recréée pour chaque canvas.Confirmé, mais impact faible : elle n'est téléchargée qu'une fois.
preserveDrawingBuffer: true augmente la mémoire, inutile ici.Précisé : inutile, oui, mais sans effet mesurable. Ce qui coûte, ce sont deux autres réglages actifs par défaut (antialias et depth) : les couper retire environ 80 %.
Un ::before + mix-blend-mode est plus léger.Précisé : plus léger, sans blocage mesuré. Avec un seul calque, la teinte est approximative (les blancs deviennent blanc pur au lieu de crème) ; avec deux calques (multiply puis screen), elle devient très proche (écart de 0,85 à 1,15 avec l'image de référence). Un ::before ne s'affiche pas sur une balise <img> : il faut le poser sur son conteneur.
Un filter CSS peut coûter cher.Précisé : vrai pour un flou ou un filtre animé. Pour notre filtre, aucun blocage mesuré sur le Mac de test ; son coût sur une machine modeste reste justement à vérifier (section 11).
Le shader convient à une image, pas à des dizaines avec un contexte chacun.Confirmé.
Appliquer la LUT en amont, sur les fichiers.Confirmé : c'est la solution la plus légère (solution A).
Si WebGL : un seul contexte, une seule LUT, à la demande, à la taille affichée.Précisé : juste ; il faut aussi couper antialias et depth, gérer la perte de contexte et la taille maximale des textures (solution C).
Optimiser les images, notamment les PNG.Précisé : l'impact est énorme, mais ce sont les JPEG qui pèsent le plus (77 % du poids des originaux) ; ceux du prototype sont en plus en qualité 94.

6. La découverte : un simple dégradé

Les quatre dégradés, lus dans les LUT de Romain : à gauche la couleur d'un pixel noir, à droite celle d'un pixel blanc.

Mosaïque des 34 photos de la frise, toutes teintées en bleu clair par le filtre SVG.
Mesure : les 34 photos de la frise passées dans le filtre SVG bleu, pour les comparer à la LUT bleue de Romain (test d'une seule teinte). Sur les quatre teintes, l'écart entre le filtre et la LUT est de 0,13 à 0,28 en Delta E, invisible à l'œil. Sur le site, chaque chapitre garde sa propre teinte.

7. Les trois solutions

Les trois partent des photos d'origine, non teintées, et reprennent les couleurs des LUT de Romain.

A. Teinte appliquée à l'avance

La piste de Sébastien : appliquer la LUT en amont.

Principe : la teinte est calculée une fois, sur le serveur ou par un script, et enregistrée dans le fichier. Le navigateur affiche une image déjà teintée, en WebP.

Dans WordPress : une petite extension crée, à l'enregistrement d'un événement, une version teintée de chaque photo selon sa rubrique.

Avantages

  • Le plus léger et le plus rapide : 34 photos en 3,7 Mo, premier écran en 0,64 s (grille) ou 0,28 s (frise) à 20 Mbit/s.
  • Aucun JavaScript, aucun calcul dans le navigateur : le rendu est identique dans tous les navigateurs.
  • La balise <img> reste intacte (alt, srcset, chargement différé).
  • La teinte est présente partout : impression, aperçus de partage.
  • Pour le client, rien de plus à faire : il envoie sa photo.

Inconvénients

  • Demande une petite extension WordPress (du code PHP). Nos LUT étant de simples dégradés, la bibliothèque GD, présente chez tout hébergeur, suffit : environ 80 ms par photo. Pour un événement avec beaucoup de photos, la génération doit se faire en arrière-plan.
  • Changer une teinte oblige à régénérer tous les fichiers.
  • Une photo utilisée dans deux rubriques demande deux fichiers.
  • Pas d'effet possible au survol (retour à la couleur d'origine) sans servir aussi l'original.

B. Filtre SVG en CSS retenue

Issue de l'analyse des LUT de Romain (et de la piste CSS de Sébastien).

Principe : on affiche la photo d'origine, en WebP. Un petit SVG invisible dans la page définit un filtre par teinte ; le CSS l'applique selon le chapitre.

Dans WordPress : l'Alliance envoie sa photo, WordPress en génère des tailles en WebP, et le gabarit indique la rubrique. Rien d'autre.

Avantages

  • Aucune extension à écrire : quelques lignes dans le thème suffisent. (Pour le client, A et B reviennent au même : il envoie sa photo.)
  • Aucun JavaScript ; la balise <img> reste intacte.
  • Reprend les couleurs exactes des LUT de Romain (33 valeurs par teinte).
  • Changer une teinte : remplacer trois lignes de valeurs dans le SVG (calculées par un petit script), rien à régénérer.
  • Une même photo peut prendre une teinte différente selon la page.
  • Effets possibles plus tard : retour à la couleur au survol (instantané) ; un fondu demande une astuce, à tester.
  • Compréhensible et modifiable par les étudiants (HTML et CSS).
  • 34 photos en 6,6 Mo, premier écran en 1,2 s (grille) ou 0,41 s (frise).

Inconvénients

  • Près de deux fois plus lourd que A (6,6 Mo contre 3,7 Mo), mais six fois plus léger que les originaux : le gros du gain vient déjà du WebP.
  • Le calcul est fait par le navigateur : aucun blocage mesuré pendant le chargement, mais le coût de rendu au défilement n'a pas été mesuré. À tester sur une machine modeste, dans Safari et dans Firefox.
  • Les aperçus de partage (réseaux sociaux) montrent la photo d'origine.
  • Deux règles à respecter : l'attribut color-interpolation-filters="sRGB" (sinon les sombres sont faux) et le SVG écrit dans la page, seule version testée (un filtre dans un fichier séparé n'est pas pris en charge par tous les navigateurs).
  • La génération des WebP par WordPress demande quelques lignes de PHP dans le thème (voir sections 9 et 11).

C. WebGL corrigé

Le script de Romain, retravaillé.

Principe : un seul contexte partagé, un calcul à la taille affichée, et le résultat remis dans la balise <img>.

Dans WordPress : un script à charger sur les pages concernées.

Avantages

  • Garde l'approche de Romain telle quelle (WebGL).
  • Accepte de vraies LUT 3D, si un jour un étalonnage complexe devient nécessaire.
  • Testé : les 34 photos s'affichent, sans blocage mesuré.

Inconvénients

  • Pas plus léger que B avec les mêmes photos en WebP, et la teinte attend le JavaScript : la photo d'origine apparaît d'abord, et rien n'est teinté sans JavaScript.
  • Code délicat : perte de contexte, taille maximale des textures, images d'un autre site.
  • Surdimensionné ici : nos LUT sont de simples dégradés, que B reproduit sans script.
  • Difficile à maintenir pour les étudiants.

8. Tableau comparatif

Mesures sur les 34 photos de la frise, dans Chrome seulement, sur un Mac très puissant (puce M4 Pro), avec une seule teinte (bleu) : les résultats sont optimistes pour une machine modeste. Firefox, Safari et les téléphones n'ont été testés pour aucune solution. Les temps sont donnés pour les photos en grille, puis dans la disposition de la frise.

Sur téléphone, faites glisser le tableau vers la gauche pour voir les solutions B et C.

Comparaison des solutions sur les 34 photos de la frise
CritèreScript de Romain tel quelA. Teinte à l'avanceB. Filtre SVG (retenue)C. WebGL corrigé
Photos affichées16 sur 3434 sur 3434 sur 3434 sur 34
Poids téléchargé42 Mo (photos actuelles)3,7 Mo6,6 Mo6,6 Mo avec les mêmes WebP que B ; 42 Mo avec les originaux
Premier écran à 20 Mbit/s (grille / frise)17,7 / 18,2 s0,64 / 0,28 s1,2 / 0,41 s1,4 / 0,67 s en WebP ; 8,0 / 5,5 s avec les originaux
Mémoire graphiqueenviron 2,8 Goentre 400 et 600 Mo environ pour A, B et C, comme les mêmes photos sans teinte (les écarts sont dans le bruit de mesure)
JavaScriptoui, avec des blocages jusqu'à 300 msnonnonoui
Texte alternatif, srcsetperdusconservésconservésconservés
Ce que fait le clientenvoyer sa photo, mais la frise ne s'affiche pas en entierenvoyer sa photoenvoyer sa photoenvoyer sa photo
Changer une teinterefaire la LUTrégénérer les fichiersmodifier le filtre (3 lignes)refaire la LUT
Même photo, teintes différentesouiun fichier par teinteouioui
Code à écrire dans WordPressnon adaptéune petite extension PHPla rubrique dans le gabarit, et quelques lignes de PHP pour le WebPun script
Maintenance par les étudiantsdifficilemoyennefaciledifficile
Encore inconnusans objettemps de calcul sur l'hébergement mutualiséfluidité sur une machine modeste ; rendu dans Safari et Firefoxautres navigateurs, cartes graphiques de téléphone

Les 42 Mo et les 17,7 s de la première colonne viennent des photos actuelles (JPEG en qualité 94), pas du script de Romain : sans aucun filtre, la frise pèse aussi 42 Mo. Le gain de A et B vient d'abord du passage en WebP.

de 42 à 6,6 Moles 34 photos de la frise en WebP (1600 px au plus), sans teinte ; 3,7 Mo teintées à l'avance.
de 37,7 à 10,6 Motoutes les images du prototype en WebP (-72 %).
34 sur 34photos affichées par A, B et C, contre 16 avec le script tel quel.
0,13 à 0,28écart en Delta E entre le filtre SVG et la LUT de Romain (invisible sous 1).

9. La solution retenue

validé le 30/09 remplacée le 30/09 au soir pour les photos actuelles Solution B : photo d'origine en WebP + filtre SVG en CSS selon le chapitre. Emmanuel a demandé de l'appliquer aux premiers blocs de la frise.

Ce que fera l'Alliance, dans WordPress

  1. Créer ou ouvrir un événement et choisir sa rubrique (champ déjà prévu).
  2. Envoyer la photo telle quelle (même une photo de téléphone de 5 Mo), avec son texte alternatif.
  3. Enregistrer. C'est tout : aucune retouche, aucune conversion.

Ce que fait le site, automatiquement

.evt[data-rubrique="contexte"] .image,
.evt[data-rubrique="histoire"] .image {
    filter: url(#teinte-brun);
}
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <filter id="teinte-brun" color-interpolation-filters="sRGB">
    <feColorMatrix type="matrix" values="0.30 0.59 0.11 0 0
                                         0.30 0.59 0.11 0 0
                                         0.30 0.59 0.11 0 0
                                         0    0    0    1 0"/>
    <feComponentTransfer>
      <feFuncR type="table" tableValues="0.247 0.259 0.275 ... 0.988 1"/>
      <feFuncG type="table" tableValues="0.149 0.165 0.18 ... 0.976 0.992"/>
      <feFuncB type="table" tableValues="0.086 0.102 0.118 ... 0.906 0.922"/>
    </feComponentTransfer>
  </filter>
</svg>

Dans le prototype, avant WordPress

Les photos d'origine sont converties une fois en WebP, par un script (outil cwebp). Le filtre s'applique ensuite exactement comme il le fera dans WordPress.

Solution de repli, si la frise manque de fluidité sur une machine modeste

proposé Solution A en automatique : une petite extension WordPress crée les versions teintées à l'enregistrement de l'événement. Pour l'Alliance, rien ne change : elle envoie toujours sa photo telle quelle.

10. Décisions des 29 et 30 septembre

Décisions et constats, avec leur état
DateDécision ou constatÉtat
29/09Analyse du prototype de Romain et des remarques de Sébastien, sans rien modifier : code, fidélité des couleurs, performance, alternatives, adéquation au projet.fait
30/09Le client ajoutera lui-même ses photos : on ne lui demande aucune retouche ni conversion.validé
30/09Solution B : photos en WebP, teinte par un filtre SVG en CSS selon le chapitre. Emmanuel demande de l'appliquer aux premiers blocs de la frise. Remplace la décision du 28/09 (aucun filtre ni teinte sur les images pour l'instant) et avance le passage en WebP, prévu « à la toute fin » le 24/09.validé
30/09Solution de repli si la fluidité ne suit pas : solution A automatique, par une extension WordPress.proposé
30/09La maquette FINALE fait foi pour la frise (« fais vraiment exactement comme dans le Figma »). Conséquence : les teintes par chapitre de la FINALE, brun, brun, rouge, bleu, jaune.validé (teintes déduites)
30/09Périmètre du premier essai de la solution B : chapitres Contexte et Histoire entiers (blocs 01 à 13, 18 photos). Dans ces deux chapitres, toutes les photos sont remplacées par leurs originaux et reçoivent le filtre. Les autres chapitres gardent pour l'instant leurs fichiers déjà teintés, sans filtre, pour ne pas les teinter deux fois.validé
30/09Conversion en WebP et filtre sur ces 18 photos : pas encore lancée, en attente de la liste des originaux.en attente
30/09On repart des photos d'origine, non teintées (dossier images_no_filtre), et non des fichiers teintés actuels. 28 photos sur 34 y ont leur original exact ; les autres sont recherchées (dernière ligne).validé
30/09Bloc 02 : on affiche la carte de Ferraris (1777) en brun, comme dans la FINALE, et non la vue aérienne du prototype actuel. Le fichier nommé « vue aérienne îlot Shell » dans le dossier est d'ailleurs cette carte.validé
30/09Bloc 12, première photo : le prototype affiche la vue aérienne de 1953, en double avec le bloc 10. Le dossier contient un fichier nommé 1996 (13_1996_vue-aerienne-depart-Shell.jpg), distinct de la vue de 1953 ; sa date reste à confirmer (les noms de fichiers de ce dossier sont parfois trompeurs).proposé
30/09Recherche des originaux des 34 photos de la frise (dossiers du client, SPIP en ligne, archives publiques), et liste précise de ceux qui manquent, à envoyer à Imane.en cours
30/09Pour mémoire, hors teintes : la frise occupe toute la hauteur de l'écran, et la bande des chapitres, en bas, est posée par-dessus les images, comme dans la maquette. Corrige la décision du 28/09 (bande sous les blocs).fait
30/09 (soir)Pour les photos actuelles de la frise : teinte appliquée à l'avance, dans les fichiers, sans filtre sur le site (demande d'Emmanuel). Le cas des photos que l'Alliance ajoutera plus tard est reporté.fait
30/09 (soir)Couleurs relevées dans les fichiers de la FINALE elle-même (photos déjà teintées dans leur fichier, aucun filtre Figma) : brun #412718, rouge #e7385c, bleu #51c4ed, jaune #a5c805, toutes vers la crème. Le bleu du Quartier est vérifié ; le jaune de la maquette est #a5c805 dans les sombres.fait
30/09 (soir)Inventaire des 34 cadres de la FINALE : 20 bons, 13 bouche-trous (12 photos déplacées pour leur couleur quand Contestation et Aujourd'hui ont échangé leurs teintes, et une vue de 1953 due à un fichier mal nommé), 1 doublon. Aucune photo ne manque pour la frise ; seul le cadre 17 (feuillage) n'a pas d'original non teinté, ce qui ne gêne pas tant que sa teinte ne change pas. Détail dans le dépôt : docs/PHOTOS-FRISE-FINALE.md.fait
30/09 (soir)Toutes les photos de la frise sont des WebP déjà teintés (33 fichiers, 6,3 Mo, contre 13,5 Mo pour les 34 JPEG remplacés), produits par docs/outils/teinter-photos.py. Choix des bouche-trous : vignette SPIP pour la destruction, photo placée par le SPIP pour les mesures sécuritaires, vue de 1996 confirmée par l'image elle-même, la végétation qui reprend (25/05/2025) pour clore la galerie de l'avis de la CRMS, l'atelier de juin 2025 pour « La lutte continue », jamais deux fois la même photo, sauf la carte de Ferraris que la maquette répète (cadres 01 et 03). Vérifié deux fois par des agents indépendants (rendu, choix, teinte).fait
30/09 (soir)Galerie de l'avis de la CRMS (bloc 26) : les photos suivent l'ordre chronologique au défilement (quinconce bas, haut, bas, haut) et gardent leurs proportions sur les écrans 4:3 et 5:4, où les banderoles étaient rognées.fait

11. Ce qui reste à trancher

Et pour Romain

12. Ce qu'on en retient

L'analyse détaillée (mesures, preuves, scripts d'essai) est conservée par le professeur.