Bonjour à tous, aujourd'hui, nous allons développer une application qui détermine la couleur moyenne d'une image dans un flux séparé et afficher un aperçu de l'image (utile lors de la création de formulaires de téléchargement d'images).
Il s'agit d'une nouvelle série d'articles qui s'adresse principalement aux débutants. Je ne sais pas si un tel matériel serait intéressant, mais j'ai décidé de l'essayer. Si ça va, je vais tourner des vidéos, pour ceux qui préfèrent absorber les informations visuellement.
Pourquoi?
Il n'y a pas de besoin urgent pour cela, mais la définition des couleurs d'une image est souvent utilisée pour:
- Recherche par couleur
- Détermination de l'arrière-plan de l'image (si elle n'occupe pas tout l'écran, afin d'être en quelque sorte combinée avec le reste de l'écran)
- Vignettes colorées pour optimiser le chargement de la page (afficher la palette de couleurs au lieu de l'image compressée)
Nous utiliserons:
- Manuscrit
- Réagissez avec l'application Create React - pourquoi pas? Nous créerons rapidement un environnement de travail et pourrons construire notre projet
- HTML Drag and Drop API - pour faire glisser une image du bureau vers le navigateur
- Travailleurs Web et Greenlet - pour intégrer des calculs complexes dans un thread séparé
- noms de classe
- API de fichier
- URL de données
Entraînement
Avant de commencer à coder, découvrons les dépendances. Je soupçonne que vous avez Node, js et NPM / NPX, alors passons directement à la création d'une application React vierge et à l'installation des dépendances:
npx create-react-app average-color-app --template typescript
Nous obtiendrons un projet avec la structure suivante:
Pour démarrer le projet, vous pouvez utiliser:
npm start
Toutes les modifications actualiseront automatiquement la page dans le navigateur.
Ensuite, installez Greenlet:
npm install greenlet
Nous en reparlerons un peu plus tard.
Glisser déposer
Bien sûr, vous pouvez trouver une bibliothèque pratique pour travailler avec le glisser-déposer, mais dans notre cas, ce sera superflu. L'API Drag and Drop est très facile à utiliser et pour notre tâche de «capture», l'image suffit à nos têtes.
Tout d'abord, supprimons tout ce qui est inutile et créons un modèle pour notre "zone de dépôt":
App.tsx
import React from "react";
import "./App.css";
function App() {
function onDrop() {}
function onDragOver() {}
function onDragEnter() {}
function onDragLeave() {}
return (
<div className="App">
<div
className="drop-zone"
onDrop={onDrop}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
></div>
</div>
);
}
export default App;
Si vous le souhaitez, vous pouvez séparer la zone de dépôt en un composant séparé, pour plus de simplicité, nous le laisserons ainsi.
Parmi les choses intéressantes, il convient de prêter attention à onDrop, onDragEnter, onDragLeave.
- onDrop - écouteur pour l'événement de dépôt, lorsque l'utilisateur relâche la souris sur cette zone, l'objet en cours de glissement sera "déposé".
- onDragEnter - lorsque l'utilisateur fait glisser un objet dans la zone de glisser-déposer
- onDragLeave - l'utilisateur a éloigné la souris
Le travailleur pour nous est onDrop, avec l'aide de celui-ci, nous recevrons une image de l'ordinateur. Mais nous avons besoin de onDragEnter et onDragLeave pour améliorer l'UX, afin que l'utilisateur comprenne ce qui se passe.
Quelques CSS pour la zone de dépôt:
App.css
.drop-zone {
height: 100vh;
box-sizing: border-box; // , .
}
.drop-zone-over {
border: black 10px dashed;
}
Notre UI / UX est très simple, l'essentiel est d'afficher la bordure lorsque l'utilisateur fait glisser l'image sur la zone de dépôt. Modifions un peu notre JS:
/// ...
function onDragEnter(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault();
e.stopPropagation();
setIsOver(true);
}
function onDragLeave(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault();
e.stopPropagation();
setIsOver(false);
}
return (
<div className="App">
<div
className={classnames("drop-zone", { "drop-zone-over": isOver })}
onDrop={onDrop}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
></div>
</div>
);
/// ...
En écrivant, j'ai réalisé qu'il ne serait pas superflu de montrer l'utilisation du package classnames. Facilite souvent le travail avec les classes dans JSX.
Pour l'installer:
npm install classnames @types/classnames
Dans l'extrait de code ci-dessus, nous avons créé une variable d'état locale et écrit la gestion des événements over and Leave. Malheureusement, il s'avère un peu inutile à cause de e.preventDefault (), mais sans cela, le navigateur ouvrira simplement le fichier. Et e.stopPropagation () nous permet de nous assurer que l'événement ne dépasse pas la zone de dépôt.
Si isOver est true, une classe est ajoutée à l'élément de zone de dépôt qui affiche la bordure:
Aperçu de l'image
Afin d'afficher l'aperçu, nous devons gérer l'événement onDrop en recevant un lien ( URL de données ) vers l'image.
FileReader nous aidera avec ceci:
// ...
const [fileData, setFileData] = useState<string | ArrayBuffer | null>();
const [isLoading, setIsLoading] = useState(false);
function onDrop(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault();
e.stopPropagation();
setIsLoading(true);
let reader = new FileReader();
reader.onloadend = () => {
setFileData(reader.result);
};
reader.readAsDataURL(e.dataTransfer.files[0]);
setIsOver(false);
}
function onDragOver(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault();
e.stopPropagation();
}
// ...
Tout comme dans les autres méthodes, nous devons écrire preventDefault et stopPropagation. De plus, pour que le glisser-déposer fonctionne, un gestionnaire onDragOver est requis. Nous ne l'utilisons d'aucune façon, mais il doit simplement l'être.
FileReader fait partie de l' API File avec laquelle nous pouvons lire des fichiers. Les gestionnaires de glisser-déposer obtiennent des fichiers glissés et en utilisant reader.readAsDataURL, nous pouvons obtenir un lien, que nous remplacerons dans le src de l'image. Nous utilisons l'état local du composant pour enregistrer le lien.
Cela nous permet de rendre des images comme ceci:
// ...
{fileData ? <img alt="Preview" src={fileData.toString()}></img> : null}
// ...
Pour que tout soit beau, ajoutons du CSS pour l'aperçu:
img {
display: block;
width: 500px;
margin: auto;
margin-top: 10%;
box-shadow: 1px 1px 20px 10px grey;
pointer-events: none;
}
Il n'y a rien de compliqué, il suffit de régler la largeur de l'image pour qu'elle soit de taille standard et puisse être centrée à l'aide de la marge. pointer-events: aucun n'utilise pour le rendre transparent à la souris. Cela nous permettra d'éviter les cas où l'utilisateur souhaite télécharger à nouveau l'image et la lancer sur l'image chargée qui n'est pas une zone de dépôt.
Lire une image
Nous devons maintenant obtenir les pixels de l'image afin de pouvoir mettre en évidence la couleur moyenne de l'image. Pour cela, nous avons besoin de Canvas. Je suis sûr que nous pouvons en quelque sorte essayer d'analyser le Blob, mais Canvas nous facilite la tâche. L'essence principale de l'approche est que nous rendons les images sur Canvas et utilisons getImageData pour obtenir les données de l'image elle-même dans un format pratique. getImageData prend des arguments de coordonnées pour extraire les données d'image. Nous avons besoin de toutes les images, nous spécifions donc la largeur et la hauteur de l'image à partir de 0, 0.
Fonction pour obtenir la taille de l'image:
function getImageSize(image: HTMLImageElement) {
const height = (canvas.height =
image.naturalHeight || image.offsetHeight || image.height);
const width = (canvas.width =
image.naturalWidth || image.offsetWidth || image.width);
return {
height,
width,
};
}
Vous pouvez alimenter l'image Canvas à l'aide de l'élément Image. Heureusement, nous avons un aperçu que nous pouvons utiliser. Pour ce faire, vous devrez faire une référence à l'élément image.
//...
const imageRef = useRef<HTMLImageElement>(null);
const [bgColor, setBgColor] = useState("rgba(255, 255, 255, 255)");
// ...
useEffect(() => {
if (imageRef.current) {
const image = imageRef.current;
const { height, width } = getImageSize(image);
ctx!.drawImage(image, 0, 0);
getAverageColor(ctx!.getImageData(0, 0, width, height).data).then(
(res) => {
setBgColor(res);
setIsLoading(false);
}
);
}
}, [imageRef, fileData]);
// ...
<img ref={imageRef} alt="Preview" src={fileData.toString()}></img>
// ...
Telle une feinte avec nos oreilles, on attend que la ref apparaisse sur l'élément et l'image est chargée à l'aide de fileData.
ctx!.drawImage(image, 0, 0);
Cette ligne est responsable du rendu d'une image dans un Canvas "virtuel", déclaré en dehors du composant:
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
Ensuite, en utilisant getImageData, nous obtenons le tableau de données d'image représentant le Uint8ClampedArray.
ctx!.getImageData(0, 0, width, height).data
Les valeurs dans lesquelles "serré" sont comprises entre 0 et 255. Comme vous le savez probablement, cette plage contient les valeurs de couleur RVB.
rgba(255, 0, 0, 0.3) /* */
Seule la transparence dans ce cas sera exprimée non pas en 0-1, mais en 0-255.
Obtenez la couleur de l'image
La question est restée aux petits, à savoir, obtenir la couleur moyenne de l'image.
Comme il s'agit d'une opération potentiellement coûteuse, nous utiliserons un fil séparé pour calculer la couleur. Bien sûr, c'est une tâche un peu fictive, mais elle fera l'affaire pour un exemple.
La fonction getAverageColor est le "flux séparé" que nous créons avec greenlet:
const getAverageColor = greenlet(async (imageData: Uint8ClampedArray) => {
const len = imageData.length;
const pixelsCount = len / 4;
const arraySum: number[] = [0, 0, 0, 0];
for (let i = 0; i < len; i += 4) {
arraySum[0] += imageData[i];
arraySum[1] += imageData[i + 1];
arraySum[2] += imageData[i + 2];
arraySum[3] += imageData[i + 3];
}
return `rgba(${[
~~(arraySum[0] / pixelsCount),
~~(arraySum[1] / pixelsCount),
~~(arraySum[2] / pixelsCount),
~~(arraySum[3] / pixelsCount),
].join(",")})`;
});
L'utilisation de greenlet est aussi simple que possible. Nous y passons simplement une fonction asynchrone et obtenons le résultat. Il y a une nuance sous le capot qui vous aidera à décider d'utiliser ou non une telle optimisation. Le fait est que greenlet utilise Web Workers et, en fait, un tel transfert de données ( Worker.prototype.postMessage () ), dans ce cas l'image, est assez coûteux et équivaut pratiquement au calcul de la couleur moyenne. Par conséquent, l'utilisation de Web Workers doit être équilibrée par le fait que le poids du temps de calcul est supérieur au transfert de données vers un thread séparé.
Dans ce cas, il vaut peut-être mieux utiliser GPU.JS - exécuter des calculs sur gpu.
La logique de calcul de la couleur moyenne est très simple, on ajoute tous les pixels au format rgba et on divise par le nombre de pixels.
Sources
PS: Laissez des idées, ce qu'il faut essayer, ce que vous aimeriez lire.