写点什么

可视化学习:实现 Canvas 图片局部放大镜

  • 2024-03-29
    福建
  • 本文字数:3432 字

    阅读完需:约 11 分钟

前言


最近我在可视化课程中学习了如何在 Canvas 中利用像素处理来实现滤镜效果,在这节课程的结尾留了一道局部放大镜的题目,提示我们用像素处理的方式去实现这个效果,最终实现随着鼠标移动将图片局部放大,本着把学到的内容落地实践的想法,我就去思考了一番,但很不幸,我思考了好几天也没思考出结果,因为刚开始我想的一直是在一个 Canvas 上来操作,但是一来我对 Canvas API 还并不是很熟悉,二来我对像素处理还不够熟练,然后第三是如果原图的部分像素被处理了,那下一次放大就会有问题,因此我最终放弃了这个思路,选择了再增加一个 Canvas 来完成最终的效果,以下就是利用这种方式实现图片局部放大的效果。


像素处理


在实现这个效果之前,我们先来了解一下如何处理像素,有些小伙伴可能不太清楚,所以这里简单说一下,在屏幕上我们知道所有显示的内容都是由像素点组成的,那么在处理像素之前,我们需要先获取到像素信息,那么 Canvas 就是提供了一个 API 叫做 getImageData 让我们可以获取到画布上的像素信息,最终这个 API 返回的是一个 ImageData 类型的值,关于这个 API 的具体描述可以参考对应的MDN页面

ImageData 类型的数据包含三个属性,包括 data、width、height。width 和 height 简单来说,就是被提取像素信息的区域的宽高,最主要的像素信息是在这个 data 属性中。data 属性指向一个数组类型的值,准确来说是 Uint8ClampedArray 的实例,Uint8ClampedArray 表示 8 位无符号整型固定数组,也就是说其中的元素是 0 到 255 之间的整数,我们知道一个像素的颜色信息可以使用 rgba 四个分量表示,那么我们就得出在 data 数组中每四个元素就能表示一个像素点的信息,因此 data 数组的长度就是width * height * 4

了解完像素处理,我们就可以开始进行具体的实现了。


具体实现

<canvas ref="canvasRef" width="0" height="0"></canvas><canvas ref="magnifier" width="0" height="0"></canvas><!-- 放大镜 -->
复制代码


1. 准备工作


在实现放大效果之前,我们需要先把图片加载到 Canvas 上:

(async function() {  const img = await loadImage('src/assets/girl1.jpg');  canvasRef.value.width = img.width;  canvasRef.value.height = img.height;  context.drawImage(img, 0, 0);}());
复制代码


这里loadImage方法是通过 Image 对象来异步加载图片,然后通过 drawImage 方法将图片绘制到画布上。

接着设置一个要放大的区域,也就是以鼠标坐标为中心,多少半径以内的内容要被放大,这里我设置一个变量 originSize 用于存储原图大小,并设置一个 5 倍的放大倍数。

let originSize = 40; // 原图大小let zoom = 5; // 放大倍数
(async function() { // ... magnifier.value.width = originSize * zoom; magnifier.value.height = originSize * zoom;}());
复制代码


用作于放大镜的 magnifier,我们使用originSize * zoom来设置它的宽高。


2. 鼠标移动事件监听


接下来就是主要的代码实现。

首先是添加鼠标移动事件的监听:

const addEvent = () => {  canvasRef.value.addEventListener('mousemove', mouseDownHandler);};
addEvent();
复制代码


然后我们就来实现mouseDownHandler函数。

  • 首先我们获取鼠标坐标在 Canvas 中的相对坐标,并通过Math.floor取整

const mouseDownHandler = e => {  // 相对于画布的坐标  const center = {    x: Math.floor(e.pageX - left),    y: Math.floor(e.pageY - top)  };};
复制代码


  • 然后利用getImageData方法获取指定区域的像素信息,这里我们用到了OffscreenCanvas,它提供了一个可以脱离屏幕渲染的 canvas 对象,可以提升渲染性能;这样我们就得到了待放大区域的像素信息。

const mouseDownHandler = e => {  // 相对于画布的坐标  // ...  // 待放大区域的imageData  const originImageData = getImageData(img, [center.x - originSize / 2, center.y - originSize / 2, originSize, originSize]);};
复制代码


  • 现在我们需要一个 ImageData 类型的变量,用于存储放大后的像素信息,因为最终要渲染到 magnifier 这个 Canvas 上,我们就使用 magnifier 的 2d 上下文对象调用createImageData方法来创建一个 ImageData 对象,关于这个方法的使用具体可查看MDN文档

const mouseDownHandler = e => {  // 相对于画布的坐标  // ...  // 待放大区域的imageData  // ...  // 构建一个imageData  const areaImageData = mContext.createImageData(magnifier.value.width, magnifier.value.height);};
复制代码


  • 接下来就是具体的像素遍历和处理,按照 areaImageData 的宽高来进行遍历,这里迭代的增量使用+zoom是因为,当我们放大 zoom 倍数之后,原图 1 个像素的信息在 magnifier 使用zoom*zoom个像素来放大,也就是zoom*zoom个像素点的色值和原图中对应的那个像素的色值是一样的。在我们这段代码中设置 zoom 为 5,也就是放大后使用 5*5=25 个像素点表示之前的一个像素点。

const mouseDownHandler = e => {  // 相对于画布的坐标  // ...  // 待放大区域的imageData  // ...  // 构建一个imageData  // ...  let count = 0;  for (let j = 0; j < originSize * zoom; j += zoom) {    for (let i = 0; i < originSize * zoom; i += zoom) {
// ...
} }};
复制代码


  • 所以我们继续使用两个 for 循环 k 和 m,把 areaImageData 的 data 数组中的对应元素赋值为原图对应像素的色值,完成赋值后我们就可以通过 putImageData 方法将像素信息渲染到 magnifier 画布上。

const mouseDownHandler = e => {  // 相对于画布的坐标  // ...  // 待放大区域的imageData  // ...  // 构建一个imageData  // ...  let count = 0;  for (let j = 0; j < originSize * zoom; j += zoom) {    for (let i = 0; i < originSize * zoom; i += zoom) {
for (let k = j; k < j + zoom; k ++) { for (let m = i; m < i + zoom; m ++) { const index = (k * originSize * zoom + m) * 4; areaImageData.data[index] = originImageData.data[count]; areaImageData.data[index + 1] = originImageData.data[count + 1]; areaImageData.data[index + 2] = originImageData.data[count + 2]; areaImageData.data[index + 3] = originImageData.data[count + 3];
} } count += 4;
} } mContext.putImageData(areaImageData, 0, 0);};
复制代码


至此我们就实现了基本的局部放大,但现在放大镜不在原图 Canvas 的上方,并且放大镜是一个正方形,我们继续简单优化一下。


3. 简单优化


  • 首先因为我对 Canvas API 还不太熟悉,所以我现在通过 css 把放大镜改为圆形,并加上一个阴影box-shadow来优化视觉效果。

#magnifier {  position: absolute;  box-shadow: 0 0 10px 4px rgba(12, 12, 12, .5);  border-radius: 50%;}
复制代码


  • 然后给两个 Canvas 外层加一个 div 容器,把放大镜设置绝对定位,把它放到鼠标坐标的位置,在鼠标移动过程中更新放大镜的位置。

<div class="canvas-container" ref="containerRef" :style="{width: containerWidth + 'px'}">  <canvas ref="canvasRef" width="0" height="0"></canvas>  <canvas ref="magnifier" width="0" height="0" id="magnifier" :style="position"></canvas></div>
复制代码


const position = reactive({  left: 0,  top: 0});const containerWidth = ref(0);
containerWidth.value = img.width;// 在鼠标移动过程中更新放大镜的位置position.top = (center.y - originSize * zoom / 2) + 'px';position.left = (center.x - originSize * zoom / 2) + 'px';
复制代码


.canvas-container {  position: relative;  overflow: hidden;}
复制代码


  • 这个时候放大镜的位置就和我们预想的一致了,但是现在还有一个问题,就是放大镜在原图的上方,在移动的过程中会看到放大镜的渲染有点卡顿,这是因为鼠标移动事件是加在原图 Canvas 上的,当鼠标悬浮在放大镜上时,这个移动事件的监听就不连贯了,此时我们可以考虑把鼠标移动监听加改为加在外层容器上,这样就能看到移动过程中放大镜的渲染是比较流畅了。

const addEvent = () => {  containerRef.value.addEventListener('mousemove', mouseDownHandler);};
复制代码


至此就完成了简单的局部放大效果,虽然还存在一些问题吧。


总结


以上的实现比较简单粗暴,就是遍历 imageData 然后赋值,不算什么很高明的思路,就当作是抛砖引玉吧。


文章转载自:beckyye

原文链接:https://www.cnblogs.com/beckyyyy/p/18101423

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
可视化学习:实现Canvas图片局部放大镜_可视化_不在线第一只蜗牛_InfoQ写作社区