html5canvas绘制图片(htmltocanvas)

  在看 D3.js 的时候,无意间看到了一个例子,觉得很有趣,像是会分裂的圆形马赛克。看了下代码,使用 svg 完成的,但是具体实现方式使得在手机端无法把玩,于是就自己实现了一个 canvas 版本的。代码很简单,canvas 初学者可以自己试试当做练笔,还是挺有趣的一个效果。

Online Demo

  online demo:https://demo.jackyang.me/circle-split/circle-split-demo.html

  在 demo 中任意从本地选择一张图片,然后通过鼠标移动或者移动端 touchmove 就能实现圆形分裂的效果。

  难点

  说是难点,其实根本不难。一开始看到的时候会好奇大大小小的圆形的颜色是怎么计算的,计算该面积下的平均值?其实很简单,就是从绘制了图片的 canvas 上获取圆心坐标在图片对应位置上的颜色值。这样的算法在圆形半径较大的时候,对被遮盖的图片区域颜色代表性其实不好,但是从整个分裂过程来看,这个取色方案的效果还不错。

  关键技术点

canvas 绘图:CanvasRenderingContext2D.drawImage()

canvas 绘制圆形:CanvasRenderingContext2D.arc()

canvas 上取指定坐标上的颜色值:CanvasRenderingContext2D.getImageData()

思路

将图片绘制在一个 offline(即不用挂在 DOM 树上)的 canvas 上,为了在指定位置获取颜色用

创建另一个 canvas,用来绘制圆。两个 canvas 尺寸保持一致(而且都是方形),方便无需坐标转换获取颜色

绘制第一个圆形,以 canvas 中心为圆心,使用对应 offline canvas 坐标上的颜色填充

维持一个circles数组,代表所有的圆,每个元素有坐标(x, y),半径(r)和是否标记分裂(readyToSplit)

需要一个渲染循环(rendering loop),不断的找出被标记需要分裂(readyToSplit)的圆,拿去做分裂绘制

事件处理:当 mousemove 或者 touchmove 发生在圆上时,该圆被标记readyToSplit = true,后面的则有渲染循环去处理

测试驱动

  在我自己做这样的编程时,会以测试驱动的方式开始代码。因此会脑子里先写下自己的类将被如何使用,怎么样能够简单易用。

  我打算把这个效果封装成一个类,它将在使用时被实例化。最终的效果肯定是要在 DOM 树上显示的,所以这里在实例化时肯定需要指定一个 mount 节点,所有的事情在其内部进行。而且,按照通常的习惯,开放一些配置,使得使用者可以做一些简单的定制化。但是目前还没有想好哪些内部的配置拿出来比较合适,所以第二个参数options可以后面再考虑。

  varcs = newCircleSplit( '#mountNode', options);

  我希望能够动态的切换显示的图片内容,所以想提供一个setImage的方法,它应该能接受图片路径,或者Image元素对象。

  cs.setImage(image);

  OK,这就是目前我希望的实例化方式,和想要提供的接口。后面再具体实现过程中,可以再继续添加或者修改。

  试想内部

  结合前面谈到的实现思路,考虑CircleSplit类里面该如果定义属性和私有共有方法。

  从构造函数入手。个人习惯在构造函数最后加上 init 方法,init 方法里做一些准备工作,完成setImage前的一些必要的事情。

  functionCircleSplit(el, options){

  ...

  this._init();

  }

  CircleSplit.prototype._init = function(){

  this._createSourceCanvas(); // 创建源canvas,用来绘制图片,作为offline canvas,提供坐标颜色使用

  this._createTargetCanvas(); // 创建目标canvas,用来绘制看到的大大小小的圆

  this._render(); // 开启渲染循环

  this.bindEvent(); // 绑定事件,touchmove mousemove这些

  }

  这样我们一下子多了好几个函数,而且目的都很明确,因此可以很容易的判断需要那些实例属性和该如何实现各自函数体。这里可能需要多注意一下_render(),思路中谈到在这里应该去绘制需要分裂的圆,那么大致应该像下面这样:

  CircleSplit.prototype._render = function(){

  // 循环体

html5canvas绘制图片(htmltocanvas)

  this.circles.forEach( function(circle){

  if(circle.readyToSplit) {

  this._splitCircle(circle);

  circle.readyToSplit = false;

  }

  }, this);

  // 下一个循环

  requestAnimationFrame( this._render.bind( this));

  }

  而什么时候设置circle.readyToSplit呢?就是在bindEvent()的事件处理函数里面。这里会通过_tagCircle()遍历circles,找到能 hit 到事件坐标的一个圆,将其标记(tag)上 readyToSplit。

  从共有方法入手。setImage之后,相当于将整个 CircleSplit 中的状态都重置了下,circles数组得重置,两个canvas得重置等。

  CircleSplit.prototype.setImage = function(image){

  this._resetCanvas( this.sourceCanvas); // clear source canvas

  this._drawSourceImage(image); // draw source canvas

  this._resetCanvas( this.targetCanvas); // clear target canvas

  this._drawCircle(x, y, r) // draw target canvas。绘制第一个,也是最大的一个圆形。圆心为canvas中心,半径为canvas的一半

  }

  _drawSourceImage()里面就是调用了CanvasRenderingContext2D.drawImage()进行图片绘制。这个 API 函数有 3 种传参形式,我这里选择了 5 参数的形式,使用了自己写的简易的居中库 CenterIt,来解决图片居中绘制问题:无论图片尺寸,都可以轻易的居中覆盖填充(cover)或者居中包含(contain)填充。

  这里的_drawCircle(x, y, r)应该能重用,后面每次圆形分裂的时候都能调用。初步给它 3 个参数,圆心坐标和半径。在其内部应该能够自己去获取坐标对应的颜色值。所以简单想象一下它的内部:

  CircleSplit.prototype._drawCircle = function(x, y, r){

  ...

  context.fillStyle = this._getColor(x, y); // 获取坐标颜色

  context.beginPath();

  context.arc(x, y, r, 0, 2* Math.PI);

  context.closePath();

  context.fill();

  ...

  }

  绘制圆时使用CanvasRenderingContext2D.arc()API,使用起来不算简单明了,每次还需要 begin 和 close Path。相比而下,一些 canvas 的游戏库或者图形库,则简单直观的多:

  // create.js

  varcircle = newcreatejs.Shape();

  circle.graphics.beginFill( "DeepSkyBlue").drawCircle( 0, 0, 50);

  // two.js

  varcircle = two.makeCircle( 72, 100, 50);

  circle.fill = '#FF8000';

  circle.stroke = 'orangered';

  circle.linewidth = 5;

  因此,如果要做比较复杂的绘制操作,推荐找一个适合自己的 canvas 库,会使得工作变得容易的多。

  关于_getColor()函数,这里使用了CanvasRenderingContext2D.getImageData():

  CircleSplit.prototype._getColor = function(x, y){

  ...

  varpixelData = this.sourceCanvas.getContext( '2d').getImageData( parseInt(x), parseInt(y), 1, 1).data;

  return'rgb('+ pixelData[ 0] + ','+ pixelData[ 1] + ','+ pixelData[ 2] + ')';

  }

  如下图:

  假设左上角起始点为(x, y),一个方格为一个像素,那么getImageData(x, y, 1, 1).data就会返回[255,0,0,255],代表 Red=255,Alpha=255。如果getImageData(x, y, 2, 2).data就会返回[255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255] 长度为 16 的数组,每 4 个为一组代表一个像素上的 rgba 值。getImageData()就是一个能帮助我们对 canvas 进行像素级别操作的API函数。

  一些基于 canvas 的“刮刮卡”插件,也是getImageData()的应用:在图片上绝对定位一个灰色的canvas,代表刮刮卡蒙层;通过对手指触摸的像素点的 alpha 值进行修改来实现被“刮“开的效果。当然这里的修改需要使用到配套的putImageData()函数;同时对整个 canvas 像素中 alpha 值为 0 的像素点的百分比 进行统计,可以完成刮开了80%就展示全部图片的效果。

  实现

  上面是大致的实现思路,和编码的思想过程。为了表达出我自己在完成一个功能的时候,是如何从无到有,定义属性,定义 API 的。只是自己的一点经验,希望有帮助。

  如果你对这些知识不熟悉,却也感兴趣的话,可以参考该 github 项目代码

  问题与优化

  github 上的代码与上面讲的思路一致,但是会有些不一样,主要是在功能实现之后,发现了一个需要优化的地方。

  渲染速度在_render()渲染循环中,我们对所有的 circles 进行遍历。但是当整副图片分裂次数很彻底时,会有上万个圆,会导致每个渲染循环里的计算时间过长,导致下一个渲染循环在理想的时间后才执行,从而导致了卡顿的感觉。于是为了解决这个问题,引入了renderingCircles数组,将被标记的 circle 全部插入这个数组中,渲染循环中只关心这里的值,用额外的存储空间换更短的计算时间。

  显示模糊最先的实现中,两个 canvas 得尺寸是根据 mountNode 决定的,canvas.width canvas.height被设为和 mountNode 一样的维度值。于是在一些设备上显示出明显的边缘锯齿。这里的解决方案就是设置 canvas 的宽和高为两倍于 mountNode 的宽高,然后通过style去设置 canvas 显示成和 mountNode 一样的尺寸。这里就是 canvas 的自身的宽高属性和 canvas style 的宽高之前的区别的理解和应用。

  图片跨域问题在 canvas 操作图片时,可能会碰到这样的错误信息:Unable to get image data from canvas because the canvas has been tainted by cross-origin data.

  关于这个的官方解释是:

  在 canvas 上可以绘制没有跨域许可的图片资源(images without CORS approval),但是这样做会“感染(taints)”的 canvas,而在感染的(tainted)canvas 上调用toBlog(),toDataURL(),getImageData()会抛出上面的安全方面的错误。

  在CircleSplit.setImage(imageUrl)时,可能就会碰到这个问题。

  解决方案,首先需要图片有跨域许可。这个需要在提供图片服务的 server 上进行配置。这里不多介绍,有跨域许可的图片被加载时,在控制台上应该能看到:(这里我使用的七牛的图片)

  

  其次,需要在加载图片时,设置crossOrigin属性:

  varimage = newImage();

  image.crossOrigin = 'anonymous';

  image. = function(){};

  image.src = imageUrl; 应用

  其实个人很喜欢最后完成的交互效果(有点强迫症,喜欢不断的戳掉泡泡),于是将这个小效果做了一个简单的 H5 页面,在年底这个时间点里,讲述和回顾在 2016 年的大事件。你也可以来体验下:2016-recap(https://2016-recap.jackyang.me/index.html)

  原文地址:https://blog.jackyang.me/blog/index.html#/post/5863182161ff4b006cf3f5f5

  【推荐阅读】

tomcat的猫- 上次发版我就改了一行代码

weichuyang - 爬取知乎 60 万用户信息之后的简单分析

乖小鬼YQ - 割裂的前端工程师——2017 年前端生态窥探

嘉宝Appian - 一段人人都应该知道的从 vue 到 react 的过渡史

Rresn - Vue 中你不知道但是很实用的黑科技

aco - Web 前端 @ 功能 JS 实现分析及其原理

1、本网站名称:源码村资源网
2、本站永久网址:https://www.yuanmacun.com
3、本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
4、本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
5、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
6、本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
源码村资源网 » html5canvas绘制图片(htmltocanvas)

1 评论

您需要 登录账户 后才能发表评论

发表评论

欢迎 访客 发表评论