Sprocket Science: анимация системы с цепным приводом

97

Целью этой задачи является создание анимации системы цепного привода , состоящей из набора звездочек, соединенных вместе цепью .

основные требования

Ваша программа получит список звездочек , указанных в виде (x, y, radius)триплетов. В результате чего система цепного привода состоит из этих цепных колес, соединенных друг с другом посредством замкнутой цепи тугой , проходящей через каждый из них, в порядке . Ваша цель - создать бесконечно цикличную анимацию , показывающую систему в движении. Например, учитывая вход

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

, вывод должен выглядеть примерно так

Пример 1,

Система координат должна быть такой, чтобы ось x указывала вправо, а ось y была направлена ​​вверх. Вы можете предположить, что радиусы являются четными числами, большими или равными 8 (мы увидим, почему это важно позже.) Вы также можете предположить, что есть как минимум две звездочки и что звездочки не пересекаются друг с другом. В блокахвходные данные не слишком критичны. Все примеры и тестовые случаи в этом посте используют пиксели в качестве входных единиц (поэтому, например, радиус средней звездочки на предыдущем рисунке равен 24 пикселям;) старайтесь не слишком сильно отклоняться от этих единиц. В остальной части задачи пространственные величины понимаются в тех же единицах, что и входные данные - обязательно соблюдайте правильные пропорции! В размерах продукции должны быть немного больше , чем ограничительная рамка всех звездочек, достаточно большой , так что вся система видна. В частности, абсолютные положения звездочек не должны влиять на производительность; только их относительные положения (так, например, если бы мы сместили все звездочки в приведенном выше примере на одну и ту же величину, результат останется прежним.)

Цепь должна касаться звездочек, через которые она проходит во всех точках контакта, и прямо везде. Цепь должна проходить через звездочки так, чтобы соседние сегменты цепи (то есть части цепи между двумя звездочками, которые встречаются в одной звездочке) не пересекались друг с другом.

Пересечение цепи,

Например, хотя приведенная выше левая система действительна, средняя - нет, поскольку два соседних сегмента цепи, которые проходят через нижнюю левую звездочку, пересекаются. Тем не менее, отметим , что правая система является действительным, так как две пересекающиеся сегменты цепи не являются смежными (эта система производится на другой вход , чем два других, хотя.)

Для простоты (r), вы можете предположить, что никакая звездочка не пересекает выпуклую оболочку двух соседних звездочек или выпуклую оболочку каждого из ее соседей и другого соседа. Другими словами, верхняя звездочка на приведенной ниже схеме не может пересекать какие-либо затененные области.

исключение

Цепные сегменты могут пересекать звездочки, отличные от тех, через которые они проходят (например, в последнем тестовом примере). В этом случае цепь всегда должна появляться перед звездочками.

Визуальные требования

Цепочка должна состоять из ряда звеньев переменной ширины. Ширина узкой ссылки должна составлять около 2, а ширина широкой ссылки должна составлять около 5. Длина обоих типов ссылок должна быть примерно одинаковой. периодцепи, то есть общая длина широкой / узкой пары звеньев, должна быть ближайшим к 4π числом, которое соответствует целому числу раз в длине цепочки. Например, если длина цепочки равна 1000, то ее период должен быть 12,5, что является ближайшим числом к ​​4π (12,566 ...), которое соответствует целому числу раз (80) в 1000. Важно, чтобы период соответствовал целому числу раз в длине цепочки, чтобы не было артефактов в точке, где цепочка оборачивается.

цепь


Звездочка радиуса 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
Эти гифки очень радуют +1
Аднан
24
Я буду впечатлен, если кто-нибудь ответит успешно с любым количеством кода.
DavidC
5
Как вы делали гифки? И как долго это было в работе?
Дж Аткин
10
@JAtkin Точно так же, как и всем остальным: я написал решение :) Если вы спрашиваете об особенностях, я использовал Cairo для отдельных кадров, а затем использовал ImageMagick для создания GIF-файлов (кстати, если кто-то захочет создать анимацию, То есть, сначала создав кадры, а затем используя внешний инструмент, чтобы превратить их в анимацию, я полностью согласен с этим, пока вы указываете зависимость от инструмента в своем посте. Просто чтобы прояснить, это ваше Программа, которая должна вызывать инструмент, а не пользователя.)
Ell
5
@Anko Хорошая новость заключается в том, что вам не нужно об этом беспокоиться: эта ситуация гарантированно не произойдет на входе; см. часть «без звездочки пересекает выпуклый корпус ...», часть с изображением с тремя заштрихованными областями. В более общем смысле цепь пересекает каждую звездочку только один раз, в соответствии с порядком звездочек, даже если она выглядит так, как будто она проходит рядом со звездочкой более одного раза.
Ell

Ответы:

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 (включая анимацию) к документу. Например, для отображения второго контрольного примера:

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

Кажется, работает удовольствие - по крайней мере, здесь, в Chrome.

Попробуйте это в приведенном ниже фрагменте (нажатие на кнопки приведет к созданию каждого из тестовых примеров OP).

Код рисует зубья цепи и зубчатой ​​передачи штриховыми штрихами. Затем он использует animateэлементы для анимации stroke-dashoffsetатрибута. Результирующий элемент SVG является автономным; нет анимации на основе JS или CSS-стилей.

Чтобы все выстроилось в ряд, зубчатое кольцо каждого зубчатого колеса фактически нарисовано как путь, состоящий из двух дуг, поэтому путь может начинаться прямо в точке касания, где цепочка касается. Это значительно упрощает его выравнивание.

Кроме того, кажется, что при использовании штриховых штрихов SVG возникает много ошибок округления. По крайней мере, это то, что я видел; чем длиннее цепь, тем хуже она будет сцепляться с каждым последующим механизмом. Таким образом, чтобы свести к минимуму проблему, цепь на самом деле состоит из нескольких путей. Каждый путь состоит из дугообразного сегмента вокруг одной передачи и прямой линии на следующую передачу. Их рычаги рассчитываются для соответствия. Тонкая «внутренняя» часть цепочки, однако, представляет собой просто петлю, поскольку она не анимирована.

Flambino
источник
2
Выглядит отлично! Престижность за ответ на давний вызов!
Ell
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);}

введите описание изображения здесь

TFeld
источник
2
Спасибо за публикацию версии для гольфа! Незначительный спор: первая звездочка в вашем gif вращается против часовой стрелки; Первая звездочка всегда должна вращаться по часовой стрелке.
Ell
Я видел только C # мимоходом, но нужен ли вам publicмодификатор перед каждым полем в вашем классе?
Дж Аткин
1
@Jatkin действительно, это все ненужно, насколько я могу судить. В остальном PointF на самом деле является System.Drawing.PointF (аналогично для List, Color и Math), поэтому usingдолжны быть включены соответствующие пункты или полностью квалифицированные типы при использовании, а также ссылка на System.Drawing. в ответе (должно ли это прибавиться к счету, я не знаю). Впечатляющий ответ в любом случае.
VisualMelon
@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();

micnic
источник
1
Вы можете сохранить более 250 байтов, используя этот инструмент.