LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

使用 HTML + JavaScript 实现像素画编辑器(附完整代码)

admin
2026年1月29日 10:46 本文热度 89
像素画是一种独特的视觉表现形式,以其复古的风格和简洁的设计吸引了众多爱好者。本文将介绍如何使用 HTML、CSS 和 JavaScript 创建一个像素画编辑器,支持基本的绘画、撤销/恢复、网格控制等功能。

效果演示

本编辑器提供了直观易用的操作界面,主要包括工具栏和画布两个区域:

  • 工具栏位于顶部,包含画笔、橡皮擦、填充等多种工具选项

  • 中央为主要工作区,展示实际绘制的像素画布

  • 可以自由调整画布大小和像素点尺寸

  • 提供撤销/恢复功能以便修正错误操作

  • 支持导出为 PNG 图像文件

页面结构

页面采用经典的垂直布局,主要分为两个区域:工具栏和主绘图区。

工具栏

工具栏包含多个交互元素,允许用户选择不同工具、设置颜色及参数,并执行特定动作,如撤销、清空等。
<div id="toolbar">  <div class="toolbar-group">    <label>工具</label>    <button id="penTool" class="tool-btn btn-active" data-tool="pen">画笔</button>    <button id="eraserTool" class="tool-btn" data-tool="eraser">橡皮</button>    <button id="fillTool" class="tool-btn" data-tool="fill">填充</button>    <label>画笔颜色      <input type="color" id="color" value="#000000"/>    </label>  </div>  <div class="toolbar-group">    <label>画布大小      <input type="number" id="gridSize" value="32" min="8" max="128" style="width:60px"/>    </label>    <label>像素尺寸      <input type="number" id="pixelSize" value="16" min="4" max="40" style="width:60px"/>    </label>  </div>  <div class="toolbar-group">    <button id="gridToggle" class="btn-active">网格 ON</button>    <button id="undoBtn" class="btn-disabled">撤销</button>    <button id="redoBtn" class="btn-disabled">恢复</button>    <button id="clearBtn">清空</button>    <button id="saveBtn">保存 PNG</button>  </div></div>

主绘图区域

主绘图区域由两个 <canvas> 元素组成:主画布用于绘制图像内容;另一个透明画布专门用来显示网格线,避免干扰实际绘画体验。
<div id="main">  <div id="canvasWrap">    <div id="canvasContainer">      <canvas id="canvas"></canvas>      <canvas id="grid"></canvas>    </div>    <div id="canvasInfo">尺寸: <span id="sizeInfo">32×32</span> 像素</div>  </div></div>

核心功能实现

状态管理

整个应用程序的状态被统一存储在一个名为 state 的对象中,涵盖了当前使用的工具类型、选定的颜色值、网格可见性标志位以及其他关键属性。这种集中式的管理模式有助于简化逻辑判断过程并提高维护效率。
var state = {  tool'pen',  color'#000000',  gridSize32,  pixelSize16,  gridVisibletrue,  history: [],  historyStep: -1};

初始化流程

程序启动后首先调用 init() 函数完成各项资源准备任务,包括设定初始画布尺寸、注册各类事件监听器等。
function init() {  resizeCanvas();  drawGrid();  updateButtonStates();  bindEvents();  setupToolButtons();}

画布尺寸调节与网格绘制

每当用户更改了“画布大小”或“像素尺寸”的数值时,都会触发对应输入框的 oninput 回调函数,进而重新计算新的宽高比例关系并对两个画布同时进行缩放更新。
function resizeCanvas() {  var ps = state.pixelSize;  var gs = state.gridSize;  var d = gs * ps;  [canvas, gridCanvas].forEach(c => {    c.width = c.height = d;    c.style.width = c.style.height = d + 'px';  });  ctx.imageSmoothingEnabled = false;  gtx.imageSmoothingEnabled = false;  // 更新尺寸信息  document.getElementById('sizeInfo').textContent = `${gs}×${gs} 像素 (${ps}px per pixel)`;  // 清空并推入空白历史  ctx.clearRect(00, canvas.width, canvas.height);  pushHistory();}
function drawGrid() {  gtx.clearRect(00, gridCanvas.width, gridCanvas.height);  if (!state.gridVisiblereturn;  gtx.strokeStyle = '#ccc';  gtx.lineWidth = 1;  var ps = state.pixelSize;  var gs = state.gridSize;  var canvasSize = gs * ps;  for (var i = 0; i <= gs; i++) {    var pos = i * ps;    // 垂直线    gtx.beginPath();    gtx.moveTo(pos, 0);    gtx.lineTo(pos, canvasSize);    gtx.stroke();    // 水平线    gtx.beginPath();    gtx.moveTo(0, pos);    gtx.lineTo(canvasSize, pos);    gtx.stroke();  }}

绘图核心逻辑

针对鼠标按下、移动、释放等行为分别绑定了相应的响应处理器,确保能够准确捕捉用户的每一次操作意图。例如,在启用画笔模式期间,只要检测到持续拖动的动作就会不断向目标位置填充指定色彩块。

而对于填充工具来说则更加复杂些,我们需借助 floodFill 算法来找出相连区域内所有相同颜色的像素点,并将其全部替换为目标色值。

function bindEvents() {  canvas.addEventListener('mousedown'e => {    drawing = true;    strokeMade = false;    var rect = canvas.getBoundingClientRect();    var x = e.clientX - rect.left;    var y = e.clientY - rect.top;    if (state.tool === 'pen'drawPixel(x, y, state.color);    else if (state.tool === 'eraser'drawPixel(x, y, '#ffffff');    else if (state.tool === 'fill') { floodFill(x, y, state.color); drawing = false; }  });  canvas.addEventListener('mousemove'e => {    if (!drawing) return;    var rect = canvas.getBoundingClientRect();    var x = e.clientX - rect.left;    var y = e.clientY - rect.top;    if (state.tool === 'pen'drawPixel(x, y, state.color);    else if (state.tool === 'eraser'drawPixel(x, y, '#ffffff');  });  window.addEventListener('mouseup'() => {    if (drawing && strokeMade) pushHistory();    drawing = false;  });  canvas.addEventListener('mouseleave'() => {    if (drawing && strokeMade) pushHistory();    drawing = false;  });}

扩展建议

  • 导入外部图片:允许用户载入已有素材作为底稿参考

  • 本地持久化存储:利用 localStorage 将未完成的作品自动缓存起来防止意外丢失

  • 调色板功能:提供常用颜色选择面板,方便快速更换颜色

  • 增加更多绘图工具:如直线工具、矩形工具、圆形工具等形状绘制工具

完整代码

git地址:https://gitee.com/ironpro/hjdemo/blob/master/pixel-art/index.html
<!DOCTYPE html><html lang="zh-CN"><head>  <meta charset="utf-8" />  <title>像素画编辑器</title>  <style>      * {          margin0;          padding0;          box-sizing: border-box;      }      body {          background#222;          color#eee;          display: flex;          flex-direction: column;          height100vh;          user-select: none;      }      #toolbar {          display: flex;          gap10px;          padding10px;          background#111;          align-items: center;          flex-wrap: wrap;          justify-content: space-between;          box-shadow0 2px 5px rgba(0000.3);      }      .toolbar-group {          display: flex;          gap10px;          align-items: center;          flex-wrap: wrap;      }      #toolbar button,      #toolbar input,      #toolbar select {          padding6px 10px;          border: none;          border-radius4px;          background#444;          color#eee;          cursor: pointer;          transition: all 0.2s ease;      }      #toolbar button:hover {          background#555;      }      #toolbar label {          display: flex;          align-items: center;          gap4px;      }      .btn-active {          background#007acc !important;          box-shadow0 0 5px rgba(01222040.5);      }      .btn-disabled {          opacity0.5;          cursor: not-allowed;      }      .tool-btn {          padding6px 10px;          border: none;          border-radius4px;          background#444;          color#eee;          cursor: pointer;          transition: all 0.2s ease;      }      .tool-btn:hover {          background#555;      }      .tool-btn.btn-active {          background#007acc !important;          box-shadow0 0 5px rgba(01222040.5);      }      #main {          display: flex;          flex1;          overflow: hidden;          padding10px;          background#2a2a2a;      }
      #canvasWrap {          margin: auto;          position: relative;          background#fff;          box-shadow0 0 20px rgba(0000.5);          border-radius4px;          padding10px;      }      #canvasContainer {          position: relative;          display: inline-block;      }      #canvas {          display: block;          image-rendering: pixelated;          image-rendering: crisp-edges;          cursor: crosshair;      }      #grid {          position: absolute;          left0;          top0;          pointer-events: none;      }      #canvasInfo {          text-align: center;          margin-top10px;          font-size14px;          color#666;      }      #gridToggle {          width80px;          text-align: center;      }  </style></head><body><div id="toolbar">  <div class="toolbar-group">    <label>工具</label>    <button id="penTool" class="tool-btn btn-active" data-tool="pen">画笔</button>    <button id="eraserTool" class="tool-btn" data-tool="eraser">橡皮</button>    <button id="fillTool" class="tool-btn" data-tool="fill">填充</button>    <label>画笔颜色      <input type="color" id="color" value="#000000"/>    </label>  </div>  <div class="toolbar-group">    <label>画布大小      <input type="number" id="gridSize" value="32" min="8" max="128" style="width:60px"/>    </label>    <label>像素尺寸      <input type="number" id="pixelSize" value="16" min="4" max="40" style="width:60px"/>    </label>  </div>  <div class="toolbar-group">    <button id="gridToggle" class="btn-active">网格 ON</button>    <button id="undoBtn" class="btn-disabled">撤销</button>    <button id="redoBtn" class="btn-disabled">恢复</button>    <button id="clearBtn">清空</button>    <button id="saveBtn">保存 PNG</button>  </div></div>
<div id="main">  <div id="canvasWrap">    <div id="canvasContainer">      <canvas id="canvas"></canvas>      <canvas id="grid"></canvas>    </div>    <div id="canvasInfo">尺寸: <span id="sizeInfo">32×32</span> 像素</div>  </div></div>
<script>  // 状态管理  var state = {    tool'pen',    color'#000000',    gridSize32,    pixelSize16,    gridVisibletrue,    history: [],    historyStep: -1  };
  var canvas = document.getElementById('canvas');  var ctx = canvas.getContext('2d');  var gridCanvas = document.getElementById('grid');  var gtx = gridCanvas.getContext('2d');  // 初始化  function init() {    resizeCanvas();    drawGrid();    updateButtonStates();    bindEvents();    setupToolButtons();  }  window.onload = init;  // 工具按钮设置  function setupToolButtons() {    document.querySelectorAll('.tool-btn').forEach(btn => {      btn.addEventListener('click'() => {        document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('btn-active'));        btn.classList.add('btn-active');        state.tool = btn.dataset.tool;      });    });  }  // 画布尺寸  function resizeCanvas() {    var ps = state.pixelSize;    var gs = state.gridSize;    var d = gs * ps;    [canvas, gridCanvas].forEach(c => {      c.width = c.height = d;      c.style.width = c.style.height = d + 'px';    });    ctx.imageSmoothingEnabled = false;    gtx.imageSmoothingEnabled = false;    // 更新尺寸信息    document.getElementById('sizeInfo').textContent = `${gs}×${gs} 像素 (${ps}px per pixel)`;    // 清空并推入空白历史    ctx.clearRect(00, canvas.width, canvas.height);    pushHistory();  }  // 绘制网格  function drawGrid() {    gtx.clearRect(00, gridCanvas.width, gridCanvas.height);    if (!state.gridVisiblereturn;    gtx.strokeStyle = '#ccc';    gtx.lineWidth = 1;    var ps = state.pixelSize;    var gs = state.gridSize;    var canvasSize = gs * ps;    for (var i = 0; i <= gs; i++) {      var pos = i * ps;      // 垂直线      gtx.beginPath();      gtx.moveTo(pos, 0);      gtx.lineTo(pos, canvasSize);      gtx.stroke();      // 水平线      gtx.beginPath();      gtx.moveTo(0, pos);      gtx.lineTo(canvasSize, pos);      gtx.stroke();    }  }  // 按钮状态更新  function updateButtonStates() {    document.getElementById('undoBtn').classList.toggle('btn-disabled', state.historyStep <= 0);    document.getElementById('redoBtn').classList.toggle('btn-disabled', state.historyStep >= state.history.length - 1);  }  // 历史栈  function pushHistory() {    state.history = state.history.slice(0, state.historyStep + 1);    state.history.push(ctx.getImageData(00, canvas.width, canvas.height));    state.historyStep++;    updateButtonStates();  }
  function undo() {    if (state.historyStep > 0) {      state.historyStep--;      redraw();      updateButtonStates();    }  }
  function redo() {    if (state.historyStep < state.history.length - 1) {      state.historyStep++;      redraw();      updateButtonStates();    }  }
  function redraw() {    ctx.putImageData(state.history[state.historyStep], 00);  }  // 绘图核心  var drawing = false;  var strokeMade = false;   // 本次笔划是否产生修改
  function drawPixel(x, y, color) {    var ps = state.pixelSize;    var gx = Math.floor(x / ps) * ps;    var gy = Math.floor(y / ps) * ps;    ctx.fillStyle = color;    ctx.fillRect(gx, gy, ps, ps);    strokeMade = true;  }
  function floodFill(x, y, fillColor) {    var img = ctx.getImageData(00, canvas.width, canvas.height);    var data = img.data;    var ps = state.pixelSize;    var gs = state.gridSize;    var w = gs * ps;    var startPos = (Math.floor(y / ps) * ps) * w + Math.floor(x / ps) * ps;    var startR = data[startPos * 4];    var startG = data[startPos * 4 + 1];    var startB = data[startPos * 4 + 2];    var startA = data[startPos * 4 + 3];    var [r, g, b] = hexToRgb(fillColor);    if (r === startR && g === startG && b === startB && startA === 255return;
    var stack = [[x, y]];    var seen = new Uint8Array(w * w);
    function matches(px) {      return data[px * 4] === startR &&        data[px * 4 + 1] === startG &&        data[px * 4 + 2] === startB &&        data[px * 4 + 3] === startA;    }    function pxIdx(x, y) { return y * w + x; }
    while (stack.length) {      var [cx, cy] = stack.pop();      var x1 = cx;      while (x1 >= 0 && matches(pxIdx(x1, cy))) x1--;      x1++;      var spanUp = false, spanDown = false;      for (var x2 = x1; x2 < w && matches(pxIdx(x2, cy)); x2++) {        var idx = pxIdx(x2, cy);        data[idx * 4] = r;        data[idx * 4 + 1] = g;        data[idx * 4 + 2] = b;        data[idx * 4 + 3] = 255;        seen[idx] = 1;        if (!spanUp && cy > 0 && matches(pxIdx(x2, cy - 1)) && !seen[pxIdx(x2, cy - 1)]) {          stack.push([x2, cy - 1]); spanUp = true;        } else if (spanUp && cy > 0 && !matches(pxIdx(x2, cy - 1))) spanUp = false;        if (!spanDown && cy < w - 1 && matches(pxIdx(x2, cy + 1)) && !seen[pxIdx(x2, cy + 1)]) {          stack.push([x2, cy + 1]); spanDown = true;        } else if (spanDown && cy < w - 1 && !matches(pxIdx(x2, cy + 1))) spanDown = false;      }    }    ctx.putImageData(img, 00);    pushHistory();  }
  function hexToRgb(hex) {    var r = parseInt(hex.slice(13), 16);    var g = parseInt(hex.slice(35), 16);    var b = parseInt(hex.slice(57), 16);    return [r, g, b];  }  // 鼠标事件  function bindEvents() {    canvas.addEventListener('mousedown'e => {      drawing = true;      strokeMade = false;      var rect = canvas.getBoundingClientRect();      var x = e.clientX - rect.left;      var y = e.clientY - rect.top;      if (state.tool === 'pen'drawPixel(x, y, state.color);      else if (state.tool === 'eraser'drawPixel(x, y, '#ffffff');      else if (state.tool === 'fill') { floodFill(x, y, state.color); drawing = false; }    });    canvas.addEventListener('mousemove'e => {      if (!drawing) return;      var rect = canvas.getBoundingClientRect();      var x = e.clientX - rect.left;      var y = e.clientY - rect.top;      if (state.tool === 'pen'drawPixel(x, y, state.color);      else if (state.tool === 'eraser'drawPixel(x, y, '#ffffff');    });    window.addEventListener('mouseup'() => {      if (drawing && strokeMade) pushHistory();      drawing = false;    });    canvas.addEventListener('mouseleave'() => {      if (drawing && strokeMade) pushHistory();      drawing = false;    });  }  // 控件事件  document.getElementById('color').oninput = e => state.color = e.target.value;  document.getElementById('gridSize').oninput = e => {    state.gridSize = +e.target.value;    resizeCanvas();    drawGrid();  };  document.getElementById('pixelSize').oninput = e => {    state.pixelSize = +e.target.value;    resizeCanvas();    drawGrid();  };  document.getElementById('gridToggle').onclick = () => {    state.gridVisible = !state.gridVisible;    var gridBtn = document.getElementById('gridToggle');    gridBtn.textContent = '网格 ' + (state.gridVisible ? 'ON' : 'OFF');    gridBtn.classList.toggle('btn-active', state.gridVisible);    drawGrid();  };  document.getElementById('undoBtn').onclick = undo;  document.getElementById('redoBtn').onclick = redo;  document.getElementById('clearBtn').onclick = () => {    ctx.clearRect(00, canvas.width, canvas.height);    pushHistory();  };  document.getElementById('saveBtn').onclick = () => {    var link = document.createElement('a');    link.download = 'pixel-art.png';    link.href = canvas.toDataURL();    link.click();  };  // 键盘快捷键  window.addEventListener('keydown'e => {    if (e.ctrlKey && e.key === 'z') { e.preventDefault(); undo(); }    if (e.ctrlKey && e.key === 'y') { e.preventDefault(); redo(); }  });</script></body></html>


阅读原文:https://mp.weixin.qq.com/s/PB3Fll1o1yqBRi6yU1rzHw


该文章在 2026/1/29 10:46:43 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved