最近突然想做一些好玩的东西,找来找去,想到了之前曾经在网上看到过有人用box-shadow画了一副蒙娜丽莎出来
感觉这个挺有意思,正好趁着周末,自己也搞一波

前言

在线地址:

优化前的版本
优化后的版本
源码仓库地址

不建议上传大图片。。喜欢听电脑引擎声的除外


首先,并不打算单纯的实现某一张图片(这样太没意思了),而是通过上传图片,来动态生成box-shadow的数据。
所以,你需要了解这些东西:

  1. box-shadow
  2. canvas

box-shadow

box-shadow可以让我们针对任意一个html标签生成阴影,我们可以控制阴影的偏移量、模糊半径、实际半径、颜色等一系列属性。
语法如下:

selector {
/* offset-x | offset-y | color */
box-shadow: 60px -16px teal; /* offset-x | offset-y | blur-radius | color */
box-shadow: 10px 5px 5px black; /* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2); /* inset | offset-x | offset-y | color */
box-shadow: inset 5em 1em gold; /* Any number of shadows, separated by commas */
box-shadow: 3px 3px red, -1em 0 0.4em olive;
}

这里是MDN的box-shadow描述,里边有一些示例。

canvas

是的,我们还需要canvas,因为我们需要将图片资源转存到canvas中,再生成我们实际需要的数据格式。
在这里并不会拿canvas去做渲染之类的,单纯的是要利用canvas的某些API。

首版规划

刚开始的规划大致是这样的:

  1. 我们上传一张图片
  2. 创建一个Image对象接收上传的图片资源
  3. Image对象放入canvas
  4. 通过canvas生成图片文件对应的rgba数据
  5. 处理rgba数据转换为box-shadow属性
  6. done

如何接收图片文件数据

我们在监听input[type="file"]change事件时,可以在target里边拿到一个files的对象。
该对象为本次上传传入的文件列表集合,一般来说我们取第一个元素就是了。
我们拿到了一个File类型的对象,接下来就是用Image来接收这个File对象了。

这里会用到一个浏览器提供的全局对象URLURL提供了一个createObjectURL的方法。
方法接收一个Blob类型的参数,而File则是继承自Blog,所以我们直接传入就可以了。
然后再使用一个Image对象进行接收就可以了:

$input.addEventListener('change', ({target: {files: [file]}}) => {
let $img = new Image() $img.addEventListener('load', _ => {
console.log('we got this image')
}) $img.src = URL.createObjectURL(file)
})

MDN关于URL.createObjectURL的介绍

通过canvas获取我们想要的数据

canvas可以直接渲染图片到画布中,可以是一个Image对象、HTMLImageElement及更多媒体相关的标签对象。
所以我们上边会把数据暂存到一个Image对象中去。
我们在调用drawImage时需要传入xywidthheight四个参数,前两个必然是0了,关于后边两个属性,正好当我们的Image对象加载完成后,直接读取它的widthheight就是真实的数据:

let context = $canvas.getContext('2d')
$img.addEventListener('load', _ => {
context.drawImage($img, 0, 0, $img.width, $img.height)
})

当我们把图片渲染至canvas后,我们可以调用另一个API获取rgba相关的数据。

getImageData

我们调用getImageData会返回如下几个参数:

  1. data
  2. width
  3. height

data为一个数组,每相邻的四个元素为一个像素点的rgba描述。
一个类似这样结构的数组:[r, g, b, a, r, g, b, a]

MDN关于context.drawImage的介绍
MDN关于context.getImageData的介绍

处理rgba数据并转换为box-shadow

在上边我们拿到了一个一维数组,接下来就是将它处理为更合理的结构。
P.S. 一维数组是从左到右从上到下排列的,而不是从上到下从左到右

我们可以发现,widthheight相乘正好是data数组的length
而数组的顺序则是先按照x轴进行增加的,所以我们这样处理得到的数据:

function getRGBA (pixels) {
let results = []
let {width, height, data} = pixels
for (let i = 0; i < data.length / 4; i++) {
results.push({
x: i % width | 0,
y: i / width | 0,
r: data[i * 4],
g: data[i * 4 + 1],
b: data[i * 4 + 2],
a: data[i * 4 + 3]
})
} return results
}

我们将length除以4作为循环的最大长度,然后在生成每个像素点的描述时
通过当前下标对图片宽度取余得到当前像素点在图片中的x轴下标
通过当前下标对图片宽度取商得到当前像素点在图片中的y轴下标
同时塞入rgba四个值,这样我们就会拿到一个类似这样结构的数据:

[{
x: 0,
y: 0,
r: 255,
g: 255,
b: 255,
a: 255
}]

将数据生成为box-shadow格式的数据

box-shadow是支持多组属性的,两组属性之间使用,进行分割。
所以,我们拿到上边的数据以后,直接遍历拼接字符串就可以生成我们想要的结果:

let boxShadow = results.map(item =>
`${item.x}px ${item.y}px rgba(${item.r}, ${item.g}, ${item.b}, ${item.a})`
).join(',')

效果图:

虽说这样就做出来了,但是对浏览器来说太不友好了。因为是每一个像素点对应的一个box-shadow属性。
好奇的童鞋可以选择F12检查元素查看该div(反正苹果本是扛不住)
所以为了我们能够正常使用F12,我们下一步的操作就是合并相邻同色值的box-shadow,减少box-shadow属性值的数量。

合并相邻的单元格

虽说图片可能是由各种颜色不规则的组合而成,但毕竟还是会有很多是重复颜色的。
所以我们要计算出某一种颜色可合并的最大面积。
针对某一种颜色,用表格表示可能是这样的:

就像在图中所示,我们最理想的合并方式应该是这样的 (radius的取值意味着我们只能设置一个正方形)

于是。。如果计算出来这一块面积就成为了一个问题-.-

目前的思路是,将数组转换为二维数组,而不是单纯的在对象中用xy标识。
所以,我们对处理数组的函数进行如下修改:

function getRGBA (pixels) {
let results = []
let {width, height, data} = pixels
for (let i = 0; i < data.length / 4; i++) {
let x = i % width | 0
let y = i / width | 0
let row = results[y] = results[y] || []
row[x] = {
rgba: `${data.slice(i * 4, i * 4 + 4)}` // 为了方便后续的对比相同颜色,直接返回一个字符串
}
} return results
}

这时我们就能得到一个按照xy排列的二维数组,下一步的操作就是以任意点为原点,进行匹配周围的cell
参考上边的表格示例,我们会拿到一个类似这样的数据 (仅作示例)

[
[1, 1, 1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1],
[1, 1, 1, 1, 1, 1],
]

获取可合并的最大半径

目前采用的是递归的方式,从0,0原点处开始搜索,获取当前原点的色值,然后与周围进行比较,获取一个最大半径的正方形:

/**
* 根据给定范围获取匹配当前节点的正方形
* @param {Array} matrix 二维矩阵数组
* @param {Object} tag 当前要匹配的节点
* @param {Number} [startRowIndex=0] 开始的行下标,默认为1
* @param {Number} [startColIndex=0] 开始的列下标,默认为1
* @return {Number} 返回一个最小范围
*/
function range (matrix, tag, startRowIndex = 0, startColIndex = 0) {
let results = []
rows:
for (let rowIndex = startRowIndex; rowIndex < matrix.length; rowIndex++) {
let row = matrix[rowIndex]
for (let colIndex = startColIndex; colIndex < row.length; colIndex++) {
let item = row[colIndex] if (item.rgba !== tag.rgba) {
if (colIndex === startColIndex) {
break rows
// 这个表示在某一行的第一列就匹配失败了,没有必要再进行后续的匹配,直接`break`到最外层
} else {
results.push(colIndex - startColIndex)
break
// 将当前下标放入集合,终止当前循环
}
} else if (colIndex === row.length - 1) {
results.push(colIndex - startColIndex)
// 这里表示一整行都可以与当前元素匹配
}
}
} // 对所有的x、y轴的值进行比较获取最小的值
let count = Math.min.apply(Math, [results.length].concat(results)) return count
}

函数会从起点开始按顺序遍历所有的元素,在遇到不匹配的节点后,就会break进入下次循环,并将当前的下标存入数组中。
在遍历完成后,我们将数组所有的item以及数组的长度(可以认为是y轴的值)一同放入Math.min获取一个最小的值。
这个最小的值就是我们以当前节点为原点时可以生成的最大范围的正方形了。
P.S. 这个计算方式并不是很好,还不够灵活

递归计算剩余面积

因为上边也只是合并了一个正方形,还会剩下很多面积没有被查看。
所以我们用递归的方式来计算剩余面积,在第一次匹配结束后,是大概这个样子的:

所以我们在递归处拆分出了两块会有重复数据的面积:

 

以及之后的递归也是参照这个样子来的,这样能保证所有的节点都会被照顾到,不会漏掉。(如果有更好的方式,求回复)。

这样配合着前边拿到的半径数据,很轻松的就可以组装出合并后的集合,下一步就是将其渲染到DOM中了。

渲染到box-shadow中

现在我们已经拿到了想要的数据,关于生成box-shadow属性处我们也要进行一些修改,之前因为是一个像素对应一个属性值,但是现在做了一些合并,所以,生成属性值的操作大概是这个样子的:

$output.style.boxShadow = results.map(item =>
`${item.x}px ${item.y}px 0px ${item.radius}px rgba(${item.target.rgba})`
).join(',')

P.S. xy的值必须要加上半径的值,否则会出现错位,因为box-shadow是从中心开始渲染的,而不是左上角

完成后的效果对比

原图&两种实现方式的效果对比:

我们拿合并前后生成的CSS存为了文件,并查看了文件大小,效果在一些背景不是太复杂的图片上还是很明显的,减少了2/3左右的体积。
如果将rgba替换为hex,还会再小一些

现在再进行检查元素不会崩溃了,但是依然会卡:)

参考资料

使用box-shadow进行画图(性能优化终结者)的更多相关文章

  1. Unity3D性能优化--- 收集整理的一堆

    http://www.cnblogs.com/willbin/p/3389837.html 官方优化文档--优化图像性能http://docs.unity3d.com/Documentation/Ma ...

  2. [Android]Android性能优化

    安卓性能优化 性能优化的几大考虑 Mobile Context 资源受限 + 内存,普遍较小,512MB很常见,开发者的机器一般比用户的机器高端 + CPU,核心少,运算能力没有全开 + GPU,上传 ...

  3. U3D开发性能优化笔记(待增加版本.x)

    http://blog.csdn.net/kaitiren/article/details/45071997 此总结由自己经验及网上收集整理优化内容 包括: .代码方面: .函数使用方面: .ui注意 ...

  4. CSS3与页面布局学习笔记(八)——浏览器兼容性问题与前端性能优化方案

    一.浏览器兼容 1.1.概要 世界上没有任何一个浏览器是一样的,同样的代码在不一样的浏览器上运行就存在兼容性问题.不同浏览器其内核亦不尽相同,相同内核的版本不同,相同版本的内核浏览器品牌不一样,各种运 ...

  5. React-Native性能优化点

    shouldComponentUpdate 确保组件在渲染之后不需要再更新的,即静态组件,尽量在其中增加shouldComponentUpdate方法,防止二次消耗所产生的性能消耗 shouldCom ...

  6. 使用Html5+C#+微信 开发移动端游戏详细教程:(六)游戏界面布局与性能优化

    本篇教程我们主要讲解在游戏界面上的布局一般遵循哪些原则和一些性能优化的通用方法. 接着教程(五),我们通过Loading类一次性加载了全部图像素材,现在要把我们所用到的素材变成图片对象显示在界面上,由 ...

  7. Web前端性能优化的9大问题

    1.请减少HTTP请求基本原理:在浏览器(客户端)和服务器发生通信时,就已经消耗了大量的时间,尤其是在网络情况比较糟糕的时候,这个问题尤其的突出.一个正常HTTP请求的流程简述:如在浏览器中输入&qu ...

  8. android app性能优化大汇总(UI渲染性能优化)

    UI性能测试 性能优化都需要有一个目标,UI的性能优化也是一样.你可能会觉得“我的app加载很快”很重要,但我们还需要了解终端用户的期望,是否可以去量化这些期望呢?我们可以从人机交互心理学的角度来考虑 ...

  9. Android UI性能优化详解

    设计师,开发人员,需求研究和测试都会影响到一个app最后的UI展示,所有人都很乐于去建议app应该怎么去展示UI.UI也是app和用户打交道的部分,直接对用户形成品牌意识,需要仔细的设计.无论你的ap ...

随机推荐

  1. Cocos2d-swift V3.x 中的update方法

    在cocos2d V3.x中update方法如果实现,则会被自动调用;不用向早期的版本那样要显式schedule. 但是你还是要显式schedule其他方法或blocks使用node的schedule ...

  2. MySql常用操作语句(2:数据库、表管理以及数据操作)

    本文主要内容转自一博文. 另外可供参考资源: SQL语句教程 SQL语法 1.数据库(database)管理  1.1 create 创建数据库 mysql> create database f ...

  3. SharePoint 查找字段内部名称的小方法

    今天逛博客园,偶然看到了下面的文章,介绍不用工具查看SharePoint字段内部名称,也介绍下自己的小方法. http://www.cnblogs.com/sunjunlin/archive/2012 ...

  4. CSS3实现多样的边框效果

    半透明边框 实现效果: 实现代码: <div> 你能看到半透明的边框吗? </div> div { /* 关键代码 */ border: 10px solid rgba(255 ...

  5. candy(贪心)

    [题目] There are N children standing in a line. Each child is assigned a rating value. You are giving ...

  6. Eclipse的优化

    1. 取消系统的自动折叠 Window->Preferences-> Java->Editor->Folding: Enable folding 2. 取消按".&q ...

  7. 初识Java——循环语句

    循环语句就是在一定条件下反复执行某一个操作.具体有三种方法实现: 1while循环语句 while语句也称作条件判断语句,它的循环方式为利用一个条件来控制是否要反复执行.语法如下: while(条件语 ...

  8. 【转载】tomcat+nginx+redis实现均衡负载、session共享(二)

    今天我们接着说上次还没完成session共享的部分,还没看过上一篇的朋友可以先看下上次内容,http://www.cnblogs.com/zhrxidian/p/5432886.html. 1.red ...

  9. C++各个存储区

    #include<iostream.h>void main(){char a[]="abc";栈 char b[]="abc";栈 char* c= ...

  10. 从前端开发看HTTP协议的应用

    一.Chrome Developer Network Tab Cheome Developer作为现在前端开发者最常用的开发调试工具,其具有前端可以涉及到的各方面的强大功能,为我们的开发和定位问题提供 ...