# 十九、项目:像素艺术编辑器 > I look at the many colors before me. I look at my blank canvas. Then, I try to apply colors like words that shape poems, like notes that shape music. > > Joan Miro ![]() 前面几章的内容为您提供了构建基本的 Web 应用所需的所有元素。 在本章中,我们将实现一个。 我们的应用将是像素绘图程序,您可以通过操纵放大视图(正方形彩色网格),来逐像素修改图像。 您可以使用它来打开图像文件,用鼠标或其他指针设备在它们上面涂画并保存。 这是它的样子: ![]() 在电脑上绘画很棒。 你不需要担心材料,技能或天赋。 你只需要开始涂画。 ## 组件 应用的界面在顶部显示大的``元素,在它下面有许多表单字段。 用户通过从``字段。 当前选择的工具决定了,当用户使用指针设备与图片交互时,发生的事情。 它们作为一个对象而提供,该对象将出现在下拉字段中的名称,映射到实现这些工具的函数。 这个函数接受图片位置,当前应用状态和`dispatch`函数作为参数。 它们可能会返回一个移动处理器,当指针移动到另一个像素时,使用新位置和当前状态调用该函数。 ```js class PixelEditor { constructor(state, config) { let {tools, controls, dispatch} = config; this.state = state; this.canvas = new PictureCanvas(state.picture, pos => { let tool = tools[this.state.tool]; let onMove = tool(pos, this.state, dispatch); if (onMove) return pos => onMove(pos, this.state); }); this.controls = controls.map( Control => new Control(state, config)); this.dom = elt("div", {}, this.canvas.dom, elt("br"), ...this.controls.reduce( (a, c) => a.concat(" ", c.dom), [])); } setState(state) { this.state = state; this.canvas.setState(state.picture); for (let ctrl of this.controls) ctrl.setState(state); } } ``` 指定给`PictureCanvas`的指针处理器,使用适当的参数调用当前选定的工具,如果返回了移动处理器,使其也接收状态。 所有控件在`this.controls`中构造并存储,以便在应用状态更改时更新它们。 `reduce`的调用会在控件的 DOM 元素之间引入空格。 这样他们看起来并不那么密集。 第一个控件是工具选择菜单。 它创建``元素为我们提供了专门用于选择颜色的表单字段。 这种字段的值始终是`"#RRGGBB"`格式(红色,绿色和蓝色分量,每种颜色两位数字)的 CSS 颜色代码。 当用户与它交互时,浏览器将显示一个颜色选择器界面。 该控件创建这样一个字段,并将其连接起来,与应用程序状态的`color`属性保持同步。 ```js class ColorSelect { constructor(state, {dispatch}) { this.input = elt("input", { type: "color", value: state.color, onchange: () => dispatch({color: this.input.value}) }); this.dom = elt("label", null, "🎨 Color: ", this.input); } setState(state) { this.input.value = state.color; } } ``` ## 绘图工具 在我们绘制任何东西之前,我们需要实现一些工具,来控制画布上的鼠标或触摸事件的功能。 最基本的工具是绘图工具,它可以将你点击或轻触的任何像素,更改为当前选定的颜色。 它分派一个动作,将图片更新为一个版本,其中所指的像素赋为当前选定的颜色。 ```js function draw(pos, state, dispatch) { function drawPixel({x, y}, state) { let drawn = {x, y, color: state.color}; dispatch({picture: state.picture.draw([drawn])}); } drawPixel(pos, state); return drawPixel; } ``` 该函数立即调用`drawPixel`函数,但也会返回它,以便在用户在图片上拖动或滑动时,再次为新的所触摸的像素调用。 为了绘制较大的形状,可以快速创建矩形。 矩形工具在开始拖动的点和拖动到的点之间画一个矩形。 ```js function rectangle(start, state, dispatch) { function drawRectangle(pos) { let xStart = Math.min(start.x, pos.x); let yStart = Math.min(start.y, pos.y); let xEnd = Math.max(start.x, pos.x); let yEnd = Math.max(start.y, pos.y); let drawn = []; for (let y = yStart; y <= yEnd; y++) { for (let x = xStart; x <= xEnd; x++) { drawn.push({x, y, color: state.color}); } } dispatch({picture: state.picture.draw(drawn)}); } drawRectangle(start); return drawRectangle; } ```