实践是检验程序员的惟一标准01:用户不想跟你说话并向你扔出一张图片 - 图片上传组件开发【思路篇】

舒适提示:这里除了一些幼稚的小组件啥也没有
舒适提示-续:这是一个新的系列,写一些实际开发中遇到的一些经常使用的功能,想法笨拙,代码乱套javascript

写在前面

图片上传,做为web端一个经常使用的功能,在不一样的项目中有不一样的需求,在这里实现一个比价基本的上传图片插件,主要能实现图片的浏览,剪裁,上传这三个功能,同时也是为了让本身对图片/文件上传和HTML5中名声在外的canvas相关可以有一些了解html

上传到了github上,以为好的给星哦!l-imgupload //181119前端

我就要自行车 - 需求整理

放眼WWW,通常的图片上传模块,主要就是实现了三个功能,图片的预览,图片的剪裁及预览,图片的上传,那我也就整这么一个吧,再细化一下需求java

图片的预览

用户使用:用户点击“选择图片”,弹出文件浏览器,能够选择本地的图片,点击确认后,所选图片会按照原始比例出如今页面的浏览区域中
组件调用:开发者能够本身定义图片预览区域的大小,并限定所传图片的文件大小和尺寸大小git

图片的剪裁

用户使用:用户根据提示,在预览区域的图片上拖动鼠标框出想要上传的图片区域,而且能在结果预览区域看到本身的剪裁结果
组件调用:开发者能够自定义是否剪裁图片,并能够定义是否限定剪裁图片的大小及比例,而且设定具体大小及比例程序员

图片的上传

用户使用:用户点击“图片上传”,图片开始上传,现实“上传中…”,完成后显示“上传完成”
组件调用:开发者获得base64格式的urlData图片,本身编写调用Ajax的函数及其回调函数github

扔出原型图

做为设计师,扔图是个人最爱,画了一套全功能,包含剪裁及剪裁浏览的原型图web

state-1:初始状态
插件原型-1ajax

state-2:点击"选择图片",浏览本地后载入图片
插件原型-2canvas

state-3:剪裁,在图片区域上拖动鼠标选择要剪裁的部分,确认要上传的部分
插件原型-3

一次历史性的对话 - 本地图片读取

自打干上web开发这活,就都是在捣鼓浏览器内部这点事,从没想过跟浏览器以外计算机本地的一些文件能发生什么关系。可是该来的总要来,既然要上传图片,就确定要从计算机本地来选择文件并在浏览器内打开,这历史性的对话就要这么开启了…

图片的选择

其实在HTML中的<input>标签就提供了浏览本地文件的功能,前提是type="file",真是很讲道理… 试过就知道一点击就会打开文件浏览器

<input id="inputArea" type="file" />

但这么作有两个经典的问题:
第一,会有一个输入框傻乎乎的在那里…
第二,我用的是Ajax,怎么才能get到表单当中的文件呢

对于问题一,很好解决直接各类方式hide这个input标签便可,再主动触发click()

var imgFrom = document.getElementById("inputArea");
function loadImg(){
    imgFrom.click(); 
}

对于问题二,这就要介绍一下FormData对象了

XMLHttpRequest Level 2添加了一个新的接口FormData.利用FormData对象,咱们能够经过JavaScript用一些键值对来模拟一系列表单控件,咱们还可使用XMLHttpRequest的send()方法来异步的提交这个"表单". 比起普通的ajax,使用FormData的最大优势就是咱们能够异步上传一个二进制文件.
摘自MDN Web docs - Web技术文档/Web API 接口/FormData

正如上面的文档所说FormData对象能够干的事无非就是用javascript模拟表单控件,也正由于如此因此能够在模拟的表单中放入一个文件

var myFrom = new FormData();
var imageData = imgFrom.files[0];//获取表单中第一个文件
myFrom.append("image",imageDate);//向表单中添加一个键值对
console.log(myFrom.getAll("image"));//获取表单中image字段对应的值,结果见下图

获取本地文件结果
正如咱们所见,文件咱们已经经过Web拿到手了

图片的展示

既然是要上传图片,咱们确定得知道本身传的是啥图片啊,因此下一步就是如何把读取的图片展示在页面上了,正如上图中的显示,个人获得的图片是一个File对象,而File对象是特殊的Blob对象,那Blob对象又是个啥呢…

Blob 对象表示不可变的相似文件对象的原始数据。Blob表示不必定是JavaScript原生形式的数据。File 接口基于Blob,继承了 blob的功能并将其扩展使其支持用户系统上的文件。
摘自MDN Web docs - Web技术文档/Web API 接口/Blob

说实话,真是懵逼
但仔细理解下大概意思就是Blob对象是用来表示/承载文件对象的原始数据(二进制)的,借助一些博文会有助于理解
js中关于Blob对象的介绍与使用 - 可乐Script
HTML5 Blob对象 - zdy0_2004
说到底,重点不在这,了解一下有个概念便可,重点在于咱们怎么展现这个File对象

这就要请出FileReader对象了

FileReader 对象容许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
摘自MDN Web docs - Web技术文档/Web API 接口/FileReader

不难看出,FileReader对象就是用来读取本地文件的,而这其方法readAsDataURL()就是咱们要用的东西啦

该方法会读取指定的 Blob 或 File 对象。读取操做完成的时候,readyState 会变成已完成(DONE),并触发 loadend 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。
摘自MDN Web docs - Web技术文档/Web API 接口/FileReader/FileReader.readAsDataURL()

这里面又提到一个新名词data:URL,也就是说readAsDataURL()的做用就是能把文件转换为data:URL,不过这个data:URL又是什么呢,执行来看看

var reader = new FileReader(); //调用FileReader对象
reader.readAsDataURL(imgData); //经过DataURL的方式返回图像
reader.onload = function(e) {                
    console.log(e.target.result);//看看你是个啥
}

控制台的结果全脸懵逼
Data:URL
能够经过这篇文章去大概了解一下DATA URL简介及DATA URL的利弊 - 薛陈磊

说到底这dataURL我就粗略的理解它为URL形式的data,也就是说这段URL并非与普通的URL同样指向某个地址,而是它自己就是数据,咱们试着把这一堆字符粘到一个<img>src属性中
Data:URL结果

终于看到了,结果正如所料,将这段包含了数据的URL赋给一个<img>确实可让数据被展示为图片
至此,咱们实现了本地文件的读取展示

指哪儿截哪儿 - 利用canvas的图片截取

舒适提示-乱入:看明白这里须要对canvas有基本的了解MDN Web docs - Web技术文档/Web API接口/Canvas/Canvas教程

在Web上对图像进行操做,没有比canvas相关技术更合适的了,因此本文用canvas技术来实现对图片的截取

canvas中的图片展示

在上文中,咱们利用<img>展示出了咱们选择的图片,可是咱们的图片截取功能但是要利用<canvas>来实现的,因此怎么在<canvas>中展示咱们刚才获取的图片就是下一步要干的事情了

canvas的API中自带drawImage()函数,其做用就是在<canvas>中渲染一张图片出来,其能够支持多种图片来源见MDN Web docs - Web技术文档/Web API接口/CanvasRenderingContext2D/CanvasRenderingContext2D.drawImage()

最简单的,咱们直接把刚刚显示图片的那个<img>传入是否是就能够呢

var theCanvas = document.getElementById("imgCanvas");
var canvasImg = theCanvas.getContext("2d");//获取2D渲染背景
var img = document.getElementById("image");
img.onload = function(){//确认图片已载入    
    canvasImg.drawImage(img,0,0);
}

结果以下
CanvasImg结果-1
从图中看,左侧是以前的'<img>',右侧是渲染了图片信息的<canvas>
这么看来虽然成功?在<canvas>中渲染出了图片可是有两个明显的问题

1.左边的'<img>'留着干啥?
2.右边看上去是否是有点不同?

这俩问题其实都好办,针对第一个问题,咱们其实能够根本不用实体的'<img>'直接利用'Image'对象便可,第二个问题明显是由于<canvas>的大小与获取到的图片大小不一致所产生的,综合这两点,对代码进行进化!

var theCanvas = document.getElementById("imgCanvas");
var canvasImg = theCanvas.getContext("2d");
var img = new Image();//建立img对象

reader.onload = function(e) {                
    img.src = e.target.result;
}
 
img.onload = function(){
    theCanvas.Width = img.width;//将img对象的长款赋给canvas标签
    theCanvas.height = img.height;    
    canvasImg.drawImage(img,0,0);
}

CanvasImg结果-2

结果与咱们所期待的同样,至此咱们成功的在'<canvas>'中展示了从本地获取的图片

canvas中图片的截取

其实截图,说白了就是在一个图像上,获取某个区域中的图像信息
canvas做为专门用来处理图像及像素相关的一套API,获取区域中的相关图像信息能够说是再简单不过的事情,利用getImageData()函数便可 //详情,固然咱们不光要把图像信息获取到,最好还能展示出来咱们的截图结果,这里就要用到与之相对的putImageData()函数 //详情

var resultCanvas = document.getElementById("resultCanvas");
var resultImg = resultCanvas.getContext("2d");
var cutData = canvasImg.getImageData(100,100,200,200);
resultImg.putImageData(cutData,0,0);

结果如图
截图结果

我也要画一个圈/框

既然这个工具是面向用户的,截图的过程确定是要所见即所得的,在函数getImageData()中有4个参数,分别是截图起点的两个坐标和区域的宽度及高度,因此问题就变成了如何更合理的让用户输入这4个值。
其实现存的主流解决方案就作的很是好了:在图上拖动鼠标,拉出一个框,这个框内就是用户但愿截取的区域。

在画布上画出一个框很简单,只需用到strokeRect()函数 //详情
可是让用户本身拖出一个框就比较复杂了,先分析一下用户的一套动做都有什么

  1. 用户选定起始点,点下鼠标左键
  2. 用户选定截图区域的大小,保持鼠标左键不抬起,同时移动鼠标选择
  3. 用户完成选择,抬起鼠标左键

回过头再来看程序须要干什么

  1. 获取起始点的坐标,并记录为已点击状态
  2. 判断一下若是为已点击状态那么,获取每一次移动/帧的鼠标坐标,并计算出与起始点之间的横纵坐标距离,而这距离就是所画框的长度和宽度,清除上一帧的整个画面,再绘制一个新的图片再画一个新的框,同时按照框的起始坐标及宽高,截取图像信息,再清除预览区域的上一帧的画布,再将这一帧的图像信息载入
  3. 鼠标抬起后,中止记录及绘制,保持最终一帧的框停留在画面上

在这里,要说明一下,为何非要清除整个画面不可,其实能够把经过canvas.getContext("2d")获取到的2D 画布的渲染上下文 //详情 就看成一块画布,已经渲染出来的东西就已经留在了上面,没法再修改,若是想要更改画面上已经存在的元素的大小位置形状等等属性,那么在程序层面,就只能(我的理解,不必定对,若是有问题请必定跟我唠唠)把以前的画布清空再从新渲染。

这个思路与咱们以前端开发中动画相关的开发思路不一样,并非像以前那样直接操做现有元素属性就能够改变该元素在画面上的呈现结果的,而在这里其实更像是在现实生活中的动画制做原理就是

每一帧都须要从新绘制整张画面

而其实这是任何动画渲染方式的最底层思路与行为

话说回来按照上文相关的开发思路,实现这个功能的代码以下

var flag = false;//记录是否为点击状态的标记
var W = img.width;
var H = img.height;
var startX = 0;
var startY = 0;

//当鼠标被按下
theCanvas.addEventListener("mousedown", e => {
    flag = true;//改变标记状态,置为点击状态
    //startX = e.clientX;//得到起始点横坐标
    //startY = e.clientY;//得到起始点纵坐标
    //添加于2018.3.6:
    //这里有些问题,在本文的条件下e.clientX是对的,但是实际上是应该为相对对象的坐标而不是浏览器,因此应该为e.offsetX 感谢 @高远 同窗提醒
    startX = e.offsetX;//得到起始点横坐标
    startY = e.offsetY;//得到起始点纵坐标
})

//当鼠标在移动
theCanvas.addEventListener("mousemove", e => {
    if(flag){//判断鼠标是否被拖动
        canvasImg.clearRect(0,0,W,H);//清空整个画面
        canvasImg.drawImage(img,0,0);//从新绘制图片
        canvasImg.strokeRect(startX, startY, e.clientX - startX, e.clientY - startY);//绘制黑框
        resultImg.clearRect(0,0,cutData.width,cutData.height);//清空预览区域
        cutData = canvasImg.getImageData(startX, startY, e.clientX - startX, e.clientY - startY);//截取黑框区域图片信息
        resultImg.putImageData(cutData,0,0);//将图片信息赋给预览区域
    }
})

//当鼠标左键抬起
theCanvas.addEventListener("mouseup", e => {
    flag = false;//将标志置为已抬起状态
})

结果如图
画框截图

能不能弄的高大上一点啊

主要吧,这个黑框太丑了,透露着一种原始和狂野,以及来自工科男审美的粗糙感…
能不能弄的好看点,起码让它看上去是一个工具不是一个实验

个人想法是这样的,待被截取的图片上应该蒙上一层半透明白色遮罩,用户框选出的部分是没有遮罩的,这样效果能够为功能增长视觉上的材质感及温馨感,同时显得高端

具体效果是这样的-下图来自ps
蒙板效果图

是否是稍微好些了

但是,怎么实现?
简单来讲,就是在原有的画布上再蒙半透明的一层画布,而后让这一层有一部分是没有的就能够实现了,总的来讲就是蒙版和遮罩的思路,在canvas中也有相关的api,可是我愣是没看明白
负责任的贴一个连接

不过开发就是这样,条条大路出bug
我想到这个功能的瞬间脑子像抽了同样,出现了这么一种实现方法

见下图
图片描述
mask层能够分为A,B,C,D四个矩形区域,在图中两个蓝色的点是已知的(用户本身画出来的),在下层图片大小已知的前提下,这四个矩形区域的四个点都是能够计算出来的,从而其高度和宽度也能够计算出来,这样就能够利用这些数据画出一个半透明的矩形,将四个半透明矩形都画出来后,就可以实现以前设计出的效果了,具体代码以下

theCanvas.addEventListener("mousemove", e => {
    if(flag){
        canvasImg.clearRect(0,0,W,H);
        resultImg.clearRect(0,0,cutData.width,cutData.height);
        canvasImg.drawImage(img,0,0);
        canvasImg.fillStyle = 'rgba(255,255,255,0.6)';//设定为半透明的白色
        canvasImg.fillRect(0, 0, e.clientX, startY);//矩形A
        canvasImg.fillRect(e.clientX, 0, W, e.clientY);//矩形B
canvasImg.fillRect(startX, e.clientY, W-startX, H-e.clientY);//矩形C
        canvasImg.fillRect(0, startY, startX, H-startY);//矩形D
        cutData = canvasImg.getImageData(startX, startY, e.clientX - startX, e.clientY - startY);
        resultImg.putImageData(cutData,0,0);
    }
})

效果如图
图片描述

没有什么把本身的脑残想法实现更爽的了

至此,截图的基本功能都实现了,但还差最后一步

另外一次历史性的对话 - 图片上传

图片已经截出来了,下一步就是怎么上传了,经过Ajax上传,须要将图像数据转化为File,而在canvas的API中自带toBlob()函数 //详情

var resultFile = {}
theCanvas.addEventListener("mouseup", e => {
    resultCanvas.toBlob(blob => {
            resultFile = blob;
            console.log(blob);//Blob(1797) {size: 1797, type: "image/png"}
        }
    })
    flag = false;
})

而后就能够用Ajax上传拉,具体怎么上传就须要具体问题具体分析了

至此,整个插件的思路及须要用到相关技术都捋清楚了,接下来就能够开始按照上文的需求进行开发了,而这是下一篇文章要讲的事情了

能看到这的绝对很闲
这篇文章的长度让我想起读研时被毕业论文统治的恐惧
原本想着连同组件开发一块儿在一篇内写完呢,可是实在太长仍是放弃了
身体和家人都是最重要的,今年还没过一个月就被上了不少课

181119修改

前端时间写完了实践是检验程序员的惟一标准02:用户不想跟你说话并向你扔出一张图片 - 图片上传组件开发【开发篇】,逐行代码说明的- -

增长了选框拖拽功能,而且上传到了github上,以为好的给星哦!l-imgupload

相关文章
相关标签/搜索