Sprocket Science: Animando um sistema de acionamento por corrente

97

O objetivo desse desafio é produzir uma animação de um sistema de acionamento por corrente , composto por um conjunto de engrenagens conectadas por uma corrente .

requerimentos gerais

Seu programa receberá uma lista de rodas dentadas , especificadas como (x, y, radius)trigêmeos. O sistema de acionamento da corrente resultante é composto por essas rodas dentadas, conectadas por uma corrente esticada fechada que passa sobre cada uma delas, em ordem . Seu objetivo é produzir uma animação infinitamente em loop , mostrando o sistema em movimento. Por exemplo, dada a entrada

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

, a saída deve parecer algo como

Exemplo 1.

O sistema de coordenadas deve ser tal que o eixo x aponte para a direita e o eixo y aponte para cima. Você pode supor que os raios sejam números pares iguais ou superiores a 8 (veremos por que isso importa mais tarde). Você também pode assumir que existem pelo menos duas rodas dentadas e que as rodas dentadas não se cruzam . As unidadesda entrada não são muito críticos. Todos os exemplos e casos de teste nesta postagem usam pixels como unidades de entrada (portanto, por exemplo, o raio da roda do meio na figura anterior é de 24 pixels;) tente não se desviar muito dessas unidades. No restante do desafio, entende-se que as quantidades espaciais são dadas nas mesmas unidades que a entrada - mantenha as proporções corretas! As dimensões da saída devem ser ligeiramente maiores que a caixa delimitadora de todas as rodas dentadas, suficientemente grandes para que todo o sistema seja visível. Em particular, as posições absolutas das rodas dentadas não devem afetar a saída; somente suas posições relativas deveriam (por exemplo, se trocássemos todas as rodas dentadas no exemplo acima pela mesma quantidade, a saída permaneceria a mesma).

A corrente deve ser tangente às rodas dentadas por onde passa em todos os pontos de contato e reta em qualquer outro lugar. A corrente deve passar sobre as rodas dentadas, de modo que os segmentos adjacentes da corrente (ou seja, partes da corrente entre duas rodas dentadas, que se encontram na mesma roda dentada) não se cruzem .

Interseção de correntes.

Por exemplo, enquanto o sistema esquerdo acima é válido, o do meio não é, pois os dois segmentos de corrente adjacentes que passam sobre a roda dentada inferior esquerda se cruzam. No entanto, observe que o sistema correto é válido, pois os dois segmentos de cadeia que se cruzam não são adjacentes (esse sistema é produzido por uma entrada diferente dos outros dois).

Para simplificar as coisas (r), você pode assumir que nenhum pinhão cruza o casco convexo de seus dois pinhões vizinhos ou os cascos convexos de cada um de seus vizinhos e seu outro vizinho. Em outras palavras, a roda dentada superior no diagrama abaixo pode não cruzar nenhuma das regiões sombreadas.

Exclusão

Os segmentos da cadeia podem cruzar as rodas dentadas diferentes daquelas que passam por cima (como no último caso de teste). Nesse caso, a corrente sempre deve aparecer na frente das rodas dentadas.

Requisitos visuais

A corrente deve consistir em uma série de elos de larguras alternadas. A largura do link estreito deve ser de cerca de 2 e a largura do link de largura deve ser de 5. O comprimento dos dois tipos de links deve ser aproximadamente igual. O períododa cadeia, ou seja, o comprimento total de um par de elos amplo / estreito, deve ser o número mais próximo de 4π que se ajusta a um número inteiro de vezes no comprimento da cadeia. Por exemplo, se o comprimento da cadeia for 1.000, seu período deverá ser 12,5, que é o número mais próximo de 4π (12,566 ...) que se ajusta a um número inteiro de vezes (80) em 1.000. É importante que o período ajuste um número inteiro de vezes no comprimento da corrente, para que não haja artefatos no ponto em que a corrente se enrola.

Cadeia


Uma roda dentada de raio R deve consistir em três partes concêntricas: um eixo central , que deve ser um círculo de raio de cerca de 3; o corpo da roda dentada , em torno do eixo, que deve ser um círculo de raio em torno de R - 4,5; e a borda da roda dentada , ao redor do corpo, que deve ser um círculo de raio em torno de
R - 1,5. O aro também deve conter os dentes da roda dentada , que devem ter uma largura de cerca de 4; o tamanho e o espaçamento dos dentes devem coincidir com os tamanhos dos elos da corrente, para que eles se enredem perfeitamente.

Roda dentada

O período dos dentes da roda dentada, ou seja, a distância entre dois dentes consecutivos ao longo da circunferência da roda dentada, deve coincidir com o período da corrente. Como o período é de cerca de 4π e como o raio da roda dentada é garantido, o período deve caber na circunferência da roda dentada um número quase inteiro de vezes, para que não haja artefatos perceptíveis no ponto em que os dentes da roda dentada se enrolam.

Você pode usar qualquer combinação de cores para a corrente, as diferentes partes da roda dentada e o fundo, desde que sejam facilmente distinguíveis . O plano de fundo pode ser transparente. Os exemplos deste post são usados Cor da corrente #202020para a corrente, Cor da roda dentada e da jante #868481o eixo e a jante Cor do corpo da roda dentada #646361da roda dentada e o corpo da roda dentada.

Requisitos de Animação

O primeiro pinhão na lista de entrada deve girar no sentido horário ; o restante das rodas dentadas deve girar de acordo. A corrente deve se mover a uma velocidade de cerca de 16π (cerca de 50) unidades por segundo; a taxa de quadros depende de você, mas a animação deve parecer suave o suficiente.

A animação deve repetir sem problemas .

Conformidade

Alguns dos atributos e proporções visuais são especificados intencionalmente apenas aproximadamente - você não precisa correspondê-los exatamente . A saída do seu programa não precisa ser uma réplica pixel a pixel dos exemplos fornecidos aqui, mas deve ser semelhante. Em particular, as proporções exatas da corrente e das rodas dentadas, e o formato exato dos elos da corrente e dos dentes da roda dentada, são flexíveis.

Os pontos mais importantes a seguir são estes:

  • A corrente deve passar sobre as rodas dentadas, na ordem de entrada, na direção correta.
  • A corrente deve ser tangente às rodas dentadas em todos os pontos de contato.
  • Os elos da corrente e os dentes das rodas dentadas devem enredar ordenadamente, pelo menos até corrigir o espaçamento e a fase.
  • O espaçamento entre os elos da corrente e os dentes das rodas dentadas deve ser tal que não haja artefatos perceptíveis no ponto em que eles se enrolam.
  • As rodas dentadas devem girar na direção correta.
  • A animação deve repetir sem problemas.

Como nota final, embora, tecnicamente, o objetivo desse desafio seja escrever o código mais curto, se você quiser ser criativo e produzir uma saída mais elaborada, por qualquer meio, vá em frente!

Desafio

Escreva um programa ou uma função , fazendo uma lista de rodas dentadas e produzindo uma animação do sistema de acionamento por corrente correspondente, conforme descrito acima.

Entrada e saída

Você pode levar a entrada pela linha de comando , pelo STDIN , como argumentos de função ou usando um método equivalente . Você pode usar qualquer formato conveniente para a entrada, mas certifique-se de especificá-lo em sua postagem.

Como saída , você pode exibir a animação diretamente , produzir um arquivo de animação (por exemplo, um GIF animado) ou produzir uma sequência de arquivos de quadro (no entanto, há uma pequena penalidade nesse caso; veja abaixo.) Se você usar a saída do arquivo, verifique se o número de quadros é razoável (os exemplos nesta postagem usam muito poucos quadros;) o número de quadros não precisa ser mínimo, mas você não deve produzir muitos quadros supérfluos. Se você produzir uma sequência de quadros, especifique a taxa de quadros na sua postagem.

Ponto

Isso é código-golfe . A resposta mais curta , em bytes, vence.

+ 10% de multa   Se o seu programa produzir uma sequência de quadros como saída, em vez de exibir a animação diretamente ou produzir um único arquivo de animação, adicione 10% à sua pontuação.

Casos de teste

Teste 1

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

Teste 1

Teste 2

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

Teste 2

Teste 3

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

Teste 3

Teste 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)

Teste 4

Teste 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)

Teste 5

Teste 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)

Teste 6



Diverta-se!

Ell
fonte
38
Esses gifs são muito gratificantes +1
Adnan
24
Ficarei impressionado se alguém responder isso com êxito com qualquer quantidade de código.
DavidC
5
Como você fez os gifs? E há quanto tempo isso está em andamento?
J Atkins
10
@JAtkin Da mesma forma que todo mundo deveria: Eu escrevi uma solução :) Se você está perguntando sobre os detalhes, usei o Cairo para os quadros individuais e depois usei o ImageMagick para criar os gifs (BTW, se alguém quiser produzir a animação) forma, ou seja, pela primeira geração dos quadros e, em seguida, usando uma ferramenta externa para transformá-los em animação, estou totalmente bem com isso, contanto que você especificar a dependência da ferramenta em seu post. Só para ficar claro, é seu programa que deve invocar a ferramenta, não o usuário.)
Ell 25/11
5
@Anko A boa notícia é que você não precisa se preocupar com isso: é garantido que esta situação não ocorra na entrada; veja a parte "sem roda dentada cruza o casco convexo ...", aquela com a imagem com as três regiões sombreadas. De maneira mais geral, a corrente cruza cada roda dentada apenas uma vez, de acordo com a ordem das rodas dentadas, mesmo que pareça passar perto de uma roda dentada mais de uma vez.
Ell

Respostas:

42

JavaScript (ES6), 2557 1915 1897 1681 bytes

Isso não é realmente um super-jogador de golfe ; é reduzido - parcialmente à mão - mas isso não é nada de especial. Sem dúvida, poderia ser mais curto se eu tivesse jogado mais antes da minificação, mas já passei (mais do que) tempo suficiente nisso.

Edit: Ok, então eu gastei mais tempo nele e joguei o código mais antes da minificação (desta vez, muito manualmente). O código ainda está usando a mesma abordagem e estrutura geral, mas mesmo assim eu acabei economizando 642 bytes. Não é tão pobre, se eu digo. Provavelmente perdi algumas oportunidades de economia de bytes, mas, neste momento, nem tenho mais certeza de como funciona. A única coisa diferente em termos de saída é que agora ela usa cores ligeiramente diferentes que podem ser escritas de maneira mais concisa.

Editar 2 (muito mais tarde): salvou 18 bytes. Agradeço a ConorO'Brien nos comentários por apontar o óbvio óbvio de que eu tinha perdido totalmente.

Edit 3: Então, imaginei que eu faria engenharia reversa do meu próprio código, porque, francamente, não conseguia me lembrar de como o havia feito, e perdi as versões não destruídas. Então eu passei, e eis que encontrei outros 316 bytes para economizar, reestruturando e praticando micro-golfe.

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}))})}}

A função acima anexa um elemento SVG (incluindo animações) ao documento. Por exemplo, para exibir o segundo caso de teste:

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

Parece funcionar - pelo menos aqui no Chrome.

Experimente no trecho abaixo (clique nos botões para desenhar cada um dos casos de teste do OP).

O código desenha os dentes da corrente e da engrenagem como traços tracejados. Em seguida, ele usa animateelementos para animar o stroke-dashoffsetatributo. O elemento SVG resultante é independente; não há animação orientada por JS ou estilo CSS.

Para alinhar bem as coisas, o anel de dentes de cada engrenagem é realmente desenhado como um caminho que consiste em dois arcos, para que o caminho possa começar exatamente no ponto tangente onde a corrente toca. Isso torna muito mais simples alinhá-lo.

Além disso, parece que há muitos erros de arredondamento ao usar os traços tracejados do SVG. Pelo menos, foi o que vi; quanto mais longa a corrente, pior seria a malha com cada engrenagem sucessiva. Portanto, para minimizar o problema, a cadeia é composta de vários caminhos. Cada caminho consiste em um segmento de arco em torno de uma marcha e uma linha reta para a próxima marcha. Seus desvios são calculados para corresponder. A parte fina "interna" da cadeia, no entanto, é apenas um caminho de loop único, pois não é animado.

Flambino
fonte
2
Parece ótimo! Parabéns por responder a um desafio antigo (ish)!
Ell
1
-2 bytes:R=g=>...
Conor O'Brien
1
@Flambino, eu gosto da sua solução para esse desafio e lamento muito que você tenha perdido a fonte original, fiz alguma engenharia reversa para recuperá-la, ela pode ser encontrada aqui: gist.github.com/micnic/6aec085d63320229a778c6775ec7f9aa, também a minifiquei manualmente para 1665 bytes (pode ser minified mais, mas eu sou preguiçoso hoje)
micnic
1
@micnic Obrigado! Vou ter que verificar isso! E não se preocupe, eu também consegui fazer a engenharia reversa, por isso tenho uma versão mais legível. Mas, dang, 16 bytes a menos? Parabéns! Definitivamente vou dar uma olhada quando encontrar tempo
Flambino
1
@ Flambino, essencialmente o maior impacto no tamanho do arquivo foi a estrutura svg, eu não coloquei nada em um <g>, mas coloquei diretamente na raiz svg. Também encontrado um lugar onde você transformou bandeira varredura e bandeira do arco grande de booleano para o número usando 1*x, mas você poderia usar+x
micnic
40

Bytes C # 3566

Não é golfe, mas funciona (acho)

Ungolfed no histórico de edições.

Usa Magick.NET para renderizar 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;}}

A classe P tem uma função F; Exemplo:

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);}

insira a descrição da imagem aqui

TFeld
fonte
2
Obrigado por postar uma versão golfed! Um pequeno detalhe: a primeira roda dentada no seu gif gira no sentido anti-horário; o primeiro pinhão deve sempre girar no sentido horário.
Ell
Eu só vi c # de passagem, mas você precisa do publicmodificador antes de cada campo da sua classe?
precisa
1
@JAtkin, de fato, tudo isso é desnecessário até onde eu sei. Em outros assuntos, o PointF é realmente System.Drawing.PointF (semelhante para List, Color e Math); portanto, as usingcláusulas correspondentes devem ser incluídas ou os tipos totalmente qualificados quando usados ​​e a referência ao System.Drawing deve ser observada. na resposta (se deve adicionar à pontuação que eu não sei). Resposta impressionante de qualquer maneira.
precisa saber é o seguinte
@JAtkin Eu tenho duas classes, S e P, então os campos em S são todos públicos. Não tenho certeza se eles são estritamente necessário, mas eu acho que sim ..
TFeld
3

JavaScript (ES6) 1626 bytes

Esta solução é o resultado da engenharia reversa da solução da @ Flambino, eu a publico com ele.

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)}}

A versão não destruída:

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();

micnic
fonte
1
Você pode salvar mais de 250 bytes usando esta ferramenta.