我的 HTML5 之旅 - 尝试 Canvas





0/5 (0投票)
在这篇文章中,Paul Farquhar 回顾了他第一次接触 HTML5 的经历,以及他如何受到启发,使用 Canvas 创建了一个 Spirograph(一种绘制曲线的数学玩具)应用程序。本文展示了 Paul 创建的 Spirograph 的初步设计,以及示例代码和附加资源。
在我的一生中,我写过相当多的代码。我使用的语言都是汇编语言或高级语言,如 Cobol(真糟糕)、Pascal、C、C++、Java,以及一些不太知名的语言,如 Algol 和 Smalltalk 80。除了偶尔看看别人的东西、改改一些片段来学习必要的东西之外,我很少接触 HTML。
如今,HTML5 正在通过各种媒体渠道推广——也许是时候让我仔细看看了?
目录
I. HTML 的第一印象如何?
第一次看 HTML/Javascript 时,我并不喜欢它(在接下来的叙述中,当我说 HTML 时,我指的是 HTML、JavaScript/ECMAScript 和 CSS 的集合)。我无法调试,只能通过试错或在代码中插入打印语句来解决问题。在我看来,这一切都一团糟。
II. 我最近再看 HTML5 感觉好多了!
使用 Chrome 浏览器,我有一个调试器——它可以真正地调试我的代码,提供完整的单步执行和断点功能。我还可以设置断点来捕获事件。“开发者工具”还为我提供了元素检查器和使用的样式(包括继承!),我可以查看使用的各种资源(或资产),还有一些我尚未深入使用的功能,比如性能监控。自从我第一次尝试以来,HTML 开发已经取得了长足的进步。阅读一些 HTML5 的文章,Canvas 的出现相当突出。Canvas 允许 Web 开发者动态地在屏幕上绘制图形,而无需任何插件,这是 HTML5 出现之前不可能实现的。我决定写点东西来尝试一下 Canvas。
III. 但首先,我需要一个想法
环顾我的家庭办公室,我旧的 Spirograph 盒子从书架上探出来——这就是我写 Spirograph 应用的灵感来源!
正如你所见,大部分零件都还在,我惊讶地发现了我在童年时期制作的一叠旧图画。
如果你不了解 Spirograph,它基本上是一个扁平的塑料环和一些齿轮。这个环被固定在一张纸上,纸的下面是硬纸板。环的两侧都有齿,齿轮上有笔孔。你把齿轮放在纸上,让它的齿与环上的齿啮合,然后把笔插进其中一个笔孔,围绕着环转动齿轮。我曾经为此投入数小时。这里有一个关于更多 历史 的链接。
Spirograph 是应用数学,基于 摆线。如果齿轮在环的外部移动,你会得到一个 外摆线;在环的内部移动,你会得到一个 内摆线。
在我这个小测试应用中,我想绘制内摆线类型的曲线。
IV. 初步设计
我想要一个简单的用户界面,允许用户
- 输入环和齿轮的半径
- 输入笔到齿轮中心的距离
- 一些按钮来轻松改变上述数字(+10, -10, +1, -1)
- 一个开始绘制的按钮和一个清除绘制的按钮
我还会添加一些代码来为更改画笔颜色和粗细做准备。这可以作为未来的扩展。
这是我获取数学公式的网站:Mathematische Basteleien。作者对所涉及的数学有很好的解释。
下一步是全部输入。我使用 Notepad++ 进行编辑,这是 spiro 的第一个版本。
spiro1.html
<!DOCTYPE html>
<html>
<head>
<title>Spiro-01</title>
<script type="application/javascript">
function DrawSpiro() {
var objCanvas = document.getElementById("canvas");
var ctx = objCanvas.getContext("2d");
ctx.save();
var size = 0;
if(objCanvas.width<objCanvas.height)
size = objCanvas.width;
else
size = objCanvas.height;
ctx.translate(size/2,size/2);
var OuterRadius = document.SpiroInput.OuterRadius.value;
var InnerRadius = document.SpiroInput.InnerRadius.value;
var PenOffset = document.SpiroInput.PenOffset.value*InnerRadius/100;
var PenColour; // for later extension
var PenThickness; // for later extension
var StartX; // we start drawing here
var StartY; // we start drawing here
var x,y;
var Step = Math.PI/180;
var Angle = 0;
StartX = (OuterRadius-InnerRadius)*Math.cos((InnerRadius / OuterRadius)*Angle)+PenOffset*Math.cos((1 - (InnerRadius / OuterRadius))*Angle);
StartY = (OuterRadius-InnerRadius)*Math.sin((InnerRadius / OuterRadius)*Angle)-PenOffset*Math.sin((1 - (InnerRadius / OuterRadius))*Angle);
ctx.beginPath();
ctx.moveTo(StartX,StartY);
do
{
Angle += Step;
x = (OuterRadius-InnerRadius)*Math.cos((InnerRadius / OuterRadius)*Angle)+PenOffset*Math.cos((1 - (InnerRadius / OuterRadius))*Angle);
y = (OuterRadius-InnerRadius)*Math.sin((InnerRadius / OuterRadius)*Angle)-PenOffset*Math.sin((1 - (InnerRadius / OuterRadius))*Angle);
ctx.lineTo(x,y);
ctx.stroke();
} while ((x != StartX) && (y != StartY));
ctx.restore();
}
function clearCanvas(){
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
}
function OuterP10(){
document.SpiroInput.OuterRadius.value = String(Number(document.SpiroInput.OuterRadius.value)+10);
}
function OuterP1(){
document.SpiroInput.OuterRadius.value = String(Number(document.SpiroInput.OuterRadius.value)+1);
}
function OuterM10(){
document.SpiroInput.OuterRadius.value = String(Number(document.SpiroInput.OuterRadius.value)-10);
}
function OuterM1(){
document.SpiroInput.OuterRadius.value = String(Number(document.SpiroInput.OuterRadius.value)-1);
}
function InnerP10(){
document.SpiroInput.InnerRadius.value = String(Number(document.SpiroInput.InnerRadius.value)+10);
}
function InnerP1(){
document.SpiroInput.InnerRadius.value = String(Number(document.SpiroInput.InnerRadius.value)+1);
}
function InnerM10(){
document.SpiroInput.InnerRadius.value = String(Number(document.SpiroInput.InnerRadius.value)-10);
}
function InnerM1(){
document.SpiroInput.InnerRadius.value = String(Number(document.SpiroInput.InnerRadius.value)-1);
}
function PenOffsetP10(){
document.SpiroInput.PenOffset.value = String(Number(document.SpiroInput.PenOffset.value)+10);
}
function PenOffsetP1(){
document.SpiroInput.PenOffset.value = String(Number(document.SpiroInput.PenOffset.value)+1);
}
function PenOffsetM10(){
document.SpiroInput.PenOffset.value = String(Number(document.SpiroInput.PenOffset.value)-10);
}
function PenOffsetM1(){
document.SpiroInput.PenOffset.value = String(Number(document.SpiroInput.PenOffset.value)-1);
}
</script>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<br/>
<button onclick="OuterP10();">+10</button>
<button onclick="OuterP1();">+1</button>
<button onclick="InnerP10();">+10</button>
<button onclick="InnerP1();">+1</button>
<button onclick="PenOffsetP10();">+10</button>
<button onclick="PenOffsetP1();">+1</button>
<form name="SpiroInput">
<input name="OuterRadius" type="text" size="7" value="77">
<input name="InnerRadius" type="text" size="7" value="29">
<input name="PenOffset" type="text" size="5" value="68">
</form>
<button onclick="OuterM10();">-10 </button>
<button onclick="OuterM1();">-1</button>
<button onclick="InnerM10();">-10</button>
<button onclick="InnerM1();">-1</button>
<button onclick="PenOffsetM10();">-10</button>
<button onclick="PenOffsetM1();">-1</button>
</br>
<button onclick="DrawSpiro();">Zeichnen</button>
<button onclick="clearCanvas();">Löschen</button>
</body>
</html>
V. 让我们来看看代码的一些片段
Canvas 本身在第 90 行定义。
<canvas id="canvas" width="400" height="400"></canvas>
所有绘图都发生在 `DrawSpiro()` 函数中,从第 6 行开始。
使用 Canvas 的第一步是获取一个上下文来工作。所有的绘图函数都需要一个上下文——它们不能直接作用于 Canvas 本身。我的上下文称为 `ctx`。
var objCanvas = document.getElementById("canvas"); var ctx = objCanvas.getContext("2d"); ctx.save();
我在这里额外做的是保存当前上下文。这允许我搞乱它,并在函数退出时恢复旧的上下文(`using ctx.restore()`)。虽然对于这个简单的应用来说不是必需的,但我认为养成这个习惯对于未来更复杂的程序很有好处。
数学要求原点在绘图区域的中心。所以,我必须将 Canvas 平移到那个点。首先,我查找最小的边,然后将中心移到 Canvas 的中间。
if(objCanvas.width<objCanvas.height)
size = objCanvas.width;
else
size = objCanvas.height;
ctx.translate(size/2,size/2);
在 Canvas 上绘制线涉及一个称为路径的东西。路径本质上是由线和弧组成的闭合图形。一个路径也可以有几个子路径。一个上下文一次只有一个路径。要绘制一些线,你需要使用 `lineTo` 和 `moveTo` 等函数来构建一个路径。然后你可以改变线条颜色和粗细等内容,最后使用 `stroke` 来绘制路径。代码中的下一步非常直接。
- 使用数学计算起点(`StartX` = 和 `StartY` = 行)。
- 使用 `ctx.beginPath();` 开始一个路径。
- 将画笔移动到该点:`ctx.moveTo(StartX,StartY);`。
接下来是主循环,当它回到起点时终止。循环所做的就是计算下一步(使用与计算起点相同的数学公式)并绘制到该点:`ctx.lineTo(x,y);`。这并不会在屏幕上画出任何线,它只是将线添加到路径中。要使其可见,我需要这个函数:`ctx.stroke();`。另一个与 Canvas 相关的函数是 `clearCanvas()`,它会完成清除工作。
- 获取一个上下文。
- 清除一个矩形:`context.clearRect(0, 0, canvas.width,canvas.height);`。
其他函数处理 +-10 和 +-1 按钮来改变参数。
应用程序可以工作了,但我并不完全满意。
VI. 这需要一些优化
是的,它能工作,但是
- 它很慢
- +-10 和 +-1 按钮函数很混乱
- 主循环中有很多数学计算可以优化
在开始优化应用程序之前,我首先使用 Chrome 中的分析工具测量绘制曲线所需的时间,以衡量 `DrawSpiro` 所花费的时间。
- 测试参数:175, 50, 130
- Sprio1:`DrawSpiro` 耗时 810ms。
第一步:将 `stroke` 函数调用移出主循环。
} while ((x != StartX) && (y != StartY)); ctx.stroke(); ctx.restore();
Sprio1-1:7ms。
区别很大!一个大的路径比许多小路径要好得多。
第二步:优化主循环中的一些数学计算。有些语句永远不会改变,可以在循环外计算一次。
var OmI = OuterRadius-InnerRadius; var IdO = InnerRadius / OuterRadius; var IdO1 = 1 - IdO; … StartX = OmI*Math.cos(IdO*Angle)+PenOffset*Math.cos(IdO1*Angle); StartY = OmI*Math.sin(IdO*Angle)-PenOffset*Math.sin(IdO1*Angle);
我需要一些新的测试参数来获得更长的绘图时间。
- 测试参数:181, 47, 130
- Spiro1-1:首次运行时 310ms,之后每次运行 190ms。
- Spiro1-2:首次运行时 180ms,之后每次运行 43ms。
再次,性能得到了大幅提升。这表明 JavaScript 解释器无法识别循环中不改变的代码部分,并将其视为常量。这是动态解释语言和编译语言之间的一个主要区别,编译语言的优化器可以发挥其奇妙的作用。奇怪的是,第二次及之后的运行比第一次快。对此我没有解释——也许你有?提高应用程序速度的另一个领域是我们计算步长的分辨率。与其每度(Step = Math.PI/180)计算一次,不如每 10 度计算一次(Step = Math.PI/180*10),这已经足够了。
- Spiro1-2:首次加载 128ms,第二次尝试 5ms。
现在我已经达到了我想要的速度——下一步是清理 +-10 和 +-1 按钮函数。我一开始为每个加号或减号都有一个函数(这里只显示两个按钮和一个函数)。
<button onclick="OuterP10();">+10</button>
<button onclick="OuterP1();">+1</button>
function OuterP10(){
document.SpiroInput.OuterRadius.value =
String(Number(document.SpiroInput.OuterRadius.value)+10);
}
所有这些都可以写得更优雅。
<button onclick="ChangeParam('O',10);">+10</button>
<button onclick="ChangeParam('O',1);">+1</button>
function ChangeParam(param,value)
{
switch(param)
{
case "O":
document.SpiroInput.OuterRadius.value =
String(Number(document.SpiroInput.OuterRadius.value)+value);
break;
case "I":
document.SpiroInput.InnerRadius.value =
String(Number(document.SpiroInput.InnerRadius.value)+value);
break;
case "PO":
document.SpiroInput.PenOffset.value =
String(Number(document.SpiroInput.PenOffset.value)+value);
break;
}
}
现在我有一个相当不错的小应用了。按钮和周围文本的格式可以更整洁,并且能够更改画笔颜色和粗细就更好了——这将是一个扩展,或者你可以自己添加。