[Latihan Frontend # 1] Seret dan Lepas, Pratinjau Gambar, Warna Gambar Sedang, dan Aliran Terpisah





Halo semuanya, hari ini kami akan mengembangkan aplikasi yang menentukan warna rata-rata suatu gambar dalam aliran terpisah dan menampilkan pratinjau gambar (berguna saat membuat formulir unggahan gambar).



Ini adalah seri artikel baru yang sebagian besar ditujukan untuk pemula. Saya tidak yakin apakah bahan seperti itu mungkin menarik, tetapi saya memutuskan untuk mencobanya. Jika oke, saya akan merekam video, bagi mereka yang lebih baik menyerap informasi secara visual.



Untuk apa?



Tidak ada kebutuhan mendesak untuk ini, tetapi menentukan warna suatu gambar sering kali digunakan untuk:



  • Cari berdasarkan warna
  • Penentuan latar belakang gambar (jika tidak menempati seluruh layar, entah bagaimana untuk digabungkan dengan bagian layar lainnya)
  • Thumbnail berwarna untuk mengoptimalkan pemuatan halaman (tampilkan palet warna alih-alih gambar terkompresi)


Kami akan menggunakan:





Latihan



Sebelum kita mulai membuat kode, mari kita cari tahu dependensinya. Saya menduga Anda memiliki Node, js, dan NPM / NPX, jadi mari langsung membuat aplikasi React kosong dan menginstal dependensi:



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


Kami akan mendapatkan proyek dengan struktur sebagai berikut:







Untuk memulai proyek, Anda dapat menggunakan:



npm start




Semua perubahan akan secara otomatis menyegarkan halaman di browser.



Selanjutnya, instal Greenlet:



npm install greenlet


Kami akan membicarakannya nanti.



Seret dan lepas



Tentu saja, Anda dapat menemukan perpustakaan yang nyaman untuk bekerja dengan Seret dan Jatuhkan, tetapi dalam kasus kami ini akan berlebihan. Drag and Drop API sangat mudah digunakan dan untuk tugas kita "menangkap" gambar sudah cukup untuk kepala kita.



Pertama, mari hapus semua yang tidak perlu dan buat template untuk "zona drop" kami:



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;


Jika mau, Anda dapat memisahkan drop zone menjadi komponen terpisah, agar lebih mudah kita biarkan seperti itu.

Dari hal-hal yang menarik, perlu diperhatikan onDrop, onDragEnter, onDragLeave.



  • onDrop - listener untuk acara drop, ketika pengguna melepaskan mouse ke area ini, objek yang diseret akan "jatuh".
  • onDragEnter - saat pengguna menyeret objek ke area seret dan lepas
  • onDragLeave - pengguna menyeret mouse menjauh


Pekerja untuk kami adalah onDrop, dengan bantuannya kami akan menerima gambar dari komputer. Namun kami membutuhkan onDragEnter dan onDragLeave untuk meningkatkan UX, sehingga pengguna memahami apa yang terjadi.



Beberapa CSS untuk zona drop:



App.css



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

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


UI / UX kami sangat sederhana, yang utama adalah menampilkan batas saat pengguna menyeret gambar ke zona lepas. Mari kita sedikit memodifikasi JS kita:

/// ...

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

/// ...


Dalam proses penulisan, saya menyadari bahwa tidak akan berlebihan untuk menunjukkan penggunaan paket nama kelas. Seringkali membuatnya lebih mudah untuk bekerja dengan kelas di JSX.



Untuk menginstalnya:



npm install classnames @types/classnames


Dalam potongan kode di atas, kami membuat variabel status lokal dan menulis penanganan peristiwa over dan leave. Sayangnya, ternyata ada sedikit sampah karena e.preventDefault (), tapi tanpa ini, browser akan langsung membuka file tersebut. Dan e.stopPropagation () memungkinkan kita untuk memastikan bahwa acara tidak melampaui zona penurunan.



Jika isOver benar, maka kelas ditambahkan ke elemen zona lepas yang menampilkan batas:







Pratinjau gambar



Untuk menampilkan pratinjau, kita perlu menangani acara onDrop dengan menerima tautan ( Data URL ) ke gambar.



FileReader akan membantu kami dalam hal ini:



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


Sama seperti di metode lain, kita perlu menulis preventDefault dan stopPropagation. Selain itu, agar Seret dan Jatuhkan berfungsi, penangan onDragOver diperlukan. Kami tidak akan menggunakannya dengan cara apa pun, tetapi harus digunakan begitu saja.



FileReader adalah bagian dari File API yang dengannya kita dapat membaca file. Drag and Drop handler mendapatkan file yang diseret dan menggunakan reader.readAsDataURL kita bisa mendapatkan link yang akan kita gantikan di src gambar. Kami menggunakan status lokal komponen untuk menyimpan tautan.



Ini memungkinkan kami untuk membuat gambar seperti ini:



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




Agar semuanya terlihat bagus, mari tambahkan beberapa CSS untuk pratinjau:

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

  pointer-events: none;
}


Gak ribet kok, tinggal atur saja lebar gambar jadi ukuran standar dan bisa dipusatkan menggunakan margin. pointer-events: none menggunakan untuk membuatnya transparan ke mouse. Ini akan memungkinkan kami untuk menghindari kasus ketika pengguna ingin mengunggah ulang gambar dan menjatuhkannya ke gambar yang dimuat yang bukan zona lepas.







Membaca gambar



Sekarang kita perlu mendapatkan piksel gambar sehingga kita bisa menyorot warna rata-rata gambar. Untuk ini kita membutuhkan Canvas. Saya yakin bahwa kami dapat mencoba dan mengurai Blob, tetapi Canvas membuatnya lebih mudah bagi kami. Esensi utama dari pendekatan ini adalah kami merender gambar di Canvas dan menggunakan getImageData untuk mendapatkan data gambar itu sendiri dalam format yang nyaman. getImageData menggunakan argumen koordinat untuk mengambil data gambar. Kita membutuhkan semua gambar, jadi kita tentukan lebar dan tinggi gambar mulai dari 0, 0.



Fungsi untuk mendapatkan ukuran gambar:



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


Anda dapat memberi makan gambar Canvas menggunakan elemen Image. Untungnya, kami memiliki pratinjau yang dapat kami gunakan. Untuk melakukan ini, Anda perlu membuat referensi ke elemen gambar.



//...  

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>

// ...


Tipuan seperti itu dengan telinga kita, kita menunggu ref muncul di elemen dan gambar dimuat menggunakan fileData.



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


Baris ini bertanggung jawab untuk merender gambar di Canvas "virtual", yang dideklarasikan di luar komponen:



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


Kemudian, menggunakan getImageData, kita mendapatkan larik data gambar yang mewakili Uint8ClampedArray.



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


Nilai di mana "dijepit" berada pada kisaran 0-255. Seperti yang mungkin Anda ketahui, rentang ini berisi nilai warna rgb.



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


Hanya transparansi dalam kasus ini yang akan diekspresikan tidak dalam 0-1, tetapi 0-255.



Dapatkan warna gambarnya



Satu-satunya hal yang harus dilakukan adalah mendapatkan warna gambar rata-rata.



Karena ini berpotensi menjadi operasi yang mahal, kami akan menggunakan utas terpisah untuk menghitung warna. Tentu saja, ini adalah tugas yang sedikit fiktif, tetapi akan berhasil sebagai contoh.



Fungsi getAverageColor adalah "aliran terpisah" yang kita buat dengan 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(",")})`;
});


Menggunakan greenlet sesederhana mungkin. Kami baru saja melewati fungsi asinkron di sana dan mendapatkan hasilnya. Ada satu nuansa di balik terpal yang akan membantu Anda memutuskan apakah akan menggunakan pengoptimalan seperti itu. Faktanya adalah bahwa greenlet menggunakan Web Workers dan, pada kenyataannya, transfer data semacam itu ( Worker.prototype.postMessage () ), dalam hal ini gambar, cukup mahal dan secara praktis sama dengan penghitungan warna rata-rata. Oleh karena itu, penggunaan Web Workers harus diimbangi dengan fakta bahwa bobot waktu komputasi lebih besar daripada transfer data ke utas terpisah.



Mungkin dalam hal ini lebih baik menggunakan GPU.JS - jalankan kalkulasi pada gpu.



Logika untuk menghitung warna rata-rata sangat sederhana, kami menambahkan semua piksel dalam format rgba dan membaginya dengan jumlah piksel.







Sumber



PS: Tinggalkan ide, apa yang harus dicoba, apa yang ingin Anda baca.



All Articles