七、折纸方向(OrigamiDirection):使用基于数学的线条画、照片和视频
在本章中,您将学习以下内容:
-
如何利用数学编写 JavaScript 函数产生精确的线条画
-
一种将线条画、照片、视频以及用于顺序说明的文本结合起来的方法
-
一种通过让您逐步进行,甚至返回并插入或更改以前的工作来促进开发的方法
介绍
本章的项目是折叠一个折纸模型,一条会说话的鱼的一系列指导。但是,您可以阅读任何主题,只要您想向查看者呈现一系列图表,包括向前和向后移动的能力,以及由线条画或来自文件或视频剪辑的图像组成的图表。
注意
折纸指的是折纸艺术。它通常与日本联系在一起,但也起源于中国和西班牙。传统的褶皱包括水弹、鹤和振翅鸟。莉莲·奥本海默被认为是在美国普及折纸艺术的功臣,并创立了后来成为美国国家组织“折纸美国”的组织。她在 1972 年亲自教我名片蛙。本章的下载中包含了一个用于名片青蛙的 HTML5 程序。折纸是一种活跃在世界各地的艺术形式,也是数学、工程和计算复杂性研究的焦点。
图 7-1 显示了会说话的鱼应用程序origamifish.html
的打开屏幕。屏幕上显示的是折纸图的标准惯例,我修改后加入了颜色。标准的折纸叫做 kami,一面是白色的,另一面是非白色的。
图 7-1
打开屏幕
注意
我减少了折纸动作。例如,我省略了反向折叠的表示,它用于将嘴唇翻过来。这些褶皱之前通常是所谓的预备褶皱,这是我为会说话的鱼描述的。
文件夹可以点击下一步(在序列的这一点上,返回不做任何事情)到达指令的第一个实际步骤,如图 7-2 所示。当然,可以添加编程来删除开始时的返回按钮和结束时的下一步按钮。
图 7-2
第一步,展示纸的正方形。说明书上说要翻纸。
跳过前面,图 7-3 显示了折叠的后续步骤。请注意,纸张的彩色面显示出来了。未折叠的折叠线由细垂直线表示,接下来要进行的折叠(向下折叠拐角)由右上角的彩色虚线对角线表示。
图 7-3
将一个角向下折叠到折叠线
在模型构建的后期,文件夹必须执行汇折叠。这被认为是一个困难的举动。图 7-4 显示了在下沉之前所谓的褶皱模式:褶皱显示为山褶皱或谷褶皱。
图 7-4
水槽标准图步骤
我决定用一个展示水槽步骤的视频剪辑来补充线条画。图 7-5 显示了视频中的一帧。我(文件夹)使用了视频控件来暂停动作。文件夹可以重放视频剪辑,并重复多次回到折痕模式。
图 7-5
显示下沉步骤的暂停视频
下沉仍然是一个挑战,但观看视频剪辑可以有所帮助。文件夹可以重放和暂停视频剪辑。图 7-6 显示了水槽后的下一步。从线条画到视频剪辑再到线条画对用户/文件夹来说很容易,对开发者来说也很简单。
图 7-6
水槽后的步骤(第一个视频剪辑)
下一步要求折叠器向后折叠右边的三角形口盖,分开角度。请注意,角度由一条弧线表示。
在折叠过程中,有一个步骤,我认为一两张照片是传达需要做的事情的最佳方式。图 7-7 显示了从上方(从鱼的喉咙向下看嘴)看到的正在制作的模型的图片。
图 7-7
显示鱼喉咙的照片
图 7-8 显示了按照图 7-7 所示方向将折叠好的材料移动到一边的结果。
图 7-8
喉咙固定的鱼的照片
说明以另一个视频剪辑结束,这个视频剪辑显示了鱼在说话,通过轻轻按下文件夹的顶部和底部来完成。图 7-9 显示了视频中的一帧。
图 7-9
显示会说话的鱼的视频
关键要求
折纸方向有一个标准格式,通常被称为图表,我是在这个标准的基础上建立的。在这种方法中,每一步都显示了下一次使用排版制作的折叠。最基本的褶皱在展开时要么呈山谷状,要么呈山形,这用虚线或点划线表示。通常,折叠是在制作折纸模型的过程中展开的。有时有褶皱的地方用细线表示,有时用虚线表示山谷褶皱,用点划线表示山脉褶皱。
我的目标是绘制线条画,就像在书上看到的那样,计算临界点和线的坐标位置。我不想手工绘制图纸并扫描它们,也不想使用典型的工程 CAD 程序。我不想测量和记录长度或角度,而是让 JavaScript 替我完成这项任务。就像折纸行话所说的那样,这甚至适用于为“品味”而做的折叠,因为我可以确定我选择使用的确切位置。使用基本的代数、几何和三角学提供了一种通过计算线端点的坐标来获得线条画的精确位置的方法。
折纸的步骤通常带有文字说明。此外,有时会使用箭头。我想在遵循标准的同时,利用这些指令将在计算机上传递的优势,并为其他媒体提供颜色和机会。
考虑到会说话的鱼和其他一些褶皱,我决定使用照片和视频来进行线条画可能对你来说不够好的操作。
注意
我给自己设定的折纸图的挑战是既要遵循标准,又要利用 HTML5 的新技术。当转向新的媒体和技术时,这是典型的。你不想放弃一个你的读者可能认为是必要的标准,但是你也想使用那些可以解决实际问题的标准。
一个更微妙的需求是,我希望在开发应用程序时对其进行测试。这意味着一种灵活但健壮的方式来指定步骤。
HTML5、CSS、JavaScript 特性和数学
我现在将描述 HTML5 的特性和用于解决 origami directions 项目需求的编程技术。最好的方法是从表示步骤的整体机制开始,然后解释我是如何导出位置的第一组值的。然后我将解释用于绘制山谷、山脉和箭头,以及用于计算交点和比例的实用函数。最后,我将简要回顾图像的显示和视频的播放。
步骤的总体机制
折纸方向的步骤由一个名为steps
的数组指定。数组的每个元素本身是一个两元素数组,包含一个函数名和一段将出现在屏幕上的文本。origamifish.html
中steps
数组的最终值如下:
var steps= [
[directions,"Diagram conventions"],
[showkami,"Make quarter turn."],
[diamond1,"Fold top point to bottom point."],
[triangleM,"Divide line into thirds and make valley folds and unfold "],
[thirds,"Fold in half to the left."],
[rttriangle,"Fold down the right corner to the fold marking a third. "],
[cornerdown,"Unfold everything."],
[unfolded,"Prepare to sink middle square by reversing folds as indicated ..."],
[changedfolds,"note middle square sides all valley folds, some other folds changed.
Flip over."],
[precollapse,"Push sides to sink middle square."],
[playsink,"Sink square, collapse model."],
[littleguy,"Now fold back the right flap to center valley fold. You are bisecting the
indicated angle."],
[oneflapup,"Do the same thing to the flap on the left"],
[bothflapsup,"Make fins by wrapping top of right flap around 1 layer and left around
back layer"],
[finsp,"Now make lips...make preparation folds"],
[preparelips,"and turn lips inside out. Turn corners in..."],
[showcleftlip,"...making cleft lips."],
[lips,"Pick up fish and look down throat..."],
[showthroat1,"Stick your finger in its mouth and move the inner folded material to one
side"],
[showthroat2,"Throat fixed."],
[rotatefish,"Squeeze & release top and bottom to make fish's mouth close and open"],
[playtalk,"Talking fish."]
];
当我开始构建应用程序时,我没有想到steps
数组。取而代之的是,我在进行的过程中添加了steps
数组,包括插入新条目和更改内容和/或函数名。我从下面的steps
数组的定义开始:
var steps= [
[showkami,"Make quarter turn"],
[diamond,"Fold top point to bottom point."]
];
我花了一些时间进入展示折叠最后阶段的节奏,并为下一步添加了标记。最终结果是一个使用单个 HTML 页面的演示文稿,该页面包含 21 个步骤,包含矢量图、照片和视频,遵循与 PowerPoint 演示文稿类似的格式,即能够前进或后退。
前进和后退由功能donext
和goback
完成。但首先我需要解释整件事是如何开始的。就像到目前为止所有项目的情况一样,名为init
的函数由<body>
标签中的onLoad
属性的动作调用。代码设置全局变量并调用函数来呈现下一步donext
。init
的功能是
function init() {
canvas1 = document.getElementById("canvas");
ctx = canvas1.getContext("2d");
cwidth = canvas1.width;
cheight = canvas1.height;
ta = document.getElementById("directions");
nextstep = 0;
ctx.fillStyle = "white";
ctx.lineWidth = origwidth;
origstyle = ctx.strokeStyle;
ctx.font = "15px Georgia, Times, serif";
donext();
}
变量nextstep
可以说是指向steps
数组的指针。我从零开始。
donext
函数的任务是展示制作折纸模型的步骤中的下一步。该功能从检查它是否在范围内开始;也就是说,如果它已经递增到超过了steps
数组的末尾,则nextstep
的值被设置为最后一个索引。接下来,该功能暂停并删除显示的最后一个视频。它将画布恢复到其最大高度,在播放视频剪辑时,我的代码可能会改变这一高度。该函数还将video
变量设置为undefined
,因此不必为该视频再次执行删除语句。在所有情况下,donext
清除画布并重置linewidth
。然后donext
功能显示下一步。显示器包括多个部分:由线条画、视频或图像组成的图形部分和由说明组成的文本部分。donext
函数调用内部数组第一个(即第 0 个)元素指示的绘图函数:
steps[nextstep][0]();
并使用内部数组的第二个(即第一个)元素显示文本:
ta.innerHTML = steps[nextstep][1];
donext
函数中的最后一条语句是递增指针。整个donext
功能是
function donext() {
if (nextstep>=steps.length) {
nextstep=steps.length-1;
}
if (v) {
v.pause();
v.style.display = "none";
v = undefined;
canvas1.height = 480;
}
ctx.clearRect(0,0,cwidth,cheight);
ctx.lineWidth = origwidth;
steps[nextstep][0]();
ta.innerHTML = steps[nextstep][1];
nextstep++;
}
编写goback
函数所花费的思考时间比它的大小所暗示的要长得多。nextstep
变量保存下一步的索引。这意味着返回需要变量递减 2。必须检查指针是否太低,即小于零。最后,goback
函数调用donext
来显示已经被设置为nextstep
的内容。代码是
function goback() {
nextstep = nextstep -2;
if (nextstep<0) {
nextstep = 0;
}
donext();
}
用户界面
我称之为文件夹的用户有两个按钮,标记为下一步和返回。它们是使用 HTML5 按钮元素实现的,并分别调用goback
和donext
函数。我选择两种不同颜色的按钮——红色代表返回,绿色代表下一步——可以讨论,因为措辞不一致。然而,它确实给了我一个机会来提醒您在名称层叠样式表中层叠这个词的意义。我在head
元素的style
元素中使用了一个指令,然后我还在 body 元素中使用了以下标记:最后一个style
指令是控制按钮并赋予按钮颜色的指令。
<button onClick="goback();" style="color: #F00">Go back </button>
<button onClick="donext();" style="color: #03F">Next step </button>
颜色名称,每个只有三个字符,相当于#FF0000
和#0033FF
。
这两节已经描述了顺序方向的基本机制。它假设每一步都由一个函数和文本来表示。下一节将展示坐标值是如何设置的。
坐标值
线条绘制是使用 HTML5 canvas 函数和变量完成的,主要指示 x 和 y 值。变量在代码中以带有初始化的var
语句出现。我是在一步一步地创建模型的过程中写下这些语句的,尽管就 JavaScript 而言,它们是常量,值是在程序加载时设置的。图 7-10 显示了顺序的第三步,并标注了点 a、b、c 和 d
图 7-10
角落标签
我如何确定这四个点的坐标?作为基础,我指定了点 a 的位置。我还指定了纸张的宽度和高度为四英寸,英寸到像素的转换为 72。变量声明是
var kamiw = 4;
var kamih = 4;
var i2p = 72;
var ax = 10;
var ay = 220;
变量名kamiw
和kamih
是指折纸用的标准方形纸的宽度和高度。从现在开始,一切都是计算好的。所需的第一个值是纸张对角线的大小。对于正方形,使用勾股定理,对角线是边长乘以 2 的平方根。以下设置变量diag
的语句将边(kamiw
)乘以 2 的平方根和表示英寸到像素转换的因子。
var diag = kamiw* Math.sqrt(2.0)*i2p;
大多数其他编程语言包含许多标准数学函数的内置代码,因此程序员不必重新发明轮子。在 JavaScript 中,这些通常作为Math
类的方法提供。你可以在网上搜索以确定确切的名称和用法。
这样,位置 b、c 和 d 的值是使用现有值的表达式。
var bx = ax+ .5*diag;
var by = ay - .5*diag;
var cx = ax + diag;
var cy = ay;
var dx = bx;
var dy = ay + .5*diag;
我通过建立模型并确定新头寸如何基于旧头寸,开发了变量的表达式。这些变量被在steps
数组中指定的函数用来绘制表示模型边缘、折叠线、箭头和角度的线条。一些计算使用了通用的数学公式。接下来的两节介绍了实用函数:阶跃函数使用的函数。
显示器的实用功能
如图 7-1 所示,山谷褶皱由虚线组成。山脉褶皱是由点和虚线组成的线来表示的。这是折纸方向的标准惯例,并使文件夹能够遵循不同语言书籍中的方向。其中一种可以是默认颜色(黑色)或另一种颜色。我需要为基础设置变量:破折号长度、点长度、两个破折号之间的间隔、点之间的间隔以及最后一个点和破折号之间的间隔。通过首先查看函数,然后定义必要的值,最容易理解需要什么。valley
功能定义如下:
function valley(x1,y1,x2,y2,color) {
var px=x2-x1;
var py = y2-y1;
var len = dist(x1,y1,x2,y2);
var nd = Math.floor(len/(dashlen+dgap));
var xs = px/nd;
var ys = py/nd;
if (color) ctx.strokeStyle = color;
ctx.beginPath();
for (var n=0;n<nd;n++) {
ctx.moveTo(x1+n*xs,y1+n*ys);
ctx.lineTo(x1+n*xs+dratio*xs,y1+n*ys+dratio*ys);
}
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = origstyle;
}
valley
功能决定有多少个破折号。这是通过将谷线的长度除以破折号的总长度和破折号之间的间隙来实现的。如果这不是一个整数,则最后的部分破折号组合将被删除。Math.floor
方法为我们完成了这个任务。Math.floor(4.3)
回报 4。
变量xs
和ys
分别是 x 和 y 的增量。color
参数可能存在,也可能不存在。如果参数存在,if (color)
语句改变笔画颜色。该函数的核心是绘制每个破折号的for
循环。
mountain
功能类似,但更复杂,因为山形褶皱排版的性质:破折号的组合后跟一个等于点的间隙,然后是一个点,然后是另一个间隙。mountain
的功能如下:
function mountain(x1,y1,x2,y2,color) {
var px=x2-x1;
var py = y2-y1;
var len = dist(x1,y1,x2,y2);
var nd = Math.floor(len/ddtotal);
var xs = px/nd;
var ys = py/nd;
if (color) ctx.strokeStyle = color;
ctx.beginPath();
for (var n=0;n<nd;n++) {
ctx.moveTo(x1+n*xs,y1+n*ys);
ctx.lineTo(x1+n*xs+ddratio1*xs,y1+n*ys+ddratio1*ys);
ctx.moveTo(x1+n*xs+ddratio2*xs,y1+n*ys+ddratio2*ys);
ctx.lineTo(x1+n*xs+ddratio3*xs,y1+n*ys+ddratio3*ys);
}
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = origstyle;
}
记住函数的语句,下面是我如何定义两个函数使用的变量:
var dashlen = 8;
var dgap = 2.0;
var ddashlen = 6.0;
var ddot = 2.0;
var dratio = dashlen/(dashlen+dgap);
var ddtotal = ddashlen+3*ddot;
var ddratio1 = ddashlen/ddtotal;
var ddratio2 = (ddashlen+ddot)/ddtotal;
var ddratio3 = (ddashlen+2*ddot)/ddtotal;
线条用来显示纸的边缘。我将这些线条的宽度设置为 2。对于纸被折叠然后展开的地方,我使用一条更细的线:线宽设置为 1。我写了一个函数来制作细线:
function skinnyline(x1,y1,x2,y2) {
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.closePath();
ctx.stroke();
ctx.lineWidth = origwidth;
}
在折纸鱼的方向上,我决定使用短的向下的箭头。我为它编写了一个通用函数,您可以在“构建应用程序并使之成为您自己的应用程序”一节中的注释代码中研究这个函数。有两个地方,当我决定显示一个长的弯曲箭头,无论是水平的还是垂直的。这被证明是项目中最长的函数,我在这里就不赘述了。您可以在完整的注释代码清单中研究该函数。用你选择的饮料来增强体力。这是一个复杂的函数,因为许多情况需要分别处理:向上或向下的垂直箭头,或者从左到右或从右到左的水平箭头。箭头是一个圆的圆弧,其圆心被计算为远离该圆弧,两条小直线指示箭头。
用于计算的效用函数
在前面的章节中,你已经看到了这个项目所需的第一个数学计算。它叫做dist
,它计算两点之间的距离:
function dist(x1,y1,x2,y2) {
var x = x2-x1;
var y = y2-y1;
return Math.sqrt(x*x+y*y);
}
下一个要讨论的函数是确定两条线的交点。交点是满足两条直线方程的点。在折纸鱼的例子中,请看图 7-14 。我(在我的程序中)将需要计算从 k 到 n 的直线和从 s 到 q 的直线的交点。在本章中进一步查看图 7-17 。xx
点就是交点。该程序的代码是
var xxa = intersect(sx,sy,qx,qy,kx,ky,nx,ny);
var xxx = xxa[0];
var xxy = xxa[1];
线由两个点定义,每个点由两个数字定义。这意味着intersect
函数有 2 × 2 × 2 个输入参数。我的功能不一般;只有当线不垂直并且确实有交叉点时,它才起作用。这对于我使用折纸鱼来说是可以接受的,但是如果你把它用于另一个应用,你可能需要做更多的工作。
现在让我们来关注线的数学表示。有不同的方程式,但我用的这个叫做点斜率形式。直线的斜率是任意两点之间 y 的变化除以 x 的变化。按照惯例,斜率被命名为 m。斜率为 m 的直线通过点(x1,y1)的公式为
- y–y1 = m *(x–x1)
注意这行是数学,不是 JavaScript。现在回到编程,我确定了传递给intersect
函数的每条线的斜率和方程。
intersect
函数将 m12 设置为从(x1,y1)到(x2,y2)的直线的斜率,将 m34 设置为从(x3,y3)到(x4,y4)的直线的斜率。该代码主要设置两个 y 值:
- y = M12 *(x–x1)+y1,y = m34 *(x–x3)+y3
下一步是将这两个表达式设置为相等,并求解 x。这样做的目的是计算位于两条线上的 x 的值。有了 x 的值,我用两个等式中的一个来得到相应的 y,对 x,y 表示一个点——实际上是唯一的点——在两条线上。这就是所谓的路口。我编写了返回数组[x,y]
的函数代码。以下是完整的代码:
function intersect(x1,y1,x2,y2,x3,y3,x4,y4) {
// only works on line segments that do intersect and
// are not vertical
var m12 = (y2-y1)/(x2-x1);
var m34 = (y4-y3)/(x4-x3);
var m = m34/m12;
var x = (x1-y1/m12-m*x3+y3/m12)/(1-m);
var y = m12*(x-x1)+y1;
return ([x,y]);
}
在这一点上,你可能会突然失去信心,因为屏幕的坐标系统是颠倒的,所以你在高中数学课上记得的东西可能都不适用。垂直值随着屏幕向下移动而增加。事实证明,这些方程仍然有效(尽管我们的解释可能不同)。例如,从(0,0)开始到(100,100)的直线的计算斜率为正 1,尽管我们可能认为它是向下倾斜的。在颠倒的世界里,它有正斜率。
折纸鱼需要的另一个计算是我命名为的比例。这个函数有五个输入参数。(x1,y1)和(x2,y2)定义一条线段。第五个参数是 p,表示比例。该函数的任务是计算从(x1,y1)到(x2,y2)的 p 线段上的(x,y)位置。
function proportion(x1,y1,x2,y2,p) {
var xs = x2-x1;
var ys = y2-y1;
var x = x1+ p*xs;
var y = y1 + p* ys;
return ([x,y]);
}
这涵盖了我所称的折纸项目的效用函数。这三个计算函数将适用于其他应用。
步进线绘制功能
为序列中的一个步骤生成图表的函数使用 HTML5 的路径绘制工具和变量,这些变量已使用计算实用函数或内置的Math
方法进行了设置。在这一节中,我不会一一介绍,但会解释几个。例如,函数triangleM
(下面将详细介绍该函数)的任务是为图 7-11 所示的步骤生成图表。
图 7-11
分成三部分的步骤
注意
我的指示并没有建议这样做的方法。文件夹这样做的一个常见方法是从一端(比如左边)的三分之一处猜测点。将正确的点折叠到那个点,然后轻轻捏一下。然后将左端折叠到捏痕处,重复直到你看不到捏痕的变化。这个方法展示了一些很好的数学,即极限。无论你在最初的猜测中犯了什么样的错误,它都会减少到原来的四分之一。如果你坚持这样做,你很快就会得到可以接受的东西。
图 7-12 显示了标注有临界点 e、f、g 和 h 标签的图片。
图 7-12
将一条线分成三份并折叠
定义这四个点的变量是
var e = proportion(ax,ay,cx,cy,.333333);
var ex = e[0];
var ey = e[1];
var f = proportion(ax,ay,cx,cy,.666666);
var fx = f[0];
var fy = f[1];
var g = proportion(ax,ay,dx,dy,.666666);
var gx = g[0];
var gy = g[1];
var h = proportion(cx,cy,dx,dy,.666666);
var hx = h[0];
var hy = h[1];
功能triangleM
定义如下:
function triangleM() {
triangle();
shortdownarrow(ex,ey);
shortdownarrow(fx,fy);
valley(ex,ey,gx,gy,"orange");
valley(fx,fy,hx,hy,"orange");
}
该函数绘制一个三角形,然后在 e 和 f 上方绘制两个向下的短箭头,然后绘制两条橙色的山谷线。
triangle
功能被定义为
function triangle() {
ctx.fillStyle="teal";
ctx.beginPath();
ctx.moveTo(ax,ay);
ctx.lineTo(cx,cy);
ctx.lineTo(dx,dy);
ctx.lineTo(ax,ay);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
triangle
函数不是一般的,而是画出这个特定的三角形。一般功能是
function generaltriangle(px,py, qx,qy, rx,ry, scolor, fcolor) {
ctx.fillStyle=fcolor;
ctx.strokeStyle = scolor;
ctx.beginPath();
ctx.moveTo(px,py);
ctx.lineTo(qx,qy);
ctx.lineTo(rx,ry);
ctx.lineTo(px,py);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
另外,不要假设我知道如何编写这个函数。我可能将这些代码放入第一个函数中,然后当我进入模型的下一步时,意识到我又需要一个三角形了。我提取了我写的代码,并将第一个函数重命名为triangleM
(表示“三角形标记”)。我让triangleM
函数和thirds
函数分别调用名为triangle
的函数。
图 7-13 显示了模型中的一个步骤,我将用一个我命名为littleguy
的函数来说明,因为在我看来就是这样。
图 7-13
在水槽之后,我称之为小家伙
图 7-14 显示了关键点的标注。
图 7-14
littleguy 临界点的标记
相应变量的定义如下
var kx = ax+diag/3;
var ky = ay;
var lx = kx + diag/3;
var ly = ay;
var mx = ax + diag/6;
var innersq = Math.sqrt(2)*diag/6;
var my = ay + innersq*Math.sin(Math.PI/4);
var nx = ax+diag/3+diag/6;
var ny = my;
var px = mx;
var py = dy;
var rx = nx;
var ry = py;
var qx = kx;
var qy = hy;
var dkq = qy-ky;
var sx = kx + (dkq/Math.cos(Math.PI/8))*Math.sin(Math.PI/8);
var sy = ay;
请注意,我没有试图节省变量。是的,rx
和nx
是同一个值,但是我更容易把它们想成截然不同的东西。
littleguy
的代码如下:
function littleguy() {
ctx.fillStyle="teal";
ctx.beginPath();
ctx.moveTo(ax,ay);
ctx.lineTo(kx,ky);
ctx.lineTo(mx,my);
ctx.lineTo(ax,ay);
ctx.moveTo(kx,ky);
ctx.lineTo(lx,ly);
ctx.lineTo(px,py);
ctx.lineTo(mx,my);
ctx.lineTo(kx,ky);
ctx.moveTo(nx,ny);
ctx.lineTo(rx,ry);
ctx.lineTo(qx,qy);
ctx.lineTo(nx,ny);
ctx.closePath();
ctx.fill();
ctx.stroke();
skinnyline(qx,qy,kx,ky);
ctx.beginPath();
ctx.arc(qx,qy,30,-.5*Math.PI,-.25*Math.PI,false);
ctx.stroke();
mountain(qx,qy,sx,sy,"orange")
}
用度数表示的圆弧是从-90 度到-45 度。请注意,零度是水平方向,正度是顺时针方向。
图 7-15 、 7-16 、 7-17 和 7-18 显示了该模型剩余关键位置的位置。
图 7-18
做好嘴唇后
图 7-17
在绕回步骤之后
图 7-16
准备下沉中心
图 7-15
半步折叠处贴标
使用图来帮助理解设置变量值的代码。比如我在描述intersect
函数的时候提到过,看图 7-14 和 7-17 ,可以看到由 xxx 和 xxy 表示的点 xx 是 s 到 q 和 k 到 n 的直线的交点
还有一个阶跃函数值得解释。结尾前的说明让鱼的头部指向屏幕下方。我想在最后一个视频剪辑之前制作图表,使其水平方向与将要显示的视频剪辑相匹配。这是使用 HTML5 的画布坐标转换完成的。之前的函数命名为lips
。rotatefish
功能保存当前的原始坐标系。然后,它转换为鱼上的一个点,调用旋转(逆时针旋转 90 度),然后撤销转换。然后,rotatefish
函数调用lips
函数,绘制鱼,但现在是水平方向。代码如下:
function rotatefish() {
ctx.save();
ctx.translate(kx,my);
ctx.rotate(-Math.PI/2);
ctx.translate(-kx,-my);
lips();
ctx.restore();
}
展示照片
显示照片的步骤与生成线条画的步骤具有相同的结构。对于应用程序所需的每个图像,我需要定义一个Image
对象,并将src
属性设置为图像文件的名称。以下陈述与图 7-7 中所示的图片相关:
var throat1 = new Image();
throat1.src = "throat1.jpg";
function showthroat1() {
ctx.drawImage(throat1,40,40);
}
第五章中介绍的创建一个定义媒体的独立文件并自动生成代码(包括 HTML 标记)的技术在这里可能是合适的。我为每张照片和每个视频剪辑编写了函数,正如我在下一节中解释的那样。
展示和移除视频
origamifish.html
文件包含两个视频剪辑的视频元素,一个 ID 为sink
,另一个 ID 为talk
。style 元素有一个所有视频都不显示的指令:
video {display: none;}
函数playsink
和playtalk
分别制作视频显示,设置当前时间为零,播放视频,调整画布高度。playsink
的定义如下:
function playsink() {
v = document.getElementById("sink");
v.style.display="block";
v.currentTime = 0;
v.play();
canvas1.height = 178;
}
在讨论了 origami directions 项目要使用的编程技术和 HTML5 特性之后,我们现在可以从整体上来看这个应用程序了。
构建应用程序并使之成为您自己的应用程序
在本章所学的基础上,最快速的方法是为另一个类似折纸的工艺项目创建方向,有线条画和一些照片和视频剪辑的好处。您可以一步一步地构建它,创建您需要的功能。可能原来有些函数就是我所说的效用函数:其他函数使用的函数。您也可以根据需要构建指示定位的变量。下面是对 origami fish 应用程序的非正式总结/概述:
-
init
用于初始化 -
donext
和goback
用于在步骤中前后移动 -
用于绘制特定类型线条的实用函数
-
用于计算的效用函数
-
阶跃函数(
steps
数组中引用的函数)
表 7-1 列出了功能和功能组,并指出它们是如何被调用的以及它们调用了什么功能。
表 7-1
折纸方向项目中的 功能
|功能
|
调用/调用者
|
打电话
|
| — | — | — |
| init
| 由<body>
标签中的onLoad
属性的动作调用 | donext
|
| donext
| 由按钮标签中的init
、goback
和onClick
属性调用 | |
| goback
| 由按钮标签中的onClick
属性调用 | donext
|
| 绘图实用函数(shortdownarrow
、valley
、mountain
、skinnyline
和curvedarrow
) | 由步骤函数调用 | |
| 用于计算的效用函数(dist
、intersect
和proportion
) | 主要在var
语句中调用,以设置代表模型中关键位置的变量 | |
| 阶跃函数 | 作为donext
中的steps
数组中的元素调用;一些(fins
、triangle
、diamond
、rttriangle
、diamondc
和lips
)被其他步骤函数调用 | 实用绘图功能,指示的其他步骤功能 |
表 7-2 显示了基本应用程序的代码,每一行都有注释。这些代码中的大部分你已经在前面的章节中看到过了。
表 7-2
折纸方向项目的完整代码
|代码行
|
描述
|
| — | — |
| <!DOCTYPE html>
| 页眉 |
| <html>
| html
标签 |
| <head>
| head
标签 |
| <title>Origami fish</title>
| 完整标题 |
| <style>
| style
标签 |
| button {font-size:large; font-family:Georgia, "Times New Roman", Times, serif;}
| 格式化按钮的指令;注意,颜色是为body
元素中的每个按钮指定的 |
| #directions {font-family:"Comic Sans MS", cursive;}
| 用于格式化所有方向的指令 |
| video {display:none;}
| 关闭所有视频元素的显示,直到被调用 |
| </style>
| 结束style
标签 |
| <script>
| 开始script
标签 |
| var ctx;
| 将为所有绘图保存画布上下文 |
| var cwidth;
| 画布宽度 |
| var cheight;
| 画布高度 |
| var ta;
| 将保存每个步骤的文本部分的元素 |
| var kamiw = 4;
| 设置纸张宽度 |
| var kamih = 4;
| 设置纸张高度 |
| var i2p = 72;
| 将英寸设置为像素 |
| var dashlen = 8;
| 设置山谷褶皱中的虚线长度 |
| var dgap = 2.0;
| 设置破折号之间的间隙 |
| var ddashlen = 6.0;
| 在山地褶皱中设置虚线长度 |
| var ddot = 2.0;
| 设置山峰褶皱中的点长度 |
| var dratio = dashlen/(dashlen+dgap);
| 用于山区线路 |
| var ddtotal = ddashlen+3*ddot;
| 用于山区线路 |
| var ddratio1 = ddashlen/ddtotal;
| 用于山区线路 |
| var ddratio2 = (ddashlen+ddot)/ddtotal;
| 用于山区线路 |
| var ddratio3 = (ddashlen+2*ddot)/ddtotal;
| 用于山地线;用于计算虚线和点的数量以及虚线和点的起点和范围的所有值 |
| var kamix = 10;
| 第一步中纸张的 x 位置 |
| var kamiy = 10;
| 第一步中纸张的 y 位置 |
| var nextstep;
| 指向steps
数组的指针 |
| function dist(x1,y1,x2,y2) {
| dist
功能的标题 |
| var x = x2-x1;
| 在x
中设置差值 |
| var y = y2-y1;
| 在y
中设置差值 |
| return Math.sqrt(x*x+y*y);
| 返回平方和的平方根 |
| }
| 关闭dist
功能 |
| function intersect(x1,y1,x2,y2,x3,y3,x4,y4) {
| 两行之间的intersect
功能标题,用 2 × 2 点表示 |
| // only works on line segments that do intersection and
| 代码中应该保留的好注释:假设有交集。。。 |
| // are not vertical
| 。。。并假设线条不是垂直的;如果是,代码将被零除,这将产生一个错误 |
| var m12 = (y2-y1)/(x2-x1);
| 计算斜率 |
| var m34 = (y4-y3)/(x4-x3);
| 计算斜率 |
| var m = m34/m12;
| 用于计算 |
| var x = (x1-y1/m12-m*x3+y3/m12)/(1-m);
| 求解x
|
| var y = m12*(x-x1)+y1;
| 求解y
|
| return ([x,y]);
| 返回对 |
| }
| 关闭intersect
功能 |
| function init() {
| init
功能的标题 |
| canvas1 = document.getElementById("canvas");
| 设置canvas1
|
| ctx = canvas1.getContext("2d");
| 设置上下文 |
| cwidth = canvas1.width;
| 设置cwidth
|
| cheight = canvas1.height;
| 设置cheight
|
| ta = document.getElementById("directions");
| 设置ta
为文本方向按住柠檬 |
| nextstep = 0;
| 初始化nextstep
|
| ctx.fillStyle = "white";
| 设置填充样式;将用于擦除 |
| ctx.lineWidth = origwidth;
| 设置线宽(之前设置) |
| origstyle = ctx.strokeStyle;
| 保存笔画颜色 |
| ctx.font = "15px Georgia, Times, serif";
| 设置字体 |
| donext();
| 从第 0 步开始 |
| }
| 关闭init
功能 |
| function directions() {
| 标题为方向,第一个“步骤”显示 |
| ctx.fillStyle = "black";
| 更改填充样式,用于文本 |
| ctx.font = "15px Georgia, Times, serif";
| 设置字体 |
| ctx.fillText("Make valley fold", 10,20);
| 输出说明 |
| valley(200,18,300,18,"orange");
| 制作橙色山谷线样本 |
| ctx.fillText("Make mountain fold",10,50);
| 输出说明 |
| mountain(200,48,300,48,"orange");
| 制作样品橙山线 |
| ctx.fillText("unfolded fold line",10,100);
| 输出说明 |
| skinnyline(200,98,300,98);
| 为展开的折叠线制作样本细线 |
| ctx.fillText("When sense of fold matters:",10,150);
| 输出说明 |
| ctx.fillText("unfolded valley fold", 10,180);
| 继续 |
| valley(200,178,300,178);
| 做样老谷 |
| ctx.fillText("unfolded mountain fold",10,210);
| 输出说明 |
| mountain(200,208,300,208);
| 弄样旧山 |
| ctx.fillStyle = "white";
| 改回填充样式 |
| }
| 关闭方向功能 |
| function donext() {
| donext
功能的标题 |
| if (nextstep>=steps.length) {
| 检查nextstep
是否过大 |
| nextstep=steps.length-1;
| 重置 |
| }
| 关闭条款 |
| if (v) {
| 检查是否设置了v
|
| v.pause();
| 暂停视频 |
| v.style.display = "none";
| 使其不显示 |
| v = undefined;
| 将v
设置为未定义 |
| canvas1.height = 480;
| 恢复高度 |
| }
| 关闭条款 |
| ctx.clearRect(0,0,cwidth,cheight);
| 透明画布 |
| ctx.lineWidth = origwidth;
| 重置线条宽度 |
| steps[nextstep][0]();
| 调用适当的阶跃函数 |
| ta.innerHTML = steps[nextstep][1];
| 显示附带的文本 |
| nextstep++;
| 增量nextstep
|
| }
| 关闭donext
功能 |
| function goback() {
| goback
的标题 |
| nextstep = nextstep -2;
| 将nextstep
减 2(因为它已经领先一位) |
| if (nextstep<0) {
| 检查nextstep
现在是否太低 |
| nextstep = 0;
| 重置 |
| }
| 关闭条款 |
| donext();
| 调用donext
|
| }
| 关闭goback
功能 |
| function shortdownarrow(x,y) {
| 短向下箭头功能的标题 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(x,y-20)
| 移动到(x,y)位置的正上方 |
| ctx.lineTo(x,y-7);
| 在(x,y)的正上方画线 |
| ctx.moveTo(x-5,y-12);
| 向左上方移动 |
| ctx.lineTo(x,y-7);
| 画对角线 |
| ctx.moveTo(x+5,y-12);
| 向右上方移动 |
| ctx.lineTo(x,y-7);
| 画对角线 |
| ctx.closePath();
| 关闭路径 |
| ctx.stroke();
| 画出完整的路径:一个短箭头 |
| }
| 关闭shortdownarrow
功能 |
| function proportion(x1,y1,x2,y2,p) {
| proportion
功能的标题 |
| var xs = x2-x1;
| 在x
中设置差值 |
| var ys = y2-y1;
| 在y
中设置差值 |
| var x = x1+ p*xs;
| 计算新的x
|
| var y = y1 + p* ys;
| 计算新的y
|
| return ([x,y]);
| 返回对 |
| }
| 关闭proportion
功能 |
| function skinnyline(x1,y1,x2,y2) {
| skinnyline
功能的标题 |
| ctx.lineWidth = 1;
| 设置线条宽度 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(x1,y1);
| 移动到开始 |
| ctx.lineTo(x2,y2);
| 要结束的行 |
| ctx.closePath();
| 关闭路径 |
| ctx.stroke();
| 画出笔划 |
| ctx.lineWidth = origwidth;
| 重置线条宽度 |
| }
| 关闭skinnyline
|
| var origstyle;
| 将保持原始颜色 |
| var origwidth = 2;
| 为大多数线条设置线条宽度 |
| function valley(x1,y1,x2,y2,color) {
| valley
功能的标题 |
| var px=x2-x1;
| 在x
中设置差值 |
| var py = y2-y1;
| 在y
中设置差值 |
| var len = dist(x1,y1,x2,y2);
| 确定长度 |
| var nd = Math.floor(len/(dashlen+dgap));
| 多少破折号和缺口 |
| var xs = px/nd;
| 称之为 x 因素 |
| var ys = py/nd;
| 称之为 y 因素 |
| if (color) ctx.strokeStyle = color;
| 如果给定了color
参数,将描边颜色设置为该值 |
| ctx.beginPath();
| 开始路径 |
| for (var n=0;n<nd;n++) {
| 虚线数量的循环 |
| ctx.moveTo(x1+n*xs,y1+n*ys);
| 移动到下一个位置 |
| ctx.lineTo(x1+n*xs+dratio*xs,y1+n*ys+dratio*ys);
| 绘制虚线 |
| }
| 关闭for
回路 |
| ctx.closePath();
| 关闭路径 |
| ctx.stroke();
| 画出路径 |
| ctx.strokeStyle = origstyle;
| 重置笔画样式 |
| }
| 关闭valley
功能 |
| function mountain(x1,y1,x2,y2,color) {
| 山地功能的标题 |
| var px=x2-x1;
| 设置 x 方向的差异 |
| var py = y2-y1;
| 设置 y 方向的差异 |
| var len = dist(x1,y1,x2,y2);
| 确定长度 |
| var nd = Math.floor(len/ddtotal);
| 确定虚线和圆点组合的数量 |
| var xs = px/nd;
| 设置 x 因子 |
| var ys = py/nd;
| 设置 y 因子 |
| if (color) ctx.strokeStyle = color;
| 如果给定了color
参数,将描边颜色设置为该值 |
| ctx.beginPath();
| 开始路径 |
| for (var n=0;n<nd;n++) {
| 组合数循环 |
| ctx.moveTo(x1+n*xs,y1+n*ys);
| 移到下一个 |
| ctx.lineTo(x1+n*xs+ddratio1*xs,y1+n*ys+ddratio1*ys);
| 画破折号 |
| ctx.moveTo(x1+n*xs+ddratio2*xs,y1+n*ys+ddratio2*ys);
| 移动到点的开头 |
| ctx.lineTo(x1+n*xs+ddratio3*xs,y1+n*ys+ ddratio3*ys);
| 画点 |
| }
| 闭环 |
| ctx.closePath();
| 关闭路径 |
| ctx.stroke();
| 画出路径 |
| ctx.strokeStyle = origstyle;
| 重置笔画样式 |
| }
| 关闭mountain
功能 |
| function curvedarrow(x1,y1,x2,y2,px,py){
| 从(x1,y1)到(x2,y2)curvedarrow
的标题偏移(px,py) |
| var arrowanglestart;
| 开始角度 |
| var arrowanglefinish;
| 完成角度 |
| var d = dist(x1,y1,x2,y2);
| 距离 |
| var rad=Math.sqrt(4.25*d*d);
| 值 4.25 是通过实验得出的,以获得箭头的吸引曲线 |
| var ctrx;
| 弯曲箭头的圆弧中心的 x 坐标 |
| var ctry;
| y 坐标 |
| var ex;
| 组成箭头的两条线 |
| var ey;
| 组成箭头的两条线 |
| var angdel = Math.atan2(d/2,2*d);
| 弧的角度 |
| var fromhorizontal;
| 弧开始的角度 |
| ctx.strokeStyle = "red";
| 设置颜色 |
| ctx.beginPath();
| 开始路径 |
| if (y1==y2) {
| 水平箭头案例 |
| arrowanglestart = 1.5*Math.PI-angdel;
| 设置起始角度 |
| arrowanglefinish = 1.5*Math.PI+angdel;
| 设置结束角度 |
| ctrx = .5*(x1+x2) +px;
| 计算中心x
|
| ctry = y1+2*d +py;
| 计算中心y
|
| if (x1<x2) {
| 对于从左向右的箭头 |
| ctx.arc(ctrx,ctry, rad,arrowanglestart,arrowanglefinish, false);
| 画弧线 |
| fromhorizontal=2*Math.PI- arrowanglefinish;
| 用于计算 |
| ex = ctrx+rad*Math.cos(fromhorizontal);
| 设置 x 增量 |
| ey = ctry - rad*Math.sin(fromhorizontal);
| 设置 y 增量 |
| ctx.lineTo(ex-8,ey+8);
| 画第一条小线 |
| ctx.moveTo(ex,ey);
| 移动到另一端 |
| ctx.lineTo(ex-8,ey-8);
| 画直线 |
| }
| 从左到右关闭箭头 |
| else {
| 从右到左 |
| ctx.arc(ctrx,ctry, rad,arrowanglefinish,arrowanglestart,``true);
| 画弧线 |
| fromhorizontal=2*Math.PI- arrowanglestart;
| 计算线条 |
| ex = ctrx+rad*Math.cos(fromhorizontal);
| 为小线条设置 x |
| ey = ctry - rad*Math.sin(fromhorizontal);
| 为小线条设置 y |
| ctx.lineTo(ex+8,ey+8);
| 绘制第一条线 |
| ctx.moveTo(ex,ey);
| 移动到另一行的末尾 |
| ctx.lineTo(ex+8,ey-8);
| 画直线 |
| }
| 结束子句 |
| ctx.stroke();
| 为两种情况画一张图 |
| }
| 结束水平情况 |
| else if (x1==x2) {
| 垂直线 |
| arrowanglestart = -angdel;
| 设置起始角度 |
| arrowanglefinish = angdel;
| 设置完成角度 |
| ctrx = x1-2*d+px;
| 计算中心 x |
| ctry = .5*(y1+y2) + py;
| 计算 y 中心 |
| if (y1<y2) {
| 如果向下箭头 |
| ctx.arc(ctrx,ctry,rad,arrowanglestart,``arrowanglefinish,false);
| 画弧线 |
| fromhorizontal=- arrowanglefinish;
| 用于计算 |
| ex = ctrx+rad*Math.cos(fromhorizontal);
| 计算小线条的 x |
| ey = ctry - rad*Math.sin(fromhorizontal);
| 计算小线条的 y 值 |
| ctx.lineTo(ex-8,ey-8);
| 画第一条小线 |
| ctx.moveTo(ex,ey);
| 移动到结尾 |
| ctx.lineTo(ex+8,ey-8);
| 画第二条小线 |
| }
| 结束向下子句 |
| else {
| 向上子句 |
| ctx.arc(ctrx,ctry, rad,arrowanglefinish,arrowanglestart,``true);
| 画弧线 |
| fromhorizontal=- arrowanglestart;
| 用于计算 |
| ex = ctrx+rad*Math.cos(fromhorizontal);
| 计算小线条的 x |
| ey = ctry - rad*Math.sin(fromhorizontal);
| 计算小线条的 y 值 |
| ctx.lineTo(ex-8,ey+8);
| 画第一条小线 |
| ctx.moveTo(ex,ey);
| 移动到第二行末尾 |
| ctx.lineTo(ex+8,ey+8);
| 画一条线 |
| }
| 结束子句 |
| ctx.stroke();
| 画弧线 |
| }
| 关闭垂直机箱 |
| ctx.strokeStyle = "black";
| 重置颜色 |
| }
| |
| // specific to fish
| 接下来的内容是特定于鱼模型的 |
| var steps= [
| 说明步骤:函数名和附带文本 |
| [directions,"Diagram conventions"],
| |
| [showkami,"Make quarter turn."],
| |
| [diamond1,"Fold top point to bottom point."],
| |
| [triangleM,"Divide line into thirds and make valley folds and unfold "],
| |
| [thirds,"Fold in half to the left."],
| |
| [rttriangle,"Fold down the right corner to the fold marking a third. "],
| |
| [cornerdown,"Unfold everything."],
| |
| [unfolded,"Prepare to sink middle square by reversing folds as indicated ..."],
| |
| [changedfolds,"note middle square sides all valley folds, some other folds changed. Flip over."],
| |
| [precollapse,"Push sides to sink middle square."],
| |
| [playsink,"Sink square, collapse model."],
| |
| [littleguy,"Now fold back the right flap to center valley fold. You are bisecting the indicated angle."],
| |
| [oneflapup,"Do the same thing to the flap on the left"],
| |
| [bothflapsup,"Make fins by wrapping top of right flap around 1 layer and left around back layer"],
| |
| [finsp,"Now make lips...make preparation folds"],
| |
| [preparelips,"and turn lips inside out. Turn corners in..."],
| |
| [showcleftlip,"...making cleft lips."],
| |
| [lips,"Pick up fish and look down throat..."],
| |
| [showthroat1,"Stick your finger in its mouth and move the inner folded material to one side"],
| |
| [showthroat2,"Throat fixed."],
| |
| [rotatefish,"Squeeze & release top and bottom to make fish's mouth close and open"],
| |
| [playtalk,"Talking fish."]
| |
| ];
| |
| var diag = kamiw* Math.sqrt(2.0)*i2p;
| 对角线长度 |
| var ax = 10;
| 为左角设置 x |
| var ay = 220;
| 为左角设置 y |
| var bx = ax+ .5*diag;
| 计算 b(顶角) |
| var by = ay - .5*diag;
| |
| var cx = ax + diag;
| 计算 c(右) |
| var cy = ay;
| |
| var dx = bx;
| 计算 d(底部) |
| var dy = ay + .5*diag;
| |
| var e = proportion(ax,ay,cx,cy,.333333);
| e 至 h 见图 7-12 |
| var ex = e[0];
| |
| var ey = e[1];
| |
| var f = proportion(ax,ay,cx,cy,.666666);
| |
| var fx = f[0];
| |
| var fy = f[1];
| |
| var g = proportion(ax,ay,dx,dy,.666666);
| |
| var gx = g[0];
| |
| var gy = g[1];
| |
| var h = proportion(cx,cy,dx,dy,.666666);
| |
| var hx = h[0];
| |
| var hy = h[1];
| |
| var jx = ax + .5*diag;
| 参见图 7-15 和 7-16 |
| var jy = ay;
| |
| var diag6 = diag/6;
| |
| var gry = ay-(gy-ay);
| |
| var kx = ax+diag/3;
| k 至 s 见图 7-14 |
| var ky = ay;
| |
| var lx = kx + diag/3;
| |
| var ly = ay;
| |
| var mx = ax + diag/6;
| |
| var innersq = Math.sqrt(2)*diag/6;
| |
| var my = ay + innersq*Math.sin(Math.PI/4);
| |
| var nx = ax+diag/3+diag/6;
| |
| var ny = my;
| |
| var px = mx;
| |
| var py = dy;
| |
| var rx = nx;
| |
| var ry = py;
| |
| var qx = kx;
| |
| var qy = hy;
| |
| var dkq = qy-ky;
| |
| var sx = kx + (dkq/Math.cos(Math.PI/8))*Math.sin(Math.PI/8);
| |
| var sy = ay;
| |
| var tx = kx;
| 参见图 7-17 |
| var ty = qy-dist(qx,qy,lx,ly);
| |
| var xxa = intersect(sx,sy,qx,qy,kx,ky,nx,ny);
| |
| var xxx = xxa[0];
| |
| var xxy = xxa[1];
| |
| var xxlx = kx-(xxx-kx);
| |
| var xxly = xxy;
| |
| var slx = kx- (sx-kx);
| |
| var sly = sy;
| |
| var tlx = tx-5;
| |
| var tly = ty;
| |
| var dkt=ky-ty;
| |
| var finlx = kx-dkt;
| 参见图 7-18 |
| var finly = ky;
| |
| var finrx = kx+dkt;
| |
| var finry = ky;
| |
| var w = Math.cos(Math.PI/4)*dkt;
| |
| var wx = kx-.5*dkt;
| |
| var wy = w*Math.sin(Math.PI/4)+ky;
| |
| var zx = kx+.5*dkt;
| |
| var zy = wy;
| |
| var plipx = px;
| |
| var plipy = py-10;
| |
| var rlipx = rx;
| |
| var rlipy = ry-10;
| |
| var plipex = px - 10;
| |
| var plipey = plipy;
| |
| var rlipex = rx + 10;
| |
| var rlipey = rlipy;
| |
| var rclipcleft1 = proportion(rlipex,rlipey,rlipx,rlipy,.5);
| |
| var pclipcleft1 = proportion(plipex,plipey,plipx,plipy,.5);
| |
| var rclipcleft2 = proportion(rlipex,rlipey,qx,qy,.1);
| |
| var pclipcleft2 = proportion(plipex,plipey,qx,qy,.1);
| |
| var rcx1 = rclipcleft1[0];
| |
| var rcy1 = rclipcleft1[1];
| |
| var rcx2 = rclipcleft2[0];
| |
| var rcy2 = rclipcleft2[1];
| |
| var pcx1 = pclipcleft1[0];
| |
| var pcy1 = pclipcleft1[1];
| |
| var pcx2 = pclipcleft2[0];
| |
| var pcy2 = pclipcleft2[1];
| |
| var v;
| 将保存视频元素 |
| var throat1 = new Image();
| 定义Image
对象 |
| throat1.src = "throat1.jpg";
| 设置src
|
| var throat2 = new Image();
| 定义Image
对象 |
| throat2.src = "throat2.jpg"
| 设置src
|
| var cleft = new Image();
| 定义Image
对象 |
| cleft.src="cleftlip.jpg";
| 设置src
|
| function showcleftlip() {
| showcleftlip
的标题 |
| ctx.drawImage(cleft,40,40);
| 绘制图像 |
| }
| 关闭showcleftlip
|
| function showthroat1() {
| showthroat1
的标题 |
| ctx.drawImage(throat1,40,40);
| 绘制图像 |
| }
| 关闭showthroat1
|
| function showthroat2() {
| showthroat2
的标题 |
| ctx.drawImage(throat2,40,40);
| 绘制图像 |
| }
| 关闭showthroat2
|
| function playtalk() {
| playtalk
的标题 |
| v = document.getElementById("talk");
| 设置为谈话视频 |
| v.style.display="block";
| 使可见 |
| v.currentTime = 0;
| 开始时设置 |
| v.play();
| 玩 |
| canvas1.height = 126;
| 调整视频的高度 |
| }
| 关闭playtalk
|
| function playsink() {
| playsink
的标题 |
| v = document.getElementById("sink");
| 设置为水槽视频 |
| v.style.display="block";
| 使可见 |
| v.currentTime = 0;
| 开始时设置 |
| v.play();
| 玩 |
| canvas1.height = 178;
| 调整视频的高度 |
| }
| 关闭playsink
|
| function lips() {
| lips
的标题 |
| ctx.fillStyle = "teal";
| 设置颜色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(finlx,finly);
| 移动到左手手指的左上角 |
| ctx.lineTo(kx,ky);
| 居中绘制 |
| ctx.lineTo(wx,wy);
| 向后向下画 |
| ctx.lineTo(finlx,finly);
| 拉起开始(左角,左鳍) |
| ctx.moveTo(finrx,finry);
| 移至右侧鳍 |
| ctx.lineTo(kx,ky);
| 居中绘制 |
| ctx.lineTo(zx,zy);
| 向下向右画 |
| ctx.lineTo(finrx,finry);
| 画到右角,右鳍 |
| ctx.moveTo(mx,my);
| 移动到 m |
| ctx.lineTo(kx,ky);
| 画到 k |
| ctx.lineTo(xxx,xxy);
| 画到 xx |
| ctx.lineTo(qx,qy);
| 向下画,中心到 q |
| ctx.lineTo(plipx,plipy);
| 向下画,对吗 |
| ctx.lineTo(mx,my);
| 一直画到 m |
| ctx.moveTo(xxx,xxy);
| 移动到 xx |
| ctx.lineTo(nx,ny);
| 向右下画 |
| ctx.lineTo(rlipx,rlipy);
| 提取到 rlip |
| ctx.lineTo(qx,qy);
| 绘制到中心 q |
| ctx.lineTo(xxx,xxy);
| 抽回 xx |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充形状 |
| ctx.stroke();
| 轮廓形状 |
| ctx.fillStyle="white";
| 设置为白色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(qx,qy);
| 从较低的中心开始 |
| ctx.lineTo(pcx2,pcy2);
| 画到嘴唇的左上方 |
| ctx.lineTo(pcx1,pcy1);
| 绘制到左外唇 |
| ctx.lineTo(plipx,plipy);
| 向右轻轻画到底角 plip |
| ctx.lineTo(qx,qy);
| 画回中心 |
| ctx.lineTo(rcx2,rcy2);
| 画到嘴唇的右上方 |
| ctx.lineTo(rcx1,rcy1);
| 向右外唇绘制 |
| ctx.lineTo(rlipx,rlipy);
| 画到底角 rlip |
| ctx.lineTo(qx,qy);
| 画回中心 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充白色形状(两部分) |
| ctx.stroke();
| 轮廓形状 |
| skinnyline(kx,ky,qx,qy);
| 画垂直中心线 |
| ctx.fillStyle="teal";
| 重置为彩色 |
| }
| 紧闭双唇 |
| function rotatefish() {
| rotatefish
的标题 |
| ctx.save();
| 保存当前坐标系 |
| ctx.translate(kx,my);
| 移动到中心点 |
| ctx.rotate(-Math.PI/2);
| 旋转 90 度 |
| ctx.translate(-kx,-my);
| 撤消翻译 |
| lips();
| 画嘴唇(到目前为止的模型) |
| ctx.restore();
| 恢复旧坐标系 |
| }
| 关闭rotatefish
|
| function preparelips() {
| preparelips
的标题 |
| ctx.fillStyle="teal";
| 设置颜色 |
| fins();
| 最多绘制 |
| valley(qx,qy,rlipx,rlipy);
| 标记山谷线 |
| valley(qx,qy,plipx,plipy);
| 标记山谷线 |
| }
| 关闭preparelips
|
| function finsp() {
| finsp
的标题 |
| ctx.fillStyle="teal";
| 设置颜色 |
| fins();
| 最多绘制 |
| valley(qx,qy,rlipx,rlipy,"orange");
| 绘制山谷褶皱 |
| valley(qx,qy,plipx,plipy,"orange");
| 绘制山谷褶皱 |
| }
| 关闭finsp
|
| function fins() {
| 翅片的集管 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(finlx,finly);
| 移动到左鳍 |
| ctx.lineTo(kx,ky);
| 向中心画线 |
| ctx.lineTo(wx,wy);
| 向左下方画线 |
| ctx.lineTo(finlx,finly);
| 向左鳍画 |
| ctx.moveTo(finrx,finry);
| 移至右侧鳍 |
| ctx.lineTo(kx,ky);
| 居中绘制 |
| ctx.lineTo(zx,zy);
| 向右下画 |
| ctx.lineTo(finrx,finry);
| 收回右手指 |
| ctx.moveTo(mx,my);
| 移动到 m(向左和向下) |
| ctx.lineTo(kx,ky);
| 居中绘制 |
| ctx.lineTo(xxx,xxy);
| 画到 xx |
| ctx.lineTo(qx,qy);
| 画到 q |
| ctx.lineTo(px,py);
| 向 p 靠拢 |
| ctx.lineTo(mx,my);
| 向左画到 m |
| ctx.moveTo(xxx,xxy);
| 移动到 xx |
| ctx.lineTo(nx,ny);
| 向右画到 n |
| ctx.lineTo(rx,ry);
| 下降到 r |
| ctx.lineTo(qx,qy);
| 向上画并向左居中 |
| ctx.lineTo(xxx,xxy);
| 画到 xx |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充形状 |
| ctx.stroke();
| 画提纲 |
| skinnyline(kx,ky,qx,qy);
| 画细线条表示中心折叠 |
| }
| 关闭鳍 |
| function bothflapsup () {
| bothflapsup
的标题 |
| ctx.fillStyle="teal";
| 设置颜色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(slx,sly);
| 移动到角落 |
| ctx.lineTo(tlx,tly);
| 将线拉到顶端 |
| ctx.lineTo(kx,ky);
| 向中心画线 |
| ctx.lineTo(xxlx,xxly);
| 向左下方画线 |
| ctx.lineTo(slx,sly);
| 拉回到尖端 |
| ctx.moveTo(mx,my);
| 向下移动(在左边) |
| ctx.lineTo(kx,ky);
| 向中心画线 |
| ctx.lineTo(sx,sy);
| 向右侧绘制 |
| ctx.lineTo(qx,qy);
| 向左下划 |
| ctx.lineTo(px,py);
| 绘制到底部,左顶端 |
| ctx.lineTo(mx,my);
| 起草 |
| ctx.moveTo(tx,ty);
| 起草 |
| ctx.lineTo(sx,sy);
| 向右绘制 |
| ctx.lineTo(kx,ky);
| 居中绘制 |
| ctx.lineTo(tx,ty);
| 起草 |
| ctx.moveTo(xxx,xxy);
| 向右绘制 |
| ctx.lineTo(nx,ny);
| 向右绘制 |
| ctx.lineTo(rx,ry);
| 向下拉到尖端 |
| ctx.lineTo(qx,qy);
| 居中绘制 |
| ctx.lineTo(xxx,xxy);
| 向右后退 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充形状 |
| ctx.stroke();
| 轮廓形状 |
| skinnyline(kx,ky,qx,qy);
| 添加指示折叠的线条 |
| }
| 关闭bothflapsup
|
| function oneflapup() {
| oneflapup
的标题 |
| ctx.fillStyle="teal";
| 设置颜色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ax,ay);
| 移动到左角 |
| ctx.lineTo(kx,ky);
| 画到中间 |
| ctx.lineTo(mx,my);
| 向下并向左画 |
| ctx.lineTo(ax,ay);
| 退回到左角 |
| ctx.moveTo(kx,ky);
| 移到中间 |
| ctx.lineTo(sx,sy);
| 向右绘制 |
| ctx.lineTo(qx,qy);
| 向下画,中间 |
| ctx.lineTo(px,py);
| 向左、向下绘制 |
| ctx.lineTo(mx,my);
| 起草 |
| ctx.lineTo(kx,ky);
| 绘制(回到)中间顶部 |
| ctx.moveTo(xxx,xxy);
| 向右下画 |
| ctx.lineTo(nx,ny);
| 招来 |
| ctx.lineTo(rx,ry);
| 向右下方画 |
| ctx.lineTo(qx,qy);
| 居中绘制 |
| ctx.lineTo(xxx,xxy);
| 向右,向上画 |
| ctx.moveTo(kx,ky);
| 移到中间 |
| ctx.lineTo(tx,ty);
| 绘制到顶部 |
| ctx.lineTo(sx,sy);
| 向下画,对吗 |
| ctx.lineTo(kx,ky);
| 拉到(回到)顶端 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充形状 |
| ctx.stroke();
| 轮廓形状 |
| skinnyline(qx,qy,kx,ky);
| 绘制折叠线 |
| }
| 关闭oneflapup
|
| function littleguy() {
| littleguy
的标题 |
| ctx.fillStyle="teal";
| 设置颜色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ax,ay);
| 移动到左角 |
| ctx.lineTo(kx,ky);
| 居中绘制 |
| ctx.lineTo(mx,my);
| 向左、向下绘制 |
| ctx.lineTo(ax,ay);
| 退到角落 |
| ctx.moveTo(kx,ky);
| 移到中间 |
| ctx.lineTo(lx,ly);
| 画到右角 |
| ctx.lineTo(px,py);
| 向下并向左画 |
| ctx.lineTo(mx,my);
| 起草 |
| ctx.lineTo(kx,ky);
| 画回中心 |
| ctx.moveTo(nx,ny);
| 向右下移动 |
| ctx.lineTo(rx,ry);
| 招来 |
| ctx.lineTo(qx,qy);
| 向下方中央绘制 |
| ctx.lineTo(nx,ny);
| 后退,对吗 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充形状 |
| ctx.stroke();
| 轮廓形状 |
| skinnyline(qx,qy,kx,ky);
| 绘制折叠线 |
| ctx.beginPath();
| 开始路径 |
| ctx.arc(qx,qy,30,-.5*Math.PI,``-.25*Math.PI,false);
| 画弧线来代表角度 |
| ctx.stroke();
| 绘制为笔画 |
| mountain(qx,qy,sx,sy,"orange")
| 指示山脉褶皱 |
| }
| 关闭littleguy
|
| function unfolded() {
| unfolded
的标题 |
| diamond();
| 画菱形 |
| valley(ax,ay,cx,cy);
| 在纸上标出山谷 |
| valley(ex,ey,gx,gy);
| 指示山谷、中间和左侧下方 |
| valley(fx,fy,hx,hy);
| 指示山谷、中间和右侧下方 |
| mountain(ex,ey,gx,gry);
| 指示左边的山、中间和上方 |
| mountain(fx,fy,hx,gry);
| 指示山,中间,向上,对吗 |
| valley(jx,jy,dx,dy);
| 从内部菱形到底部的山谷 |
| mountain(jx,jy,bx,by);
| 从内部菱形到顶部的山 |
| valley(ex,ey,jx,jy+diag6);
| 山谷左侧,内部菱形的上侧 |
| valley(jx,jy-diag6,fx,fy);
| 山谷右侧,内部菱形的下侧 |
| mountain(ex,ey,jx,jy-diag6);
| 山,左,内菱形的下侧 |
| mountain(jx,jy+diag6,fx,fy);
| 内部菱形顶部右侧的山 |
| }
| 关闭unfolded
|
| function precollapse() {
| precollapse
的标题 |
| diamondc();
| 彩色钻石 |
| mountain(ax,ay,cx,cy);
| 纸对面的山 |
| valley(ex,ey,gx,gy);
| 山谷中心,在左边 |
| valley(fx,fy,hx,hy);
| 山谷中心,在右边 |
| valley(ex,ey,gx,gry);
| 山谷中心,在左边 |
| valley(fx,fy,hx,gry);
| 山谷中心,在右边 |
| valley(jx,jy-diag6,jx,jy+diag6);
| 纸张中间的山谷,垂直 |
| mountain(jx,jy-diag6,bx,by);
| 从内部菱形向上的山 |
| mountain(jx,jy+diag6,dx,dy);
| 从内部菱形向下的山 |
| mountain(ex,ey,jx,jy+diag6);
| 山,底部,内部菱形左侧 |
| mountain(jx,jy-diag6,fx,fy);
| 内部菱形右侧顶部的山 |
| mountain(ex,ey,jx,jy-diag6);
| 内部菱形左侧顶部的山 |
| mountain(jx,jy+diag6,fx,fy);
| 山,底部,内部菱形的右侧 |
| }
| 关闭precollapse
|
| function changedfolds() {
| changedfolds
的表头;请注意,这与展开相同,除了一些褶皱的感觉(山与谷) |
| diamond();
| 画菱形 |
| valley(ax,ay,cx,cy);
| 纸谷 |
| mountain(ex,ey,gx,gy);
| 山,在纸的中间,在左边 |
| mountain(fx,fy,hx,hy);
| 山,中间,右下方 |
| mountain(ex,ey,gx,gry);
| 山,中间,在左边 |
| mountain(fx,fy,hx,gry);
| 山,中间,在右边 |
| mountain(jx,jy-diag6,jx,jy+diag6);
| 山,中间,垂直 |
| valley(jx,jy-diag6,bx,by);
| 山谷,内部菱形向上 |
| valley(jx,jy+diag6,dx,dy);
| 山谷,内部菱形向下 |
| valley(ex,ey,jx,jy+diag6);
| 谷,底部,内部菱形的左侧 |
| valley(jx,jy-diag6,fx,fy);
| 山谷,顶部,内部菱形的右侧 |
| valley(ex,ey,jx,jy-diag6);
| 山谷,顶部,内部菱形左侧 |
| valley(jx,jy+diag6,fx,fy);
| 谷,底部,内部菱形的右侧 |
| }
| 关闭changefolds
|
| function triangleM() {
| triangleM
的标题 |
| triangle();
| 画三角形 |
| shortdownarrow(ex,ey);
| 用箭头表示,三分之一点 |
| shortdownarrow(fx,fy);
| 用箭头表示,三分之二点 |
| valley(ex,ey,gx,gy,"orange");
| 下一个山谷褶皱 |
| valley(fx,fy,hx,hy,"orange");
| 下一个山谷褶皱 |
| }
| 关闭triangleM
|
| function thirds() {
| thirds
的标题 |
| triangle();
| 画三角形 |
| skinnyline(ex,ey,gx,gy);
| 指示折叠线 |
| skinnyline(fx,fy,hx,hy);
| 指示折叠线 |
| curvedarrow(cx,cy,ax,ay,0,-20);
| 从右向左绘制曲线,垂直偏移 |
| valley(jx,jy,dx,dy,"orange");
| 绘制(下一条)山谷线 |
| }
| 关闭thirds
|
| function cornerdown() {
| cornerdown
的标题 |
| rttriangle();
| 画直角三角形 |
| ctx.clearRect(ex,ey, diag6+5,diag6);
| 擦除覆盖角的矩形 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ex,ey);
| 移动到开始 |
| ctx.lineTo(ex+diag6,ey+diag6);
| 向右下画 |
| ctx.lineTo(ex,ey+diag6);
| 一直往下画 |
| ctx.lineTo(ex,ey);
| 退回起点 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充三角形 |
| ctx.stroke();
| 轮廓三角形 |
| }
| 关闭cornerdown
|
| function showkami() {
| showkami
的标题 |
| ctx.strokeRect(kamix,kamiy,kamiw*i2p,kamih*i2p);
| 画一个长方形 |
| }
| 关闭showkami
|
| function diamond1() {
| diamond1
的标题 |
| diamond();
| 画菱形 |
| valley(ax,ay,cx,cy,"orange");
| 添加橙谷 |
| curvedarrow(bx,by,dx,dy,10,0);
| 添加垂直弯曲箭头 |
| }
| 关闭diamond1
|
| function diamondc() {
| diamondc
的标题 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ax,ay);
| 移动到左角 |
| ctx.lineTo(bx,by);
| 向右排成一行 |
| ctx.lineTo(cx,cy);
| 向右下方排队 |
| ctx.lineTo(dx,dy);
| 向下排到中间 |
| ctx.lineTo(ax,ay)
| 开始行 |
| ctx.closePath();
| 关闭路径 |
| ctx.fillStyle="teal";
| 设置颜色 |
| ctx.fill();
| 填入菱形 |
| ctx.stroke();
| 画提纲 |
| }
| 关闭diamondc
|
| function diamond() {
| diamond
的标题 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ax,ay);
| 移动到左角 |
| ctx.lineTo(bx,by);
| 向上画一条线 |
| ctx.lineTo(cx,cy);
| 向下并向上画线 |
| ctx.lineTo(dx,dy);
| 向下画线到中心 |
| ctx.lineTo(ax,ay)
| 退回起点 |
| ctx.closePath();
| 关闭路径 |
| ctx.stroke();
| 画提纲 |
| }
| 关闭diamond
|
| function triangle() {
| triangle
功能的标题 |
| ctx.fillStyle="teal";
| 设置为彩色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ax,ay);
| 移动到左角 |
| ctx.lineTo(cx,cy);
| 划线穿过 |
| ctx.lineTo(dx,dy);
| 向下画线 |
| ctx.lineTo(ax,ay);
| 把线拉回来 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填充形状 |
| ctx.stroke();
| 画提纲 |
| }
| 关闭triangle
|
| function rttriangle() {
| rttriangle
的标题 |
| ctx.fillStyle="teal";
| 设置颜色 |
| ctx.beginPath();
| 开始路径 |
| ctx.moveTo(ax,ay);
| 移动到左角 |
| ctx.lineTo(jx,jy);
| 在中间画一条线 |
| ctx.lineTo(dx,dy);
| 向下画线 |
| ctx.lineTo(ax,ay);
| 把线拉回来 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填写直角三角形 |
| valley(ex,ey,ex+diag6,ey+diag6,"orange");
| 画对角线山谷 |
| skinnyline(ex,ey,gx,gy);
| 在 ex,ey 和 gx,gy 之间画一条更窄的线 |
| }
| 关闭rttriangle
|
| </script>
| 结束script
标签 |
| </head>
| 结束head
标签 |
| <body onLoad="init();">
| body
,调用init
|
| <video id="sink" loop="loop" preload="auto" controls="controls" width="400">
| video
标签 |
| <source src="``sink.mp4video.mp4
| MP4 |
| <source src="``sink.theora.ogv
| OGG 型;我只有文件扩展名 |
| <source src="``sink.webmvp8.webm
| WEBM;我只有文件扩展名 |
| Your browser does not accept the video tag.
| 针对旧浏览器的消息 |
| </video>
| 结束video
标签 |
| <video id="talk" loop="loop" preload="auto" controls="controls">
| video
标签 |
| <source src="``talk.mp4video.mp4
| MP4 类型 |
| <source src="``talk.theora.ogv
| OGG 型;请注意,我只有文件扩展名 |
| <source src="``talk.webmvp8.webm
| WEBM 型;请注意,我只有文件扩展名 |
| Your browser does not accept the video tag.
| 针对旧浏览器的消息 |
| </video>
| 结束video
标签 |
| <canvas id="canvas" width="900" height="480">
| 设置画布 |
| Your browser does not recognize the canvas element
| 针对旧浏览器的消息 |
| </canvas>
| 结束canvas
标签 |
| <br/>
| 破裂 |
| <div id="directions"> Press buttons to advance or go back </div>
| 放置方向的地方,带有结束标签div
|
| <hr/>
| 水平规则 |
| <button onClick="goback();" style="color: #F00">Go back </button>
| 设置返回按钮 |
| <button onClick="donext();" style="color: #03F">Next step </button>
| 设置下一步按钮 |
| </body>
| 结束body
标签 |
| </html>
| 结束html
标签 |
你可以将这种方法直接应用于为其他折纸模型或类似的建筑项目准备指导。但是,请更广泛地思考其他主题,在这些主题中,线条画将受益于数学计算,并且线条画、图像和视频可以一起使用。你不必一开始就什么都知道。准备好一步一步地完成这个项目。
测试和上传应用程序
假设你下载了照片和视频剪辑,这个应用程序可以在你自己的电脑上进行全面测试。如果你把它或者你自己的应用程序上传到服务器上,你需要上传 HTML 文件,所有的图像文件和所有的视频文件。请记住,要让一个应用程序在所有浏览器上工作,您可能需要为每个视频提供多种格式。
摘要
在这一章中,你学习了如何构建一个实际的应用程序来展示包含线条画、照片和视频剪辑的方向。编程技术包括以下内容:
-
利用数学(代数、几何和三角学)绘制精确的图形
-
使用数组保存对应于每个步骤的文本和函数名
-
通过使用功能整合照片和视频剪辑
在下一章中,我们将处理另一个整合照片和视频剪辑的项目:构建一个拼图游戏,当玩家将拼图拼在一起时,它会变成一个视频。
八、拼图视频(JigsawVideo)
在本章中,您将学习以下内容:
-
将图像分割成小块以制作拼图玩具的方法
-
如何响应玩家移动棋子来解决难题
-
如何计算水平和垂直坐标,并操作
left
和top
样式属性来重新定位屏幕上的元素 -
关于容差或余量的概念,这样你的玩家就不必完美地解决这个难题
-
如何让完成的拼图看起来变成一个运行的视频
介绍
本章的项目是一个拼图游戏,完成后会变成一个视频。它已经在配有鼠标的电脑上的 Chrome、Firefox、Opera 和 Safari 上进行了测试。每次加载程序或点击按钮重启程序时,拼图块会随机出现在屏幕上。图 8-1 显示了程序在运行 Firefox 浏览器的台式电脑上运行时的打开屏幕。
图 8-1
在计算机上打开屏幕
在电脑上,玩家使用鼠标移动和重新定位棋子。随机放置的碎片可能会叠放在一起。图 8-2 显示了展开的拼图块。我是用鼠标做的。我的例子有六个矩形的部分。
图 8-2
碎片散开
图 8-3 显示了我是如何把拼图拼在一起的。我可以把拼图放在屏幕上的任何地方。拼图的三块已经拼在一起了。
图 8-3
拼图的进展
请注意,带有标签反馈的框告诉继续工作。图 8-4 显示拼图接近完成。
图 8-4
只剩一片可以放进拼图玩具了
当把这些零件放在一起时,程序允许有误差,我称之为公差。你可以注意到白色的缺口,这说明拼图没有拼好。当我移动到最后一个棋子时,图 8-5 显示了我最后一次移动后不久的屏幕截图。
图 8-5
被视频取代的片段
请注意,反馈现在显示为“好!”一个视频已经开始播放,我停止它,并重置到开始,以获得这个截图。这幅画看起来很完美。事实上,这六块拼图已经被视频取代了。图 8-6 显示了控制显示的视频。这些控件不会自动显示,但是如果玩家将鼠标放在视频下部的顶部,就可以看到它们。不同浏览器的视频控制各不相同。
图 8-6
带控件的视频剪辑
我决定接受挑战,让这个项目适用于 iPhone、iPad 和 Android 手机。这意味着构建一个允许玩家使用手指触摸的用户界面。更准确地说,为了展示我的雄心,我想制作一个既能使用鼠标又能触摸的网站程序。我将把如何响应触摸的解释推迟到第十章,“响应性设计和可访问性”。在这一章中,我将讨论修改程序以适应不同尺寸的窗口的问题。这可以在桌面上通过改变窗口的宽度和/或高度来检查。
请注意,移动设备上的苹果操作系统可能要求用户点击播放按钮来启动所有视频。这被苹果认为是一个特性,而不是一个 bug。要求点击确实给了设备所有者阻止下载视频的机会,这需要时间和电池电量,并可能产生费用。对于拼图到视频的项目,我更希望它是无缝的,这就是在台式机或笔记本电脑上。该程序在我的 Mac 桌面上使用 Chrome 时确实表现出了无缝的行为,所以第二章中讨论的自动播放策略似乎得到了满足。
有了这个可以称为视频奖励拼图游戏项目的介绍,我们可以继续讨论项目的需求和实现。
背景和关键要求
三种截然不同的情况激发我想要建立这个特殊的项目。在我教的一门编程游戏课程中,我用 Adobe Flash 将拼图玩具制作成视频,许多学生很乐意将它们作为自己项目的模型。当我在做一个美国各州教育游戏时,这是第九章的主题,我决定用拼图游戏将各州拼在一起,这是对其他问题的一个很好的补充,比如让玩家在整个美国的地图上点击一个州来识别这个州。最后,我经常收到家庭成员的视频,并无耻地将它们融入我的教学实例中。这些情况是创作拼图变成视频项目的动机。
该项目的需求始于创建拼图块的挑战。一种方法是在这个程序之外创建——剪切——基本图片。如果你这样做,你必须记录每个拼图块的相对位置。你可以让碎片比这个例子更不规则。请参阅“如何构建应用程序并使之成为您自己的应用程序”一节。我在这里描述了一种不同的方法。我的程序剪切了基本图片。这些碎片都是同样的长方形。
在进行调整以适应窗口之后,主要的技术需求是构建用户界面。用户界面包括移动单个棋子的鼠标或手指触摸动作,以及重新拼图的按钮和文本字段中提供的反馈。
该程序在屏幕上显示随机放置的棋子。然后玩家移动棋子来构建图像。每次游戏结束后,程序都会进行一次计算,看看谜题是否已经解开。这个计算必须满足两个要求。拼图可以放在屏幕上的任何地方,所以计算必须根据相对位置进行。其次,在片段的定位中需要有一个公差,因为我们不能要求定位是完美的(例如,到像素)。
当拼图完成后,它会变成一个视频。更准确地说,一段视频出现在屏幕上棋子所在的位置。
HTML5、CSS、JavaScript 和编程特性
jigsaw video 项目使用的特性是 HTML5 结构和通用编程技术的混合。
创建基础图片
第一步是从视频的第一帧创建一个图像文件。如何做到这一点取决于您拥有的工具和您觉得使用起来舒服的东西。我在 Mac 上使用了抓取工具。其他可能的方法是按两次 PC Print Screen 键来捕捉屏幕,或者按 Command+Shift+4 在 Mac 上获得十字光标。还有 SnagIt。如果您有视频编辑工具,可以使用该工具访问第一帧。另一种方法是让拼图图片独立于视频。我永远不会想到这一点,但确实有人建议过。
动态创建的元素
在第二章中,你读到了家庭剪贴画项目,其中图像被重新放置在画布上。我在这里采用了一种稍微不同的方法。每一块都是自己的画布元素,标记是动态创建的,基本图像的各个部分都绘制在每块画布上。这些片段是在由init
函数调用的名为makePieces
的函数中创建的。游戏是使用一个叫做setupGame
的函数来设置的,这个函数也是从init
调用的。事实上,我有三个功能——init
、makePieces
和setupGame
——部分是这个项目历史的产物。我重用了为美国各州游戏创建的代码,拼图只是其中的一部分。然而,将一个函数分解成更小的部分通常是一件好事。init
函数做一些工作,调用makePieces
,再做一些工作,然后调用setupGame
。setupGame
功能也从endjigsaw
调用,这样玩家可以再次玩游戏。通常,我懒得让一个球员再玩一次,因为这很有挑战性。什么需要重置,什么不需要重置?然而,我决定在这种情况下做出努力。这些部分不会被重新创建,而是再次被随机放置在窗口中。我创建这个应用程序的方式并不是唯一可行的方式。在某些情况下,在这里和其他章节中,我选择编写一个比需要的更通用的函数,而在其他情况下我没有这样做。
基本图像(img
)和视频元素都在 HTML 主体中指定。元素中的指令使得这些都不可见。基地img
从来没有被公开过,但是它的内容被用来建造棋子。init
功能由body
标签中的onload
属性的动作调用。这意味着在加载基础图像文件和视频文件之前,游戏不会开始。init
函数执行一些内务处理任务,获取对元素的引用,并调用makePieces
和setupGame
函数。
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
函数将使用opieceW
、opieceH
、pieceW
和pieceH
从原始基础创建拼图块。拼图块和视频可能会改变原始尺寸。
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
循环中。基础图像分为numOfRows
和numOfCols
。片段,即对画布元素的引用以及 x 值和 y 值分别存储在数组片段piecesx
和piecesy
中。将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*opieceW
、i*opieceW
、opieceW
宽、opieceH
高开始提取—剪辑—基本图像的一部分。它将这部分图像绘制到 sCTX canvas 元素中,将其缩放为pieceW
和pieceH
。这占据了整个画布。
您可以查看表 8-2 中的完整代码。通过将画布块附加到 body 元素并将样式属性 visibility 设置为visible
,画布块变得可见。每个画布元素的addEventListener
方法为每个画布设置对mousedown
的响应。该代码排列这些片段,使它们类似于原始图片,即视频剪辑的第一帧。然而,setupGame
功能很快就会被调用,因此玩家将看不到谜题的解答。在嵌套的for
循环之后,执行另一个初始化。firstpkel
变量指向新创建的元素,该元素包含第一个片段,即左上角的片段。这是代码用来定位视频剪辑的参考点。将各部分相对于彼此正确定位的计算与第一部分的位置无关。
设置游戏
设置拼图游戏的工作从停止视频并使其不显示开始。这在第一次没有必要,但是让代码总是执行这些操作会更容易。下一个任务是在屏幕上随机放置棋子。代码使用Math.random
和Math.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 的某些家庭成员构建一个应用程序时,我通过让触摸事件模拟鼠标事件来实现手指触摸。我在本章解释了鼠标事件,在第十章解释了触摸的编码。
使用鼠标事件
移动拼图块的任务是
-
认识到鼠标按钮是按下的,鼠标是在一块的上面
-
当鼠标移动时,移动棋子,调整位置以确保棋子不会跳跃,而是保持如同光标附着在它的原始位置一样,可能在元素的中间。
-
当玩家释放鼠标按钮时,释放或放下元素。
你可能还记得第二章中类似的操作。这个推理表明,我的代码将设置至少三个事件,这就是发生的情况。在makePieces
函数中,下面的语句是在嵌套的for
循环中执行的,该循环为每一块创建一个画布元素。变量s
保存对canvas
元素的引用。
s.addEventListener('mousedown',startdragging);
这为每个片段设置了mousedown
的事件处理。startdragging
函数将名为movingobj
的变量设置为事件目标,也就是特定的拼图块。该函数还将全局变量oldx
和oldy
设置为鼠标的位置。该函数为mousemove
和mouseup
设置事件处理。
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.left
和style.top
值是字符串,不是数字,包括"px"
。代码需要删除代表“像素”的"px"
,并计算数值。差异存储在数组deltax
和deltay
中。
该函数计算这些差异的平均值(一个用于 x,一个用于 y)。如果拼图完全按照piecesx
和piecesy
数组中的值放在一起,那么差值都为零,因此,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,第十章的源代码。
构建应用程序并使之成为您自己的应用程序
您可以使用自己的视频剪辑来制作这些项目。你也可以自己制作一个拼图玩具,不过你也许应该等下一章再看,那一章描述了一个更复杂的拼图玩具,并且包含了如何切割更复杂形状的一些提示。如果这些部分有透明区域,您仍然需要为整个 canvas 元素设置mousedown
事件。但是,你需要检查鼠标“下面”的像素是透明的还是不透明的。
拼图游戏的另一种方法是使用一些公差或余量计算来检查一个块是否足够靠近另一个块,然后将它们合在一起。然后,您的代码必须将咬合在一起的片段移动到一起。
您可以决定忽略或更改“继续工作”或“表现良好”的反馈。我的实现在片段的顶部和视频剪辑的下面有“重新拼图”按钮和反馈框。这意味着,如果玩家选择创建谜题,以便隐藏重做按钮,除了重新加载以重新开始之外,没有其他方法。
以下是拼图转视频项目的非正式总结/大纲:
-
init
:初始化,包括调用setupGame
和setupjigsaw
。 -
makePieces
:用于创作作品。 -
setupGame
:随机定位棋子,设置事件处理。 -
endjigsaw:
停止视频并使其不显示,然后调用setupGame
开始新游戏。 -
startdragging
、、moving
、、、release:
用于处理事件。 -
checkpositions
:用于确定拼图是否完成。 -
doaverage
:用于计算数组中数值的平均值。
表 8-1 列出了所有的功能,并指出它们是如何被调用的以及它们调用了什么功能。
表 8-1
拼图转视频项目中的功能
|功能
|
调用/调用者
|
打电话
|
| — | — | — |
| init
| 由<body>
标签中的onLoad
属性的动作调用 | makePieces
,setupGame
|
| makePieces
| 由init
调用 | |
| setupGame
| 由init
和endjigsaw
调用 | |
| endjigsaw
| 由表单主体中的onSubmit
设置调用 | setupGame
|
| checkpositions
| 由release
调用 | doaverage
|
| doaverage
| 由checkpositions
调用 | |
| startdragging
| 由makePieces
中的事件设置调用 | |
| moving
| 由startdragging
中的事件设置调用 | |
| release
| 由主体的startdragging for individual pieces
和makePieces
中的事件设置调用 | checkpositions
|
表 8-2 显示了基本应用程序的代码,每一行都有注释。这些代码中的大部分你已经在前面的章节中看到过了。
表 8-2
完成拼图转视频项目的 代码
| `` | 页眉 | | `` | `html`标签 | | `` | 特定字符集 | | `` | `head`标签 | | `Monkey` `bars
` | 显示的标题 | | `` | 提交按钮结束和重新开始以及反馈的表单 | | `` | 提交按钮 | | `Feedback: ` | 反馈字段 | | `` | 关闭表单 | | `` | 视频元素;放在正文中,以便在采取任何操作之前加载它 | | `` | 视频类型 | | `` | 视频类型 | | `` | 视频类型 | | `Your browser` `does not accept the video tag.` | 旧浏览器注意事项 | | `` | 关闭视频元素 | | `
测试和上传应用程序
测试应用程序要求基础图像的视频文件和图像文件与 HTML 文档位于同一文件夹中。你可以通过改变窗口和重装来测试对不同窗口尺寸的适应性。
摘要
在本章中,您学习了如何构建一个可以变成视频剪辑的拼图玩具。这些技术包括以下内容:
-
适应不同的屏幕尺寸,同时保持拼图块和视频的比例。
-
通过动态创建 HTML 元素和设置 HTML 标记来形成拼图块。
-
为鼠标事件定义事件处理。
-
在游戏开始时将拼图块随机放在屏幕上,然后随着鼠标的移动移动拼图块。
-
生成代码来检查拼图是否完整,是否在公差范围内。
-
在适当的时候,让视频出现并播放。
在下一章中,我们将处理另一个项目,包括一个拼图游戏,以及玩家其他可能的动作。因为像我的 50 个州这样的拼图很有挑战性,所以我解释了一种使用 HTML5 的localStorage
特性将拼图存储为正在进行的工作的方法。