应用
为了能够逐步构建应用,我们将主要组件实现为画布周围的外壳,以及一组动态工具和控件,我们将其传递给其构造器。
控件是出现在图片下方的界面元素。 它们为组件构造器的数组而提供。
工具是绘制像素或填充区域的东西。 该应用将一组可用工具显示为<select>
字段。 当前选择的工具决定了,当用户使用指针设备与图片交互时,发生的事情。 它们作为一个对象而提供,该对象将出现在下拉字段中的名称,映射到实现这些工具的函数。 这个函数接受图片位置,当前应用状态和dispatch
函数作为参数。 它们可能会返回一个移动处理器,当指针移动到另一个像素时,使用新位置和当前状态调用该函数。
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 元素之间引入空格。 这样他们看起来并不那么密集。
第一个控件是工具选择菜单。 它创建<select>
元素,每个工具带有一个选项,并设置"change"
事件处理器,用于在用户选择不同的工具时更新应用状态。
class ToolSelect {
constructor(state, {tools, dispatch}) {
this.select = elt("select", {
onchange: () => dispatch({tool: this.select.value})
}, ...Object.keys(tools).map(name => elt("option", {
selected: name == state.tool
}, name)));
this.dom = elt("label", null, "? Tool: ", this.select);
}
setState(state) { this.select.value = state.tool; }
}
通过将标签文本和字段包装在<label>
元素中,我们告诉浏览器该标签属于该字段,例如,你可以点击标签来聚焦该字段。
我们还需要能够改变颜色 - 所以让我们添加一个控件。 type
属性为颜色的 HTML <input>
元素为我们提供了专门用于选择颜色的表单字段。 这种字段的值始终是"#RRGGBB"
格式(红色,绿色和蓝色分量,每种颜色两位数字)的 CSS 颜色代码。 当用户与它交互时,浏览器将显示一个颜色选择器界面。
该控件创建这样一个字段,并将其连接起来,与应用状态的color
属性保持同步。
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; }
}