CanvasRenderingContext2D.drawimage(el, …) canvas的APIcontext.drawimage(el, ...)
,接口详情可以参考MDN ,这个接口的作用就是在canvas中画出el元素,el可以是video,image,canvas元素,后面的参数就是一些位置宽高信息,从el元素的什么位置截取多少宽高,得到一个图像,在把这个绘制到当前canvas的什么位置宽高。
实现思路 核心就是context.drawimage(el, ...)
,构造Image元素,然后再取得位置信息即可获取截取图片中一小块绘制到canvas上,然后canvas可以设置大小为截取图大小,然后再使用canvas.toDataURL()
导出canvas图形即可。
截取的位置和宽高 这里的难点在于,如何获取截取的位置和宽高?
我们思考把原图Image设想为div矩形parent,截取框设想为div矩形child;如果child的相对parent定位,那么child的top就是所求Top,child的left就是所求的Left,child的宽度width就是所求的Width,child的高度height就是所求的Height。
所以我们构造父子元素,父元素设置大小为原图大小,子元素为可移动,缩放拉伸的元素;截取的图片就是当前子元素的top,left,width,height;
截取框的移动和伸缩 上一节了解,截取框其实就是“child元素“而已。那么现在剩下的问题就是这个子元素的移动伸缩了,因为截取框肯定是大小可变,位置可移的。
移动
可以利用mousedown、mousemove、mouseup事件来进行实现。
截取框元素中mousedown时,记录状态isMouseDown为true和位置X/Y(由event.pageX/Y取得)
截取元素中出啊发mouseup时,记录状态为isMouseDown为false
在截取框元素中触发mousemove时,检测状态isMouseDown,若为true说明鼠标点击后移动没放起,即正在拖动元素,那么就进行4;若为false则跳过
当前位置(event.pageX/Y)和上一次位置X/Y可以算出位置移动的left和top
left = event.pageX - X
top = event.pageY - Y
有这个移动信息 可以直接赋值给截取框css的left和top实现移动
伸缩
同理,也是三个鼠标事件的监听,得到鼠标移动的位置信息。移动的时赋值给css的left,top实现移动,那伸缩的就是赋值这些位置信息给css的width和height实现元素的伸缩。
不过按照常规的操作习惯,伸缩最好是在一个顶点拉伸,所以截取框中还要做一个小元素作为拉伸点,如何再实现三个事件即可
具体实现 工具函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function readFile (file ) { return new Promise ((resolve, reject ) => { let reader = new FileReader() reader.readAsDataURL(file) reader.onload = function (data ) { console .log('FileReader' , data) resolve(reader.result) } reader.onerror = (err ) => reject(err) }) } function createImgEl (fileData ) { return new Promise ((resolve, reject ) => { const img = new Image() img.src = fileData img.onload = function (e ) { console .log(e) resolve(img) } img.onerror = (err ) => reject(err) }) } function creatEL (tag, children ) { let el = document .createElement(tag); if (children && Array .isArray(children)) { children.forEach((v ) => el.appendChild(v)); } else if (children && typeof children == "object" ) { el.appendChild(children); } else if (children) { el.innerHTML = children; } return el; } function throttle (delay, fn ) { let cando = true return function (...args ) { if (cando) { fn.apply(this , args) cando = false setTimeout (() => { cando = true }, delay); } } }
构造”父元素“,原图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class CropComponent { constructor (file, options ) { if (!options.el) { throw new Error ('there must have a el as mounted point' ) } this .file = file this .el = options.el } async build ( ) { const sourceFileData = await readFile(this .file) const imgEl = await createImgEl(sourceFileData) const naturalWidth = imgEl.naturalWidth const naturalHeight = imgEl.naturalHeight this .el.style.position = `relative` this .el.style.width = `${naturalWidth} px` this .el.style.height = `${naturalHeight} px` this .el.style.background = `url(${sourceFileData} ) no-repeat` this .el.style.backgroundSize = '100% 100%' } }
创建“子元素”,裁剪框 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 createCropBox ( ) { this .cutFieldEl = document .createElement('div' ) this .cutFieldEl.classList.add('cropbox' ) this .moveFielEl = document .createElement('div' ) this .moveFielEl.classList.add('moveField' ) this .el.appendChild(this .cutFieldEl) this .cutFieldEl.appendChild(this .moveFielEl) }
裁剪框移动实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 class CropComponent { constructor (file, options ) { if (!options.el) { throw new Error ('there must have a el as mounted point' ) } this .file = file this .el = options.el this .containerEl = options.containerEl this .cutFieldEl = null this .moveFielEl = null this .mouseCtx = null this .move = this ._move.bind(this ) } async build ( ) { this .createCropBox() } createCropBox ( ) { this .cutFieldEl = document .createElement('div' ) this .cutFieldEl.classList.add('cropbox' ) this .moveFielEl = document .createElement('div' ) this .moveFielEl.classList.add('moveField' ) this .mouseCtx = { isMouseDown: false , x: 0 , y: 0 , } this .cutFieldEl.addEventListener( 'mousedown' , (e) => { this .mouseCtx = { isMouseDown: true , x: e.pageX, y: e.pageY, } }, false ) this .cutFieldEl.addEventListener( 'mousemove' , throttle(5 , (e ) => { if (this .mouseCtx.isMouseDown) { this .move(e) } if (this .moveMouseCtx.isMouseDown) { this .expand(e) } }), false ) this .cutFieldEl.addEventListener( 'mouseup' , (e) => { this .mouseCtx.isMouseDown = false }, false ) this .el.appendChild(this .cutFieldEl) this .cutFieldEl.appendChild(this .moveFielEl) } _move (e ) { const offsetX = e.pageX - this .mouseCtx.x const offsetY = e.pageY - this .mouseCtx.y const cutFieldStyle = window .getComputedStyle(this .cutFieldEl) const workspaceStyle = window .getComputedStyle(this .el) let left = cutFieldStyle.left.slice(0 , -2 ) - 0 + offsetX let top = cutFieldStyle.top.slice(0 , -2 ) - 0 + offsetY const maxLeft = workspaceStyle.width.slice(0 , -2 ) - cutFieldStyle.width.slice(0 , -2 ) const maxTop = workspaceStyle.height.slice(0 , -2 ) - cutFieldStyle.height.slice(0 , -2 ) left = left < 0 ? 0 : left left = left > maxLeft ? maxLeft : left top = top < 0 ? 0 : top top = top > maxTop ? maxTop : top this .cutFieldEl.style.left = left + 'px' this .cutFieldEl.style.top = top + 'px' this .mouseCtx.x += offsetX this .mouseCtx.y += offsetY } }
裁剪框拉伸实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 class CropComponent { constructor (file, options ) { this .expand = this ._expand.bind(this ) } async build ( ) { } createCropBox ( ) { this .moveMouseCtx = { isMouseDown: false , x: 0 , y: 0 , } this .moveFielEl.addEventListener('mousedown' , (e ) => { e.stopPropagation() this .moveMouseCtx = { isMouseDown: true , x: e.pageX, y: e.pageY, } }) this .moveFielEl.addEventListener( 'mousemove' , throttle(5 , (e ) => { e.stopPropagation() if (this .moveMouseCtx.isMouseDown) { this .expand(e) } }) ) this .moveFielEl.addEventListener('mouseup' , (e ) => { e.stopPropagation() this .moveMouseCtx.isMouseDown = false }) if (this .containerEl) { this .handleContainer() } this .el.appendChild(this .cutFieldEl) this .cutFieldEl.appendChild(this .moveFielEl) } _move (e ) { } _expand (e ) { const offsetX = e.pageX - this .moveMouseCtx.x const offsetY = e.pageY - this .moveMouseCtx.y const cutFieldStyle = window .getComputedStyle(this .cutFieldEl) const workspaceStyle = window .getComputedStyle(this .el) let width = cutFieldStyle.width.slice(0 , -2 ) - 0 let height = cutFieldStyle.height.slice(0 , -2 ) - 0 width = width + offsetX height = height + offsetY const maxWidth = workspaceStyle.width.slice(0 , -2 ) - cutFieldStyle.left.slice(0 , -2 ) const maxHeight = workspaceStyle.height.slice(0 , -2 ) - cutFieldStyle.top.slice(0 , -2 ) width = width < 0 ? 0 : width width = width > maxWidth ? maxWidth : width height = height < 0 ? 0 : height height = height > maxHeight ? maxHeight : height this .cutFieldEl.style.width = width + 'px' this .cutFieldEl.style.height = height + 'px' this .moveMouseCtx.x += offsetX this .moveMouseCtx.y += offsetY } }
获取位置宽高信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class CropComponent { constructor (file, options ) { } async build ( ) { } createCropBox ( ) { } _move (e ) { } _expand (e ) { } getPositionMsg ( ) { const cutFieldStyle = window .getComputedStyle(this .cutFieldEl) return { top: cutFieldStyle.top.slice(0 , -2 ) - 0 , left: cutFieldStyle.left.slice(0 , -2 ) - 0 , width: cutFieldStyle.width.slice(0 , -2 ) - 0 , height: cutFieldStyle.height.slice(0 , -2 ) - 0 , } } }
裁剪逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 async function crop (sourceFile, options ) { const sourceFileData = await readFile(sourceFile) const imgEl = await createImgEl(sourceFileData) const defaultOptions = { top: 0 , left: 0 , width: imgEl.naturalWidth, height: imgEl.naturalHeight, type: 'image/jpeg' , } const realOptions = { ...defaultOptions, ...options, } const resultCanvas = document .createElement('canvas' ) resultCanvas.width = realOptions.width resultCanvas.height = realOptions.height const resultCtx = resultCanvas.getContext('2d' ) resultCtx.drawImage( imgEl, realOptions.left, realOptions.top, realOptions.width, realOptions.height, 0 , 0 , realOptions.width, realOptions.height ) const code = resultCanvas.toDataURL(realOptions.type, 1 ) return code }
代码参考demo
系列推荐:
图片压缩
图片马赛克
前端抠图?
参考 https://juejin.cn/post/6860024132730519560#heading-5