本文翻译自Having fun with Html5 Canvas ,感谢作者
本篇教程我们将会构建一个齿轮系统,用HTML canvas和JavaScript描述出来。
本教程适合刚学习JavaScript以及对齿轮系统只有最基本了解的读者。
第一部分 渲染单个齿轮 这个简单的物理齿轮制作教程 是一个好的开端。根据这个教程,我们的齿轮将由它的半径和齿数来定义。
首先,让我们用齿轮的有关属性来创建一个齿轮类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var Gear = function (x, y, connectionRadius, teeth, fillStyle, strokeStyle ) { this .x = x; this .y = y; this .connectionRadius = connectionRadius; this .teeth = teeth; this .fillStyle = fillStyle; this .strokeStyle = strokeStyle; this .diameter = teeth * 4 * connectionRadius; this .radius = this .diameter / (2 * Math .PI); this .phi0 = 0 ; this .angularSpeed = 0 ; this .createdAt = new Date (); }
接着写渲染的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Gear.prototype.render = function (context ) { var ellapsed = new Date () - this .createdAt; var phiDegrees = this .angularSpeed * (ellapsed / 1000 ); var phi = this .phi0 + deg2rad(phiDegrees); context.fillStyle = this .fillStyle; context.strokeStyle = this .strokeStyle; context.lineCap = 'round' ; context.lineWidth = 1 ; context.beginPath(); for (var i = 0 ; i < this .teeth * 2 ; i++) { var alpha = 2 * Math .PI * (i / (this .teeth * 2 )) + phi; var x = this .x + Math .cos(alpha) * this .radius; var y = this .y + Math .sin(alpha) * this .radius; context.arc(x, y, this .connectionRadius, -Math .PI / 2 + alpha, Math .PI / 2 + alpha, i % 2 == 0 ); } context.fill(); context.stroke(); context.beginPath(); context.arc(this .x, this .y, this .connectionRadius, 0 , 2 * Math .PI, true ); context.fill(); context.stroke(); }
使用方法:
1 2 3 4 5 6 7 8 9 10 var canvas = document .getElementById('myCanvas' );var context = canvas.getContext('2d' );var W = canvas.width;var H = canvas.height;var gear = new Gear(W / 2 , H / 2 , 5 , 12 , "white" , "rgba(61, 142, 198, 1)" );gear.angularSpeed = 36 ; setInterval(function ( ) { canvas.width = canvas.width; gear.render(context); }, 20 );
以及HTML:
1 2 <canvas id ="myCanvas" width ="200" height ="200" > </canvas > <script src ="myScript.js" type ="text/javascript" > </script >
See the Pen PpgQBB by molunerfinn (@molunerfinn ) on CodePen .
第二部分 渲染两个啮合的齿轮 我们的目的是让用户控制第二个齿轮的位置。同时我们想要保证齿轮仍然啮合并且同步转动。
下面是个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Gear.prototype.connect = function (x, y ) { var r = this .radius; var dist = distance(x, y, this .x, this .y); var newRadius = Math .max(dist - r, 10 ); var newDiam = newRadius * 2 * Math .PI; var newTeeth = Math .round(newDiam / (4 * this .connectionRadius)); var newGear = new Gear(x, y, this .connectionRadius, newTeeth, this .fillStyle, this .strokeStyle); var gearRatio = this .teeth / newTeeth; newGear.angularSpeed = -this .angularSpeed * gearRatio; return newGear; }
我们可以把这个挂载在Canvas的mousemove
的事件上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var gear2 = gear.connect(3 * (W / 4 ), H / 2 );function getMousePos (canvas, evnt ) { var rect = canvas.getBoundingClientRect(); return { x: evnt.clientX - rect.left, y: evnt.clientY - rect.top }; } canvas.onmousemove = function (evnt ) { var pos = getMousePos(canvas, evnt); var x = Math .min(0.7 * W, Math .max(0.3 * W, pos.x)); var y = Math .min(0.7 * H, Math .max(0.3 * H, pos.y)); gear2 = gear.connect(x, y); } setInterval(function ( ) { canvas.width = canvas.width; gear.render(context); gear2.render(context); }, 20 );
See the Pen Mpdvwo by molunerfinn (@molunerfinn ) on CodePen .
上面的步骤可以实现正确的旋转方向,但是并不是那么准确——齿轮没有啮合,并且每个齿轮只是在做自己的运动。
以下是我们必须解决的两个问题:
gear2.radius
(齿轮2的理论半径)与我们计算的newRadius
(实际半径)不一致。这是因为我们不得不保留齿数的缘故。齿轮2并没有和齿轮1同步转动。 要解决第一个问题,我们必须让我们的新齿轮改变它的实际位置以确保在拥有它所需要的齿数的同时还能和第一个齿轮啮合。
(下面是对于问题1的解决方案)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Gear.prototype.connect = function (x, y ) { var r = this .radius; var dist = distance(x, y, this .x, this .y); var newRadius = Math .max(dist - r, 10 ); var newDiam = newRadius * 2 * Math .PI; var newTeeth = Math .round(newDiam / (4 * this .connectionRadius)); var actualDiameter = newTeeth * 4 * this .connectionRadius; var actualRadius = actualDiameter / (2 * Math .PI); var actualDist = r + actualRadius; var alpha = Math .atan2(y - this .y, x - this .x); var actualX = this .x + Math .cos(alpha) * actualDist; var actualY = this .y + Math .sin(alpha) * actualDist; var newGear = new Gear(actualX, actualY, this .connectionRadius, newTeeth, this .fillStyle, this .strokeStyle); var gearRatio = this .teeth / newTeeth; newGear.angularSpeed = -this .angularSpeed * gearRatio; return newGear; }
See the Pen Gear-2-2 by molunerfinn (@molunerfinn ) on CodePen .
上面的调整单独处理了一个令人讨厌的问题,我们可以在一个齿轮上放置另一个齿轮了。但是这还不足以同步两个齿轮的旋转。
现在我们必须在连接点使齿轮互相啮合:
1 2 3 4 5 this .phi0 = alpha; newGear.phi0 = alpha + Math .PI + (Math .PI / newTeeth); newGear.createdAt = this .createdAt;
上面的做法是有效的。然而,这种方法的缺点是我们在一直改变this.phi
,这意味着以前与之同步的任何其他齿轮将不再同步。
每次this.phi
都被一些delta
更新,那么newGear.phi
应该被更新多少?答案是delta * (newGear.angularSpeed / this.angularSpeed)
,因为要考虑齿轮转速之比。了解到这个之后,我们可以把两个齿轮都更新一下:delta = (this.phi0 - alpha)
,以消除这种影响:
1 2 3 4 5 6 this .phi0 = alpha + (this .phi0 - alpha); newGear.phi0 = alpha + Math .PI + (Math .PI / newTeeth) + (this .phi0 - alpha) * (newGear.angularSpeed / this .angularSpeed); newGear.createdAt = this .createdAt;
通过这样的处理,this.phi0
保持不变,不过另一个齿轮却得以同步:
See the Pen Gear-2-3 by molunerfinn (@molunerfinn ) on CodePen .
第三部分 3D齿轮 在上一个部分,我们已经展示了如何通过HTML5 Canvas渲染出啮合的齿轮对。本部分,我们将会探索通过使用Three.js这个库,渲染出完全3D的JavaScript齿轮,同时也会进一步扩大视图的复杂性。
3D渲染 Three.js是一个用于3D图形渲染的JavaScript库,是基于WebGL的。它是免费,开放,并且不断更新的。我们将从移植我们的渲染代码开始,首先试着描绘出2D平面里的齿轮轮廓。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var shape = new THREE.Shape();var x0 = this .radius;var y0 = 0 ;shape.moveTo(x0, y0); for (var i = 0 ; i < this .legs * 2 ; i++) { var alpha = 2 * Math .PI * (i / (this .legs * 2 )) + this .phi; var x = Math .cos(alpha) * this .radius; var y = Math .sin(alpha) * this .radius; createArc(shape, x, y, this .connectionRadius, -Math .PI / 2 + alpha, Math .PI / 2 + alpha, i % 2 == 0 , 3 ); } return shape;
我用了一个辅助函数把弧形段分割成离散的段:
1 2 3 4 5 6 7 8 9 10 11 12 function createArc (shape, x, y, radius, from, to, sign, parts ) { var src = sign ? from : to; var trg = sign ? to : from ; var delta = sign ? 0 : Math .PI; for (var i = 1 ; i < parts; i++) { var t = i / parts; var cx = x + radius * Math .cos(delta + (src * (1 - t) + trg * t)); var cy = y + radius * Math .sin(delta + (src * (1 - t) + trg * t)); shape.lineTo(cx, cy); } }
当前的代码里有一个问题——啮合的齿轮会从一个小角度phi开始旋转,而不是恰好在(r,0)
。要正确地获取(x0,y0)
需要进行以下调整:
1 2 3 4 5 6 7 8 9 var sign = this .legs % 2 ;var from = -Math .PI / 2 + this .phi;var to = Math .PI / 2 + this .phi;var src = sign ? from : to;var trg = sign ? to : from ;var delta = sign ? 0 : Math .PI;var x0 = Math .cos(this .phi)*this .radius + this .connectionRadius*Math .cos(delta + src);var y0 = Math .sin(this .phi)*this .radius + this .connectionRadius*Math .sin(delta + src);
现在,让我们增加一些代码来渲染齿轮中心的孔:
1 2 3 4 var holePath = new THREE.Path();holePath.moveTo(this .connectionRadius, 0 ); createArc(holePath, 0 , 0 , this .connectionRadius, 0 , 2 * Math .PI, true , 10 ); this .shape.holes.push(holePath);
一旦我们齿轮的轮廓被描绘,我们可以使用Three.js的拉伸功能使其变得更加真实起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var extrudeSettings = { steps: 1 , amount: gearDepth, bevelEnabled: false , }; var geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);var material = new THREE.MeshPhongMaterial({ color: color, polygonOffset: true , polygonOffsetFactor: 1.0 , polygonOffsetUnits: 4.0 }); var mesh = new THREE.Mesh(geometry, material);scene.add(mesh);
这里是结果:
See the Pen Gear-3-1 by molunerfinn (@molunerfinn ) on CodePen .
生成更复杂的场景 使用第二部分的代码我们可以生成一对互相啮合的齿轮,为了生成更复杂的场景,只要我们避免碰撞,可以一次添加一个齿轮。
这个能够生成我们的齿轮啮合网络的方法应该是这样的:
放置随机的齿轮 找到不被任何其他齿轮占用的随机点p = (x,y) 找到最接近的齿轮g 创建一个新的齿轮中心点在p并且与g相连 回到第二步 使用这个方法你可以生成互相啮合的齿轮的整个网络,如下所示:
See the Pen GWbbpO by molunerfinn (@molunerfinn ) on CodePen .
以下是冲突检测查询的一个可能的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function detectCollision (newGear, neighbor ) { var result = false ; var that = this ; this .gears.forEach(function (gear ) { var dist = distance(gear.x, gear.y, newGear.x, newGear.y); if (dist < gear.radius + newGear.radius + 2 * that.connectionRadius + 5 && neighbor != gear) { result = true ; } }); return result; }
可以使用类似的方法实现冲突的近邻查询。但是你应该注意一个细节,那就是我们必须避免大齿轮以很大的角速度旋转。否则一些齿轮在动画的帧与帧之间转动得太多,将会形成混乱的情况。
下一个逻辑步骤就是增加我们模型的深度。我们可以随机放置几层,但是这样会让它们看起来不自然。这些层可以通过一些共同的转轴来进行交互。我们可以通过从一层到另一层复制圆点和角速度来模拟这种效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 function tryAddFromLayer (x, y, legs, angularSpeed ) { var newGear = new Gear(x, y, this .connectionRadius, legs, this .fillStyle, this .strokeStyle); if (!this .detectCollision(newGear, null ) && this .minFps * angularSpeed * newGear.radius < newGear.diameter ) { newGear.angularSpeed = angularSpeed; this .gears.push(newGear); } }
现在,我们能够堆积从以前生成的层中挑选齿轮,并从中生成新的齿轮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function addFromLayer (layers, options, maxR ) { var retries = 0 ; while (this .gears.length < options.maxGears && retries < options.qouta) { var layerIdx = Math .floor((options.generator.random() * layers.length)); var layer = layers[layerIdx]; var gearIdx = Math .floor((options.generator.random() * layer.gears.length)); var gear = layer.gears[gearIdx]; var legs = options.minLegs + Math .floor((options.generator.random() * (maxLegsFromRadius(maxR, this .connectionRadius) - options.minLegs))); this .tryAddFromLayer(gear.x, gear.y, legs, gear.angularSpeed); retries++; } }
这是算法的一个可能的输出:
See the Pen dvBBZz by molunerfinn (@molunerfinn ) on CodePen .
所有的源码你都可以通过查看上面的codepen
的源码以及github仓库源码 获取