uniapp实现背景颜色跟随图片主题色变化(多端兼容)

发布时间 2024-01-04 15:05:27作者: 柯基与佩奇

canvas 00_00_00-00_00_30.gif

最近做 uniapp 项目时遇到一个需求,要求模仿腾讯视频 app 首页的背景颜色跟随 banner 图片的主题颜色变化,并且还要兼容 H5、APP、微信小程序三端。

由于项目的技术栈为 uniapp,所以以下使用 uni-ui 的组件库作为栗子

需求分析

腾讯视频 app 效果如下:

fe5c83b7f461b47bd0d68a827a07412.jpg

从上图看出,大致分为两步:

1.获取图片主题色

2.设置从上到下的主题色to白色的渐变:

background: linear-gradient(to bottom, 主题色, 白色)

获取主题色主要采用canvas绘图,绘制完成后获取r、g、b三个通道的颜色像素累加值,最后再分别除以画布大小,得到每个颜色通道的平均值即可。

搭建页面结构

page.vue

<template>
  <view class="index">
    <!-- 由于获取主题色需要canvas绘制。绝对定位,把canvas移除屏幕外绘制 -->
    <canvas
      canvas-id="canvas"
      style="position: absolute;left: -400px;"
    ></canvas>
    <!-- box:填充主题颜色容器 -->
    <view class="box" :style="[getStyle]">
      <!-- 其他内容 -->
      <view class="tabs"></view>
      <!-- 轮播图 -->
      <swiper
        class="swiper"
        :current="current"
        circular
        autoplay
        indicator-dots
        @change="onChange"
        :interval="3000"
      >
        <swiper-item class="swiper-item" v-for="(url, i) in list" :key="i">
          <image :src="url" mode="aspectFill"></image>
        </swiper-item>
      </swiper>
    </view>
  </view>
</template>

<script>
import { getImageThemeColor } from "@/utils/index";
export default {
  data() {
    return {
      // 图片列表
      list: [],
      // 当前轮播图索引
      current: 0,
      // 缓存banner图片主题色
      colors: [],
      // 记录当前提取到第几张banner图片
      count: 0,
    };
  },
  computed: {
    // 动态设置banner主题颜色背景
    getStyle() {
      const color = this.colors[this.current];
      return {
        background: color
          ? `linear-gradient(to bottom, rgb(${color}), #fff)`
          : "#fff",
      };
    },
  },
  methods: {
    // banner改变
    onChange(e) {
      this.current = e.target.current;
    },
    getList() {
      this.list = [
        "https://img.zcool.cn/community/0121e65c3d83bda8012090dbb6566c.jpg@3000w_1l_0o_100sh.jpg",
        "https://img.zcool.cn/community/010ff956cc53d86ac7252ce64c31ff.jpg@900w_1l_2o_100sh.jpg",
        "https://img.zcool.cn/community/017fc25ee25221a801215aa050fab5.jpg@1280w_1l_2o_100sh.jpg",
      ];
    },
    // 获取主题颜色
    getThemColor() {
      getImageThemeColor(this, this.list[this.count], "canvas", (color) => {
        const colors = [...this.colors];
        colors[this.count] = color;
        this.colors = colors;
        this.count++;
        if (this.count < this.list.length) {
          this.getThemColor();
        }
      });
    },
  },
  onLoad() {
    this.getList();
    // banner图片请求完成后,获取主题色
    this.getThemColor();
  },
};
</script>

<style>
.box {
  display: flex;
  flex-direction: column;
  background-color: deeppink;
  padding: 10px;
}

.tabs {
  height: 100px;
  color: #fff;
}

.swiper {
  width: 95%;
  height: 200px;
  margin: auto;
  border-radius: 10px;
  overflow: hidden;
}

image {
  width: 100%;
  height: 100%;
}
</style>

封装获取图片主题颜色函数

先简单讲下思路 (想直接看源码可直接跳到下面) 。先通过 request 请求图片地址,获取图片的二进制数据,再将图片资源其转换成 base64,调用drawImage进行绘图,最后调用draw方法绘制到画布上。

CanvasContext.draw 介绍

image.png 更多 api 使用方法可参考:uniapp 官方文档

getImageThemeColor.js

/**
 * 获取图片主题颜色
 * @param path 图片的路径
 * @param canvasId 画布id
 * @param success 获取图片颜色成功回调,主题色的RGB颜色值
 * @param fail 获取图片颜色失败回调
 */
export const getImageThemeColor = (
  that,
  path,
  canvasId,
  success = () => {},
  fail = () => {}
) => {
  // 获取图片后缀名
  const suffix = path.split(".").slice(-1)[0];
  // uni.getImageInfo({
  //   src: path,
  //   success: (e) => {
  //     console.log(e.path) // 在安卓app端,不管src路径怎样变化,path路径始终为第一次调用的图片路径
  //   }
  // })
  // 由于getImageInfo存在问题,所以改用base64
  uni.request({
    url: path,
    responseType: "arraybuffer",
    success: (res) => {
      let base64 = uni.arrayBufferToBase64(res.data);
      const img = {
        path: `data:image/${suffix};base64,${base64}`,
      };
      // 创建canvas对象
      const ctx = uni.createCanvasContext(canvasId, that);

      // 图片绘制尺寸
      const imgWidth = 300;
      const imgHeight = 150;

      ctx.drawImage(img.path, 0, 0, imgWidth, imgHeight);

      ctx.save();
      ctx.draw(true, () => {
        uni.canvasGetImageData(
          {
            canvasId: canvasId,
            x: 0,
            y: 0,
            width: imgWidth,
            height: imgHeight,
            fail: fail,
            success(res) {
              let data = res.data;
              let r = 1,
                g = 1,
                b = 1;
              // 获取所有像素的累加值
              for (let row = 0; row < imgHeight; row++) {
                for (let col = 0; col < imgWidth; col++) {
                  if (row == 0) {
                    r += data[imgWidth * row + col];
                    g += data[imgWidth * row + col + 1];
                    b += data[imgWidth * row + col + 2];
                  } else {
                    r += data[(imgWidth * row + col) * 4];
                    g += data[(imgWidth * row + col) * 4 + 1];
                    b += data[(imgWidth * row + col) * 4 + 2];
                  }
                }
              }
              // 求rgb平均值
              r /= imgWidth * imgHeight;
              g /= imgWidth * imgHeight;
              b /= imgWidth * imgHeight;
              // 四舍五入
              r = Math.round(r);
              g = Math.round(g);
              b = Math.round(b);
              success([r, g, b].join(","));
            },
          },
          that
        );
      });
    },
  });
};

主题色计算公式

计算图片主题色的公式主要有两种常见的方法:平均法和主成分分析法。

平均法:

平均法是最简单的一种方法,它通过对图片中所有像素点的颜色进行平均来计算主题色。具体步骤如下:

  • 遍历图片的每个像素点,获取其 RGB 颜色值。
  • 将所有像素点的 R、G、B 分量分别求和,并除以像素点的总数,得到平均的 R、G、B 值。
  • 最终的主题色即为平均的 R、G、B 值。

主成分分析法

主成分分析法是一种更复杂但更准确的方法,它通过对图片中的颜色数据进行降维处理,提取出最能代表整个图片颜色分布的主要特征。具体步骤如下:

  • 将图片的所有像素点的颜色值转换为 Lab 颜色空间(Lab 颜色空间是一种与人眼感知相关的颜色空间)。
  • 对转换后的颜色数据进行主成分分析,找出相应的主成分。
  • 根据主成分的权重,计算得到最能代表整个图片颜色分布的主题色。

需要注意的是,计算图片主题色的方法可以根据具体需求和算法的实现方式有所不同,上述方法只是其中的两种常见做法。