八、拼图视频(JigsawVideo)

在本章中,您将学习以下内容:

  • 将图像分割成小块以制作拼图玩具的方法

  • 如何响应玩家移动棋子来解决难题

  • 如何计算水平和垂直坐标,并操作lefttop样式属性来重新定位屏幕上的元素

  • 关于容差或余量的概念,这样你的玩家就不必完美地解决这个难题

  • 如何让完成的拼图看起来变成一个运行的视频

介绍

本章的项目是一个拼图游戏,完成后会变成一个视频。它已经在配有鼠标的电脑上的 Chrome、Firefox、Opera 和 Safari 上进行了测试。每次加载程序或点击按钮重启程序时,拼图块会随机出现在屏幕上。图 8-1 显示了程序在运行 Firefox 浏览器的台式电脑上运行时的打开屏幕。

img/272384_2_En_8_Fig1_HTML.jpg

图 8-1

在计算机上打开屏幕

在电脑上,玩家使用鼠标移动和重新定位棋子。随机放置的碎片可能会叠放在一起。图 8-2 显示了展开的拼图块。我是用鼠标做的。我的例子有六个矩形的部分。

img/272384_2_En_8_Fig2_HTML.jpg

图 8-2

碎片散开

8-3 显示了我是如何把拼图拼在一起的。我可以把拼图放在屏幕上的任何地方。拼图的三块已经拼在一起了。

img/272384_2_En_8_Fig3_HTML.jpg

图 8-3

拼图的进展

请注意,带有标签反馈的框告诉继续工作。图 8-4 显示拼图接近完成。

img/272384_2_En_8_Fig4_HTML.jpg

图 8-4

只剩一片可以放进拼图玩具了

当把这些零件放在一起时,程序允许有误差,我称之为公差。你可以注意到白色的缺口,这说明拼图没有拼好。当我移动到最后一个棋子时,图 8-5 显示了我最后一次移动后不久的屏幕截图。

img/272384_2_En_8_Fig5_HTML.jpg

图 8-5

被视频取代的片段

请注意,反馈现在显示为“好!”一个视频已经开始播放,我停止它,并重置到开始,以获得这个截图。这幅画看起来很完美。事实上,这六块拼图已经被视频取代了。图 8-6 显示了控制显示的视频。这些控件不会自动显示,但是如果玩家将鼠标放在视频下部的顶部,就可以看到它们。不同浏览器的视频控制各不相同。

img/272384_2_En_8_Fig6_HTML.jpg

图 8-6

带控件的视频剪辑

我决定接受挑战,让这个项目适用于 iPhone、iPad 和 Android 手机。这意味着构建一个允许玩家使用手指触摸的用户界面。更准确地说,为了展示我的雄心,我想制作一个既能使用鼠标又能触摸的网站程序。我将把如何响应触摸的解释推迟到第 10 章,“响应性设计和可访问性”。在这一章中,我将讨论修改程序以适应不同尺寸的窗口的问题。这可以在桌面上通过改变窗口的宽度和/或高度来检查。

请注意,移动设备上的苹果操作系统可能要求用户点击播放按钮来启动所有视频。这被苹果认为是一个特性,而不是一个 bug。要求点击确实给了设备所有者阻止下载视频的机会,这需要时间和电池电量,并可能产生费用。对于拼图到视频的项目,我更希望它是无缝的,这就是在台式机或笔记本电脑上。该程序在我的 Mac 桌面上使用 Chrome 时确实表现出了无缝的行为,所以第 2 章中讨论的自动播放策略似乎得到了满足。

有了这个可以称为视频奖励拼图游戏项目的介绍,我们可以继续讨论项目的需求和实现。

背景和关键要求

三种截然不同的情况激发我想要建立这个特殊的项目。在我教的一门编程游戏课程中,我用 Adobe Flash 将拼图玩具制作成视频,许多学生很乐意将它们作为自己项目的模型。当我在做一个美国各州教育游戏时,这是第 9 章的主题,我决定用拼图游戏将各州拼在一起,这是对其他问题的一个很好的补充,比如让玩家在整个美国的地图上点击一个州来识别这个州。最后,我经常收到家庭成员的视频,并无耻地将它们融入我的教学实例中。这些情况是创作拼图变成视频项目的动机。

该项目的需求始于创建拼图块的挑战。一种方法是在这个程序之外创建——剪切——基本图片。如果你这样做,你必须记录每个拼图块的相对位置。你可以让碎片比这个例子更不规则。请参阅“如何构建应用程序并使之成为您自己的应用程序”一节。我在这里描述了一种不同的方法。我的程序剪切了基本图片。这些碎片都是同样的长方形。

在进行调整以适应窗口之后,主要的技术需求是构建用户界面。用户界面包括移动单个棋子的鼠标或手指触摸动作,以及重新拼图的按钮和文本字段中提供的反馈。

该程序在屏幕上显示随机放置的棋子。然后玩家移动棋子来构建图像。每次游戏结束后,程序都会进行一次计算,看看谜题是否已经解开。这个计算必须满足两个要求。拼图可以放在屏幕上的任何地方,所以计算必须根据相对位置进行。其次,在片段的定位中需要有一个公差,因为我们不能要求定位是完美的(例如,到像素)。

当拼图完成后,它会变成一个视频。更准确地说,一段视频出现在屏幕上棋子所在的位置。

HTML5、CSS、JavaScript 和编程特性

jigsaw video 项目使用的特性是 HTML5 结构和通用编程技术的混合。

创建基础图片

第一步是从视频的第一帧创建一个图像文件。如何做到这一点取决于您拥有的工具和您觉得使用起来舒服的东西。我在 Mac 上使用了抓取工具。其他可能的方法是按两次 PC Print Screen 键来捕捉屏幕,或者按 Command+Shift+4 在 Mac 上获得十字光标。还有 SnagIt。如果您有视频编辑工具,可以使用该工具访问第一帧。另一种方法是让拼图图片独立于视频。我永远不会想到这一点,但确实有人建议过。

动态创建的元素

在第 2 章中,你读到了家庭剪贴画项目,其中图像被重新放置在画布上。我在这里采用了一种稍微不同的方法。每一块都是自己的画布元素,标记是动态创建的,基本图像的各个部分都绘制在每块画布上。这些片段是在由init函数调用的名为makePieces的函数中创建的。游戏是使用一个叫做setupGame的函数来设置的,这个函数也是从init调用的。事实上,我有三个功能——initmakePiecessetupGame——部分是这个项目历史的产物。我重用了为美国各州游戏创建的代码,拼图只是其中的一部分。然而,将一个函数分解成更小的部分通常是一件好事。init函数做一些工作,调用makePieces,再做一些工作,然后调用setupGamesetupGame功能也从endjigsaw调用,这样玩家可以再次玩游戏。通常,我懒得让一个球员再玩一次,因为这很有挑战性。什么需要重置,什么不需要重置?然而,我决定在这种情况下做出努力。这些部分不会被重新创建,而是再次被随机放置在窗口中。我创建这个应用程序的方式并不是唯一可行的方式。在某些情况下,在这里和其他章节中,我选择编写一个比需要的更通用的函数,而在其他情况下我没有这样做。

基本图像(img)和视频元素都在 HTML 主体中指定。元素中的指令使得这些都不可见。基地img从来没有被公开过,但是它的内容被用来建造棋子。init功能由body标签中的onload属性的动作调用。这意味着在加载基础图像文件和视频文件之前,游戏不会开始。init函数执行一些内务处理任务,获取对元素的引用,并调用makePiecessetupGame函数。

makePieces负责确定适应窗口尺寸所需的调整。然后,它实际上切割拼图块。为了调整片段和视频到不同的窗口保持比例,我需要确定基础图像和窗口的关系。我决定让底部宽度不超过window.innerWidth的 80%,底部高度不超过window.innerHeight的 80%。我也不希望基地在尺寸上增长,如果宽度和高度小于这些数额。以下语句在变量ratio中产生关键因素:

origW = base.width;
origH = base.height;
var ratio =Math.min(1.0,.80*window.innerWidth/origW,.80*window.innerHeight/origH);

你可以这么想:如果origW值大于.80*window.innerWidth,说明我的代码需要缩小图片。在这种情况下,Math.min函数的第二个参数将小于 1。身高也是如此。将最小的因子分配给ratio变量。如果这两个因子都大于 1,那么ratio被设置为 1,片段和视频的大小不会增加。如果这些因子中的一个小于 1,那么ratio将被设置为小于 1。以下语句使用ratio设置关键变量。调用drawImage函数将使用opieceWopieceHpieceWpieceH从原始基础创建拼图块。拼图块和视频可能会改变原始尺寸。

baseImgW = origW*ratio;         //possibly modified
baseImgH = origH*ratio;         //possibly modified
v.width = baseImgW;             //possibly modified video width
v.height =baseImgH;             //possibly modified video height
opieceW = origW/numOfCols;      //width of the source for a jigsaw piece
opieceH = origH/numOfRows;      //height of the source for a jigsaw piece
pieceW = ratio*opieceW;         //jigsaw piece width
pieceH = ratio*opieceH;         //jigsaw piece height

makePieces函数调用drawImage来提取和缩放要绘制的基本图像片段,每个片段都放入它自己新创建的画布元素中。这个操作发生在嵌套的for循环中。基础图像分为numOfRowsnumOfCols。片段,即对画布元素的引用以及 x 值和 y 值分别存储在数组片段piecesxpiecesy中。将drawImage方法想象成执行拼图操作,尽管因为缩放的发生而更加复杂:

for(i=0.0; i<numOfRows; i++) {
  for (j=0.0; j<numOfCols; j++) {

//Some other tasks
sCTX.drawImage(base, j*opieceW, i*opieceH, opieceW, opieceH, 0, 0, pieceW, pieceH);
//Some other tasks

  }
}

基本映像未被更改。sCTX是为每件作品创建的画布的上下文。drawImage功能从j*opieceWi*opieceWopieceW宽、opieceH高开始提取—剪辑—基本图像的一部分。它将这部分图像绘制到 sCTX canvas 元素中,将其缩放为pieceWpieceH。这占据了整个画布。

您可以查看表 8-2 中的完整代码。通过将画布块附加到 body 元素并将样式属性 visibility 设置为visible,画布块变得可见。每个画布元素的addEventListener方法为每个画布设置对mousedown的响应。该代码排列这些片段,使它们类似于原始图片,即视频剪辑的第一帧。然而,setupGame功能很快就会被调用,因此玩家将看不到谜题的解答。在嵌套的for循环之后,执行另一个初始化。firstpkel变量指向新创建的元素,该元素包含第一个片段,即左上角的片段。这是代码用来定位视频剪辑的参考点。将各部分相对于彼此正确定位的计算与第一部分的位置无关。

设置游戏

设置拼图游戏的工作从停止视频并使其不显示开始。这在第一次没有必要,但是让代码总是执行这些操作会更容易。下一个任务是在屏幕上随机放置棋子。代码使用Math.randomMath.floor来完成这项工作。将display属性设置为inline以使片段可见,但不带有换行符,如果代码使用了block,就会出现这种情况。当发生播放视频的情况时,通过将显示设置为none来使所有的片段不可见,所以这段代码是必需的。注意,变量v已经在init函数中被设置为指向video元素。

function setupGame() {
  var i;
  var x;
  var y;
  var thingelem;
  v.pause();
  v.style.display = "none";
  doingjigsaw = true;
  for (i=0;i<nums;i++) {
             x = 10+Math.floor(Math.random()*baseImgW*.9);
             y = 50+Math.floor(Math.random()*baseImgH*.9);
             thingelem = pieces[i];
             thingelem.style.top = String(y)+"px";
             thingelem.style.left = String(x)+"px";
             thingelem.style.visibility='visible';
             thingelem.style.display="inline";
  }
  questionfel.feedback.value = "  ";
}

注意

如果您注意到在处理重玩拼图游戏的问题的编码中出现了一定的复杂性,这是典型的。重启、重新初始化等等比编程让某件事情只发生一次更具挑战性。

处理玩家动作

我的方法是首先实现鼠标事件并让它们工作。然后,当我的野心上升到为使用 iPhones 和 iPads 的某些家庭成员构建一个应用程序时,我通过让触摸事件模拟鼠标事件来实现手指触摸。我在本章解释了鼠标事件,在第 10 章解释了触摸的编码。

使用鼠标事件

移动拼图块的任务是

  • 认识到鼠标按钮是按下的,鼠标是在一块的上面

  • 当鼠标移动时,移动棋子,调整位置以确保棋子不会跳跃,而是保持如同光标附着在它的原始位置一样,可能在元素的中间。

  • 当玩家释放鼠标按钮时,释放或放下元素。

你可能还记得第二章中类似的操作。这个推理表明,我的代码将设置至少三个事件,这就是发生的情况。在makePieces函数中,下面的语句是在嵌套的for循环中执行的,该循环为每一块创建一个画布元素。变量s保存对canvas元素的引用。

s.addEventListener('mousedown',startdragging);

这为每个片段设置了mousedown的事件处理。startdragging函数将名为movingobj的变量设置为事件目标,也就是特定的拼图块。该函数还将全局变量oldxoldy设置为鼠标的位置。该函数为mousemovemouseup设置事件处理。

movingobj.addEventListener("mousemove",moving);
movingobj.addEventListener("mouseup",release);

请注意,当玩家按下鼠标按钮时,如果鼠标不在某个棋子上,则不会发生任何事情,因为“监听”的唯一事件是画布元素上的事件。移动功能是:

function moving(ev)
{
 if((movingobj!=null) &&(mouseDown)){
   newx = parseInt(ev.pageX);
   newy = parseInt(ev.pageY);
   delx = newx-oldx;
   dely = newy-oldy;
   oldx = newx;
   oldy = newy;
   curx = parseInt(movingobj.style.left);
   cury = parseInt(movingobj.style.top);
   movingobj.style.left = String(curx+delx)+"px";
   movingobj.style.top = String(cury+dely)+"px";
 }
};

检查movingobj不为 null 和mouseDown为 true 是多余的,但我决定保留它,以防将来要添加什么。moving功能执行movingobj的相对移动。当鼠标移动时,移动的拼图块水平和垂直移动相同的量。鼠标在画布上的位置无关紧要。无论从上一次mousemove事件发生以来发生了什么变化,都使用相同的变化来调整画布。

当玩家释放鼠标按钮时,调用release功能。我通过设置另一个事件来处理当一个部分在另一个部分之上时调用发布失败的情况:

    document.body.onmouseup = release;

多次调用 release 没有问题。

function release(e){
   mouseDown = false;
   movingobj = e.target;
   movingobj.removeEventListener("mousemove",moving);
   movingobj.removeEventListener("mouseup",release);
   movingobj=null;
   checkpositions();
}

将变量mouseDown改为false意味着如果玩家移动鼠标,直到玩家再次按下鼠标按钮,调用startdragging函数,什么都不会发生。这就完成了鼠标事件的处理。下一节将解释checkpositions功能。

计算拼图是否完成

回想一下,我设置了计算谜题是否完整的要求,即谜题可以位于屏幕上的任何位置,玩家不必精确。另一个或多或少隐含的需求是自动完成检查。玩家释放鼠标或抬起手指后,release函数调用checkpositions。每次移动后都会调用checkpositions函数。别担心,是 JavaScript 在做这项工作,而不是你。

checkpositions函数计算每个块元素的piecesx值和style.left值之间的差值,以及每个块元素的piecesy值和style.top值之间的差值。style.leftstyle.top值是字符串,不是数字,包括"px"。代码需要删除代表“像素”的"px",并计算数值。差异存储在数组deltaxdeltay中。

该函数计算这些差异的平均值(一个用于 x,一个用于 y)。如果拼图完全按照piecesxpiecesy数组中的值放在一起,那么差值都为零,因此,x 和 y 的平均值都为 0。如果将拼图放在一起,使得实际位置每个都更靠近左侧 100 个像素,也就是说,页面更靠左 50 个像素,这是更高的 y 值,那么平均值将是 100 和 50。拼图将被完美地组合在一起,但是位于原始位置的左下方。所有物件的 x 差值为 100,所有物件的 y 差值为 50。每个差异将具有与相应的(x 或 y)平均值相同的值。

目标是而不是要求完美。checkpositions函数的任务是计算 x 和 y 的差值,计算两个平均值,并检查每个差值是否足够接近平均值。

计算完差值后,该函数通过迭代每一部分并与相应的平均值进行比较来执行这些任务。检查是使用绝对值来完成的,因为我们的代码并不关心一块是向左、向右、向上还是向下几个像素。足够接近的标准是保存在变量tolerance中的值。如果任何一块的差距大于tolerance,则认为拼图不完整。关键的if考验是

if ((Math.abs(averagex - deltax[i])>tolerance) || (Math.abs(averagey-deltay[i])>tolerance)) {
         break;
      }

函数计算并返回数组中数字的平均值。这是以通常的方式完成的。变量sum被称为累加器。它被初始化为 0。一个for循环遍历数组中的元素,将每个元素添加到变量sum中。

function doaverage(arr) {
   var sum;
   var i;
   var n = arr.length;
   sum = 0;
   for(i=0;i<n;i++) {
      sum += arr[i];
   }
   return (sum/n);
}

为了以不同的方式总结动作,checkpositions函数使用第一个for循环来确定每个棋子当前水平和垂直位置的差异。然后,它计算两个平均值:x 和 y。然后,该函数使用第二个for循环来查看任何棋子的水平或垂直差异在绝对值上是否与相关平均值有显著差异。一旦发生这种情况,控制就离开for循环,拼图被认为没有完成。如果循环已经完成,则拼图完成,并且视频被定位和播放。checkpositions功能如表 8-2 所示。我选择向玩家显示一条信息,给出对谜题的反馈。表单元素questionfel保存对表单的引用,而feedback是一个输入字段。

我将在下一节描述当谜题被认为完成时会发生什么。

准备、定位和播放视频,并使其隐藏或可见

准备视频剪辑与您在其他涉及视频的项目中看到的一样。您需要创建视频的多种编码。此外,与其他项目一样,当我们不希望视频在特定情况发生之前出现时,style 部分包含使视频最初不可见的指令,将它设置为绝对定位,并(当它显示时)将其放在窗口中与左上角块firstpkel相同的位置。相关代码是

      v.style.left = firstpkel.style.left;
      v.style.top = firstpkel.style.top;
      v.style.display="block";
      v.currentTime = 0;
      v.play();

视频可能会在不同的情况下展示不同的行为。具体来说,在 iPad 或 iPhone 上,播放器可能需要单击一个箭头来播放视频。在我使用 Chrome 或 Firefox 的桌面上(我用的是 iMac ),以及安卓手机上,视频会自动播放,这是我更喜欢的。在第二章中,我讨论了自动播放政策的问题。我没有静音单杠视频。可能是在 Chrome 中对媒体参与指数进行的计算(参见 https://developers.google.com/web/updates/2017/09/autoplay-policy-changes )产生了这些结果。

您已经看到了几个可以使用的 HTML5 特性,以及可以在其他应用程序中使用的编程技巧。下一节将向您展示该项目的大部分代码。整个程序与源代码存储在一起。我包含了同一个应用程序的程序,但是使用了 touch,第 10 章的源代码。

构建应用程序并使之成为您自己的应用程序

您可以使用自己的视频剪辑来制作这些项目。你也可以自己制作一个拼图玩具,不过你也许应该等下一章再看,那一章描述了一个更复杂的拼图玩具,并且包含了如何切割更复杂形状的一些提示。如果这些部分有透明区域,您仍然需要为整个 canvas 元素设置mousedown事件。但是,你需要检查鼠标“下面”的像素是透明的还是不透明的。

拼图游戏的另一种方法是使用一些公差或余量计算来检查一个块是否足够靠近另一个块,然后将它们合在一起。然后,您的代码必须将咬合在一起的片段移动到一起。

您可以决定忽略或更改“继续工作”或“表现良好”的反馈。我的实现在片段的顶部和视频剪辑的下面有“重新拼图”按钮和反馈框。这意味着,如果玩家选择创建谜题,以便隐藏重做按钮,除了重新加载以重新开始之外,没有其他方法。

以下是拼图转视频项目的非正式总结/大纲:

  • init:初始化,包括调用setupGamesetupjigsaw

  • makePieces:用于创作作品。

  • setupGame:随机定位棋子,设置事件处理。

  • endjigsaw:停止视频并使其不显示,然后调用setupGame开始新游戏。

  • startdragging movingrelease:用于处理事件。

  • checkpositions:用于确定拼图是否完成。

  • doaverage:用于计算数组中数值的平均值。

8-1 列出了所有的功能,并指出它们是如何被调用的以及它们调用了什么功能。

表 8-1

拼图转视频项目中的功能

|

功能

|

调用/调用者

|

打电话

| | --- | --- | --- | | init | 由<body>标签中的onLoad属性的动作调用 | makePiecessetupGame | | makePieces | 由init调用 |   | | setupGame | 由initendjigsaw调用 |   | | endjigsaw | 由表单主体中的onSubmit设置调用 | setupGame | | checkpositions | 由release调用 | doaverage | | doaverage | 由checkpositions调用 |   | | startdragging | 由makePieces中的事件设置调用 |   | | moving | 由startdragging中的事件设置调用 |   | | release | 由主体的startdragging for individual piecesmakePieces中的事件设置调用 | checkpositions |

8-2 显示了基本应用程序的代码,每一行都有注释。这些代码中的大部分你已经在前面的章节中看到过了。

表 8-2

完成拼图转视频项目代码

| <!DOCTYPE html> | 页眉 | | <html> | html标签 | | <meta charset="UTF-8"> | 特定字符集 | | <head> | head标签 | | <title>Jigsaw Monkey bars | 选项卡的标题 | | </title> | 结束title标签 | | <style> | 样式标签 | | #base {position:absolute; border:none; visibility: hidden;} | 使基础图像开始隐藏,绝对定位 | | form {position: absolute; z-index: 10;} | 将表单放在各部分的顶部 | | body { height:100%; margin: 30px;} | 正文格式 | | video {display:none; position:absolute; z-index: 100;} | 使视频开始时不显示;棋子上方的位置 | | </style> | 关闭style | | <script type="text/javascript"> | script标签 | | var pieces = []; | 将保持碎片 | | var nums; | 将容纳若干块 | | var baseImgW; | 将保留基础图像的调整宽度 | | var baseImgH; | 将保留基础图像的调整高度 | | var origW; | 基底的原始宽度 | | var origH; | 基底的原始高度 | | var opieceW; | 根据基础图像计算出的一块的宽度 | | var opieceH; | 根据基础图像计算的工件高度 | | var pieceW; | 一块的宽度;缩放操作的结果 | | var pieceH; | 一块的高度;缩放操作的结果 | | var numOfRows = 2.0; | 行数 | | var numOfCols = 3.0; | 列数 | | var piecesx = [ ]; | 将保存所有工件的原始 x 坐标 | | var piecesy = [ ]; | 将保存所有工件的原始 y 坐标 | | var v; | 将保存对视频元素的引用 | | var base; | 将保存对base img的引用 | | var doingjigsaw = false; | 竖锯游戏时的标志 | | var firstpkel; | 第一部分,位于左上角 | | var oldx; | 将保持移动项目的前一个 x 坐标 | | var oldy; | 将保持移动项目的前一个 y 坐标 | | var questionfel; | 引用问题(状态)元素 | | var mouseDown = false; | 表示正在拖动棋子的标志 | | var movingobj; | 将保存对当前移动块的引用 | | function init(){ | init的标题 | | v = document.getElementById("bars"); | 获取视频参考 | | base = document.getElementById("base"); | 获取对基础图像的引用 | | makePieces(); | 调用makePieces | | nums = pieces.length; | 现在已经设置并填充了片段数组,因此可以存储长度 | | questionfel = document.getElementById("questionform"); | 对保存重做按钮和反馈的窗体的显示位置的引用 | | questionfel.style.left = "20px"; | 表单的位置 | | questionfel.style.top = "600px"; | 表单的位置 | | questionfel.submitbut.value = "Do jigsaw again."; | 表单提交按钮上的标签 | | setupGame(); | 调用函数来设置拼图,主要是随机放置棋子 | | } | 关闭init | | function makePieces() { | makePieces功能的标题 | | var i; | 索引变量 | | var x; | 将保持水平坐标 | | var y; | 将保持垂直坐标 | | var s; | 将保存对每个动态创建的画布元素的引用 | | var sCTX ; | 画布元素的上下文 | | origW = base.width; | 基础图像的宽度 | | origH = base.height; | 基础图像的高度 | | var ratio =Math.min(1.0,.80*window.innerWidth/origW,.80*window.innerHeight/origH); | 计算所需的比例因子 | | baseImgW = origW*ratio; | 缩放宽度 | | baseImgH = origH*ratio; | 缩放高度 | | v.width = baseImgW; | 缩放后设置视频宽度 | | v.height =baseImgH; | 缩放后设置视频的高度 | | opieceW = origW/numOfCols; | 计算要在每个画布元素上缩放和绘制的基本图像部分的宽度 | | opieceH = origH/numOfRows; | 计算高度 | | pieceW = ratio*opieceW; | 计算每块画布的实际宽度 | | pieceH = ratio*opieceH; | 计算实际高度 | | for(i=0.0;i<numOfRows;i++) { | for循环遍历各行 | | for (j=0.0;j<numOfCols;j++) { | for在一行内循环,遍历各列 | | s = document.createElement('canvas'); | 创建画布 | | s.width = pieceW; | 设置其宽度 | | s.height = pieceH; | 设置其高度 | | s.style.position = 'absolute'; | 设置位置样式 | | sCTX = s.getContext('2d'); | 设置上下文 | | sCTX.drawImage(base,j*opieceW,i*opieceH,opieceW,opieceH,0,0,pieceW,pieceH); | 从基本图像中剪切出要缩放并绘制到画布上的部分 | | document.body.appendChild(s); | 通过附加到主体上使可见 | | pieces .push(s); | 添加到片段数组 | | x = j*pieceW +100; | 设置临时 x 值;把这想象成名义上正确的 x | | y = i*pieceH +100; | 设置临时 y 值;把这当成名义上正确的 y | | s.style.top = String(y)+"px"; | 设置此画布元素的顶部 | | s.style.left = String(x)+"px"; | 设置此画布元素的左侧 | | piecesx.push(x); | 将值添加到piecesx中,供以后在checkposition中使用 | | piecesy.push(y); | 将值添加到piecesy中,供以后在checkpositions中使用 | | s.addEventListener('mousedown',startdragging); | 为此元素上的mousedown设置事件处理;请注意,这是永远不会删除的 | | s.style.visibility='visible'; | 使可见 | | } | 关闭内部for | | } | 关闭外部for | | firstpkel = pieces[0]; | 参考第一首曲子 | | document.body.onmouseup = release; | 确保在各部分相互重叠的情况下调用释放 | | } | 关闭makePieces | | function endjigsaw() { | endjigsaw 的标题;这是结束,然后重新开始 | | if (doingjigsaw) { | 如果拼图完成了 | | doingjigsaw = false; | 将doingjigsaw设置为false | | v.pause(); | 暂停视频 | | v.style.display = "none"; | 不显示视频 | | } | 如果为真,则关闭子句 | | setupGame(); | 重新开始游戏 | | return false ; | 防止页面刷新 | | } | 关闭endjigsaw | | function checkpositions() { | 标题checkpositions | | var i; | 索引变量,用于for循环 | | var x; | 将保存水平值 | | var y; | 将保存垂直值 | | var tolerance = 10; | 设置用于检查的边距 | | var deltax = []; | 将保持与标称 x 位置的差异 | | var deltay = []; | 将保持与名义 y 位置的差异 | | var delx ; | 用于 x 值的计算 | | var dely; | 用于 y 值的计算 | | for (i=0;i<nums;i++) { | for循环,确定水平和垂直差异 | | x = pieces[i].style.left; | 获取左侧样式 | | y = pieces[i].style.top; | 获得顶级风格 | | x = x.substr(0,x.length-2); | 移除px | | y = y.substr(0,y.length-2); | 移除px | | x = Number(x); | 转换为数字 | | y = Number(y); | 转换为数字 | | delx = x - piecesx[i]; | 计算差异 | | dely = y - piecesy[i]; | 计算差异 | | deltax.push(delx); | 添加到数组 | | deltay.push(dely); | 添加到数组 | | } | 结束for循环 | | var averagex = doaverage(deltax); | 计算平均水平差异 | | var averagey = doaverage(deltay); | 计算平均垂直差 | | for (i=0;i<nums;i++) { | for循环检查是否有任何增量(x 或 y)超过平均值的公差 | | if ((Math.abs(averagex - deltax[i])>tolerance) &#124;&#124; (Math.abs(averagey-deltay[i])>tolerance)) { | 进行比较(取绝对值) | | break; | 如果为真,则退出循环 | | } | 如果为真,则关闭子句 | | } | 关闭for回路 | | if (i<nums) { | 看看最后的我;如果这个小于nums,那么有一个deltaxdeltay大于公差 | | questionfel.feedback.value = "Keep working."; | 显示消息 | | } | 如果为真,则关闭 | | else { | 其他 | | questionfel.feedback.value = "GOOD!"; | 显示消息 | | for (i=0;i<nums;i++) { | for循环使所有的棋子不被显示 | | pieces[i].style.display = "none"; | 不显示 | | } | 关闭for回路 | | v.style.left = firstpkel.style.left; | 水平放置视频 | | v.style.top = firstpkel.style.top; | …垂直方向 | | v.style.display="block"; | 制作视频显示 | | v.currentTime = 0; | 在开始时设置视频 | | v.play(); | 播放视频 | | } | 关闭 else | | } | 关闭功能 | | function doaverage(arr) { | doaverage的标题 | | var sum; | 将保存总和 | | var i; | 索引变量 | | var n = arr.length; | 存储长度 | | sum = 0; | 初始化总和 | | for(i=0;i<n;i++) { | for循环遍历arr中的项目 | | sum += arr[i]; | 添加到总和 | | } | 关闭for | | return (sum/n); | 返回计算的平均值 | | } | 关闭doaverage | | function setupGame() { | setupGame的标题 | | v.pause(); | 暂停视频;如果视频没有播放,没有动作或错误 | | v.style.display = "none"; | 不显示视频 | | doingjigsaw = true; | 将标志设置为真 | | var i; | 索引变量 | | var x; | 将保存水平值 | | var y; | 将保存垂直值 | | var thingelem; | 将保存对棋子的引用(棋子的画布) | | for (i=0;i<nums;i++) { | 循环所有的片段 | | x = 10+Math.floor(Math.random()*baseImgW*.9); | 为水平位置设置随机值 | | y = 50+Math.floor(Math.random()*baseImgH*.9); | 为垂直位置设置随机值 | | thingelem = pieces[i]; | 得到第 I 块 | | thingelem.style.top = String(y)+"px"; | 设置顶部 | | thingelem.style.left = String(x)+"px"; | 设置左侧 | | thingelem.style.visibility='visible'; | 使可见 | | thingelem.style.display="inline"; | 设置显示 | | } | 关闭for回路 | | questionfel.feedback.value = "  "; | 删除任何以前的反馈 | | } | 关闭setupGame功能 | | function release(e){ | 用于发布的标题 | | movingobj = e.target; | 设置为释放件 | | mouseDown = false; | 将标志设置为假 | | movingobj.removeEventListener("mousemove",moving); | 停止监听鼠标移动 | | movingobj.removeEventListener("mouseup",release); | 停止监听鼠标抬起。 | | movingobj=null; | 将此变量设置为 null | | checkpositions(); | 调用checkpositions | | } | 关闭释放功能 | | function startdragging(e)  { | startdragging的标题 | | movingobj = e.target; | 存储事件目标;这是被拖动的棋子 | | mouseDown = true; | 将标志设置为真 | | oldx = parseInt(e.pageX); | 存储鼠标的当前水平值 | | oldy = parseInt(e.pageY); | 存储鼠标的当前垂直值 | | movingobj.addEventListener("mousemove",moving); | 为mousemove设置监听 | | movingobj.addEventListener("mouseup",release); | 为mouseuup设置监听 | | } | 关闭startdragging | | function moving(ev)  { | 移动用割台 | | if((movingobj!=null) &&(mouseDown)){ | 仅当定义了movingobj并且mouseDown为真时才执行任何操作;这是多余的,但是可以简化以后的增强 | | newx = parseInt(ev.pageX); | 提取水平鼠标位置 | | newy = parseInt(ev.pageY); | 提取垂直鼠标位置 | | delx = newx-oldx ; | 计算oldx中存储的最后一个值的变化(增量) | | dely = newy-oldy; | 计算oldy中存储的最后一个值的变化(增量) | | oldx = newx; | 现在,将 newx 存储在oldx中 | | oldy = newy; | …将newy存储在oldy中 | | curx = parseInt(movingobj.style.left); | 从style.left中提取该元素的数字 | | cury = parseInt(movingobj.style.top); | 从style.top中提取该元素的数字 | | movingobj.style.left = String(curx+delx)+"px"; | 使用计算值重置style.left并连接"px" | | movingobj.style.top = String(cury+dely)+"px"; | 使用计算值重置style.top并连接"px" | | } | 关闭if | | } | 关闭移动功能 | | </script> | 结束脚本标签 | | </head> | 结束标题标签 | | <body id="body" onLoad="init();"> | 正文标签;注意onload属性 | | <h2> Monkey bars</h2> | 显示的标题 | | <form id="questionform" name="questionform" onSubmit="return endjigsaw();" > | 提交按钮结束和重新开始以及反馈的表单 | | <input name="submitbut" type="submit" value=" " size="30"/> | 提交按钮 | | Feedback: <input name="feedback" value=" " size="11" /> | 反馈字段 | | </form> | 关闭表单 | | <video id="bars" controls="controls"  preload="auto" width="800"> | 视频元素;放在正文中,以便在采取任何操作之前加载它 | | <source src="monkeybars.mp4" > | 视频类型 | | <source src="monkeybars.webm" > | 视频类型 | | <source src="monkeybars.ogv" > | 视频类型 | | Your browser does not accept the video tag. | 旧浏览器注意事项 | | </video> | 关闭视频元素 | | <img src="barsbase.png" id="base" width="800px" height="420px"/> | img为基础图像;t 从不显示 | | </body> | 关闭body | | </html> | 关闭html |

测试和上传应用程序

测试应用程序要求基础图像的视频文件和图像文件与 HTML 文档位于同一文件夹中。你可以通过改变窗口和重装来测试对不同窗口尺寸的适应性。

摘要

在本章中,您学习了如何构建一个可以变成视频剪辑的拼图玩具。这些技术包括以下内容:

  • 适应不同的屏幕尺寸,同时保持拼图块和视频的比例。

  • 通过动态创建 HTML 元素和设置 HTML 标记来形成拼图块。

  • 为鼠标事件定义事件处理。

  • 在游戏开始时将拼图块随机放在屏幕上,然后随着鼠标的移动移动拼图块。

  • 生成代码来检查拼图是否完整,是否在公差范围内。

  • 在适当的时候,让视频出现并播放。

在下一章中,我们将处理另一个项目,包括一个拼图游戏,以及玩家其他可能的动作。因为像我的 50 个州这样的拼图很有挑战性,所以我解释了一种使用 HTML5 的localStorage特性将拼图存储为正在进行的工作的方法。