练习

我们的程序还有提升空间。让我们添加一些更多特性作为练习。

键盘绑定

将键盘快捷键添加到应用。 工具名称的第一个字母用于选择工具,而control-Zcommand-Z激活撤消工作。

通过修改PixelEditor组件来实现它。 为<div>元素包装添加tabIndex属性 0,以便它可以接收键盘焦点。 请注意,与tabindex属性对应的属性称为tabIndexI大写,我们的elt函数需要属性名称。 直接在该元素上注册键盘事件处理器。 这意味着你必须先单击,触摸或按下 TAB 选择应用,然后才能使用键盘与其交互。

请记住,键盘事件具有ctrlKeymetaKey(用于 Mac 上的Command键)属性,你可以使用它们查看这些键是否被按下。

  1. <div></div>
  2. <script>
  3. // The original PixelEditor class. Extend the constructor.
  4. class PixelEditor {
  5. constructor(state, config) {
  6. let {tools, controls, dispatch} = config;
  7. this.state = state;
  8. this.canvas = new PictureCanvas(state.picture, pos => {
  9. let tool = tools[this.state.tool];
  10. let onMove = tool(pos, this.state, dispatch);
  11. if (onMove) {
  12. return pos => onMove(pos, this.state, dispatch);
  13. }
  14. });
  15. this.controls = controls.map(
  16. Control => new Control(state, config));
  17. this.dom = elt("div", {}, this.canvas.dom, elt("br"),
  18. ...this.controls.reduce(
  19. (a, c) => a.concat(" ", c.dom), []));
  20. }
  21. setState(state) {
  22. this.state = state;
  23. this.canvas.setState(state.picture);
  24. for (let ctrl of this.controls) ctrl.setState(state);
  25. }
  26. }
  27. document.querySelector("div")
  28. .appendChild(startPixelEditor({}));
  29. </script>

高效绘图

绘图过程中,我们的应用所做的大部分工作都发生在drawPicture中。 创建一个新状态并更新 DOM 的其余部分的开销并不是很大,但重新绘制画布上的所有像素是相当大的工作量。

找到一种方法,通过重新绘制实际更改的像素,使PictureCanvassetState方法更快。

请记住,drawPicture也由保存按钮使用,所以如果你更改它,请确保更改不会破坏旧用途,或者使用不同名称创建新版本。

另请注意,通过设置其widthheight属性来更改<canvas>元素的大小,将清除它,使其再次完全透明。

  1. <div></div>
  2. <script>
  3. // Change this method
  4. PictureCanvas.prototype.setState = function(picture) {
  5. if (this.picture == picture) return;
  6. this.picture = picture;
  7. drawPicture(this.picture, this.dom, scale);
  8. };
  9. // You may want to use or change this as well
  10. function drawPicture(picture, canvas, scale) {
  11. canvas.width = picture.width * scale;
  12. canvas.height = picture.height * scale;
  13. let cx = canvas.getContext("2d");
  14. for (let y = 0; y < picture.height; y++) {
  15. for (let x = 0; x < picture.width; x++) {
  16. cx.fillStyle = picture.pixel(x, y);
  17. cx.fillRect(x * scale, y * scale, scale, scale);
  18. }
  19. }
  20. }
  21. document.querySelector("div")
  22. .appendChild(startPixelEditor({}));
  23. </script>

定义一个名为circle的工具,当你拖动时绘制一个实心圆。 圆的中心位于拖动或触摸手势开始的位置,其半径由拖动的距离决定。

  1. <div></div>
  2. <script>
  3. function circle(pos, state, dispatch) {
  4. // Your code here
  5. }
  6. let dom = startPixelEditor({
  7. tools: Object.assign({}, baseTools, {circle})
  8. });
  9. document.querySelector("div").appendChild(dom);
  10. </script>

合适的直线

这是比前两个更高级的练习,它将要求你设计一个有意义的问题的解决方案。 在开始这个练习之前,确保你有充足的时间和耐心,并且不要因最初的失败而感到气馁。

在大多数浏览器上,当你选择绘图工具并快速在图片上拖动时,你不会得到一条闭合直线。 相反,由于"mousemove""touchmove"事件没有快到足以命中每个像素,因此你会得到一些点,在它们之间有空隙。

改进绘制工具,使其绘制完整的直线。 这意味着你必须使移动处理器记住前一个位置,并将其连接到当前位置。

为此,由于像素可以是任意距离,所以你必须编写一个通用的直线绘制函数。

两个像素之间的直线是连接像素的链条,从起点到终点尽可能直。对角线相邻的像素也算作连接。 所以斜线应该看起来像左边的图片,而不是右边的图片。

练习 - 图1

如果我们有了代码,它在两个任意点间绘制一条直线,我们不妨继续,并使用它来定义line工具,它在拖动的起点和终点之间绘制一条直线。

  1. <div></div>
  2. <script>
  3. // The old draw tool. Rewrite this.
  4. function draw(pos, state, dispatch) {
  5. function drawPixel({x, y}, state) {
  6. let drawn = {x, y, color: state.color};
  7. dispatch({picture: state.picture.draw([drawn])});
  8. }
  9. drawPixel(pos, state);
  10. return drawPixel;
  11. }
  12. function line(pos, state, dispatch) {
  13. // Your code here
  14. }
  15. let dom = startPixelEditor({
  16. tools: {draw, line, fill, rectangle, pick}
  17. });
  18. document.querySelector("div").appendChild(dom);
  19. </script>