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

React Canvas缩放图片后getImageData仅返回rgba[0,0,0,0]问题求助

问题:图片缩放至Canvas尺寸后,getImageData仅返回rgba[0,0,0,0],无法实现洪水填充

我尝试通过操作像素颜色实现floodfill(洪水填充)功能,但将图片缩放至Canvas尺寸后,context.getImageData仅返回rgba[0,0,0,0]。当Canvas尺寸与图片尺寸一致时,填充功能可正常获取正确像素颜色,期望实现:即使图片缩放至全屏尺寸的Canvas,也能获取正确的像素颜色。

我的React组件代码

/* library */
import { useCallback, useEffect, useRef } from "react";
import { useSelector } from "react-redux";
/* module from local */
import { RootState } from "../../store";
import img1 from "../../test.png";

interface srcProps {
  src: string;
}

export function ImageCanvas({ src }: srcProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const contextRef = useRef<CanvasRenderingContext2D | null>(null);
  const awareness = useSelector((state: RootState) => state.yjs.awareness);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    canvas.addEventListener(
      "touchmove",
      (e) => {
        e.preventDefault();
      },
      { passive: false }
    );
  }, []);

  useEffect(() => {
    const img = new Image();
    img.src = img1;
    img.onload = () => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      const ctx = canvas.getContext("2d");
      if (ctx === null) return;
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      contextRef.current = ctx;
    };
  }, []);

  const _valid = useCallback((x: number, y: number) => {
    console.log("Enter _valid");
    const width = canvasRef.current?.offsetWidth as number;
    const height = canvasRef.current?.offsetHeight as number;
    console.log(`width: ${width}, height: ${height}`);

    if (x > 0 && x < width && y > 0 && y < height) {
      const imageData = contextRef.current?.getImageData(x, y, 1, 1);
      console.log(imageData?.data);
      if (
        imageData?.data[0] === 255 &&
        imageData?.data[1] === 255 &&
        imageData?.data[2] === 255
      ) {
        console.log("_valid => Success");
        return true;
      } else {
        console.log("_valid => valid Failed!!");
        return false;
      }
    }
  }, []);

  const _isPixel = useCallback((x: number, y: number) => {
    console.log("Enter _isPixel");
    const imageData = contextRef.current?.getImageData(x, y, 1, 1);
    if (imageData) {
      return imageData.data.length > 0;
    } else {
      return false;
    }
  }, []);

  const _setPixel = useCallback((x: number, y: number) => {
    console.log("Enter _setPixel");
    const imageData = contextRef.current?.getImageData(x, y, 1, 1);
    const pixelData = imageData?.data;

    let color = awareness.getLocalState().color;
    if (color === "black") {
      color = "rgba(223,154,36)";
    }

    const values = color.match(/\d+/g).map(Number);
    console.log("This is pixel data => _setPixel");
    console.log(pixelData);

    if (pixelData) {
      pixelData[0] = values[0];
      pixelData[1] = values[1];
      pixelData[2] = values[2];
      pixelData[3] = 255;
    }
    if (imageData) {
      contextRef.current?.putImageData(imageData, x, y);
    }
  }, []);

  const _fill = useCallback((x: number, y: number) => {
    const fillStack = [];

    fillStack.push([x, y]);

    while (fillStack.length > 0) {
      const [x, y]: any = fillStack.pop();

      if (_valid(x, y)) {
        console.log("It is valid");
      } else {
        console.log("NOT!! Valid");
        continue;
      }
      if (_isPixel(x, y)) {
        console.log("This is pixel");
      } else {
        console.log("NOT!! pixel");
        continue;
      }
      _setPixel(x, y);

      fillStack.push([x + 1, y]);
      fillStack.push([x - 1, y]);
      fillStack.push([x, y + 1]);
      fillStack.push([x, y - 1]);
    }
  }, []);

  const handlePointerDown = useCallback((e: React.PointerEvent<any>) => {
    _fill(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
  }, []);

  return (
    <canvas
      ref={canvasRef}
      onPointerDown={handlePointerDown}
      style={{ touchAction: "none" }}
      className={"w-full"}
    />
  );
}

export default ImageCanvas;

已尝试的方案

  • 在图片加载完成后执行填充函数
  • 设置ctx.imageSmoothingEnabled = false;
  • 测试当Canvas尺寸与图片尺寸一致时,填充功能可正常获取正确像素颜色

问题原因

Canvas的**绘制尺寸(width/height属性)和显示尺寸(CSS样式)**不匹配是核心问题:

  • 你给Canvas添加了w-full的CSS类,让它显示为全屏,但Canvas的默认绘制尺寸是300x150,并未跟随显示尺寸调整,导致图片被拉伸显示,但实际绘制的像素区域仍为默认尺寸,超出区域就是透明的rgba[0,0,0,0]。
  • 你用offsetWidth/offsetHeight获取的是显示尺寸,但getImageData是基于Canvas的绘制尺寸读取像素的,坐标不匹配就会读到透明区域。

解决步骤

1. 同步Canvas的绘制尺寸与显示尺寸

在图片加载的useEffect中,将Canvas的width/height属性设置为它的显示尺寸,确保绘制区域和显示区域一致:

useEffect(() => {
  const img = new Image();
  img.src = img1;
  img.onload = () => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    
    // 关键:同步绘制尺寸和显示尺寸
    const displayWidth = canvas.offsetWidth;
    const displayHeight = canvas.offsetHeight;
    canvas.width = displayWidth;
    canvas.height = displayHeight;
    
    ctx.imageSmoothingEnabled = false; // 保持像素清晰,可选
    ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
    contextRef.current = ctx;
  };
}, []);

2. 修复坐标边界判断

原_valid函数的边界判断漏掉了x=0或y=0的像素,修改为:

if (x >= 0 && x < width && y >= 0 && y < height) {
  // 后续逻辑
}

3. 完善useCallback依赖

原代码中useCallback的依赖为空数组,会导致闭包捕获旧值,需要补上相关依赖,比如_valid的依赖:

const _valid = useCallback((x: number, y: number) => {
  // 函数体
}, [canvasRef, contextRef]);

同理,_fill、_setPixel等函数也需要补充对应的依赖,避免闭包过期问题。

4. 可选:监听窗口resize(适配窗口变化)

如果需要适配窗口大小变化,添加resize监听重新绘制图片:

useEffect(() => {
  const handleResize = () => {
    const canvas = canvasRef.current;
    const ctx = contextRef.current;
    const img = new Image();
    img.src = img1;
    img.onload = () => {
      if (!canvas || !ctx) return;
      const displayWidth = canvas.offsetWidth;
      const displayHeight = canvas.offsetHeight;
      canvas.width = displayWidth;
      canvas.height = displayHeight;
      ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
    };
  };
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 06:20:53