65.9K
CodeProject 正在变化。 阅读更多。
Home

我的 HTML5 之旅 - 尝试 Canvas

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011 年 12 月 19 日

CPOL

7分钟阅读

viewsIcon

36112

downloadIcon

306

在这篇文章中,Paul Farquhar 回顾了他第一次接触 HTML5 的经历,以及他如何受到启发,使用 Canvas 创建了一个 Spirograph(一种绘制曲线的数学玩具)应用程序。本文展示了 Paul 创建的 Spirograph 的初步设计,以及示例代码和附加资源。

在我的一生中,我写过相当多的代码。我使用的语言都是汇编语言或高级语言,如 Cobol(真糟糕)、Pascal、C、C++、Java,以及一些不太知名的语言,如 Algol 和 Smalltalk 80。除了偶尔看看别人的东西、改改一些片段来学习必要的东西之外,我很少接触 HTML。

如今,HTML5 正在通过各种媒体渠道推广——也许是时候让我仔细看看了?

目录

  1. HTML 的第一印象如何?
  2. 但首先,我需要一个想法
  3. 初步设计
  4. 让我们来看看代码的一些片段
  5. 这需要一些优化
  6. 下载示例代码
  7. 观看示例代码的实际效果
  8. 额外资源

I. HTML 的第一印象如何?

第一次看 HTML/Javascript 时,我并不喜欢它(在接下来的叙述中,当我说 HTML 时,我指的是 HTML、JavaScript/ECMAScript 和 CSS 的集合)。我无法调试,只能通过试错或在代码中插入打印语句来解决问题。在我看来,这一切都一团糟。

II. 我最近再看 HTML5 感觉好多了!

使用 Chrome 浏览器,我有一个调试器——它可以真正地调试我的代码,提供完整的单步执行和断点功能。我还可以设置断点来捕获事件。“开发者工具”还为我提供了元素检查器和使用的样式(包括继承!),我可以查看使用的各种资源(或资产),还有一些我尚未深入使用的功能,比如性能监控。自从我第一次尝试以来,HTML 开发已经取得了长足的进步。阅读一些 HTML5 的文章,Canvas 的出现相当突出。Canvas 允许 Web 开发者动态地在屏幕上绘制图形,而无需任何插件,这是 HTML5 出现之前不可能实现的。我决定写点东西来尝试一下 Canvas。

III. 但首先,我需要一个想法

环顾我的家庭办公室,我旧的 Spirograph 盒子从书架上探出来——这就是我写 Spirograph 应用的灵感来源!

First-Steps-Canvas/image001.jpg

First-Steps-Canvas/image002.jpg

正如你所见,大部分零件都还在,我惊讶地发现了我在童年时期制作的一叠旧图画。

First-Steps-Canvas/image003.jpg

如果你不了解 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;
    }
}

现在我有一个相当不错的小应用了。按钮和周围文本的格式可以更整洁,并且能够更改画笔颜色和粗细就更好了——这将是一个扩展,或者你可以自己添加。

VII. 观看示例代码的实际效果(点击观看视频)

© . All rights reserved.