譯者語:我沒有任何技術(shù)背景祟霍,翻譯這篇文章挺費(fèi)勁杏头,Google,維基百科左右相伴沸呐。感謝我的同事醇王,程序宋少和美術(shù)李大師,給予的專業(yè)指導(dǎo)崭添。與大家分享好的技術(shù)文章寓娩,也許翻譯中有不合邏輯的地方,也希望各位IT專家提寶貴修改意見呼渣,以完善棘伴。
Coutre Jour Snot 背后的技術(shù)
作者: Nathan Gonzalez
獲獎(jiǎng)游戲Contre Jour美輪美奐的視覺效果,除了直觀給大家來外美好體驗(yàn)屁置,其獨(dú)特的技術(shù)手法也值得關(guān)注焊夸。正如我一個(gè)同事在其博客中寫到的,游戲圖像的創(chuàng)造是通過OpenGL drawing framework以及利用動(dòng)態(tài)渲染這種較為復(fù)雜的方式來替代簡單繪制一些元素蓝角。游戲中的Snot既是這種制作方法的體現(xiàn)之一淳地。Snots幫助并指引Petit(游戲主人公)完成了它的偉大冒險(xiǎn)旅程〔篮現(xiàn)在請跟我們一起來探知如何在HTML5中制作snot。
神奇的物理
要想了解drawing snots如何工作颇象,需要了解其基本結(jié)構(gòu)伍伤。Contre Jour使用的是2D物理引擎Box2d。我們將不會(huì)在文章中過多的討論snot的物理特征遣钳,但如果你從未使用過物理引擎扰魂,那么先簡單了解一下會(huì)對理解下文有幫助。一名來自Mokus Games的優(yōu)秀程序員蕴茴,將多個(gè)Box2d body通過接點(diǎn)(接點(diǎn)的種類取決于snot的種類)有順序地關(guān)聯(lián)起來劝评,設(shè)計(jì)出snot。每個(gè)body好比一個(gè)合葉倦淀,當(dāng)受到外力時(shí)就會(huì)彎曲蒋畜。因?yàn)槊總€(gè)body是彼此關(guān)聯(lián)的,所以當(dāng)一個(gè)body受力時(shí)撞叽,另外一個(gè)也會(huì)做出相應(yīng)反應(yīng)姻成。對我們來說,每一個(gè)當(dāng)前的body位置都是可以訪問的愿棋,這很重要科展。這些控制點(diǎn)將會(huì)是服務(wù)于接下來要說的繪圖技術(shù)的基礎(chǔ)。
繪制一個(gè)Snot
我們已經(jīng)對snot的原理有了簡單了解糠雨。接下來我們來講如何在Box2d中來完成一個(gè)snot才睹。如下圖所示,snot由兩部分組成甘邀,需要不同的技術(shù)來實(shí)現(xiàn)琅攘。
黃色部分是snot的頭和尾,完全是圖松邪。我們只要確定頭尾的Box2d body位置坞琴,然后畫圖。用HTML 5 canvas 來實(shí)現(xiàn)會(huì)更容易测摔,只需要使用drawImage方法置济。藍(lán)色方框內(nèi)是snot的中間部分,制作起來更復(fù)雜锋八。如果精確的模仿出bodies動(dòng)起來的感覺浙于,靜止圖像(static image)實(shí)現(xiàn)起來夠嗆。取而代之挟纱,游戲開發(fā)者寫了一個(gè)公式羞酗,計(jì)算snot bodies的Bezier曲線的控制點(diǎn);計(jì)算由控制點(diǎn)的中介點(diǎn)組成的數(shù)組紊服;把數(shù)組給到OpenGL檀轨,從而繪制出流暢的曲線胸竞,而不是很多個(gè)三角。這么一看参萄,這似乎不能用javascript實(shí)現(xiàn)卫枝。畢竟我們沒有OpenGL接口。結(jié)論是讹挎,用HTML5 Canvas來制作body確實(shí)比通過最初的OpenGL容易校赤!我們很容易從最初的程序中得到控制點(diǎn)的計(jì)算結(jié)果,通過Canvas的quadraticCurveTo功能畫出二次曲線筒溃,然后根據(jù)結(jié)果填充路徑马篮。代碼和結(jié)果如下:
// variables: ctx = HTML5 context, color = rgba color string,
// pairs: array of control point pairs, with each element containing a
// first and second point that mirror each other along the body.
ctx.fillStyle = color;
ctx.beginPath();
start = pairs[0].first, control, anchor;
ctx.moveTo(start.x, start.y);
for (var i = 1; i < pairs.length; i += 2) {
control = pairs[i].first;
anchor = pairs[i + 1].first;
ctx.quadraticCurveTo(control.x, control.y, anchor.x, anchor.y);
}
ctx.lineTo(pairs[pairs.length - 1].second.x, points[pairs.length -1].second.y);
for (var i = pairs.length - 2; i > 0; i -= 2) {
control = pairs[i].second;
anchor = pairs[i - 1].second;
ctx.quadraticCurveTo(control.x, control.y, anchor.x, anchor.y);
}
ctx.lineTo(start.x, start.y);
ctx.closePath();
ctx.fill();
繪制漸變Snot
相比最初的應(yīng)用,在HTML5中繪制一條顏色一致的snot不是難事怜奖。為“Night“世界(第二章)繪制snots就不一樣了浑测。修改代碼讓snot有陰影漸變,在最初版本中歪玲,簡單地給OpenGL一個(gè)與曲線點(diǎn)數(shù)組一一對應(yīng)的顏色數(shù)組迁央。OpenGL繪制snot頂點(diǎn)時(shí),使用的是存儲(chǔ)在數(shù)組中的顏色读慎,然后給代碼生成的形狀上色就好了漱贱。不幸的是槐雾,在這方面Canvas不能也如此靈活夭委。Canvas能做到的是,用API(Application Programming Interface,應(yīng)用程序編程接口)生成linear gradients(線性漸變)募强,正如你看到的snot body株灸。這并不像我們之前我們做純黑色snot那樣簡單,生成一個(gè)漸變并給snot body填色擎值,就能一下子搞定慌烧。就像名字說得那樣,要逐~漸~變~(譯者:字面表述很奇怪)鸠儿。API需要一個(gè)起點(diǎn)和一個(gè)終點(diǎn)屹蚊,然后在兩點(diǎn)間生成漸變。所以进每,如果我們只是簡單以snot的頭到尾為點(diǎn)生成一個(gè)漸變汹粤,將不會(huì)完全與snot body相吻合,除非這這兩點(diǎn)之間是直線田晚。如果這是一個(gè)可以伸縮的snot嘱兼,問題就來了。不過我們可以通過簡單的方法贤徒,把繪制snot過程分段來解決芹壕。代替一下子畫出整個(gè)snot body路徑的方式汇四,我們將控制點(diǎn)分為很多組,大致對應(yīng)Box2d bodies中每個(gè)段踢涌,然后單獨(dú)完成每個(gè)段通孽,把這個(gè)段的顏色從頭到尾用線性映射出來。
雖然不是100%的準(zhǔn)確睁壁,通過接點(diǎn)關(guān)聯(lián)的bodies之間的行動(dòng)能確保利虫,每個(gè)段在絕大多數(shù)情況下都會(huì)相對線性(除非一個(gè)snot被彎得很扭曲)。即便是“不是100%準(zhǔn)確”和“相對線性”堡僻,聽起來沒有十足把握糠惫,但是這個(gè)技術(shù)的實(shí)驗(yàn)結(jié)果還是非常接近原始效果的。
// The colors parameter is an array of colors that represent
// the color the snot should be at each control point.
var point0, point1, point2, grd, color0, color1;
context.save();
var colorIndex = 0;
// Iterate through all the segments and draw each one individually.
for (var i = 1; i < points.length; i += 2) {
point0 = points[i - 1];
point1 = points[i];
point2 = points[i + 1];
color0 = colors[colorIndex];
color1 = colors[colorIndex + 1];
// In order to hide breaks it helps to draw the segments
// with a slight overlap.
if (i > 1) {
Box2dUtil.b2Vec2Lerp(point0.first, points[i - 2].first, 0.05, point0.first);
Box2dUtil.b2Vec2Lerp(point0.second, points[i - 2].second, 0.05, point0.second);
}
Box2dUtil.getCenter(point0.first, point0.second, DrawUtil.center);
Box2dUtil.getCenter(point2.first, point2.second, DrawUtil.center2);
// Create a gradient that runs from the top to the bottom of the segment
// using the color value array and set the gradient as the fillStyle.
grd = context.createLinearGradient(DrawUtil.center.x, DrawUtil.center.y, DrawUtil.center2.x, DrawUtil.center2.y);
grd.addColorStop(0, color0);
grd.addColorStop(1, color1);
context.fillStyle = grd;
// Fill in the segment.
context.beginPath();
context.moveTo(point0.first.x, point0.first.y);
context.quadraticCurveTo(point1.first.x, point1.first.y, point2.first.x, point2.first.y);
context.lineTo(point2.second.x, point2.second.y);
context.quadraticCurveTo(point1.second.x, point1.second.y,point0.second.x, point0.second.y);
context.lineTo(point0.first.x, point0.first.y);
context.closePath();
context.fill();
colorIndex++;
}
context.restore();
繩子紋理
盡管把OpenGL代碼倒進(jìn)HTML5是一個(gè)困難的任務(wù)钉疫,到目前為止我們還沒有出過問題硼讽。我們想到的解決方法都相對簡而易懂和實(shí)現(xiàn),并與最初的游戲效果基本一致牲阁。遺憾的是固阁,總會(huì)有不按常理出牌的個(gè)體出來給你潑冷水,讓你沮喪城菊。我們的那個(gè)“個(gè)體”就是rope snot备燃。
在iOS版本中,rope body身上的條紋圖案是通過single texture(單材質(zhì)紋理)創(chuàng)造出來:
OpenGL沿著body的長度循環(huán)一個(gè)圖樣凌唬,通過紋理映射(texture mapping)到snot的形狀上并齐。這項(xiàng)技術(shù)保證了段之間的紋理無縫連接。
難題來了客税,如何在HTML5中况褪,既不影響瀏覽器處理速度,又模仿出texture mapping的結(jié)果呢更耻。我們試了很多方法测垛,包括linear gradients, Canvas pattern fills,甚至試圖在Canvas中創(chuàng)造真正意義上的紋理映射秧均。但沒有一種方法能做出原始版本那樣的效果食侮。幸運(yùn)的是,我們又找到另外一個(gè)方法目胡,盡量去靠近紋理映射效果锯七。這算是上面討論的gradient技術(shù)的延伸。首先讶隐,將像night snot那樣將snot body分段起胰。將這些段都切成兩部分。每一部分,我們算一個(gè)變換矩陣效五,以盡可能的符合紋理映射地消。變換矩陣包括每段的頭到尾的角度向量,中心作為紋理的位置畏妖,通過計(jì)算段的平均長度和寬度和紋理的平均長度和寬度的比例脉执,逐個(gè)得出x和y的縮放系數(shù)。然后戒劫,用cavans clip方法確保任何超出段的紋理不會(huì)被畫進(jìn)來半夷。
這個(gè)方法最大的缺點(diǎn)是rope的每個(gè)段之間有非常明顯的斷點(diǎn),外層曲線也會(huì)缺少一定平滑度迅细。
不管怎么說巫橄,在繪制紋理段之前,要確保紋理圖樣的頭和尾保持一致茵典,始終用一種顏色(普通snots的顏色)來繪制整個(gè)body湘换,以縮小斷點(diǎn)。盡管這能有效遮蓋縫隙统阿,但snot的體外有了邊兒彩倚,當(dāng)snot扭曲和轉(zhuǎn)動(dòng)時(shí),這個(gè)邊兒更明顯扶平。不過這個(gè)紋理邊兒的顏色讓snot有了3D效果帆离,也算游戲多了一個(gè)“屬性”吧蝌以。
// backColor is a passed in rgba string that we can use to set the
// context color.
var point0, point1, point2;
// Fill in back with solid color to hide breaks using the method
// described in the solid snot section.
DrawUtil.drawBezier(context, points, backColor);
// Fill the rest of the straight areas with texture.
// This method makes use of a custom Bitmap class to handle drawing
// the texture.
context.save();
for (var i = 1; i < points.length-1; i += 2) {
point0 = points[i - 1];
point1 = points[i];
point2 = points[i + 1];
if (i > 1){
Box2dUtil.b2Vec2Lerp(point0.first, points[i - 2].first, 0.05, point0.first);
Box2dUtil.b2Vec2Lerp(point0.second, points[i - 2].second, 0.05, point0.second);
}
// Draw segment path.
context.beginPath();
context.moveTo(point0.first.x, point0.first.y);
context.quadraticCurveTo(point1.first.x, point1.first.y, point2.first.x, point2.first.y);
context.lineTo(point2.second.x, point2.second.y);
context.quadraticCurveTo(point1.second.x, point1.second.y, point0.second.x, point0.second.y);
context.lineTo(point0.first.x, point0.first.y);
context.closePath();
// Calculate a position, rotation, and scale to map half of the
// texture with the top half of the segment.
context.save();
// Set the texture to draw only the front half.
texture.frame = 0;
Box2dUtil.getCenter(point0.first, point0.second, DrawUtil.center);
Box2dUtil.getCenter(point1.first, point1.second, DrawUtil.center2);
// Calculate the rotation of the texture from the vector between the top and bottom centers of the top half segment.
var direction = Box2dUtil.b2Vec2Subtract(DrawUtil.center2, DrawUtil.center);
var rotation = Box2dUtil.atan2Vec(direction);
// Calculate the y scale by dividing the segment's length by the
// texture's length.
var scaleY = direction.Length() / 16;
// Calculate the x scale by averaging the widths of the top and
// bottom of the segment and then divide that by
// twice the width of the texture (because we are averaging two together).
var point1Width = Box2dUtil.b2Vec2Distance(point1.first, point1.second);
var scaleX =(Box2dUtil.b2Vec2Distance(point0.first, point0.second) + point1Width) / (32 * 2);
// Calculate the center of this half of the segment.
Box2dUtil.getCenter(DrawUtil.center, DrawUtil.center2, DrawUtil.center);
// Clip the context and use a draw method of the Bitmap to draw
// the texture with the calculated values.
context.clip();
texture.drawNoTransform(context, DrawUtil.center.x, DrawUtil.center.y, rotation, scaleX, scaleY);
context.restore();
// Repeat for the bottom part of the segment.
context.save();
texture.frame = 1;
Box2dUtil.getCenter(point2.first, point2.second, DrawUtil.center);
var direction = Box2dUtil.b2Vec2Subtract(DrawUtil.center, DrawUtil.center2);
var rotation = Box2dUtil.atan2Vec(direction);
scaleY = direction.Length() / 16;
scaleX = (point1Width + Box2dUtil.b2Vec2Distance(point2.first, point2.second)) / (32 * 2);
Box2dUtil.getCenter(DrawUtil.center, DrawUtil.center2,DrawUtil.center);
context.clip();
texture.drawNoTransform(context, DrawUtil.center.x, DrawUtil.center.y, rotation, scaleX, scaleY);
context.restore();
}
context.restore();
如你所見琼了,用canvas替代OpenGL用了我們很多想法,但通過積極嘗試并結(jié)合IE10這樣的瀏覽器的處理能力彪标,我們也能創(chuàng)造出與原始應(yīng)用差不多的效果概而,并沒有讓snot的動(dòng)感變得糟糕呼巷。大膽去嘗試HTML5吧囱修,你也可能做出一個(gè)基于瀏覽器的游戲大作赎瑰。
The Tech Behind a Contre Jour Snot
By: Nathan Gonzalez
While the award winning game Contre Jour is amazing on many fronts, its stunning visuals are probably its most unique asset. As detailed in a colleague’s blog post?the game creates its graphics through the OpenGL drawing framework and several elements are dynamically rendered in a more complex manner than by simply drawing images. One of these is the Snot (also known as the Tentacle). They are the most common tools used to help guide Petit through the perils of his epic journey. So sit back and relax as we take a deep dive into how they are constructed as well as how we overcame the pitfalls of translating one into HTML5
The Physics Magic
In order to understand how drawing snots works, it is important to understand the basic structure of one. Please note that Contre Jour uses the 2d physics engine Box2d?for its physics. We aren’t going to go over much of the physics aspect of the snot in this blog, but if you’ve never dealt with a physics engine before a quick overview of the documentation may help you understand the next sentence. The talented programmers at Mokus Games designed the snot out of multiple Box2d bodies which are sequentially connected to each other with joints (with the type of joint depending on the type of snot). You can think of each body as being a hinge that flexes when forces are applied to it. Since each body is connected to one another, forces applied to one body will cause the others to react as well.? What is most important for us, however, is that the current location of each body is accessible. These control points will serve as the basis of all the upcoming drawing techniques.
Drawing a Snot
Now that we have a very basic understanding of the inner workings of a snot, we can take off our Box2d hats and think about how we are going to draw one. As shown in the picture below, there are two distinct sections of the snot that will require us to draw them using a different technique.
snot sections
The yellow sections contain the top and bottom portions of the snot and are comprised completely of images, so all we have to do is get the respective positions of the top and bottom Box2d bodies and draw the images at the appropriate location. This is easy to do in the HTML5 canvas and only requires the use of the drawImage method. The blue section that denotes the middle of the snot, however, is a bit more complex to draw. In order to precisely mimic the look and feel of the movement of the underlying bodies, a static image just won’t cut it. Instead, the game’s creators developed a formula that calculated the control points for a bezier curve from the snot’s bodies, calculated an array of intermediate points from the control points, and then gave the array to OpenGL to draw the smooth curve out of many triangles. At first glance, this may seem an impossible technique to translate to javascript. After all, we have no access to OpenGL. However, using the power of the HTML5, it is actually easier to implement the drawing of the body in the canvas than it is through native OpenGL! We simply took the control point calculation from the original code and drew quadratic curves through the canvas’s quadraticCurveTo function and filled the resulting path. The code and results are shown below:
// variables: ctx = HTML5 context, color = rgba color string,
// pairs: array of control point pairs, with each element containing a
// first and second point that mirror each other along the body.
ctx.fillStyle = color;
ctx.beginPath();
start = pairs[0].first, control, anchor;
ctx.moveTo(start.x, start.y);
for (var i = 1; i < pairs.length; i += 2) {
control = pairs[i].first;
anchor = pairs[i + 1].first;
ctx.quadraticCurveTo(control.x, control.y, anchor.x, anchor.y);
}
ctx.lineTo(pairs[pairs.length - 1].second.x, points[pairs.length -1].second.y);
for (var i = pairs.length - 2; i > 0; i -= 2) {
control = pairs[i].second;
anchor = pairs[i - 1].second;
ctx.quadraticCurveTo(control.x, control.y, anchor.x, anchor.y);
}
ctx.lineTo(start.x, start.y);
ctx.closePath();
ctx.fill();
iOS snot on left, HTML5 snot on right
Drawing a Gradient Snot
While drawing a solid colored snot in HTML5 isn’t a big challenge compared to the native application, drawing the snots for the “Night” world (chapter 2) is a somewhat thornier issue. To modify the code to shade the snot with a gradient, the original version simply gives OpenGL a color array that it calculates based on the number of calculated curve points. When OpenGL draws the snot vertices, it uses the colors stored in the array to color the resulting shapes appropriately. Unfortunately, this is an area where canvas does not have the same flexibility. What canvas does have, however, is an API for creating linear gradients, which is what the snot body looks like. It isn’t as simple as creating a gradient and filling the snot body in one shot as we did with the solid black snot though. As the name implies, a linear gradient is defined as being, well, linear. The API expects a start point and an end point, which it takes and creates a gradient that goes straight between the two points. So if we simply create a gradient that goes from the snot head to its tail, the gradient will not fit correctly on the snot body unless it is in a straight line between the two points. Since a snot is very elastic, this will not always be the case. Luckily for us, simply segmenting how the snot is drawn solves this problem for us. Instead of drawing a path for the whole snot body at once, we break the control points up into groups that roughly correspond to the segments between the Box2d bodies and draw each segment individually and with a gradient that maps to the top and bottom of said segment.
visual representation of the gradient segments
While not 100% accurate, the behaviour of the bodies connected by joints guarantees us that each segment will be fairly linear most of the time (except when a snot is bent at a hard angle). Though using phrases such as “not 100% accurate” and “fairly linear” are hardly confidence inspiring, the results of this technique are very close to the original.
// The colors parameter is an array of colors that represent
// the color the snot should be at each control point.
var point0, point1, point2, grd, color0, color1;
context.save();
var colorIndex = 0;
// Iterate through all the segments and draw each one individually.
for (var i = 1; i < points.length; i += 2) {
point0 = points[i - 1];
point1 = points[i];
point2 = points[i + 1];
color0 = colors[colorIndex];
color1 = colors[colorIndex + 1];
// In order to hide breaks it helps to draw the segments
// with a slight overlap.
if (i > 1) {
Box2dUtil.b2Vec2Lerp(point0.first, points[i - 2].first, 0.05, point0.first);
Box2dUtil.b2Vec2Lerp(point0.second, points[i - 2].second, 0.05, point0.second);
}
Box2dUtil.getCenter(point0.first, point0.second, DrawUtil.center);
Box2dUtil.getCenter(point2.first, point2.second, DrawUtil.center2);
// Create a gradient that runs from the top to the bottom of the segment
// using the color value array and set the gradient as the fillStyle.
grd = context.createLinearGradient(DrawUtil.center.x, DrawUtil.center.y, DrawUtil.center2.x, DrawUtil.center2.y);
grd.addColorStop(0, color0);
grd.addColorStop(1, color1);
context.fillStyle = grd;
// Fill in the segment.
context.beginPath();
context.moveTo(point0.first.x, point0.first.y);
context.quadraticCurveTo(point1.first.x, point1.first.y, point2.first.x, point2.first.y);
context.lineTo(point2.second.x, point2.second.y);
context.quadraticCurveTo(point1.second.x, point1.second.y,point0.second.x, point0.second.y);
context.lineTo(point0.first.x, point0.first.y);
context.closePath();
context.fill();
colorIndex++;
}
context.restore();
iOS snot on left, HTML5 snot on right
Texturing the Rope
Despite the daunting task of porting all the OpenGL code to HTML5, so far we’ve done all right. The solutions we’ve come up with are fairly easy to understand and implement and stay pretty true to the original game. Sadly, there is always that one unruly individual who likes to rain on everyone’s parade and steal all the cookies. In our case this came to us in the form of the rope snot.
In the iOS game, the striped pattern on the rope body is created by using a single texture:
rope texture
The texture is then mapped to the snot body shape by OpenGL using texture mapping in a repeat pattern along the length of the body. This technique ensures that the texture fits perfectly with no overlap or breaks between sections.
Now the million dollar question is how to emulate the results of texture mapping in HTML5 (which has no concept of true texture coordinates) without slowing the browser to a crawl in the process. We tried several techniques to get this to work, including linear gradients, canvas pattern fills and even experimented with trying to create true texture mapping in the canvas. None of these created an effect that looked at all like the original. Luckily, after all these options were exhausted there was one method left to be tried, a rough approximation of texture mapping. It’s really an extension of the gradient technique from the last section. First, the snot body was segmented in the same fashion as the night snot. These sections were then cut into two halves. For each half, we calculated a transform to apply to the texture that mapped as close as possible to the half size. The center of the half was used as the texture’s position, while the rotation was found through calculating the angle of the vector from the top of the segment to the bottom. The x and y scale values were both individually computed by calculating a proportion of the average length and width of the segment with the length and width of the texture. Then, the canvas clip method was used to ensure that any excess texture outside of the segment wouldn’t be drawn.
The big flaw with this method is that without anything else it produces a noticeable disjoint between the individual segments of the rope and the outer curve loses some of its smoothness.
disjointed texture sections
However, this can be minimized by drawing the entire body in with a solid color (like the normal snots) that matches the start and end of the texture pattern before drawing the texture segments. While it effectively hides the seams, it does create a small border on the outside of the snot that becomes more noticeable when the snot does a lot of twisting and turning. It actually creates a sort of 3D effect because of the color of the texture border, so we can chalk this one up as a “feature” and call it a day.
// backColor is a passed in rgba string that we can use to set the
// context color.
var point0, point1, point2;
// Fill in back with solid color to hide breaks using the method
// described in the solid snot section.
DrawUtil.drawBezier(context, points, backColor);
// Fill the rest of the straight areas with texture.
// This method makes use of a custom Bitmap class to handle drawing
// the texture.
context.save();
for (var i = 1; i < points.length-1; i += 2) {
point0 = points[i - 1];
point1 = points[i];
point2 = points[i + 1];
if (i > 1){
Box2dUtil.b2Vec2Lerp(point0.first, points[i - 2].first, 0.05, point0.first);
Box2dUtil.b2Vec2Lerp(point0.second, points[i - 2].second, 0.05, point0.second);
}
// Draw segment path.
context.beginPath();
context.moveTo(point0.first.x, point0.first.y);
context.quadraticCurveTo(point1.first.x, point1.first.y, point2.first.x, point2.first.y);
context.lineTo(point2.second.x, point2.second.y);
context.quadraticCurveTo(point1.second.x, point1.second.y, point0.second.x, point0.second.y);
context.lineTo(point0.first.x, point0.first.y);
context.closePath();
// Calculate a position, rotation, and scale to map half of the
// texture with the top half of the segment.
context.save();
// Set the texture to draw only the front half.
texture.frame = 0;
Box2dUtil.getCenter(point0.first, point0.second, DrawUtil.center);
Box2dUtil.getCenter(point1.first, point1.second, DrawUtil.center2);
// Calculate the rotation of the texture from the vector between the top and bottom centers of the top half segment.
var direction = Box2dUtil.b2Vec2Subtract(DrawUtil.center2, DrawUtil.center);
var rotation = Box2dUtil.atan2Vec(direction);
// Calculate the y scale by dividing the segment's length by the
// texture's length.
var scaleY = direction.Length() / 16;
// Calculate the x scale by averaging the widths of the top and
// bottom of the segment and then divide that by
// twice the width of the texture (because we are averaging two together).
var point1Width = Box2dUtil.b2Vec2Distance(point1.first, point1.second);
var scaleX =(Box2dUtil.b2Vec2Distance(point0.first, point0.second) + point1Width) / (32 * 2);
// Calculate the center of this half of the segment.
Box2dUtil.getCenter(DrawUtil.center, DrawUtil.center2, DrawUtil.center);
// Clip the context and use a draw method of the Bitmap to draw
// the texture with the calculated values.
context.clip();
texture.drawNoTransform(context, DrawUtil.center.x, DrawUtil.center.y, rotation, scaleX, scaleY);
context.restore();
// Repeat for the bottom part of the segment.
context.save();
texture.frame = 1;
Box2dUtil.getCenter(point2.first, point2.second, DrawUtil.center);
var direction = Box2dUtil.b2Vec2Subtract(DrawUtil.center, DrawUtil.center2);
var rotation = Box2dUtil.atan2Vec(direction);
scaleY = direction.Length() / 16;
scaleX = (point1Width + Box2dUtil.b2Vec2Distance(point2.first, point2.second)) / (32 * 2);
Box2dUtil.getCenter(DrawUtil.center, DrawUtil.center2,DrawUtil.center);
context.clip();
texture.drawNoTransform(context, DrawUtil.center.x, DrawUtil.center.y, rotation, scaleX, scaleY);
context.restore();
}
context.restore();
iOS rope on left, HTML5 rope on right
As you can see, it took some creativity to replace OpenGL drawing with the canvas, but with a little elbow grease and the processing power provided by hardware-accelerated browsers such as IE10 we managed to create a pretty consistent look with the native application that managed not to slow it down to a crawl. So I encourage you to go out and experiment with HTML5 yourself. Maybe you’ll accidentally make the next big browser game.