Sprocket Science: Animating a Chain Drive System


97

El objetivo de este desafío es producir una animación de un sistema de transmisión por cadena , compuesto por un conjunto de engranajes de piñón conectados entre sí por una cadena .

requerimientos generales

Su programa recibirá una lista de ruedas dentadas , especificadas como (x, y, radius)trillizos. El sistema de accionamiento de la cadena resultante se compone de estas ruedas dentadas, conectadas entre sí por una cadena tensa cerrada que pasa sobre cada una de ellas, en orden . Su objetivo es producir una animación de bucle infinito , que muestre el sistema en movimiento. Por ejemplo, dada la entrada

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

, la salida debería verse algo así

Ejemplo 1.

El sistema de coordenadas debe ser tal que el eje x apunte hacia la derecha y el eje y apunte hacia arriba. Puede suponer que los radios son números pares mayores o iguales a 8 (veremos por qué esto importa más adelante). También puede suponer que hay al menos dos ruedas dentadas y que las ruedas dentadas no se cruzan entre sí. Las unidadesde la entrada no son demasiado críticos. Todos los ejemplos y casos de prueba en esta publicación usan píxeles como unidades de entrada (por lo tanto, por ejemplo, el radio de la rueda dentada central en la figura anterior es de 24 píxeles;) trate de no desviarse demasiado de estas unidades. En el resto del desafío, se entiende que las cantidades espaciales se dan en las mismas unidades que la entrada; ¡asegúrese de mantener las proporciones correctas! Las dimensiones de la salida deben ser ligeramente más grandes que el cuadro delimitador de todas las ruedas dentadas, lo suficientemente grandes como para que todo el sistema sea visible. En particular, las posiciones absolutas de las ruedas dentadas no deberían afectar la salida; solo sus posiciones relativas deberían (así, por ejemplo, si cambiamos todas las ruedas dentadas en el ejemplo anterior por la misma cantidad, la salida seguirá siendo la misma).

La cadena debe ser tangente a las ruedas dentadas sobre las que pasa en todos los puntos de contacto, y en línea recta en cualquier otro lugar. La cadena debe pasar sobre las ruedas dentadas de manera que los segmentos de cadena adyacentes (es decir, partes de la cadena entre dos ruedas dentadas, que se encuentran en la misma rueda dentada) no se crucen entre sí.

Intersección de la cadena.

Por ejemplo, mientras que el sistema izquierdo de arriba es válido, el del medio no lo es, ya que los dos segmentos de cadena adyacentes que pasan sobre la rueda dentada inferior izquierda se cruzan. Sin embargo, tenga en cuenta que el sistema correcto es válido, ya que los dos segmentos de la cadena de intersección no son adyacentes (sin embargo, este sistema es producido por una entrada diferente a los otros dos).

Para simplificar las cosas (r), puede suponer que ninguna rueda dentada se cruza con el casco convexo de sus dos ruedas dentadas vecinas, o las cascas convexas de cada uno de sus vecinos y su otro vecino. En otras palabras, la rueda dentada superior en el siguiente diagrama puede no cruzarse con ninguna de las regiones sombreadas.

Exclusión

Los segmentos de cadena pueden cruzarse con ruedas dentadas distintas de las que pasan (como en el último caso de prueba). En este caso, la cadena siempre debe aparecer delante de las ruedas dentadas.

Requerimientos visuales

La cadena debe consistir en una serie de eslabones de anchos alternos. El ancho del enlace estrecho debe ser de aproximadamente 2, y el ancho del enlace ancho debe ser de aproximadamente 5. La longitud de ambos tipos de enlaces debe ser aproximadamente igual. El periodode la cadena, es decir, la longitud total de un par de enlaces ancho / estrecho, debe ser el número más cercano a 4π que se ajuste a un número entero de veces en la longitud de la cadena. Por ejemplo, si la longitud de la cadena es 1,000, entonces su período debe ser 12.5, que es el número más cercano a 4π (12.566 ...) que se ajusta a un número entero de veces (80) en 1,000. Es importante que el período se ajuste a un número entero de veces en la longitud de la cadena, para que no haya artefactos en el punto donde la cadena se enrolla.

Cadena


Una rueda dentada de radio R debe constar de tres partes concéntricas: un eje central , que debe ser un círculo de radio de aproximadamente 3; el cuerpo del piñón , alrededor del eje, que debería ser un círculo de radio alrededor de R - 4.5; y el borde de la rueda dentada , alrededor del cuerpo, que debería ser un círculo de radio alrededor de
R - 1.5. El borde también debe contener los dientes del piñón , que deben tener un ancho de aproximadamente 4; El tamaño y el espacio de los dientes deben coincidir con los tamaños de los eslabones de la cadena, para que encajen perfectamente.

Rueda de espigas

El período de los dientes de la rueda dentada, es decir, la distancia entre dos dientes consecutivos a lo largo de la circunferencia de la rueda dentada, debe coincidir con el período de la cadena. Como el período es de aproximadamente 4π, y dado que se garantiza que el radio de la rueda dentada sea uniforme, el período debe caber en la circunferencia de la rueda dentada un número casi entero de veces, de modo que no debería haber artefactos notables en el punto donde los dientes de la rueda dentada se enrollan.

Puede usar cualquier combinación de colores para la cadena, las diferentes partes de la rueda dentada y el fondo, siempre que sean fácilmente distinguibles . El fondo puede ser transparente. Los ejemplos en este post se usan Color de la cadena #202020para la cadena, Eje de la rueda dentada y color de la llanta #868481para el eje y el borde Color del cuerpo del piñón #646361de la rueda dentada , y para el cuerpo de la rueda dentada.

Requisitos de animación

La primera rueda dentada en la lista de entrada debe girar en sentido horario ; El resto de las ruedas dentadas deben girar en consecuencia. La cadena debe moverse a una velocidad de aproximadamente 16π (aproximadamente 50) unidades por segundo; la velocidad de fotogramas depende de usted, pero la animación debería verse lo suficientemente suave.

La animación debería repetirse sin problemas .

Conformidad

Algunos de los atributos y proporciones visuales se especifican intencionalmente solo de manera aproximada: no es necesario que coincidan exactamente . La salida de su programa no tiene que ser una réplica de píxel a píxel de los ejemplos dados aquí, pero debería ser similar. En particular, las proporciones exactas de la cadena y las ruedas dentadas, y la forma exacta de los eslabones y los dientes de la cadena, son flexibles.

Los puntos más importantes a seguir son estos:

  • La cadena debe pasar sobre las ruedas dentadas, en el orden de entrada, desde la dirección correcta.
  • La cadena debe ser tangente a las ruedas dentadas en todos los puntos de contacto.
  • Los eslabones de la cadena y los dientes de las ruedas dentadas deben engranarse perfectamente, al menos para corregir la separación y la fase.
  • El espacio entre los eslabones de la cadena y los dientes de las ruedas dentadas debe ser tal que no haya artefactos notables en el punto donde se envuelven.
  • Las ruedas dentadas deben girar en la dirección correcta.
  • La animación debería repetirse sin problemas.

Como nota final, mientras que, técnicamente, el objetivo de este desafío es escribir el código más corto, si tienes ganas de ser creativo y producir un resultado más elaborado, ¡hazlo!

Desafío

Escriba un programa o una función , tome una lista de ruedas dentadas y produzca la animación correspondiente del sistema de transmisión por cadena, como se describió anteriormente.

Entrada y salida

Puede tomar la entrada a través de la línea de comando , a través de STDIN , como argumentos de función , o utilizando un método equivalente . Puede usar cualquier formato conveniente para la entrada, pero asegúrese de especificarlo en su publicación.

Como salida , puede mostrar la animación directamente , producir un archivo de animación (por ejemplo, un GIF animado) o producir una secuencia de archivos de marco (sin embargo, hay una pequeña penalización en este caso; ver más abajo). Si usa la salida de archivo, asegúrese de que el número de fotogramas sea razonable (los ejemplos en esta publicación usan muy pocos fotogramas;) el número de fotogramas no tiene que ser mínimo, pero no debe producir demasiados fotogramas superfluos. Si genera una secuencia de fotogramas, asegúrese de especificar la velocidad de fotogramas en su publicación.

Puntuación

Este es el código de golf . La respuesta más corta , en bytes, gana.

Penalización de + 10%   Si su programa produce una secuencia de cuadros como salida, en lugar de mostrar la animación directamente o producir un solo archivo de animación, agregue 10% a su puntaje.

Casos de prueba

Prueba 1

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

Prueba 1

Prueba 2

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

Prueba 2

Prueba 3

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

Prueba 3

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

Prueba 4

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

Prueba 5

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

Prueba 6



¡Que te diviertas!


38
Estos gifs son muy satisfactorios +1
Adnan

24
Me impresionará si alguien responde esto con éxito con cualquier cantidad de código.
DavidC

55
¿Cómo hiciste los gifs? ¿Y cuánto tiempo ha estado en las obras?
J Atkin

10
@JAtkin De la misma manera que todos los demás deberían: escribí una solución :) Si está preguntando acerca de los detalles, usé Cairo para los cuadros individuales, y luego usé ImageMagick para crear los gifs (por cierto, si alguien quiere producir la animación esto manera, es decir, mediante la generación primera de las tramas y luego usando una herramienta externa para convertirlas en animación, estoy totalmente bien con eso, siempre y cuando se especifica la dependencia de la herramienta en su puesto. para que quede claro, es su programa que debería invocar la herramienta, no el usuario.)
Ell

55
@Anko La buena noticia es que no tiene que preocuparse por ello: se garantiza que esta situación no sucederá en la entrada; vea la parte "sin rueda dentada se cruza con el casco convexo ...", el que tiene la imagen con las tres regiones sombreadas. En términos más generales, la cadena cruza cada rueda dentada solo una vez, de acuerdo con el orden de las ruedas dentadas, incluso si parece que pasa cerca de una rueda dentada más de una vez.
Ell

Respuestas:


42

JavaScript (ES6), 2557 1915 1897 1681 bytes

Esto no es super- golfista realmente; está minimizado, en parte a mano, pero eso no es nada especial. Sin duda, podría ser más corto si hubiera jugado más golf antes de minificar, pero ya he dedicado (más que) suficiente tiempo a esto.

Editar: Ok, así que pasé más tiempo en él y jugué el código más antes de minificar (esta vez de forma muy manual). El código sigue usando el mismo enfoque y estructura general, pero aun así terminé ahorrando 642 bytes. No está mal, si lo digo yo mismo. Probablemente perdí algunas oportunidades de ahorro de bytes, pero en este momento ni siquiera estoy seguro de cómo funciona. Lo único que es diferente en términos de salida, es que ahora usa colores ligeramente diferentes que podrían escribirse más brevemente.

Edición 2 (mucho más tarde): guardado 18 bytes. Gracias a ConorO'Brien en los comentarios por señalar lo cegadoramente obvio que me había perdido por completo.

Edición 3: Entonces, pensé en aplicar ingeniería inversa a mi propio código, porque, francamente, no podía recordar cómo lo había hecho, y perdí las versiones sin golf. Así que revisé, y he aquí que encontré otros 316 bytes para ahorrar reestructurando y haciendo un poco de micro golf.

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

La función anterior agrega un elemento SVG (incluidas animaciones) al documento. Por ejemplo, para mostrar el segundo caso de prueba:

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

Parece ser una delicia, al menos aquí en Chrome.

Pruébelo en el fragmento a continuación (al hacer clic en los botones se dibujarán cada uno de los casos de prueba de OP).

El código dibuja la cadena y los dientes del engranaje como trazos discontinuos. Luego usa animateelementos para animar el stroke-dashoffsetatributo. El elemento SVG resultante es autocontenido; no hay animación basada en JS o estilo CSS.

Para que las cosas se alineen bien, el anillo de dientes de cada engranaje se dibuja en realidad como una ruta que consta de dos arcos, por lo que la ruta puede comenzar justo en el punto tangente donde toca la cadena. Esto hace que sea mucho más simple alinearlo.

Además, parece que hay muchos errores de redondeo al usar los trazos discontinuos de SVG. Al menos, eso es lo que vi; cuanto más larga sea la cadena, peor se engranará con cada equipo sucesivo. Entonces, para minimizar el problema, la cadena está compuesta de varias rutas. Cada ruta consiste en un segmento de arco alrededor de un engranaje y una línea recta al siguiente engranaje. Sus compensaciones de tablero se calculan para que coincidan. Sin embargo, la parte delgada "interna" de la cadena es solo una ruta de bucle, ya que no está animada.


2
¡Se ve muy bien! ¡Felicitaciones por responder a un viejo desafío (ish)!
Ell

1
-2 bytes:R=g=>...
Conor O'Brien

1
@Flambino, me gusta su solución para este desafío y lamenté mucho que haya perdido la fuente original, hice un poco de ingeniería inversa para recuperarla, se puede encontrar aquí: gist.github.com/micnic/6aec085d63320229a778c6775ec7f9aa también lo minimicé manualmente a 1665 bytes (se puede minimizar más, pero hoy soy vago)
micnic

1
@micnic ¡Gracias! ¡Tendré que comprobar eso! Y no se preocupe, también logré realizar ingeniería inversa, así que tengo una versión más legible. Pero, dang, ¿16 bytes menos? ¡Prestigio! Definitivamente lo echaré un vistazo cuando pueda encontrar el tiempo
Flambino

1
@Flambino, esencialmente el impacto más grande en el tamaño del archivo fue la estructura svg, no puse todo en una <g>, sino que lo puse directamente en la raíz svg. También encontré un lugar donde transformó la bandera de barrido y la bandera de arco grande de booleano a número usando 1*x, pero podría usar+x
micnic el

40

C # 3566 bytes

No juega golf en absoluto, pero funciona (creo)

Ungolfed en la historia de edición.

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

La clase P tiene una función F; Ejemplo:

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

ingrese la descripción de la imagen aquí


2
¡Gracias por publicar una versión de golf! Una pequeña objeción: la primera rueda dentada en tu gif gira en sentido antihorario; la primera rueda dentada siempre debe girar en sentido horario.
Ell

Solo he visto pasar C #, pero ¿necesitas el publicmodificador antes de cada campo de tu clase?
J Atkin

1
@JAtkin, de hecho, todos son innecesarios por lo que puedo decir. En otros asuntos, PointF es realmente System.Drawing.PointF (similar para List, Color y Math), por lo usingque deben incluirse las cláusulas correspondientes , o los tipos completamente calificados cuando se usan, y la referencia a System.Drawing debe tenerse en cuenta en la respuesta (si debería agregar al puntaje no lo sé). Impresionante respuesta de todos modos.
VisualMelon

@JAtkin Tengo dos clases, S y P, por lo que los campos en S son públicos. No estoy seguro si son estrictamente necesarios, pero creo que sí ..
TFeld

3

JavaScript (ES6) 1626 bytes

Esta solución es el resultado de la ingeniería inversa de la solución de @ Flambino, la publico con su acuerdo.

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

La versión sin golf:

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
Puede guardar más de 250 bytes con esta herramienta.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.