[Práctica de frontend n. ° 1] Arrastrar y soltar, vista previa de imagen, color de imagen medio y flujo independiente





Hola a todos, hoy desarrollaremos una aplicación que determina el color promedio de una imagen en una secuencia separada y muestra una vista previa de la imagen (útil al crear formularios de carga de imágenes).



Esta es una nueva serie de artículos que está dirigida principalmente a principiantes. No estoy seguro de si ese material podría ser interesante, pero decidí intentarlo. Si está bien, filmaré videos, para aquellos que están mejor absorbiendo información visualmente.



¿Para qué?



No hay una necesidad urgente para esto, pero definir los colores de una imagen se usa a menudo para:



  • Buscar por color
  • Determinación del fondo de la imagen (si no ocupa toda la pantalla, para de alguna manera combinarse con el resto de la pantalla)
  • Miniaturas de colores para optimizar la carga de la página (mostrar paleta de colores en lugar de imagen comprimida)


Usaremos:





Formación



Antes de comenzar a codificar, descubramos las dependencias. Sospecho que tiene Node, js y NPM / NPX, así que vayamos directamente a crear una aplicación React en blanco e instalar las dependencias:



npx create-react-app average-color-app --template typescript


Obtendremos un proyecto con la siguiente estructura:







Para iniciar el proyecto, puede utilizar:



npm start




Todos los cambios actualizarán automáticamente la página en el navegador.



A continuación, instale Greenlet:



npm install greenlet


Hablaremos de ello un poco más tarde.



Arrastrar y soltar



Por supuesto, puede encontrar una biblioteca conveniente para trabajar con arrastrar y soltar, pero en nuestro caso será superflua. La API de arrastrar y soltar es muy fácil de usar y para nuestra tarea de "captar" la imagen es suficiente para nuestras cabezas.



Primero, eliminemos todo lo innecesario y creemos una plantilla para nuestra "zona de colocación ":



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 lo desea, puede separar la zona de caída en un componente separado, por simplicidad lo dejaremos así.

De las cosas interesantes, vale la pena prestar atención a onDrop, onDragEnter, onDragLeave.



  • onDrop - escucha del evento de caída, cuando el usuario suelta el mouse sobre esta área, el objeto que se arrastra será "soltado".
  • onDragEnter: cuando el usuario arrastra un objeto al área de arrastrar y soltar
  • onDragLeave: el usuario arrastró el mouse


El trabajador para nosotros es onDrop, con la ayuda de él recibiremos una imagen de la computadora. Pero necesitamos onDragEnter y onDragLeave para mejorar la UX, para que el usuario entienda lo que está sucediendo.



Algunos CSS para la zona de colocación :



App.css



.drop-zone {
  height: 100vh;
  box-sizing: border-box; //  ,          .
}

.drop-zone-over {
  border: black 10px dashed;
}


Nuestra UI / UX es muy simple, lo principal es mostrar el borde cuando el usuario arrastra la imagen sobre la zona de caída. Modifiquemos un poco nuestro 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>
  );

/// ...


Mientras escribía, me di cuenta de que no sería superfluo mostrar el uso del paquete classnames. A menudo facilita el trabajo con clases en JSX.



Para instalarlo:



npm install classnames @types/classnames


En el fragmento de código anterior, creamos una variable de estado local y escribimos el manejo de eventos over y leave. Desafortunadamente, resulta un poco basura debido a e.preventDefault (), pero sin él, el navegador simplemente abrirá el archivo. Y e.stopPropagation () nos permite asegurarnos de que el evento no vaya más allá de la zona de caída.



Si isOver es verdadero, entonces se agrega una clase al elemento de la zona de colocación que muestra el borde:







Vista previa de la imagen



Para mostrar la vista previa, debemos manejar el evento onDrop recibiendo un enlace ( URL de datos ) a la imagen.



FileReader nos ayudará con esto:



// ...
  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();
  }
// ...


Al igual que en otros métodos, necesitamos escribir preventDefault y stopPropagation. Además, para que funcione Arrastrar y soltar, se requiere un controlador onDragOver. No lo usaremos de ninguna manera, pero tiene que ser así.



FileReader son parte de la API de archivos con la que podemos leer archivos. Los manejadores de arrastrar y soltar obtienen archivos arrastrados y usando reader.readAsDataURL podemos obtener un enlace que sustituiremos en el src de la imagen. Usamos el estado local del componente para guardar el enlace.



Esto nos permite renderizar imágenes como esta:



// ...
{fileData ? <img alt="Preview" src={fileData.toString()}></img> : null}
// ...




Para que todo se vea bien, agreguemos algo de CSS para la vista previa:

img {
  display: block;
  width: 500px;
  margin: auto;
  margin-top: 10%;
  box-shadow: 1px 1px 20px 10px grey;

  pointer-events: none;
}


No hay nada complicado, simplemente configure el ancho de la imagen para que sea de tamaños estándar y pueda centrarse usando un margen. pointer-events: ninguno se usa para hacerlo transparente para el mouse. Esto nos permitirá evitar casos en los que el usuario quiera volver a cargar la imagen y soltarla en la imagen cargada que no es una zona de colocación.







Leer una imagen



Ahora necesitamos obtener los píxeles de la imagen para que podamos resaltar el color promedio de la imagen. Para ello necesitamos Canvas. Estoy seguro de que de alguna manera podemos intentar analizar el Blob, pero Canvas nos lo facilita. La esencia principal del enfoque es que renderizamos imágenes en Canvas y usamos getImageData para obtener los datos de la imagen en un formato conveniente. getImageData toma argumentos de coordenadas para tomar los datos de la imagen. Necesitamos todas las imágenes, por lo que especificamos el ancho y el alto de la imagen empezando por 0, 0.



Función para obtener el tamaño de la imagen:



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,
  };
}


Puede alimentar la imagen de Canvas utilizando el elemento Image. Afortunadamente, tenemos una vista previa que podemos usar. Para hacer esto, necesitará hacer una Ref al elemento de imagen.



//...  

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>

// ...


Tal finta con nuestros oídos, estamos esperando que aparezca la referencia en el elemento y la imagen se cargue debido a fileData.



 ctx!.drawImage(image, 0, 0);


Esta línea es responsable de renderizar una imagen en un Canvas "virtual", declarado fuera del componente:



const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");


Luego, usando getImageData, obtenemos la matriz de datos de imagen que representa Uint8ClampedArray.



ctx!.getImageData(0, 0, width, height).data


Los valores en los que "sujetado" están en el rango de 0-255. Como probablemente sepa, este rango contiene los valores de color rgb.



rgba(255, 0, 0, 0.3) /*    */


Solo la transparencia en este caso se expresará no en 0-1, sino en 0-255.



Obtén el color de la imagen



El asunto quedó en los pequeños, es decir, obtener el color medio de la imagen.



Dado que esta es una operación potencialmente costosa, usaremos un hilo separado para calcular el color. Por supuesto, esta es una tarea un poco ficticia, pero servirá como ejemplo.



La función getAverageColor es la "secuencia separada" que creamos con 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(",")})`;
});


Usar greenlet es lo más simple posible. Simplemente pasamos una función asincrónica allí y obtenemos el resultado. Hay un matiz bajo el capó que le ayudará a decidir si utilizar dicha optimización. El caso es que greenlet utiliza Web Workers y, de hecho, dicha transferencia de datos ( Worker.prototype.postMessage () ), en este caso la imagen, es bastante cara y es prácticamente igual al cálculo del color medio. Por lo tanto, el uso de Web Workers debe equilibrarse con el hecho de que el peso del tiempo de cálculo es mayor que la transferencia de datos a un hilo independiente.



Quizás en este caso sea mejor usar GPU.JS - ejecutar cálculos en gpu.



La lógica para calcular el color promedio es muy simple, sumamos todos los píxeles en formato rgba y dividimos por la cantidad de píxeles.







Fuentes



PD: Deje ideas, qué probar, qué le gustaría leer.



All Articles