练习
我们的程序还有提升空间。让我们添加一些更多特性作为练习。
键盘绑定
将键盘快捷键添加到应用。 工具名称的第一个字母用于选择工具,而control-Z
或command-Z
激活撤消工作。
通过修改PixelEditor
组件来实现它。 为<div>
元素包装添加tabIndex
属性 0,以便它可以接收键盘焦点。 请注意,与tabindex
属性对应的属性称为tabIndex
,I
大写,我们的elt
函数需要属性名称。 直接在该元素上注册键盘事件处理器。 这意味着你必须先单击,触摸或按下 TAB 选择应用,然后才能使用键盘与其交互。
请记住,键盘事件具有ctrlKey
和metaKey
(用于 Mac 上的Command
键)属性,你可以使用它们查看这些键是否被按下。
<div></div>
<script>
// The original PixelEditor class. Extend the constructor.
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, dispatch);
}
});
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);
}
}
document.querySelector("div")
.appendChild(startPixelEditor({}));
</script>
高效绘图
绘图过程中,我们的应用所做的大部分工作都发生在drawPicture
中。 创建一个新状态并更新 DOM 的其余部分的开销并不是很大,但重新绘制画布上的所有像素是相当大的工作量。
找到一种方法,通过重新绘制实际更改的像素,使PictureCanvas
的setState
方法更快。
请记住,drawPicture
也由保存按钮使用,所以如果你更改它,请确保更改不会破坏旧用途,或者使用不同名称创建新版本。
另请注意,通过设置其width
或height
属性来更改<canvas>
元素的大小,将清除它,使其再次完全透明。
<div></div>
<script>
// Change this method
PictureCanvas.prototype.setState = function(picture) {
if (this.picture == picture) return;
this.picture = picture;
drawPicture(this.picture, this.dom, scale);
};
// You may want to use or change this as well
function drawPicture(picture, canvas, scale) {
canvas.width = picture.width * scale;
canvas.height = picture.height * scale;
let cx = canvas.getContext("2d");
for (let y = 0; y < picture.height; y++) {
for (let x = 0; x < picture.width; x++) {
cx.fillStyle = picture.pixel(x, y);
cx.fillRect(x * scale, y * scale, scale, scale);
}
}
}
document.querySelector("div")
.appendChild(startPixelEditor({}));
</script>
圆
定义一个名为circle
的工具,当你拖动时绘制一个实心圆。 圆的中心位于拖动或触摸手势开始的位置,其半径由拖动的距离决定。
<div></div>
<script>
function circle(pos, state, dispatch) {
// Your code here
}
let dom = startPixelEditor({
tools: Object.assign({}, baseTools, {circle})
});
document.querySelector("div").appendChild(dom);
</script>
合适的直线
这是比前两个更高级的练习,它将要求你设计一个有意义的问题的解决方案。 在开始这个练习之前,确保你有充足的时间和耐心,并且不要因最初的失败而感到气馁。
在大多数浏览器上,当你选择绘图工具并快速在图片上拖动时,你不会得到一条闭合直线。 相反,由于"mousemove"
或"touchmove"
事件没有快到足以命中每个像素,因此你会得到一些点,在它们之间有空隙。
改进绘制工具,使其绘制完整的直线。 这意味着你必须使移动处理器记住前一个位置,并将其连接到当前位置。
为此,由于像素可以是任意距离,所以你必须编写一个通用的直线绘制函数。
两个像素之间的直线是连接像素的链条,从起点到终点尽可能直。对角线相邻的像素也算作连接。 所以斜线应该看起来像左边的图片,而不是右边的图片。
如果我们有了代码,它在两个任意点间绘制一条直线,我们不妨继续,并使用它来定义line
工具,它在拖动的起点和终点之间绘制一条直线。
<div></div>
<script>
// The old draw tool. Rewrite this.
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;
}
function line(pos, state, dispatch) {
// Your code here
}
let dom = startPixelEditor({
tools: {draw, line, fill, rectangle, pick}
});
document.querySelector("div").appendChild(dom);
</script>