From b13d8fe00ddfb2c6bd3602c935ac1018c89f41e4 Mon Sep 17 00:00:00 2001 From: Pomax Date: Fri, 7 Aug 2020 15:47:52 -0700 Subject: [PATCH] bezier.js, as ES6 code --- chapters/whatis/interpolation.js | 24 +- images/chapters/introduction/cubic.png | Bin 9853 -> 10071 bytes images/chapters/introduction/quadratic.png | Bin 8609 -> 8703 bytes images/chapters/whatis/interpolation.png | Bin 27630 -> 27762 bytes lib/bezierjs/bezier.js | 964 ------------------ lib/bezierjs/lib/bezier.js | 964 ------------------ lib/bezierjs/lib/normalise-svg.js | 197 ---- lib/bezierjs/lib/poly-bezier.js | 68 -- lib/bezierjs/lib/svg-to-beziers.js | 41 - lib/bezierjs/lib/utils.js | 893 ---------------- lib/bezierjs/poly-bezier.js | 68 -- lib/bezierjs/svg-to-beziers.js | 41 - lib/bezierjs/utils.js | 893 ---------------- lib/custom-element/api/graphics-api.js | 93 +- lib/custom-element/api/types/bezier.js | 114 +-- lib/custom-element/api/types/bezier/base.js | 40 - .../api/types/bezier/bezier-cubic.js | 47 - .../api/types/bezier/bezier-quadratic.js | 39 - lib/custom-element/api/types/bezier/bezier.js | 134 --- lib/custom-element/api/types/point.js | 85 -- lib/custom-element/api/types/vector.js | 80 ++ lib/custom-element/graphics-element.js | 2 +- lib/custom-element/lib/bezierjs/bezier.js | 963 +++++++++++++++++ .../lib}/bezierjs/normalise-svg.js | 120 ++- .../lib/bezierjs/poly-bezier.js | 70 ++ .../lib/bezierjs/svg-to-beziers.js | 45 + lib/custom-element/lib/bezierjs/utils.js | 906 ++++++++++++++++ package.json | 4 +- tools/build/rewrite-graphics-element.js | 2 +- 29 files changed, 2234 insertions(+), 4663 deletions(-) delete mode 100644 lib/bezierjs/bezier.js delete mode 100644 lib/bezierjs/lib/bezier.js delete mode 100644 lib/bezierjs/lib/normalise-svg.js delete mode 100644 lib/bezierjs/lib/poly-bezier.js delete mode 100644 lib/bezierjs/lib/svg-to-beziers.js delete mode 100644 lib/bezierjs/lib/utils.js delete mode 100644 lib/bezierjs/poly-bezier.js delete mode 100644 lib/bezierjs/svg-to-beziers.js delete mode 100644 lib/bezierjs/utils.js delete mode 100644 lib/custom-element/api/types/bezier/base.js delete mode 100644 lib/custom-element/api/types/bezier/bezier-cubic.js delete mode 100644 lib/custom-element/api/types/bezier/bezier-quadratic.js delete mode 100644 lib/custom-element/api/types/bezier/bezier.js delete mode 100644 lib/custom-element/api/types/point.js create mode 100644 lib/custom-element/api/types/vector.js create mode 100644 lib/custom-element/lib/bezierjs/bezier.js rename lib/{ => custom-element/lib}/bezierjs/normalise-svg.js (67%) create mode 100644 lib/custom-element/lib/bezierjs/poly-bezier.js create mode 100644 lib/custom-element/lib/bezierjs/svg-to-beziers.js create mode 100644 lib/custom-element/lib/bezierjs/utils.js diff --git a/chapters/whatis/interpolation.js b/chapters/whatis/interpolation.js index 9ef4b40d..67edc3dd 100644 --- a/chapters/whatis/interpolation.js +++ b/chapters/whatis/interpolation.js @@ -21,13 +21,15 @@ drawBasics() { translate(this.height, 0); - line({x:0, y:0}, {x:0, y:this.height}); + line(0, 0, 0, this.height); + this.curve.drawSkeleton(); text(`Second interpolation, between each generated pair`, {x:5, y:15}); translate(this.height, 0); - line({x:0, y:0}, {x:0, y:this.height}); + line(0, 0, 0, this.height); + this.curve.drawSkeleton(); text(`Curve points generated this way`, {x:5, y:15}); } @@ -36,7 +38,7 @@ drawPointCurve() { setStroke(`lightgrey`); for(let i=1, e=50, p; i<=e; i++) { p = this.curve.get(i/e); - circle(p, 1); + circle(p.x, p.y, 1); } } @@ -57,12 +59,14 @@ setIterationColor(i) { } drawFirstInterpolation(p, i) { + p = p.map(v => new Vector(v)); + let np2 = p[1].subtract(p[1].subtract(p[0]).scale(1 - i/100)); - circle(np2, 5); + circle(np2.x, np2.y, 5); text(`${i}%`, np2.add({x:10,y:0})); let np3 = p[2].subtract(p[2].subtract(p[1]).scale(1 - i/100)); - circle(np3, 5); + circle(np3.x, np3.y, 5); text(`${i}%`, np3.add({x:-10,y:-15})); return [np2, np3]; @@ -71,12 +75,12 @@ drawFirstInterpolation(p, i) { drawSecondInterpolation(np2, np3, i) { translate(this.height, 0); - line(np2, np3); - circle(np2, 5); - circle(np3, 5); + line(np2.x, np2.y, np3.x, np3.y); + circle(np2.x, np2.y, 5); + circle(np3.x, np3.y, 5); let np4 = np3.subtract(np3.subtract(np2).scale(1 - i/100)); - circle(np4, 2); + circle(np4.x, np4.y, 2); text(`${i}%`, np4.add({x:10,y:10})); return np4; @@ -84,7 +88,7 @@ drawSecondInterpolation(np2, np3, i) { drawOnCurve(np4, i) { translate(this.height, 0); - circle(np4, 2); + circle(np4.x, np4.y, 2); text(`ratio = ${i/100}`, np4.add({x:10,y:15})); } diff --git a/images/chapters/introduction/cubic.png b/images/chapters/introduction/cubic.png index a4fc51bbaadd108627f0919116f67213b61fd122..ec23f9434b67e746b12e125d587dc0e4f2905a98 100644 GIT binary patch literal 10071 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 XZpHGeh8pmt97^($oM@J?_N)H_Yqe@y literal 9853 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

Yy7tpsZ%afv$K9cea6BShi>NX zS#?wyVf3NZBoj|s3lH@4d`*+_|FP&ea8n99jWGAMf}{@%^FU?(oqm~YIlmzOf~?@D zr>CdLF8#gTwf)B6_e=1S`#L(flBTANhlhs)H*FpQ49_Vj(5%FxwT^!vG3V>&XTP`d z2t5H~*m0CeEkTWi-$Rgpg`$zXoc|#A#NNrtwExcsFbyj2-}Xq>2aU7#bhi*5Fan8z zov1y+&dE9dBTWXvFv9P7z+(YN#Kpx$Ka1wj8(Ce`lMtOBn4a%ULf|`#de#QP*#<#0 z8Doup*F=pwTCY#Mzdu%IXXo!<1m2Ga3-ks&s(eYc^1hfWfexIUtgNoC9@;WKHYR`f zE+C+c(PD^_Uyhd80Mk|+uT{l~y7IvO@9Wb*Tn|*$;d0v))66{CjN(6TOG`_ak5`;C zj#t2!Xld&&-v>)DFfncYkRd>WcvT0212=gvto_4*2OzQmlnlL_Y4lfRp|NC08Jjn* zgWG=OsDZ=hDpCOm3nK)j-@?kO0eI5FLO@ImiJ_q(5JL(Qk{GH&Cp$Zu8#nxcl>IYv28ZW$%B8Tm6_$>FGTY~-nN(=}NZ6l>tn*jJSLdD5{>_4n_;ZE^B6+))Y$jA-yrfXR~VS~vt zwisX<-#R*0w@P1O5p2Q6-Wt%cEq2?rtow9my*g6b*WVwWkZ=#eHTWFW>gpeK>j>f$|}NJ}F5_Rh0;k7!8m!s5qgCwKM(8C$RKFNzdLtF*1rY z|c5(9^iK9L6W2f~Q9_{HJ&6N{!g1)#05I~gEeM?_pMDJcOl#SrxI+p@Bk$;eu( zUANzHs0^WSh>39`rC{|C^$fa!Xhlg$iM#}(lb8SYVM0lN2><*o!w-M{Nhczb07x?e z*7+Kz{>^$Lf0z)1V%~e;+Q42PMel*=Kn$OPJ}#PPnHb3hl!^>(?tFBR`v{4MxKmH0 zjjSWi9~eR{&ttb=3oNXvT8a~&3x@-kkh1`)QoyJrj=Dn;d0R>Jq6S`yFO*nXnJq0X zr@#z)(Yq~;jf9kxHv0r;RPE=3xWY4bBK_4_9+~$LYM7G`RN2^X=Bby$phMBHCNoMq@_V>}^%2Q| zq*sPH`;yKF(HPe1MN5oyKx}w*ui8NVYvXNDM6umRI2n*u>14-Te*SaW?=trH0+gO5lp#|L2l-;NQCf*Z#f?;BL@7f8lMCDu>~|No|--Bn)Bu-2^9t7}76y0dw&` z{pw_t{(U(V9RAM`R(kCAasNG*AeY^XHaRw;cw;3^8r%2cy=qo$GL9d9pyVs}%g>@5y46Y9Rcc+KFm_VyYy zb8Bmgs1a-AsK={~6GfGEN;fR5`~?=ed-tMcX=RHumHP^JviGSgfKW8%{||sbAywjvid*fwAoSL_@1ti$m5YtP zOsf^JwJFaQs6Cv!KH+t22{ScjjmiC3Oudnzh;8Jc7Oc$RMC67V9S88i#he1dvE0)ES3OL@7}1Hq$CQCGKfNn#q$g2d1Vt+M___LDG827 za7j2OmzS+vDX(2KJ6wk|5F5i!QZ~QycI~*GK$$x*D5#`5Bq%8T{rmU#Vs{4202(4$ z#!(owHJvhR+8Z~ry97KvJz3bc?;`?+a8BUgBLeKb75{xe;G|5*e(E-r$A*;i`b2PUE}LYi&9$pnivew* zjlb7%W0Gc0O;3M0GNNr{YzztsfjiegL{yYtXea@|@snh>^D|ViDgIX{tAft3)VkcO ViakGh!Fe*2!X4$?1=5dS{tpR{0mT3S diff --git a/images/chapters/introduction/quadratic.png b/images/chapters/introduction/quadratic.png index 8e10c4ae3abff6c2afdb7a76cfe08249d5b9ca35..ab30ca127051d188d9ed4b8daa556fb4d8515be7 100644 GIT binary patch literal 8703 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 N>y{3@ME%M0{{>N~@OuCN literal 8609 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 ZC)b#GohIWQ{LTeJOG6i3aQWWD{{q;Ulh*(M diff --git a/images/chapters/whatis/interpolation.png b/images/chapters/whatis/interpolation.png index 7267adcd27f780f947d001b42bea514e7b895e4c..b90ef554593ae6aac241bef1c16acbb5a7630ece 100644 GIT binary patch literal 27762 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 Ls+i9=A9?)`iayVN literal 27630 zcmeFZ^;=bI)HS>nB?P3TTT;3^6j7w5yFt3UMOsR_8$m+4Hz6(E&8EA%q69Yj zw4P=Y58KP_P5v)2g$f&` zzPcMW&9EHFrIL<5b#2ye%IWTJ+(=6ixDdKXa}RITWx+!i36ea$T!}aV$AvSiANorf z{b2M~f>%+ih~!1@GCz|oi6YwOQ^q}hHNEg0T?CQb5By^uMJe|0t0hIsZj685<-r!W zd;BIiL*_#i(LX1W`zhG&Z6P@APCa=mA<;u+S!*e1$>iuMIGxK_ZPaf!C0Y0Tr|?DI zuU|nuJrYsT(Uwhn#fD$LU}9p57#bRS<93)PnmL~z&u$+i=rW(_n#1We|1OJw%S^le zKBna!Hx^iDl*9BvHc{FR{EU`?RQ@n;*<`M;n3z`tgoKRRt}3W(f`YJJch8^D^4V>K zF<6r`gP83097e+ILu;!@)73iRs~68t*82@Mhcds+lK<%<^$r!uTnr$zf-LqWvU?nN z)6Cfq3evuM5k2d*Th^soWfpsMfJGxyYC1|CBSAH(<1N@0fVxwC>p3i=NQXTr{2;*G zd|N&~VJiCq8TsmZr{qZjr#TLVSa_u?zEl-|FjAalGxV6J;GXB@nX>iH9Knr=byr5V zSVXz~hD=g&vbeM~m5@+VbBJMFV>_OC#TWP8S|)qTiTRn#QtvO(DW|6nBcr4I#EPRM zBMjPgR4r9BJ5WWKWv}*27vf&MUcPPSbnkoK2%&4*{QP_lF0SLh6cMFHy*L$nhlM3g z`e)nYkjv|9JDB6-QJ3(2QT!OKp}HfrrGz!kVr!$2F@;`t#>);qOTjFi|Y_>r!z{THkAGxXmYumSnxJu_WV} z_71Ydr8cSD^W%MTgm)U6XJyUCp4^Ct>*C9cx4 z=H?ZtSxHVB$?kpT31EU2{`~pVG0CykfV*O3M837HO`s++ zo@r?^Nmd)N;7P?zj#cwpWlK9ca?;48=(c=&9*W2M^lMZU=0u4O+MlcsAv28DW~2k@ z!VCFCmh$rQQpF{|1#-6sQbL*wL*hBB&~mr&Gm45r9d{<=6ck2{Jm2la-l7t6p<`hg zlnZ2k3|n4o@pW-=fh;a9wM_ihc!K!i#cLLpA4_e4guQO3n{qfznuveES4gO+Yc~hY zG~C>iyF%8}<^Hv`JSz)GNJzUdC@wcQH$*^CaJt^nJdd&~$_b{#&#IJ1ydz9S<^Ss! zZMDU8>vBg(|GqE9OuZvL82&ZVhf9Q^8JP%DVJnXX&1BZ`CbyY;sJSyH*34|JtwG+g z)7(N-RFuq`t+TUO_VACmxXySTFQh-;o+E#fmF*fFjCiAvn{bx~Eg0PIS%@}(bFfmd z^twIjVyaR2{P|A+Dq&Dq7!?`WAJq#%Mktjf4HZ>K7MIPNqw2z`v2oj~u>6%mMzEWY zBfM{Yi;5V&eftJ6v9jXoj1G5x))xx}yFM6;hRtk@7IJ)i{P*wQ#r0%sE{#0P`T}UN zSBND;Fl%l&8m@irxr?oJSoqQJm4oBugPKCV>#L1)XqZ^;%vBPndF_7vPAIGs`NfN= z3S%_x1rPf?JLImRwLftD&iku%8)+-xVCO2Xrr+7x+M09Q`E3D4wzKo2(Cy*-{iYkM zp59)$!tr0@W&{KTCMG6te=8?9_PZQid)@8moXoo)*j}HS7B}C!IUUSPfB5h`np*lb z1%>aoZ%@Hepk-iqre3mLIBphB%-_j5srlZ?$tllHt^PNoMzsT7Nu8zh+($PzK6>RM zU0A&%ShlRL$A%IT60~pL=(YMGfxYu1GP33JXypwfqoqo2_>IltA0L@D6Z(p}v&2Gv zdp<7}tv%m5Hzr0#Dqh~iM)wOR4V{jT4yi;oe=wwAHFX7&dQIg^fsN7mEH5kTo8@dZ ztMk4lxVcZCK4laXe0QFA4YXb9%v3HEmy$BPyFM$@X-><@p}ao~6h4e^zV#0y+HI#m2uklQ8-bcT^XiWnspr_5)6C3pB!!r2jTJE% zMlwOc#xA3p%01N?cqXd(bB_6B{{7jRVl8Nbuy+${%M%-WdkS*$mRcQ4=jTlKP=mU{ z!a`SY3t_#2w})+tDk{#?xlG!|vR<=-VXW)nXqG9z1-ZDmk_1#Z&Y%{BUHjX=pe}}H z>E#s_Abu{Dc;6M;3O*b>+zHds)4NZ6^4HormXXoOh8y)HFKIkipLbb7g>YF+y_J?m zjg5_E(5(I8kBocVjwK6ms3$a(F*7T$^|Z3GvY4$7^F=_zBqEY>aNvLp4-b1>t;Nr| zA2hnSyR*9->OO^o3yz4`fF3mMC^AXbjK}rsXwGxbyR@x>@MO>xPTZHk>RYqmMfg?c z;d&BGB8hLyp9a^HP?b_^=pGkX3X{b|#24>MCY|ZTG``hY!T2h6*p-|W@?<#c4!pd) z_L>S4*^QH6feUy0&eS{8~jFM=De) zS%*0<7|l1hLK<%Np{pAk(J?WG=k89<&fFU5u-Jt;zJ6jtsj7TpekTWLDu~c!E=T$k zMQU(=dSdCJb4_*S$^3EeHVopZEEnZhEM=GPIv9AHX0k(iJv*>f4#6zitp3Hw;B>vd zXnyDilVw~jtyNUhY$>TP)n%U_-i2Un9${)~+UeqC>%p$H&Xqf$$HWD;Hq%OEzUTEm z^x*RDz$Uj{fPuzN{<#!HU^C+ zi?yUu`C`WlRZdzENHY}aSn*JBaMzd5TwPu9vFLv*n>TzHvYg=n(Ob3IOR#3n4*B`> z=OBb_@9g9k7w=c5rCIbUx=LHlj#z%(!rI|lKhG?5W3LgicQpQ64RFH7kZGzl#kfdN9oaSwfo&COnoJvdIl>q_&{ z_3i$sYYvtBG!jBL4cYbWraYq>!>hDPAIadFTvd7JxayN88!%U;dTsCp1qGocoZQ^e zsi~-yPWDZgBI97ihd%6Irp`G|Y6+b5u|FPyv z)z@EZIjM-jIwS@w#N&vZWbP7Knw>H(*^!yd7_`^cfJ3VJUY=V_gi^%Y|O8) zkX}wsPT=lz6ryS0&)K-$38dIYm#HE z*&6GOvMv&kDSYTl8lrZJq2cT6U%>hSAVM~e>93{N1#|&F9m8~o+G5LjEP0`tH`i+x zz!A%oGQp$}>-7Zw0PUhe7elM`jI-q}4eQ1aUS2{a04CHJVVw6{L>VxVvtGDqJbWZ2 zCDnG{zBYU$s`;M-D38~`=Rl)9any?*@~!eg`e^=SW(f~?^9zFz-h ze4hi0%0xrx_3NzOGsn5~;sCwfOD)0s%N6R27EQrGfB#4@SM`oNv~+aauyJ-hq$Utcm~)4bAH)MCWi=j1<452t)_KdLCI0AuKPME?6F0jYS|CpjA2PSy~-UBP*}0Jh4`RG%*s*2Wxca*3;AL zlyf7B6v^C_yHGU#Qqr84*8a@41whNGCQp8V#XRoLCJl{^u?Pre7CN>$-7V8CcX{Kc z-otn`+$(YUopyCWT#kv2JxX4Y!#?*oQ$k$g`9~MX>>VEekM_3?e~O~`_q7e(qlEC!>y+=5y#MJ2d9TI8|5XTD z^Z(ze|F;oXo&k!>@ zjT-aa-@3k0h@TXJ%x#aUN6H~3`$BVU=~fvRbBW2%&$K^mUi;N5VoLg09;VQ_a+;pKcy3BM*V8#&VhdAgUhA3_ddB3SSAn4Z zI}W6#&#J0HAG4qQSy)Kl+%y5=gIvDiour7SL< zEU<6%E+lw(Z+My?1*7QB!I^eWdeyG!T%sVivX8~vBYcM-Ch~-`Ui6iUCqm_wlsW*? z$^ZSE^QtK#0?mHCkDP^NsFP}k&`$rg=)w=x*M6k|J~)zwi%;~pz3xvwQVVhAjqbpw zrr8Hd&}usWz<+4yxOBTp6Or)hTgh$c>UK4bX9SYLZ{^-x>EvI9h1nUpEQDNAtL?$$ zkoNWzz582~tn6Mufvi=lEfN*9crip&Bib>^{WwwDRlyD^MgBP(KB}lB)*i~UVs4pT z{6;Gl+`RtW9eeO6;_>U;mOlgT!UjDGC$=ZHH0QWjJlE?^5GHRYJ?~u+{y;R++~VSJ zEE@QeR^-Xik!5`kW*VBPm9DnBhx?MC;5QqGOKnmqJY}Sk^nRr(|I924x!(KGNF}S;&8TP?Lkus(#w}$x}&JLm@?1Aak0pYWdD%~;q@6J!RFtD zvqU?=#tBH$$}P2Ffou$>*N7+jp@)n8e?+zcUrH=Q@*uhf_0_po3C2#ab-v1x{9Igd8Tyg zCWw!Z4@VdG_Yd4l2JbHXW9K-+hwxOcgsGT`CCJXWv|$nBGu`EKlZL12flqI*Rom9}!DUchA8>tH%SGb>WB`rh6y3VBo%!MgcO`>HRQd$3l^z62-_6UAD{f57R8 zh=?q8sekeRvLtx(<_)#5JCK+FZk{tGJ}xaqhk=nS6Gb%tTsTg| z?}QTe@jcN003@Q};;PnSc*DfBK38Y&;^u~ljm>$eA56&`Zr-+n@K>Cfm6{95bg4b> zLx%apiQzF{E0#K;jG7FmcaLWKl5~fM+XD&2%EOh;pqQ`++I$`S)rJ}8&QZR3FHmrM zD>$gm5oGwpq$GmqgPZGXHZV^W!iNO~bn*%cye@}iLwsyK0GLXcnbCoS@;yMu$#Q^gq~!P?udAiWfH|g+-EmM>EqTVNDNVNEB9FR?t!EWowI3k?1Hw# zq)~emR%AF}@oMFUNYm5PuU@_Stfhte_U+q8vJa%yq(=Ftk3#J7GE*NR2LMJdNd$rd z0}&u3BqSC9xSAK&b*}E@$);0taKy+8-@W>Z&;I3buV#S_=s7EV>}+fo1Y&%H>df{*LSuf3XD3yi=XLY z!Mr*_2S7-p?6%dVP=Ftu`;}c>5WIUTKk={yiP%q-y*fHUNq?oUY1N+dE@ndsD-F-2 zA=OV`^3T8-q0udc0CAyY%055DZr)dAxB13YgGU>k8@nTG?x z^1K4<#uOggtgaghKlUiq5eX1Y;yV=?fQCg06i=WSgaBGnlbYsxban=$F?@d!pVn66 zm+n!A{%M95U zvIS?x|T{pLQB%;yu-~&@!GJgu0Yi-yY-Y_kBqStJ$g|$jSvg1uL=7+x=YKRA5_xQ2gQxlzHGVSL^zY`w ze(~I(GgNQ&ZzPVR*p7MlKS>i|f~#KLK07@6W>79gsbf3T-{rbc>ZL%2gJu6Yw}98z zatltd!=j7AK6uzI_%m6Y|5(Lya0_oJOKn6e$*U{QAHA^>BGEkWdm6Q0kLh22R`89J zWWS1ANlEEZyXfnaf(Tu2Qp7Rq3;@k40o)cuAy;Y%$c)rFKqF$k`Z1p-_^`N`x@ApkOLkZxv{lR5UavKU1TW9#>jkh=33b0jphgN>-<4U;m_{Z*dQ^^e4kW{g%OOj6e$IGjB= z;GEMZ5IU9j5{!vNuH!gI!;r$?S-;IxEIqyIh2Xh+n&Wc4dI9}p0~xDZGZZ@c-5rhj zb$#X|TiSek-5Jp_XD6m`p1vAAW5MKU6KtE2s{B|bzjr|`Z{^c3-+U0 zZ5N#)45kVf*AFfggHdsUzyK)D@YK{_Ao+^>UPol9nE3b|-T?-ouMYD=Q&3GIa)ZLsWn2!z@j_0J``5q+)k zBI?dy;T{oII4RlaarIs>vEV_AZmW%{qk6g$$g&hUz3b^hJd-q#{W{+RCVs!d@bn|v zR<9Pi$dpf7XH@;NSwVQr^7hZyZqv_B-FzS6s?vJH$j%v1l^)TN@^AI=t-Fx)HjF## zw=V?_yUnp{Yv~f|D8J^y3!*VH>JIPu@@R~>-5d>cwaqX-q{k0L#jBycy`{9q`Hk|$ zRD?^mN%-69mJk z4bpWk&Ns}qqi_zvv9IlzUF2n=rvaRUk|20otwzg&%9&ATDE{Tu)tO%R2Oa!xM2h79 z;{`YrALuGkK3v%_o4?hgl!%{@YE&~PbN3eXZYcYaCq&dTr0TpcB^WO9#QRG3S#x^o zprlU6VY_ACS7fcJltlJ{r%XkXGE#n2Mc9&+8-)3M>Jkb{f<8y@4?|uAgcMi5=%OkU zRc%!-Yu9C|ger_y?*l<@4h93M)q>kK&xQz&tJ`#f;PQr4Mj>JJFS1VO;>y8@Y2pL4 zB%vkJiotua^)G*+HE;J1J-TlqTHS(&3s8Qn#@eH_&&jck{Ac6p$JMcsC!R=-O42R} zby(ig&$5dNzNvhLe%Seu^YJt$C4q<3J(Q~7QCe{$PC=!h#oG|^iLI|3EmfT@HP~2h zJZ-h@w#VL#nQ_v^e)se9b2{MUHr!ubCs67kG_#^fhvp{Q@EmU~Nm%d90gE4NYhL z7MTyOyqtBTZ5|lLxjJ0)q?q;C>OfcujR#- z)+t~g{cIfWrWv(MCM&!j?qvpHh#S=6;n9GHr0^+=#M4k&R@!kYBI>DocVPOqy!8wY zuvhE&=A33NLy7q(cz%2ZxE$zlFb{PeLUB7HKdKfA`#bUZxjQ(w)GPW@3Ebz|3TuH( z?Qe}_%W|4-1(X6MSvOIiQTZjN{#8H(Oq@J_>(ftpR$z?yDr3J8d#|E9ZJqfU#y-`sU=z8SEf&P06-d&Fm-Zo8u!syljf0JpWG@_^^yS9X> zPLPBo^CEurV&d{1_t(>#8HhxmBva#lJzv4&;*N~tWBezS5InOhQ*s~!_j2kiKPnTb zipj?xm)>yl2`~O{3a|a)#eslvJbKjXX>=0`2`T|&mlqucf1_W0&&LK|!B3w;$o6|! z1n!^*#>}GDe}lyzHbYcL6lbLdB7TZeKbN;tgzcz~sLJ(rs+Gt`PV+5<%dd&dbkPT= z3B&t`Xc}SXiZg7HI>nAT484h~g|c0z)?Ic6{)mb3lc3t*%3^n=?)qpk?L0oJ(^2c% zhR-caU5(i~ed@?CT(#M~pd=$IPv)C-hxcl468sp=1yKV>-y>_!3X%izCLmwwA?zL{ z5-7-|@^vIOjB-qHJxW)Op+qF_Rqa9r+$s8^DD!-;)diJ|lTC*SY?R(0zHn^o&D%gQ z6hk5r32IvURkk{IZ}0N_7h>?fXN~gvkhJaF3Ps~|Jrgq`S5ng2ykfvRPd zB8ZZ_%dWxNC1SsoPYx9#Y!{Rs-g}ByvBo{436T6oQBMgQzb{lXU(72hxbl#u>Oz~4 zQ_7^EZTF8{X;iZlV^JAE{v&O4-(@&hpe$_T!tl*||Ia^{Q*mg*rzc{sB+Ba@hbyA_V^mXKsJVb9*2eG8gp!=!< z_0_&$@8+1zT%$WzciubejCDh*?OnDx-$o>f7h-F(H@pr_4cpJnLJGsY>0@5R?P_$a z`&(DLd$v^H0LgCgv}Sr&yDBY`Tl%%@cooFr0oL^_B8+TlV5^ZSaFY~M(ydEdU)*}j zfiIjy@ViVNg^&;lS_CpHNN0bw2!{DlWSd)6x^!-+d61ld4h-DZE8Dqe=8V*kp4JFnl(~PC6A8W3SSuo*^V(1HP-j z6nYQ@BQ*9ZU)=LR+2fvnwQ};u1e4>l(peq|9tn!F)iZEX>Nr+ItY-1mrG&4HJ$1%KhKG&EP zuiueaSrEQEr-)&CA;KJ@e~dM`LcA|4sf9(}9Z9Phvq@07+i=0okIZ?8Xr)!-8#)I2=3%^?&iUnzP2bT@SF<34@;o;os)E6W<0!p}xE98Xe)m?tnf zkKT8MA=xcg-iCd&z1s3g$!z)O{J0TCg`z0P_T`ThI{MbApm?5pt{HA zb$jW>lxRZ@fJhyW%PwpKEwpk`!49g3IGje&74`iaf!QEu)DN8Ly>fMFIHhSLlX28% zp(LR?iY%wLxKVx0p0c-lZ#tWqo7&j#st7NQ7vVURCs^-W%!3}zuxXWwJ%}EjO?uTw zrC$8D+-p4_!lk_1rj`rD8_6N`8 zGV>xEDLCrxEjpJzZS?NIx-(d@flCSzr9a+g3RT>g5~rM9&i7&VR=N5lS>+^if_wpa z2?ZnKC)x1qm*uptdYWWMwg*%4Y+&!0ZgxDP57K8`@cauyj_}DY$gIx=t9?r8#n@cT zIQkuDCYUUWnKqNufz8s%n2d^(`0DcVM^cj6t+V$|!dTy}uK!V@u=xo0&eYA{_>_C5 zz^}va6YhK11Q2Q#WMmPvG1hr4qei0_F|a58)R3+#$musRdz$1BsxbfPq-=k?5(>bM zc-P}fOXopE(;0dWqXJL`B+jrkOKmZ(Iu4{JEjwk`~z4|8QOXI-oOKk`Xix9;_9v?i*96UMHikl$MADou?R0spLga!T+6A z{%##avlQ%;IzPc#mBH(TBmM6A{V@6C5*g|5Snntj{N2gkj({71EUs!PhkcDBXw^jG z5>VNET`0Kl$=(ucehD{?(M*1Fg=BGh1GeqwD9&KZWT`a@OhH%UC0^rJW0g(kYf8-M zjSf#;A7=V@nY}|~4Zj}g2XE*KgVIVooHwjJQR}&jKSG9sQn~Kz-R+_9gW-C!b=&x8 z7!1wp-PL^bWjNycu`w>D0CmGC7gJN)Nat8;9CL}AkGOZ5r06y+$oQ_#L%z4_Cr>3* zMiKl33ny3|l%HbcXJ4|IaTaSA$iS5#o)DeOgmabz(Z-gV7_pFJ6nor%dEWJs>$6rS z1zEd)?U6RaT|ZfF|55xiF+`fN_%cdBBsi7ap1$9siAxzM5Y1FXj~fzd`3C{k$gFM= zWBe&qL)ybc(Kx}M;eE2Ryj<0f9#MVn|qvZ7@ z7MN3f@r40#@t8xnV4xZ}oU`pzFkfwd&#N)y7U4nG=#26dp2)CMVX?>w)M}3^7Ro2E6uof>Rk&!$^*fN<2-LPz9F_DB|CRUqZtAUhJCVEZcZN!rVX*HX|N z$~<5^>`&$f*1p;OGvMt8-Z`-G^?)Dd8NAcJ(@^9yG;(+*<1i{|RQNf;n+F-25M{CZ zV!MEFdZk%~~Ge83(exXHo^U#SQlTqfQh{APJU2<(m1{ubJ&IQX)bA`3+;J(JeE91uxa z_&1-tTyPdNANU?nu5~o$v=STlWin1Stptf(J|s-+_#gmzH{J6b-_g>S8lQH1R>m=AIq;qyQ*{CLt-Bp`j>jY-~K4f-T6g%tIoQ&;yF?WVb%gXQ04#R;7|!BAVt+$ zRw{)vZu1Pjx~P!a(N?$l<-QUqaTs}F`*<v59vpGbbm7N_N5(0#{saS0q z-pAsi8FD|9!qnV#8j#X~!F+2k7c?34sTqhX_%2OJ1 zTOsFdJYLe@-@nXc_%*0eczLn${w!r~l(Az-%vAPiT{iMZhl{T0j0$iQBuT4gw3d9& zKJL0^@?t**Yi(@;<8{%9qb}ev~}=Z*3=3^<{f{ zdTIgV%r7YsD>)FbC`UifnG{?6^=sm7*&pX8#ne`ab;~^-=~sJkoIin4y05Qq9;a;^ z_$5JIXa7JSkyV$+o8!FEKn*8^~S})IR@sNjL)K_Qg4tv&@XBW?4qo(*eIy%6nXh6i@m<{TT<>lo7 z?mk;Q85G_CekLkT&RAdsf2{BV2c8HRkkLF@8glX{UsbeS{Xu0xQ;Yc(s9n|?biD+6 zd+58ixt;$0e(K4@goMAqaU3daMlQNwH=&A&&-FdDPf$6G3f|Y}8WVM+{JY$h3kxL` z&c)Y>;n2!6MnH)q$6`LuHA+uLqYpeTUGKE`)z#HQpoND3Df~xDip8iUvTeRw>M8_0 zUO6!H0kH9gmNuilo&b_66PTAr16)#^gr6Xf_o}Ixa&#b<{n7St@f}FUki)}6-JOZw zDMFq}Lqng1?oJgTU-}YSfL*v3NJ3%h(vp%CjErF*12n+6`Wl-+_e#Z1==bmXp!I@5 zyPsOTv<@51`y#zvVnb)gltyaJR>OuIsvo$3!Y*dBglatx7 zo=D)&MWMvT#(wle1H-W0?WHZXbdsUwe^o&(`8ue&v7sR)=*6hbIYTCQ>zvjXSIG9! zYYWu2`$xgCbz_PRlt7d~qBT;J9Z4%21m7KJTLu67msL;_mNm z!g_ZXnjfNkrBrqpn3$-!xXz;<1+SN4p;QzB5$!OA=|a^k!G*txu;(e$T3-i;-Ujqb zuq4hw{-YdIrtd%3_q>G@ap=G>2>v}N2?W%~dJ$i$Oh6FMOW8*!kZ0NQ@v$9=`!4Ie z$BA@>3Iuo_6L{@&ZGUxwR-d4?-uMcz^gxb4{1fv=;gh&HIWO<@C5twwup}lX?oL-A zdZ7fo7&7k*V6PLq{#oKDszV zy~v6D+oSIJnZb?+=m?X5fG*nOMWC}d9Gb(UmD(>L!0z%;uQP(w+Ac98gAA-S(0;)M zYA`@DK3w!gd-_|L<0~*S0>|Z4nL(hfY6qwJ#Nzh0845ZuMsyBK%zXQy{yVpeUUe8>};3enDgWI*5k3=q@wqK zt7bDRXF49(?dp%MC90};6dyxNXZZZ_5U;nl>3Osncx@|vd;`x}kG?N`F$m85AnSOq zR&de-G!jZ$Ruo*c-T*Hl&tFjh1xAjxF>Te6{!`aGzdmlrsjQ4SXu1ji`}Y$QbeGM; z)AM%|D*3f!IyEow=WL*PU&2{890k}qfI5!-b}tr|SP0eh&CU^;6V+>p@hw4L8QHU= z3X|~E4)Y*VpJ2UOSExqAK7i7|_&_Pdh^Z2bvO|{wTuZaO?&l1^7Xv~To71jpLqo$g z^pl9HbUF$7-R*5qNQn6bSt`27X-sxUR3|)0w0qWNAlwh&revFp$|a4R`3(k9IP?UO zQ1Jkb@I&H8P@R4dC6CDIH}HWb90GzK;Ozu>JcW{{b?fv>&hGkj3l{$6wI4dnHb-@2 zETaon!P!870>AyekoV4#%duk|B@w6XNB;0H4|gZ*x(|gE-YUHByPmIL4_&>CL~msa ze^6uReaMLM*?l=(O&lM0f!qi*xxEAP66mVq&7RvY8!>4EeV2nt8ag8ViAihMM zY8FJqVzJrc>8W%slT4iy{sghU)f4-fpF4)YT;2^DB6*)(j;g+;TjTtcQOU~ZX%U`e zAz3@XClXooo&u}G{*E*fc}hZdJ7(j+S7j}+j^Y(X5(a7(p-_^ck6 zBj7pf>5NMSmMbXtC=b@P66oi?{FoV&Ip$Xi?;b%lkvqBtN|o@sr<=o}q{7!%4;x;d$d8M%B+52I9-x>2r^m`T1wEq+vRzWK5188@V%IUU(Oi zT_1m*6R1z*2Ubv4aJFYA@QZ_9Bmg)=?e7p-(Wij1#MV}Cdn|ut{J(W!vTQreuNzhdfncu5L_ z4$WkZ+jHr(O?#j|hteOu_>6(MXS_CoO#VkmUt9+`G+gT4*^PQ$#W8Ah0x)q6H4&K# zE&R>(XdIT$&&kO6nvj5}VX2I`WO&UUx2bVza;!mfd>W3ZJ5}^dQ(z7V6l1!7Mxh(x zGLq5ZC22f=syXX`dm-wJnB5M)idx~ONYKlavxXeTI15jDgs+7Mp~my+G>h9zTG}0d z^hc*xfklujgX9!TvKToD3xFTG)H$GXr`F1v!~%p z9f(WD7U989?!bf_ODR}BU7rXRXe$Xd`a5Q#+1M}JIjvf<A}W{`ivoXIeZCJ(CFPBM!WbI`N2} z>*A4BuFzb`A8JY971vzMq=$`#*1Cl84Z~$X)aB{M4M3!lmy?zOdobXJ^ z^W8oZ_a(q`p8X;&{3b#HQfJ$NkWfB6zw|6lBJYNpkQFH{fAT2T~A?J|VlMyi~n1Dh;W zO>ay3anTMa>@3g+aTi3}6UZh6kJ%x~>HO{1!m{6u<)dfS**9Z#{bWuDrguy%yGZ-P%~J*&!)@>tb)#=hMTNQewX; z3f0n9D>=4!%AhGrSO`l?v#vQ@lCYHr5(yP5TNQuXQF;ZW&{OK08%z>d4Mf%(dvY$m zon3+3;aan?KKOBG-7O4momKnWz{g`s?)n2X3|itoz(lPLI4k42YKFv zBZdI+r7SN=1sF5{X2Cr9K`u;UuI%vtA#wQnT z-jZf8H8okEYtjEcY7cgKO6m|__DJUhAZ9i z85H;0BoKJ-QB*H0f;hV)&hW@e2lzvBGoC(dqJwzZ{=o=_J{7%hmRCV1M~m%8^-SGp zwuzji@8;wgN$4#iB>f%9Sc)YXS!;4q1?pu$W!k0YMVIa4k<$Hp-CD(JH8@++m0@4O z>&BJ98g@ugp%PXh;R>|<{r!EZ1Qs6{)S2D=eD|IyyaSkMfgR(qYwXd|iiAZy)qqk% ze?MGYScHDwx-K3rGb^$`%YWnn4K1iRQ`9=uo-m(HIPFpkZSqvFp&^|)2?I5j(lQP8 z;{x-1`tc#n(NCSD3v5Tj)1h@YWb`@5QWI;~>PmKhp?6nlCA2#N9S#Pd%GGYgAQ4?RXpaOe;}PyP zG+MGtIdtXZmS7U~C&JIy8PPv?AHZbijG3KpCU_v%!*oqgJwL5qDUzgq@RLJg5`P$K z{`iwkSiFqFj8I^$#@zULJUX$+ebrd`_M}IqS6rW>PY%=zlBIoTHAnxhcbL=44DQ+0 z{jD7CyIy{&*pyoWRBu!dt|K)s+fBJSsXhViRQCX8BF3zWJn8^?_!1JlC-2<^vRk% z-1z_=b({%!lZzRPKylxeeS?FJz7$qp8*ZFywP>AZye`^ywoo&Z= zp4>aSaN>j2qZZ;>SHpvEPQ9nA#UXVq3R1sIahGYphm$NQ*062wUp_L;*F# zAw}5Go?X~D>L!$j)mE+!{b8qYwsC?hD+v_yDkdKShPrVB0+lyXmdcHB2%?kJ(tmTj zq-XTA5t#T+H8D0~@ir5J%We;gF;dAkpTOe%mCw@h~Fak)NCamYx)W{ocJITAH9@88K3|vtRXddR*#8iHMF_B^So^eUS9N zZj?>1F51BA@Rb4|5VoI9@c?A&+#&cUB;b1zQ~U|asmOE(N@F+L;P&ZE$Ka~Nt2oT} zsy+U?Yy7Vh$}V!%KkJkG36?7}l;swubq&Xi+ZUGxTvj3$q9`%?ZSwyo3pk~(f-u>_ zbNIDNGv%+x(V>vnUCVfAijd#R?BL-?~YzisY|i9}8mDcFf=-zLUA&ZTf^ z|9tqNxsg92qM+$Q_&zC-GTIZayfoVHs~>t0W|-Qm9A%An^cm;IRaL)hYJTsOm|0d$ zOsM~Co~=Kd&^Vi!FRZcq0Ht+&j!U+PiAe7A_A5jW3H?*TTefKNFY6a#eI{f+ekq34 z7bi!5SI8?;uB1rTJqI>Y4ldl8K9LL9xezd6@VbLv?EwPBk{#;8=gE7}Qf>hBE6|M) zT3X8To-id^%WV?n{-hJ_%5`67TI1^KI@aDgv2!r5*{w$@kuaV9|CIKfVNESv+fg}i zIA94VMJW;jcoacU=~W^{ng~*)ccn>}CZHk&X@=fG1yPZX^df>_K)N(Tk*>7RL+4$= z=Q;2B-oGE$l|N+f*|R4zYu38gy6?%H+q54Pow+XM_ef@VdRtSgI4h zQ#v>eyIw*aD>faiucylfR#`}D2F*39s>4rpFedSgY=}4BA0Cq&ydH&*XNBibV6jDl z)9u2MPHVq(bm)=$f`d^{cP}a*n{)3P>{n`=yGu0BRTI`95tq54E%8clEGY9BPG-90 zDOCmV&mRJU7N2l=*H+gX1R}_SCZM^%1JIJ(_X#XTh3Wome3=R5Q{`N=y8#`~@$XpH zxQ0KJbIrzfnClH=u@p>xEs^U+46OZ{z5zcQ{=6!JI`q1%EFM6q?TS!MjJ&*ax2iC+ zl!_{v>F9P03nGH>#D3eu@oQOZDgtRKH_iD@het_XvF5&T(-2OQfr{l z7#(kD>s$q6d!2pL-FMP;-^$Ia=smAbeBBwnp{A@%1NfjgRNfcApx+Mr;V6=SC&#yX zD+w#jw4U_naQs(px~vSr*;M7~2z!qV+U-kUJQlxKec#v^Re*Ubovi`nFXcU4@?!W)W6BGH6ev`s|V!LZy`B(}DV@svV zp(j_HLyK~v4B6SOZpr@D8^36pCx;0gRc``l_Nv6z&sR`#y-{%z=RYc}ss=$j&}91U zgpA`*s6$_M^tv1#n1A@x<`$%w6shQA^DW=E2Fuiv7Owo&>N3X4GBjKm?PLU^4jC0#{^So#Ms9$HLq2O0+-8en{y;7*`8w*jw+T<{LpQZxB> zPavYBqc>VJO_9_F^(kCxK|$;0?sp=@zMNBJE@?l7doejXPFG3*fR%dJvbmeg3V`^gsHc~3&|{%wy{>_UmnCy2%&pM$%vnzS|pSg~Q;n!X=ACXLG9yiue%f&G$x)H3$Uy!~YVp0*&LV&>2&$1T-x#g5Yp<3K%&(A3bV z1Ns3>)%x{vR9yof&`G_nU#6@d!+pu#+Kr!xB3snqd>lfZ74~e<03|vEJXK4K5 z=CvcIbGhku0$0k=DAZ%1;aqZ?(Yh#d);V@(ou5n|1M&?_?SsBw;w;KAZ4oeFb!eR8 z_GWh}sVva_XxACTf-?~^XyO7`*bEFk@Vk8!r4`WqoAy!k?Ck0QHBL@W*6>}&9FEt} zGqb%r{v~(Zg5kB9pxSyp}XP+)ADz{J1IiOzciru*0a8~Z2ftgT}KvL_@1dlX_kTz+Uj+wT0 z_%b8VlJxaMTCG;x{zlP#q>SPM!zSL#X7+UzZKfcfx?EMi(Cfp3J0ZZ*#0C8Rv(nOO zb8~YX+qAfwKl;3_vU_d-(;aN}Klgh#N1RAus(TZG#ZJut$BdIUSxfyiz_R0f#YTK7f z&W8uyH%5Qk*i97`7LJ1?4dh>b&y+`2N-d`1K{oalM)6+XW4bE>v+r46#oZLpYU1LH z3jr4Y%*>3VSW2LSKKkV5!zl9tT~O)C0DP-26RU_gqZlhf5F1!qGUYy%VG<^~n~yxI zLx1pmI!4AO0J!z?O%4H(?BM=C8dA$MG6HUAYmY8@{C@tPMR3!+>2!r$hN==;>gr4= zK`V-r&MR=H3=Jv+#l=^K5Lyig(;B{XX-GF+N^1Sq(*ulq(R9*+uFG>)%OI%{ZH@DK zpAD@Qzb6v(_M7<-ZHCFRu4WX7G`=BfZu+9UR>Vj54zC~-FhM0e5_Qa*Ezn(pN z)@Ji^B=!{58=vabUMrrQRy8peJ_O9v*l1qsmdfpcgxK4PQokoUck>A+9fevPSD0Iqs8fTiyzGtIlF5AUE9?qrOv=2I(tSIjGsLQu zWAF)gNa8Si?q3#po(>v(f58~A%X9L19CwhY@v4F*P(2bl9!<+VZ`w?q2M+Fh-oISs7y?CMWWX zSr0R{B?vce6ZdtiB9|>Iw|1(kHpjsfJi^<3a&UpI0_7eM6X&-ofavXawDNIFmK{;U z1hym;TJ-TKc&y-V)U22}=jnAN-jb6&6*?qK|92uJtW}Q5=ce zTiUK(=_MV%xi&T2UGsXTwc76CzD#Y2bxHhUnPi4qxV5_HE7H=?q(T*a7)?#8((umG z&f2hg*?IwuNd6>KQEuW#C@gSHulO}r8wwjxr{3WT8IUZ1U>rLaW=s zUly6{Vpm_=s)Yx}Vgug&(gryn2B?!g?G}PxRa40Y z)nB(Qids~pnKq27tF5|1Woy^h6)J6Xd89I6^=Nyv$+6j)O$mqN+(p6H_lZ6TD}&Cf z0Y{^hn-i}8VqZ)*LsSgdjslbZ8MGOd~r$7)E_MWc&MIlxzfw=Mg{E( zjnJpuo&4U!XEGHtE};L_f4bR9p7SAECk&r*IusTCpAyi|zYT&GS0n zmV&lPE9}RGIvjMeIO&?5U0k4SCga76eVRD9Xey*t!Z#%N75h-b@-%NV9rDN@4XrYV z=7e}YWS{OYG?UIzV4V}yQrPko7<46Gdr7zTK%EB3EcVFxNNt^!l8ZS{sgEa>{)O)d zEcVJ$)(6dau?t>rlo_e1A4PKUY9WAVEJgp(FI)bNDjt?dd#x<7Nfi4k) zl`);NPoyQ&yOT&i;SS!-nFr~GzM@f=T{asZ@N36nna-Kyfs-2>3vwiZEt6s^A=fcm zmpXWZu~(v`q#o;K7Bujz5e+(PQs#H&0*83p20qQ}%w-hLQ6PkkwOSs3Z5>XN+ZIFU zlH6&G)AZ`o;#ig)#yi;;CVQmWn17Vg<3LO>1Gaj1=Pb#b(2vduV6^ zgrolL#<>fDQ%^b}>VM|Z-qyCasGkvwa;)V|=j(?{eJy#^f|8!Bsj=;JQ>6L|hh&Yk znpxb@>0Vtua@1g#pL4U__^Qc$H_bJT3)x1cOELG+$2SgE``uU&fW)PxsT&!wnVKGw zER$~_Qn*GPxoKeFqix$Ag7xW?DQYl%U>-KJA133sZ^fF(ap-RH3umQ7|Lkm+Q3;j(OYuQw_R6aJ9DBe7CH@3L6 zlxo7p8MVjvgA@ypC+IpI3aTId?O>yo#l$7zRSeB%wp;83JV&;FsCt*}g==8slRVe! z?b!Szqk@d$6$5%2QfS-{JAM{ca9p|mVIr>a*18o9BAByYjTyF@JJLjQkxU> zLLdf48xtH5>wn)F)C2HsZLh1U2LJ2u6cdJYsV-^d=)-iE-sp0tQwUs2+3r51c!s?$ zp7z3kr^e0(X}rI*h?UT(=1?Ub^WgZ@@}e>K5>a}nJ%a5_3p@MV=}+r}az~3(I#iEb z-|3Vo%*e?JlinFU4pG72!i(Lcn$4GYh{@8pnq0_)hFQ z7?_RhJu5uaA(ZX!Z8Jk@iB(ZSkOhzM)#KHdI>o2k6;oK**lN9{`zCuFi61KOin-cw zUhRMgKlH@O{~Is9uO&|{+Kn4+)qM+NKkJUgNl?NJLNe{N<86_){ z3fdkGslm4a1UA;AA0OZ6@(#-93hh8!BK>+sc!Pt3Ie2)2keu>NM)k0*=eD&^Z)Jo< zFm;?D?A_VY;v}5^zVX>l73~HLOTWCm=ssH4(NjpycnMiS!aRs&nJrJ?4crGTVZo!( z2`aDC<0S}byaTt#KTa+`zbg4|gdn-;f8?yC9fiVi_a8H>Cs?gH?rF8N`q+kql+ZiK zhaDqH^>)pn_&U30W6t7CPTbHNb1uJYZ_mZeO&OBuNBU9@zXjQH%=$?+6ZddRGyRUl z+eyf`rar_v*S(d|CtA7omfwCl!YHTiUP{`|8~L2KXHAFG;3f}d4|{2(RA;k&L^$^a z;hE_6pY!jUi`cEA#DsRD`}SJeyAvdZ!y))D=M*kzj_rH(!nsmn*fW?_?Rea4ou#Sa zUtl4wSrjwFHnX~7zR_o#LHZ(&tNSxCmh4JH(d1h^5-0mhJyvASVT_K_y}PD(330|{UAU-l+$~Zp_d?Tq4XZJ_i!9<@cx-WdQcU;7s)bq_v9e*>`mhpC4kF~c3tIB9&`|fsP2f7>yHY;t@!8mpXsb# zf}wxZOWZHaB~1C)N55t_ZuY_wThk z^tX~~i^rqWIH|=%T>BC-h4meM^%WqsQb6iIdR1F z`d=J_HjSO`HY?Fm9FNtt*?7$Fp!J&xj+GK7t@0C^t zvwRGbxsJY)^0zWbkCD*?LBZws(&h_&bRIjo4+otGjv4LDa}aMwi|TMW?w0GT>wTE( zRcKm_YdXG?`Av0av1@*CJ7{Tm)MBV2FF$#woABw6kqY6knF#%aCtaGcF4jZ8@R1vdEFjeHe`gBI ztB@kSs3-~`3#inR!(c{MR%(8lRCiaV%aoRk8`gh*InFDYDsAdwD*j#R`^KNAkGrmV z7=u}*#>Z-&PA=-{`le)XD_)qfR<~(@w+bbwD@*dz($GMH*qC+{C+bO;POJzk8w%BG zG^02hp9fl`)L0a?ud)l~`$%9R0Er#oks|<#6%(aONaH#e5_4Ep7t}*_{4(ch1 ziQtp{1xV3>kD+U$U8d=YG?OQ-43QGO&`=`Jawu7gcn(k1^r$O`mt|dbw8S!Eo&HG zGASAmaT*JVI8RS)BwL;p9udJKE}jJCX9sZD`%+$~eo4$bxc<=3*|G@`Nq(Y3B%auq<_hxKaTU)e;Ema&RqENYNCh!@ zZ05?Igda5l*o{HF-pdQ3C}*K7(MA>?f6S*=Kpn^T?|1Bk2>v76IW)GLgd&BzVuN_1 zpO^o3ZNVA;tc9WWr71O{dZ{-8z*hqiBF1t1lozLS1Dbb-%^*I*de@}IaU!b04IWE&!tjJe$9!EWZ z%c_305aA~so;=Wr7v6N& zD-M&c65oubr21&~%O(KPsPgQ`{vhsCqVOH21h%%%I^6j%e*F8khY8ump&t~_3Zj%T zn3^yDfVL!oh$gv*4<-5HoG}fGvcmT=$_t|AW7aR%ICy8KOf{;RdEGN{knlTG@+!q= zcm3oZi4=w~@U#AFe?_;?Bc|~P>wBNfgju4>(%rIqz6b=3ANh=cnkM(+sA)EvAJW=t zlxHspKclKC55kw@XnwOE0uGe*;AAJ+9 z#oi*m?^iwK^ zxmpA_eh{1DS3WcpZLy)o^nX9=-|&_a=n#RXLJglX`X_~96urY$WE_*c%lNP3YIKpj z0c!$!8(_p~e>(Xmd@lIPyXUjn)Jb0%m>;ZYaxi#anp#>7AhwGC`ju>f0fTtjW3%RD ziK^rLIW++(U)fiI3Gs9qMp!`Z&P>7Sji zYWP&qcgg*rw>BLm$*)!D%9Q>Txg4_RhVCqoC9MHI3b7*dc>tB6PW|TX+fTcHS%3}zOePP)O!`o;ow3HCh!_S2p~oj9T%r*U@&Jtr^1~gf)u!R^O(-8G6^ylxVoEQ zIgk}a4Wo`XH8f-)OLZ*B0mWw)@Z823YN6lcZ;=tc!EF8u^x`aL#WVDvyp`Tw~Z(ln00GUJ@#6Z!o4kJ&=Z<7X*BwMG5FBHX5Z%ggP9(B~e7**? z)s~iIcNw5^c-zp96f#C^2?f=lqdV>@_Rug^@gq$+_kYqK=h3~RjJOxC{`1KE`y)~y z2O1ir7a1(qvGC|fb`&Y7nd=-Ud zFjnAN4Zh%?ZRlV(SAo?a5BWnf10zSj^cKuA1qE;l=%^K;oqs(9v_{Cv$nUbVAwRG} zekmo@W8^sj)DAeW;a@N1(k2=NFW&+#%84^)W@nu-ha(eNKK^56|M$%>3JNYjgaF__ zUzfLrPXT@QEq)Uu@u{=5bGj;4Asa@X)sJDBIewe);l6Nzrf5LL(C~LU@L+6UIw0BE z+d)LVfG|&P;YYlL*mZ%OY=^6P$l5lU-jW~N)^QP7r* zUFG<8Tm^oP#+$L{lp)+*b|L2vD;`@+tL1LY-)Gj>l#B0HitonvC!U_#UoXvA7*v90 z4}3r!%uY0Uex>sw1?0a7i$ZBKrOCQ&p6RbSiAhfvYo2M4wU6j~hD|>BQNeUDaZV($ zB(+k?H0K&Vtl?fOn!a=APGBZCl3L_p&3N}8G+tFD!fR`5T3TAP zsyOQxaIyi&rf*iz;-0_wZ^JLRGE&sYq$agw5!vR2p=!P!505d7nYp=Mm50FS=xDXW zbmN)(6L8Fl04RLC*DDHyJ&Ua!RL)?#!GOGCUe)qn2ms-^1q7C&?Lm4z1Qel;MBy7C z2Mkr`gMSlv&!2x;R>qXxXYT;gh1RK_fjp@mW{b6GoNq^A9=rs+BK!{WB5NmQvB6mE z5732~&M6%pTWkof@+8=2RdMt2g~N|t)qeJ#jSV>!2=f{7J1@R2wGR|Ir_ra3U7ek> zFkj)@BR}9Qh75S!eE#IOo@+fiZ>>a~? z?McEBo-0#_9D$JoP90D`sNsAAvV>&S&TT(M>NKNP z!1DZQ)s~EhwyAc8L8Jc?9OGk@<~{Q^t86sz41z3sJe~3&l~fKI^AP|?ul*SXVtd~E zm5^3HF1_U(I(+L{gye4r$` zw5M@@q$Y)YsK^;K0$PQ88!O+BU2d0<1Bwcqz=5dvWby5d#^6KJ5Mnsy=A66u@3bp~ z^W{^CwjSUE05|-PpKeLe90xzYx-5!L=D$-icY?_Z^x=T&3iS;lKS=IDc*drZW4(}b zfHP;T#~OkI?KLM{UDtb 4) { - if (arguments.length !== 1) { - throw new Error( - "Only new Bezier(point[]) is accepted for 4th and higher order curves" - ); - } - higher = true; - } - } else { - if (len !== 6 && len !== 8 && len !== 9 && len !== 12) { - if (arguments.length !== 1) { - throw new Error( - "Only new Bezier(point[]) is accepted for 4th and higher order curves" - ); - } - } - } - var _3d = - (!higher && (len === 9 || len === 12)) || - (coords && coords[0] && typeof coords[0].z !== "undefined"); - this._3d = _3d; - var points = []; - for (var idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) { - var point = { - x: args[idx], - y: args[idx + 1] - }; - if (_3d) { - point.z = args[idx + 2]; - } - points.push(point); - } - this.order = points.length - 1; - this.points = points; - var dims = ["x", "y"]; - if (_3d) dims.push("z"); - this.dims = dims; - this.dimlen = dims.length; - - (function(curve) { - var order = curve.order; - var points = curve.points; - var a = utils.align(points, { p1: points[0], p2: points[order] }); - for (var i = 0; i < a.length; i++) { - if (abs(a[i].y) > 0.0001) { - curve._linear = false; - return; - } - } - curve._linear = true; - })(this); - - this._t1 = 0; - this._t2 = 1; - this.update(); - }; - - var svgToBeziers = require("./svg-to-beziers"); - - /** - * turn an svg d attribute into a sequence of Bezier segments. - */ - Bezier.SVGtoBeziers = function(d) { - return svgToBeziers(Bezier, d); - }; - - function getABC(n, S, B, E, t) { - if (typeof t === "undefined") { - t = 0.5; - } - var u = utils.projectionratio(t, n), - um = 1 - u, - C = { - x: u * S.x + um * E.x, - y: u * S.y + um * E.y - }, - s = utils.abcratio(t, n), - A = { - x: B.x + (B.x - C.x) / s, - y: B.y + (B.y - C.y) / s - }; - return { A: A, B: B, C: C }; - } - - Bezier.quadraticFromPoints = function(p1, p2, p3, t) { - if (typeof t === "undefined") { - t = 0.5; - } - // shortcuts, although they're really dumb - if (t === 0) { - return new Bezier(p2, p2, p3); - } - if (t === 1) { - return new Bezier(p1, p2, p2); - } - // real fitting. - var abc = getABC(2, p1, p2, p3, t); - return new Bezier(p1, abc.A, p3); - }; - - Bezier.cubicFromPoints = function(S, B, E, t, d1) { - if (typeof t === "undefined") { - t = 0.5; - } - var abc = getABC(3, S, B, E, t); - if (typeof d1 === "undefined") { - d1 = utils.dist(B, abc.C); - } - var d2 = d1 * (1 - t) / t; - - var selen = utils.dist(S, E), - lx = (E.x - S.x) / selen, - ly = (E.y - S.y) / selen, - bx1 = d1 * lx, - by1 = d1 * ly, - bx2 = d2 * lx, - by2 = d2 * ly; - // derivation of new hull coordinates - var e1 = { x: B.x - bx1, y: B.y - by1 }, - e2 = { x: B.x + bx2, y: B.y + by2 }, - A = abc.A, - v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) }, - v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t }, - nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t }, - nc2 = { - x: E.x + (v2.x - E.x) / (1 - t), - y: E.y + (v2.y - E.y) / (1 - t) - }; - // ...done - return new Bezier(S, nc1, nc2, E); - }; - - var getUtils = function() { - return utils; - }; - - Bezier.getUtils = getUtils; - - Bezier.PolyBezier = PolyBezier; - - Bezier.prototype = { - getUtils: getUtils, - valueOf: function() { - return this.toString(); - }, - toString: function() { - return utils.pointsToString(this.points); - }, - toSVG: function(relative) { - if (this._3d) return false; - var p = this.points, - x = p[0].x, - y = p[0].y, - s = ["M", x, y, this.order === 2 ? "Q" : "C"]; - for (var i = 1, last = p.length; i < last; i++) { - s.push(p[i].x); - s.push(p[i].y); - } - return s.join(" "); - }, - setRatios: function(ratios) { - if (ratios.length !== this.points.length) { - throw new Error("incorrect number of ratio values"); - } - this.ratios = ratios; - this._lut = []; // invalidate any precomputed LUT - }, - verify: function() { - var print = this.coordDigest(); - if (print !== this._print) { - this._print = print; - this.update(); - } - }, - coordDigest: function() { - return this.points.map(function(c,pos) { - return '' + pos + c.x + c.y + (c.z?c.z:0); - }).join(''); - }, - update: function(newprint) { - // invalidate any precomputed LUT - this._lut = []; - this.dpoints = utils.derive(this.points, this._3d); - this.computedirection(); - }, - computedirection: function() { - var points = this.points; - var angle = utils.angle(points[0], points[this.order], points[1]); - this.clockwise = angle > 0; - }, - length: function() { - return utils.length(this.derivative.bind(this)); - }, - _lut: [], - getLUT: function(steps) { - this.verify(); - steps = steps || 100; - if (this._lut.length === steps) { - return this._lut; - } - this._lut = []; - // We want a range from 0 to 1 inclusive, so - // we decrement and then use <= rather than <: - steps--; - for (var t = 0; t <= steps; t++) { - this._lut.push(this.compute(t / steps)); - } - return this._lut; - }, - on: function(point, error) { - error = error || 5; - var lut = this.getLUT(), - hits = [], - c, - t = 0; - for (var i = 0; i < lut.length; i++) { - c = lut[i]; - if (utils.dist(c, point) < error) { - hits.push(c); - t += i / lut.length; - } - } - if (!hits.length) return false; - return (t /= hits.length); - }, - project: function(point) { - // step 1: coarse check - var LUT = this.getLUT(), - l = LUT.length - 1, - closest = utils.closest(LUT, point), - mdist = closest.mdist, - mpos = closest.mpos; - - // step 2: fine check - var ft, - t, - p, - d, - t1 = (mpos - 1) / l, - t2 = (mpos + 1) / l, - step = 0.1 / l; - mdist += 1; - for (t = t1, ft = t; t < t2 + step; t += step) { - p = this.compute(t); - d = utils.dist(point, p); - if (d < mdist) { - mdist = d; - ft = t; - } - } - p = this.compute(ft); - p.t = ft; - p.d = mdist; - return p; - }, - get: function(t) { - return this.compute(t); - }, - point: function(idx) { - return this.points[idx]; - }, - compute: function(t) { - if (this.ratios) return utils.computeWithRatios(t, this.points, this.ratios, this._3d); - return utils.compute(t, this.points, this._3d, this.ratios); - }, - raise: function() { - var p = this.points, - np = [p[0]], - i, - k = p.length, - pi, - pim; - for (var i = 1; i < k; i++) { - pi = p[i]; - pim = p[i - 1]; - np[i] = { - x: (k - i) / k * pi.x + i / k * pim.x, - y: (k - i) / k * pi.y + i / k * pim.y - }; - } - np[k] = p[k - 1]; - return new Bezier(np); - }, - derivative: function(t) { - var mt = 1 - t, - a, - b, - c = 0, - p = this.dpoints[0]; - if (this.order === 2) { - p = [p[0], p[1], ZERO]; - a = mt; - b = t; - } - if (this.order === 3) { - a = mt * mt; - b = mt * t * 2; - c = t * t; - } - var ret = { - x: a * p[0].x + b * p[1].x + c * p[2].x, - y: a * p[0].y + b * p[1].y + c * p[2].y - }; - if (this._3d) { - ret.z = a * p[0].z + b * p[1].z + c * p[2].z; - } - return ret; - }, - curvature: function(t) { - return utils.curvature(t, this.points, this._3d); - }, - inflections: function() { - return utils.inflections(this.points); - }, - normal: function(t) { - return this._3d ? this.__normal3(t) : this.__normal2(t); - }, - __normal2: function(t) { - var d = this.derivative(t); - var q = sqrt(d.x * d.x + d.y * d.y); - return { x: -d.y / q, y: d.x / q }; - }, - __normal3: function(t) { - // see http://stackoverflow.com/questions/25453159 - var r1 = this.derivative(t), - r2 = this.derivative(t + 0.01), - q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z), - q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z); - r1.x /= q1; - r1.y /= q1; - r1.z /= q1; - r2.x /= q2; - r2.y /= q2; - r2.z /= q2; - // cross product - var c = { - x: r2.y * r1.z - r2.z * r1.y, - y: r2.z * r1.x - r2.x * r1.z, - z: r2.x * r1.y - r2.y * r1.x - }; - var m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z); - c.x /= m; - c.y /= m; - c.z /= m; - // rotation matrix - var R = [ - c.x * c.x, - c.x * c.y - c.z, - c.x * c.z + c.y, - c.x * c.y + c.z, - c.y * c.y, - c.y * c.z - c.x, - c.x * c.z - c.y, - c.y * c.z + c.x, - c.z * c.z - ]; - // normal vector: - var n = { - x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z, - y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z, - z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z - }; - return n; - }, - hull: function(t) { - var p = this.points, - _p = [], - pt, - q = [], - idx = 0, - i = 0, - l = 0; - q[idx++] = p[0]; - q[idx++] = p[1]; - q[idx++] = p[2]; - if (this.order === 3) { - q[idx++] = p[3]; - } - // we lerp between all points at each iteration, until we have 1 point left. - while (p.length > 1) { - _p = []; - for (i = 0, l = p.length - 1; i < l; i++) { - pt = utils.lerp(t, p[i], p[i + 1]); - q[idx++] = pt; - _p.push(pt); - } - p = _p; - } - return q; - }, - split: function(t1, t2) { - // shortcuts - if (t1 === 0 && !!t2) { - return this.split(t2).left; - } - if (t2 === 1) { - return this.split(t1).right; - } - - // no shortcut: use "de Casteljau" iteration. - var q = this.hull(t1); - var result = { - left: - this.order === 2 - ? new Bezier([q[0], q[3], q[5]]) - : new Bezier([q[0], q[4], q[7], q[9]]), - right: - this.order === 2 - ? new Bezier([q[5], q[4], q[2]]) - : new Bezier([q[9], q[8], q[6], q[3]]), - span: q - }; - - // make sure we bind _t1/_t2 information! - result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2); - result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2); - result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2); - result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2); - - // if we have no t2, we're done - if (!t2) { - return result; - } - - // if we have a t2, split again: - t2 = utils.map(t2, t1, 1, 0, 1); - var subsplit = result.right.split(t2); - return subsplit.left; - }, - extrema: function() { - var dims = this.dims, - result = {}, - roots = [], - p, - mfn; - dims.forEach( - function(dim) { - mfn = function(v) { - return v[dim]; - }; - p = this.dpoints[0].map(mfn); - result[dim] = utils.droots(p); - if (this.order === 3) { - p = this.dpoints[1].map(mfn); - result[dim] = result[dim].concat(utils.droots(p)); - } - result[dim] = result[dim].filter(function(t) { - return t >= 0 && t <= 1; - }); - roots = roots.concat(result[dim].sort(utils.numberSort)); - }.bind(this) - ); - roots = roots.sort(utils.numberSort).filter(function(v, idx) { - return roots.indexOf(v) === idx; - }); - result.values = roots; - return result; - }, - bbox: function() { - var extrema = this.extrema(), - result = {}; - this.dims.forEach( - function(d) { - result[d] = utils.getminmax(this, d, extrema[d]); - }.bind(this) - ); - return result; - }, - overlaps: function(curve) { - var lbbox = this.bbox(), - tbbox = curve.bbox(); - return utils.bboxoverlap(lbbox, tbbox); - }, - offset: function(t, d) { - if (typeof d !== "undefined") { - var c = this.get(t); - var n = this.normal(t); - var ret = { - c: c, - n: n, - x: c.x + n.x * d, - y: c.y + n.y * d - }; - if (this._3d) { - ret.z = c.z + n.z * d; - } - return ret; - } - if (this._linear) { - var nv = this.normal(0); - var coords = this.points.map(function(p) { - var ret = { - x: p.x + t * nv.x, - y: p.y + t * nv.y - }; - if (p.z && n.z) { - ret.z = p.z + t * nv.z; - } - return ret; - }); - return [new Bezier(coords)]; - } - var reduced = this.reduce(); - return reduced.map(function(s) { - if (s._linear) { - return s.offset(t)[0]; - } - return s.scale(t); - }); - }, - simple: function() { - if (this.order === 3) { - var a1 = utils.angle(this.points[0], this.points[3], this.points[1]); - var a2 = utils.angle(this.points[0], this.points[3], this.points[2]); - if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false; - } - var n1 = this.normal(0); - var n2 = this.normal(1); - var s = n1.x * n2.x + n1.y * n2.y; - if (this._3d) { - s += n1.z * n2.z; - } - var angle = abs(acos(s)); - return angle < pi / 3; - }, - reduce: function() { - var i, - t1 = 0, - t2 = 0, - step = 0.01, - segment, - pass1 = [], - pass2 = []; - // first pass: split on extrema - var extrema = this.extrema().values; - if (extrema.indexOf(0) === -1) { - extrema = [0].concat(extrema); - } - if (extrema.indexOf(1) === -1) { - extrema.push(1); - } - - for (t1 = extrema[0], i = 1; i < extrema.length; i++) { - t2 = extrema[i]; - segment = this.split(t1, t2); - segment._t1 = t1; - segment._t2 = t2; - pass1.push(segment); - t1 = t2; - } - - // second pass: further reduce these segments to simple segments - pass1.forEach(function(p1) { - t1 = 0; - t2 = 0; - while (t2 <= 1) { - for (t2 = t1 + step; t2 <= 1 + step; t2 += step) { - segment = p1.split(t1, t2); - if (!segment.simple()) { - t2 -= step; - if (abs(t1 - t2) < step) { - // we can never form a reduction - return []; - } - segment = p1.split(t1, t2); - segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); - segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2); - pass2.push(segment); - t1 = t2; - break; - } - } - } - if (t1 < 1) { - segment = p1.split(t1, 1); - segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); - segment._t2 = p1._t2; - pass2.push(segment); - } - }); - return pass2; - }, - scale: function(d) { - var order = this.order; - var distanceFn = false; - if (typeof d === "function") { - distanceFn = d; - } - if (distanceFn && order === 2) { - return this.raise().scale(distanceFn); - } - - // TODO: add special handling for degenerate (=linear) curves. - var clockwise = this.clockwise; - var r1 = distanceFn ? distanceFn(0) : d; - var r2 = distanceFn ? distanceFn(1) : d; - var v = [this.offset(0, 10), this.offset(1, 10)]; - var o = utils.lli4(v[0], v[0].c, v[1], v[1].c); - if (!o) { - throw new Error("cannot scale this curve. Try reducing it first."); - } - // move all points by distance 'd' wrt the origin 'o' - var points = this.points, - np = []; - - // move end points by fixed distance along normal. - [0, 1].forEach( - function(t) { - var p = (np[t * order] = utils.copy(points[t * order])); - p.x += (t ? r2 : r1) * v[t].n.x; - p.y += (t ? r2 : r1) * v[t].n.y; - }.bind(this) - ); - - if (!distanceFn) { - // move control points to lie on the intersection of the offset - // derivative vector, and the origin-through-control vector - [0, 1].forEach( - function(t) { - if (this.order === 2 && !!t) return; - var p = np[t * order]; - var d = this.derivative(t); - var p2 = { x: p.x + d.x, y: p.y + d.y }; - np[t + 1] = utils.lli4(p, p2, o, points[t + 1]); - }.bind(this) - ); - return new Bezier(np); - } - - // move control points by "however much necessary to - // ensure the correct tangent to endpoint". - [0, 1].forEach( - function(t) { - if (this.order === 2 && !!t) return; - var p = points[t + 1]; - var ov = { - x: p.x - o.x, - y: p.y - o.y - }; - var rc = distanceFn ? distanceFn((t + 1) / order) : d; - if (distanceFn && !clockwise) rc = -rc; - var m = sqrt(ov.x * ov.x + ov.y * ov.y); - ov.x /= m; - ov.y /= m; - np[t + 1] = { - x: p.x + rc * ov.x, - y: p.y + rc * ov.y - }; - }.bind(this) - ); - return new Bezier(np); - }, - outline: function(d1, d2, d3, d4) { - d2 = typeof d2 === "undefined" ? d1 : d2; - var reduced = this.reduce(), - len = reduced.length, - fcurves = [], - bcurves = [], - p, - alen = 0, - tlen = this.length(); - - var graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined"; - - function linearDistanceFunction(s, e, tlen, alen, slen) { - return function(v) { - var f1 = alen / tlen, - f2 = (alen + slen) / tlen, - d = e - s; - return utils.map(v, 0, 1, s + f1 * d, s + f2 * d); - }; - } - - // form curve oulines - reduced.forEach(function(segment) { - slen = segment.length(); - if (graduated) { - fcurves.push( - segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen)) - ); - bcurves.push( - segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen)) - ); - } else { - fcurves.push(segment.scale(d1)); - bcurves.push(segment.scale(-d2)); - } - alen += slen; - }); - - // reverse the "return" outline - bcurves = bcurves - .map(function(s) { - p = s.points; - if (p[3]) { - s.points = [p[3], p[2], p[1], p[0]]; - } else { - s.points = [p[2], p[1], p[0]]; - } - return s; - }) - .reverse(); - - // form the endcaps as lines - var fs = fcurves[0].points[0], - fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1], - bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1], - be = bcurves[0].points[0], - ls = utils.makeline(bs, fs), - le = utils.makeline(fe, be), - segments = [ls] - .concat(fcurves) - .concat([le]) - .concat(bcurves), - slen = segments.length; - - return new PolyBezier(segments); - }, - outlineshapes: function(d1, d2, curveIntersectionThreshold) { - d2 = d2 || d1; - var outline = this.outline(d1, d2).curves; - var shapes = []; - for (var i = 1, len = outline.length; i < len / 2; i++) { - var shape = utils.makeshape( - outline[i], - outline[len - i], - curveIntersectionThreshold - ); - shape.startcap.virtual = i > 1; - shape.endcap.virtual = i < len / 2 - 1; - shapes.push(shape); - } - return shapes; - }, - intersects: function(curve, curveIntersectionThreshold) { - if (!curve) return this.selfintersects(curveIntersectionThreshold); - if (curve.p1 && curve.p2) { - return this.lineIntersects(curve); - } - if (curve instanceof Bezier) { - curve = curve.reduce(); - } - return this.curveintersects( - this.reduce(), - curve, - curveIntersectionThreshold - ); - }, - lineIntersects: function(line) { - var mx = min(line.p1.x, line.p2.x), - my = min(line.p1.y, line.p2.y), - MX = max(line.p1.x, line.p2.x), - MY = max(line.p1.y, line.p2.y), - self = this; - return utils.roots(this.points, line).filter(function(t) { - var p = self.get(t); - return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY); - }); - }, - selfintersects: function(curveIntersectionThreshold) { - var reduced = this.reduce(); - // "simple" curves cannot intersect with their direct - // neighbour, so for each segment X we check whether - // it intersects [0:x-2][x+2:last]. - var i, - len = reduced.length - 2, - results = [], - result, - left, - right; - for (i = 0; i < len; i++) { - left = reduced.slice(i, i + 1); - right = reduced.slice(i + 2); - result = this.curveintersects(left, right, curveIntersectionThreshold); - results = results.concat(result); - } - return results; - }, - curveintersects: function(c1, c2, curveIntersectionThreshold) { - var pairs = []; - // step 1: pair off any overlapping segments - c1.forEach(function(l) { - c2.forEach(function(r) { - if (l.overlaps(r)) { - pairs.push({ left: l, right: r }); - } - }); - }); - // step 2: for each pairing, run through the convergence algorithm. - var intersections = []; - pairs.forEach(function(pair) { - var result = utils.pairiteration( - pair.left, - pair.right, - curveIntersectionThreshold - ); - if (result.length > 0) { - intersections = intersections.concat(result); - } - }); - return intersections; - }, - arcs: function(errorThreshold) { - errorThreshold = errorThreshold || 0.5; - var circles = []; - return this._iterate(errorThreshold, circles); - }, - _error: function(pc, np1, s, e) { - var q = (e - s) / 4, - c1 = this.get(s + q), - c2 = this.get(e - q), - ref = utils.dist(pc, np1), - d1 = utils.dist(pc, c1), - d2 = utils.dist(pc, c2); - return abs(d1 - ref) + abs(d2 - ref); - }, - _iterate: function(errorThreshold, circles) { - var t_s = 0, - t_e = 1, - safety; - // we do a binary search to find the "good `t` closest to no-longer-good" - do { - safety = 0; - - // step 1: start with the maximum possible arc - t_e = 1; - - // points: - var np1 = this.get(t_s), - np2, - np3, - arc, - prev_arc; - - // booleans: - var curr_good = false, - prev_good = false, - done; - - // numbers: - var t_m = t_e, - prev_e = 1, - step = 0; - - // step 2: find the best possible arc - do { - prev_good = curr_good; - prev_arc = arc; - t_m = (t_s + t_e) / 2; - step++; - - np2 = this.get(t_m); - np3 = this.get(t_e); - - arc = utils.getccenter(np1, np2, np3); - - //also save the t values - arc.interval = { - start: t_s, - end: t_e - }; - - var error = this._error(arc, np1, t_s, t_e); - curr_good = error <= errorThreshold; - - done = prev_good && !curr_good; - if (!done) prev_e = t_e; - - // this arc is fine: we can move 'e' up to see if we can find a wider arc - if (curr_good) { - // if e is already at max, then we're done for this arc. - if (t_e >= 1) { - // make sure we cap at t=1 - arc.interval.end = prev_e = 1; - prev_arc = arc; - // if we capped the arc segment to t=1 we also need to make sure that - // the arc's end angle is correct with respect to the bezier end point. - if (t_e > 1) { - var d = { - x: arc.x + arc.r * cos(arc.e), - y: arc.y + arc.r * sin(arc.e) - }; - arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1)); - } - break; - } - // if not, move it up by half the iteration distance - t_e = t_e + (t_e - t_s) / 2; - } else { - // this is a bad arc: we need to move 'e' down to find a good arc - t_e = t_m; - } - } while (!done && safety++ < 100); - - if (safety >= 100) { - break; - } - - // console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e); - - prev_arc = prev_arc ? prev_arc : arc; - circles.push(prev_arc); - t_s = prev_e; - } while (t_e < 1); - return circles; - } - }; - - module.exports = Bezier; -})(); diff --git a/lib/bezierjs/lib/bezier.js b/lib/bezierjs/lib/bezier.js deleted file mode 100644 index 25196206..00000000 --- a/lib/bezierjs/lib/bezier.js +++ /dev/null @@ -1,964 +0,0 @@ -/** - A javascript Bezier curve library by Pomax. - - Based on http://pomax.github.io/bezierinfo - - This code is MIT licensed. -**/ -(function() { - "use strict"; - - // math-inlining. - var abs = Math.abs, - min = Math.min, - max = Math.max, - cos = Math.cos, - sin = Math.sin, - acos = Math.acos, - sqrt = Math.sqrt, - pi = Math.PI, - // a zero coordinate, which is surprisingly useful - ZERO = { x: 0, y: 0, z: 0 }; - - // quite needed - var utils = require("./utils.js"); - - // only used for outlines atm. - var PolyBezier = require("./poly-bezier.js"); - - /** - * Bezier curve constructor. The constructor argument can be one of three things: - * - * 1. array/4 of {x:..., y:..., z:...}, z optional - * 2. numerical array/8 ordered x1,y1,x2,y2,x3,y3,x4,y4 - * 3. numerical array/12 ordered x1,y1,z1,x2,y2,z2,x3,y3,z3,x4,y4,z4 - * - */ - var Bezier = function(coords) { - var args = coords && coords.forEach ? coords : [].slice.call(arguments); - var coordlen = false; - if (typeof args[0] === "object") { - coordlen = args.length; - var newargs = []; - args.forEach(function(point) { - ["x", "y", "z"].forEach(function(d) { - if (typeof point[d] !== "undefined") { - newargs.push(point[d]); - } - }); - }); - args = newargs; - } - var higher = false; - var len = args.length; - if (coordlen) { - if (coordlen > 4) { - if (arguments.length !== 1) { - throw new Error( - "Only new Bezier(point[]) is accepted for 4th and higher order curves" - ); - } - higher = true; - } - } else { - if (len !== 6 && len !== 8 && len !== 9 && len !== 12) { - if (arguments.length !== 1) { - throw new Error( - "Only new Bezier(point[]) is accepted for 4th and higher order curves" - ); - } - } - } - var _3d = - (!higher && (len === 9 || len === 12)) || - (coords && coords[0] && typeof coords[0].z !== "undefined"); - this._3d = _3d; - var points = []; - for (var idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) { - var point = { - x: args[idx], - y: args[idx + 1] - }; - if (_3d) { - point.z = args[idx + 2]; - } - points.push(point); - } - this.order = points.length - 1; - this.points = points; - var dims = ["x", "y"]; - if (_3d) dims.push("z"); - this.dims = dims; - this.dimlen = dims.length; - - (function(curve) { - var order = curve.order; - var points = curve.points; - var a = utils.align(points, { p1: points[0], p2: points[order] }); - for (var i = 0; i < a.length; i++) { - if (abs(a[i].y) > 0.0001) { - curve._linear = false; - return; - } - } - curve._linear = true; - })(this); - - this._t1 = 0; - this._t2 = 1; - this.update(); - }; - - var svgToBeziers = require("./svg-to-beziers"); - - /** - * turn an svg d attribute into a sequence of Bezier segments. - */ - Bezier.SVGtoBeziers = function(d) { - return svgToBeziers(Bezier, d); - }; - - function getABC(n, S, B, E, t) { - if (typeof t === "undefined") { - t = 0.5; - } - var u = utils.projectionratio(t, n), - um = 1 - u, - C = { - x: u * S.x + um * E.x, - y: u * S.y + um * E.y - }, - s = utils.abcratio(t, n), - A = { - x: B.x + (B.x - C.x) / s, - y: B.y + (B.y - C.y) / s - }; - return { A: A, B: B, C: C }; - } - - Bezier.quadraticFromPoints = function(p1, p2, p3, t) { - if (typeof t === "undefined") { - t = 0.5; - } - // shortcuts, although they're really dumb - if (t === 0) { - return new Bezier(p2, p2, p3); - } - if (t === 1) { - return new Bezier(p1, p2, p2); - } - // real fitting. - var abc = getABC(2, p1, p2, p3, t); - return new Bezier(p1, abc.A, p3); - }; - - Bezier.cubicFromPoints = function(S, B, E, t, d1) { - if (typeof t === "undefined") { - t = 0.5; - } - var abc = getABC(3, S, B, E, t); - if (typeof d1 === "undefined") { - d1 = utils.dist(B, abc.C); - } - var d2 = d1 * (1 - t) / t; - - var selen = utils.dist(S, E), - lx = (E.x - S.x) / selen, - ly = (E.y - S.y) / selen, - bx1 = d1 * lx, - by1 = d1 * ly, - bx2 = d2 * lx, - by2 = d2 * ly; - // derivation of new hull coordinates - var e1 = { x: B.x - bx1, y: B.y - by1 }, - e2 = { x: B.x + bx2, y: B.y + by2 }, - A = abc.A, - v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) }, - v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t }, - nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t }, - nc2 = { - x: E.x + (v2.x - E.x) / (1 - t), - y: E.y + (v2.y - E.y) / (1 - t) - }; - // ...done - return new Bezier(S, nc1, nc2, E); - }; - - var getUtils = function() { - return utils; - }; - - Bezier.getUtils = getUtils; - - Bezier.PolyBezier = PolyBezier; - - Bezier.prototype = { - getUtils: getUtils, - valueOf: function() { - return this.toString(); - }, - toString: function() { - return utils.pointsToString(this.points); - }, - toSVG: function(relative) { - if (this._3d) return false; - var p = this.points, - x = p[0].x, - y = p[0].y, - s = ["M", x, y, this.order === 2 ? "Q" : "C"]; - for (var i = 1, last = p.length; i < last; i++) { - s.push(p[i].x); - s.push(p[i].y); - } - return s.join(" "); - }, - setRatios: function(ratios) { - if (ratios.length !== this.points.length) { - throw new Error("incorrect number of ratio values"); - } - this.ratios = ratios; - this._lut = []; // invalidate any precomputed LUT - }, - verify: function() { - var print = this.coordDigest(); - if (print !== this._print) { - this._print = print; - this.update(); - } - }, - coordDigest: function() { - return this.points.map(function(c,pos) { - return '' + pos + c.x + c.y + (c.z?c.z:0); - }).join(''); - }, - update: function(newprint) { - // invalidate any precomputed LUT - this._lut = []; - this.dpoints = utils.derive(this.points, this._3d); - this.computedirection(); - }, - computedirection: function() { - var points = this.points; - var angle = utils.angle(points[0], points[this.order], points[1]); - this.clockwise = angle > 0; - }, - length: function() { - return utils.length(this.derivative.bind(this)); - }, - _lut: [], - getLUT: function(steps) { - this.verify(); - steps = steps || 100; - if (this._lut.length === steps) { - return this._lut; - } - this._lut = []; - // We want a range from 0 to 1 inclusive, so - // we decrement and then use <= rather than <: - steps--; - for (var t = 0; t <= steps; t++) { - this._lut.push(this.compute(t / steps)); - } - return this._lut; - }, - on: function(point, error) { - error = error || 5; - var lut = this.getLUT(), - hits = [], - c, - t = 0; - for (var i = 0; i < lut.length; i++) { - c = lut[i]; - if (utils.dist(c, point) < error) { - hits.push(c); - t += i / lut.length; - } - } - if (!hits.length) return false; - return (t /= hits.length); - }, - project: function(point) { - // step 1: coarse check - var LUT = this.getLUT(), - l = LUT.length - 1, - closest = utils.closest(LUT, point), - mdist = closest.mdist, - mpos = closest.mpos; - - // step 2: fine check - var ft, - t, - p, - d, - t1 = (mpos - 1) / l, - t2 = (mpos + 1) / l, - step = 0.1 / l; - mdist += 1; - for (t = t1, ft = t; t < t2 + step; t += step) { - p = this.compute(t); - d = utils.dist(point, p); - if (d < mdist) { - mdist = d; - ft = t; - } - } - p = this.compute(ft); - p.t = ft; - p.d = mdist; - return p; - }, - get: function(t) { - return this.compute(t); - }, - point: function(idx) { - return this.points[idx]; - }, - compute: function(t) { - if (this.ratios) return utils.computeWithRatios(t, this.points, this.ratios, this._3d); - return utils.compute(t, this.points, this._3d, this.ratios); - }, - raise: function() { - var p = this.points, - np = [p[0]], - i, - k = p.length, - pi, - pim; - for (var i = 1; i < k; i++) { - pi = p[i]; - pim = p[i - 1]; - np[i] = { - x: (k - i) / k * pi.x + i / k * pim.x, - y: (k - i) / k * pi.y + i / k * pim.y - }; - } - np[k] = p[k - 1]; - return new Bezier(np); - }, - derivative: function(t) { - var mt = 1 - t, - a, - b, - c = 0, - p = this.dpoints[0]; - if (this.order === 2) { - p = [p[0], p[1], ZERO]; - a = mt; - b = t; - } - if (this.order === 3) { - a = mt * mt; - b = mt * t * 2; - c = t * t; - } - var ret = { - x: a * p[0].x + b * p[1].x + c * p[2].x, - y: a * p[0].y + b * p[1].y + c * p[2].y - }; - if (this._3d) { - ret.z = a * p[0].z + b * p[1].z + c * p[2].z; - } - return ret; - }, - curvature: function(t) { - return utils.curvature(t, this.points, this._3d); - }, - inflections: function() { - return utils.inflections(this.points); - }, - normal: function(t) { - return this._3d ? this.__normal3(t) : this.__normal2(t); - }, - __normal2: function(t) { - var d = this.derivative(t); - var q = sqrt(d.x * d.x + d.y * d.y); - return { x: -d.y / q, y: d.x / q }; - }, - __normal3: function(t) { - // see http://stackoverflow.com/questions/25453159 - var r1 = this.derivative(t), - r2 = this.derivative(t + 0.01), - q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z), - q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z); - r1.x /= q1; - r1.y /= q1; - r1.z /= q1; - r2.x /= q2; - r2.y /= q2; - r2.z /= q2; - // cross product - var c = { - x: r2.y * r1.z - r2.z * r1.y, - y: r2.z * r1.x - r2.x * r1.z, - z: r2.x * r1.y - r2.y * r1.x - }; - var m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z); - c.x /= m; - c.y /= m; - c.z /= m; - // rotation matrix - var R = [ - c.x * c.x, - c.x * c.y - c.z, - c.x * c.z + c.y, - c.x * c.y + c.z, - c.y * c.y, - c.y * c.z - c.x, - c.x * c.z - c.y, - c.y * c.z + c.x, - c.z * c.z - ]; - // normal vector: - var n = { - x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z, - y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z, - z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z - }; - return n; - }, - hull: function(t) { - var p = this.points, - _p = [], - pt, - q = [], - idx = 0, - i = 0, - l = 0; - q[idx++] = p[0]; - q[idx++] = p[1]; - q[idx++] = p[2]; - if (this.order === 3) { - q[idx++] = p[3]; - } - // we lerp between all points at each iteration, until we have 1 point left. - while (p.length > 1) { - _p = []; - for (i = 0, l = p.length - 1; i < l; i++) { - pt = utils.lerp(t, p[i], p[i + 1]); - q[idx++] = pt; - _p.push(pt); - } - p = _p; - } - return q; - }, - split: function(t1, t2) { - // shortcuts - if (t1 === 0 && !!t2) { - return this.split(t2).left; - } - if (t2 === 1) { - return this.split(t1).right; - } - - // no shortcut: use "de Casteljau" iteration. - var q = this.hull(t1); - var result = { - left: - this.order === 2 - ? new Bezier([q[0], q[3], q[5]]) - : new Bezier([q[0], q[4], q[7], q[9]]), - right: - this.order === 2 - ? new Bezier([q[5], q[4], q[2]]) - : new Bezier([q[9], q[8], q[6], q[3]]), - span: q - }; - - // make sure we bind _t1/_t2 information! - result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2); - result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2); - result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2); - result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2); - - // if we have no t2, we're done - if (!t2) { - return result; - } - - // if we have a t2, split again: - t2 = utils.map(t2, t1, 1, 0, 1); - var subsplit = result.right.split(t2); - return subsplit.left; - }, - extrema: function() { - var dims = this.dims, - result = {}, - roots = [], - p, - mfn; - dims.forEach( - function(dim) { - mfn = function(v) { - return v[dim]; - }; - p = this.dpoints[0].map(mfn); - result[dim] = utils.droots(p); - if (this.order === 3) { - p = this.dpoints[1].map(mfn); - result[dim] = result[dim].concat(utils.droots(p)); - } - result[dim] = result[dim].filter(function(t) { - return t >= 0 && t <= 1; - }); - roots = roots.concat(result[dim].sort(utils.numberSort)); - }.bind(this) - ); - roots = roots.sort(utils.numberSort).filter(function(v, idx) { - return roots.indexOf(v) === idx; - }); - result.values = roots; - return result; - }, - bbox: function() { - var extrema = this.extrema(), - result = {}; - this.dims.forEach( - function(d) { - result[d] = utils.getminmax(this, d, extrema[d]); - }.bind(this) - ); - return result; - }, - overlaps: function(curve) { - var lbbox = this.bbox(), - tbbox = curve.bbox(); - return utils.bboxoverlap(lbbox, tbbox); - }, - offset: function(t, d) { - if (typeof d !== "undefined") { - var c = this.get(t); - var n = this.normal(t); - var ret = { - c: c, - n: n, - x: c.x + n.x * d, - y: c.y + n.y * d - }; - if (this._3d) { - ret.z = c.z + n.z * d; - } - return ret; - } - if (this._linear) { - var nv = this.normal(0); - var coords = this.points.map(function(p) { - var ret = { - x: p.x + t * nv.x, - y: p.y + t * nv.y - }; - if (p.z && n.z) { - ret.z = p.z + t * nv.z; - } - return ret; - }); - return [new Bezier(coords)]; - } - var reduced = this.reduce(); - return reduced.map(function(s) { - if (s._linear) { - return s.offset(t)[0]; - } - return s.scale(t); - }); - }, - simple: function() { - if (this.order === 3) { - var a1 = utils.angle(this.points[0], this.points[3], this.points[1]); - var a2 = utils.angle(this.points[0], this.points[3], this.points[2]); - if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false; - } - var n1 = this.normal(0); - var n2 = this.normal(1); - var s = n1.x * n2.x + n1.y * n2.y; - if (this._3d) { - s += n1.z * n2.z; - } - var angle = abs(acos(s)); - return angle < pi / 3; - }, - reduce: function() { - var i, - t1 = 0, - t2 = 0, - step = 0.01, - segment, - pass1 = [], - pass2 = []; - // first pass: split on extrema - var extrema = this.extrema().values; - if (extrema.indexOf(0) === -1) { - extrema = [0].concat(extrema); - } - if (extrema.indexOf(1) === -1) { - extrema.push(1); - } - - for (t1 = extrema[0], i = 1; i < extrema.length; i++) { - t2 = extrema[i]; - segment = this.split(t1, t2); - segment._t1 = t1; - segment._t2 = t2; - pass1.push(segment); - t1 = t2; - } - - // second pass: further reduce these segments to simple segments - pass1.forEach(function(p1) { - t1 = 0; - t2 = 0; - while (t2 <= 1) { - for (t2 = t1 + step; t2 <= 1 + step; t2 += step) { - segment = p1.split(t1, t2); - if (!segment.simple()) { - t2 -= step; - if (abs(t1 - t2) < step) { - // we can never form a reduction - return []; - } - segment = p1.split(t1, t2); - segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); - segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2); - pass2.push(segment); - t1 = t2; - break; - } - } - } - if (t1 < 1) { - segment = p1.split(t1, 1); - segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); - segment._t2 = p1._t2; - pass2.push(segment); - } - }); - return pass2; - }, - scale: function(d) { - var order = this.order; - var distanceFn = false; - if (typeof d === "function") { - distanceFn = d; - } - if (distanceFn && order === 2) { - return this.raise().scale(distanceFn); - } - - // TODO: add special handling for degenerate (=linear) curves. - var clockwise = this.clockwise; - var r1 = distanceFn ? distanceFn(0) : d; - var r2 = distanceFn ? distanceFn(1) : d; - var v = [this.offset(0, 10), this.offset(1, 10)]; - var o = utils.lli4(v[0], v[0].c, v[1], v[1].c); - if (!o) { - throw new Error("cannot scale this curve. Try reducing it first."); - } - // move all points by distance 'd' wrt the origin 'o' - var points = this.points, - np = []; - - // move end points by fixed distance along normal. - [0, 1].forEach( - function(t) { - var p = (np[t * order] = utils.copy(points[t * order])); - p.x += (t ? r2 : r1) * v[t].n.x; - p.y += (t ? r2 : r1) * v[t].n.y; - }.bind(this) - ); - - if (!distanceFn) { - // move control points to lie on the intersection of the offset - // derivative vector, and the origin-through-control vector - [0, 1].forEach( - function(t) { - if (this.order === 2 && !!t) return; - var p = np[t * order]; - var d = this.derivative(t); - var p2 = { x: p.x + d.x, y: p.y + d.y }; - np[t + 1] = utils.lli4(p, p2, o, points[t + 1]); - }.bind(this) - ); - return new Bezier(np); - } - - // move control points by "however much necessary to - // ensure the correct tangent to endpoint". - [0, 1].forEach( - function(t) { - if (this.order === 2 && !!t) return; - var p = points[t + 1]; - var ov = { - x: p.x - o.x, - y: p.y - o.y - }; - var rc = distanceFn ? distanceFn((t + 1) / order) : d; - if (distanceFn && !clockwise) rc = -rc; - var m = sqrt(ov.x * ov.x + ov.y * ov.y); - ov.x /= m; - ov.y /= m; - np[t + 1] = { - x: p.x + rc * ov.x, - y: p.y + rc * ov.y - }; - }.bind(this) - ); - return new Bezier(np); - }, - outline: function(d1, d2, d3, d4) { - d2 = typeof d2 === "undefined" ? d1 : d2; - var reduced = this.reduce(), - len = reduced.length, - fcurves = [], - bcurves = [], - p, - alen = 0, - tlen = this.length(); - - var graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined"; - - function linearDistanceFunction(s, e, tlen, alen, slen) { - return function(v) { - var f1 = alen / tlen, - f2 = (alen + slen) / tlen, - d = e - s; - return utils.map(v, 0, 1, s + f1 * d, s + f2 * d); - }; - } - - // form curve oulines - reduced.forEach(function(segment) { - slen = segment.length(); - if (graduated) { - fcurves.push( - segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen)) - ); - bcurves.push( - segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen)) - ); - } else { - fcurves.push(segment.scale(d1)); - bcurves.push(segment.scale(-d2)); - } - alen += slen; - }); - - // reverse the "return" outline - bcurves = bcurves - .map(function(s) { - p = s.points; - if (p[3]) { - s.points = [p[3], p[2], p[1], p[0]]; - } else { - s.points = [p[2], p[1], p[0]]; - } - return s; - }) - .reverse(); - - // form the endcaps as lines - var fs = fcurves[0].points[0], - fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1], - bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1], - be = bcurves[0].points[0], - ls = utils.makeline(bs, fs), - le = utils.makeline(fe, be), - segments = [ls] - .concat(fcurves) - .concat([le]) - .concat(bcurves), - slen = segments.length; - - return new PolyBezier(segments); - }, - outlineshapes: function(d1, d2, curveIntersectionThreshold) { - d2 = d2 || d1; - var outline = this.outline(d1, d2).curves; - var shapes = []; - for (var i = 1, len = outline.length; i < len / 2; i++) { - var shape = utils.makeshape( - outline[i], - outline[len - i], - curveIntersectionThreshold - ); - shape.startcap.virtual = i > 1; - shape.endcap.virtual = i < len / 2 - 1; - shapes.push(shape); - } - return shapes; - }, - intersects: function(curve, curveIntersectionThreshold) { - if (!curve) return this.selfintersects(curveIntersectionThreshold); - if (curve.p1 && curve.p2) { - return this.lineIntersects(curve); - } - if (curve instanceof Bezier) { - curve = curve.reduce(); - } - return this.curveintersects( - this.reduce(), - curve, - curveIntersectionThreshold - ); - }, - lineIntersects: function(line) { - var mx = min(line.p1.x, line.p2.x), - my = min(line.p1.y, line.p2.y), - MX = max(line.p1.x, line.p2.x), - MY = max(line.p1.y, line.p2.y), - self = this; - return utils.roots(this.points, line).filter(function(t) { - var p = self.get(t); - return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY); - }); - }, - selfintersects: function(curveIntersectionThreshold) { - var reduced = this.reduce(); - // "simple" curves cannot intersect with their direct - // neighbour, so for each segment X we check whether - // it intersects [0:x-2][x+2:last]. - var i, - len = reduced.length - 2, - results = [], - result, - left, - right; - for (i = 0; i < len; i++) { - left = reduced.slice(i, i + 1); - right = reduced.slice(i + 2); - result = this.curveintersects(left, right, curveIntersectionThreshold); - results = results.concat(result); - } - return results; - }, - curveintersects: function(c1, c2, curveIntersectionThreshold) { - var pairs = []; - // step 1: pair off any overlapping segments - c1.forEach(function(l) { - c2.forEach(function(r) { - if (l.overlaps(r)) { - pairs.push({ left: l, right: r }); - } - }); - }); - // step 2: for each pairing, run through the convergence algorithm. - var intersections = []; - pairs.forEach(function(pair) { - var result = utils.pairiteration( - pair.left, - pair.right, - curveIntersectionThreshold - ); - if (result.length > 0) { - intersections = intersections.concat(result); - } - }); - return intersections; - }, - arcs: function(errorThreshold) { - errorThreshold = errorThreshold || 0.5; - var circles = []; - return this._iterate(errorThreshold, circles); - }, - _error: function(pc, np1, s, e) { - var q = (e - s) / 4, - c1 = this.get(s + q), - c2 = this.get(e - q), - ref = utils.dist(pc, np1), - d1 = utils.dist(pc, c1), - d2 = utils.dist(pc, c2); - return abs(d1 - ref) + abs(d2 - ref); - }, - _iterate: function(errorThreshold, circles) { - var t_s = 0, - t_e = 1, - safety; - // we do a binary search to find the "good `t` closest to no-longer-good" - do { - safety = 0; - - // step 1: start with the maximum possible arc - t_e = 1; - - // points: - var np1 = this.get(t_s), - np2, - np3, - arc, - prev_arc; - - // booleans: - var curr_good = false, - prev_good = false, - done; - - // numbers: - var t_m = t_e, - prev_e = 1, - step = 0; - - // step 2: find the best possible arc - do { - prev_good = curr_good; - prev_arc = arc; - t_m = (t_s + t_e) / 2; - step++; - - np2 = this.get(t_m); - np3 = this.get(t_e); - - arc = utils.getccenter(np1, np2, np3); - - //also save the t values - arc.interval = { - start: t_s, - end: t_e - }; - - var error = this._error(arc, np1, t_s, t_e); - curr_good = error <= errorThreshold; - - done = prev_good && !curr_good; - if (!done) prev_e = t_e; - - // this arc is fine: we can move 'e' up to see if we can find a wider arc - if (curr_good) { - // if e is already at max, then we're done for this arc. - if (t_e >= 1) { - // make sure we cap at t=1 - arc.interval.end = prev_e = 1; - prev_arc = arc; - // if we capped the arc segment to t=1 we also need to make sure that - // the arc's end angle is correct with respect to the bezier end point. - if (t_e > 1) { - var d = { - x: arc.x + arc.r * cos(arc.e), - y: arc.y + arc.r * sin(arc.e) - }; - arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1)); - } - break; - } - // if not, move it up by half the iteration distance - t_e = t_e + (t_e - t_s) / 2; - } else { - // this is a bad arc: we need to move 'e' down to find a good arc - t_e = t_m; - } - } while (!done && safety++ < 100); - - if (safety >= 100) { - break; - } - - // console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e); - - prev_arc = prev_arc ? prev_arc : arc; - circles.push(prev_arc); - t_s = prev_e; - } while (t_e < 1); - return circles; - } - }; - - module.exports = Bezier; -})(); diff --git a/lib/bezierjs/lib/normalise-svg.js b/lib/bezierjs/lib/normalise-svg.js deleted file mode 100644 index b5fd3230..00000000 --- a/lib/bezierjs/lib/normalise-svg.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Normalise an SVG path to absolute coordinates - * and full commands, rather than relative coordinates - * and/or shortcut commands. - */ -function normalizePath(d) { - // preprocess "d" so that we have spaces between values - d = d - .replace(/,/g, " ") // replace commas with spaces - .replace(/-/g, " - ") // add spacing around minus signs - .replace(/-\s+/g, "-") // remove spacing to the right of minus signs. - .replace(/([a-zA-Z])/g, " $1 "); - - // set up the variables used in this function - var instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"), - instructionLength = instructions.length, - i, - instruction, - op, - lop, - args = [], - alen, - a, - sx = 0, - sy = 0, - x = 0, - y = 0, - cx = 0, - cy = 0, - cx2 = 0, - cy2 = 0, - normalized = ""; - - // we run through the instruction list starting at 1, not 0, - // because we split up "|M x y ...." so the first element will - // always be an empty string. By design. - for (i = 1; i < instructionLength; i++) { - // which instruction is this? - instruction = instructions[i]; - op = instruction.substring(0, 1); - lop = op.toLowerCase(); - - // what are the arguments? note that we need to convert - // all strings into numbers, or + will do silly things. - args = instruction - .replace(op, "") - .trim() - .split(" "); - args = args - .filter(function(v) { - return v !== ""; - }) - .map(parseFloat); - alen = args.length; - - // we could use a switch, but elaborate code in a "case" with - // fallthrough is just horrid to read. So let's use ifthen - // statements instead. - - // moveto command (plus possible lineto) - if (lop === "m") { - normalized += "M "; - if (op === "m") { - x += args[0]; - y += args[1]; - } else { - x = args[0]; - y = args[1]; - } - // records start position, for dealing - // with the shape close operator ('Z') - sx = x; - sy = y; - normalized += x + " " + y + " "; - if (alen > 2) { - for (a = 0; a < alen; a += 2) { - if (op === "m") { - x += args[a]; - y += args[a + 1]; - } else { - x = args[a]; - y = args[a + 1]; - } - normalized += ["L",x,y,''].join(" "); - } - } - } else if (lop === "l") { - // lineto commands - for (a = 0; a < alen; a += 2) { - if (op === "l") { - x += args[a]; - y += args[a + 1]; - } else { - x = args[a]; - y = args[a + 1]; - } - normalized += ["L",x,y,''].join(" "); - } - } else if (lop === "h") { - for (a = 0; a < alen; a++) { - if (op === "h") { - x += args[a]; - } else { - x = args[a]; - } - normalized += ["L",x,y,''].join(" "); - } - } else if (lop === "v") { - for (a = 0; a < alen; a++) { - if (op === "v") { - y += args[a]; - } else { - y = args[a]; - } - normalized += ["L",x,y,''].join(" "); - } - } else if (lop === "q") { - // quadratic curveto commands - for (a = 0; a < alen; a += 4) { - if (op === "q") { - cx = x + args[a]; - cy = y + args[a + 1]; - x += args[a + 2]; - y += args[a + 3]; - } else { - cx = args[a]; - cy = args[a + 1]; - x = args[a + 2]; - y = args[a + 3]; - } - normalized += ["Q",cx,cy,x,y,''].join(" "); - } - } else if (lop === "t") { - for (a = 0; a < alen; a += 2) { - // reflect previous cx/cy over x/y - cx = x + (x - cx); - cy = y + (y - cy); - // then get real end point - if (op === "t") { - x += args[a]; - y += args[a + 1]; - } else { - x = args[a]; - y = args[a + 1]; - } - normalized += ["Q",cx,cy,x,y,''].join(" "); - } - } else if (lop === "c") { - // cubic curveto commands - for (a = 0; a < alen; a += 6) { - if (op === "c") { - cx = x + args[a]; - cy = y + args[a + 1]; - cx2 = x + args[a + 2]; - cy2 = y + args[a + 3]; - x += args[a + 4]; - y += args[a + 5]; - } else { - cx = args[a]; - cy = args[a + 1]; - cx2 = args[a + 2]; - cy2 = args[a + 3]; - x = args[a + 4]; - y = args[a + 5]; - } - normalized += ["C",cx,cy,cx2,cy2,x,y,''].join(" "); - } - } else if (lop === "s") { - for (a = 0; a < alen; a += 4) { - // reflect previous cx2/cy2 over x/y - cx = x + (x - cx2); - cy = y + (y - cy2); - // then get real control and end point - if (op === "s") { - cx2 = x + args[a]; - cy2 = y + args[a + 1]; - x += args[a + 2]; - y += args[a + 3]; - } else { - cx2 = args[a]; - cy2 = args[a + 1]; - x = args[a + 2]; - y = args[a + 3]; - } - normalized +=["C",cx,cy,cx2,cy2,x,y,''].join(" "); - } - } else if (lop === "z") { - normalized += "Z "; - // not unimportant: path closing changes the current x/y coordinate - x = sx; - y = sy; - } - } - return normalized.trim(); -} - -module.exports = normalizePath; diff --git a/lib/bezierjs/lib/poly-bezier.js b/lib/bezierjs/lib/poly-bezier.js deleted file mode 100644 index 17656960..00000000 --- a/lib/bezierjs/lib/poly-bezier.js +++ /dev/null @@ -1,68 +0,0 @@ -(function() { - "use strict"; - - var utils = require("./utils.js"); - - /** - * Poly Bezier - * @param {[type]} curves [description] - */ - var PolyBezier = function(curves) { - this.curves = []; - this._3d = false; - if (!!curves) { - this.curves = curves; - this._3d = this.curves[0]._3d; - } - }; - - PolyBezier.prototype = { - valueOf: function() { - return this.toString(); - }, - toString: function() { - return ( - "[" + - this.curves - .map(function(curve) { - return utils.pointsToString(curve.points); - }) - .join(", ") + - "]" - ); - }, - addCurve: function(curve) { - this.curves.push(curve); - this._3d = this._3d || curve._3d; - }, - length: function() { - return this.curves - .map(function(v) { - return v.length(); - }) - .reduce(function(a, b) { - return a + b; - }); - }, - curve: function(idx) { - return this.curves[idx]; - }, - bbox: function() { - var c = this.curves; - var bbox = c[0].bbox(); - for (var i = 1; i < c.length; i++) { - utils.expandbox(bbox, c[i].bbox()); - } - return bbox; - }, - offset: function(d) { - var offset = []; - this.curves.forEach(function(v) { - offset = offset.concat(v.offset(d)); - }); - return new PolyBezier(offset); - } - }; - - module.exports = PolyBezier; -})(); diff --git a/lib/bezierjs/lib/svg-to-beziers.js b/lib/bezierjs/lib/svg-to-beziers.js deleted file mode 100644 index 3923a04f..00000000 --- a/lib/bezierjs/lib/svg-to-beziers.js +++ /dev/null @@ -1,41 +0,0 @@ -var normalise = require("./normalise-svg.js"); - -var M = { x: false, y: false }; - -function makeBezier(Bezier, term, values) { - if (term === 'Z') return; - if (term === 'M') { - M = {x: values[0], y: values[1]}; - return; - } - // ES7: new Bezier(M.x, M.y, ...values) - var cvalues = [false, M.x, M.y].concat(values); - var PreboundConstructor = Bezier.bind.apply(Bezier, cvalues) - var curve = new PreboundConstructor(); - var last = values.slice(-2); - M = { x : last[0], y: last[1] }; - return curve; -} - -function convertPath(Bezier, d) { - var terms = normalise(d).split(" "), - term, - matcher = new RegExp("[MLCQZ]", ""), - segment, - values, - segments = [], - ARGS = { "C": 6, "Q": 4, "L": 2, "M": 2}; - - while (terms.length) { - term = terms.splice(0,1)[0]; - if (matcher.test(term)) { - values = terms.splice(0, ARGS[term]).map(parseFloat); - segment = makeBezier(Bezier, term, values); - if (segment) segments.push(segment); - } - } - - return new Bezier.PolyBezier(segments); -} - -module.exports = convertPath; diff --git a/lib/bezierjs/lib/utils.js b/lib/bezierjs/lib/utils.js deleted file mode 100644 index 32cd5635..00000000 --- a/lib/bezierjs/lib/utils.js +++ /dev/null @@ -1,893 +0,0 @@ -(function() { - "use strict"; - - // math-inlining. - var abs = Math.abs, - cos = Math.cos, - sin = Math.sin, - acos = Math.acos, - atan2 = Math.atan2, - sqrt = Math.sqrt, - pow = Math.pow, - // cube root function yielding real roots - crt = function(v) { - return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3); - }, - // trig constants - pi = Math.PI, - tau = 2 * pi, - quart = pi / 2, - // float precision significant decimal - epsilon = 0.000001, - // extremas used in bbox calculation and similar algorithms - nMax = Number.MAX_SAFE_INTEGER || 9007199254740991, - nMin = Number.MIN_SAFE_INTEGER || -9007199254740991, - // a zero coordinate, which is surprisingly useful - ZERO = { x: 0, y: 0, z: 0 }; - - // Bezier utility functions - var utils = { - // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) - Tvalues: [ - -0.0640568928626056260850430826247450385909, - 0.0640568928626056260850430826247450385909, - -0.1911188674736163091586398207570696318404, - 0.1911188674736163091586398207570696318404, - -0.3150426796961633743867932913198102407864, - 0.3150426796961633743867932913198102407864, - -0.4337935076260451384870842319133497124524, - 0.4337935076260451384870842319133497124524, - -0.5454214713888395356583756172183723700107, - 0.5454214713888395356583756172183723700107, - -0.6480936519369755692524957869107476266696, - 0.6480936519369755692524957869107476266696, - -0.7401241915785543642438281030999784255232, - 0.7401241915785543642438281030999784255232, - -0.8200019859739029219539498726697452080761, - 0.8200019859739029219539498726697452080761, - -0.8864155270044010342131543419821967550873, - 0.8864155270044010342131543419821967550873, - -0.9382745520027327585236490017087214496548, - 0.9382745520027327585236490017087214496548, - -0.9747285559713094981983919930081690617411, - 0.9747285559713094981983919930081690617411, - -0.9951872199970213601799974097007368118745, - 0.9951872199970213601799974097007368118745 - ], - - // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) - Cvalues: [ - 0.1279381953467521569740561652246953718517, - 0.1279381953467521569740561652246953718517, - 0.1258374563468282961213753825111836887264, - 0.1258374563468282961213753825111836887264, - 0.121670472927803391204463153476262425607, - 0.121670472927803391204463153476262425607, - 0.1155056680537256013533444839067835598622, - 0.1155056680537256013533444839067835598622, - 0.1074442701159656347825773424466062227946, - 0.1074442701159656347825773424466062227946, - 0.0976186521041138882698806644642471544279, - 0.0976186521041138882698806644642471544279, - 0.086190161531953275917185202983742667185, - 0.086190161531953275917185202983742667185, - 0.0733464814110803057340336152531165181193, - 0.0733464814110803057340336152531165181193, - 0.0592985849154367807463677585001085845412, - 0.0592985849154367807463677585001085845412, - 0.0442774388174198061686027482113382288593, - 0.0442774388174198061686027482113382288593, - 0.0285313886289336631813078159518782864491, - 0.0285313886289336631813078159518782864491, - 0.0123412297999871995468056670700372915759, - 0.0123412297999871995468056670700372915759 - ], - - arcfn: function(t, derivativeFn) { - var d = derivativeFn(t); - var l = d.x * d.x + d.y * d.y; - if (typeof d.z !== "undefined") { - l += d.z * d.z; - } - return sqrt(l); - }, - - compute: function(t, points, _3d) { - // shortcuts - if (t === 0) { - return points[0]; - } - - var order = points.length-1; - - if (t === 1) { - return points[order]; - } - - var p = points; - var mt = 1 - t; - - // constant? - if (order === 0) { - return points[0]; - } - - // linear? - if (order === 1) { - ret = { - x: mt * p[0].x + t * p[1].x, - y: mt * p[0].y + t * p[1].y - }; - if (_3d) { - ret.z = mt * p[0].z + t * p[1].z; - } - return ret; - } - - // quadratic/cubic curve? - if (order < 4) { - var mt2 = mt * mt, - t2 = t * t, - a, - b, - c, - d = 0; - if (order === 2) { - p = [p[0], p[1], p[2], ZERO]; - a = mt2; - b = mt * t * 2; - c = t2; - } else if (order === 3) { - a = mt2 * mt; - b = mt2 * t * 3; - c = mt * t2 * 3; - d = t * t2; - } - var ret = { - x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, - y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y - }; - if (_3d) { - ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z; - } - return ret; - } - - // higher order curves: use de Casteljau's computation - var dCpts = JSON.parse(JSON.stringify(points)); - while (dCpts.length > 1) { - for (var i = 0; i < dCpts.length - 1; i++) { - dCpts[i] = { - x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t, - y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t - }; - if (typeof dCpts[i].z !== "undefined") { - dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t; - } - } - dCpts.splice(dCpts.length - 1, 1); - } - return dCpts[0]; - }, - - computeWithRatios: function (t, points, ratios, _3d) { - var mt = 1 - t, r = ratios, p = points, d; - var f1 = r[0], f2 = r[1], f3 = r[2], f4 = r[3]; - - // spec for linear - f1 *= mt; - f2 *= t; - - if (p.length === 2) { - d = f1 + f2; - return { - x: (f1 * p[0].x + f2 * p[1].x)/d, - y: (f1 * p[0].y + f2 * p[1].y)/d, - z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z)/d - }; - } - - // upgrade to quadratic - f1 *= mt; - f2 *= 2 * mt; - f3 *= t * t; - - if (p.length === 3) { - d = f1 + f2 + f3; - return { - x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x)/d, - y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y)/d, - z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z)/d - }; - } - - // upgrade to cubic - f1 *= mt; - f2 *= 1.5 * mt; - f3 *= 3 * mt; - f4 *= t * t * t; - - if (p.length === 4) { - d = f1 + f2 + f3 + f4; - return { - x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x)/d, - y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y)/d, - z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z)/d - }; - } - }, - - derive: function (points, _3d) { - var dpoints = []; - for (var p = points, d = p.length, c = d - 1; d > 1; d--, c--) { - var list = []; - for (var j = 0, dpt; j < c; j++) { - dpt = { - x: c * (p[j + 1].x - p[j].x), - y: c * (p[j + 1].y - p[j].y) - }; - if (_3d) { - dpt.z = c * (p[j + 1].z - p[j].z); - } - list.push(dpt); - } - dpoints.push(list); - p = list; - } - return dpoints; - }, - - between: function(v, m, M) { - return ( - (m <= v && v <= M) || - utils.approximately(v, m) || - utils.approximately(v, M) - ); - }, - - approximately: function(a, b, precision) { - return abs(a - b) <= (precision || epsilon); - }, - - length: function(derivativeFn) { - var z = 0.5, - sum = 0, - len = utils.Tvalues.length, - i, - t; - for (i = 0; i < len; i++) { - t = z * utils.Tvalues[i] + z; - sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn); - } - return z * sum; - }, - - map: function(v, ds, de, ts, te) { - var d1 = de - ds, - d2 = te - ts, - v2 = v - ds, - r = v2 / d1; - return ts + d2 * r; - }, - - lerp: function(r, v1, v2) { - var ret = { - x: v1.x + r * (v2.x - v1.x), - y: v1.y + r * (v2.y - v1.y) - }; - if (!!v1.z && !!v2.z) { - ret.z = v1.z + r * (v2.z - v1.z); - } - return ret; - }, - - pointToString: function(p) { - var s = p.x + "/" + p.y; - if (typeof p.z !== "undefined") { - s += "/" + p.z; - } - return s; - }, - - pointsToString: function(points) { - return "[" + points.map(utils.pointToString).join(", ") + "]"; - }, - - copy: function(obj) { - return JSON.parse(JSON.stringify(obj)); - }, - - angle: function(o, v1, v2) { - var dx1 = v1.x - o.x, - dy1 = v1.y - o.y, - dx2 = v2.x - o.x, - dy2 = v2.y - o.y, - cross = dx1 * dy2 - dy1 * dx2, - dot = dx1 * dx2 + dy1 * dy2; - return atan2(cross, dot); - }, - - // round as string, to avoid rounding errors - round: function(v, d) { - var s = "" + v; - var pos = s.indexOf("."); - return parseFloat(s.substring(0, pos + 1 + d)); - }, - - dist: function(p1, p2) { - var dx = p1.x - p2.x, - dy = p1.y - p2.y; - return sqrt(dx * dx + dy * dy); - }, - - closest: function(LUT, point) { - var mdist = pow(2, 63), - mpos, - d; - LUT.forEach(function(p, idx) { - d = utils.dist(point, p); - if (d < mdist) { - mdist = d; - mpos = idx; - } - }); - return { mdist: mdist, mpos: mpos }; - }, - - abcratio: function(t, n) { - // see ratio(t) note on http://pomax.github.io/bezierinfo/#abc - if (n !== 2 && n !== 3) { - return false; - } - if (typeof t === "undefined") { - t = 0.5; - } else if (t === 0 || t === 1) { - return t; - } - var bottom = pow(t, n) + pow(1 - t, n), - top = bottom - 1; - return abs(top / bottom); - }, - - projectionratio: function(t, n) { - // see u(t) note on http://pomax.github.io/bezierinfo/#abc - if (n !== 2 && n !== 3) { - return false; - } - if (typeof t === "undefined") { - t = 0.5; - } else if (t === 0 || t === 1) { - return t; - } - var top = pow(1 - t, n), - bottom = pow(t, n) + top; - return top / bottom; - }, - - lli8: function(x1, y1, x2, y2, x3, y3, x4, y4) { - var nx = - (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), - ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), - d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); - if (d == 0) { - return false; - } - return { x: nx / d, y: ny / d }; - }, - - lli4: function(p1, p2, p3, p4) { - var x1 = p1.x, - y1 = p1.y, - x2 = p2.x, - y2 = p2.y, - x3 = p3.x, - y3 = p3.y, - x4 = p4.x, - y4 = p4.y; - return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4); - }, - - lli: function(v1, v2) { - return utils.lli4(v1, v1.c, v2, v2.c); - }, - - makeline: function(p1, p2) { - var Bezier = require("./bezier"); - var x1 = p1.x, - y1 = p1.y, - x2 = p2.x, - y2 = p2.y, - dx = (x2 - x1) / 3, - dy = (y2 - y1) / 3; - return new Bezier( - x1, - y1, - x1 + dx, - y1 + dy, - x1 + 2 * dx, - y1 + 2 * dy, - x2, - y2 - ); - }, - - findbbox: function(sections) { - var mx = nMax, - my = nMax, - MX = nMin, - MY = nMin; - sections.forEach(function(s) { - var bbox = s.bbox(); - if (mx > bbox.x.min) mx = bbox.x.min; - if (my > bbox.y.min) my = bbox.y.min; - if (MX < bbox.x.max) MX = bbox.x.max; - if (MY < bbox.y.max) MY = bbox.y.max; - }); - return { - x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx }, - y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my } - }; - }, - - shapeintersections: function( - s1, - bbox1, - s2, - bbox2, - curveIntersectionThreshold - ) { - if (!utils.bboxoverlap(bbox1, bbox2)) return []; - var intersections = []; - var a1 = [s1.startcap, s1.forward, s1.back, s1.endcap]; - var a2 = [s2.startcap, s2.forward, s2.back, s2.endcap]; - a1.forEach(function(l1) { - if (l1.virtual) return; - a2.forEach(function(l2) { - if (l2.virtual) return; - var iss = l1.intersects(l2, curveIntersectionThreshold); - if (iss.length > 0) { - iss.c1 = l1; - iss.c2 = l2; - iss.s1 = s1; - iss.s2 = s2; - intersections.push(iss); - } - }); - }); - return intersections; - }, - - makeshape: function(forward, back, curveIntersectionThreshold) { - var bpl = back.points.length; - var fpl = forward.points.length; - var start = utils.makeline(back.points[bpl - 1], forward.points[0]); - var end = utils.makeline(forward.points[fpl - 1], back.points[0]); - var shape = { - startcap: start, - forward: forward, - back: back, - endcap: end, - bbox: utils.findbbox([start, forward, back, end]) - }; - var self = utils; - shape.intersections = function(s2) { - return self.shapeintersections( - shape, - shape.bbox, - s2, - s2.bbox, - curveIntersectionThreshold - ); - }; - return shape; - }, - - getminmax: function(curve, d, list) { - if (!list) return { min: 0, max: 0 }; - var min = nMax, - max = nMin, - t, - c; - if (list.indexOf(0) === -1) { - list = [0].concat(list); - } - if (list.indexOf(1) === -1) { - list.push(1); - } - for (var i = 0, len = list.length; i < len; i++) { - t = list[i]; - c = curve.get(t); - if (c[d] < min) { - min = c[d]; - } - if (c[d] > max) { - max = c[d]; - } - } - return { min: min, mid: (min + max) / 2, max: max, size: max - min }; - }, - - align: function(points, line) { - var tx = line.p1.x, - ty = line.p1.y, - a = -atan2(line.p2.y - ty, line.p2.x - tx), - d = function(v) { - return { - x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a), - y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a) - }; - }; - return points.map(d); - }, - - roots: function(points, line) { - line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } }; - var order = points.length - 1; - var p = utils.align(points, line); - var reduce = function(t) { - return 0 <= t && t <= 1; - }; - - if (order === 2) { - var a = p[0].y, - b = p[1].y, - c = p[2].y, - d = a - 2 * b + c; - if (d !== 0) { - var m1 = -sqrt(b * b - a * c), - m2 = -a + b, - v1 = -(m1 + m2) / d, - v2 = -(-m1 + m2) / d; - return [v1, v2].filter(reduce); - } else if (b !== c && d === 0) { - return [(2*b - c)/(2*b - 2*c)].filter(reduce); - } - return []; - } - - // see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm - var pa = p[0].y, - pb = p[1].y, - pc = p[2].y, - pd = p[3].y, - d = -pa + 3 * pb - 3 * pc + pd, - a = 3 * pa - 6 * pb + 3 * pc, - b = -3 * pa + 3 * pb, - c = pa; - - if (utils.approximately(d, 0)) { - // this is not a cubic curve. - if (utils.approximately(a, 0)) { - // in fact, this is not a quadratic curve either. - if (utils.approximately(b, 0)) { - // in fact in fact, there are no solutions. - return []; - } - // linear solution: - return [-c / b].filter(reduce); - } - // quadratic solution: - var q = sqrt(b * b - 4 * a * c), - a2 = 2 * a; - return [(q - b) / a2, (-b - q) / a2].filter(reduce); - } - - // at this point, we know we need a cubic solution: - - a /= d; - b /= d; - c /= d; - - var p = (3 * b - a * a) / 3, - p3 = p / 3, - q = (2 * a * a * a - 9 * a * b + 27 * c) / 27, - q2 = q / 2, - discriminant = q2 * q2 + p3 * p3 * p3, - u1, - v1, - x1, - x2, - x3; - if (discriminant < 0) { - var mp3 = -p / 3, - mp33 = mp3 * mp3 * mp3, - r = sqrt(mp33), - t = -q / (2 * r), - cosphi = t < -1 ? -1 : t > 1 ? 1 : t, - phi = acos(cosphi), - crtr = crt(r), - t1 = 2 * crtr; - x1 = t1 * cos(phi / 3) - a / 3; - x2 = t1 * cos((phi + tau) / 3) - a / 3; - x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3; - return [x1, x2, x3].filter(reduce); - } else if (discriminant === 0) { - u1 = q2 < 0 ? crt(-q2) : -crt(q2); - x1 = 2 * u1 - a / 3; - x2 = -u1 - a / 3; - return [x1, x2].filter(reduce); - } else { - var sd = sqrt(discriminant); - u1 = crt(-q2 + sd); - v1 = crt(q2 + sd); - return [u1 - v1 - a / 3].filter(reduce); - } - }, - - droots: function(p) { - // quadratic roots are easy - if (p.length === 3) { - var a = p[0], - b = p[1], - c = p[2], - d = a - 2 * b + c; - if (d !== 0) { - var m1 = -sqrt(b * b - a * c), - m2 = -a + b, - v1 = -(m1 + m2) / d, - v2 = -(-m1 + m2) / d; - return [v1, v2]; - } else if (b !== c && d === 0) { - return [(2 * b - c) / (2 * (b - c))]; - } - return []; - } - - // linear roots are even easier - if (p.length === 2) { - var a = p[0], - b = p[1]; - if (a !== b) { - return [a / (a - b)]; - } - return []; - } - }, - - curvature: function(t, points, _3d, kOnly) { - var dpoints = utils.derive(points); - var d1 = dpoints[0]; - var d2 = dpoints[1]; - var num, dnm, adk, dk, k=0, r=0; - - // - // We're using the following formula for curvature: - // - // x'y" - y'x" - // k(t) = ------------------ - // (x'² + y'²)^(3/2) - // - // from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition - // - // With it corresponding 3D counterpart: - // - // sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²) - // k(t) = ------------------------------------------------------- - // (x'² + y'² + z'²)^(3/2) - // - - var d = utils.compute(t, d1); - var dd = utils.compute(t, d2); - var qdsum = d.x*d.x + d.y*d.y; - if (_3d) { - num = sqrt( - pow(d.y*dd.z - dd.y*d.z, 2) + - pow(d.z*dd.x - dd.z*d.x, 2) + - pow(d.x*dd.y - dd.x*d.y, 2) - ); - dnm = pow(qdsum + d.z*d.z, 3/2); - } else { - num = d.x*dd.y - d.y*dd.x; - dnm = pow(qdsum, 3/2); - } - - if (num === 0 || dnm === 0) { - return { k:0, r:0 }; - } - - k = num/dnm; - r = dnm/num; - - // We're also computing the derivative of kappa, because - // there is value in knowing the rate of change for the - // curvature along the curve. And we're just going to - // ballpark it based on an epsilon. - if (!kOnly) { - // compute k'(t) based on the interval before, and after it, - // to at least try to not introduce forward/backward pass bias. - var pk = utils.curvature(t-0.001, points, _3d, true).k; - var nk = utils.curvature(t+0.001, points, _3d, true).k; - dk = ((nk-k) + (k-pk))/2; - adk = (abs(nk-k) + abs(k-pk))/2; - } - - return { k: k, r: r, dk: dk, adk:adk, }; - }, - - inflections: function(points) { - if (points.length < 4) return []; - - // FIXME: TODO: add in inflection abstraction for quartic+ curves? - - var p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }), - a = p[2].x * p[1].y, - b = p[3].x * p[1].y, - c = p[1].x * p[2].y, - d = p[3].x * p[2].y, - v1 = 18 * (-3 * a + 2 * b + 3 * c - d), - v2 = 18 * (3 * a - b - 3 * c), - v3 = 18 * (c - a); - - if (utils.approximately(v1, 0)) { - if (!utils.approximately(v2, 0)) { - var t = -v3 / v2; - if (0 <= t && t <= 1) return [t]; - } - return []; - } - - var trm = v2 * v2 - 4 * v1 * v3, - sq = Math.sqrt(trm), - d = 2 * v1; - - if (utils.approximately(d, 0)) return []; - - return [(sq - v2) / d, -(v2 + sq) / d].filter(function(r) { - return 0 <= r && r <= 1; - }); - }, - - bboxoverlap: function(b1, b2) { - var dims = ["x", "y"], - len = dims.length, - i, - dim, - l, - t, - d; - for (i = 0; i < len; i++) { - dim = dims[i]; - l = b1[dim].mid; - t = b2[dim].mid; - d = (b1[dim].size + b2[dim].size) / 2; - if (abs(l - t) >= d) return false; - } - return true; - }, - - expandbox: function(bbox, _bbox) { - if (_bbox.x.min < bbox.x.min) { - bbox.x.min = _bbox.x.min; - } - if (_bbox.y.min < bbox.y.min) { - bbox.y.min = _bbox.y.min; - } - if (_bbox.z && _bbox.z.min < bbox.z.min) { - bbox.z.min = _bbox.z.min; - } - if (_bbox.x.max > bbox.x.max) { - bbox.x.max = _bbox.x.max; - } - if (_bbox.y.max > bbox.y.max) { - bbox.y.max = _bbox.y.max; - } - if (_bbox.z && _bbox.z.max > bbox.z.max) { - bbox.z.max = _bbox.z.max; - } - bbox.x.mid = (bbox.x.min + bbox.x.max) / 2; - bbox.y.mid = (bbox.y.min + bbox.y.max) / 2; - if (bbox.z) { - bbox.z.mid = (bbox.z.min + bbox.z.max) / 2; - } - bbox.x.size = bbox.x.max - bbox.x.min; - bbox.y.size = bbox.y.max - bbox.y.min; - if (bbox.z) { - bbox.z.size = bbox.z.max - bbox.z.min; - } - }, - - pairiteration: function(c1, c2, curveIntersectionThreshold) { - var c1b = c1.bbox(), - c2b = c2.bbox(), - r = 100000, - threshold = curveIntersectionThreshold || 0.5; - if ( - c1b.x.size + c1b.y.size < threshold && - c2b.x.size + c2b.y.size < threshold - ) { - return [ - ((r * (c1._t1 + c1._t2) / 2) | 0) / r + - "/" + - ((r * (c2._t1 + c2._t2) / 2) | 0) / r - ]; - } - var cc1 = c1.split(0.5), - cc2 = c2.split(0.5), - pairs = [ - { left: cc1.left, right: cc2.left }, - { left: cc1.left, right: cc2.right }, - { left: cc1.right, right: cc2.right }, - { left: cc1.right, right: cc2.left } - ]; - pairs = pairs.filter(function(pair) { - return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox()); - }); - var results = []; - if (pairs.length === 0) return results; - pairs.forEach(function(pair) { - results = results.concat( - utils.pairiteration(pair.left, pair.right, threshold) - ); - }); - results = results.filter(function(v, i) { - return results.indexOf(v) === i; - }); - return results; - }, - - getccenter: function(p1, p2, p3) { - var dx1 = p2.x - p1.x, - dy1 = p2.y - p1.y, - dx2 = p3.x - p2.x, - dy2 = p3.y - p2.y; - var dx1p = dx1 * cos(quart) - dy1 * sin(quart), - dy1p = dx1 * sin(quart) + dy1 * cos(quart), - dx2p = dx2 * cos(quart) - dy2 * sin(quart), - dy2p = dx2 * sin(quart) + dy2 * cos(quart); - // chord midpoints - var mx1 = (p1.x + p2.x) / 2, - my1 = (p1.y + p2.y) / 2, - mx2 = (p2.x + p3.x) / 2, - my2 = (p2.y + p3.y) / 2; - // midpoint offsets - var mx1n = mx1 + dx1p, - my1n = my1 + dy1p, - mx2n = mx2 + dx2p, - my2n = my2 + dy2p; - // intersection of these lines: - var arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n), - r = utils.dist(arc, p1), - // arc start/end values, over mid point: - s = atan2(p1.y - arc.y, p1.x - arc.x), - m = atan2(p2.y - arc.y, p2.x - arc.x), - e = atan2(p3.y - arc.y, p3.x - arc.x), - _; - // determine arc direction (cw/ccw correction) - if (s < e) { - // if s m || m > e) { - s += tau; - } - if (s > e) { - _ = e; - e = s; - s = _; - } - } else { - // if e 1) { - for (var i = 0; i < dCpts.length - 1; i++) { - dCpts[i] = { - x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t, - y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t - }; - if (typeof dCpts[i].z !== "undefined") { - dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t; - } - } - dCpts.splice(dCpts.length - 1, 1); - } - return dCpts[0]; - }, - - computeWithRatios: function (t, points, ratios, _3d) { - var mt = 1 - t, r = ratios, p = points, d; - var f1 = r[0], f2 = r[1], f3 = r[2], f4 = r[3]; - - // spec for linear - f1 *= mt; - f2 *= t; - - if (p.length === 2) { - d = f1 + f2; - return { - x: (f1 * p[0].x + f2 * p[1].x)/d, - y: (f1 * p[0].y + f2 * p[1].y)/d, - z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z)/d - }; - } - - // upgrade to quadratic - f1 *= mt; - f2 *= 2 * mt; - f3 *= t * t; - - if (p.length === 3) { - d = f1 + f2 + f3; - return { - x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x)/d, - y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y)/d, - z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z)/d - }; - } - - // upgrade to cubic - f1 *= mt; - f2 *= 1.5 * mt; - f3 *= 3 * mt; - f4 *= t * t * t; - - if (p.length === 4) { - d = f1 + f2 + f3 + f4; - return { - x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x)/d, - y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y)/d, - z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z)/d - }; - } - }, - - derive: function (points, _3d) { - var dpoints = []; - for (var p = points, d = p.length, c = d - 1; d > 1; d--, c--) { - var list = []; - for (var j = 0, dpt; j < c; j++) { - dpt = { - x: c * (p[j + 1].x - p[j].x), - y: c * (p[j + 1].y - p[j].y) - }; - if (_3d) { - dpt.z = c * (p[j + 1].z - p[j].z); - } - list.push(dpt); - } - dpoints.push(list); - p = list; - } - return dpoints; - }, - - between: function(v, m, M) { - return ( - (m <= v && v <= M) || - utils.approximately(v, m) || - utils.approximately(v, M) - ); - }, - - approximately: function(a, b, precision) { - return abs(a - b) <= (precision || epsilon); - }, - - length: function(derivativeFn) { - var z = 0.5, - sum = 0, - len = utils.Tvalues.length, - i, - t; - for (i = 0; i < len; i++) { - t = z * utils.Tvalues[i] + z; - sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn); - } - return z * sum; - }, - - map: function(v, ds, de, ts, te) { - var d1 = de - ds, - d2 = te - ts, - v2 = v - ds, - r = v2 / d1; - return ts + d2 * r; - }, - - lerp: function(r, v1, v2) { - var ret = { - x: v1.x + r * (v2.x - v1.x), - y: v1.y + r * (v2.y - v1.y) - }; - if (!!v1.z && !!v2.z) { - ret.z = v1.z + r * (v2.z - v1.z); - } - return ret; - }, - - pointToString: function(p) { - var s = p.x + "/" + p.y; - if (typeof p.z !== "undefined") { - s += "/" + p.z; - } - return s; - }, - - pointsToString: function(points) { - return "[" + points.map(utils.pointToString).join(", ") + "]"; - }, - - copy: function(obj) { - return JSON.parse(JSON.stringify(obj)); - }, - - angle: function(o, v1, v2) { - var dx1 = v1.x - o.x, - dy1 = v1.y - o.y, - dx2 = v2.x - o.x, - dy2 = v2.y - o.y, - cross = dx1 * dy2 - dy1 * dx2, - dot = dx1 * dx2 + dy1 * dy2; - return atan2(cross, dot); - }, - - // round as string, to avoid rounding errors - round: function(v, d) { - var s = "" + v; - var pos = s.indexOf("."); - return parseFloat(s.substring(0, pos + 1 + d)); - }, - - dist: function(p1, p2) { - var dx = p1.x - p2.x, - dy = p1.y - p2.y; - return sqrt(dx * dx + dy * dy); - }, - - closest: function(LUT, point) { - var mdist = pow(2, 63), - mpos, - d; - LUT.forEach(function(p, idx) { - d = utils.dist(point, p); - if (d < mdist) { - mdist = d; - mpos = idx; - } - }); - return { mdist: mdist, mpos: mpos }; - }, - - abcratio: function(t, n) { - // see ratio(t) note on http://pomax.github.io/bezierinfo/#abc - if (n !== 2 && n !== 3) { - return false; - } - if (typeof t === "undefined") { - t = 0.5; - } else if (t === 0 || t === 1) { - return t; - } - var bottom = pow(t, n) + pow(1 - t, n), - top = bottom - 1; - return abs(top / bottom); - }, - - projectionratio: function(t, n) { - // see u(t) note on http://pomax.github.io/bezierinfo/#abc - if (n !== 2 && n !== 3) { - return false; - } - if (typeof t === "undefined") { - t = 0.5; - } else if (t === 0 || t === 1) { - return t; - } - var top = pow(1 - t, n), - bottom = pow(t, n) + top; - return top / bottom; - }, - - lli8: function(x1, y1, x2, y2, x3, y3, x4, y4) { - var nx = - (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), - ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), - d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); - if (d == 0) { - return false; - } - return { x: nx / d, y: ny / d }; - }, - - lli4: function(p1, p2, p3, p4) { - var x1 = p1.x, - y1 = p1.y, - x2 = p2.x, - y2 = p2.y, - x3 = p3.x, - y3 = p3.y, - x4 = p4.x, - y4 = p4.y; - return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4); - }, - - lli: function(v1, v2) { - return utils.lli4(v1, v1.c, v2, v2.c); - }, - - makeline: function(p1, p2) { - var Bezier = require("./bezier"); - var x1 = p1.x, - y1 = p1.y, - x2 = p2.x, - y2 = p2.y, - dx = (x2 - x1) / 3, - dy = (y2 - y1) / 3; - return new Bezier( - x1, - y1, - x1 + dx, - y1 + dy, - x1 + 2 * dx, - y1 + 2 * dy, - x2, - y2 - ); - }, - - findbbox: function(sections) { - var mx = nMax, - my = nMax, - MX = nMin, - MY = nMin; - sections.forEach(function(s) { - var bbox = s.bbox(); - if (mx > bbox.x.min) mx = bbox.x.min; - if (my > bbox.y.min) my = bbox.y.min; - if (MX < bbox.x.max) MX = bbox.x.max; - if (MY < bbox.y.max) MY = bbox.y.max; - }); - return { - x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx }, - y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my } - }; - }, - - shapeintersections: function( - s1, - bbox1, - s2, - bbox2, - curveIntersectionThreshold - ) { - if (!utils.bboxoverlap(bbox1, bbox2)) return []; - var intersections = []; - var a1 = [s1.startcap, s1.forward, s1.back, s1.endcap]; - var a2 = [s2.startcap, s2.forward, s2.back, s2.endcap]; - a1.forEach(function(l1) { - if (l1.virtual) return; - a2.forEach(function(l2) { - if (l2.virtual) return; - var iss = l1.intersects(l2, curveIntersectionThreshold); - if (iss.length > 0) { - iss.c1 = l1; - iss.c2 = l2; - iss.s1 = s1; - iss.s2 = s2; - intersections.push(iss); - } - }); - }); - return intersections; - }, - - makeshape: function(forward, back, curveIntersectionThreshold) { - var bpl = back.points.length; - var fpl = forward.points.length; - var start = utils.makeline(back.points[bpl - 1], forward.points[0]); - var end = utils.makeline(forward.points[fpl - 1], back.points[0]); - var shape = { - startcap: start, - forward: forward, - back: back, - endcap: end, - bbox: utils.findbbox([start, forward, back, end]) - }; - var self = utils; - shape.intersections = function(s2) { - return self.shapeintersections( - shape, - shape.bbox, - s2, - s2.bbox, - curveIntersectionThreshold - ); - }; - return shape; - }, - - getminmax: function(curve, d, list) { - if (!list) return { min: 0, max: 0 }; - var min = nMax, - max = nMin, - t, - c; - if (list.indexOf(0) === -1) { - list = [0].concat(list); - } - if (list.indexOf(1) === -1) { - list.push(1); - } - for (var i = 0, len = list.length; i < len; i++) { - t = list[i]; - c = curve.get(t); - if (c[d] < min) { - min = c[d]; - } - if (c[d] > max) { - max = c[d]; - } - } - return { min: min, mid: (min + max) / 2, max: max, size: max - min }; - }, - - align: function(points, line) { - var tx = line.p1.x, - ty = line.p1.y, - a = -atan2(line.p2.y - ty, line.p2.x - tx), - d = function(v) { - return { - x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a), - y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a) - }; - }; - return points.map(d); - }, - - roots: function(points, line) { - line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } }; - var order = points.length - 1; - var p = utils.align(points, line); - var reduce = function(t) { - return 0 <= t && t <= 1; - }; - - if (order === 2) { - var a = p[0].y, - b = p[1].y, - c = p[2].y, - d = a - 2 * b + c; - if (d !== 0) { - var m1 = -sqrt(b * b - a * c), - m2 = -a + b, - v1 = -(m1 + m2) / d, - v2 = -(-m1 + m2) / d; - return [v1, v2].filter(reduce); - } else if (b !== c && d === 0) { - return [(2*b - c)/(2*b - 2*c)].filter(reduce); - } - return []; - } - - // see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm - var pa = p[0].y, - pb = p[1].y, - pc = p[2].y, - pd = p[3].y, - d = -pa + 3 * pb - 3 * pc + pd, - a = 3 * pa - 6 * pb + 3 * pc, - b = -3 * pa + 3 * pb, - c = pa; - - if (utils.approximately(d, 0)) { - // this is not a cubic curve. - if (utils.approximately(a, 0)) { - // in fact, this is not a quadratic curve either. - if (utils.approximately(b, 0)) { - // in fact in fact, there are no solutions. - return []; - } - // linear solution: - return [-c / b].filter(reduce); - } - // quadratic solution: - var q = sqrt(b * b - 4 * a * c), - a2 = 2 * a; - return [(q - b) / a2, (-b - q) / a2].filter(reduce); - } - - // at this point, we know we need a cubic solution: - - a /= d; - b /= d; - c /= d; - - var p = (3 * b - a * a) / 3, - p3 = p / 3, - q = (2 * a * a * a - 9 * a * b + 27 * c) / 27, - q2 = q / 2, - discriminant = q2 * q2 + p3 * p3 * p3, - u1, - v1, - x1, - x2, - x3; - if (discriminant < 0) { - var mp3 = -p / 3, - mp33 = mp3 * mp3 * mp3, - r = sqrt(mp33), - t = -q / (2 * r), - cosphi = t < -1 ? -1 : t > 1 ? 1 : t, - phi = acos(cosphi), - crtr = crt(r), - t1 = 2 * crtr; - x1 = t1 * cos(phi / 3) - a / 3; - x2 = t1 * cos((phi + tau) / 3) - a / 3; - x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3; - return [x1, x2, x3].filter(reduce); - } else if (discriminant === 0) { - u1 = q2 < 0 ? crt(-q2) : -crt(q2); - x1 = 2 * u1 - a / 3; - x2 = -u1 - a / 3; - return [x1, x2].filter(reduce); - } else { - var sd = sqrt(discriminant); - u1 = crt(-q2 + sd); - v1 = crt(q2 + sd); - return [u1 - v1 - a / 3].filter(reduce); - } - }, - - droots: function(p) { - // quadratic roots are easy - if (p.length === 3) { - var a = p[0], - b = p[1], - c = p[2], - d = a - 2 * b + c; - if (d !== 0) { - var m1 = -sqrt(b * b - a * c), - m2 = -a + b, - v1 = -(m1 + m2) / d, - v2 = -(-m1 + m2) / d; - return [v1, v2]; - } else if (b !== c && d === 0) { - return [(2 * b - c) / (2 * (b - c))]; - } - return []; - } - - // linear roots are even easier - if (p.length === 2) { - var a = p[0], - b = p[1]; - if (a !== b) { - return [a / (a - b)]; - } - return []; - } - }, - - curvature: function(t, points, _3d, kOnly) { - var dpoints = utils.derive(points); - var d1 = dpoints[0]; - var d2 = dpoints[1]; - var num, dnm, adk, dk, k=0, r=0; - - // - // We're using the following formula for curvature: - // - // x'y" - y'x" - // k(t) = ------------------ - // (x'² + y'²)^(3/2) - // - // from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition - // - // With it corresponding 3D counterpart: - // - // sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²) - // k(t) = ------------------------------------------------------- - // (x'² + y'² + z'²)^(3/2) - // - - var d = utils.compute(t, d1); - var dd = utils.compute(t, d2); - var qdsum = d.x*d.x + d.y*d.y; - if (_3d) { - num = sqrt( - pow(d.y*dd.z - dd.y*d.z, 2) + - pow(d.z*dd.x - dd.z*d.x, 2) + - pow(d.x*dd.y - dd.x*d.y, 2) - ); - dnm = pow(qdsum + d.z*d.z, 3/2); - } else { - num = d.x*dd.y - d.y*dd.x; - dnm = pow(qdsum, 3/2); - } - - if (num === 0 || dnm === 0) { - return { k:0, r:0 }; - } - - k = num/dnm; - r = dnm/num; - - // We're also computing the derivative of kappa, because - // there is value in knowing the rate of change for the - // curvature along the curve. And we're just going to - // ballpark it based on an epsilon. - if (!kOnly) { - // compute k'(t) based on the interval before, and after it, - // to at least try to not introduce forward/backward pass bias. - var pk = utils.curvature(t-0.001, points, _3d, true).k; - var nk = utils.curvature(t+0.001, points, _3d, true).k; - dk = ((nk-k) + (k-pk))/2; - adk = (abs(nk-k) + abs(k-pk))/2; - } - - return { k: k, r: r, dk: dk, adk:adk, }; - }, - - inflections: function(points) { - if (points.length < 4) return []; - - // FIXME: TODO: add in inflection abstraction for quartic+ curves? - - var p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }), - a = p[2].x * p[1].y, - b = p[3].x * p[1].y, - c = p[1].x * p[2].y, - d = p[3].x * p[2].y, - v1 = 18 * (-3 * a + 2 * b + 3 * c - d), - v2 = 18 * (3 * a - b - 3 * c), - v3 = 18 * (c - a); - - if (utils.approximately(v1, 0)) { - if (!utils.approximately(v2, 0)) { - var t = -v3 / v2; - if (0 <= t && t <= 1) return [t]; - } - return []; - } - - var trm = v2 * v2 - 4 * v1 * v3, - sq = Math.sqrt(trm), - d = 2 * v1; - - if (utils.approximately(d, 0)) return []; - - return [(sq - v2) / d, -(v2 + sq) / d].filter(function(r) { - return 0 <= r && r <= 1; - }); - }, - - bboxoverlap: function(b1, b2) { - var dims = ["x", "y"], - len = dims.length, - i, - dim, - l, - t, - d; - for (i = 0; i < len; i++) { - dim = dims[i]; - l = b1[dim].mid; - t = b2[dim].mid; - d = (b1[dim].size + b2[dim].size) / 2; - if (abs(l - t) >= d) return false; - } - return true; - }, - - expandbox: function(bbox, _bbox) { - if (_bbox.x.min < bbox.x.min) { - bbox.x.min = _bbox.x.min; - } - if (_bbox.y.min < bbox.y.min) { - bbox.y.min = _bbox.y.min; - } - if (_bbox.z && _bbox.z.min < bbox.z.min) { - bbox.z.min = _bbox.z.min; - } - if (_bbox.x.max > bbox.x.max) { - bbox.x.max = _bbox.x.max; - } - if (_bbox.y.max > bbox.y.max) { - bbox.y.max = _bbox.y.max; - } - if (_bbox.z && _bbox.z.max > bbox.z.max) { - bbox.z.max = _bbox.z.max; - } - bbox.x.mid = (bbox.x.min + bbox.x.max) / 2; - bbox.y.mid = (bbox.y.min + bbox.y.max) / 2; - if (bbox.z) { - bbox.z.mid = (bbox.z.min + bbox.z.max) / 2; - } - bbox.x.size = bbox.x.max - bbox.x.min; - bbox.y.size = bbox.y.max - bbox.y.min; - if (bbox.z) { - bbox.z.size = bbox.z.max - bbox.z.min; - } - }, - - pairiteration: function(c1, c2, curveIntersectionThreshold) { - var c1b = c1.bbox(), - c2b = c2.bbox(), - r = 100000, - threshold = curveIntersectionThreshold || 0.5; - if ( - c1b.x.size + c1b.y.size < threshold && - c2b.x.size + c2b.y.size < threshold - ) { - return [ - ((r * (c1._t1 + c1._t2) / 2) | 0) / r + - "/" + - ((r * (c2._t1 + c2._t2) / 2) | 0) / r - ]; - } - var cc1 = c1.split(0.5), - cc2 = c2.split(0.5), - pairs = [ - { left: cc1.left, right: cc2.left }, - { left: cc1.left, right: cc2.right }, - { left: cc1.right, right: cc2.right }, - { left: cc1.right, right: cc2.left } - ]; - pairs = pairs.filter(function(pair) { - return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox()); - }); - var results = []; - if (pairs.length === 0) return results; - pairs.forEach(function(pair) { - results = results.concat( - utils.pairiteration(pair.left, pair.right, threshold) - ); - }); - results = results.filter(function(v, i) { - return results.indexOf(v) === i; - }); - return results; - }, - - getccenter: function(p1, p2, p3) { - var dx1 = p2.x - p1.x, - dy1 = p2.y - p1.y, - dx2 = p3.x - p2.x, - dy2 = p3.y - p2.y; - var dx1p = dx1 * cos(quart) - dy1 * sin(quart), - dy1p = dx1 * sin(quart) + dy1 * cos(quart), - dx2p = dx2 * cos(quart) - dy2 * sin(quart), - dy2p = dx2 * sin(quart) + dy2 * cos(quart); - // chord midpoints - var mx1 = (p1.x + p2.x) / 2, - my1 = (p1.y + p2.y) / 2, - mx2 = (p2.x + p3.x) / 2, - my2 = (p2.y + p3.y) / 2; - // midpoint offsets - var mx1n = mx1 + dx1p, - my1n = my1 + dy1p, - mx2n = mx2 + dx2p, - my2n = my2 + dy2p; - // intersection of these lines: - var arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n), - r = utils.dist(arc, p1), - // arc start/end values, over mid point: - s = atan2(p1.y - arc.y, p1.x - arc.x), - m = atan2(p2.y - arc.y, p2.x - arc.x), - e = atan2(p3.y - arc.y, p3.x - arc.x), - _; - // determine arc direction (cw/ccw correction) - if (s < e) { - // if s m || m > e) { - s += tau; - } - if (s > e) { - _ = e; - e = s; - s = _; - } - } else { - // if e this[`draw${s.type}`](this.ctx, s.points, s.factor) ); @@ -264,51 +268,6 @@ class GraphicsAPI extends BaseAPI { this.end(); } - /** - * Polygon draw function - */ - drawPolygon(ctx, points) { - points.forEach((p) => ctx.lineTo(p.x, p.y)); - } - - /** - * Curve draw function, which draws a CR curve as a series of Beziers - */ - drawCatmullRom(ctx, points, f) { - // invent a virtual first and last point - const f0 = points[0], - f1 = points[1], - fn = f0.reflect(f1), - l1 = points[points.length - 2], - l0 = points[points.length - 1], - ln = l0.reflect(l1), - cpoints = [fn, ...points, ln]; - - // four point sliding window over the segment - for (let i = 0, e = cpoints.length - 3; i < e; i++) { - let [c1, c2, c3, c4] = cpoints.slice(i, i + 4); - let p2 = { - x: c2.x + (c3.x - c1.x) / (6 * f), - y: c2.y + (c3.y - c1.y) / (6 * f), - }; - let p3 = { - x: c3.x - (c4.x - c2.x) / (6 * f), - y: c3.y - (c4.y - c2.y) / (6 * f), - }; - ctx.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, c3.x, c3.y); - } - } - - /** - * Curve draw function, which assumes Bezier coordinates - */ - drawBezier(ctx, points) { - for (let i = 0, e = points.length; i < e; i += 3) { - let [p1, p2, p3] = points.slice(i, i + 3); - ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); - } - } - /** * convenient grid drawing function */ @@ -320,8 +279,6 @@ class GraphicsAPI extends BaseAPI { this.line({ x: 0, y }, { x: this.width, y }); } } - - // TODO: add in transform functions (translate, rotate, scale, skew) } -export { GraphicsAPI, Bezier, Point }; +export { GraphicsAPI, Bezier, Vector }; diff --git a/lib/custom-element/api/types/bezier.js b/lib/custom-element/api/types/bezier.js index d7ed27db..fbae1100 100644 --- a/lib/custom-element/api/types/bezier.js +++ b/lib/custom-element/api/types/bezier.js @@ -1,70 +1,21 @@ -import { Point } from "./point.js"; - -function compute(t, a, b, c, d) { - let mt = 1 - t, - t2 = t * t, - t3 = t2 * t, - mt2 = mt * mt, - mt3 = mt2 * mt; - return a * mt3 + 3 * b * mt2 * t + 3 * c * mt * t2 + d * t3; -} - -function computeDerivative(t, a, b, c, d) { - let mt = 1 - t, - t2 = t * t, - mt2 = mt * mt, - u = 3 * (a - b), - v = 3 * (b - c), - w = 3 * (c - d); - return u * mt2 + 2 * v * mt * t + w * t2; -} +import { Bezier as Original } from "../../lib/bezierjs/bezier.js"; /** * A canvas-aware Bezier curve class */ -class Bezier { +class Bezier extends Original { + static defaultQuadratic(apiInstance) { + return new Bezier(apiInstance, 70,250, 20,110, 220,60); + } + + static defaultCubic(apiInstance) { + return new Bezier(apiInstance, 110,150, 25,190, 210,250, 210,30); + } + constructor(apiInstance, ...coords) { - if (coords.length === 8) { - this.points = [ - new Point(coords[0], coords[1]), - new Point(coords[2], coords[3]), - new Point(coords[4], coords[5]), - new Point(coords[6], coords[7]), - ]; - } + super(...coords); + this.api = apiInstance; this.ctx = apiInstance.ctx; - this.update(); - } - - update() { - this.buildLUT(25); - } - - buildLUT(n) { - this.lut = []; - for (let i = 0; i <= n; i++) { - this.lut[i] = this.get(i / n); - } - } - - get(t) { - let p = this.points; - let ret = new Point( - compute(t, p[0].x, p[1].x, p[2].x, p[3].x), - compute(t, p[0].y, p[1].y, p[2].y, p[3].y) - ); - ret.t = t; - return ret; - } - - getDerivative(t) { - let p = this.points; - let ret = new Point( - computeDerivative(t, p[0].x, p[1].x, p[2].x, p[3].x), - computeDerivative(t, p[0].y, p[1].y, p[2].y, p[3].y) - ); - ret.t = t; - return ret; } getPointNear(point, d = 5) { @@ -127,25 +78,33 @@ class Bezier { ctx.strokeStyle = `#333`; ctx.beginPath(); ctx.moveTo(p[0].x, p[0].y); - ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); + if (p[3]) { + ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); + } else { + ctx.quadraticCurveTo(p[1].x, p[1].y, p[2].x, p[2].y); + } ctx.stroke(); ctx.restoreStyle(); } drawPoints() { + const colors = [`red`, `green`, `blue`, `yellow`]; + const api = this.api; const ctx = this.ctx; + ctx.cacheStyle(); ctx.lineWidth = 2; ctx.strokeStyle = `#999`; - const colors = [`red`, `green`, `blue`, `yellow`]; this.points.forEach((p, i) => { - ctx.fillStyle = colors[i]; - p.draw(ctx); + api.setFill(colors[i % colors.length]); + api.circle(p.x, p.y, 5); + api.setFill(`black`); + api.text(`(${p.x},${p.y})`, p.x + 10, p.y + 10); }); ctx.restoreStyle(); } - drawSkeleton() { + drawSkeleton(t = false) { const ctx = this.ctx; ctx.cacheStyle(); const p = this.points; @@ -154,22 +113,15 @@ class Bezier { ctx.moveTo(p[0].x, p[0].y); ctx.lineTo(p[1].x, p[1].y); ctx.lineTo(p[2].x, p[2].y); - ctx.lineTo(p[3].x, p[3].y); - ctx.stroke(); - ctx.restoreStyle(); - } - drawNormals() { - const ctx = this.ctx; - ctx.cacheStyle(); - this.lut.forEach((p) => { - let tp = this.getDerivative(p.t).normalize(20); - ctx.beginPath(); - ctx.moveTo(p.x, p.y); - ctx.lineTo(p.x - tp.y, p.y + tp.x); - ctx.strokeStyle = `#CC00FFCC`; - ctx.stroke(); - }); + if (p[3]) { + ctx.lineTo(p[3].x, p[3].y); + if (t !== false) { + // TODO: additional cubic struts + // ... code goes here ... + } + } + ctx.stroke(); ctx.restoreStyle(); } } diff --git a/lib/custom-element/api/types/bezier/base.js b/lib/custom-element/api/types/bezier/base.js deleted file mode 100644 index a7c4f188..00000000 --- a/lib/custom-element/api/types/bezier/base.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Point } from "../point.js"; -import { Quadratic } from "./bezier-quadratic.js"; -import { Cubic } from "./bezier-cubic.js"; - -class Bezier { - static create(apiInstance, ...points) { - let coords = []; - if (points.length === 9 || points.length === 12) { - for(let i=0, e=points.length; i { - d = p.dist(x, y); - if (d < smallestDistance) { - smallestDistance = d; - p.t = i / n; - closest = p; - } - }); - - // fine check - for (let o = -0.1, t, np, st = closest.t; o <= 0.1; o += 0.005) { - t = st + o; - if (t < 0) continue; - if (t > 1) continue; - np = this.get(t); - d = np.dist(x, y); - if (d < smallestDistance) { - smallestDistance = d; - closest = np; - closest.t = t; - } - } - - return closest; - } - - drawCurve() { - const ctx = this.ctx; - const p = this.points; - ctx.cacheStyle(); - ctx.lineWidth = 2; - ctx.strokeStyle = `#333`; - ctx.beginPath(); - ctx.moveTo(p[0].x, p[0].y); - if (!p[3]) { - ctx.quadraticCurveTo(p[1].x, p[1].y, p[2].x, p[2].y); - } else { - ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); - } - ctx.stroke(); - ctx.restoreStyle(); - } - - drawPoints() { - const ctx = this.ctx; - ctx.cacheStyle(); - ctx.lineWidth = 2; - ctx.strokeStyle = `#999`; - const colors = [`red`, `green`, `blue`, `yellow`]; - this.points.forEach((p, i) => { - ctx.fillStyle = colors[i]; - p.draw(ctx); - }); - ctx.restoreStyle(); - } - - drawSkeleton() { - const ctx = this.ctx; - ctx.cacheStyle(); - const p = this.points; - ctx.strokeStyle = `#555`; - ctx.beginPath(); - ctx.moveTo(p[0].x, p[0].y); - ctx.lineTo(p[1].x, p[1].y); - ctx.lineTo(p[2].x, p[2].y); - if (p[3]) { - ctx.lineTo(p[3].x, p[3].y); - } - ctx.stroke(); - ctx.restoreStyle(); - } - - drawNormals() { - const ctx = this.ctx; - ctx.cacheStyle(); - this.lut.forEach((p) => { - let tp = this.getDerivative(p.t).normalize(20); - ctx.beginPath(); - ctx.moveTo(p.x, p.y); - ctx.lineTo(p.x - tp.y, p.y + tp.x); - ctx.strokeStyle = `#CC00FFCC`; - ctx.stroke(); - }); - ctx.restoreStyle(); - } -} - -export { Bezier, Point } diff --git a/lib/custom-element/api/types/point.js b/lib/custom-element/api/types/point.js deleted file mode 100644 index 605b4ec4..00000000 --- a/lib/custom-element/api/types/point.js +++ /dev/null @@ -1,85 +0,0 @@ -class Point { - constructor(x, y, z) { - this.x = x; - this.y = y; - if (z !== undefined) { - this.z = z; - } - } - draw(ctx) { - ctx.cacheStyle(); - ctx.beginPath(); - ctx.arc(this.x, this.y, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - ctx.fillStyle = `black`; - ctx.fillText(`(${this.x},${this.y})`, this.x + 10.5, this.y + 10.5); - ctx.restoreStyle(); - } - dist(other, y, z = 0) { - if (y !== undefined) other = { x: other, y, z }; - let sum = 0; - sum += (this.x - other.x) ** 2; - sum += (this.y - other.y) ** 2; - let z1 = this.z !== undefined ? this.z : 0; - let z2 = other.z !== undefined ? other.z : 0; - sum += (z1 - z2) ** 2; - return sum ** 0.5; - } - normalize(f) { - let mag = this.dist(0, 0, 0); - return new Point( - (f * this.x) / mag, - (f * this.y) / mag, - (f * this.z) / mag - ); - } - getAngle() { - return -Math.atan2(this.y, this.x); - } - reflect(other) { - let p = new Point( - other.x - this.x, - other.y - this.y - ); - if (other.z !== undefined) { - p.z = other.z - if (this.z !== undefined) { - p.z -= this.z; - } - } - return this.subtract(p); - } - add(other) { - let p = new Point(this.x + other.x, this.y + other.y); - if (this.z !== undefined) { - p.z = this.z; - if (other.z !== undefined) { - p.z += other.z; - } - } - return p; - } - subtract(other) { - let p = new Point(this.x - other.x, this.y - other.y); - if (this.z !== undefined) { - p.z = this.z; - if (other.z !== undefined) { - p.z -= other.z; - } - } - return p; - } - scale(f = 1) { - if (f === 0) { - return new Point(0, 0, this.z === undefined ? undefined : 0); - } - let p = new Point(this.x * f, this.y * f); - if (this.z !== undefined) { - p.z = this.z * f; - } - return p; - } -} - -export { Point }; diff --git a/lib/custom-element/api/types/vector.js b/lib/custom-element/api/types/vector.js new file mode 100644 index 00000000..591dbfcd --- /dev/null +++ b/lib/custom-element/api/types/vector.js @@ -0,0 +1,80 @@ +class Vector { + constructor(x, y, z) { + if (arguments.length === 1) { + z = x.z; + y = x.y; + x = x.x; + } + this.x = x; + this.y = y; + if (z !== undefined) { + this.z = z; + } + } + dist(other, y, z = 0) { + if (y !== undefined) other = { x: other, y, z }; + let sum = 0; + sum += (this.x - other.x) ** 2; + sum += (this.y - other.y) ** 2; + let z1 = this.z ?? 0; + let z2 = other.z ?? 0; + sum += (z1 - z2) ** 2; + return sum ** 0.5; + } + normalize(f) { + let mag = this.dist(0, 0, 0); + return new Vector( + (f * this.x) / mag, + (f * this.y) / mag, + (f * this.z) / mag + ); + } + getAngle() { + return -Math.atan2(this.y, this.x); + } + reflect(other) { + let p = new Vector( + other.x - this.x, + other.y - this.y + ); + if (other.z !== undefined) { + p.z = other.z + if (this.z !== undefined) { + p.z -= this.z; + } + } + return this.subtract(p); + } + add(other) { + let p = new Vector(this.x + other.x, this.y + other.y); + if (this.z !== undefined) { + p.z = this.z; + if (other.z !== undefined) { + p.z += other.z; + } + } + return p; + } + subtract(other) { + let p = new Vector(this.x - other.x, this.y - other.y); + if (this.z !== undefined) { + p.z = this.z; + if (other.z !== undefined) { + p.z -= other.z; + } + } + return p; + } + scale(f = 1) { + if (f === 0) { + return new Vector(0, 0, this.z === undefined ? undefined : 0); + } + let p = new Vector(this.x * f, this.y * f); + if (this.z !== undefined) { + p.z = this.z * f; + } + return p; + } + } + + export { Vector }; diff --git a/lib/custom-element/graphics-element.js b/lib/custom-element/graphics-element.js index e70cb464..4dbe1f5e 100644 --- a/lib/custom-element/graphics-element.js +++ b/lib/custom-element/graphics-element.js @@ -147,7 +147,7 @@ class GraphicsElement extends CustomElement { const height = this.getAttribute(`height`, 200); this.code = ` - import { GraphicsAPI, Bezier, Point } from "${MODULE_PATH}/api/graphics-api.js"; + import { GraphicsAPI, Bezier, Vector } from "${MODULE_PATH}/api/graphics-api.js"; ${globalCode} diff --git a/lib/custom-element/lib/bezierjs/bezier.js b/lib/custom-element/lib/bezierjs/bezier.js new file mode 100644 index 00000000..2adcbacd --- /dev/null +++ b/lib/custom-element/lib/bezierjs/bezier.js @@ -0,0 +1,963 @@ +/** + A javascript Bezier curve library by Pomax. + + Based on http://pomax.github.io/bezierinfo + + This code is MIT licensed. +**/ + +import { utils } from "./utils.js"; +import { PolyBezier } from "./poly-bezier.js"; +import { convertPath } from "./svg-to-beziers.js"; + +// math-inlining. +const { abs, min, max, cos, sin, acos, sqrt } = Math; +const pi = Math.PI; +// a zero coordinate, which is surprisingly useful +const ZERO = { x: 0, y: 0, z: 0 }; + +// TODO: figure out where this function goes, it has no reason to exist on its lonesome. +function getABC(n, S, B, E, t) { + if (typeof t === "undefined") { + t = 0.5; + } + const u = utils.projectionratio(t, n), + um = 1 - u, + C = { + x: u * S.x + um * E.x, + y: u * S.y + um * E.y, + }, + s = utils.abcratio(t, n), + A = { + x: B.x + (B.x - C.x) / s, + y: B.y + (B.y - C.y) / s, + }; + return { A: A, B: B, C: C }; +} + +/** + * Bezier curve constructor. + * + * ...docs pending... + */ +class Bezier { + constructor(coords) { + let args = + coords && coords.forEach ? coords : Array.from(arguments).slice(); + let coordlen = false; + + if (typeof args[0] === "object") { + coordlen = args.length; + const newargs = []; + args.forEach(function (point) { + ["x", "y", "z"].forEach(function (d) { + if (typeof point[d] !== "undefined") { + newargs.push(point[d]); + } + }); + }); + args = newargs; + } + + let higher = false; + const len = args.length; + + if (coordlen) { + if (coordlen > 4) { + if (arguments.length !== 1) { + throw new Error( + "Only new Bezier(point[]) is accepted for 4th and higher order curves" + ); + } + higher = true; + } + } else { + if (len !== 6 && len !== 8 && len !== 9 && len !== 12) { + if (arguments.length !== 1) { + throw new Error( + "Only new Bezier(point[]) is accepted for 4th and higher order curves" + ); + } + } + } + + const _3d = (this._3d = + (!higher && (len === 9 || len === 12)) || + (coords && coords[0] && typeof coords[0].z !== "undefined")); + + const points = (this.points = []); + for (let idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) { + var point = { + x: args[idx], + y: args[idx + 1], + }; + if (_3d) { + point.z = args[idx + 2]; + } + points.push(point); + } + const order = (this.order = points.length - 1); + + const dims = (this.dims = ["x", "y"]); + if (_3d) dims.push("z"); + this.dimlen = dims.length; + + const aligned = utils.align(points, { p1: points[0], p2: points[order] }); + this._linear = !aligned.some((p) => abs(p.y) > 0.0001); + + this._lut = []; + + this._t1 = 0; + this._t2 = 1; + this.update(); + } + + static SVGtoBeziers = function (d) { + return convertPath(Bezier, d); + }; + + static quadraticFromPoints(p1, p2, p3, t) { + if (typeof t === "undefined") { + t = 0.5; + } + // shortcuts, although they're really dumb + if (t === 0) { + return new Bezier(p2, p2, p3); + } + if (t === 1) { + return new Bezier(p1, p2, p2); + } + // real fitting. + const abc = getABC(2, p1, p2, p3, t); + return new Bezier(p1, abc.A, p3); + } + + static cubicFromPoints(S, B, E, t, d1) { + if (typeof t === "undefined") { + t = 0.5; + } + const abc = getABC(3, S, B, E, t); + if (typeof d1 === "undefined") { + d1 = utils.dist(B, abc.C); + } + const d2 = (d1 * (1 - t)) / t; + + const selen = utils.dist(S, E), + lx = (E.x - S.x) / selen, + ly = (E.y - S.y) / selen, + bx1 = d1 * lx, + by1 = d1 * ly, + bx2 = d2 * lx, + by2 = d2 * ly; + // derivation of new hull coordinates + const e1 = { x: B.x - bx1, y: B.y - by1 }, + e2 = { x: B.x + bx2, y: B.y + by2 }, + A = abc.A, + v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) }, + v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t }, + nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t }, + nc2 = { + x: E.x + (v2.x - E.x) / (1 - t), + y: E.y + (v2.y - E.y) / (1 - t), + }; + // ...done + return new Bezier(S, nc1, nc2, E); + } + + static getUtils() { + return utils; + } + + getUtils() { + return Bezier.getUtils(); + } + + static get PolyBezier() { + return PolyBezier; + } + + valueOf() { + return this.toString(); + } + + toString() { + return utils.pointsToString(this.points); + } + + toSVG() { + if (this._3d) return false; + const p = this.points, + x = p[0].x, + y = p[0].y, + s = ["M", x, y, this.order === 2 ? "Q" : "C"]; + for (let i = 1, last = p.length; i < last; i++) { + s.push(p[i].x); + s.push(p[i].y); + } + return s.join(" "); + } + + setRatios(ratios) { + if (ratios.length !== this.points.length) { + throw new Error("incorrect number of ratio values"); + } + this.ratios = ratios; + this._lut = []; // invalidate any precomputed LUT + } + + verify() { + const print = this.coordDigest(); + if (print !== this._print) { + this._print = print; + this.update(); + } + } + + coordDigest() { + return this.points + .map(function (c, pos) { + return "" + pos + c.x + c.y + (c.z ? c.z : 0); + }) + .join(""); + } + + update() { + // invalidate any precomputed LUT + this._lut = []; + this.dpoints = utils.derive(this.points, this._3d); + this.computedirection(); + } + + computedirection() { + const points = this.points; + const angle = utils.angle(points[0], points[this.order], points[1]); + this.clockwise = angle > 0; + } + + length() { + return utils.length(this.derivative.bind(this)); + } + + getLUT(steps) { + this.verify(); + steps = steps || 100; + if (this._lut.length === steps) { + return this._lut; + } + this._lut = []; + // We want a range from 0 to 1 inclusive, so + // we decrement and then use <= rather than <: + steps--; + for (let t = 0; t <= steps; t++) { + this._lut.push(this.compute(t / steps)); + } + return this._lut; + } + + on(point, error) { + error = error || 5; + const lut = this.getLUT(), + hits = []; + for (let i = 0, c, t = 0; i < lut.length; i++) { + c = lut[i]; + if (utils.dist(c, point) < error) { + hits.push(c); + t += i / lut.length; + } + } + if (!hits.length) return false; + return (t /= hits.length); + } + + project(point) { + // step 1: coarse check + const LUT = this.getLUT(), + l = LUT.length - 1, + closest = utils.closest(LUT, point), + mpos = closest.mpos, + t1 = (mpos - 1) / l, + t2 = (mpos + 1) / l, + step = 0.1 / l; + + // step 2: fine check + let mdist = closest.mdist, + t = t1, + ft = t, + p; + mdist += 1; + for (let d; t < t2 + step; t += step) { + p = this.compute(t); + d = utils.dist(point, p); + if (d < mdist) { + mdist = d; + ft = t; + } + } + p = this.compute(ft); + p.t = ft; + p.d = mdist; + return p; + } + + get(t) { + return this.compute(t); + } + + point(idx) { + return this.points[idx]; + } + + compute(t) { + if (this.ratios) { + return utils.computeWithRatios(t, this.points, this.ratios, this._3d); + } + return utils.compute(t, this.points, this._3d, this.ratios); + } + + raise() { + const p = this.points, + np = [p[0]], + k = p.length; + for (let i = 1, pi, pim; i < k; i++) { + pi = p[i]; + pim = p[i - 1]; + np[i] = { + x: ((k - i) / k) * pi.x + (i / k) * pim.x, + y: ((k - i) / k) * pi.y + (i / k) * pim.y, + }; + } + np[k] = p[k - 1]; + return new Bezier(np); + } + + derivative(t) { + const mt = 1 - t; + let a, + b, + c = 0, + p = this.dpoints[0]; + if (this.order === 2) { + p = [p[0], p[1], ZERO]; + a = mt; + b = t; + } + if (this.order === 3) { + a = mt * mt; + b = mt * t * 2; + c = t * t; + } + const ret = { + x: a * p[0].x + b * p[1].x + c * p[2].x, + y: a * p[0].y + b * p[1].y + c * p[2].y, + }; + if (this._3d) { + ret.z = a * p[0].z + b * p[1].z + c * p[2].z; + } + return ret; + } + + curvature(t) { + return utils.curvature(t, this.points, this._3d); + } + + inflections() { + return utils.inflections(this.points); + } + + normal(t) { + return this._3d ? this.__normal3(t) : this.__normal2(t); + } + + __normal2(t) { + const d = this.derivative(t); + const q = sqrt(d.x * d.x + d.y * d.y); + return { x: -d.y / q, y: d.x / q }; + } + + __normal3(t) { + // see http://stackoverflow.com/questions/25453159 + const r1 = this.derivative(t), + r2 = this.derivative(t + 0.01), + q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z), + q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z); + r1.x /= q1; + r1.y /= q1; + r1.z /= q1; + r2.x /= q2; + r2.y /= q2; + r2.z /= q2; + // cross product + const c = { + x: r2.y * r1.z - r2.z * r1.y, + y: r2.z * r1.x - r2.x * r1.z, + z: r2.x * r1.y - r2.y * r1.x, + }; + const m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z); + c.x /= m; + c.y /= m; + c.z /= m; + // rotation matrix + const R = [ + c.x * c.x, + c.x * c.y - c.z, + c.x * c.z + c.y, + c.x * c.y + c.z, + c.y * c.y, + c.y * c.z - c.x, + c.x * c.z - c.y, + c.y * c.z + c.x, + c.z * c.z, + ]; + // normal vector: + const n = { + x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z, + y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z, + z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z, + }; + return n; + } + + hull(t) { + let p = this.points, + _p = [], + q = [], + idx = 0; + q[idx++] = p[0]; + q[idx++] = p[1]; + q[idx++] = p[2]; + if (this.order === 3) { + q[idx++] = p[3]; + } + // we lerp between all points at each iteration, until we have 1 point left. + while (p.length > 1) { + _p = []; + for (let i = 0, pt, l = p.length - 1; i < l; i++) { + pt = utils.lerp(t, p[i], p[i + 1]); + q[idx++] = pt; + _p.push(pt); + } + p = _p; + } + return q; + } + + split(t1, t2) { + // shortcuts + if (t1 === 0 && !!t2) { + return this.split(t2).left; + } + if (t2 === 1) { + return this.split(t1).right; + } + + // no shortcut: use "de Casteljau" iteration. + const q = this.hull(t1); + const result = { + left: + this.order === 2 + ? new Bezier([q[0], q[3], q[5]]) + : new Bezier([q[0], q[4], q[7], q[9]]), + right: + this.order === 2 + ? new Bezier([q[5], q[4], q[2]]) + : new Bezier([q[9], q[8], q[6], q[3]]), + span: q, + }; + + // make sure we bind _t1/_t2 information! + result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2); + result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2); + result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2); + result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2); + + // if we have no t2, we're done + if (!t2) { + return result; + } + + // if we have a t2, split again: + t2 = utils.map(t2, t1, 1, 0, 1); + return result.right.split(t2).left; + } + + extrema() { + const result = {}; + let roots = []; + + this.dims.forEach( + function (dim) { + let mfn = function (v) { + return v[dim]; + }; + let p = this.dpoints[0].map(mfn); + result[dim] = utils.droots(p); + if (this.order === 3) { + p = this.dpoints[1].map(mfn); + result[dim] = result[dim].concat(utils.droots(p)); + } + result[dim] = result[dim].filter(function (t) { + return t >= 0 && t <= 1; + }); + roots = roots.concat(result[dim].sort(utils.numberSort)); + }.bind(this) + ); + + result.values = roots.sort(utils.numberSort).filter(function (v, idx) { + return roots.indexOf(v) === idx; + }); + + return result; + } + + bbox() { + const extrema = this.extrema(), + result = {}; + this.dims.forEach( + function (d) { + result[d] = utils.getminmax(this, d, extrema[d]); + }.bind(this) + ); + return result; + } + + overlaps(curve) { + const lbbox = this.bbox(), + tbbox = curve.bbox(); + return utils.bboxoverlap(lbbox, tbbox); + } + + offset(t, d) { + if (typeof d !== "undefined") { + const c = this.get(t), + n = this.normal(t); + const ret = { + c: c, + n: n, + x: c.x + n.x * d, + y: c.y + n.y * d, + }; + if (this._3d) { + ret.z = c.z + n.z * d; + } + return ret; + } + if (this._linear) { + const nv = this.normal(0), + coords = this.points.map(function (p) { + const ret = { + x: p.x + t * nv.x, + y: p.y + t * nv.y, + }; + if (p.z && nv.z) { + ret.z = p.z + t * nv.z; + } + return ret; + }); + return [new Bezier(coords)]; + } + return this.reduce().map(function (s) { + if (s._linear) { + return s.offset(t)[0]; + } + return s.scale(t); + }); + } + + simple() { + if (this.order === 3) { + const a1 = utils.angle(this.points[0], this.points[3], this.points[1]); + const a2 = utils.angle(this.points[0], this.points[3], this.points[2]); + if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false; + } + const n1 = this.normal(0); + const n2 = this.normal(1); + let s = n1.x * n2.x + n1.y * n2.y; + if (this._3d) { + s += n1.z * n2.z; + } + return abs(acos(s)) < pi / 3; + } + + reduce() { + // TODO: examine these var types in more detail... + let i, + t1 = 0, + t2 = 0, + step = 0.01, + segment, + pass1 = [], + pass2 = []; + // first pass: split on extrema + let extrema = this.extrema().values; + if (extrema.indexOf(0) === -1) { + extrema = [0].concat(extrema); + } + if (extrema.indexOf(1) === -1) { + extrema.push(1); + } + + for (t1 = extrema[0], i = 1; i < extrema.length; i++) { + t2 = extrema[i]; + segment = this.split(t1, t2); + segment._t1 = t1; + segment._t2 = t2; + pass1.push(segment); + t1 = t2; + } + + // second pass: further reduce these segments to simple segments + pass1.forEach(function (p1) { + t1 = 0; + t2 = 0; + while (t2 <= 1) { + for (t2 = t1 + step; t2 <= 1 + step; t2 += step) { + segment = p1.split(t1, t2); + if (!segment.simple()) { + t2 -= step; + if (abs(t1 - t2) < step) { + // we can never form a reduction + return []; + } + segment = p1.split(t1, t2); + segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); + segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2); + pass2.push(segment); + t1 = t2; + break; + } + } + } + if (t1 < 1) { + segment = p1.split(t1, 1); + segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); + segment._t2 = p1._t2; + pass2.push(segment); + } + }); + return pass2; + } + + scale(d) { + const order = this.order; + let distanceFn = false; + if (typeof d === "function") { + distanceFn = d; + } + if (distanceFn && order === 2) { + return this.raise().scale(distanceFn); + } + + // TODO: add special handling for degenerate (=linear) curves. + const clockwise = this.clockwise; + const r1 = distanceFn ? distanceFn(0) : d; + const r2 = distanceFn ? distanceFn(1) : d; + const v = [this.offset(0, 10), this.offset(1, 10)]; + const points = this.points; + const np = []; + const o = utils.lli4(v[0], v[0].c, v[1], v[1].c); + + if (!o) { + throw new Error("cannot scale this curve. Try reducing it first."); + } + // move all points by distance 'd' wrt the origin 'o' + + // move end points by fixed distance along normal. + [0, 1].forEach(function (t) { + const p = (np[t * order] = utils.copy(points[t * order])); + p.x += (t ? r2 : r1) * v[t].n.x; + p.y += (t ? r2 : r1) * v[t].n.y; + }); + + if (!distanceFn) { + // move control points to lie on the intersection of the offset + // derivative vector, and the origin-through-control vector + [0, 1].forEach((t) => { + if (order === 2 && !!t) return; + const p = np[t * order]; + const d = this.derivative(t); + const p2 = { x: p.x + d.x, y: p.y + d.y }; + np[t + 1] = utils.lli4(p, p2, o, points[t + 1]); + }); + return new Bezier(np); + } + + // move control points by "however much necessary to + // ensure the correct tangent to endpoint". + [0, 1].forEach(function (t) { + if (order === 2 && !!t) return; + var p = points[t + 1]; + var ov = { + x: p.x - o.x, + y: p.y - o.y, + }; + var rc = distanceFn ? distanceFn((t + 1) / order) : d; + if (distanceFn && !clockwise) rc = -rc; + var m = sqrt(ov.x * ov.x + ov.y * ov.y); + ov.x /= m; + ov.y /= m; + np[t + 1] = { + x: p.x + rc * ov.x, + y: p.y + rc * ov.y, + }; + }); + return new Bezier(np); + } + + outline(d1, d2, d3, d4) { + d2 = typeof d2 === "undefined" ? d1 : d2; + const reduced = this.reduce(), + len = reduced.length, + fcurves = [], + bcurves = []; + let p, + alen = 0, + tlen = this.length(); + + const graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined"; + + function linearDistanceFunction(s, e, tlen, alen, slen) { + return function (v) { + const f1 = alen / tlen, + f2 = (alen + slen) / tlen, + d = e - s; + return utils.map(v, 0, 1, s + f1 * d, s + f2 * d); + }; + } + + // form curve oulines + reduced.forEach(function (segment) { + slen = segment.length(); + if (graduated) { + fcurves.push( + segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen)) + ); + bcurves.push( + segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen)) + ); + } else { + fcurves.push(segment.scale(d1)); + bcurves.push(segment.scale(-d2)); + } + alen += slen; + }); + + // reverse the "return" outline + bcurves = bcurves + .map(function (s) { + p = s.points; + if (p[3]) { + s.points = [p[3], p[2], p[1], p[0]]; + } else { + s.points = [p[2], p[1], p[0]]; + } + return s; + }) + .reverse(); + + // form the endcaps as lines + const fs = fcurves[0].points[0], + fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1], + bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1], + be = bcurves[0].points[0], + ls = utils.makeline(bs, fs), + le = utils.makeline(fe, be), + segments = [ls].concat(fcurves).concat([le]).concat(bcurves), + slen = segments.length; + + return new PolyBezier(segments); + } + + outlineshapes(d1, d2, curveIntersectionThreshold) { + d2 = d2 || d1; + const outline = this.outline(d1, d2).curves; + const shapes = []; + for (let i = 1, len = outline.length; i < len / 2; i++) { + const shape = utils.makeshape( + outline[i], + outline[len - i], + curveIntersectionThreshold + ); + shape.startcap.virtual = i > 1; + shape.endcap.virtual = i < len / 2 - 1; + shapes.push(shape); + } + return shapes; + } + + intersects(curve, curveIntersectionThreshold) { + if (!curve) return this.selfintersects(curveIntersectionThreshold); + if (curve.p1 && curve.p2) { + return this.lineIntersects(curve); + } + if (curve instanceof Bezier) { + curve = curve.reduce(); + } + return this.curveintersects( + this.reduce(), + curve, + curveIntersectionThreshold + ); + } + + lineIntersects(line) { + const mx = min(line.p1.x, line.p2.x), + my = min(line.p1.y, line.p2.y), + MX = max(line.p1.x, line.p2.x), + MY = max(line.p1.y, line.p2.y); + return utils.roots(this.points, line).filter((t) => { + var p = this.get(t); + return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY); + }); + } + + selfintersects(curveIntersectionThreshold) { + // "simple" curves cannot intersect with their direct + // neighbour, so for each segment X we check whether + // it intersects [0:x-2][x+2:last]. + + const reduced = this.reduce(), + len = reduced.length - 2, + results = []; + + for (let i = 0, result, left, right; i < len; i++) { + left = reduced.slice(i, i + 1); + right = reduced.slice(i + 2); + result = this.curveintersects(left, right, curveIntersectionThreshold); + results = results.concat(result); + } + return results; + } + + curveintersects(c1, c2, curveIntersectionThreshold) { + const pairs = []; + // step 1: pair off any overlapping segments + c1.forEach(function (l) { + c2.forEach(function (r) { + if (l.overlaps(r)) { + pairs.push({ left: l, right: r }); + } + }); + }); + // step 2: for each pairing, run through the convergence algorithm. + let intersections = []; + pairs.forEach(function (pair) { + const result = utils.pairiteration( + pair.left, + pair.right, + curveIntersectionThreshold + ); + if (result.length > 0) { + intersections = intersections.concat(result); + } + }); + return intersections; + } + + arcs(errorThreshold) { + errorThreshold = errorThreshold || 0.5; + return this._iterate(errorThreshold, []); + } + + _error(pc, np1, s, e) { + const q = (e - s) / 4, + c1 = this.get(s + q), + c2 = this.get(e - q), + ref = utils.dist(pc, np1), + d1 = utils.dist(pc, c1), + d2 = utils.dist(pc, c2); + return abs(d1 - ref) + abs(d2 - ref); + } + + _iterate(errorThreshold, circles) { + let t_s = 0, + t_e = 1, + safety; + // we do a binary search to find the "good `t` closest to no-longer-good" + do { + safety = 0; + + // step 1: start with the maximum possible arc + t_e = 1; + + // points: + let np1 = this.get(t_s), + np2, + np3, + arc, + prev_arc; + + // booleans: + let curr_good = false, + prev_good = false, + done; + + // numbers: + let t_m = t_e, + prev_e = 1, + step = 0; + + // step 2: find the best possible arc + do { + prev_good = curr_good; + prev_arc = arc; + t_m = (t_s + t_e) / 2; + step++; + + np2 = this.get(t_m); + np3 = this.get(t_e); + + arc = utils.getccenter(np1, np2, np3); + + //also save the t values + arc.interval = { + start: t_s, + end: t_e, + }; + + let error = this._error(arc, np1, t_s, t_e); + curr_good = error <= errorThreshold; + + done = prev_good && !curr_good; + if (!done) prev_e = t_e; + + // this arc is fine: we can move 'e' up to see if we can find a wider arc + if (curr_good) { + // if e is already at max, then we're done for this arc. + if (t_e >= 1) { + // make sure we cap at t=1 + arc.interval.end = prev_e = 1; + prev_arc = arc; + // if we capped the arc segment to t=1 we also need to make sure that + // the arc's end angle is correct with respect to the bezier end point. + if (t_e > 1) { + let d = { + x: arc.x + arc.r * cos(arc.e), + y: arc.y + arc.r * sin(arc.e), + }; + arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1)); + } + break; + } + // if not, move it up by half the iteration distance + t_e = t_e + (t_e - t_s) / 2; + } else { + // this is a bad arc: we need to move 'e' down to find a good arc + t_e = t_m; + } + } while (!done && safety++ < 100); + + if (safety >= 100) { + break; + } + + // console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e); + + prev_arc = prev_arc ? prev_arc : arc; + circles.push(prev_arc); + t_s = prev_e; + } while (t_e < 1); + return circles; + } +} + +export { Bezier }; diff --git a/lib/bezierjs/normalise-svg.js b/lib/custom-element/lib/bezierjs/normalise-svg.js similarity index 67% rename from lib/bezierjs/normalise-svg.js rename to lib/custom-element/lib/bezierjs/normalise-svg.js index b5fd3230..9a9f793a 100644 --- a/lib/bezierjs/normalise-svg.js +++ b/lib/custom-element/lib/bezierjs/normalise-svg.js @@ -3,7 +3,7 @@ * and full commands, rather than relative coordinates * and/or shortcut commands. */ -function normalizePath(d) { +export default function normalizePath(d) { // preprocess "d" so that we have spaces between values d = d .replace(/,/g, " ") // replace commas with spaces @@ -12,9 +12,10 @@ function normalizePath(d) { .replace(/([a-zA-Z])/g, " $1 "); // set up the variables used in this function - var instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"), - instructionLength = instructions.length, - i, + const instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"), + instructionLength = instructions.length; + + let i, instruction, op, lop, @@ -29,6 +30,11 @@ function normalizePath(d) { cy = 0, cx2 = 0, cy2 = 0, + rx = 0, + ry = 0, + xrot = 0, + lflag = 0, + sweep = 0, normalized = ""; // we run through the instruction list starting at 1, not 0, @@ -42,12 +48,9 @@ function normalizePath(d) { // what are the arguments? note that we need to convert // all strings into numbers, or + will do silly things. - args = instruction - .replace(op, "") - .trim() - .split(" "); + args = instruction.replace(op, "").trim().split(" "); args = args - .filter(function(v) { + .filter(function (v) { return v !== ""; }) .map(parseFloat); @@ -81,11 +84,13 @@ function normalizePath(d) { x = args[a]; y = args[a + 1]; } - normalized += ["L",x,y,''].join(" "); + normalized += "L " + x + " " + y + " "; } } - } else if (lop === "l") { - // lineto commands + } + + // lineto commands + else if (lop === "l") { for (a = 0; a < alen; a += 2) { if (op === "l") { x += args[a]; @@ -94,7 +99,7 @@ function normalizePath(d) { x = args[a]; y = args[a + 1]; } - normalized += ["L",x,y,''].join(" "); + normalized += "L " + x + " " + y + " "; } } else if (lop === "h") { for (a = 0; a < alen; a++) { @@ -103,7 +108,7 @@ function normalizePath(d) { } else { x = args[a]; } - normalized += ["L",x,y,''].join(" "); + normalized += "L " + x + " " + y + " "; } } else if (lop === "v") { for (a = 0; a < alen; a++) { @@ -112,10 +117,12 @@ function normalizePath(d) { } else { y = args[a]; } - normalized += ["L",x,y,''].join(" "); + normalized += "L " + x + " " + y + " "; } - } else if (lop === "q") { - // quadratic curveto commands + } + + // quadratic curveto commands + else if (lop === "q") { for (a = 0; a < alen; a += 4) { if (op === "q") { cx = x + args[a]; @@ -128,7 +135,7 @@ function normalizePath(d) { x = args[a + 2]; y = args[a + 3]; } - normalized += ["Q",cx,cy,x,y,''].join(" "); + normalized += "Q " + cx + " " + cy + " " + x + " " + y + " "; } } else if (lop === "t") { for (a = 0; a < alen; a += 2) { @@ -143,10 +150,12 @@ function normalizePath(d) { x = args[a]; y = args[a + 1]; } - normalized += ["Q",cx,cy,x,y,''].join(" "); + normalized += "Q " + cx + " " + cy + " " + x + " " + y + " "; } - } else if (lop === "c") { - // cubic curveto commands + } + + // cubic curveto commands + else if (lop === "c") { for (a = 0; a < alen; a += 6) { if (op === "c") { cx = x + args[a]; @@ -163,7 +172,20 @@ function normalizePath(d) { x = args[a + 4]; y = args[a + 5]; } - normalized += ["C",cx,cy,cx2,cy2,x,y,''].join(" "); + normalized += + "C " + + cx + + " " + + cy + + " " + + cx2 + + " " + + cy2 + + " " + + x + + " " + + y + + " "; } } else if (lop === "s") { for (a = 0; a < alen; a += 4) { @@ -182,7 +204,57 @@ function normalizePath(d) { x = args[a + 2]; y = args[a + 3]; } - normalized +=["C",cx,cy,cx2,cy2,x,y,''].join(" "); + normalized += + "C " + + cx + + " " + + cy + + " " + + cx2 + + " " + + cy2 + + " " + + x + + " " + + y + + " "; + } + } + + // rx ry x-axis-rotation large-arc-flag sweep-flag x y + // a 25,25 -30 0, 1 50,-25 + + // arc command + else if (lop === "a") { + for (a = 0; a < alen; a += 7) { + rx = args[a]; + ry = args[a + 1]; + xrot = args[a + 2]; + lflag = args[a + 3]; + sweep = args[a + 4]; + if (op === "a") { + x += args[a + 5]; + y += args[a + 6]; + } else { + x = args[a + 5]; + y = args[a + 6]; + } + normalized += + "A " + + rx + + " " + + ry + + " " + + xrot + + " " + + lflag + + " " + + sweep + + " " + + x + + " " + + y + + " "; } } else if (lop === "z") { normalized += "Z "; @@ -193,5 +265,3 @@ function normalizePath(d) { } return normalized.trim(); } - -module.exports = normalizePath; diff --git a/lib/custom-element/lib/bezierjs/poly-bezier.js b/lib/custom-element/lib/bezierjs/poly-bezier.js new file mode 100644 index 00000000..091acc80 --- /dev/null +++ b/lib/custom-element/lib/bezierjs/poly-bezier.js @@ -0,0 +1,70 @@ +import { utils } from "./utils.js"; + +/** + * Poly Bezier + * @param {[type]} curves [description] + */ +class PolyBezier { + constructor(curves) { + this.curves = []; + this._3d = false; + if (!!curves) { + this.curves = curves; + this._3d = this.curves[0]._3d; + } + } + + valueOf() { + return this.toString(); + } + + toString() { + return ( + "[" + + this.curves + .map(function (curve) { + return utils.pointsToString(curve.points); + }) + .join(", ") + + "]" + ); + } + + addCurve(curve) { + this.curves.push(curve); + this._3d = this._3d || curve._3d; + } + + length() { + return this.curves + .map(function (v) { + return v.length(); + }) + .reduce(function (a, b) { + return a + b; + }); + } + + curve(idx) { + return this.curves[idx]; + } + + bbox() { + const c = this.curves; + var bbox = c[0].bbox(); + for (var i = 1; i < c.length; i++) { + utils.expandbox(bbox, c[i].bbox()); + } + return bbox; + } + + offset(d) { + const offset = []; + this.curves.forEach(function (v) { + offset = offset.concat(v.offset(d)); + }); + return new PolyBezier(offset); + } +} + +export { PolyBezier }; diff --git a/lib/custom-element/lib/bezierjs/svg-to-beziers.js b/lib/custom-element/lib/bezierjs/svg-to-beziers.js new file mode 100644 index 00000000..9a49a91f --- /dev/null +++ b/lib/custom-element/lib/bezierjs/svg-to-beziers.js @@ -0,0 +1,45 @@ +import normalise from "./normalise-svg.js"; + +let M = { x: false, y: false }; + +/** + * ... + */ +function makeBezier(Bezier, term, values) { + if (term === "Z") return; + if (term === "M") { + M = { x: values[0], y: values[1] }; + return; + } + const curve = new Bezier(M.x, M.y, ...values); + const last = values.slice(-2); + M = { x: last[0], y: last[1] }; + return curve; +} + +/** + * ... + */ +function convertPath(Bezier, d) { + const terms = normalise(d).split(" "), + matcher = new RegExp("[MLCQZ]", ""); + + let term, + segment, + values, + segments = [], + ARGS = { C: 6, Q: 4, L: 2, M: 2 }; + + while (terms.length) { + term = terms.splice(0, 1)[0]; + if (matcher.test(term)) { + values = terms.splice(0, ARGS[term]).map(parseFloat); + segment = makeBezier(Bezier, term, values); + if (segment) segments.push(segment); + } + } + + return new Bezier.PolyBezier(segments); +} + +export { convertPath }; diff --git a/lib/custom-element/lib/bezierjs/utils.js b/lib/custom-element/lib/bezierjs/utils.js new file mode 100644 index 00000000..3a55db4c --- /dev/null +++ b/lib/custom-element/lib/bezierjs/utils.js @@ -0,0 +1,906 @@ +import { Bezier } from "./bezier.js"; + +// math-inlining. +const { abs, cos, sin, acos, atan2, sqrt, pow } = Math; + +// cube root function yielding real roots +function crt(v) { + return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3); +} + +// trig constants +const pi = Math.PI, + tau = 2 * pi, + quart = pi / 2, + // float precision significant decimal + epsilon = 0.000001, + // extremas used in bbox calculation and similar algorithms + nMax = Number.MAX_SAFE_INTEGER || 9007199254740991, + nMin = Number.MIN_SAFE_INTEGER || -9007199254740991, + // a zero coordinate, which is surprisingly useful + ZERO = { x: 0, y: 0, z: 0 }; + +// Bezier utility functions +const utils = { + // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + Tvalues: [ + -0.0640568928626056260850430826247450385909, + 0.0640568928626056260850430826247450385909, + -0.1911188674736163091586398207570696318404, + 0.1911188674736163091586398207570696318404, + -0.3150426796961633743867932913198102407864, + 0.3150426796961633743867932913198102407864, + -0.4337935076260451384870842319133497124524, + 0.4337935076260451384870842319133497124524, + -0.5454214713888395356583756172183723700107, + 0.5454214713888395356583756172183723700107, + -0.6480936519369755692524957869107476266696, + 0.6480936519369755692524957869107476266696, + -0.7401241915785543642438281030999784255232, + 0.7401241915785543642438281030999784255232, + -0.8200019859739029219539498726697452080761, + 0.8200019859739029219539498726697452080761, + -0.8864155270044010342131543419821967550873, + 0.8864155270044010342131543419821967550873, + -0.9382745520027327585236490017087214496548, + 0.9382745520027327585236490017087214496548, + -0.9747285559713094981983919930081690617411, + 0.9747285559713094981983919930081690617411, + -0.9951872199970213601799974097007368118745, + 0.9951872199970213601799974097007368118745, + ], + + // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) + Cvalues: [ + 0.1279381953467521569740561652246953718517, + 0.1279381953467521569740561652246953718517, + 0.1258374563468282961213753825111836887264, + 0.1258374563468282961213753825111836887264, + 0.121670472927803391204463153476262425607, + 0.121670472927803391204463153476262425607, + 0.1155056680537256013533444839067835598622, + 0.1155056680537256013533444839067835598622, + 0.1074442701159656347825773424466062227946, + 0.1074442701159656347825773424466062227946, + 0.0976186521041138882698806644642471544279, + 0.0976186521041138882698806644642471544279, + 0.086190161531953275917185202983742667185, + 0.086190161531953275917185202983742667185, + 0.0733464814110803057340336152531165181193, + 0.0733464814110803057340336152531165181193, + 0.0592985849154367807463677585001085845412, + 0.0592985849154367807463677585001085845412, + 0.0442774388174198061686027482113382288593, + 0.0442774388174198061686027482113382288593, + 0.0285313886289336631813078159518782864491, + 0.0285313886289336631813078159518782864491, + 0.0123412297999871995468056670700372915759, + 0.0123412297999871995468056670700372915759, + ], + + arcfn: function (t, derivativeFn) { + const d = derivativeFn(t); + let l = d.x * d.x + d.y * d.y; + if (typeof d.z !== "undefined") { + l += d.z * d.z; + } + return sqrt(l); + }, + + compute: function (t, points, _3d) { + // shortcuts + if (t === 0) { + return points[0]; + } + + const order = points.length - 1; + + if (t === 1) { + return points[order]; + } + + const mt = 1 - t; + let p = points; + + // constant? + if (order === 0) { + return points[0]; + } + + // linear? + if (order === 1) { + const ret = { + x: mt * p[0].x + t * p[1].x, + y: mt * p[0].y + t * p[1].y, + }; + if (_3d) { + ret.z = mt * p[0].z + t * p[1].z; + } + return ret; + } + + // quadratic/cubic curve? + if (order < 4) { + let mt2 = mt * mt, + t2 = t * t, + a, + b, + c, + d = 0; + if (order === 2) { + p = [p[0], p[1], p[2], ZERO]; + a = mt2; + b = mt * t * 2; + c = t2; + } else if (order === 3) { + a = mt2 * mt; + b = mt2 * t * 3; + c = mt * t2 * 3; + d = t * t2; + } + const ret = { + x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, + y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y, + }; + if (_3d) { + ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z; + } + return ret; + } + + // higher order curves: use de Casteljau's computation + const dCpts = JSON.parse(JSON.stringify(points)); + while (dCpts.length > 1) { + for (let i = 0; i < dCpts.length - 1; i++) { + dCpts[i] = { + x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t, + y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t, + }; + if (typeof dCpts[i].z !== "undefined") { + dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t; + } + } + dCpts.splice(dCpts.length - 1, 1); + } + return dCpts[0]; + }, + + computeWithRatios: function (t, points, ratios, _3d) { + const mt = 1 - t, + r = ratios, + p = points; + + let f1 = r[0], + f2 = r[1], + f3 = r[2], + f4 = r[3], + d; + + // spec for linear + f1 *= mt; + f2 *= t; + + if (p.length === 2) { + d = f1 + f2; + return { + x: (f1 * p[0].x + f2 * p[1].x) / d, + y: (f1 * p[0].y + f2 * p[1].y) / d, + z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d, + }; + } + + // upgrade to quadratic + f1 *= mt; + f2 *= 2 * mt; + f3 *= t * t; + + if (p.length === 3) { + d = f1 + f2 + f3; + return { + x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d, + y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d, + z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d, + }; + } + + // upgrade to cubic + f1 *= mt; + f2 *= 1.5 * mt; + f3 *= 3 * mt; + f4 *= t * t * t; + + if (p.length === 4) { + d = f1 + f2 + f3 + f4; + return { + x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x) / d, + y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y) / d, + z: !_3d + ? false + : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d, + }; + } + }, + + derive: function (points, _3d) { + const dpoints = []; + for (let p = points, d = p.length, c = d - 1; d > 1; d--, c--) { + const list = []; + for (let j = 0, dpt; j < c; j++) { + dpt = { + x: c * (p[j + 1].x - p[j].x), + y: c * (p[j + 1].y - p[j].y), + }; + if (_3d) { + dpt.z = c * (p[j + 1].z - p[j].z); + } + list.push(dpt); + } + dpoints.push(list); + p = list; + } + return dpoints; + }, + + between: function (v, m, M) { + return ( + (m <= v && v <= M) || + utils.approximately(v, m) || + utils.approximately(v, M) + ); + }, + + approximately: function (a, b, precision) { + return abs(a - b) <= (precision || epsilon); + }, + + length: function (derivativeFn) { + const z = 0.5, + len = utils.Tvalues.length; + + let sum = 0; + + for (let i = 0, t; i < len; i++) { + t = z * utils.Tvalues[i] + z; + sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn); + } + return z * sum; + }, + + map: function (v, ds, de, ts, te) { + const d1 = de - ds, + d2 = te - ts, + v2 = v - ds, + r = v2 / d1; + return ts + d2 * r; + }, + + lerp: function (r, v1, v2) { + const ret = { + x: v1.x + r * (v2.x - v1.x), + y: v1.y + r * (v2.y - v1.y), + }; + if (!!v1.z && !!v2.z) { + ret.z = v1.z + r * (v2.z - v1.z); + } + return ret; + }, + + pointToString: function (p) { + let s = p.x + "/" + p.y; + if (typeof p.z !== "undefined") { + s += "/" + p.z; + } + return s; + }, + + pointsToString: function (points) { + return "[" + points.map(utils.pointToString).join(", ") + "]"; + }, + + copy: function (obj) { + return JSON.parse(JSON.stringify(obj)); + }, + + angle: function (o, v1, v2) { + const dx1 = v1.x - o.x, + dy1 = v1.y - o.y, + dx2 = v2.x - o.x, + dy2 = v2.y - o.y, + cross = dx1 * dy2 - dy1 * dx2, + dot = dx1 * dx2 + dy1 * dy2; + return atan2(cross, dot); + }, + + // round as string, to avoid rounding errors + round: function (v, d) { + const s = "" + v; + const pos = s.indexOf("."); + return parseFloat(s.substring(0, pos + 1 + d)); + }, + + dist: function (p1, p2) { + const dx = p1.x - p2.x, + dy = p1.y - p2.y; + return sqrt(dx * dx + dy * dy); + }, + + closest: function (LUT, point) { + let mdist = pow(2, 63), + mpos, + d; + LUT.forEach(function (p, idx) { + d = utils.dist(point, p); + if (d < mdist) { + mdist = d; + mpos = idx; + } + }); + return { mdist: mdist, mpos: mpos }; + }, + + abcratio: function (t, n) { + // see ratio(t) note on http://pomax.github.io/bezierinfo/#abc + if (n !== 2 && n !== 3) { + return false; + } + if (typeof t === "undefined") { + t = 0.5; + } else if (t === 0 || t === 1) { + return t; + } + const bottom = pow(t, n) + pow(1 - t, n), + top = bottom - 1; + return abs(top / bottom); + }, + + projectionratio: function (t, n) { + // see u(t) note on http://pomax.github.io/bezierinfo/#abc + if (n !== 2 && n !== 3) { + return false; + } + if (typeof t === "undefined") { + t = 0.5; + } else if (t === 0 || t === 1) { + return t; + } + const top = pow(1 - t, n), + bottom = pow(t, n) + top; + return top / bottom; + }, + + lli8: function (x1, y1, x2, y2, x3, y3, x4, y4) { + const nx = + (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), + ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), + d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (d == 0) { + return false; + } + return { x: nx / d, y: ny / d }; + }, + + lli4: function (p1, p2, p3, p4) { + const x1 = p1.x, + y1 = p1.y, + x2 = p2.x, + y2 = p2.y, + x3 = p3.x, + y3 = p3.y, + x4 = p4.x, + y4 = p4.y; + return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4); + }, + + lli: function (v1, v2) { + return utils.lli4(v1, v1.c, v2, v2.c); + }, + + makeline: function (p1, p2) { + const x1 = p1.x, + y1 = p1.y, + x2 = p2.x, + y2 = p2.y, + dx = (x2 - x1) / 3, + dy = (y2 - y1) / 3; + return new Bezier( + x1, + y1, + x1 + dx, + y1 + dy, + x1 + 2 * dx, + y1 + 2 * dy, + x2, + y2 + ); + }, + + findbbox: function (sections) { + let mx = nMax, + my = nMax, + MX = nMin, + MY = nMin; + sections.forEach(function (s) { + const bbox = s.bbox(); + if (mx > bbox.x.min) mx = bbox.x.min; + if (my > bbox.y.min) my = bbox.y.min; + if (MX < bbox.x.max) MX = bbox.x.max; + if (MY < bbox.y.max) MY = bbox.y.max; + }); + return { + x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx }, + y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my }, + }; + }, + + shapeintersections: function ( + s1, + bbox1, + s2, + bbox2, + curveIntersectionThreshold + ) { + if (!utils.bboxoverlap(bbox1, bbox2)) return []; + const intersections = []; + const a1 = [s1.startcap, s1.forward, s1.back, s1.endcap]; + const a2 = [s2.startcap, s2.forward, s2.back, s2.endcap]; + a1.forEach(function (l1) { + if (l1.virtual) return; + a2.forEach(function (l2) { + if (l2.virtual) return; + const iss = l1.intersects(l2, curveIntersectionThreshold); + if (iss.length > 0) { + iss.c1 = l1; + iss.c2 = l2; + iss.s1 = s1; + iss.s2 = s2; + intersections.push(iss); + } + }); + }); + return intersections; + }, + + makeshape: function (forward, back, curveIntersectionThreshold) { + const bpl = back.points.length; + const fpl = forward.points.length; + const start = utils.makeline(back.points[bpl - 1], forward.points[0]); + const end = utils.makeline(forward.points[fpl - 1], back.points[0]); + const shape = { + startcap: start, + forward: forward, + back: back, + endcap: end, + bbox: utils.findbbox([start, forward, back, end]), + }; + shape.intersections = function (s2) { + return utils.shapeintersections( + shape, + shape.bbox, + s2, + s2.bbox, + curveIntersectionThreshold + ); + }; + return shape; + }, + + getminmax: function (curve, d, list) { + if (!list) return { min: 0, max: 0 }; + let min = nMax, + max = nMin, + t, + c; + if (list.indexOf(0) === -1) { + list = [0].concat(list); + } + if (list.indexOf(1) === -1) { + list.push(1); + } + for (let i = 0, len = list.length; i < len; i++) { + t = list[i]; + c = curve.get(t); + if (c[d] < min) { + min = c[d]; + } + if (c[d] > max) { + max = c[d]; + } + } + return { min: min, mid: (min + max) / 2, max: max, size: max - min }; + }, + + align: function (points, line) { + const tx = line.p1.x, + ty = line.p1.y, + a = -atan2(line.p2.y - ty, line.p2.x - tx), + d = function (v) { + return { + x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a), + y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a), + }; + }; + return points.map(d); + }, + + roots: function (points, line) { + line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } }; + + const order = points.length - 1; + const aligned = utils.align(points, line); + const reduce = function (t) { + return 0 <= t && t <= 1; + }; + + if (order === 2) { + const a = aligned[0].y, + b = aligned[1].y, + c = aligned[2].y, + d = a - 2 * b + c; + if (d !== 0) { + const m1 = -sqrt(b * b - a * c), + m2 = -a + b, + v1 = -(m1 + m2) / d, + v2 = -(-m1 + m2) / d; + return [v1, v2].filter(reduce); + } else if (b !== c && d === 0) { + return [(2 * b - c) / (2 * b - 2 * c)].filter(reduce); + } + return []; + } + + // see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm + const pa = aligned[0].y, + pb = aligned[1].y, + pc = aligned[2].y, + pd = aligned[3].y; + + let d = -pa + 3 * pb - 3 * pc + pd, + a = 3 * pa - 6 * pb + 3 * pc, + b = -3 * pa + 3 * pb, + c = pa; + + if (utils.approximately(d, 0)) { + // this is not a cubic curve. + if (utils.approximately(a, 0)) { + // in fact, this is not a quadratic curve either. + if (utils.approximately(b, 0)) { + // in fact in fact, there are no solutions. + return []; + } + // linear solution: + return [-c / b].filter(reduce); + } + // quadratic solution: + const q = sqrt(b * b - 4 * a * c), + a2 = 2 * a; + return [(q - b) / a2, (-b - q) / a2].filter(reduce); + } + + // at this point, we know we need a cubic solution: + + a /= d; + b /= d; + c /= d; + + const p = (3 * b - a * a) / 3, + p3 = p / 3, + q = (2 * a * a * a - 9 * a * b + 27 * c) / 27, + q2 = q / 2, + discriminant = q2 * q2 + p3 * p3 * p3; + + let u1, v1, x1, x2, x3; + if (discriminant < 0) { + const mp3 = -p / 3, + mp33 = mp3 * mp3 * mp3, + r = sqrt(mp33), + t = -q / (2 * r), + cosphi = t < -1 ? -1 : t > 1 ? 1 : t, + phi = acos(cosphi), + crtr = crt(r), + t1 = 2 * crtr; + x1 = t1 * cos(phi / 3) - a / 3; + x2 = t1 * cos((phi + tau) / 3) - a / 3; + x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3; + return [x1, x2, x3].filter(reduce); + } else if (discriminant === 0) { + u1 = q2 < 0 ? crt(-q2) : -crt(q2); + x1 = 2 * u1 - a / 3; + x2 = -u1 - a / 3; + return [x1, x2].filter(reduce); + } else { + const sd = sqrt(discriminant); + u1 = crt(-q2 + sd); + v1 = crt(q2 + sd); + return [u1 - v1 - a / 3].filter(reduce); + } + }, + + droots: function (p) { + // quadratic roots are easy + if (p.length === 3) { + const a = p[0], + b = p[1], + c = p[2], + d = a - 2 * b + c; + if (d !== 0) { + const m1 = -sqrt(b * b - a * c), + m2 = -a + b, + v1 = -(m1 + m2) / d, + v2 = -(-m1 + m2) / d; + return [v1, v2]; + } else if (b !== c && d === 0) { + return [(2 * b - c) / (2 * (b - c))]; + } + return []; + } + + // linear roots are even easier + if (p.length === 2) { + const a = p[0], + b = p[1]; + if (a !== b) { + return [a / (a - b)]; + } + return []; + } + }, + + curvature: function (t, points, _3d, kOnly) { + const dpoints = utils.derive(points); + const d1 = dpoints[0]; + const d2 = dpoints[1]; + + let num, + dnm, + adk, + dk, + k = 0, + r = 0; + + // + // We're using the following formula for curvature: + // + // x'y" - y'x" + // k(t) = ------------------ + // (x'² + y'²)^(3/2) + // + // from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition + // + // With it corresponding 3D counterpart: + // + // sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²) + // k(t) = ------------------------------------------------------- + // (x'² + y'² + z'²)^(3/2) + // + + const d = utils.compute(t, d1); + const dd = utils.compute(t, d2); + const qdsum = d.x * d.x + d.y * d.y; + + if (_3d) { + num = sqrt( + pow(d.y * dd.z - dd.y * d.z, 2) + + pow(d.z * dd.x - dd.z * d.x, 2) + + pow(d.x * dd.y - dd.x * d.y, 2) + ); + dnm = pow(qdsum + d.z * d.z, 3 / 2); + } else { + num = d.x * dd.y - d.y * dd.x; + dnm = pow(qdsum, 3 / 2); + } + + if (num === 0 || dnm === 0) { + return { k: 0, r: 0 }; + } + + k = num / dnm; + r = dnm / num; + + // We're also computing the derivative of kappa, because + // there is value in knowing the rate of change for the + // curvature along the curve. And we're just going to + // ballpark it based on an epsilon. + if (!kOnly) { + // compute k'(t) based on the interval before, and after it, + // to at least try to not introduce forward/backward pass bias. + const pk = utils.curvature(t - 0.001, points, _3d, true).k; + const nk = utils.curvature(t + 0.001, points, _3d, true).k; + dk = (nk - k + (k - pk)) / 2; + adk = (abs(nk - k) + abs(k - pk)) / 2; + } + + return { k: k, r: r, dk: dk, adk: adk }; + }, + + inflections: function (points) { + if (points.length < 4) return []; + + // FIXME: TODO: add in inflection abstraction for quartic+ curves? + + const p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }), + a = p[2].x * p[1].y, + b = p[3].x * p[1].y, + c = p[1].x * p[2].y, + d = p[3].x * p[2].y, + v1 = 18 * (-3 * a + 2 * b + 3 * c - d), + v2 = 18 * (3 * a - b - 3 * c), + v3 = 18 * (c - a); + + if (utils.approximately(v1, 0)) { + if (!utils.approximately(v2, 0)) { + let t = -v3 / v2; + if (0 <= t && t <= 1) return [t]; + } + return []; + } + + const trm = v2 * v2 - 4 * v1 * v3, + sq = Math.sqrt(trm), + d2 = 2 * v1; + + if (utils.approximately(d2, 0)) return []; + + return [(sq - v2) / d2, -(v2 + sq) / d2].filter(function (r) { + return 0 <= r && r <= 1; + }); + }, + + bboxoverlap: function (b1, b2) { + const dims = ["x", "y"], + len = dims.length; + + for (let i = 0, dim, l, t, d; i < len; i++) { + dim = dims[i]; + l = b1[dim].mid; + t = b2[dim].mid; + d = (b1[dim].size + b2[dim].size) / 2; + if (abs(l - t) >= d) return false; + } + return true; + }, + + expandbox: function (bbox, _bbox) { + if (_bbox.x.min < bbox.x.min) { + bbox.x.min = _bbox.x.min; + } + if (_bbox.y.min < bbox.y.min) { + bbox.y.min = _bbox.y.min; + } + if (_bbox.z && _bbox.z.min < bbox.z.min) { + bbox.z.min = _bbox.z.min; + } + if (_bbox.x.max > bbox.x.max) { + bbox.x.max = _bbox.x.max; + } + if (_bbox.y.max > bbox.y.max) { + bbox.y.max = _bbox.y.max; + } + if (_bbox.z && _bbox.z.max > bbox.z.max) { + bbox.z.max = _bbox.z.max; + } + bbox.x.mid = (bbox.x.min + bbox.x.max) / 2; + bbox.y.mid = (bbox.y.min + bbox.y.max) / 2; + if (bbox.z) { + bbox.z.mid = (bbox.z.min + bbox.z.max) / 2; + } + bbox.x.size = bbox.x.max - bbox.x.min; + bbox.y.size = bbox.y.max - bbox.y.min; + if (bbox.z) { + bbox.z.size = bbox.z.max - bbox.z.min; + } + }, + + pairiteration: function (c1, c2, curveIntersectionThreshold) { + const c1b = c1.bbox(), + c2b = c2.bbox(), + r = 100000, + threshold = curveIntersectionThreshold || 0.5; + + if ( + c1b.x.size + c1b.y.size < threshold && + c2b.x.size + c2b.y.size < threshold + ) { + return [ + (((r * (c1._t1 + c1._t2)) / 2) | 0) / r + + "/" + + (((r * (c2._t1 + c2._t2)) / 2) | 0) / r, + ]; + } + + const cc1 = c1.split(0.5), + cc2 = c2.split(0.5), + pairs = [ + { left: cc1.left, right: cc2.left }, + { left: cc1.left, right: cc2.right }, + { left: cc1.right, right: cc2.right }, + { left: cc1.right, right: cc2.left }, + ]; + + pairs = pairs.filter(function (pair) { + return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox()); + }); + + const results = []; + + if (pairs.length === 0) return results; + + pairs.forEach(function (pair) { + results = results.concat( + utils.pairiteration(pair.left, pair.right, threshold) + ); + }); + + results = results.filter(function (v, i) { + return results.indexOf(v) === i; + }); + + return results; + }, + + getccenter: function (p1, p2, p3) { + const dx1 = p2.x - p1.x, + dy1 = p2.y - p1.y, + dx2 = p3.x - p2.x, + dy2 = p3.y - p2.y, + dx1p = dx1 * cos(quart) - dy1 * sin(quart), + dy1p = dx1 * sin(quart) + dy1 * cos(quart), + dx2p = dx2 * cos(quart) - dy2 * sin(quart), + dy2p = dx2 * sin(quart) + dy2 * cos(quart), + // chord midpoints + mx1 = (p1.x + p2.x) / 2, + my1 = (p1.y + p2.y) / 2, + mx2 = (p2.x + p3.x) / 2, + my2 = (p2.y + p3.y) / 2, + // midpoint offsets + mx1n = mx1 + dx1p, + my1n = my1 + dy1p, + mx2n = mx2 + dx2p, + my2n = my2 + dy2p, + // intersection of these lines: + arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n), + r = utils.dist(arc, p1); + + // arc start/end values, over mid point: + let s = atan2(p1.y - arc.y, p1.x - arc.x), + m = atan2(p2.y - arc.y, p2.x - arc.x), + e = atan2(p3.y - arc.y, p3.x - arc.x), + _; + + // determine arc direction (cw/ccw correction) + if (s < e) { + // if s m || m > e) { + s += tau; + } + if (s > e) { + _ = e; + e = s; + s = _; + } + } else { + // if e{});