大家好,今天我们将开发一个应用程序,该应用程序确定单独流中图像的平均颜色并显示图像预览(在创建图像上传表单时很有用)。
这是一系列新文章,主要针对初学者。我不确定这种材料是否有趣,但是我决定尝试一下。如果可以的话,我将为那些希望通过视觉吸收信息的人拍摄视频。
做什么的?
对此没有迫切需求,但是定义图像的颜色通常用于:
- 按颜色搜索
- 确定图像的背景(如果它不占据整个屏幕,以便以某种方式与屏幕的其余部分组合)
- 彩色缩略图可优化页面加载(显示调色板而不是压缩图像)
我们将使用:
- 打字稿
- React与Create React App一起-为什么不呢?我们将快速创建一个工作环境并能够构建我们的项目
- HTML拖放API-将图像从桌面拖动到浏览器
- Web worker和Greenlet-用于将复杂的计算纳入单独的线程
- 类名
- 文件API
- 资料网址
训练
在开始编码之前,让我们弄清楚相关性。我怀疑您有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-放置事件的侦听器,当用户在该区域上释放鼠标时,“放置”被拖动的对象。
- onDragEnter-用户将对象拖动到拖放区域中时
- onDragLeave-用户将鼠标拖走了
对我们来说,工作人员是onDrop,在它的帮助下,我们将从计算机接收图像。但是我们需要onDragEnter和onDragLeave来改善UX,以便用户了解正在发生的事情。
放置区的一些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和离开事件处理程序。不幸的是,由于e.preventDefault()导致了一些垃圾,但是如果没有这个,浏览器将只打开文件。使用e.stopPropagation(),我们可以确保事件不会超出放置区。
如果isOver为true,则将一个类添加到显示边框的放置区域元素中:
图片预览
为了显示预览,我们需要通过接收图像的链接(Data 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是File API的一部分,通过它我们可以读取文件。拖放处理程序获取被拖动的文件,并使用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;
}
没什么复杂的,只需设置图像的宽度,使其具有标准尺寸并可以使用边距居中。指针事件:没有用于使鼠标透明的指针事件。这将使我们避免用户想要重新上传图像并将其拖放到不是放置区的已加载图像上的情况。
读取图像
现在我们需要获取图像的像素,以便可以突出显示图像的平均颜色。为此,我们需要画布。我确信我们可以以某种方式尝试解析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图像。幸运的是,我们有一个可以使用的预览。为此,您将需要对image元素进行引用。
//...
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(",")})`;
});
使用greenlet尽可能简单。我们只是在那里传递一个异步函数并获得结果。引擎盖下的细微差别将帮助您决定是否使用这种优化。事实是,greenlet使用Web Workers,实际上,这种数据传输(Worker.prototype.postMessage())(在这种情况下为图像)非常昂贵,实际上等于平均颜色的计算。因此,应该通过以下事实来平衡Web Worker的使用:计算时间的权重大于将数据传输到单独的线程中。
也许在这种情况下,最好使用GPU.JS-在gpu上运行计算。
计算平均颜色的逻辑非常简单,我们将所有像素以rgba格式相加并除以像素数。
消息来源
:留下您的想法,尝试的方法,您想了解的内容。