【总结】1111- 如何搞定Banner背景自动换色的功能?
一、背景概述
我是谁?我是一名前端攻城狮,这周刚到公司不久,我司的产品经理就跑到我面前说:“浪哥,昨晚我看到某 APP 首页 Banner 切换时,Banner 区域的背景色会跟随 Banner 图片的色调一起切换,这个功能我们能实现么?”。听完他的话,我在脑海中脑补了一下他描述的功能,然后淡淡的回了一句 —— 应该可以吧。话音刚落,他立马来了一句 —— ”那就这么定了,我们在下个版本就上线这个功能“。

竟然还有这么玩的,看来下次不能随随便便使用 “应该” 二字了。既然是自己挖的坑,那还得自己填,所以我重新梳理了一下功能需求。之后,发现其实这个功能最核心的难点就是提取图片的主题色。那么接下来,我们的重心就是寻找如何提取图片主题色的方案。
二、提取主题色方案探索
这当然难不倒一个熟练掌握百度、谷歌、Bing 等主流搜索引擎的程序猿,经过一番信息检索与筛选。在 如何对前端图片主题色进行提取?这篇文章详细告诉你 这篇文章中,我找到了比较常用的主题色提取算法,主要包括 最小差值法、中位切分法、八叉树算法、聚类、色彩建模法 等(可以考虑放几张算法概述图?)。第一眼看到这么多陌生的算法后,我的第一感受是这样的:

我只是想提取图片的主题色啊,能不能来点简单粗暴的方法。经过一番搜索,我找到了一种纯 CSS 实现的方案。在 小技巧!CSS 提取图片主题色功能探索 这篇文章中,介绍了通过 filter: blur()
和 transform: scale()
来获取图片的主题色。具体的实现效果如下图所示:

★在线示例:https://codepen.io/Chokcoco/pen/poRBQGg
”
这种方案实现起来并不会复杂,但会存在以下的问题:
只能大致拿到图片的主题色,结果并不是很精确;
模糊滤镜比较消耗性能,如果在页面上大量使用的话,可能会对应用的性能造成影响。
很明显 CSS 方案虽然实现起来比较简单,但并不是最好的方案。既然 CSS 方案行不通,那么我们就把目光转向 JS 方案。功夫不负有心人,在全球最大的程序员交流平台上,我找到了 color-thief 这个项目。该项目通过 JS 实现了从图片中获取调色板的功能,同时支持浏览器和 Node.js 环境。

(图片来源:https://lokeshdhakar.com/projects/color-thief/)
上图是 color-thief 项目提供的在线示例,提取图片调色板的效果,图中的 Dominat Color 表示图片的主题色。经过一番测试,发现该库提取图片调色板的功能还是挺不错的,那时心里的第一感觉就是就用它了。之后,我就开始在脑海中回顾产品经理提的需求。
我司 App 首页的 Banner 区,可以配置多张不同的 Banner 图,这些图会以轮播的形式展示。因为图片使用的是线上的地址,在轮播到下一张图片的时候,Banner 区的背景就要立即发生变化。如果在浏览器端进行解析的话,就没有办法及时切换背景色了,特别对于较大的图片来说,可能会导致背景色切换延迟。因为这些问题,我们只能考虑在服务端进行图片主题色提取了。心想幸亏 color-thief 也支持 Node.js 环境,不过没过几秒,就发现情况不对了。我们 App 首页上的 Banner 图片,在管理后台上传的时候,是直接传到七牛云 CDN 的,不是上传到我司的服务器。

难道为了开发这个功能,我们要调整 Banner 图片的上传方式?直觉告诉我,这种方案肯定不太合适。那么应该如何处理呢?要不翻翻七牛云的官方文档,看能不能找到一些有用的信息。不看不知道,一看吓一跳。七牛云智能多媒体服务 竟然为了开发者提供了十几种的图片处理功能:
图片瘦身
图片压缩
图片水印
图片盲水印
图片 EXIF 信息
图片圆角处理
图片平均色调
动图合成
图片全景拼接
图片样式
...
那时我第一眼就看中了 图片平均色调 这个功能,该功能的介绍如下图所示:

(图片来源:https://developer.qiniu.com/dora/1268/image-average-hue-imageave)
使用起来也很简单,直接在七牛云图片资源后面添加 imageAve 查询参数即可,成功请求之后就会以 JSON 的形式返回图片的 RGB 信息:
{"RGB": "0x85694d"
}
三、功能实现
之后,我测试了多张图片并把测试结果反馈给产品经理。在产品经理确认之后,我就开始着手开发上述功能。不过在具体开发前,我对 Banner 图片的处理过程进行了梳理,具体的流程如下图所示:

在经过上述的流程处理后,我们就可以取得每张 banner 图片对应的 url
地址和 rgb
图片平均色调的值。有了这些信息之后,我们就只要在 banner 图片切换的时候,同步更新该 banner 图片对应背景色即可。为了提高用户的视觉体验,我们对背景色进行了渐变处理。
在看具体的核心代码之前,我们先来看一下实际的运行效果:

看完上述的实际效果之后,接下来我们来简单介绍一下相关代码。
图片数据
// 实际项目中,该数据是从服务端接口中获取
const images = [{url: "https://***.jpg", // 七牛云图片地址rgb: "0xa28c60", // 当前图片对应的平均色调},...{url: "https://***.jpg",rgb: "0x98aea5",},
];
Vue 页面模板
<template><div class="block"><div class="banner-bg" :style="{ 'background-image': bgColor }"></div><el-carousel@change="changeCard"trigger="click"height="150px":initial-index="initialIndex"v-if="bannerImages.length > 0"><el-carousel-item v-for="(image, index) in bannerImages" :key="index"><img class="slide-img" :src="image.url" /></el-carousel-item></el-carousel></div>
</template>
Vue 逻辑代码
export default defineComponent({name: "App",data() {return {bannerImages: [], // Banner上要显示的图片列表initialIndex: 0, // 初始索引值bgColor: "none", // 默认背景颜色};},mounted() {this.bannerImages = images.map((image, index) => {const bannerImage: Record<string, any> = { ...image };const bannerBgColor = image.rgb.slice(2, 8); // 获取banner背景色const nextBannerBgColor =index === images.length - 1? images[0].rgb.slice(2, 8): images[index + 1].rgb.slice(2, 8);bannerImage.startRgb = hex2Rgb(`#${bannerBgColor}`);bannerImage.endRgba = hex2Rgb(`#${bannerBgColor}`, 0.8);bannerImage.gradientColor = gradientColors( // 生成渐变颜色`#${bannerBgColor}`,`#${nextBannerBgColor}`,5);const rgbArr = hex2Rgb(`#${bannerBgColor}`, null, true);bannerImage.lightNess =(rgbArr[0] * 0.2126 + rgbArr[1] * 0.7152 + rgbArr[2] * 0.0722) / 255;if (this.initialIndex === index) {this.bgColor = `linear-gradient(-180deg, ${bannerImage.startRgb} 20%, ${bannerImage.endRgba} 98%)`;}return bannerImage;});},methods: {changeCard(index) {this.bgColor = `linear-gradient(-180deg, ${this.bannerImages[index].startRgb} 20%, ${this.bannerImages[index].endRgba} 98%)`;},},
});
在以上代码中,我们使用了 hex2Rgb
和 gradientColors
两个工具函数,它们分别用来把十六进制的颜色转成 RGB/RGBA 的格式和生成渐变颜色。它们的具体实现如下所示:
hex2Rgb 函数
export function hex2Rgb(color, opacity?: number, getRgbList?: boolean) {const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;let sColor = color.toLowerCase();if (sColor && reg.test(sColor)) {if (sColor.length === 4) {let sColorNew = "#";for (let i = 1; i < 4; i += 1) {sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));}sColor = sColorNew;}//处理六位的颜色值const sColorChange = [];for (let i = 1; i < 7; i += 2) {sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));}if (getRgbList) {return sColorChange;}return typeof opacity !== "undefined"? `RGBA(${sColorChange.join(",")},${opacity})`: `RGB(${sColorChange.join(",")})`;} else {return sColor;}
}
gradientColors 函数
// 计算两个颜色之间渐变值
export function gradientColors(startColor: string,endColor: string,step: number
) {const startRGB = colorRgb(startColor); //转换为rgb数组模式const startR = startRGB[0];const startG = startRGB[1];const startB = startRGB[2];const endRGB = colorRgb(endColor);const endR = endRGB[0];const endG = endRGB[1];const endB = endRGB[2];const sR = (endR - startR) / step; //总差值const sG = (endG - startG) / step;const sB = (endB - startB) / step;const colorArr = [];for (let i = 0; i < step; i++) {//计算每一步的hex值const hex = colorHex("rgb(" +parseInt(sR * i + startR) +"," +parseInt(sG * i + startG) +"," +parseInt(sB * i + startB) +")");colorArr.push(hex);}return colorArr;
}
利用这个图片处理功能,最终完成了我司产品经理提出的需求。通过这次功能的开发,我对图片主题色提取算法也有一定的了解。另外,除了图片处理,七牛云智能多媒体服务还提供了 盲水印处理、动图合成 和 图片全景拼接 等挺好玩的功能,感兴趣的小伙伴可以体验一下,点击阅读原文即可跳转到官网页面~
四、参考资源
小技巧!CSS 提取图片主题色功能探索
如何对前端图片主题色进行提取?这篇文章详细告诉你
1. JavaScript 重温系列(22篇全)
2. ECMAScript 重温系列(10篇全)
3. JavaScript设计模式 重温系列(9篇全)
4. 正则 / 框架 / 算法等 重温系列(16篇全)
5. Webpack4 入门(上)|| Webpack4 入门(下)
6. MobX 入门(上) || MobX 入门(下)
7. 120+篇原创系列汇总
回复“加群”与大佬们一起交流学习~
点击“阅读原文”查看 120+ 篇原创文章