绘图工具

在我们绘制任何东西之前,我们需要实现一些工具,来控制画布上的鼠标或触摸事件的功能。

最基本的工具是绘图工具,它可以将你点击或轻触的任何像素,更改为当前选定的颜色。 它分派一个动作,将图片更新为一个版本,其中所指的像素赋为当前选定的颜色。

  1. function draw(pos, state, dispatch) {
  2. function drawPixel({x, y}, state) {
  3. let drawn = {x, y, color: state.color};
  4. dispatch({picture: state.picture.draw([drawn])});
  5. }
  6. drawPixel(pos, state);
  7. return drawPixel;
  8. }

该函数立即调用drawPixel函数,但也会返回它,以便在用户在图片上拖动或滑动时,再次为新的所触摸的像素调用。

为了绘制较大的形状,可以快速创建矩形。 矩形工具在开始拖动的点和拖动到的点之间画一个矩形。

  1. function rectangle(start, state, dispatch) {
  2. function drawRectangle(pos) {
  3. let xStart = Math.min(start.x, pos.x);
  4. let yStart = Math.min(start.y, pos.y);
  5. let xEnd = Math.max(start.x, pos.x);
  6. let yEnd = Math.max(start.y, pos.y);
  7. let drawn = [];
  8. for (let y = yStart; y <= yEnd; y++) {
  9. for (let x = xStart; x <= xEnd; x++) {
  10. drawn.push({x, y, color: state.color});
  11. }
  12. }
  13. dispatch({picture: state.picture.draw(drawn)});
  14. }
  15. drawRectangle(start);
  16. return drawRectangle;
  17. }

此实现中的一个重要细节是,拖动时,矩形将从原始状态重新绘制在图片上。 这样,你可以在创建矩形时将矩形再次放大和缩小,中间的矩形不会在最终图片中残留。 这是不可变图片对象实用的原因之一 - 稍后我们会看到另一个原因。

实现洪水填充涉及更多东西。 这是一个工具,填充和指针下的像素,和颜色相同的所有相邻像素。 “相邻”是指水平或垂直直接相邻,而不是对角线。 此图片表明,在标记像素处使用填充工具时,着色的一组像素:

绘图工具 - 图1

有趣的是,我们的实现方式看起来有点像第 7 章中的寻路代码。那个代码搜索图来查找路线,但这个代码搜索网格来查找所有“连通”的像素。 跟踪一组可能的路线的问题是类似的。

  1. const around = [{dx: -1, dy: 0}, {dx: 1, dy: 0},
  2. {dx: 0, dy: -1}, {dx: 0, dy: 1}];
  3. function fill({x, y}, state, dispatch) {
  4. let targetColor = state.picture.pixel(x, y);
  5. let drawn = [{x, y, color: state.color}];
  6. for (let done = 0; done < drawn.length; done++) {
  7. for (let {dx, dy} of around) {
  8. let x = drawn[done].x + dx, y = drawn[done].y + dy;
  9. if (x >= 0 && x < state.picture.width &&
  10. y >= 0 && y < state.picture.height &&
  11. state.picture.pixel(x, y) == targetColor &&
  12. !drawn.some(p => p.x == x && p.y == y)) {
  13. drawn.push({x, y, color: state.color});
  14. }
  15. }
  16. }
  17. dispatch({picture: state.picture.draw(drawn)});
  18. }

绘制完成的像素的数组可以兼作函数的工作列表。 对于每个到达的像素,我们必须看看任何相邻的像素是否颜色相同,并且尚未覆盖。 随着新像素的添加,循环计数器落后于绘制完成的数组的长度。 任何前面的像素仍然需要探索。 当它赶上长度时,没有剩下未探测的像素,并且该函数就完成了。

最终的工具是一个颜色选择器,它允许你指定图片中的颜色,来将其用作当前的绘图颜色。

  1. function pick(pos, state, dispatch) {
  2. dispatch({color: state.picture.pixel(pos.x, pos.y)});
  3. }

我们现在可以测试我们的应用了!

  1. <div></div>
  2. <script>
  3. let state = {
  4. tool: "draw",
  5. color: "#000000",
  6. picture: Picture.empty(60, 30, "#f0f0f0")
  7. };
  8. let app = new PixelEditor(state, {
  9. tools: {draw, fill, rectangle, pick},
  10. controls: [ToolSelect, ColorSelect],
  11. dispatch(action) {
  12. state = updateState(state, action);
  13. app.setState(state);
  14. }
  15. });
  16. document.querySelector("div").appendChild(app.dom);
  17. </script>