[Frontend Practice # 1] السحب والإفلات ومعاينة الصورة ولون الصورة المتوسط ​​والدفق المنفصل





مرحبًا بالجميع ، سنطور اليوم تطبيقًا يحدد متوسط ​​لون الصورة في دفق منفصل ويعرض معاينة للصورة (مفيد عند إنشاء نماذج تحميل الصور).



هذه سلسلة جديدة من المقالات تستهدف في الغالب المبتدئين. لست متأكدًا مما إذا كانت هذه المواد ستكون ممتعة ، لكنني قررت أن أجربها. إذا كان موافقًا ، فسأطلق النار على vidos ، لمن هم أفضل حالًا في استيعاب المعلومات بصريًا.



لاجل ماذا؟



ليست هناك حاجة ماسة لذلك ، ولكن غالبًا ما يتم استخدام تحديد ألوان الصورة من أجل:



  • البحث عن طريق اللون
  • تحديد خلفية الصورة (إذا لم تشغل الشاشة بأكملها ، من أجل دمجها بطريقة ما مع باقي الشاشة)
  • الصور المصغرة الملونة لتحسين تحميل الصفحة (إظهار لوحة الألوان بدلاً من الصورة المضغوطة)


سوف نستخدم:





تدريب



قبل أن نبدأ في البرمجة ، دعنا نفهم التبعيات. أظن أن لديك Node و js و NPM / NPX ، لذلك دعونا نبدأ مباشرة في إنشاء تطبيق React فارغ وتثبيت التبعيات:



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


سنحصل على مشروع بالهيكل التالي:







لبدء المشروع ، يمكنك استخدام:



npm start




ستعمل جميع التغييرات على تحديث الصفحة تلقائيًا في المتصفح.



بعد ذلك ، قم بتثبيت Greenlet:



npm install greenlet


سنتحدث عنها بعد قليل.



السحب والإفلات



بالطبع ، يمكنك العثور على مكتبة ملائمة للعمل مع Drag and Drop ، ولكن في حالتنا ستكون غير ضرورية. واجهة برمجة تطبيقات السحب والإفلات سهلة الاستخدام للغاية ومهمة "التقاط" الصورة كافية لرؤوسنا.



أولاً ، دعنا نزيل كل ما هو غير ضروري ونصنع نموذجًا لـ "منطقة الإسقاط":



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 - مستمع لحدث الإفلات ، عندما يقوم المستخدم بتحرير الماوس فوق هذه المنطقة ، "يسقط" الكائن المسحوب.
  • onDragEnter - عندما يقوم المستخدم بسحب كائن إلى منطقة السحب والإفلات
  • onDragLeave - قام المستخدم بسحب الماوس بعيدًا


العامل بالنسبة لنا هو onDrop ، وبمساعدته سوف نتلقى صورة من الكمبيوتر. لكننا نحتاج إلى 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


في مقتطف الشفرة أعلاه ، أنشأنا متغيرًا محليًا للولاية وكتبنا أكثر من معالجة الحدث. لسوء الحظ ، اتضح أن هناك القليل من القمامة بسبب e.preventDefault () ، ولكن بدونها سيفتح المتصفح الملف ببساطة. ويتيح لنا e.stopPropagation () التأكد من أن الحدث لا يتجاوز منطقة الإسقاط.



إذا كانت قيمة isOver هي true ، فسيتم إضافة فئة إلى عنصر منطقة الإسقاط الذي يعرض الحد:







معاينة الصورة



لعرض المعاينة ، نحتاج إلى معالجة حدث onDrop من خلال تلقي رابط ( عنوان URL للبيانات ) للصورة.



سوف يساعدنا 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();
  }
// ...


تمامًا كما هو الحال في الطرق الأخرى ، نحتاج إلى كتابة PreventionDefault و stopPropagation. أيضًا ، لكي يعمل السحب والإفلات ، يلزم وجود معالج onDragOver. لن نستخدمه بأي شكل من الأشكال ، ولكن يجب أن يكون كذلك.



FileReader هو جزء من File API يمكننا من خلاله قراءة الملفات. معالجات السحب والإفلات تحصل على الملفات المسحوبة واستخدام القارئ .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;
}


لا يوجد شيء معقد ، ما عليك سوى تعيين عرض الصورة بحيث تكون ذات أحجام قياسية ويمكن توسيطها باستخدام الهامش. أحداث المؤشر: لا شيء يستخدم لجعلها شفافة للماوس. سيسمح لنا ذلك بتجنب الحالات التي يريد فيها المستخدم إعادة تحميل الصورة وإفلاتها على الصورة المحملة التي ليست منطقة إفلات.







قراءة صورة



نحتاج الآن إلى الحصول على وحدات البكسل في الصورة حتى نتمكن من إبراز متوسط ​​لون الصورة. لهذا نحتاج قماش. أنا متأكد من أنه يمكننا بطريقة ما محاولة تحليل 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,
  };
}


يمكنك تغذية صورة 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>

// ...


مثل هذه الخدعة بآذاننا ، نحن ننتظر ظهور المرجع على العنصر ويتم تحميل الصورة باستخدام 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(",")})`;
});


استخدام Greenlet بسيط قدر الإمكان. نحن فقط نمرر وظيفة غير متزامنة هناك ونحصل على النتيجة. هناك فارق بسيط واحد تحت الغطاء سيساعدك على تحديد ما إذا كنت ستستخدم مثل هذا التحسين. الحقيقة هي أن Greenlet يستخدم Web Workers ، وفي الواقع ، فإن نقل البيانات هذا ( Worker.prototype.postMessage () ) ، في هذه الحالة الصورة ، مكلف للغاية ويساوي عمليًا حساب متوسط ​​اللون. لذلك ، يجب موازنة استخدام Web Workers بحقيقة أن وزن وقت الحساب أكبر من نقل البيانات إلى مؤشر ترابط منفصل.



ربما في هذه الحالة من الأفضل استخدام GPU.JS - تشغيل العمليات الحسابية على gpu.



منطق حساب متوسط ​​اللون بسيط للغاية ، فنحن نضيف جميع وحدات البكسل بتنسيق rgba ونقسمها على عدد وحدات البكسل.







المصادر



ملاحظة: اترك الأفكار ، وما الذي تجرِّبه ، وما الذي تود أن تقرأ عنه.



All Articles