スプロケットサイエンス:チェーンドライブシステムのアニメーション化


97

この課題の目標は、チェーンで接続されたスプロケットギアのセットで構成されるチェーンドライブシステムのアニメーションを作成することです

一般的な要件

プログラムには、トリプレットとして指定されたスプロケットのリストが与えられ(x, y, radius)ます。得られたチェーン駆動システムによって互いに接続され、これらのスプロケット、から構成されている閉鎖ピンと張っ鎖、それらの各々を通過するために。あなたの目標は、無限ループのアニメーションを生成し、システムが動いていることを示すことです。例えば、与えられた入力

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

、出力は次のようになります

例1

座標系は、右のX軸点、及びy軸はアップポイントようなものであるべきです。半径が 8以上の偶数であると仮定することもできます(これが後で問題になる理由がわかります)。また、スプロケットが少なくとも2つあり、スプロケットが互いに交差しないと仮定することもできます。ユニット入力の重要性はあまり高くありません。この投稿のすべての例とテストケースでは、入力単位としてピクセルを使用しています(たとえば、前の図の中央のスプロケットの半径は24ピクセルです)。これらの単位から大きく逸脱しないようにしてください。課題の残りの部分では、空間量は入力と同じ単位で与えられると理解されます-比率を正しく保つようにしてください!出力寸法は、すべてのスプロケットの境界ボックスよりわずかに大きく、システム全体が見えるように十分大きくする必要があります。特に、スプロケットの絶対位置は出力に影響しないはずです。相対的な位置のみが必要です(たとえば、上記の例ですべてのスプロケットを同じ量だけシフトした場合、出力は同じままになります)。

チェーンは、あるべき接線それは接触のすべての点で上を通過し、スプロケットにまっすぐ他のどこでも。チェーンは、隣接するチェーンセグメント(つまり、同じスプロケットで出会う2つのスプロケット間のチェーンの部分)互いに交差しないようにスプロケットを通過する必要があります。

チェーン交差点

たとえば、左上のスプロケットを通過する2つの隣接するチェーンセグメントは交差するため、上の左のシステムは有効ですが、中央のシステムは無効です。ただし、2つの交差するチェーンセグメントは隣接していないため、正しいシステム有効であることに注意してください(ただし、このシステムは他の2つとは異なる入力によって生成されます)。

物事を単純(r)に保つために、スプロケットがその2つの隣接するスプロケットの凸包、またはその隣人と他の隣人のそれぞれの凸包と交差ないと仮定することができます。言い換えれば、下の図の上部のスプロケットは、影付きの領域と交差しない場合があります。

除外

チェーンセグメントは、通過するスプロケット以外のスプロケットと交差する場合があります(最後のテストケースなど)。この場合、チェーンは常にスプロケットの前に表示されます。

視覚的要件

チェーンは、交互の幅の一連のリンクで構成する必要があります。幅の狭いリンクの幅は約2、幅の広いリンクの幅は約5である必要があります。両方のタイプのリンクの長さはほぼ等しいはずです。期間チェーンの長さ、つまり、リンクの幅の広いペアと狭いペアの合計の長さは、チェーンの長さの整数倍に適合する4πに最も近い数でなければなりません。たとえば、チェーンの長さが1,000の場合、その周期は12.5である必要があります。これは、1,000の整数回(80)に適合する4π(12.566 ...)に最も近い数値です。チェーンが折り返されるポイントにアーティファクトがないように、期間がチェーンの長さに整数回適合することが重要です。

鎖


半径Rのスプロケットは、3つの同心円状の部分で構成される必要があります。中心、半径約3の円である必要があります。車軸の周りのスプロケットのボディR -4.5の半径の円である必要があります。そして、スプロケットのリム約半径の円であるべき本体の周りに、
R - 1.5。また、リムにはスプロケットの歯が含まれている必要があります。の幅は約4です。歯のサイズと間隔は、チェーンリンクのサイズと一致している必要があります。

スプロケット

スプロケットの歯の周期、つまりスプロケットの円周に沿った2つの連続する歯の間の距離は、チェーンの周期と一致する必要があります。周期は約4πであり、スプロケットの半径は均一であることが保証されているため、周期はスプロケットの円周にほぼ整数回収まる必要があり、その結果、顕著なアーティファクトが発生しないはずです。スプロケットの歯が回ります。

簡単に区別できる限り、チェーン、スプロケットのさまざまな部分、および背景に任意の色の組み合わせを使用できます。背景は透明かもしれません。この投稿の例では、チェーン、スプロケットの車軸とリム、スプロケットのボディに使用しています。チェーンカラー #202020スプロケットの車軸とリムの色 #868481スプロケット本体色 #646361

アニメーションの要件

入力リストの最初のスプロケット時計回りに回転するはずです。残りのスプロケットはそれに応じて回転するはずです。チェーンは、毎秒約16π(約50)ユニットの速度で移動する必要があります。フレームレートはあなた次第ですが、アニメーションは十分に滑らかに見えるはずです。

アニメーションはシームレスにループする必要があります

適合性

視覚的な属性と比率の一部は、大まかにのみ意図的に指定されています。正確に一致させる必要はありません。プログラムの出力は、ここに示した例のピクセルごとの複製である必要はありませんが、似ているはずです。特に、チェーンとスプロケットの正確な比率、およびチェーンのリンクとスプロケットの歯の正確な形状は柔軟です。

従うべき最も重要な点は次のとおりです。

  • チェーンは、正しい方向から入力順にスプロケットを通過する必要があります。
  • チェーンは、すべての接触点でスプロケットに接する必要があります。
  • チェーンとスプロケットの歯のリンクは、少なくとも正しい間隔と位相まできれいに噛み合う必要があります。
  • チェーンのリンクとスプロケットの歯の間の間隔は、それらが巻き付く点で顕著なアーティファクトがないようにすべきです。
  • スプロケットは正しい方向に回転するはずです。
  • アニメーションはシームレスにループする必要があります。

最後の注意点として、技術的には、このチャレンジの目標は最短のコードを書くことです。もしあなたが創造的になり、より精巧な出力を生成したいと思うなら、ぜひやってください!

チャレンジ

上記のように、スプロケットのリストを取得し、対応するチェーンドライブシステムアニメーションを生成するプログラムまたは関数を作成します。

入出力

入力は、コマンドラインSTDIN関数の引数、または同等の方法を使用して取得できます。入力には便利な形式を使用できますが、投稿で必ず指定してください。

出力、あなたは可能直接アニメーションを表示、生成アニメーションファイル(例えば、アニメーションGIF)を、または生成フレームファイルのシーケンスを(ただし、この場合は小さなペナルティがあります。下記参照。)ファイル出力を使用する場合は、フレーム数が適切であることを確認してください(この投稿の例ではごく少数のフレームを使用しています)。フレーム数は最小限である必要はありませんが、余計なフレームを多く作成しないでください。フレームのシーケンスを出力する場合は、投稿でフレームレート指定してください。

スコア

これはcode-golfです。バイト単位の最短回答が勝ちです。

+ 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

24
どんな量のコードでもこれに成功裏に答えれば、感心するでしょう。
DavidC

5
どのようにgif​​を作成しましたか?そして、これはどのくらいの期間作業中ですか?
Jアトキン

10
@JAtkin他のみんなと同じように:私は解決策を書いた:)詳細について尋ねる場合、個々のフレームにCairoを使用し、ImageMagickを使用してgifを作成します(BTW、アニメーションを作成したい場合は、方法は、最初のフレームを生成して、アニメーションにそれらを有効にする外部ツールを使用することによって、私はすなわち完全に罰金限り、あなたはあなたのポスト内のツールへの依存性を指定すると、それと。ただ、明確にし、それはだあなたユーザーではなく、ツールを起動するプログラム。)
2015年

5
@Anko良いニュースは、心配する必要がないということです。この状況は、入力時に発生しないことが保証されています。「スプロケットが凸包と交差しない...」の部分を参照してください。この部分には、3つの影付き領域がある画像があります。より一般的には、スプロケットの近くを複数回通過するように見える場合でも、チェーンはスプロケットの順序に従って各スプロケットを一度だけ横断します。
エル

回答:


42

JavaScript(ES6)、2557 1915 1897 1681バイト

これは本当に超大型ゴルフではありません。部分的に手作業で縮小されますが、それは特別なことではありません。ミニファイする前にもっとゴルフした方が間違いなく短くなりますが、すでにこれに十分な時間を費やしました。

編集:わかりましたので、私はそれにもっと時間を費やし、縮小する前にコードをもっとゴルフしました(今回は非常に手動で)。コードはまだ同じアプローチと全体的な構造を使用していますが、それでも642バイトを節約できました。自分でそう言うなら、あまりみすぼらしくありません。おそらくバイト節約の機会を逃したかもしれませんが、この時点でそれがどのように機能するかはまだわかりません。出力の点で異なる唯一のことは、より簡潔に記述できるわずかに異なる色を使用することです。

編集2(後で):18バイトを保存しました。私がまったく見逃していたという目がくらむほど明白なことを指摘してくれたコメントのConorO'Brienに感謝します。

編集3:それで、率直に言って、どうやってそれをやったのか思い出せず、私は自分のコードをリバースエンジニアリングすると思いました。だから私は調べてみたところ、再編成といくつかのマイクロゴルフをすることで節約するためにさらに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要素(アニメーションを含む)を追加します。たとえば、2番目のテストケースを表示するには:

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

少なくともここではChromeで-御treat走を働くようです。

以下のスニペットで試してください(ボタンをクリックすると、OPの各テストケースが描画されます)。

このコードは、チェーンとギアの歯を破線で描いています。次に、animate要素を使用してstroke-dashoffset属性をアニメーション化します。結果のSVG要素は自己完結型です。JS駆動のアニメーションやCSSスタイルはありません。

物事をうまく整列させるために、各歯車の歯の輪は実際には2つの円弧からなるパスとして描かれているため、パスはチェーンが接触する接点から始まることができます。これにより、整列が非常に簡単になります。

さらに、SVGの破線ストロークを使用すると、多くの丸めエラーがあるようです。少なくとも、それは私が見たものです。チェーンが長ければ長いほど、連続する各ギアと噛み合います。そのため、問題を最小限に抑えるために、チェーンは実際には複数のパスで構成されています。各パスは、1つのギアの周りのアークセグメントと、次のギアへの直線で構成されています。ダッシュオフセットは一致するように計算されます。ただし、チェーンの細い「内側」の部分は、アニメーション化されていないため、単一のループパスです。


2
素晴らしく見える!古い(っぽい)チャレンジに答えてくれた名誉!
エル

1
-2バイト:R=g=>...
コナーオブライエン

1
@Flambino、このチャレンジのソリューションが好きで、元のソースを失ったことは本当に残念でした、それを回復するためにいくつかのリバースエンジニアリングを行いました、それはここで見つけることができます:gist.github.com/micnic/6aec085d63320229a778c6775ec7f9aa またそれを縮小しました手動で1665バイト(それはより縮小されることができますが、私は今日怠け者午前)まで
micnic

1
@micnicありがとう!私はそれをチェックする必要があります!心配しないでください、私もそれをリバースエンジニアリングすることができたので、より読みやすいバージョンがあります。しかし、ちょっと、16バイト少ない?称賛!私は時間を見つけることができたら絶対に見てみます
フランビーノ

1
@Flambino、本質的にファイルサイズに最も大きな影響を与えるのはsvg構造でした。私はaにすべてを入れず<g>、svgルートに直接入れました。また、あなたが使用して番号を付けるために、ブールからスイープフラグと大きな弧フラグを変え場所を見つけた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アトキン

1
実際、@ JAtkinは、私が知る限りこれらはすべて不要です。その他の点では、PointFは実際にはSystem.Drawing.PointF(List、Color、Mathに類似)であるため、対応するusing句を含めるか、使用時に完全修飾された型を指定し、System.Drawingへの参照に注意する必要があります。答え(それがスコアに追加すべきかどうかわからない)とにかく印象的な答え。
VisualMelon

@JAtkin SとPの2つのクラスがあるため、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バイト以上を保存できます。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.