Наука зірочок: анімація системи ланцюгового приводу


96

Мета цього завдання - створити анімацію системи приводу ланцюга , що складається з набору зубчастих передач, з'єднаних між собою ланцюгом .

Загальні вимоги

Вашій програмі буде наданий список зірочок , вказаний у вигляді (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
Ці gif дуже задовольняють +1
Adnan

24
Я буду вражений, якщо хтось успішно відповість на це будь-якою кількістю коду.
DavidC

5
Як ви робили gif? І як довго це було у творах?
J Atkin

10
@JAtkin Так само, як і всі: я написав рішення :) Якщо ви запитуєте про специфіку, я використовував Каїр для окремих кадрів, а потім використовував ImageMagick для створення gif-файлів (BTW, якщо хто хоче створити анімацію цього Таким чином, спочатку генеруючи кадри, а потім використовуючи зовнішній інструмент, щоб перетворити їх на анімацію, я з цим цілком добре , доки ви вкажете залежність від інструменту у своєму дописі. Просто для того, щоб було зрозуміло, це ваш програма, яка повинна викликати інструмент, а не користувача.)
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.

Спробуйте в фрагменті нижче (натискання кнопок намалює кожен із тестових випадків ОП).

Код малює зубці ланцюга та зубчастого колеса як штрихові штрихи. Потім він використовує animateелементи, щоб анімувати stroke-dashoffsetатрибут. Отриманий елемент SVG є автономним; немає анімації, керованої JS, або стилів CSS.

Для того, щоб все добре вирівнялося, кільце зубців кожної передачі насправді намальовано як шлях, що складається з двох дуг, тому шлях може починатися прямо в дотичній точці, до якої торкається ланцюг. Це робить його набагато простіше вирівняти.

Крім того, здається, що існує багато помилок округлення при використанні штрихових штрихів SVG. Принаймні, це я бачив; Чим довший ланцюг, тим гірше він буде сіткою з кожною послідовною передачею. Отже, щоб мінімізувати проблему, ланцюг насправді складається з декількох шляхів. Кожен шлях складається з стрілочного сегмента навколо однієї передачі та прямої лінії до наступної передачі. Їх компенсації тире розраховані на відповідність. Однак тонка "внутрішня" частина ланцюга - це лише один циклічний шлях, оскільки він не анімований.


2
Виглядає здорово! Кудо за відповідь на старий (іш) виклик!
Ел

1
-2 байти:R=g=>...
Conor O'Brien

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 байт

Зовсім не гольф, але працює (я думаю)

Ungolfed в історії редагування.

Використовує Magick.NET для надання gif.

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

Клас P має функцію F; Приклад:

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

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


2
Дякуємо, що опублікували версію для гольфу! Незначна каламбур: перша зірочка у вашому gif обертається проти годинникової стрілки; перша зірочка завжди повинна обертатися за годинниковою стрілкою.
Ell

Я бачив лише C # мимо, але чи потрібен вам publicмодифікатор перед кожним полем у вашому класі?
J Atkin

1
@JAtkin дійсно, все це непотрібно, наскільки я можу сказати. В інших питаннях PointF - це дійсно System.Drawing.PointF (подібний до списку, кольорів та математики), тому відповідні usingпункти повинні бути включені або типи, які повністю використовуються при використанні, і посилання на System.Drawing слід зазначити. у відповідь (чи варто це додавати до оцінки, я не знаю). Вражаюча відповідь як завгодно.
VisualMelon

@JAtkin У мене є два класи, S і P, тому поля в S є загальнодоступними. Не впевнений, чи потрібні вони строго, але я вважаю, що так.
TFeld

3

JavaScript (ES6) 1626 байт

Це рішення є результатом зворотної інженерії рішення @ Фламбіно, я публікую його за його згодою.

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

Негольована версія:

class Vector {

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

    add(vector) {

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

    subtract(vector) {

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

    multiply(scalar) {

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

    rotate(radians) {

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

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

    cross(vector) {

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

    toString() {

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

class Gear {

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

    getVector() {

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

const setAttributes = (element, attributes) => {

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

const createElement = (tagName, attributes) => {

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

    setAttributes(element, attributes);

    return element;
};

const cloneElement = (element, attributes) => {

    const clone = element.cloneNode();

    setAttributes(clone, attributes);

    return clone;
};

const createPath = (attributes) => {

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

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

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

const loopGears = (gears, callback) => {

    const length = gears.length;

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

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

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

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

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

const renderGears = (data) => {

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

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

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

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

        return gear;
    });

    const firstGear = gears[0];

    w -= x;
    h -= y;

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

    let chainPath = '';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

clear();


1
За допомогою цього інструменту можна зберегти більше 250 байт .
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.