ImageData对象
canvas的ctx.getImageData(left.top,width,height)
可以获取canvas图像内的特定位置宽高的像素信息,这些信息存放在一个ImageData对象里面。
ImageData内就是数组,根据特定公式获取对应位置的像素的R/G/B/Alpha的信息:
例如,要读取图片中位于第50行,第200列的像素的蓝色部份,你会写以下代码:
1 blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2];
根据行、列读取某像素点的R/G/B/A值的公式:
1 imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 0/1/2/3];
canvas的ctx.putImageData(imgData, left, top)
可以在特定位置写入像素信息。
这个几个API合起来其实就是修改图像的某个地方。相对于直接画,这种方式操作的粒度更细而已。
更详细查看MDN
原理 像素其实是一个四方形的色块,图片是由不同像素(色块)组成的,在一定大小下,像素越多代表的色块越多那么数据越多,图片也就越清晰;像素少,那么数据少,图片只有寥寥几个色块组成,每个色块就会大。因为大,色块的棱角就会明显,图形也会因此出现一些“锯齿”(可以由此理解抗锯齿),失真。
马赛克可以是失真,马赛克就是某个区域内的色块更大,相对其他地方,图形更模糊。那么想要某个区域打上马赛克,只需要模糊对应区域,即减少组成的色块数量。
如何减少色块数量?屏幕的像素数量是固定,即原始色块数量是固定的,这个无法改变。那么就用多个原始色块组成一个大的色块。具体到代码其实就利用ImageData修改某一个区域的像素为同一种颜色。
整体的实现思路:
获取图片,使用canvasContext.drawImage
,canvas元素图片预览
canvas监听mousedown、mousemove、mouseup三个事件,mousedown表示鼠标点击下,准备开始打马赛克记录状态isDraw = true
;mouseup表示鼠标松开,结束打马赛克,记录状态isDraw = false
;mousemove表示图像种鼠标在移动,通过isDraw判断是否在打马赛克;若是执行3,若不是不做操作。
马赛克逻辑:通过mousemove的event参数可以获取触发事件元素的位置偏移值、
1 2 left = event.offsetX top = event.offsetY
这个位置即当前需要打马赛克的位置。马赛克的宽高可以事先配置
canvas的ctx.getImageData(left.top,width,height)
获取像素信息
获取像素数据的区域,划分比像素更大若干的色块(大的色块),每个色块设置成同一个颜色,这个颜色在色块自身区域随机选取。达成“失真模糊”效果
把这块像素数据写回canvas原来位置
具体实现 一些工具函数 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 function readFile (file ) { return new Promise ((resolve, reject ) => { let reader = new FileReader() reader.readAsDataURL(file) reader.onload = function (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 ) { resolve(img) } img.onerror = (err ) => reject(err) }) } function getXY (obj, x, y ) { const w = obj.width; let color = []; color[0 ] = obj.data[4 * (y * w + x)]; color[1 ] = obj.data[4 * (y * w + x) + 1 ]; color[2 ] = obj.data[4 * (y * w + x) + 2 ]; color[3 ] = obj.data[4 * (y * w + x) + 3 ]; return color; } function setXY (obj, x, y, color ) { var w = obj.width; obj.data[4 * (y * w + x)] = color[0 ]; obj.data[4 * (y * w + x) + 1 ] = color[1 ]; obj.data[4 * (y * w + x) + 2 ] = color[2 ]; obj.data[4 * (y * w + x) + 3 ] = color[3 ]; }
创建canvas 首先应该实现创建canvas,因为马赛克是在canvas上绘制的
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 class MasaicComponent { constructor (options ) { if (!options.file) { throw new Error ('musted have file in the options as source picture' ) } if (!options.el) { throw new Error ('musted have el in the options as place to work ' ) } this .file = options.file this .el = options.el this .canvas = null this .init() } async init ( ) { this .canvas = await this .createCanvas() this .el.innerHTML = '' this .el.appendChild(this .canvas) } async createCanvas ( ) { const fileData = await readFile(this .file) const imgEl = await createImgEl(fileData) const canvas = document .createElement('canvas' ) const ctx = canvas.getContext('2d' ) canvas.position = 'relative' const containerStyle = window .getComputedStyle(this .el) const maxWidth = containerStyle.width.slice(0 , -2 ) const maxHeight = containerStyle.height.slice(0 , -2 ) let width = imgEl.naturalWidth let height = imgEl.naturalHeight if (width > maxWidth) { width = maxWidth height = Math .floor((width / maxWidth) * maxHeight) } else if (height > maxHeight) { width = Math .floor((height * maxWidth) / maxHeight) height = maxHeight } canvas.width = width canvas.height = height ctx.drawImage(imgEl, 0 , 0 , width, height) return canvas } }
画笔ui相关 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 class MasaicComponent { constructor (options ) { if (!options.file) { throw new Error ('musted have file in the options as source picture' ) } if (!options.el) { throw new Error ('musted have el in the options as place to work ' ) } this .file = options.file this .el = options.el this .canvas = null this .init() } async init ( ) { this .canvas = await this .createCanvas() this .el.innerHTML = '' this .el.appendChild(this .canvas) this .panEl = document .createElement('div' ) this .panEl.style.width = this .pan.radius + 'px' this .panEl.style.height = this .pan.radius + 'px' this .panEl.style.borderRadius = '50%' this .panEl.style.position = 'absolute' this .panEl.style.backgroundColor = 'yellow' this .panEl.style.transform = 'translate(-50%,-50%)' this .panEl.style.pointerEvents = 'none' this .panEl.style.display = 'none' this .panEl.style.opacity = 0.5 this .panOffest = { left: this .canvas.offsetLeft, top: this .canvas.offsetTop, } this .el.appendChild(this .panEl) this .drawPan = this ._drawPan.bind(this ) } async createCanvas ( ) { } setPan (options ) { this .pan = { ...this.pan, ...options, } this .panEl.style.width = this .pan.radius + 'px' this .panEl.style.height = this .pan.radius + 'px' } _drawPan ( ) { this .panEl.style.left = `${this .panOffest.left + this .pan.x} px` this .panEl.style.top = `${this .panOffest.top + this .pan.y} px` this .raf = window .requestAnimationFrame(this .drawPan) } }
绑定事件 点击移动时获取绘制马赛克的位置数据和触发绘制马赛克
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 class MasaicComponent { constructor (options ) { } async init ( ) { this .bindEvent() } async createCanvas ( ) { } bindEvent ( ) { let isDraw = false this .canvas.addEventListener('mousedown' , (e ) => { isDraw = true }) this .canvas.addEventListener('mousemove' , (e ) => { this .pan.x = e.offsetX this .pan.y = e.offsetY if (isDraw) { this .drawMasaic() } }) this .canvas.addEventListener('mouseup' , (e ) => { isDraw = false }) this .raf = null this .canvas.addEventListener('mouseover' , (e ) => { this .panEl.style.display = 'block' this .raf = window .requestAnimationFrame(this .drawPan) }) this .canvas.addEventListener('mouseout' , (e ) => { this .panEl.style.display = 'none' window .cancelAnimationFrame(this .raf) }) } setPan (options ) { } _drawPan ( ) { } }
马赛克绘制的逻辑 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 class MasaicComponent { constructor (options ) { } async init ( ) { } async createCanvas ( ) { } bindEvent ( ) { } drawMasaic ( ) { const ctx = this .canvas.getContext('2d' ) let left = this .pan.x - Math .floor(this .pan.radius / 2 ) left = left == 0 ? 0 : left let top = this .pan.y - Math .floor(this .pan.radius / 2 ) top = top == 0 ? 0 : top const imgData = ctx.getImageData( left, top, this .pan.radius, this .pan.radius ) const w = imgData.width const h = imgData.height const num = 2 const sw = Math .floor(w / num) const sh = Math .floor(h / num) for (let i = 0 ; i < sh; i++) { for (let j = 0 ; j < sw; j++) { let rw = Math .floor(Math .random() * num) let rh = Math .floor(Math .random() * num) const color = getXY(imgData, j * num + rw, i * num + rh) for (let ii = 0 ; ii < num; ii++) { for (let jj = 0 ; jj < num; jj++) { setXY(imgData, j * num + jj, i * num + ii, color) } } } } ctx.putImageData(imgData, left, top) } setPan (options ) { } _drawPan ( ) { } }
详细代码 查看demo
系列推荐:
图片压缩
图片裁剪
前端抠图?
参考 canva实践小实例 —— 马赛克效果
ImageData——MDN