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;
截图:
问题原因
核心问题是重复绑定onload事件:
- 先给创建的
image对象绑定了一次onload,触发时调用getAverageColor - 在
getAverageColor内部,又给传入的img对象重新绑定了一次onload,直接覆盖了之前的绑定 - 此时图片已经加载完成,内部的
onload事件不会再触发,导致ctx.drawImage和后续颜色计算逻辑完全没执行,所有totalR/G/B和count都是初始值0,亮度自然为0
另外还有潜在问题:crossOrigin属性应该在设置src之前赋值,否则可能触发跨域限制,导致getImageData报错。
修复方案
- 移除
getAverageColor内部的img.onload绑定,直接在外部的onload回调里执行所有计算逻辑 - 将
crossOrigin的设置移到image.src赋值之前 - 可选优化:缩小图片尺寸来降低计算量(比如把画布尺寸设为原图的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
相关产品推荐
相关产品推荐

