[フロントエンドプラクティス#1]ドラッグアンドドロップ、画像プレビュー、中程度の画像カラー、個別のストリーム





みなさん、こんにちは。今日は、別のストリームで画像の平均色を決定し、画像のプレビューを表示するアプリケーションを開発します(画像アップロードフォームを作成するときに便利です)。



これは主に初心者を対象とした新しいシリーズの記事です。そのような資料が面白いかどうかはわかりませんが、試してみることにしました。よろしければ、視覚的に情報を吸収したほうがいい人のために、vidosを撮影します。



何のために?



これは緊急の必要はありませんが、画像の色を定義することは、次の目的でよく使用されます。



  • 色で検索
  • 画像の背景の決定(画面の残りの部分と何らかの形で組み合わせるために、画面全体を占めていない場合)
  • ページの読み込みを最適化するための色付きのサムネイル(圧縮された画像の代わりにカラーパレットを表示)


我々は使用するだろう:





トレーニング



コーディングを開始する前に、依存関係を理解し​​ましょう。Node、js、およびNPM / NPXがあると思われるので、空のReactアプリを作成して依存関係をインストールしてみましょう。



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


次の構造のプロジェクトを取得します。プロジェクト







を開始するには、次を使用できます。



npm start




すべての変更により、ブラウザのページが自動的に更新されます。



次に、Greenletをインストールします。



npm install greenlet


それについては少し後で話します。



ドラッグアンドドロップ



もちろん、ドラッグアンドドロップを操作するための便利なライブラリを見つけることができますが、私たちの場合は不要です。ドラッグアンドドロップAPIは非常に使いやすく、画像を「キャッチ」するタスクには頭に十分です。



まず、不要なものをすべて削除して、「ドロップゾーン」のテンプレートを作成しましょう:



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;


必要に応じて、ドロップゾーンを別のコンポーネントに分割できます。簡単にするために、そのままにしておきます。

興味深いことに、onDrop、onDragEnter、onDragLeaveに注意を払う価値があります。



  • onDrop-dropイベントのリスナー。ユーザーがこの領域にマウスを離すと、ドラッグされたオブジェクトが「ドロップ」されます。
  • onDragEnter-ユーザーがオブジェクトをドラッグアンドドロップ領域にドラッグしたとき
  • onDragLeave-ユーザーがマウスをドラッグして離しました


私たちのワーカーはonDropであり、その助けを借りて、コンピューターから画像を受け取ります。ただし、UXを改善するには、onDragEnterとonDragLeaveが必要です。これにより、ユーザーは何が起こっているのかを理解できます。



ドロップゾーンのCSS:



App.css



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

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


私たちのUI / UXは非常にシンプルです。主なことは、ユーザーがドロップゾーン上に画像をドラッグしたときに境界線を表示することです。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>
  );

/// ...


執筆の過程で、classnamesパッケージの使用法を示すことは不必要ではないことに気づきました。多くの場合、JSXのクラスでの作業が簡単になります。



それをインストールするには:



npm install classnames @types/classnames


上記のコードスニペットでは、ローカル状態変数を作成し、overイベントとleaveイベントの処理を記述しました。残念ながら、e.preventDefault()が原因で少しゴミが表示されますが、これがないと、ブラウザはファイルを開くだけです。また、e.stopPropagation()を使用すると、イベントがドロップゾーンを超えないようにすることができます。



isOverがtrueの場合、境界線を表示するドロップゾーン要素にクラスが追加されます。







画像プレビュー



プレビューを表示するには、画像へのリンク(データURL)を受信してonDropイベントを処理する必要があります



FileReaderはこれを支援します:



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


他のメソッドと同様に、preventDefaultとstopPropagationを記述する必要があります。また、ドラッグアンドドロップを機能させるには、onDragOverハンドラーが必要です。使用することはありませんが、使用する必要があります。



FileReaderは、ファイルを読み取ることができるFileAPIの一部です。ドラッグアンドドロップハンドラーはドラッグされたファイルを取得し、reader.readAsDataURLを使用して、画像のsrcで置換するリンクを取得できます。コンポーネントのローカル状態を使用してリンクを保存します。



これにより、次のような画像をレンダリングできます。



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




すべてを美しく見せるために、プレビュー用にCSSを追加しましょう。

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

  pointer-events: none;
}


複雑なことは何もありません。画像の幅を標準サイズに設定し、マージンを使用して中央に配置できるようにします。ポインタイベント:マウスに対して透過的にするために使用するものはありません。これにより、ユーザーがイメージを再アップロードして、ドロップゾーンではないロードされたイメージにドロップしたい場合を回避できます。







画像を読む



次に、画像の平均色を強調表示できるように、画像のピクセルを取得する必要があります。これにはCanvasが必要です。どういうわけかBlobの解析を試みることができると確信していますが、Canvasを使用すると簡単に解析できます。このアプローチの主な本質は、Canvasで画像をレンダリングし、getImageDataを使用して画像自体のデータを便利な形式で取得することです。getImageDataは、画像データを取得するための座標引数を取ります。すべての画像が必要なので、0、0から始まる画像の幅と高さを指定します。



画像サイズを取得するための関数:



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


Image要素を使用してCanvasイメージをフィードできます。幸い、使用できるプレビューがあります。これを行うには、画像要素への参照を作成する必要があります。



//...  

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>

// ...


私たちの耳のような気まぐれで、refが要素に表示されるのを待っており、fileDataのために画像がロードされます。



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


この行は、コンポーネントの外部で宣言された「仮想」キャンバスで画像をレンダリングする役割を果たします。



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


次に、getImageDataを使用して、Uint8ClampedArrayを表す画像データ配列を取得します。



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


「クランプされた」値は0〜255の範囲です。ご存知かもしれませんが、この範囲には色のrgb値が含まれています。



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


この場合の透明度のみが0-1ではなく0-255で表されます。



画像の色を取得します



あとは、画像の平均色を取得するだけです。



これは潜在的にコストのかかる操作であるため、別のスレッドを使用して色を計算します。もちろん、これは少し架空のタスクですが、例としては実行できます。getAverageColor



関数は、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(",")})`;
});


グリーンレットの 使用可能な限り簡単です。そこで非同期関数を渡して結果を取得します。このような最適化を使用するかどうかを決定するのに役立つ、内部的なニュアンスが1つあります。事実、グリーンレットはWebワーカーを使用しており、実際、このようなデータ転送(Worker.prototype.postMessage())、この場合は画像は非常に高価であり、平均色の計算と実質的に同じです。したがって、Webワーカーの使用は、計算時間の重みが別のスレッドへのデータの転送よりも大きいという事実によってバランスを取る必要があります。



おそらくこの場合、GPU.JSを使用する方が良いでしょう-gpuで計算を実行します。



平均色を計算するロジックは非常に単純です。すべてのピクセルをrgba形式で追加し、ピクセル数で除算します。







出典



PS:アイデア、何を試すか、何について読みたいかを残してください。



All Articles