Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
eloquent-js-3e-zh
提交
f533a446
E
eloquent-js-3e-zh
项目概览
OpenDocCN
/
eloquent-js-3e-zh
9 个月 前同步成功
通知
1
Star
22
Fork
5
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
E
eloquent-js-3e-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
f533a446
编写于
5月 13, 2018
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
19.
上级
8e47d083
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
210 addition
and
8 deletion
+210
-8
19.md
19.md
+210
-8
未找到文件。
19.md
浏览文件 @
f533a446
# 十九、项目:像素艺术编辑器
>
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 应用所需的所有元素。 在本章中,我们将实现一个。
前面几章的内容为
你
提供了构建基本的 Web 应用所需的所有元素。 在本章中,我们将实现一个。
我们的应用将是像素绘图程序,
您可以通过操纵放大视图(正方形彩色网格),来逐像素修改图像。 您
可以使用它来打开图像文件,用鼠标或其他指针设备在它们上面涂画并保存。 这是它的样子:
我们的应用将是像素绘图程序,
你可以通过操纵放大视图(正方形彩色网格),来逐像素修改图像。 你
可以使用它来打开图像文件,用鼠标或其他指针设备在它们上面涂画并保存。 这是它的样子:
![](
)
...
...
@@ -24,7 +24,7 @@
为了明白为什么这很重要,让我们考虑替代方案:将状态片段分配给整个界面。 直到某个时期,这更容易编写。 我们可以放入颜色字段,并在需要知道当前颜色时读取其值。
但是,我们添加了颜色选择器。它是一种工具,可让
您
单击图片来选择给定像素的颜色。 为了保持颜色字段显示正确的颜色,该工具必须知道它存在,并在每次选择新颜色时对其进行更新。 如果你添加了另一个让颜色可见的地方(也许鼠标光标可以显示它),你必须更新你的改变颜色的代码来保持同步。
但是,我们添加了颜色选择器。它是一种工具,可让
你
单击图片来选择给定像素的颜色。 为了保持颜色字段显示正确的颜色,该工具必须知道它存在,并在每次选择新颜色时对其进行更新。 如果你添加了另一个让颜色可见的地方(也许鼠标光标可以显示它),你必须更新你的改变颜色的代码来保持同步。
实际上,这会让你遇到一个问题,即界面的每个部分都需要知道所有其他部分,它们并不是非常模块化的。 对于本章中的小应用,这可能不成问题。 对于更大的项目,它可能变成真正的噩梦。
...
...
@@ -272,7 +272,7 @@ class ToolSelect {
}
```
通过将标签文本和字段包装在
`<label>`
元素中,我们告诉浏览器该标签属于该字段,例如,
您
可以点击标签来聚焦该字段。
通过将标签文本和字段包装在
`<label>`
元素中,我们告诉浏览器该标签属于该字段,例如,
你
可以点击标签来聚焦该字段。
我们还需要能够改变颜色 - 所以让我们添加一个控件。
`type`
属性为颜色的 HTML
`<input>`
元素为我们提供了专门用于选择颜色的表单字段。 这种字段的值始终是
`"#RRGGBB"`
格式(红色,绿色和蓝色分量,每种颜色两位数字)的 CSS 颜色代码。 当用户与它交互时,浏览器将显示一个颜色选择器界面。
...
...
@@ -333,7 +333,7 @@ function rectangle(start, state, dispatch) {
}
```
此实现中的一个重要细节是,拖动时,矩形将从原始状态重新绘制在图片上。 这样,
您
可以在创建矩形时将矩形再次放大和缩小,中间的矩形不会在最终图片中残留。 这是不可变图片对象实用的原因之一 - 稍后我们会看到另一个原因。
此实现中的一个重要细节是,拖动时,矩形将从原始状态重新绘制在图片上。 这样,
你
可以在创建矩形时将矩形再次放大和缩小,中间的矩形不会在最终图片中残留。 这是不可变图片对象实用的原因之一 - 稍后我们会看到另一个原因。
实现洪水填充涉及更多东西。 这是一个工具,填充和指针下的像素,和颜色相同的所有相邻像素。 “相邻”是指水平或垂直直接相邻,而不是对角线。 此图片表明,在标记像素处使用填充工具时,着色的一组像素:
...
...
@@ -365,7 +365,7 @@ function fill({x, y}, state, dispatch) {
绘制完成的像素的数组可以兼作函数的工作列表。 对于每个到达的像素,我们必须看看任何相邻的像素是否颜色相同,并且尚未覆盖。 随着新像素的添加,循环计数器落后于绘制完成的数组的长度。 任何前面的像素仍然需要探索。 当它赶上长度时,没有剩下未探测的像素,并且该函数就完成了。
最终的工具是一个颜色选择器,它允许
您
指定图片中的颜色,来将其用作当前的绘图颜色。
最终的工具是一个颜色选择器,它允许
你
指定图片中的颜色,来将其用作当前的绘图颜色。
```
js
function
pick
(
pos
,
state
,
dispatch
)
{
...
...
@@ -428,7 +428,7 @@ class SaveButton {
为了让浏览器真正下载图片,我们将创建一个链接元素,指向此 URL 并具有
`download`
属性。 点击这些链接后,浏览器将显示一个文件保存对话框。 我们将该链接添加到文档,模拟点击它,然后再将其删除。
您
可以使用浏览器技术做很多事情,但有时候做这件事的方式很奇怪。
你
可以使用浏览器技术做很多事情,但有时候做这件事的方式很奇怪。
并且情况变得更糟了。 我们也希望能够将现有的图像文件加载到我们的应用中。 为此,我们再次定义一个按钮组件。
...
...
@@ -555,3 +555,205 @@ class UndoButton {
}
```
## 让我们绘图吧
为了建立应用,我们需要创建一个状态,一组工具,一组控件和一个分派函数。 我们可以将它们传递给
`PixelEditor`
构造器来创建主要组件。 由于我们需要在练习中创建多个编辑器,因此我们首先定义一些绑定。
```
js
const
startState
=
{
tool
:
"
draw
"
,
color
:
"
#000000
"
,
picture
:
Picture
.
empty
(
60
,
30
,
"
#f0f0f0
"
),
done
:
[],
doneAt
:
0
};
const
baseTools
=
{
draw
,
fill
,
rectangle
,
pick
};
const
baseControls
=
[
ToolSelect
,
ColorSelect
,
SaveButton
,
LoadButton
,
UndoButton
];
function
startPixelEditor
({
state
=
startState
,
tools
=
baseTools
,
controls
=
baseControls
})
{
let
app
=
new
PixelEditor
(
state
,
{
tools
,
controls
,
dispatch
(
action
)
{
state
=
historyUpdateState
(
state
,
action
);
app
.
setState
(
state
);
}
});
return
app
.
dom
;
}
```
解构对象或数组时,可以在绑定名称后面使用
`=`
,来为绑定指定默认值,该属性在缺失或未定义时使用。
`startPixelEditor`
函数利用它来接受一个对象,包含许多可选属性作为参数。 例如,如果你未提供
`tools`
属性,则
`tools`
将绑定到
`baseTools`
。
这就是我们在屏幕上获得实际的编辑器的方式:
```
js
<
div
><
/div
>
<
script
>
document
.
querySelector
(
"
div
"
)
.
appendChild
(
startPixelEditor
({}));
<
/script>
```
来吧,画一些东西。 我会等着你。
## 为什么这个很困难
浏览器技术是惊人的。 它提供了一组强大的界面积木,排版和操作方法,以及检查和调试应用的工具。 你为浏览器编写的软件可以在几乎所有电脑和手机上运行。
与此同时,浏览器技术是荒谬的。 你必须学习大量愚蠢的技巧和难懂的事实才能掌握它,而它提供的默认编程模型非常棘手,大多数程序员喜欢用几层抽象来封装它,而不是直接处理它。
虽然情况肯定有所改善,但它以增加更多元素来解决缺点的方式,改善了它 - 也创造了更多复杂性。 数百万个网站使用的特性无法真正被取代。 即使可能,也很难决定它应该由什么取代。
技术从不存在于真空中 - 我们受到我们的工具,以及产生它们的社会,经济和历史因素的制约。 这可能很烦人,但通常更加有效的是,试图理解现有的技术现实如何发挥作用,以及为什么它是这样 - 而不是对抗它,或者转向另一个现实。
新的抽象可能会有所帮助。 我在本章中使用的组件模型和数据流约定,是一种粗糙的抽象。 如前所述,有些库试图使用户界面编程更愉快。 在编写本文时,React 和 Angular 是主流选择,但是这样的框架带有整个全家桶。 如果你对编写 Web 应用感兴趣,我建议调查其中的一些内容,来了解它们的原理,以及它们提供的好处。
## 练习
我们的程序还有提升空间。让我们添加一些更多特性作为练习。
### 键盘绑定
将键盘快捷键添加到应用。 工具名称的第一个字母用于选择工具,而
`control-Z`
或
`command-Z`
激活撤消工作。
通过修改
`PixelEditor`
组件来实现它。 为
`<div>`
元素包装添加
`tabIndex`
属性 0,以便它可以接收键盘焦点。 请注意,与
`tabindex`
属性对应的属性称为
`tabIndex`
,
`I`
大写,我们的
`elt`
函数需要属性名称。 直接在该元素上注册键盘事件处理器。 这意味着你必须先单击,触摸或按下 TAB 选择应用,然后才能使用键盘与其交互。
请记住,键盘事件具有
`ctrlKey`
和
`metaKey`
(用于 Mac 上的
`Command`
键)属性,你可以使用它们查看这些键是否被按下。
```
html
<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>`
元素的大小,将清除它,使其再次完全透明。
```
html
<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`
的工具,当你拖动时绘制一个实心圆。 圆的中心位于拖动或触摸手势开始的位置,其半径由拖动的距离决定。
```
html
<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`
工具,它在拖动的起点和终点之间绘制一条直线。
```
js
<
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>
```
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录