当前位置:嗨网首页>书籍在线阅读

19-应用程序设计

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

11.3.2 应用程序设计

在创建框架时,需要创建一些可以帮助用户设计应用程序的对象和资源文件。以下是将要创建的文件和简介。

  • EventDispatcher.js;

需要向其他对象广播事件的对象的基类。

  • Ornament.js;

在本程序中代表可拖动对象的类。

  • DisplayList.js;

在程序中用于模拟“保留模式”的类。

  • GameUtilities.js;

资源文件,用于存放可重用的通用函数。

  • DragAndDrop.js;

主程序类。

  • DragAndDrop.html;

集成所有代码的HTML文件。

读者可以在本书代码包中的Chapter11\draganddrop文件夹中找到这些文件。

1.EventDispatcher.js

需要做的第一件工作是创建一个方法使得JavaScript对象可以订阅和分发事件。标准的DOM对象可以有一个事件监听器来监听事件,比如:

theCanvas.addEventListener("mouseup",onMouseUp, false);

然而,Canvas图像和绘制路径不能创建事件,因为它们不能在保留模式中保存。用户的任务是在JavaScript中创建一个事件分发对象作为其他的对象的基类。使用EventDispatcher对象作为所有Ornament类的基类。当一个装饰物被点击时,可以发送一个事件,然后时间的订阅者可以做出响应。

EventDispatcher.js 中有以下方法。

  • addEventListener();

允许为指定事件添加一个监听器(订阅者)。

  • dispatch();

将事件分发给监听器。

  • removeEventListener();

当一个监听器不再需要时可以将它移除。

这些方法都通过修改EventDispatcher的原型属性进行定义(EventDispatcher.prototype. addEventListener()),其他类可以将这个类作为基类并继承这些方法。

//Adapted from code Copyright (c) 2010 Nicholas C. Zakas. All rights reserved.
//MIT License
function EventDispatcher(){
  this._listeners = {};
}
EventDispatcher.prototype.addEventListener = function(type, listener){
    if (typeof this._listeners[type] == "undefined"){
      this._listeners[type] = [];
    }
    this._listeners[type].push(listener);
 }
EventDispatcher.prototype.dispatch = function(event){
    if (typeof event == "string"){
      event = { type: event };
    }
    if (!event.target){
      event.target = this;
    }
    if (!event.type){ //false
      throw new Error("Event object missing 'type' property.");
    }
    if (this._listeners[event.type] instanceof Array){
      var listeners = this._listeners[event.type];
      for (var i=0, len=listeners.length; i <len; i++){
        listeners[i].call(this, event);
      }
    }
  }
EventDispatcher.prototype.removeEve ntListener = function(type, listener){
    if (this._listeners[type] instanceof Array){
      var listeners = this._listeners[type];
      for (var i=0, len=listeners.length;
        i <len; i++){
        if (listeners[i] === listener){
          listeners.splice(i, 1);
          break;
        }
      }
    }
}

2.Ornament.js

Ornament类定义在Ornament.js文件中,这个类使用EventDispatcher作为它的基类。Ornament类的实例用来代表可以拖动并放置在圣诞树上的灯泡。

为了继承EventListner类的方法,还需要创建一个对象,然后将这个对象的原型设置为EventDispatcher。将构造函数设置为Ornament函数,这样就可以储存所有Ornament类中定义的方法,代码如下所示。

function Ornament(color,height,width,context) {
...(此处省略其他代码)
}
Ornament.prototype = new EventDispatcher();
Ornament.prototype.constructor = Ornament;

因为Ornament类是本应用的核心,所以它包含了许多重要属性。这些属性是本程序运行的基础。

  • bulbColor;

灯泡的颜色(红色、绿色、蓝色、黄色、橙色、紫色)。

  • file;

要加载的灯泡图片的文件名。在得知灯泡的颜色之后生成文件名。

  • height;

灯泡图片的高度。

  • width;

灯泡图片的宽度。

  • x;

灯泡的x轴坐标。

  • y;

灯泡的y轴坐标。

  • context;

Canvas的环境对象。

  • loaded;

布尔值,在灯泡图片加载完成后为True。

  • image;

要加载的图片。

  • EVENT_CLICKED;

点击时分发的事件。

  • type;

取值为“Factory”或“copy”(Factory灯泡是那些点击后可以产生一个副本的灯泡)。

  • dragging;

布尔值,表示灯泡是否正在被拖动。这个属性是支持拖放的程序中一个重要信息。

以下是创建上述变量的代码。

this.bulbColor = color;
this.file = "bulb_"+ this.bulbColor + ".gif";
this.height = height;
this.width = width;
this.x = 0;
this.y = 0;
this.context = context;
this.loaded = false;
this.image = null;
this.EVENT_CLICKED = "clicked";
this.type = "factory";
this.dragging = false;

当用户点击一个Ornament的实例时,mouseUp()方法被调用(这是通过DisplayList类实现的,下一节将介绍),然后这个时间被分发到订阅者那里,即被点击的灯泡。在本程序中,唯一的订阅者是一个DragAndDrop类的实例。但是在理论上,可以有多个订阅者。

this.onMouseUp = function (mouseX,mouseY) {
  this.dispatch(this.EVENT_CLICKED);
}

这个示例不会像本书其他示例那样,而且在DragAndDrop.js中的主drawScreen()方法中绘制所有的东西。每一个显示对象(比如Ornament对象)都有自己的draw()函数。这个函数负责在给定的x轴、y轴坐标位置上根据width和height的值绘制灯泡图片。

这样做可以让主程序中的draw()函数尽可能的保持简洁。

this.draw = function() {
this.context.drawImage(this.image,this.x,this.y, this.width,this.height);
  }

编写这个类中最后一件工作是调用loadImage()函数,这个函数负责根据file属性的值加载图片。

this.loadImage = function (file) {
     this.image = new Image();
     this.image.onload = this.imageLoaded;
     this.image.src = file;
     }
     this.imageLoaded = function() {
       this.loaded = true;
     }
this.loadImage(this.file);

读者可以在本书代码包中的Ornament.js文件中查看完整的代码。

3.DisplayList.js

DisplayList是一个JavaScript对象,该对象负责所有显示在画布上的项目列表。当用户点击在画布上点击这些项目时,它将鼠标点击事件发送给它们。它可以让程序按照“保留模式”工作。

DisplayList有以两个属性。

  • objectList(array);

显示项目的列表。

  • theCanvas;

指向Canvas的环境对象的引用。当用户点击显示列表中的项目时,只有这个对象才能找到鼠标指针正确的x轴和y轴坐标。

DisplayList的addChild()函数负责通过调用数组的push()方法向objectList数组中添加一个对象。objectList数组中的所有项目都有自己的draw()函数。这些函数会在displayList对象的draw()函数调用时被调用。

this.addChild = function(child) {
  this.objectList.push(child);
}

removeChild()函数负责在objectList数组中找出第一个与传入参数匹配的对象,然后将之移除。使用array.indexOf()方法可以找出第一个匹配的对象实例,然后将它移除。

this.removeChild = function(child) {
var removeIndex = null;
  removeIndex = this.objectList.indexOf(child,0);
  if (removeIndex != null) {
    this.objectList.splice(removeIndex,1);
  }
}

DisplayList对象的draw()函数中将遍历objectList数组中所有的对象,并调用每个对象的draw()函数。

this.draw = function() {
  for (var i = 0; i <this.objectList.length; i++) {
     tempObject = this.objectList[i];
     tempObject.draw();
  }
}

mouseUp函数中将按照本书第6章中介绍的方法,获得鼠标指针的当前x轴和y轴位置。然后使用“点类型”的碰撞检测(见第6章),查看鼠标是否点到了objectList数组中的元素。如果是,就调用元素对象的mouseUp()函数。

this.onMouseUp = function(event) {
  var x;
  var y;
  if (event.pageX || event.pageY) {
    x = event.pageX;
    y = event.pageY;
  } else {
   x = e.clientX + document.body.scrollLeft +
      document.documentElement.scrollLeft;
   y = e.clientY + document.body.scrollTop +
      document.documentElement.scrollTop;
  }
  x -= this.theCanvas.offsetLeft;
  y -= this.theCanvas.offsetTop;
  var mouseX=x;
  var mouseY=y;
  for (i=0; i<this.objectList.length; i++) {
     var to = this.objectList[i];
     if ( (mouseY >= to.y) &&(mouseY <= to.y+to.height)
         &&(mouseX >= to.x) &&(mouseX <=
        to.x+to.width) ) {
        to.onMouseUp(mouseX,mouseY);
       }
     }
  }

读者可以在本书代码包中的DisplayList.js文件中查看完整的代码。

4.GameUtilities.js

在GameUtilities.js中放置调试器函数和canvasSupport()检测函数。将来,读者可以将那些不需要放在类中的其他工具函数放在这里。

var Debugger = function () { };
Debugger.log = function (message) {
  try {
    console.log(message);
  } catch (exception) {
    return;
  }
}
function canvasSupport () {
  return Modernizr.canvas;
}

5.DragAndDrop.js

DragAndDrop.js是本次支持拖放应用程序的主类。它是所有其他代码和类的控制器。

首先在DrapAndDrop.js中需要做以下工作:

(1)通过GameUtilities.js中的新函数检查是否支持Canvas;

(2)获得Canvas环境的引用(theCanvas)。

下面开始定义类。

function DragAndDrop() {
  if (!canvasSupport()) {
    return;
    }
  var theCanvas = document.getElementById("canvasOne");
  var context = theCanvas.getContext("2d");
  ...(这里添加其他代码)...
}

接下来,开始创建应用所必须的属性。

var backGround;
var bulbColors = new Array ("red","blue","green","yellow","orange","pink",
                "purple");
var bulbs;
  • backGround;

保存背景图片(黑色背景上有雪花和圣诞树)。

  • bulbColors;

保存颜色值的数组,在将灯泡放在画布上时会被用到。

  • bulbs;

一个数组,其中保存了所有在画布上被操作的灯泡。

现在定义一些变量,在将对象放置在画布上时需要用到这些变量。这些变量用于放置工厂类型的灯泡。当用点击工厂类型的灯泡时,会创建可以被拖动和放置的新灯泡。尽管这些数值可以被定义为const常量,但是这里依然使用var关键字定义。这是因为,IE浏览器不支持JavaScript的const关键字。

var BULB_START_X = 40;
var BULB_START_Y = 50;
var BULB_Y_SPACING = 10;
var BULB_WIDTH = 25;
var BULB_HEIGHT = 25;

接下来,创建一个用于保存工厂类型灯泡的数组。

var clickBulbs;

接下来的两个变量用于限制程序响应的鼠标点击次数。clickWait是等待次数,即调用gameLoop()函数这些次数之后才允许用户再次点击。clickWaitedFrames的值在用户点击灯泡时被设置为0,这样,计数就可以重新开始。如果不对监听的鼠标点击次数做某种限制,就会发现对象无法被拖动,也无法被放下。这是因为每次点击都会触发多次事件。

var clickWait = 5;
var clickWaitedFrames = 5;

接下来,创建一个DisplayList的对象实例,并将Canvas环境引用传进去。这个对象将保存画布上显示的所有Ornament对象。

var displayList = new DisplayList(theCanvas);

为mousemove和mouseup事件创建两个监听器,使用这两个事件在屏幕上进行点击和拖动操作。

theCanvas.addEventListener("mouseup",onMouseUp, false);
theCanvas.addEventListener("mousemove",onMouseMove, false);

接下来,初始化bulbs和clickBulbs数组,然后加载背景图片。

bulbs = new Array();
clickBulbs = new Array();
backGround = new Image();
backGround.src = "background.gif";

在用户点击画布时,可以创建一个可以被拖动和放置的灯泡。为了创建工厂类型的灯泡,将遍历bulbColors数组,为每种颜色创建一个Ornament类的实例。并将该实例的type属性设置为factory,通过位置变量(x,y)将它放在画布上,然后将它添加到clickBulbs数组中,再添加到displayList中。

for (var i=0;i <bulbColors.length; i++) {
  var tempBulb = new Ornament(bulbColors[i],BULB_WIDTH,
    BULB_HEIGHT,context);
  tempBulb.addEventListener(
    tempBulb.EVENT_CLICKED ,
    onBulbClicked);
  tempBulb.x = BULB_START_X;
  tempBulb.y = BULB_START_Y +
    i*BULB_Y_SPACING +i*BULB_HEIGHT;
  tempBulb.type = "factory";
  clickBulbs.push(tempBulb);
  displayList.addChild(tempBulb);
}

接下来,创建游戏循环。创建一个setTimeout循环,每20ms调用一次draw()函数。其中要更新clickWaitedFrames变量,这样可以检测是否允许接受另一次的鼠标点击。

function gameLoop() {
  window.setTimeout(gameLoop, 20);
  clickWaitedFrames++;
  draw();
}

draw()函数与本书之前的章节相比大幅度的缩减了。首先在函数中绘制背景图片,然后调用displayList.draw()方法绘制displayList中的所有对象。为了让这个简单显示列表能够工作,需要将对象按照与屏幕上显示层次相反的顺序添加到列表中。这是因为,后加入的对象会被绘制在较早加入的对象的上面。

function draw () {
  context.drawImage(backGround,0,0);
  displayList.draw();
}

onBulbClicked()函数是DragAndDrop类的核心。这个函数负责以下工作。

(1)将clickWaitedFrames与clickWait进行比较,检测一个鼠标点击是否有效。

(2)如果一个鼠标点击有效,那么查看用户是否点击了一个工厂类型的灯泡(如果是,则要创建一个新灯泡)或者是否是一个可拖拽的灯泡实例,然后将clickWaitedFrame归零。这样,程序在几帧之后才会接受下一个点击事件。

(3)通过event.target属性可以得到被点击的Ornament的实例。该属性指向被分发到该事件的对象的引用。

(4)如果该实例是一个工厂类型的灯泡,并且不是当前拖动的任何灯泡的话,就创建一个新的Ornament的实例,然后开始拖动它。新对象的type属性设置为copy,这意味它是可拖动的。

(5)如果该实例是一个可拖动的Ornament的实例,则查看当前是否正在拖动它。如果是,就将它的dragging属性设置为false,这就可以将它放下了。如果不是,就意味着没有拖动任何灯泡,则将它的dragging属性属性设置为true,即开始拖动这个新的灯泡。

上述操作将通过以下代码实现。

function onBulbClicked(event) {
  if (clickWaitedFrames >= clickWait) {
    clickWaitedFrames = 0;
    var clickedBulb = event.target;
    if ( clickedBulb.type == "factory" && !currentlyDragging()) {
      var tempBulb = new Ornament(clickedBulb.bulbColor,BULB_WIDTH,
                       BULB_HEIGHT,context);
      tempBulb.addEventListener(tempBulb.EVENT_CLICKED , onBulbClicked);
      tempBulb.y = clickedBulb.y+10;
      tempBulb.x = clickedBulb.x+10;
      tempBulb.type = "copy";
      tempBulb.dragging = true;
      bulbs.push(tempBulb);
      displayList.addChild(tempBulb);
    } else {
      if (clickedBulb.dragging) {
        clickedBulb.dragging = false;
      } else {
        if (!currentlyDragging()) {
           clickedBulb.dragging = true;
        }
      }
    }
  }
}

现在,创建一个函数,用于查看正在拖动的灯泡与onCBulbClicked()函数中检测的灯泡是否是同一个。为此,需要遍历bulbs数组查看是否有哪个灯泡的dragging属性被设置为true。

function currentlyDragging() {
  isDragging = false
  for (var i =0; i <bulbs.length; i++) {
    if (bulbs[i].dragging) {
      isDragging = true;
    }
  }
  return isDragging;
}

当用户点击鼠标时(即Canvas的DOM对象触发了mouseup事件),需要向显示列表发送一个消息,这样就可以检测是否有任何列表中的对象被点击。

function onMouseUp(event) {
  displayList.onMouseUp(event);
}

当触发Canvas DOM对象的mousemove时间时,需要做以下两件事。

(1)将被拖动的灯泡移动到鼠标指针的下面,可以通过将灯泡的x和y属性设置为鼠标指针x轴坐标和y轴坐标来实现。

(2)检查鼠标指针是否在某些可点击的对象上面,如果是,就利用CSS将鼠标指针更换为“手型”(代表一个按钮)。为此,将遍历所有Ornament对象(factory和copy类型的都包括),然后进行点类型的碰撞检测,查看鼠标指针是否经过了这些对象。如果在某个对象上面,就将鼠标的样式设置为“pointer”(通过设置cursor变量);否则,就将它设置为“default”。更新鼠标样式的代码如下。

theCanvas.style.cursor = cursor;
function onMouseMove(event) {
  var x;
    var y;
  if (event.pageX || event.pageY) {
     x = event.pageX;
     y = event.pageY;
  } else {
     x = e.clientX + document.body.scrollLeft +
      document.documentElement.scrollLeft;
     y = e.clientY + document.body.scrollTop +
      document.documentElement.scrollTop;
  }
  x -= theCanvas.offsetLeft;
  y -= theCanvas.offsetTop;
  var mouseX=x;
  var mouseY=y;
  for (var i =0; i <bulbs.length; i++) {
    if (bulbs[i].dragging) {
      bulbs[i].x = mouseX - BULB_WIDTH/2;
      bulbs[i].y = mouseY - BULB_HEIGHT/2;
    }
  }
  var cursor ="default";
  for (i=0; i<bulbs.length; i++) {
    var tp = bulbs[i];
    if ( (mouseY >= tp.y) &&(mouseY <= tp.y+tp.height) &&
       (mouseX >= tp.x)
      &&(mouseX <= tp.x+tp.width) ) {
        cursor = "pointer";
    }
  }
  for (i=0; i<clickBulbs.length; i++) {
    var tp = clickBulbs[i];
    if ( (mouseY >= tp.y) &&(mouseY <= tp.y+tp.height) &&
       (mouseX >= tp.x)
        &&(mouseX <= tp.x+tp.width) ) {
      cursor = "pointer";
    }
  }
  theCanvas.style.cursor = cursor;
}

6.DragAndDrop.html

由于大部分代码已经移动到了HTML文件的外面,因此这个文件与在开始编写支持拖放的应用程序时差别不大。

需要包含上述创建的JavaScript文件。

<script type="text/javascript" src="EventDispatcher.js"></script>
<script type="text/javascript" src="DisplayList.js"></script>
<script type="text/javascript" src="GameUtilities.js"></script>
<script type="text/javascript" src="DragAndDrop.js"></script>
<script type="text/javascript" src="Ornament.js"></script>
<script type="text/javascript" src="modernizr.js"></script>

在HTML中创建画布。在这个应用程序中,将画布在屏幕上居中显示。

<div align="center">
<canvas id="canvasOne" width="640" height="480" style="cursor: default;">
 Your browser does not support the HTML 5 Canvas.
</canvas>
</div>

最后,在window对象加载之后启动应用程序。

<script type="text/javascript">
window.addEventListener("load", eventWindowLoaded, false);
function eventWindowLoaded () {
  DragAndDrop();
}
</script>

现在,本例已经为Canvas应用程序创建了一个简单面向对象的架构。当在这个应用程序中试图解决创建画布的应用程序时,存在一些明显的应用程序开发问题。本例已经为画布上的逻辑对象创建了一种订阅和广播事件的方式,并且通过用显示列表模拟“保留模式”的方法实现对这些对象的追踪。当然,这种对象模式还有许多改进的方式,不过对于读者来说,如果将来打算在HTML5 Canvas方面发展,那么本章的内容可以作为一个不错的起点。

读者可以在本书的代码包的Chapter11\draganddrop文件夹中找到draganddrop.html文件,用浏览器打开可以测试本示例。