一、背景概述
我是谁?我是一名前端攻城狮,这周刚到公司不久,我司的产品经理就跑到我面前说:“浪哥,昨晚我看到某 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 提取图片主题色功能探索
如何对前端图片主题色进行提取?这篇文章详细告诉你