应用

为了能够逐步构建应用,我们将主要组件实现为画布周围的外壳,以及一组动态工具和控件,我们将其传递给其构造器。

控件是出现在图片下方的界面元素。 它们为组件构造器的数组而提供。

工具是绘制像素或填充区域的东西。 该应用将一组可用工具显示为<select>字段。 当前选择的工具决定了,当用户使用指针设备与图片交互时,发生的事情。 它们作为一个对象而提供,该对象将出现在下拉字段中的名称,映射到实现这些工具的函数。 这个函数接受图片位置,当前应用状态和dispatch函数作为参数。 它们可能会返回一个移动处理器,当指针移动到另一个像素时,使用新位置和当前状态调用该函数。

  1. class PixelEditor {
  2. constructor(state, config) {
  3. let {tools, controls, dispatch} = config;
  4. this.state = state;
  5. this.canvas = new PictureCanvas(state.picture, pos => {
  6. let tool = tools[this.state.tool];
  7. let onMove = tool(pos, this.state, dispatch);
  8. if (onMove) return pos => onMove(pos, this.state);
  9. });
  10. this.controls = controls.map(
  11. Control => new Control(state, config));
  12. this.dom = elt("div", {}, this.canvas.dom, elt("br"),
  13. ...this.controls.reduce(
  14. (a, c) => a.concat(" ", c.dom), []));
  15. }
  16. setState(state) {
  17. this.state = state;
  18. this.canvas.setState(state.picture);
  19. for (let ctrl of this.controls) ctrl.setState(state);
  20. }
  21. }

指定给PictureCanvas的指针处理器,使用适当的参数调用当前选定的工具,如果返回了移动处理器,使其也接收状态。

所有控件在this.controls中构造并存储,以便在应用状态更改时更新它们。 reduce的调用会在控件的 DOM 元素之间引入空格。 这样他们看起来并不那么密集。

第一个控件是工具选择菜单。 它创建<select>元素,每个工具带有一个选项,并设置"change"事件处理器,用于在用户选择不同的工具时更新应用状态。

  1. class ToolSelect {
  2. constructor(state, {tools, dispatch}) {
  3. this.select = elt("select", {
  4. onchange: () => dispatch({tool: this.select.value})
  5. }, ...Object.keys(tools).map(name => elt("option", {
  6. selected: name == state.tool
  7. }, name)));
  8. this.dom = elt("label", null, "? Tool: ", this.select);
  9. }
  10. setState(state) { this.select.value = state.tool; }
  11. }

通过将标签文本和字段包装在<label>元素中,我们告诉浏览器该标签属于该字段,例如,你可以点击标签来聚焦该字段。

我们还需要能够改变颜色 - 所以让我们添加一个控件。 type属性为颜色的 HTML <input>元素为我们提供了专门用于选择颜色的表单字段。 这种字段的值始终是"#RRGGBB"格式(红色,绿色和蓝色分量,每种颜色两位数字)的 CSS 颜色代码。 当用户与它交互时,浏览器将显示一个颜色选择器界面。

该控件创建这样一个字段,并将其连接起来,与应用状态的color属性保持同步。

  1. class ColorSelect {
  2. constructor(state, {dispatch}) {
  3. this.input = elt("input", {
  4. type: "color",
  5. value: state.color,
  6. onchange: () => dispatch({color: this.input.value})
  7. });
  8. this.dom = elt("label", null, "? Color: ", this.input);
  9. }
  10. setState(state) { this.input.value = state.color; }
  11. }