Colorflow

Case study

Par où commencer ?

Un side-project, peu de contraintes, aller vers un domaine qui nous intéresse, qui nous motive et qui nous permettra de trouver un stage. Voilà ce qu’on savait de notre TFA début avril. Pour trouver une idée qui soit pertinente, j’ai listé mes attentes, ce que je savais déjà faire, ce que je voulais et ce que je pouvais apprendre en un temps relativement réduit.

J’ai de bonnes bases en code, particulièrement en JavaScript et je sais que je suis capable d’apprendre de nouveaux concepts rapidement, ainsi que de les mettre en application dans un autre contexte. Je suis très intéressé par le fait de coder plus proprement, c’est-à-dire en orienté objet. De plus, je veux me perfectionner en JavaScript Vanilla, donc sans passer par des libraires externes, et c’est pour moi la meilleure façon de comprendre un langage.

Je suis assez fasciné par les animations en général, j’aime qu’un site soit vivant, qu’il m’épate. Le plus difficile quand on design un site, c’est de trouver le juste milieu. Les animations doivent le servir et avoir une vraie plus-value, alors qu'il est facile d'en faire trop.

Enfin, je voulais travailler sur de nouvelles notions. J’ai eu un aperçu des canvas il y a de cela quelques mois (à dater d'avril 2018) et je m’y suis de plus en plus intéressé, jusqu’à me rendre compte de la place de plus en plus grande que prenait cet élément HTML sur les sites actuels, souvent couplé à des librairies adaptées.

Colorflow, le juste milieu

Après une (très) longue réflexion, j’ai trouvé un concept, si pas neuf, au moins intéressant et suffisamment solide à développer. L'idée derrière Colorflow est d'extraire une palette de couleurs d’une image donnée . Cela me permet de développer toute une série de compétences telles que :

Problèmes et solutions

Développer proprement

L'avantage du JavaScript, c'est qu'il est facile à coder. Il suffit de créer une fichier avec l'extension .js et de commencer à écrire. Le problème étant la tendance à avoir un code spaghetti une fois qu'il atteint les quelques dizaines ou centaines de lignes, c'est-à-dire qu'il devient peu clair car déstructuré et donc difficile à mettre à jour et à lire.

J'ai donc entrepris, avant de me lancer dans la programmation de Colorflow, de suivre un cours de JavaScript orienté objet sur Udemy où j'ai pu apprendre une meilleure manière de coder. À ce jour, je n'ai pas intrégré absolument toutes les notions, j'ai réussi à aboutir à avoir un code bien mieux structuré et maintenable. Je reviendrai sans doute sur ce cours (ou un autre) dans un futur proche afin d'assimiler ce qu'il me manque.

Trop de données

Comment obtenir une palette de couleur à partir de n'importe quelle image ? La réponse tient en plusieurs étapes :

  • redessiner l'image sur un canvas ;
  • récupérer les valeurs RGB de chaque pixel ;
  • appliquer un algorithme spécifique afin d'avoir des couleurs représentatives ;
  • afficher les couleurs.

Le premier problème vient du fait qu'une image contient énormément de pixels. Pour mes tests, j'ai utilisé un canvas de 600x375 pixels, ce qui me donne au total 900 000 données de couleur (chaque pixel comporte les valeurs rouge, verte, bleue et alpha)!

screenshot arrayBuffer

log de l'arrayBuffer d'une image 600x375px

En pratique, un canvas permet de récupérer quatre tableaux de valeurs (qui sont de type ArrayBuffer), mais seuls deux vont nous intéresser ici : Int32Array et Uint8Array. Ce dernier contient 900 000 valeurs qui sont triées par pixel [r1, g1, b1, a1, r2, g2, b2, a2, ...] et va nous servir pour l'algorithme. Quant à Int32Array, il contient 225 000 données et la seule chose importante à savoir, c'est qu'une valeur égale à zéro est un pixel noir.

Des 900 000 valeurs, il est déjà possible de réduire ce nombre de 25 % en enlevant la valeur alpha de transparence qui est toujours égale à 255 et qui n'a donc aucune utilité.

Je me retrouve donc avec un nombre de pixels qui reste conséquent. Après mes tests, les calculs pouvant prendre plusieurs secondes, j'ai pris la décision de créer un second canvas, invisible à l'écran, qui a une taille de 100 pixels au maximum (largeur ou hauteur), ce qui réduit sa surface drastiquement et rend le calcul de la palette bien plus court (de l'ordre de la centaine de millisecondes au maximum).

En revanche, les résultats entre un canvas ayant les dimensions de l'image originale et un canvas de 100 pixels maximum sont différents. C'est pourquoi j'ai introduit un bouton de précision sur la page afin de laisser le choix à l'utilisateur (100 ou 800 pixels maximum).

Comparaison des résultats par surface de canvas
  • 1000px * 625px
  • 67.6 %
  • 13.1 %
  • 9.1 %
  • 4.7 %
  • 2.4 %
  • 1.5 %
  • 0.9 %
  • 0.6 %
  • 66.1 %
  • 12.9 %
  • 9.5 %
  • 5.9 %
  • 2.4 %
  • 1.5 %
  • 0.9 %
  • 0.7 %
  • 65.9 %
  • 12.6 %
  • 12.0 %
  • 3.9 %
  • 2.4 %
  • 1.6 %
  • 0.9 %
  • 0.7 %
  • 70 %
  • 12.6 %
  • 11.9 %
  • 2.4 %
  • 1.2 %
  • 1.0 %
  • 0.7 %
  • 0.1 %

La différence entre le format 1 000 et 600 est vraiment négligeable, où seules certaines couleurs changent légèrement. Par contre, on peut s'apercevoir qu'une petite taille de canvas influe beaucoup sur le rendu.

Median-cut algorithm

Avant d'arriver à tous mes tests de performance, il m'a fallu construire l'algorithme qui est basé sur le median-cut de Paul Heckbert.

Métaphoriquement, le principe est de mettre tous les pixels dans un seau, puis de calculer la plus longue distance des valeurs r, g et b. La distance, c'est la différence entre la valeur maximale et minimale de tous les pixels.

Par exemple, si la distance du rouge est à 200, celle du vert à 198 et le bleu à 180, alors le median cut sera 200. C'est à ce stade que le seau initial est divisé en deux nouveaux seaux, où chaque pixel est transvasé selon le median cut. Si l'on fait la moyenne des valeurs r, g et b de chaque seau, on obtient deux couleurs. Et si l'on veut quatre couleurs, on refait l'opération sur chaque sous-seau.

Et c'est ici que le problème de division apparaît. Si je divise chaque seau en deux, j'aurai toujours un nombre de couleurs équivalent à 2n. Mais que se passe-t-il si je veux trois, cinq, six couleurs ? J'ai pris la décision, la plus simple et correcte de mon point de vue, de diviser à chaque fois le seau qui a le plus grand median cut, puisque c'est c'est là que se trouve la plus grande variance.

Des résultats étonnants

Quelques exemples

Si appliquer un algorithme, c'est aussi être capable de le comprendre, il n'en reste pas moins frustrant de voir que les résultats ne sont pas toujours à la hauteur de ce que l'on attend. Et particulèrement dans le cas de Colorflow, si les couleurs calculées ne correspondent pas à notre image, alors personne ne l'utilisera, car il est évident que la précision est primordiale.

Je ne suis pas arrivé dès la première itération de mon code à obtenir des couleurs fidèles. Par exemple, après avoir lu plusieurs sources expliquant la méthode de median-cut, j'en avais déduis que la division des seaux devait se faire équitablement, et non par la valeur median-cut, peu importe le nombre de pixels dans chaque subdivision. Les résultats obtenus étaient alors souvent fades, et les petites parties colorées de l'image ainsi que les couleurs pétantes n'étaient jamais présentes.

Comparaison avec la concurrence

Le plus étonnant est la différence entre mes résultats et ceux des sites concurrents. J'ai en général de bien meilleurs résultats, en particulier lorsqu'il s'agit de récupérer une couleur peu présente mais bien visible.

En regardant les codes de ces sites, je me suis aperçu que la plupart utilisait une libraire externe, quantize.js, pour effectuer le calcul. Cette librairie est bien plus complète que ce que j'ai codé, mais les résultats à la sortie sont moins bons. Je n'ai pas testé cette méthode sur mon site, et il s'agit peut-être simplement de mauvais paramétrage.

Une image parle mieux que des mots

J'ai effectué plusieurs palettes d'une même image sur différents sites et je les ai compilés pour vous donner une idée des différences qu'il peut y avoir. Ces sites n'offrent pas de possiblité de choisir le nombre de couleurs, j'ai donc ajusté ma palette aux leurs.

image abstraite de test
  • Colorflow
  • Colorflow

Rendre l'algorithme utilisable

La zone de sélection

Avant de m'intéresser au design du site, il me restait une dernière fonctionnalité à implémenter : la zone de sélection. Dès le début, j'ai voulu donner l'opportunité à l'utilisateur de sélectionner une partie de l'image, car il n'est pas dit qu'il veuille la palette de l'entièreté de l'image.

Malheureusement, je ne pouvais pas utiliser le canvas de l'image pour dessiner un rectangle de sélection car il aurait effacé les pixels correspondants et j'ai donc dû remettre un nouveau canvas transparent par dessus. J'ai dû faire face à plusieurs problèmes, comme le fait de devoir réinitialiser le canvas à chaque fois que l'utilisateur tente une sélection ou faire correspondre la sélection au canvas en dessous, puisqu'en fait, je sélectionne du vide.

Mais le problème le plus important est le fait de pouvoir continuer la sélection même si le curseur est en dehors du canvas. Ça peut paraître futile, mais c'est une situation de frustration pour l'utilisateur et donc de mauvaise expérience, ce que je veux éviter. Je me suis obstiné à prendre mon canvas comme référence ce qui m'a amené à beaucoup de frustration.

Pour calculer le rectangle à dessiner, il faut soustraire la position du curseur à la position de départ (lors du clic). Mais lorsque le curseur arrivait hors des limites du canvas, les positions x et y devenaient celles de l'élement en hover, ce qui empêchait une utilisation hors canvas. Pour y remédier, j'ai eu la brillante idée, deux mois plus tard, de calculer la position par rapport au body, ce qui résout tous les problèmes. Parfois, les choses simples sont les plus difficiles à trouver.

Un problème de ratio

Lors de mes essais de rendre le site responsive, un problème est apparu. Si la zone de sélection fonctionne très bien sur le canvas orginal, le rectangle se dessinait avec un décalage lorsque le canvas changeait de dimension par le CSS.

Pour dessiner une rectangle, il faut quatre valeurs : les points de départ en x et en y, ainsi que la largeur et la hauteur. Toutes ces données se décalaient inexplicablement dès que l'image s'agrandissait, ce qui m'a valu beaucoup d'incompréhension.

Au final, j'ai remarqué que le décalage s'amplifiait plus le rectangle grandissait. Le problème est le fait que le dessin se construit par rapport aux dimensions originales, et pour y remédier, il faut calculer un ratio entre les valeurs des deux canvas (l'ancien et l'actuel).

Le design

Première idée, premiers mauvais choix

La partie délicate de mon projet, c'est bien l'interface et le design du site. Mon code fonctionne, mais il doit être mis en valeur par une vitrine tout en étant utilisable et agréable à naviguer. Mon site devait réunir plusieurs impératifs :

  • La palette de couleurs doit être visible en permanence ;
  • Les codes couleurs doivent être explicitement affichés ;
  • Les paramètres doivent être facilement utilisables et explicites ;
  • Les codes doivent pouvoir être copiés et cette fonction doit être explicite ;
  • L'utilisateur doit savoir qu'il est possible de dessiner une zone de sélection sur le canvas ;
  • La quantité et le nom de chaque couleur doivent être visibles ;
  • Le site doit uniquement servir à promouvoir l'outil développé.

Ma première démarche a été de placer l'image au centre de la page et de l'encadrer. Pourquoi l'encadrer ? Pour rappel, lors du développement de la zone de sélection, j'étais focalisé sur le canvas et j'avais trouvé un moyen de pouvoir sortir de cette zone afin de continuer à la dessiner, mais cela causait des bugs lorsque le curseur touchait un autre élément.

J'ai donc cherché à créer une «safe zone» autour de l'image. Les couleurs étaient disposées en grille sous l'image avec sa quantité, son code hexadécimal et son nom. J'avais imaginé de dévoiler les codes sur la tuile qui se serait alors retournée. Enfin, après quelques itérations, j'ai placé un cadre avec quelques lignes d'explication de l'outil.

premier design de Colorflow

Dernière itération avant de repartir de zéro.

design final de Colorflow

Une des dernières itérations avant le passage au code. La première roue a disparu au profit d'un switch.

Changement de cap

Après plusieurs discussions, des problèmes évidents de design ont été mis en lumière. Si expliquer le fonctionnement du site peut sembler louable, il révèle surtout un mauvais design. Un site bien conçu doit être auto-suffisant, c'est-à-dire que l'utilisateur doit comprendre ce qu'il doit faire sans devoir l'obliger à lire une explication au préalable.

Le cadre explicatif a aussi l'inconvénient de prendre une place significative sur la page. De l'autre côté, centrer tous le contenu fait perdre beaucoup de place et oblige le scroll pour accéder aux couleurs. Ceci peut devenir problématique lorsque l'on veut passer des couleurs aux paramètres de l'image.

J'ai alors décidé de repartir de zéro. Les quelques références telles que Adobe Colors et Coolors n'ont peu ou pas de scroll, tout est visible directement. Sachant que mon outil n'a que peu d'éléments (paramètre de précision, nombre de couleurs, tuiles de couleurs, codes et le canvas), il me semblait plus approprié de tout placer sur la largeur de la page, sans permettre de scroll (en version bureau du moins).

Pour simplifier l'utilisation, j'ai ajouté un overlay sur le canvas pour indiquer qu'il est possible de drag & drop une image. En dessous, j'ai choisi de laisser une explication sur la sélection de zone. Enfin, le nombre de couleurs est représenté par une roue et la précision par un switch. À noter que le switch était initialement une roue, mais n'était pas suffisamment explicite pour l'utilisateur, là où un choix binaire est plus clair.

Les couleurs sont disposées à la droite du canvas et sont visibles en permanence. Qu'il y en ait deux ou dix, les tuiles gardent la même dimension et sont triées par quantité. Pour terminer, les codes ne s'affichent qu'au clic d'une tuile, mais le squelette est toujours visible pour indiquer que oui, le site permet de les récupérer.