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

前言

最近我在可视化课程中学习了如何在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 => {// 相对于画布的坐标// ...// 待放大区域的imageDataconst 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// ...// 构建一个imageDataconst 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

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/781508.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

elementUI this.$msgbox msgBox自定义 样式自定义 富文本

看这个效果是不是很炫?突出重点提示内容,对于用户交互相当的棒! 下来说说具体实现: let self = this const h = self.$createElement; this.$msgbox({title: null,message: h("p", {style: "margin-top:10px"}, [h("i", {class: "el-i…

命名空间【C++】(超详细)

文章目录 命名空间的概念命名空间的定义命名空间定义的位置作用域每一个命名空间都是一个独立的域作用域符&#xff1a;&#xff1a; 编译器找一个变量/函数等的定义&#xff0c;寻找域的顺序为什么要有命名空间&#xff1f;1.解决库与程序员定义的同名的重定义问题2.解决程序员…

【氮化镓】p-GaN栅极退化的温度和结构相关性

论文总结&#xff1a; 本文献深入研究了带有p-GaN栅极的正常关断型(normally-off)高电子迁移率晶体管(GaN-HEMTs)在恒定电压应力下的时序退化行为。通过直流特性分析和温度依赖性分析&#xff0c;研究了故障时间(TTF)与应力温度和器件几何结构的依赖性。结果显示&#xff0c;p…

JavaWeb作业四

一. 单选题&#xff08;共5题&#xff0c;30分&#xff09; 1. (单选题, 6分) 以下关于Spring核心容器相关说法错误的是。 A Spring框架的所有功能都是通过其核心容器来实现的。 B 创建BeanFactory实例时&#xff0c;需要提供Spring所管理容器的详细配置信息&#xff0c;这…

FME学习之旅---day17

我们付出一些成本&#xff0c;时间的或者其他&#xff0c;最终总能收获一些什么。 【FME-HOW-TO系列】28 栅格邻域函数 RasterConvolver转换器说明&#xff1a; 接受包含栅格几何对象的输入要素&#xff0c;并在对所有波段应用卷积滤波 器后输出要素。 本人对栅格数据处理的较…

mysql笔记:25. docker环境中mysql主从复制、主主复制实操

文章目录 一、准备工作1. 安装配置Docker2. 准备MySQL相关的配置和数据目录 二、基于日志点的主从复制1. 配置Master服务器1.1 修改配置文件1.2. 在docker中启动Master节点1.3. 创建用户并授权 2. 配置Slave1服务器2.1. 修改配置2.2. 启动服务2.3. 指定Master2.4. 开始复制 3. …

vue computed和watch的区别【网易,京东,拼多多】

知识点 computed和watch的区别 题目 讲一下vue组件的computed和watch的区别。 答案 应用场景不同 computed用在根据data属性或者其他computed计算得到一个新值的情况&#xff0c;computed的值一般被用在渲染中。 watch用在监听数据变化&#xff0c;然后做一些有副作用的操…

【2023】kafka在linux和docker安装(kafka-1)

目录&#x1f4bb; 一、linux安装kafka1. 安装jdk2. 上传解压到/usr/local目录下3、使用kafka 二、docker安装kafka1. 下载2. 安装zookeeper3. 安装kafka 一、linux安装kafka 环境主机 mac m2、虚拟机Ubuntu22.04.4 1. 安装jdk yum install -y java-1.8.0-openjdk.x86_64下载k…

11-设计模式:Go常用设计模式概述

设计模式是啥呢&#xff1f;简单来说&#xff0c;就是将软件开发中需要重复性解决的编码场景&#xff0c;按最佳实践的方式抽象成一个模型&#xff0c;模型描述的解决方法就是设计模式。使用设计模式&#xff0c;可以使代码更易于理解&#xff0c;保证代码的重用性和可靠性。 …

【Entity Framework】EF中DbSet类详解

【Entity Framework】EF中DbSet类详解 文章目录 【Entity Framework】EF中DbSet类详解一、概述二、定义DbSet2.1 具有DbSet属性的DbContext2.2 具有 IDbSet 属性的 DbContext 2.3 具有 IDbSet 属性的 DbContext三、DbSet属性四、DbSet方法五、DbContext动态生成DbSet 一、概述 …

打工人神器! Raccoon 代码小浣熊

继这三个之后&#xff0c;今天又来了一个 [ Raccoon代码小浣熊 ] 核心精要与产品特点 全面支持多种编程语言和IDE&#xff1a;「代码小浣熊」支持超过90种主流编程语言&#xff0c;包括但不限于Python、Java、JavaScript、C、Go和SQL等。同时&#xff0c;它集成了市面上主流的…

Quiet-STaR:让语言模型在“说话”前思考

大型语言模型(llm)已经变得越来越复杂&#xff0c;能够根据各种提示和问题生成人类质量的文本。但是他们的推理能力让仍然是个问题&#xff0c;与人类不同LLM经常在推理中涉及的隐含步骤中挣扎&#xff0c;这回导致输出可能在事实上不正确或缺乏逻辑。 考虑以下场景:正在阅读一…

CTF题型 php://filter特殊编码绕过小汇总

CTF题型 php://filter特殊编码绕过小汇总 文章目录 CTF题型 php://filter特殊编码绕过小汇总特殊编码base64编码string过滤器iconv字符集 例题1.[Newstarctf 2023 week2 include]2.[Ctfshow web 117] php://filter 是一个伪协议&#xff0c;它允许你读取经过过滤器处理的数据流…

YOLO图像前处理及格式转换

import cv2 import numpy as np import os import glob# 数据增强函数 def augment_data(img):rows,cols,_ img.shape# 水平翻转图像if np.random.random() > 0.5:img cv2.flip(img, 1)img_name os.path.splitext(save_path)[0] "_flip.png"cv2.imwrite(img_n…

【C++】string类(常用接口)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 修改操作 push_back append operator assign insert erase replace c_str find string类非成…

AI学习-Pandas数据处理分析

文章目录 1. Pandas概述2. Series用法2.1 Series的创建2.2 Series的取值2.3 Series的相关方法 3. DataFrame用法3.1 DataFrame创建3.2 DataFrame取值3.3 DataFrame相关方法 1. Pandas概述 ​ Pandas 是一个开源的数据分析处理库&#xff0c;它应用在数据科学、统计分析、机器学…

9.0-源码分析:Dubbo Remoting 层核心接口分析

dubbo-remoting 模块&#xff0c;该模块提供了多种客户端和服务端通信的功能。在 Dubbo 的整体架构设计图中&#xff0c;我们可以看到最底层红色框选中的部分即为 Remoting 层&#xff0c;其中包括了 Exchange、Transport和Serialize 三个子层次。这里我们要介绍的 dubbo-remot…

C++类继承继承5——构造函数与拷贝控制

构造函数与拷贝控制 和其他类一样&#xff0c;位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为&#xff0c;这些操作包括创建、拷贝、移动、赋值和销毁。 如果一个类(基类或派生类)没有定义拷贝控制操作&#xff0c;则编译器将为它合成一个版本。当然…

手写简易操作系统(十七)--编写键盘驱动

前情提要 上一节我们实现了锁与信号量&#xff0c;这一节我们就可以实现键盘驱动了&#xff0c;访问键盘输入的数据也属于临界区资源&#xff0c;所以需要锁的存在。 一、键盘简介 之前的 ps/2 键盘使用的是中断驱动的&#xff0c;在当时&#xff0c;按下键盘就会触发中断&a…

乐理通识

2023 年搞了台雅马哈 61 键的电子琴&#xff0c;顺手看了下啊 B 的上的课程 《零基础自学音乐学乐理合集-第一季》&#xff0c;这里是部分笔记&#xff08;给博客加点不一样的东西&#x1f440;&#xff09;。 简谱各部分一览 C 表示音名竖线为小节线 音名 完整钢琴键盘 88 键…