You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13中图片亮度计算始终为0的问题求助

问题:Next.js广告牌组件图片亮度计算始终为0,无法切换文字颜色

我用Next.js 13搭建电商站点,做了一个带图片的广告牌组件,需求是根据图片亮度自动切换文字颜色。写了getAverageColor函数并在useEffect里调用,但不管切换深色还是浅色图片,计算出来的亮度始终是0。

代码如下:

import { Billboard as BillboardType } from "@/types";
import { useEffect, useState } from "react";

interface BillboardProps {
  data: BillboardType;
}

const BillboardComponent: React.FC<BillboardProps> = ({ data }) => {
  const [textColor, setTextColor] = useState<string>("red"); // Default text color

  useEffect(() => {
    const getAverageColor = (img: HTMLImageElement) => {
      img.setAttribute("crossOrigin", "anonymous");
      const canvas = document.createElement("canvas");
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;

      img.onload = () => {
        ctx.drawImage(img, 0, 0, img.width, img.height);
        const imageData = ctx.getImageData(0, 0, img.width, img.height).data;

        let totalR = 0;
        let totalG = 0;
        let totalB = 0;
        let count = 0;

        for (let i = 0; i < imageData.length; i += 4) {
          totalR += imageData[i];
          totalG += imageData[i + 1];
          totalB += imageData[i + 2];
          count++;
        }

        const avgR = totalR / count;
        const avgG = totalG / count;
        const avgB = totalB / count;

        // Calculate the brightness using the standard formula
        const brightness = (avgR * 299 + avgG * 587 + avgB * 114) / 1000;
        console.log(totalR, totalG, totalB, count, brightness);
        // Set the text color based on brightness
        if (brightness > 128) {
          setTextColor("black"); // Use black text for light backgrounds
        } else {
          setTextColor("white"); // Use white text for dark backgrounds
        }
      };
    };

    const image = new Image();
    image.src = data?.imageUrl || "";

    // Ensure the image is loaded before calculating the average color
    image.onload = () => {
      getAverageColor(image);
    };
  }, [data?.imageUrl]);

  return (
    <div className="p-4 sm:p-6 lg:p-8 rounded-xl overflow-hidden">
      <div
        style={{ backgroundImage: `url(${data?.imageUrl})` }}
        className="rounded-xl relative aspect-square md:aspect-[2.4/1] overflow-hidden bg-cover"
      >
        <div className="h-full w-full flex flex-col justify-center items-center text-center gap-y-8">
          <div
            style={{
              color: textColor,
            }}
            className="font-bold text-3xl sm:text-5xl lg:text-6xl sm:max-w-xl max-w-xs"
          >
            {data?.label}
          </div>
        </div>
      </div>
    </div>
  );
};

export default BillboardComponent;

截图:
调试面板显示亮度相关数值均为0


问题原因

核心问题是重复绑定onload事件:

  1. 先给创建的image对象绑定了一次onload,触发时调用getAverageColor
  2. 在getAverageColor内部,又给传入的img对象重新绑定了一次onload,直接覆盖了之前的绑定
  3. 此时图片已经加载完成,内部的onload事件不会再触发,导致ctx.drawImage和后续颜色计算逻辑完全没执行,所有totalR/G/B和count都是初始值0,亮度自然为0

另外还有潜在问题:crossOrigin属性应该在设置src之前赋值,否则可能触发跨域限制,导致getImageData报错。


修复方案
  1. 移除getAverageColor内部的img.onload绑定,直接在外部的onload回调里执行所有计算逻辑
  2. 将crossOrigin的设置移到image.src赋值之前
  3. 可选优化:缩小图片尺寸来降低计算量(比如把画布尺寸设为原图的1/10),提升性能

修复后完整代码
import { Billboard as BillboardType } from "@/types";
import { useEffect, useState } from "react";

interface BillboardProps {
  data: BillboardType;
}

const BillboardComponent: React.FC<BillboardProps> = ({ data }) => {
  const [textColor, setTextColor] = useState<string>("red");

  useEffect(() => {
    const calculateBrightnessAndSetColor = (img: HTMLImageElement) => {
      // 优化:缩小画布尺寸,减少计算量
      const scale = 0.1;
      const canvas = document.createElement("canvas");
      canvas.width = img.width * scale;
      canvas.height = img.height * scale;
      const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;

      // 绘制缩小后的图片
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;

      let totalR = 0;
      let totalG = 0;
      let totalB = 0;
      let count = 0;

      for (let i = 0; i < imageData.length; i += 4) {
        totalR += imageData[i];
        totalG += imageData[i + 1];
        totalB += imageData[i + 2];
        count++;
      }

      const avgR = totalR / count;
      const avgG = totalG / count;
      const avgB = totalB / count;
      const brightness = (avgR * 299 + avgG * 587 + avgB * 114) / 1000;

      setTextColor(brightness > 128 ? "black" : "white");
    };

    const image = new Image();
    // 先设置跨域,再赋值src
    image.crossOrigin = "anonymous";
    image.src = data?.imageUrl || "";

    image.onload = () => {
      calculateBrightnessAndSetColor(image);
    };

    // 处理图片加载失败的情况
    image.onerror = () => {
      setTextColor("white"); //  fallback颜色
    };
  }, [data?.imageUrl]);

  return (
    <div className="p-4 sm:p-6 lg:p-8 rounded-xl overflow-hidden">
      <div
        style={{ backgroundImage: `url(${data?.imageUrl})` }}
        className="rounded-xl relative aspect-square md:aspect-[2.4/1] overflow-hidden bg-cover"
      >
        <div className="h-full w-full flex flex-col justify-center items-center text-center gap-y-8">
          <div
            style={{ color: textColor }}
            className="font-bold text-3xl sm:text-5xl lg:text-6xl sm:max-w-xl max-w-xs"
          >
            {data?.label}
          </div>
        </div>
      </div>
    </div>
  );
};

export default BillboardComponent;

内容的提问来源于stack exchange,提问作者Updater XD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 22:59:53