链轮科学:动画链传动系统


96

这种挑战的目标是产生一个的动画链驱动系统中,由一组中的链轮的齿轮通过一个连接在一起的

一般要求

您的程序将获得链轮列表,指定为(x, y, radius)三重。将所得的链驱动系统由这些链轮,由连接在一起的闭合绷紧链越过它们各自,为了。您的目标是制作无限循环的动画,以显示运动中的系统。例如,给定输入

(0, 0, 16),  (100, 0, 16),  (100, 100, 12),  (50, 50, 24),  (0, 100, 12)

,输出应类似于

例子1

坐标系统应该是这样的x轴右点,和y轴指向上。您可以假定半径是大于或等于8的偶数(我们稍后将说明为什么如此)。您还可以假定至少有两个链轮,并且链轮彼此不相交。该单位的输入不是太关键。这篇文章中的所有示例和测试用例都使用像素作为输入单位(例如,上图中的中间链轮的半径为24像素;)请尽量不要与这些单位偏离太多。在其余的挑战中,空间量应理解为与输入相同的单位-请确保比例正确!输出尺寸应略大于所有链轮的边界框,并应足够大以使整个系统可见。尤其是,链轮的绝对位置不应影响输出。仅它们的相对位置应该如此(例如,如果我们将上述示例中的所有链轮偏移相同的量,则输出将保持不变。)

链条应在所有接触点处都与链轮相切,并在其他任何地方笔直。链条应越过链轮,以使相邻的链段(即,两个链轮之间的链的一部分,在同一链轮上相遇)不会相交

链交叉点

例如,虽然上面的左系统有效,但中间的系统无效,因为经过左下链轮的两个相邻链节相交。但是,请注意,正确的系统有效的,因为两个相交的链段不相邻(不过,该系统由与其他两个不同的输入产生)。

为了使事情简单化,您可以假定没有链轮与其两个相邻链轮的凸包或每个邻居和另一个邻居的凸包相交。换句话说,下图中的上链轮不得与任何阴影区域相交。

排除

链段可能与链轮相交,而不是与链轮相交(例如,在最后一个测试案例中)。在这种情况下,链条应始终出现在链轮的前面。

视觉要求

链条应包括一系列宽度互不相同的链节窄链接的宽度应约为2,宽链接的宽度应约为5。两种类型的链接的长度应大致相等。该链的宽/窄对的总长度应该是最接近4π的数字,该整数适合链长度的整数倍。例如,如果链的长度为1,000,则其周期应为12.5,这是最接近4π(12.566 ...)的数字,该整数适合1,000中的整数倍(80)。重要的是,该时间段必须在链长中适合整数倍,以便在链缠绕的点处没有伪影。

链


半径为R的链轮应包括三个同心部分:中心,其半径应为3左右;的链轮的身体,围绕轮轴,这应该是大约半径的圆- [R - 4.5; 和链轮的轮辋,在车身周围,半径应为
R -1.5。轮辋还应包含链轮齿,其宽度应约为4;齿的大小和间距应与链节的大小匹配,以使它们整齐地啮合。

链轮

链轮齿的周期,即沿链轮圆周的两个连续齿之间的距离,应与链条的周期匹配。由于周期大约为4π,并且由于链轮的半径保证是均匀的,因此该周期应适合链轮圆周的次数几乎为整数,因此在链轮的牙齿缠绕。

您可以对链条,链轮的不同部分和背景使用任何颜色组合,只要可以轻松区分即可。背景可能是透明的。本文中的示例链条颜色 #202020用于链条,链轮轴和轮辋颜色 #868481链轮的轴和轮辋以及链轮的车身颜色 #646361链轮的主体。

动画要求

输入列表中的第一个链轮应该顺时针旋转 ; 其余链轮应相应旋转。链条应以每秒约16π(约50个)单位的速度运动;帧速率取决于您,但是动画应该看起来足够平滑。

动画应无缝循环

符合标准

某些视觉属性和比例是有意仅粗略指定的-您不必完全匹配它们。程序的输出不必是此处给出的示例的像素到像素的副本,但是它看起来应该相似。特别是,链条和链轮的确切比例以及链节和链轮齿的确切形状都是灵活的。

以下是最重要的几点:

  • 链条应按输入顺序从正确的方向经过链轮。
  • 链条在所有接触点均应与链轮相切。
  • 链条的链节和链轮的齿应整齐地啮合,至少要保持正确的间距和相位。
  • 链节和链轮齿之间的间距应确保在它们缠绕的点处没有明显的伪影。
  • 链轮应沿正确的方向旋转。
  • 动画应无缝循环。

最后要说明的是,从技术上讲,此挑战的目标是编写最短的代码,如果您想发挥创造力并产生更精致的输出,请务必尝试!

挑战

如上所述,编写程序函数,获取链轮的列表,并生成相应的链传动系统动画。

输入输出

你可以采取输入通过命令行,通过STDIN,作为函数参数,或使用等效方法。您可以使用任何方便的格式进行输入,但是请确保在您的帖子中指定该格式

作为输出,您可以直接显示动画,生成动画文件(例如,动画GIF),或生成一系列帧文件(但是,在这种情况下要付出很小的代价;请参阅下文。)如果您使用文件输出,确保帧数合理(本文中的示例使用的帧数很少;)帧数不必一定要最小,但是您不应产生太多多余的帧。如果输出一系列帧,请确保在帖子中指定帧速率

得分

这是代码高尔夫球。在简短的回答,以字节为单位,胜。

+ 10%的惩罚   如果您的程序生成一系列帧作为输出,而不是直接显示动画或生成单个动画文件,请在分数上加10%。

测试用例

测试1

(0, 0, 26),  (120, 0, 26)

测试1

测试2

(100, 100, 60),  (220, 100, 14)

测试2

测试3

(100, 100, 16),  (100, 0, 24),  (0, 100, 24),  (0, 0, 16)

测试3

测试4

(0, 0, 60),  (44, 140, 16),  (-204, 140, 16),  (-160, 0, 60),  (-112, 188, 12),
(-190, 300, 30),  (30, 300, 30),  (-48, 188, 12)

测试4

测试5

(0, 128, 14),  (46.17, 63.55, 10),  (121.74, 39.55, 14),  (74.71, -24.28, 10),
(75.24, -103.55, 14),  (0, -78.56, 10),  (-75.24, -103.55, 14),  (-74.71, -24.28, 10),
(-121.74, 39.55, 14),  (-46.17, 63.55, 10)

测试5

测试6

(367, 151, 12),  (210, 75, 36),  (57, 286, 38),  (14, 181, 32),  (91, 124, 18),
(298, 366, 38),  (141, 3, 52),  (80, 179, 26),  (313, 32, 26),  (146, 280, 10),
(126, 253, 8),  (220, 184, 24),  (135, 332, 8),  (365, 296, 50),  (248, 217, 8),
(218, 392, 30)

测试6



玩得开心!


38
这些GIF非常令人满意+1
Adnan 2015年

24
如果有人用任何数量的代码成功回答这个问题,我将印象深刻。
DavidC

5
您是如何制作gif的?这项工作进行了多久了?
J Atkin

10
@JAtkin其他人也应该这样做:我写了一个解决方案:)如果您要询问细节,我使用开罗制作单个帧,然后使用ImageMagick来创建gif(顺便说一句,如果有人想制作动画,方式,即通过首先产生的帧,然后使用外部工具将它们变成动画,我完全罚款与,只要你指定在您的文章工具的依赖。只要是明确的,它是你的程序,应调用该工具而不是用户。)
Ell 2015年

5
@Anko好消息是您不必为此担心:可以保证在输入中不会发生这种情况;请参见“没有链轮与凸包相交...”部分,该部分的图像带有三个阴影区域。更普遍的是,根据链轮的顺序,链条只穿过每个链轮一次,即使看起来它在链轮附近通过的次数不止一次。
2015年

Answers:


42

JavaScript(ES6),2557 1915 1897 1681字节

这真的不是超级高尔夫。它是缩小的-部分是手工制作的-但这没什么特别的。如果在缩小之前我要打更多的球,无疑会更短一些,但是我已经花了很多时间。

编辑:好的,所以我花了更多时间在压缩之前花费了更多时间,并且打了很多代码(这次是非常手动的)。该代码仍然使用相同的方法和整体结构,但是即使如此,我仍然节省了642个字节。如果我自己这么说的话,不要太寒酸。可能错过了一些节省字节的机会,但是在这一点上,我什至不知道它如何工作。唯一不同的是输出现在使用的颜色略有不同,可以更简洁地书写。

编辑2(稍后):保存了18个字节。感谢ConorO'Brien在评论中指出我完全想念的那种显而易见的明显现象。

编辑3:因此,我认为我将对自己的代码进行逆向工程,因为坦率地说,我不记得自己是怎么做到的,而且我丢掉了无用的版本。因此,我仔细检查了一下,lo和behold发现另外316个字节可以通过重组和微高尔夫球来保存。

R=g=>{with(Math){V=(x,y,o)=>o={x,y,l:sqrt(x*x+y*y),a:v=>V(x+v.x,y+v.y),s:v=>o.a(v.m(-1)),m:f=>V(x*f,y*f),t:r=>V(x*cos(r)-y*sin(r),x*sin(r)+y*cos(r)),c:v=>x*v.y-y*v.x,toString:_=>x+','+y};a='appendChild',b='setAttribute';S=(e,a)=>Object.keys(a).map(n=>e[b](n,a[n]))&&e;T=(t,a)=>S(k.createElementNS('http://www.w3.org/2000/svg',t),a);C=(e,a)=>S(e.cloneNode(),a);P=a=>T('path',(a.fill='none',a));w=h=-(x=y=1/0);G=g.map((a,g)=>(g=V(...a))&&(u=(g.r=a[2])+5,x=min(x,g.x-u),y=min(y,g.y-u),w=max(w,g.x+u),h=max(h,g.y+u))&&g);k=document;I=k[a].bind(k.body[a](T('svg',{width:w-x,height:h-y}))[a](T('g',{transform:`translate(${-x},${h})scale(1,-1)`})));L=(c)=>(h=G.length)&&G.map((g,i)=>c(G[i],G[i?i-1:h-1],G[(i+1)%h]))&&L;l='';L((g,p,n)=>g.f=p.s(g).c(n.s(g))>0)((g,a,n)=>{d=g.s(n),y=x=1/d.l;g.f!=n.f?(a=asin((g.r+n.r)*x),g.f?(x=-x,a=-a):(y=-y)):(a=asin((g.r-n.r)*x),g.f&&(x=y=-x,a=-a));t=d.t(a+PI/2);g.o=t.m(x*g.r).a(g);n.i=t.m(y*n.r).a(n)})((g,p,n)=>{z='#888';d=(l,s,e)=>`A${g.r},${g.r} 0 ${1*l},${1*s} ${e}`;e=(f,r)=>T('circle',{cx:g.x,cy:g.y,r,fill:f});g.k=p.o.s(n.i).l<g.i.s(g.o).l;w=d(g.k,!g.f,g.o);g.j=`${w}L${n.i}`;l+=g.j;I(e(z,g.r-1.5));g.g=I(P({d:`M${g.i}${w}${d(!g.k,!g.f,g.i)}`,stroke:z,'stroke-width':5}));g.h=I(C(g.g,{d:`M${g.i}${g.j}`,stroke:'#222'}));I(e('#666',g.r-4.5));I(e(z,3))});t=e=>e.getTotalLength(),u='stroke-dasharray',v='stroke-dashoffset',f=G[0];l=I(C(f.h,{d:'M'+f.i+l,'stroke-width':2}));s=f.w=t(l)/round(t(l)/(4*PI))/2;X=8*s;Y=f.v=0;L((g,p)=>{g.g[b](u,s);g.h[b](u,s);g==f||(g.w=p.w+t(p.h),g.v=p.v+t(p.h));g.g[b](v,g.w);g.h[b](v,g.v);g.h[a](C(g.g[a](T('animate',{attributeName:v,from:g.w+X,to:g.w+Y,repeatCount:'indefinite',dur:'1s'})),{from:g.v+X,to:g.v+Y}))})}}

上面的函数将SVG元素(包括动画)添加到文档中。例如,显示第二个测试用例:

R([[100, 100, 60],  [220, 100, 14]]);

似乎可以享受服务-至少在Chrome中是这样。

在下面的代码段中尝试一下(单击按钮将绘制OP的每个测试用例)。

该代码以虚线画出了链条和齿轮的齿。然后,它使用animate元素为stroke-dashoffset属性设置动画。生成的SVG元素是独立的;没有JS驱动的动画或CSS样式。

为使各部分排列整齐,实际上将每个齿轮的齿圈绘制为一条由两个圆弧组成的路径,因此该路径可以从链条接触的切线点开始。这使得排列起来变得容易得多。

此外,当使用SVG的虚线笔划时,似乎还有很多舍入错误。至少,这就是我所看到的;链条越长,与每个后续齿轮的啮合越差。因此,为了最大程度地减少问题,该链实际上由多条路径组成。每条路径由围绕一个齿轮的弧形段和通往下一齿轮的直线组成。计算它们的破折号偏移以匹配。但是,链的薄“内部”部分只是一条循环路径,因为它没有动画。


2
看起来很棒!回答旧的(挑战)荣誉!
埃尔

1
-2个字节:R=g=>...
科纳·奥布莱恩

1
@Flambino,我喜欢您为此挑战提供的解决方案,非常抱歉您丢失了原始资源,我进行了一些反向工程来恢复它,可以在这里找到:gist.github.com/micnic/6aec085d63320229a778c6775ec7f9aa 我也将其缩小了手动设置为1665个字节(可以进一步缩小,但今天我很懒)
micnic

1
@micnic谢谢!我必须检查一下!不用担心,我也设法对其进行了逆向工程,因此我的可读性更高。但是,党,少16个字节?荣誉!如果可以的话,我一定会来看一下
Flambino

1
@Flambino,本质上对文件大小的最大影响是svg结构,我没有将eththing放在中<g>,而是直接将其放在svg根目录中。还找到了使用来将扫描标志和大弧标志从boolean转换为数字的地方1*x,但是您可以使用+x
micnic

40

C#3566字节

根本不打高尔夫球,但可以工作(我认为)

取消编辑历史记录中的内容。

使用Magick.NET渲染gif。

class S{public float x,y,r;public bool c;public double i,o,a=0,l=0;public S(float X,float Y,float R){x=X;y=Y;r=R;}}class P{List<S>q=new List<S>();float x=float.MaxValue,X=float.MinValue,y=float.MaxValue,Y=float.MinValue,z=0,Z=0,N;int w=0,h=0;Color c=Color.FromArgb(32,32,32);Pen p,o;Brush b,n,m;List<PointF>C;double l;void F(float[][]s){p=new Pen(c,2);o=new Pen(c,5);b=new SolidBrush(c);n=new SolidBrush(Color.FromArgb(134,132,129));m=new SolidBrush(Color.FromArgb(100,99,97));for(int i=0;i<s.Length;i++){float[]S=s[i];q.Add(new S(S[0],S[1],S[2]));if(S[0]-S[2]<x)x=S[0]-S[2];if(S[1]-S[2]<y)y=S[1]-S[2];if(S[0]+S[2]>X)X=S[0]+S[2];if(S[1]+S[2]>Y)Y=S[1]+S[2];}q[0].c=true;z=-x+16;Z=-y+16;w=(int)(X-x+32);h=(int)(Y-y+32);for(int i=0;i<=q.Count;i++)H(q[i%q.Count],q[(i+1)%q.Count],q[(i+2)%q.Count]);C=new List<PointF>();for(int i=0;i<q.Count;i++){S g=q[i],k=q[(i+1)%q.Count];if(g.c)for(double a=g.i;a<g.i+D(g.o,g.i);a+=Math.PI/(2*g.r)){C.Add(new PointF((float)(g.x+z+g.r*Math.Cos(a)),(float)(g.y+Z+g.r*Math.Sin(a))));}else
for(double a=g.o+D(g.i,g.o);a>g.o;a-=Math.PI/(2*g.r)){C.Add(new PointF((float)(g.x+z+g.r*Math.Cos(a)),(float)(g.y+Z+g.r*Math.Sin(a))));}C.Add(new PointF((float)(g.x+z+g.r*Math.Cos(g.o)),(float)(g.y+Z+g.r*Math.Sin(g.o))));C.Add(new PointF((float)(k.x+z+k.r*Math.Cos(k.i)),(float)(k.y+Z+k.r*Math.Sin(k.i))));k.l=E(C);}l=E(C);N=(float)(K(l)/10.0);o.DashPattern=new float[]{N,N};double u=q[0].i;for(int i=0;i<q.Count;i++){S g=q[i];double L=g.l/(N*5);g.a=g.i+((1-(L%2))/g.r*Math.PI*2)*(g.c?1:-1);}List<MagickImage>I=new List<MagickImage>();for(int i=0;i<t;i++){using(Bitmap B=new Bitmap(w,h)){using(Graphics g=Graphics.FromImage(B)){g.Clear(Color.White);g.SmoothingMode=System.Drawing.Drawing2D.SmoothingMode.AntiAlias;foreach(S U in q){float R=U.x+z,L=U.y+Z,d=7+2*U.r;PointF[]f=new PointF[4];for(double a=(i*(4.0/t));a<2*U.r;a+=4){double v=U.a+((U.c?-a:a)/U.r*Math.PI),j=Math.PI/U.r*(U.c?1:-1),V=v+j,W=V+j,r=U.r+3.5;f[0]=new PointF(R,L);f[1]=new PointF(R+(float)(r*Math.Cos(v)),L+(float)(r*Math.Sin(v)));f[2]=new PointF(R+(float)(r*Math.Cos(V)),L+(float)(r*Math.Sin(V)));f[3]=new PointF(R+(float)(r*Math.Cos(W)),L+(float)(r*Math.Sin(W)));g.FillPolygon(n,f);}d=2*(U.r-1.5f);g.FillEllipse(n,R-d/2,L-d/2,d,d);d=2*(U.r-4.5f);g.FillEllipse(m,R-d/2,L-d/2,d,d);d=6;g.FillEllipse(n,R-d/2,L-d/2,d,d);}g.DrawLines(p,C.ToArray());o.DashOffset=(N*2.0f/t)*i;g.DrawLines(o,C.ToArray());B.RotateFlip(RotateFlipType.RotateNoneFlipY);B.Save(i+".png",ImageFormat.Png);I.Add(new MagickImage(B));}}}using(MagickImageCollection collection=new MagickImageCollection()){foreach(MagickImage i in I){i.AnimationDelay=5;collection.Add(i);}QuantizeSettings Q=new QuantizeSettings();Q.Colors=256;collection.Quantize(Q);collection.Optimize();collection.Write("1.gif");}}int t=5;double D(double a,double b){double P=Math.PI,r=a-b;while(r<0)r+=2*P;return r%(2*P);}double E(List<PointF> c){double u=0;for(int i=0;i<c.Count-1;i++){PointF s=c[i];PointF t=c[i+1];double x=s.X-t.X,y=s.Y-t.Y;u+=Math.Sqrt(x*x+y*y);}return u;}double K(double L){double P=4*Math.PI;int i=(int)(L/P);float a=(float)L/i,b=(float)L/(i+1);if(Math.Abs(P-a)<Math.Abs(P-b))return a;return b;}void H(S a,S b,S c){double A=0,r=0,d=b.x-a.x,e=b.y-a.y,f=Math.Atan2(e,d)+Math.PI/2,g=Math.Atan2(e,d)-Math.PI/2,h=Math.Atan2(-e,-d)-Math.PI/2,i=Math.Atan2(-e,-d)+Math.PI/2;double k=c.x-b.x,n=c.y-b.y,l=Math.Sqrt(d*d+e*e);A=D(Math.Atan2(n,k),Math.Atan2(-e,-d));bool x=A>Math.PI!=a.c;b.c=x!=a.c;if(a.r!=b.r)r=a.r+(x?b.r:-b.r);f-=Math.Asin(r/l);g+=Math.Asin(r/l);h+=Math.Asin(r/l);i-=Math.Asin(r/l);b.i=x==a.c?h:i;a.o=a.c?g:f;}}

P类具有函数F;例:

static void Main(string[]a){
P p=new P();
float[][]s=new float[][]{
new float[]{10,200,20},
new float[]{240,200,20},
new float[]{190,170,10},
new float[]{190,150,10},
new float[]{210,120,20},
new float[]{190,90,10},
new float[]{160,0,20},
new float[]{130,170,10},
new float[]{110,170,10},
new float[]{80,0,20},
new float[]{50,170,10}
};
p.F(s);}

在此处输入图片说明


2
感谢您发布高尔夫版本!轻微的小怪:gif中的第一个链轮逆时针旋转;第一链轮应始终顺时针旋转。
2015年

我只看过C#,但是public在类中的每个字段之前都需要修饰符吗?
J Atkin 2015年

1
@JAtkin的确,据我所知,这些都是不必要的。在其他方面,PointF实际上是System.Drawing.PointF(与List,Color和Math相似),因此using应包括相应的子句,或使用时完全限定的类型,并应注意对System.Drawing的引用。在答案中(是否应该增加我不知道的分数)。无论如何,印象深刻的答案。
VisualMelon 2015年

@JAtkin我有两个类S和P,所以S中的字段都是公共的。不知道是否确实需要它们,但我是这样认为的
。– TFeld

3

JavaScript(ES6)1626字节

此解决方案是@Flambino解决方案的反向工程的结果,我与他一道将其发布。

R=g=>{with(Math){v='stroke';j=v+'-dasharray';q=v+'-dashoffset';m='appendChild';n='getTotalLength';b='setAttribute';z='#888';k=document;V=(x,y,r,o)=>o={x,y,r,l:sqrt(x*x+y*y),a:v=>V(x+v.x,y+v.y),s:v=>o.a(v.m(-1)),m:f=>V(x*f,y*f),t:r=>V(x*cos(r)-y*sin(r),x*sin(r)+y*cos(r)),c:v=>x*v.y-y*v.x,toString:_=>x+','+y};S=(e,a)=>Object.keys(a).map(n=>e[b](n,a[n]))&&e;T=(t,a)=>S(k.createElementNS('http://www.w3.org/2000/svg',t),a);C=(e,a)=>S(e.cloneNode(),a);w=h=-(x=y=1/0);G=g.map((a,g)=>(g=V(...a))&&(u=(g.r=a[2])+5,x=min(x,g.x-u),y=min(y,g.y-u),w=max(w,g.x+u),h=max(h,g.y+u))&&g);f=G[0];w-=x;h-=y;s=T('svg',{width:w,height:h,viewBox:x+' '+y+' '+w+' '+h,transform:'scale(1,-1)'});c='';L=(c)=>(h=G.length)&&G.map((g,i)=>c(G[i],G[(h+i-1)%h],G[(i+1)%h]))&&L;L((g,p,n)=>g.w=(p.s(g).c(n.s(g))>0))((g,p,n)=>{d=g.s(n),y=x=1/d.l;g.w!=n.w?(p=asin((g.r+n.r)*x),g.w?(x=-x,p=-p):(y=-y)):(p=asin((g.r-n.r)*x),g.w&&(x=y=-x,p=-p));t=d.t(p+PI/2);g.o=t.m(x*g.r).a(g);n.i=t.m(y*n.r).a(n)})((g,p,n)=>{l=(p.o.s(n.i).l<g.i.s(g.o).l);d=(l,e)=>`A${g.r} ${g.r} 0 ${+l} ${+!g.w} ${e}`;a=d(l,g.o);e=(f,r)=>T('circle',{cx:g.x,cy:g.y,r,fill:f});c+=a+'L'+n.i;s[m](e(z,g.r-1.5));s[m](e('#666',g.r-4.5));s[m](e(z,3));g.p=s[m](C(g.e=s[m](T('path',{d:'M'+g.i+a+d(!l,g.i),fill:'none',[v]:z,[v+'-width']:5})),{d:'M'+g.i+a+'L'+n.i,[v]:'#222'}))});c=C(f.p,{d:'M'+f.i+c,[v+'-width']:2});g=c[n]();y=8*(x=g/round(g/(4*PI))/2);f.g=x;f.h=0;L((g,p)=>{g!=f&&(g.g=p.g+p.p[n](),g.h=p.h+p.p[n]());S(g.p,{[j]:x,[q]:g.h})[m](C(S(g.e,{[j]:x,[q]:g.g})[m](T('animate',{attributeName:[q],from:g.g+y,to:g.g,repeatCount:'indefinite',dur:'1s'})),{from:g.h+y,to:g.h}))});k.body[m](s)[m](c)}}

非高尔夫版本:

class Vector {

    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.length = Math.sqrt(x * x + y * y);
    }

    add(vector) {

        return new Vector(this.x + vector.x, this.y + vector.y);
    }

    subtract(vector) {

        return new Vector(this.x - vector.x, this.y - vector.y);
    }

    multiply(scalar) {

        return new Vector(this.x * scalar, this.y * scalar);
    }

    rotate(radians) {

        const cos = Math.cos(radians);
        const sin = Math.sin(radians);

        return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
    }

    cross(vector) {

        return this.x * vector.y - this.y * vector.x;
    }

    toString() {

        return `${this.x},${this.y}`;
    }
}

class Gear {

    constructor(x, y, radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    getVector() {

        return new Vector(this.x, this.y);
    }
}

const setAttributes = (element, attributes) => {

    Object.keys(attributes).forEach((attribute) => {
        element.setAttribute(attribute, attributes[attribute]);
    });
};

const createElement = (tagName, attributes) => {

    const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);

    setAttributes(element, attributes);

    return element;
};

const cloneElement = (element, attributes) => {

    const clone = element.cloneNode();

    setAttributes(clone, attributes);

    return clone;
};

const createPath = (attributes) => {

    return createElement('path', {
        ...attributes,
        fill: 'none'
    });
};

const createCircle = (cx, cy, r, fill) => {

    return createElement('circle', {
        cx,
        cy,
        r,
        fill
    });
};

const loopGears = (gears, callback) => {

    const length = gears.length;

    gears.forEach((gear, index) => {

        const prevGear = gears[(length + index - 1) % length];
        const nextGear = gears[(index + 1) % length];

        callback(gear, prevGear, nextGear);
    });
};

const arcDescription = (radius, largeArcFlag, sweepFlag, endVector) => {

    return `A${radius} ${radius} 0 ${+largeArcFlag} ${+sweepFlag} ${endVector}`;
};

const renderGears = (data) => {

    let x = Infinity;
    let y = Infinity;
    let w = -Infinity;
    let h = -Infinity;

    const gears = data.map((params) => {

        const gear = new Gear(...params);
        const unit = params[2] + 5;

        x = Math.min(x, gear.x - unit);
        y = Math.min(y, gear.y - unit);
        w = Math.max(w, gear.x + unit);
        h = Math.max(h, gear.y + unit);

        return gear;
    });

    const firstGear = gears[0];

    w -= x;
    h -= y;

    const svg = createElement('svg', {
        width: w,
        height: h,
        viewBox: `${x} ${y} ${w} ${h}`,
        transform: `scale(1,-1)`
    });

    let chainPath = '';

    loopGears(gears, (gear, prevGear, nextGear) => {

        const gearVector = gear.getVector();
        const prevGearVector = prevGear.getVector().subtract(gearVector);
        const nextGearVector = nextGear.getVector().subtract(gearVector);

        gear.sweep = (prevGearVector.cross(nextGearVector) > 0);
    });

    loopGears(gears, (gear, prevGear, nextGear) => {

        const diffVector = gear.getVector().subtract(nextGear.getVector());

        let angle = 0;
        let x = 1 / diffVector.length;
        let y = x;

        if (gear.sweep === nextGear.sweep) {

            angle = Math.asin((gear.radius - nextGear.radius) * x);

            if (gear.sweep) {
                x = -x;
                y = -y;
                angle = -angle;
            }
        } else {

            angle = Math.asin((gear.radius + nextGear.radius) * x);

            if (gear.sweep) {
                x = -x;
                angle = -angle;
            } else {
                y = -y;
            }
        }

        const perpendicularVector = diffVector.rotate(angle + Math.PI / 2);

        gear.out = perpendicularVector.multiply(x * gear.radius).add(gear.getVector());
        nextGear.in = perpendicularVector.multiply(y * nextGear.radius).add(nextGear.getVector());
    });

    loopGears(gears, (gear, prevGear, nextGear) => {

        const largeArcFlag = (prevGear.out.subtract(nextGear.in).length < gear.in.subtract(gear.out).length);
        const arcPath = arcDescription(gear.radius, largeArcFlag, !gear.sweep, gear.out);

        const gearExterior = createCircle(gear.x, gear.y, gear.radius - 1.5, '#888');
        const gearInterior = createCircle(gear.x, gear.y, gear.radius - 4.5, '#666');
        const gearCenter = createCircle(gear.x, gear.y, 3, '#888');

        const gearTeeth = createPath({
            d: `M${gear.in}${arcPath}${arcDescription(gear.radius, !largeArcFlag, !gear.sweep, gear.in)}`,
            stroke: '#888',
            'stroke-width': 5
        });

        const chainParts = cloneElement(gearTeeth, {
            d: `M${gear.in}${arcPath}L${nextGear.in}`,
            stroke: '#222'
        });

        gear.teeth = gearTeeth;
        gear.chainParts = chainParts;

        chainPath += `${arcPath}L${nextGear.in}`;

        svg.appendChild(gearExterior);
        svg.appendChild(gearInterior);
        svg.appendChild(gearCenter);
        svg.appendChild(gearTeeth);
        svg.appendChild(chainParts);
    });

    const chain = cloneElement(firstGear.chainParts, {
        d: 'M' + firstGear.in + chainPath,
        'stroke-width': 2
    });

    const chainLength = chain.getTotalLength();
    const chainUnit = chainLength / Math.round(chainLength / (4 * Math.PI)) / 2;
    const animationOffset = 8 * chainUnit;

    loopGears(gears, (gear, prevGear) => {

        if (gear === firstGear) {
            gear.teethOffset = chainUnit;
            gear.chainOffset = 0;
        } else {
            gear.teethOffset = prevGear.teethOffset + prevGear.chainParts.getTotalLength();
            gear.chainOffset = prevGear.chainOffset + prevGear.chainParts.getTotalLength();
        }

        setAttributes(gear.teeth, {
            'stroke-dasharray': chainUnit,
            'stroke-dashoffset': gear.teethOffset
        });

        setAttributes(gear.chainParts, {
            'stroke-dasharray': chainUnit,
            'stroke-dashoffset': gear.chainOffset
        });

        const animate = createElement('animate', {
            attributeName: 'stroke-dashoffset',
            from: gear.teethOffset + animationOffset,
            to: gear.teethOffset,
            repeatCount: 'indefinite',
            dur: '1s'
        });

        const cloneAnimate = cloneElement(animate, {
            from: gear.chainOffset + animationOffset,
            to: gear.chainOffset
        });

        gear.teeth.appendChild(animate);
        gear.chainParts.appendChild(cloneAnimate);
    });

    svg.appendChild(chain);
    document.body.appendChild(svg);
};

var testCases = [
    [[0, 0, 16],  [100, 0, 16],  [100, 100, 12],  [50, 50, 24],  [0, 100, 12]],
    [[0, 0, 26],  [120, 0, 26]],
    [[100, 100, 60],  [220, 100, 14]],
    [[100, 100, 16],  [100, 0, 24],  [0, 100, 24],  [0, 0, 16]],
    [[0, 0, 60],  [44, 140, 16],  [-204, 140, 16],  [-160, 0, 60],  [-112, 188, 12], [-190, 300, 30],  [30, 300, 30],  [-48, 188, 12]],
    [[0, 128, 14],  [46.17, 63.55, 10],  [121.74, 39.55, 14],  [74.71, -24.28, 10], [75.24, -103.55, 14],  [0, -78.56, 10],  [-75.24, -103.55, 14],  [-74.71, -24.28, 10], [-121.74, 39.55, 14],  [-46.17, 63.55, 10]],
    [[367, 151, 12],  [210, 75, 36],  [57, 286, 38],  [14, 181, 32],  [91, 124, 18], [298, 366, 38],  [141, 3, 52],  [80, 179, 26],  [313, 32, 26],  [146, 280, 10], [126, 253, 8],  [220, 184, 24],  [135, 332, 8],  [365, 296, 50],  [248, 217, 8], [218, 392, 30]]
];

function clear() {
    var buttons = document.createElement('div');
    document.body.innerHTML = "";
    document.body.appendChild(buttons);
    testCases.forEach(function (data, i) {
        var button = document.createElement('button');
        button.innerHTML = String(i);
        button.onclick = function () {
            clear();
            renderGears(data);
            return false;
        };
        buttons.appendChild(button);
    });
}

clear();


1
您可以使用工具保存250个以上的字节。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.