如何在 React 中构建像素艺术绘图应用程序

2025-06-08

如何在 React 中构建像素艺术绘图应用程序

近年来,像素画游戏强势回归,但这一次并非因为技术的限制,而是因为像素画本身就很棒。一些游戏开发者正在调整工作流程,将最终产品转化为像素画游戏,即使他们最初可能从3D建模开始。另一些人则选择在像素画编辑器中绘制精灵图的常规方式,而这正是我们今天要构建的。我们将讲解如何生成动态像素网格、如何设置颜色选择器、如何实际绘制像素,以及如何将完成的像素画图导出为PNG图像。

如果您更喜欢视频版本,您可以在 Youtube 上观看我制作它:

我通过在工作文件夹中运行 create-react-app 命令启动了一个新的 React 应用程序。

create-react-app pixels
Enter fullscreen mode Exit fullscreen mode

在这个项目中,我将更多地关注 React 代码部分,但会提供一个 SandBox 链接,指向包含 CSS 文件的完整代码示例。我主要使用 flexbox 来居中元素,这几乎占了 CSS 代码的 80%。剩下的部分是添加自定义颜色和边距来区分元素。

首先 - 我从 App.js 中删除了所有不必要的代码,并导入了编辑器组件,它将成为该应用程序的核心。

import "../styles/App.scss"
import Editor from "./Editor"

function App() {
  return (
    <div className="App">
      <Editor />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

接下来我们需要设置编辑器。

import React, { useState } from "react"
import "../styles/editor.scss"

export default function Editor() {
  return (
    <div id="editor">
      <h1>Pixel Editor</h1>
      <h2>Enter Panel Dimensions</h2>

      <div id="options">
        <div className="option">
          <input type="number" className="panelInput" />
          <span>Width</span>
        </div>
        <div className="option">
          <input type="number" className="panelInput" />
          <span>Height</span>
        </div>
      </div>

      <button className="button">Start Drawing</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们有几个标题元素和一些输入字段,用于动态设置绘图面板的宽度和高度。此外,还有一个按钮,用于隐藏选项并初始化绘图面板,以及在用户想要重新开始时重置像素网格。为此,我们需要设置一些 useState 钩子。

import React, { useState } from "react"
import "../styles/editor.scss"

export default function Editor() {
  const [panelWidth, setPanelWidth] = useState(16)
  const [panelHeight, setPanelHeight] = useState(16)
  const [hideOptions, setHideOptions] = useState(false)
  const [hideDrawingPanel, setHideDrawingPanel] = useState(true)
  const [buttonText, setButtonText] = useState("start drawing")
  const [selectedColor, setColor] = useState("#f44336")

  function initializeDrawingPanel() {
    setHideOptions(!hideOptions)
    setHideDrawingPanel(!hideDrawingPanel)

    buttonText === "start drawing"
      ? setButtonText("reset")
      : setButtonText("start drawing")
  }

  return (
    <div id="editor">
      <h1>Pixel Editor</h1>
      {hideDrawingPanel && <h2>Enter Panel Dimensions</h2>}
      {hideDrawingPanel && (
        <div id="options">
          <div className="option">
            <input
              type="number"
              className="panelInput"
              defaultValue={panelWidth}
              onChange={e => {
                setPanelWidth(e.target.value)
              }}
            />
            <span>Width</span>
          </div>
          <div className="option">
            <input
              type="number"
              className="panelInput"
              defaultValue={panelHeight}
              onChange={e => {
                setPanelHeight(e.target.value)
              }}
            />
            <span>Height</span>
          </div>
        </div>
      )}
      <button onClick={initializeDrawingPanel} className="button">
        {buttonText}
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

使用 useState 我们可以控制绘图面板的宽度和高度。我还添加了一些属性来控制元素的可见性。设置尺寸并点击按钮开始绘图后,所有选项都会隐藏,直到我们点击重新设计的“重置”按钮。

如果没有颜色选项,绘图应用就没什么用。在这个项目中,我决定使用 React-color 插件,它有很多不同的颜色选择器选项。我选择了他们的 CirclePicker 组件,不过你也可以在他们的网站上找到所有可选组件的完整列表,它们的工作方式都大同小异。

您可以通过运行来安装它

npm install react-color
Enter fullscreen mode Exit fullscreen mode

现在我们需要进行设置。

import React, { useState } from "react"
import "../styles/editor.scss"
import { CirclePicker } from "react-color"

export default function Editor() {
  const [panelWidth, setPanelWidth] = useState(16)
  const [panelHeight, setPanelHeight] = useState(16)
  const [hideOptions, setHideOptions] = useState(false)
  const [hideDrawingPanel, setHideDrawingPanel] = useState(true)
  const [buttonText, setButtonText] = useState("start drawing")
  const [selectedColor, setColor] = useState("#f44336")

  function initializeDrawingPanel() {
    setHideOptions(!hideOptions)
    setHideDrawingPanel(!hideDrawingPanel)

    buttonText === "start drawing"
      ? setButtonText("reset")
      : setButtonText("start drawing")
  }

  function changeColor(color) {
    setColor(color.hex)
  }

  return (
    <div id="editor">
      <h1>Pixel Editor</h1>
      {hideDrawingPanel && <h2>Enter Panel Dimensions</h2>}
      {hideDrawingPanel && (
        <div id="options">
          <div className="option">
            <input
              type="number"
              className="panelInput"
              defaultValue={panelWidth}
              onChange={e => {
                setPanelWidth(e.target.value)
              }}
            />
            <span>Width</span>
          </div>
          <div className="option">
            <input
              type="number"
              className="panelInput"
              defaultValue={panelHeight}
              onChange={e => {
                setPanelHeight(e.target.value)
              }}
            />
            <span>Height</span>
          </div>
        </div>
      )}
      <button onClick={initializeDrawingPanel} className="button">
        {buttonText}
      </button>
      {hideOptions && (
        <CirclePicker color={selectedColor} onChangeComplete={changeColor} />
      )}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

CirclePicker 中的 color 属性用于标记当前选择的颜色,useChangeComplete 是组件的事件,可用于触发操作。在我们的例子中,在从选择器中选择不同的颜色后,我们希望切换所选颜色的状态。

在编辑器中唯一要做的就是添加我们还需要构建的 DrawingPanel 组件。

我们以通常的方式导入组件:

import DrawingPanel from "./DrawingPanel"
Enter fullscreen mode Exit fullscreen mode

我们可以将其添加到按钮正下方的代码中:

{
  hideOptions && (
    <DrawingPanel
      width={panelWidth}
      height={panelHeight}
      selectedColor={selectedColor}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

以下是 DrawingPanel 组件的外观:

import React, { useRef } from "react"
import "../styles/drawingPanel.scss"
import Row from "./Row"

export default function DrawingPanel(props) {
  const { width, height, selectedColor } = props

  let rows = []

  for (let i = 0; i < height; i++) {
    rows.push(<Row key={i} width={width} selectedColor={selectedColor} />)
  }

  return (
    <div id="drawingPanel">
      <div id="pixels">{rows}</div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

根据输入的高度,我们生成相同数量的行,并将它们推送到 div 容器中,但我们还需要将宽度传递给每个 Row 组件,以便知道每行需要生成多少“像素”。接下来我们需要设置 Row 组件,但我们会回到 DrawingPanel 来设置导出为 PNG 格式。

import React from "react"
import "../styles/row.scss"
import Pixel from "./Pixel"

export default function Row(props) {
  const { width, selectedColor } = props

  let pixels = []

  for (let i = 0; i < width; i++) {
    pixels.push(<Pixel key={i} selectedColor={selectedColor} />)
  }

  return <div className="row">{pixels}</div>
}
Enter fullscreen mode Exit fullscreen mode

如您所见,我们以与在绘图面板中设置行相同的方式生成每行像素。现在我们需要设置像素组件,我们快完成了!

import React, { useState } from "react"
import "../styles/pixel.scss"

export default function Pixel(props) {
  const { selectedColor } = props

  const [pixelColor, setPixelColor] = useState("#fff")
  const [oldColor, setOldColor] = useState(pixelColor)
  const [canChangeColor, setCanChangeColor] = useState(true)

  function applyColor() {
    setPixelColor(selectedColor)
    setCanChangeColor(false)
  }

  function changeColorOnHover() {
    setOldColor(pixelColor)
    setPixelColor(selectedColor)
  }

  function resetColor() {
    if (canChangeColor) {
      setPixelColor(oldColor)
    }

    setCanChangeColor(true)
  }

  return (
    <div
      className="pixel"
      onClick={applyColor}
      onMouseEnter={changeColorOnHover}
      onMouseLeave={resetColor}
      style={{ backgroundColor: pixelColor }}
    ></div>
  )
}
Enter fullscreen mode Exit fullscreen mode

我希望有一个可视化的指示器来指示绘图操作,而不仅仅是通过 CSS 将光标变为指针。因此,我设置了一些 useState 钩子来实现这一点。这里的想法是,我们希望在鼠标悬停时暂时改变像素的颜色。如果鼠标悬停离开,我们希望将其恢复到原来的颜色,并将其存储为单独的状态。但是,如果在鼠标悬停离开之前点击它,我们希望永久设置该颜色,因此我们设置了一个辅助状态属性 canChangeColor 来防止 onMouseLeave 函数干扰颜色变化。

我们已经完成了绘图函数的设置,剩下的就是设置导出了。回到DrawingPanel组件!

首先我们需要安装导出插件:

npm install react-component-export-image
Enter fullscreen mode Exit fullscreen mode

之后,我们需要使用 useRef hook 进行设置。该插件需要引用要导出到图像的组件或元素。

import React, { useRef } from "react"
import "../styles/drawingPanel.scss"
import Row from "./Row"

import { exportComponentAsPNG } from "react-component-export-image"

export default function DrawingPanel(props) {
  const { width, height, selectedColor } = props

  const panelRef = useRef()

  let rows = []

  for (let i = 0; i < height; i++) {
    rows.push(<Row key={i} width={width} selectedColor={selectedColor} />)
  }

  return (
    <div id="drawingPanel">
      <div id="pixels" ref={panelRef}>
        {rows}
      </div>
      <button onClick={() => exportComponentAsPNG(panelRef)} className="button">
        Export as PNG
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

现在,如果您在面板中绘制内容并点击我们新的导出按钮,您将获得一张包含导出组件的新 PNG 图像。您还可以使用此插件将组件导出为 JPEG 和 PDF 格式。

以上就是我在这个项目中使用的所有代码(CSS 除外)。如果你有兴趣查看完整的工作版本,可以在 CodeSandbox 上查看该项目

如果您有任何问题或意见,可以通过TwitterInstagram联系我,我还会在那里发布有趣的代码花絮和设计。

我还定期将 React 和 Web 开发教程上传到 Youtube,所以如果您感兴趣的话,请随时通过订阅我的频道来支持我。

鏂囩珷鏉ユ簮锛�https://dev.to/alekswritescode/how-to-build-a-pixel-art-drawing-app-in-react-37n8
PREV
✨ 可访问性:简单介绍 ✨ 定义
NEXT
具有指标和跟踪功能的 Go、Kafka、gRPC 和 MongoDB 微服务👋