https://zhuanlan.zhihu.com/p/24551014?utm_source=tuicool&utm_medium=referral
链接:https://zhuanlan.zhihu.com/p/24551014
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
今天圣诞节,昨天平安夜,看了大家各种雪花特效,徒手css画苹果,麋鹿,圣诞树什么的,我也想着自己拿代码写个圣诞老人给大家开心一下。
然后我选了这么一张图:
很萌吧,看起来非常好画,这里我就不说拿css怎么画了,反正我没画……,因为我太懒了。。。
我想了想既然是拿css画图,为什么不能直接用js生成呢,也就是说拿程序直接把一张图片转换成css的写法那不是更好?
说干就干,我第一件事就是打开了google,输入了img2css…… 这个关键字,你看我多机智……
果然没有让我失望,已经有老外实现了,还有人在15年底转发了微博,果然out了,项目在这里:img2css
很强大,瞬间转换出来了,然后我想着这玩意是怎么实现的呢?看了看源码,主要是先拿canvas draw了一张图,然后通过拿整个图的像素点信息,就是getImageData方法,返回的ImageData对象保存了每一个像素点的rgba的相关信息,再遍历这个矩阵拿到每一个像素点的hex值,当然你不拿hex也可以,就是输出后的空间太大了。rgba(x,x,x,x)和#fxc 这种写法肯定是hex更节约空间,作者还考虑到了颜色name缩写的转换,最后再利用box-shadow 1*1的黑科技给所有的像素点都来了一个复制shadow的效果,ok,转换完毕之后输出到一个div上就成了。
我们看下效果:
好吧,B都让他装了,我不服,我不干,我想了想依赖浏览器的canvas来转换不好,我要装逼还要开浏览器,我写个nodejs版本的吧,其他的步骤都好说,主要是getImageData这个方法在nodejs里怎么搞呢?
幸亏我原来用过一个pure的javascript图形库可以干到,这个库叫 oliver-moran/jimp 里面read的方法可以拿到bitmap,里面的data就是了~于是第一个版本的img2css nodejs版本就搞出来了。
最后写到一个指定的html文件里就完活了,然后正当我想装一下逼的时候,发现,这尼玛生成的html比图片本身都要大啊。
大了足足好几十倍。。。
当然了,谁让你一个点一个点的去画了,重复的颜色肯定占字节数大,这里开始想有没有优化的空间了?
我想到了两个办法:
1,css3的属性path-clip的polygon用法。
2,svg的path标签。
这2个都是用同样的单一颜色来优化用1*1的方法来绘图的办法,1方法可能需要多个N个不相邻的多边形图层叠加来实现,svg则可以只用一个path来描述一个相同颜色,肯定svg更优,此时我又打开了google,输入了img2svg的关键字……
果然,又有人提前把逼装好了……直接看这个库吧:59naga/pixel-to-svg 这样我们就可以绕过一些无用的细节。
他的做法也是拿图片的所有位图点信息,然后每一个点转换成rgba的格式,存储到一个map中,把所有相同颜色的点的x,y坐标进行了保存,最后通过每一个相连的width的连接来进行的优化。可以看下具体实现的代码:
看这个地方就可以了,D那个构造函数是把相关的坐标转换成对应的x,y,w,h的一个svg path写法的类。
通过不断的去画同一个颜色的path,如果遇到了x轴相邻的节点,直接把节点的width+1。最后我们看下优化后生成的结果:
可能大家看不懂,如果对应看一下svg path的缩写含义,就应该明白了:
这样就保证了所有的0,0,0,0的rgba的绘制共用了同一个path标签,并且会进行相邻的宽度合并,看下生成的svg大小:
恩,还是比png原图要大,但是比用box-shadow一个点一个点的去画要小了3倍,如果你不服,还想优化怎么办?最简单的办法了,相近的颜色进行合并就好了,这里我针对svg这一步再进行一次优化,最简单的办法,我们把png使用其他压缩工具先压缩一下,直接去除里面的相邻相近颜色,代码如下,这里用的imagemin:
这次我们用10%的压缩比,先看下效果图:
还不错吧,看起来虽然有了一些些噪点,我们看一下大小。
test2 是css的结果,test是直接转svg没优化的结果,test3 是10%压缩比例的svg效果,基本接近原图,当然也是因为这个圣诞老人比较简单。
好了,最后所有的代码发一下吧,比较简单一共没有超过100行,一个命令行的img2css/svg就搞定了,如果有更多优化空间,大家一起试试呗。
var Jimp = require('jimp');
var imagemin = require('imagemin');
var imageminPngquant = require('imagemin-pngquant');
var fs = require('fs');
var imgpath = process.argv[2];
var htmlpath = process.argv[3];
var type = process.argv[4] || 'css';
var quality = process.argv[5] || 60;
var convert = require('pixel-to-svg').convert;function rgb2hex(r, g, b) {return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}if (fs.existsSync(imgpath) && htmlpath) {if (type === 'css') {Jimp.read(imgpath).then((image) => {var data = image.bitmap.data,h = image.bitmap.height,w = image.bitmap.width;var RGBMatrix = [];for (var y = 0; y < h; y++) {RGBMatrix[y] = [];for (var x = 0; x < w; x++) {RGBMatrix[y][x] = {r: data[y * w * 4 + x * 4],g: data[y * w * 4 + x * 4 + 1],b: data[y * w * 4 + x * 4 + 2],a: data[y * w * 4 + x * 4 + 3]};}}var shadow = RGBMatrix.map((row, rowIndex) => {return row.map((col, colIndex) => {var color = rgb2hex(col.r, col.g, col.b);return `${color} ${colIndex ? colIndex + 'px' : 0} ${rowIndex ? rowIndex + 'px' : 0}`;}).join(',');}).join(',');var html = `<div style="height:1px;width:1px;box-shadow:${shadow};"></div>`;fs.writeFileSync(htmlpath, html, 'utf-8');console.info(`${htmlpath} file created!`);});} else if (type === 'svg') {imagemin([imgpath], {plugins: [imageminPngquant({quality: quality})]}).then(files => {var file = files[0];Jimp.read(file.data, (err, image) => {var svg = convert(image.bitmap);fs.writeFileSync(htmlpath, svg, 'utf-8');console.info(`${htmlpath} file created!`);});});}
} else {console.error(`${imgpath} or ${htmlpath} not exists!`);
}
祝各位圣诞快乐,如果对工程化相关的主题感兴趣,比如为什么前端面试的时候会问,徒手写个命令行工具的实现思路时,你该怎么办呢?