热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

图片在canvas中的选中/平移/缩放/旋转,包含了所有canvas的2D变化,让你认识到数学的重要性...

1、介绍canvas已经出来好久了,相信大家多少都有接触。如果你是前端页面开发移动开发,那么你肯定会有做过图片上传处理,图片优化ÿ

1、介绍

 

  canvas 已经出来好久了,相信大家多少都有接触。

  如果你是前端页面开发/移动开发,那么你肯定会有做过图片上传处理,图片优化,以及图片合成,这些都是可以用 canvas 实现的。

  如果你是做前端游戏开发的,可能会非常熟悉,或者说对几何和各种图形变化非常了解。

  这里我介绍的是简单的、基本的,但是非常完全的一个 2d 的 canvas 案例。

  基本上了解了这些,所有的 canvas 中的 2d 变化基本都可以会了。

 

  先来一个截图看看效果:

 

 

  如上面所看,可以总结出几个功能点:

 

    1、添加多张图片或者文字到 canvas 。( 这里没有添加文字,我们可以先把文字利用canvas转为图片,然后添加 canvas 上 )

    2、图片的缩放,根据选择不同的点实现不同缩放

    3、图片移动,改变图片在 canvas 的中心位置

    4、图片旋转,根据旋转点在移动的角度进行旋转

    5、图片选择,两种方式:一种根据图片的位置,确定当前选择的图形,第二种是点击列表选择

    6、数据的保存,提供了保存按钮,保存图形的位置和大小以及旋转角度

    7、初始化数据,通过之前保存的数据,重新绘制。

 

 

  代码案例介绍: 

    html 代码:

  




  

  js代码是模块形式开发的,并且传到 npm 上面,可以自行下载并且有源码:

 

yarn add xl_canvas

  

  代码调用和实现:

import Canvas from 'xl_cnavas';const dataCa = sessionStorage.getItem('test_tst_111');const canvas = new Canvas({canvas: 'test',target: 'test',list: 'list',height: 960,width: 960,data: dataCa?JSON.parse(dataCa):[],});document.getElementById('save').addEventListener('click', () => {sessionStorage.setItem('test_tst_111',JSON.stringify(canvas.save()));});// canvas.addPhoto('https://cdn.eoniq.co/spree/images/283205/desktop/CI-26-LS_b6bb28a3914ae9caa651abbddb548054.jpg?1533196945');// canvas.addPhoto('http://www.runoob.com/wp-content/uploads/2013/11/img_the_scream.jpg');

  

  npm 包没有测试,本地的可以实现各种方法了。如有问题可以留言。。

 

2、项目开发

  

  知识梳理:

 

  在开发中我们需要很多关于平面几何的知识来处理我们的操作,例如:

  

    1、确定某个点是否在矩形内   : 用于确定点击时候选中的图形

    2、计算向量的角度 : 用于处理旋转

    3、计算某个向量在另一个向量上面的距离 : 用于旋转之后,的移动距离计算

    4、某点绕道某点旋转一定角度的点 : 用于确定旋转后的点的位置

    

  是不是脑子里浮现了很多高中初中的数学几何公式。

  如果没有,百度下吧,都是很多有意思的公式,让自己重温下高中数学,回忆一下高中。

  证明一下自己学过高中数学。

  

  

  代码设计/简要开发介绍:

  

  以下如果需要查看,最好下载源码对照的查看

  如何开始这个功能的开发呢?

  

  1、首先创建一个 Canvas  类

 

constructor(options) {this.options = options;const {canvas,height,width,target,before,after,data = [],list = null,} = this.options;this.canvas = null; // 画布this.height = height; // 画布的宽高this.width = width;this.target = target;this.before = before;this.after = after;this.data = data;this.layers = []; // 画布的层if (typeof canvas === 'string') {this.canvas = document.getElementById(canvas);} else {this.canvas = canvas;}if (typeof target === 'string') {this.target = document.getElementById(target);} else {this.target = target;}if (typeof list === 'string') {this.list = document.getElementById(list);} else {this.list = list;}this.canvas.width = width;this.canvas.height = height;this.context = this.canvas.getContext('2d'); // 画布对象this.loaded = 0;this.border = new Border(this);this.current = null;this.init();this.initEvent();}

  

  这是 canvas 类的构造函数,这里接受有参数:

 

    canvas : 传入 canvas 对象或者当前 html 的元素的 id,以供整个功能的开发。

    height / width : 宽和高,整个绘制过程中,宽和高都是这个为基准

    target : 这个是用来接受事件的元素。这个应该和 canvas 对象的元素宽高相等

    before / after :当初始化数据到时候,会知道初始化数据之前操作和初始化之后操作

    data : 绘制的数据

 

 

  重要的属性:

  

    layers :添加到画布的图形,类似图层。

    context : canvas 的上下文,用来绘制的 api 集合

    border : 绘制的骨架,当选中某一个图形的时候,会出现外层的骨架。( 这个单独创建一个类 )

    current :当前的图形,也可以理解为当前的图层。

 

  主要方法介绍(介绍几个重要的):

    

  addPhoto 方法:

// 添加图片addPhoto(image) {if (typeof image &#61;&#61;&#61; &#39;string&#39;) {this.loaded &#43;&#61; 1;const lyr &#61; new Photo(image, this, () &#61;> {setTimeout(() &#61;> {this.loaded -&#61; 1;if (this.loaded <1) {this.draw();}}, 100);});this.layers.push(lyr);this.addItem(image, lyr.id);} else {const lyr &#61; new Photo(image, this);this.layers.push(lyr);this.addItem(image, lyr.id);this.draw();}}

  

  这里是添加 Photo 的方法&#xff0c;其中 photo 是用 Photo 类创建实例的。

  可以先看一下下面介绍的 Photo 类&#xff0c;可以更好了解开发过程。

 

  draw方法&#xff08;用来触发绘制&#xff09;&#xff1a;

draw() {this.clear();this.layers.forEach((item) &#61;> {if (typeof item &#61;&#61;&#61; &#39;function&#39;) {item.apply(null, this.context, this.canvas);} else {item.draw();}});if (this.current) {this.border.refresh(this.current.rect);}}

  

  上面代码是来绘制 layers 的图层到 canvas 上。

  这里会判断 layers 中是否是图层&#xff0c;如果是图层才会绘制图层

  如果不是&#xff0c;就会直接执行方法&#xff0c;该方法传入的当前的 canvas 这个实例。

  也可以绘图案到 canvas 上&#xff0c;这样就可以实现层级关系。

  

  上面做了一个判断&#xff0c;就是是否绘制 border &#xff0c;在有选中的情况下会绘制 骨架的

  即 调用 border 的 refresh 方法。在这里可以先去看看 Border 类。( 下面有介绍 )

 

  initEvent 方法&#xff08;用于绑定方法&#xff09;&#xff1a;

initEvent() {this.target.addEventListener(&#39;mousedown&#39;, (e) &#61;> {let p_x &#61; e.pageX;let p_y &#61; e.pageY;const position &#61; getDocPosition(this.target);const scale &#61; this.width / this.target.offsetWidth;const point &#61; [(p_x - position.x) * scale,(p_y - position.y) * scale,];const status &#61; this.selectPhoto(point);if (status) {const move &#61; (event) &#61;> {const m_x &#61; event.pageX;const m_y &#61; event.pageY;const vector &#61; [(m_x - p_x) * scale, (m_y - p_y) * scale];if (status &#61;&#61;&#61; 1) {this.current.rect.translate(vector);} else if (status &#61;&#61;&#61; &#39;r_point&#39;) {const e_point &#61; [(m_x - position.x) * scale, (m_y - position.y) * scale];const angle &#61; Canvas.getAngle(this.current.rect.center,this.border.r_point,e_point,);if (!isNaN(angle)) {this.current.rect.rotate(angle);} else {return;}} else {this.current.rect.zoom(status, vector);}this.draw();p_x &#61; m_x;p_y &#61; m_y;};this.target.addEventListener(&#39;mousemove&#39;, move);this.target.addEventListener(&#39;mouseup&#39;, () &#61;> {this.target.removeEventListener(&#39;mousemove&#39;, move);});}});this.list.addEventListener(&#39;click&#39;, (e) &#61;> {if (e.target && e.target.nodeName.toUpperCase() &#61;&#61;&#61; &#39;IMG&#39;) {const id &#61; parseInt(e.target.getAttribute(&#39;data-id&#39;));this.layers.forEach((item, index) &#61;> {if (item.id &#61;&#61;&#61; id) {this.chooseItem(index);}});}});}

  

  这个是给 target 对象绑定事件&#xff0c;通过对事件的不同处理来就触发不同的方法。

  都是直接改变当前的 current 上面的 rect 数据&#xff0c;然后重新绘制。

 

    图形的选取 &#xff1a; selectPhoto 方法调用&#xff0c;当选中的时候就会设置当前的 current 的图层

    图形的移动 &#xff1a; move 方法调用&#xff0c;移动图层

    图形的缩放 &#xff1a; zoom 方法调用&#xff0c;接受不同的缩放形式

    图形的旋转 &#xff1a; rotate 方法调用&#xff0c;接受角度进行旋转

 

  其他的方法&#xff1a;

 

  addItem : 向 list 元素对象中添加元素

  selectPhoto &#xff1a;判断当前的位置确定选中的 Photo

  chooseItem &#xff1a;用于 list 元素中的选取

  clear &#xff1a; 清楚 canvas 画布

  save &#xff1a; 返回 rect  数据。用于存储数据和保存

 

 

 

  2、Photo 类

 

constructor(image, canvas, load) {this.canvas &#61; canvas;this.img &#61; image;this.load &#61; load;this.id &#61; new Date().getTime();this.isLoad &#61; false;if (image.rect) {this.options &#61; image;this.img &#61; this.options.img;this.id &#61; this.options.id;}this.pre();}

  

  还是看构造函数&#xff0c;介绍属性和方法&#xff1a;

    

    canvas &#xff1a; 就是相当于继承来的&#xff0c;或者是说 canvas 要全局使用

    image &#xff1a;可能是对象&#xff0c;也可以能是 资源地址&#xff0c;但是大多数应该是资源地址

    id &#xff1a; photo 的 id&#xff0c;用于查找和选择等

    rect &#xff1a;这个是重要的&#xff0c;photo 的数据&#xff0c;如&#xff1a;坐标/宽高/角度等 

 

  稍后介绍 rect 类&#xff0c;先介绍下 photo 的方法&#xff1a;

  

  用于创建 rect 的init方法&#xff1a;

init() {if (this.load) this.load();if (this.options) {const {width, height, center, angle,} &#61; this.options.rect;this.rect &#61; new Rect(width,height, [center[0], center[1]], angle);return;}this.rect &#61; new Rect(this.image.width,this.image.height, [this.canvas.width / 2, this.canvas.height / 2], 0);}

  

  每次 new Photo 都会创建了一个 ract 实例&#xff0c;作为它的数据存储 this.rect 。

  每次创建一个 Photo 的时候并且加入到 canvas 的 layers 中的时候并没有开始绘图

  绘图需要调用 Photo 的 draw 方法来触发&#xff0c;如下&#xff1a;

 

draw() {const { image, canvas, rect } &#61; this;const { context } &#61; canvas;const points &#61; rect.point;const [c_x, c_y] &#61; rect.center;context.save();context.translate(c_x, c_y);context.rotate(rect.angle);context.drawImage(image, 0, 0, image.width, image.height,points[0][0] - c_x,points[0][1] - c_y,rect.width,rect.height);context.restore();}

  

  在 canvas 实例调用 draw 方法时候&#xff0c;会一次绘制 layers 中的所有 photo 实例进行绘制。

 

  

  3、rect 类

 

constructor(width, height, center, angle) {this.height &#61; height;this.width &#61; width;this.center &#61; center;this.angle &#61; angle;this.point&#61;[]this.getPoint();}

  

  这里是通过传入 width / height /center / angle 来确定和初始化 photo 在 canvas 上的输出。

  

    height / width  &#xff1a; 这是图形的宽高

    center &#xff1a; 图形的中间位置

    angle &#xff1a;很显热&#xff0c;是图形旋转的角度

    point &#xff1a; 四个顶点的位置

 

  一个图形&#xff0c;有了这个写数据&#xff0c;基本上能在 canvas 确定位置、大小以及各种形变。

   

  rect 实例的方法&#xff1a;

 

 

  代码有点多&#xff0c;就简要介绍吧&#xff01;

  我们的操作实际上都是操作 rect 的数据。

  一些判断也是于 rect 数据做对比&#xff0c;或者计算 rect 对象里面的数据。

 

    rotate &#xff1a; 旋转后 rect 的顶点位置的计算

    translate &#xff1a; 移动后中点位置计算和顶点位置计算

    zoom &#xff1a; 缩放后顶点和中点的位置计算

    isPointInRect &#xff1a; 是否在 Rect 的四个顶点里面

 

  Rect 的类基本介绍完毕了。每次改变后调用 canvas 的 draw 方法重绘制。

 

 

  4、Border 类 

 

  查看这个类最好先浏览下 rect 类 和 Photo 类

 

constructor(canvas) {this.canvas &#61; canvas;}

    

  这里创建只是获取到了全局的 canvas 实例。用于后面调用

  

  refresh 方法&#xff1a;  

refresh(rect) {this.rect &#61; rect;this.point &#61; this.rect.point;// 中点this.c_point &#61; [];this.point.reduce((a, b) &#61;> {this.c_point.push([(a[0] &#43; b[0]) / 2, (a[1] &#43; b[1]) / 2]);return b;}, this.point[3]);// 旋转点this.r_point &#61; [(this.point[0][0] &#43; this.point[1][0]) / 2,this.point[0][1] - 35];this.draw();}

  

  这里是接受 rect 的数据&#xff0c;

  然后通过 rect 数据&#xff0c;得到顶点 / 各个线上的中点 / 旋转点

  调用 refresh 之后就会执行 draw 方法&#xff1a;

draw() {const {point,center,angle,width,height,} &#61; this.rect;const { context } &#61; this.canvas;const [c_x, c_y] &#61; center;const points &#61; point;context.save();context.translate(c_x, c_y);context.rotate(angle);context.beginPath();context.lineWidth &#61; &#39;2&#39;;context.strokeStyle &#61; &#39;#73BFF9&#39;;context.rect(points[0][0] - c_x,points[0][1] - c_y,width,height);const pointList &#61; points.concat(this.c_point);pointList.push(this.r_point);pointList.forEach((item) &#61;> {const [x, y] &#61; item;context.fillStyle &#61; &#39;#73BFF9&#39;;context.fillRect(x - 6 - c_x, y - 6 - c_y, 12, 12);});context.moveTo((points[0][0] &#43; points[1][0]) / 2 - c_x,points[0][1] - c_y);context.lineTo(this.r_point[0] - c_x, this.r_point[1] - c_y);context.stroke();context.closePath();context.restore();}

  

  可以看到这里是绘制&#xff0c;并且绘制都是依赖 rect 的数据。

  所以我们并不需要处理旋转 / 移动 / 缩放等操作&#xff0c;因为每次修改后 rect 数据就会变。

 

 

   isPointInSkeletion &#xff1a; 判断时候在对应的操作点上&#xff0c;并返回对应的操作点名称

 

  

 介绍完毕&#xff0c;简要的介绍开发的设计和流程。如需谅解&#xff0c;请看看源码。。

 

https://www.cnblogs.com/jiebba/p/9667600.html 

   我的博客 &#xff1a;  XiaoLong&#39;s Blog

   博客园小结巴巴&#xff1a; https://www.cnblogs.com/jiebba

  

 

转:https://www.cnblogs.com/jiebba/p/9667600.html



推荐阅读
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Servlet多用户登录时HttpSession会话信息覆盖问题的解决方案
    本文讨论了在Servlet多用户登录时可能出现的HttpSession会话信息覆盖问题,并提供了解决方案。通过分析JSESSIONID的作用机制和编码方式,我们可以得出每个HttpSession对象都是通过客户端发送的唯一JSESSIONID来识别的,因此无需担心会话信息被覆盖的问题。需要注意的是,本文讨论的是多个客户端级别上的多用户登录,而非同一个浏览器级别上的多用户登录。 ... [详细]
  • 本文介绍了使用C++Builder实现获取USB优盘序列号的方法,包括相关的代码和说明。通过该方法,可以获取指定盘符的USB优盘序列号,并将其存放在缓冲中。该方法可以在Windows系统中有效地获取USB优盘序列号,并且适用于C++Builder开发环境。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • {moduleinfo:{card_count:[{count_phone:1,count:1}],search_count:[{count_phone:4 ... [详细]
  • 1.Listener是Servlet的监听器,它可以监听客户端的请求、服务端的操作等。通过监听器,可以自动激发一些操作,比如监听在线的用户的数量。当增加一个HttpSession时 ... [详细]
  • Vue基础一、什么是Vue1.1概念Vue(读音vjuː,类似于view)是一套用于构建用户界面的渐进式JavaScript框架,与其它大型框架不 ... [详细]
  • 工作经验谈之-让百度地图API调用数据库内容 及详解
    这段时间,所在项目中要用到的一个模块,就是让数据库中的内容在百度地图上展现出来,如经纬度。主要实现以下几点功能:1.读取数据库中的经纬度值在百度上标注出来。2.点击标注弹出对应信息。3 ... [详细]
  • x86 linux的进程调度,x86体系结构下Linux2.6.26的进程调度和切换
    进程调度相关数据结构task_structtask_struct是进程在内核中对应的数据结构,它标识了进程的状态等各项信息。其中有一项thread_struct结构的 ... [详细]
  • struts2重点——ValueStack和OGNL
    一、值栈(ValueStack)1.实现类:OGNLValueStack2.对象栈:CompoundRoot( ... [详细]
  • 必须先赞下国人npm库作品:node-images(https:github.comzhangyuanweinode-images),封装了跨平台的C++逻辑,形成nodejsAP ... [详细]
  • 相关的部分代码如下 ... [详细]
author-avatar
感激遇见彭
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有