• 练习
    • 键盘绑定
    • 高效绘图
    • 合适的直线

    练习

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

    键盘绑定

    将键盘快捷键添加到应用。 工具名称的第一个字母用于选择工具,而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>