[Frontend-Übung Nr. 1] Drag & Drop, Bildvorschau, mittlere Bildfarbe und separater Stream





Hallo allerseits, heute werden wir eine Anwendung entwickeln, die die durchschnittliche Farbe eines Bildes in einem separaten Stream ermittelt und eine Vorschau des Bildes anzeigt (nützlich beim Erstellen von Formularen zum Hochladen von Bildern).



Dies ist eine neue Artikelserie, die sich hauptsächlich an Anfänger richtet. Ich bin mir nicht sicher, ob solches Material interessant sein könnte, aber ich habe beschlossen, es auszuprobieren. Wenn es in Ordnung ist, mache ich Videos für diejenigen, die Informationen besser visuell aufnehmen können.



Wozu?



Dies ist nicht dringend erforderlich, aber die Definition der Farben eines Bildes wird häufig verwendet, um:



  • Suche nach Farbe
  • Bestimmung des Hintergrunds des Bildes (wenn es nicht den gesamten Bildschirm einnimmt, um irgendwie mit dem Rest des Bildschirms kombiniert zu werden)
  • Farbige Miniaturansichten zur Optimierung des Seitenladens (Farbpalette anstelle von komprimiertem Bild anzeigen)


Wir werden verwenden:





Ausbildung



Bevor wir mit dem Codieren beginnen, wollen wir die Abhängigkeiten herausfinden. Ich vermute, Sie haben Node, js und NPM / NPX. Lassen Sie uns also gleich eine leere React-App erstellen und die Abhängigkeiten installieren:



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


Wir erhalten ein Projekt mit der folgenden Struktur:







Um das Projekt zu starten, können Sie verwenden:



npm start




Alle Änderungen aktualisieren automatisch die Seite im Browser.



Installieren Sie als Nächstes Greenlet:



npm install greenlet


Wir werden etwas später darüber sprechen.



Drag & Drop



Natürlich finden Sie eine praktische Bibliothek für die Arbeit mit Drag & Drop, aber in unserem Fall ist sie überflüssig. Die Drag & Drop-API ist sehr einfach zu verwenden und für unsere Aufgabe, das Bild zu "fangen", reicht es für unsere Köpfe.



Entfernen wir zunächst alle unnötigen Elemente und erstellen eine Vorlage für unsere "Drop-Zone":



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;


Wenn Sie möchten, können Sie die Drop-Zone in eine separate Komponente aufteilen. Der Einfachheit halber lassen wir dies so.

Von den interessanten Dingen lohnt es sich, auf onDrop, onDragEnter, onDragLeave zu achten.



  • onDrop - Listener für das Drop-Ereignis: Wenn der Benutzer die Maus über diesen Bereich lässt, wird das gezogene Objekt "fallen gelassen".
  • onDragEnter - Wenn der Benutzer ein Objekt in den Drag & Drop-Bereich zieht
  • onDragLeave - Der Benutzer hat die Maus weggezogen


Der Mitarbeiter für uns ist onDrop, mit dessen Hilfe wir ein Bild vom Computer erhalten. Wir benötigen jedoch onDragEnter und onDragLeave, um die Benutzeroberfläche zu verbessern, damit der Benutzer versteht, was passiert.



Einige CSS für die Drop-Zone:



App.css



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

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


Unsere UI / UX ist sehr einfach. Die Hauptsache ist, den Rand anzuzeigen, wenn der Benutzer das Bild über die Ablagezone zieht. Lassen Sie uns unser JS ein wenig modifizieren:

/// ...

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>
  );

/// ...


Während des Schreibens wurde mir klar, dass es nicht überflüssig wäre, die Verwendung des Klassennamen-Pakets zu zeigen. Erleichtert häufig die Arbeit mit Klassen in JSX.



So installieren Sie es:



npm install classnames @types/classnames


Im obigen Code-Snippet haben wir eine lokale Statusvariable erstellt und die Ereignisbehandlung over und Leave geschrieben. Leider stellt sich aufgrund von e.preventDefault () ein wenig Müll heraus, aber ohne dies öffnet der Browser einfach die Datei. Mit e.stopPropagation () können wir sicherstellen, dass das Ereignis nicht über die Drop-Zone hinausgeht.



Wenn isOver true ist, wird dem Drop-Zone-Element, das den Rahmen anzeigt, eine Klasse hinzugefügt:







Bildvorschau



Um die Vorschau anzuzeigen, müssen wir das onDrop-Ereignis behandeln, indem wir einen Link ( Daten-URL ) zum Bild erhalten.



FileReader hilft uns dabei:



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


Genau wie bei anderen Methoden müssen wir PreventDefault und StopPropagation schreiben. Damit Drag & Drop funktioniert, ist ein onDragOver-Handler erforderlich. Wir werden es in keiner Weise benutzen, aber es muss einfach sein.



FileReader sind Teil der Datei-API, mit der wir Dateien lesen können. Drag & Drop-Handler erhalten gezogene Dateien und mit reader.readAsDataURL können wir einen Link erhalten, den wir im src des Bildes ersetzen. Wir verwenden den lokalen Status der Komponente, um den Link zu speichern.



Dies ermöglicht es uns, Bilder wie folgt zu rendern:



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




Damit alles gut aussieht, fügen wir CSS für die Vorschau hinzu:

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

  pointer-events: none;
}


Es ist nichts kompliziertes, stellen Sie einfach die Breite des Bildes so ein, dass es Standardgrößen hat und mithilfe des Randes zentriert werden kann. Zeigerereignisse: Keine, um sie für die Maus transparent zu machen. Auf diese Weise können wir Fälle vermeiden, in denen der Benutzer das Bild erneut hochladen und auf das geladene Bild werfen möchte, das keine Drop-Zone ist.







Ein Bild lesen



Jetzt müssen wir die Pixel des Bildes erhalten, damit wir die durchschnittliche Farbe des Bildes hervorheben können. Dafür brauchen wir Canvas. Ich bin sicher, dass wir irgendwie versuchen können, den Blob zu analysieren, aber Canvas macht es uns einfacher. Das Wesentliche des Ansatzes ist, dass wir Bilder auf Canvas rendern und getImageData verwenden, um die Daten des Bildes selbst in einem praktischen Format abzurufen. getImageData verwendet Koordinatenargumente, um die Bilddaten zu übernehmen. Wir benötigen alle Bilder, daher geben wir die Breite und Höhe des Bildes ab 0, 0 an.



Funktion zum Abrufen der Bildgröße:



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


Sie können das Leinwandbild mit dem Bildelement füttern. Zum Glück haben wir eine Vorschau, die wir verwenden können. Dazu müssen Sie einen Verweis auf das Bildelement erstellen.



//...  

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>

// ...


Solch eine Finte mit unseren Ohren, wir warten darauf, dass der Ref auf dem Element erscheint und das Bild mit fileData geladen wird.



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


Diese Zeile ist für das Rendern eines Bildes in einem "virtuellen" Canvas verantwortlich, der außerhalb der Komponente deklariert ist:



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


Mit getImageData erhalten wir dann das Bilddatenarray, das das Uint8ClampedArray darstellt.



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


Die Werte, in denen "geklemmt" ist, liegen im Bereich von 0 bis 255. Wie Sie wahrscheinlich wissen, enthält dieser Bereich die RGB-Werte der Farbe.



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


Nur Transparenz wird in diesem Fall nicht in 0-1, sondern in 0-255 ausgedrückt.



Holen Sie sich die Farbe des Bildes



Die Sache blieb bei den Kleinen, nämlich die durchschnittliche Farbe des Bildes zu erhalten.



Da dies möglicherweise eine teure Operation ist, verwenden wir einen separaten Thread, um die Farbe zu berechnen. Natürlich ist dies eine leicht fiktive Aufgabe, aber sie reicht als Beispiel.



Die Funktion getAverageColor ist der "separate Stream", den wir mit greenlet erstellen:



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(",")})`;
});


Die Verwendung von Greenlet ist so einfach wie möglich. Wir übergeben dort einfach eine asynchrone Funktion und erhalten das Ergebnis. Unter der Haube befindet sich eine Nuance, die Ihnen bei der Entscheidung hilft, ob Sie eine solche Optimierung verwenden möchten. Tatsache ist, dass Greenlet Web Worker verwendet und tatsächlich eine solche Datenübertragung ( Worker.prototype.postMessage () ), in diesem Fall das Bild, ziemlich teuer ist und praktisch der Berechnung der durchschnittlichen Farbe entspricht. Daher sollte die Verwendung von Web Workern durch die Tatsache ausgeglichen werden, dass das Gewicht der Rechenzeit größer ist als die Übertragung von Daten an einen separaten Thread.



Vielleicht ist es in diesem Fall besser, GPU.JS zu verwenden - Berechnungen auf GPU ausführen.



Die Logik zur Berechnung der durchschnittlichen Farbe ist sehr einfach. Wir addieren alle Pixel im RGBA-Format und dividieren durch die Anzahl der Pixel.







Quellen



PS: Hinterlassen Sie Ideen, was Sie versuchen sollten, worüber Sie lesen möchten.



All Articles