From a05627e4563630f2cb6c391579f9c8cafc05ddd6 Mon Sep 17 00:00:00 2001
From: Sam <34695753+SamEyeBam@users.noreply.github.com>
Date: Fri, 11 Apr 2025 20:40:23 +1200
Subject: [PATCH] Rays

WTF
---
 Larry the snail/larry_photos/legs0.png | Bin 0 -> 5815 bytes
 Larry the snail/larry_photos/legs1.png | Bin 0 -> 5814 bytes
 Larry the snail/larry_photos/legs2.png | Bin 0 -> 5815 bytes
 Larry the snail/larry_photos/legs4.png | Bin 0 -> 5815 bytes
 docs/index.html                        |   1 +
 docs/js/helper.js                      |  89 +++++--
 docs/js/index.js                       |  33 ++-
 docs/js/objects.js                     | 306 ++++++++++++++++++++++++-
 8 files changed, 402 insertions(+), 27 deletions(-)
 create mode 100644 Larry the snail/larry_photos/legs0.png
 create mode 100644 Larry the snail/larry_photos/legs1.png
 create mode 100644 Larry the snail/larry_photos/legs2.png
 create mode 100644 Larry the snail/larry_photos/legs4.png

diff --git a/Larry the snail/larry_photos/legs0.png b/Larry the snail/larry_photos/legs0.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d742412e9fdf9d0acb239b61a93c41b8635590d
GIT binary patch
literal 5815
zcmeHKeQ*<H8sG5IVu3nU3ba$`HXTmBHv5sU&C;}%B#;1U(-N?;pl){GBukTQ%x;^|
z5kxo`fnh*9)q+@IMEpA7oH<Wa9CV~}z#TVtBI6uzSP(3p2=!#9G9bNoH|duana=4S
zchk*m_I;m^-}5}b_j%vh-RP;9KRzoz3qg?aE~mX3J|Bd4#+VHFFNInTz{j;lZ;f2d
zCeT=13<zO>$}3|41xX=*Aj#8Ps%oQ0h%vXiW;Bi(H~EDx=&j9->#Vmc!p|C-XXYMD
zP98JWdHVCu4_vRGY_52Ch2C7;@*a~|Gw<Yc=k;H8er%|Iy6_3@=27eJJ4@L*+7q20
zTKrq*|J(PZF>Bi#&%5`3ka@V|#<cwS&3{gx;B3WDPAs$+pZ84A@7sO!(5XL#&Mc^`
z>#BM^+!kMw1aF)wI7kX@rSI4-*MY(<$Cq7p?EKbu{BK9z=Zlg#FU-6|JD98E?%nzQ
z8fyC5&#E?6b<Qf%9JfyWmyCVa5xqL)2evzJ)+%ILUc+14%W_sAh-STDvw2)LTTkh*
z;GL_UndAJ}Ghsvb-c8!v%&`+6Kk9k$O8dzA*-K`xUH{3_(5a6u?!`l&Y}<2?&e}gZ
zvvBj0%t^uJQ<jY`@6=q-(KFt2ZtVEY`ibP!gJ-vPw{~}&lQg@l*f;)>JKB4Ks{Xd6
z<;1>C)&reIzb*QUf3AOTs}m2j`yTE3)LlR6yzktqCzl?6)|}UV|HhXWnP*=Ze_^|O
zVaK7$0)CpSsPuuU=F4l3Y+d&J^3`7~lWW?ymlrqfcp&?}&28Dw&1q>K*PPrz|L(Q?
zOJjY{=l->J=cxr--oAS5(aA^aW(oUG=9d>NbfWj<P%EatJYsClX=C-GJ(o+qJYU#w
z%N0L%apt;bIL(sRwiT3pUwEr^pRZ+y+%$sRb+he{W%HhDUB1ir&NtV%GtpM$cCdXx
z-OV@0KX$e0dSmU_Le0oCAGX!kHg&D}^Az;@o`oAOI#)kDcQ<nArN5gmt@)^Smw4jE
z+OG><z4yTlfw!L+Sx_*7a~yx9E13{G&ffk;IB_j=&XsuAr<G;RutysN*o!spMGPlK
zbSy9Wfi4+|!6rqJlCors<?4Wp`aw{LTD8~SKdeOs-l~1b=qB7T8wd%`#yD8qSmEUw
z>o^OqEi26`Niq;10%R6VM#52vNm{ikE(6Dk7}uhziCkyZ*0?>WO^gH7pfl(Q%#jot
zC~avLS`z02OtpPp8UoI&+K?>A7#vR|61s$5C&q&~X|Y&vg2E{ZgBF;yGAgr4EGiW%
z5Gf2hkhr)IlLaw~DwwQate34?E!3ku`9)%GcQ1TYO0xj@z>{nYCv^lKiQxS`B-zmb
zLDC5g>LGdImd2}rB-Y0{;Aj9*xwt<B&-MDp>f>Rx9G=5L7(}3{1f!AzLe6)&J-r?Z
z1wkPaQ@tSB11x1B&_~vQ+!T#kPX9!pdoS()>z>+GV`$}eGj@@ySJHFYty(2M!;752
zGwP6{^?>9F5;N;b7Bc{n#w-L)U<T4}0)PTEZ(;jExuTNHMmeBBL2#V_aX2Hd=S`Fm
zqf8`+8O$aMqX`R%akPmrkrW3EB;609GA_WbWW)WlQlNMUWwH1HVFV_O<t!9t;9)i_
zWu!2DfTxWxL9@kAs!%+~%oF1g7A~g{VS@mVMT4qBA)G1qxU5=ANAyWNVO9=62e=1>
zC@&_YK9yI9fW<Pa@JX8WX0u688A;kqnGC5=D&kTQmtZd{s3f7&n^bpYVHj8rq?T1W
z6#}SoSPf%~16CH}UQrBNwF(5Pc=oE@a6|E|%-UHQKv04*G6cy`MlY#n2!b(~W~qZd
zdyy9cEAM8lY#+2FwdhVkg7H_XqST&R3?5I-Qq!=YZYC5}w*|v;sS+f%0q|;Eh?P=t
zAvPKW@cc+OY>!+Rq#68lfHwO51V+<F*hMBii}^VVjRBrCuq3M|{3IBNE{Op-!Nx&(
z5b_AQf(@#2g-%Z|)r^7mi4aiM0aAv+w(X_NNNVwPvAA+#^i*4d|Adbc)u7)I1N~Am
zcyz&&5br$-(|jq1&aZf-*Wp);0HKG242j=ix`yc*5(7gr9`3GTx`xESkc@}B>ldRd
ztM6k9MBy7y0)8qz?QKfJ&#Vmp!ufWjTlww!;MFE5xhLlINeH4VR&Gu5m9hpX%#>Yj
zN9I?WJOt5Km;T-dMR~HLMz)C&<((W+Ulak4CImTzDldu;J(WEPqUX5m<z6~RlaX=b
zh6XwfxU_FA>K-8YADW!2{KHS`xm)_91~JHovM&db=fVJ2dgMbTN=Xj-Oi25G-Gg(d
z=U~PBT+G4j?}Xi7K4ixFJ1b+DltSXoAIsD8Y?#=0chITg9~CtM+TXPUq0bM{S+V`E
n^Z&WrhIPXX{2wy_w^6oIp6|2bi6-Z2lQMQWD(w5`KDPEhMLHJn

literal 0
HcmV?d00001

diff --git a/Larry the snail/larry_photos/legs1.png b/Larry the snail/larry_photos/legs1.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1302205790341128ff117875a21dfdb7760f514
GIT binary patch
literal 5814
zcmeHKeQXow8MpaJS_gtcn-WlHE*H^;kMn)`^J0@Gw!tnqm|!4j$lCMWYx{`pbA6ZC
zWYAI=Xn?BFW})FLr7MMKTS2R!>jxFB3R3AtwY6o^0MS69DIE<=ixv=!>^*-3r>zQ+
z)<34jiGA-q@9+6N&+~hq_to7dPtE*^nrRx9N;T2twAaGxWAK}EUk?10f}IE9rLWan
zC)cuZBpMU_LI@!8swhA}Lh!3piPO(7ZipNs?z=TGvvu5rqMcuw+B#a-TW{BdUetBW
zDm;-WDlT%K{^E;+{mn(jnkQD$#<I?LnE2XxpKUu&U%vLCuJ)HDtI;jv)=xf5*?N28
z*WNe#+8(*F`q@Iw)(X$t54@Lq)N*5nCU&#2{6S~eZeh!$UvG_;Z{Ge_4}b0(?ojx(
z{fBRMd*AfEJ?jW{YGwY}&O^SJ&y@Bbext1#X#3GmuCM<1&&S^7OA`e<XI(Nmn7#><
zc3odfbzeSHXF0#Tdd#nWuKPyD{@EMpoBnThaO&(esu`UvZ%nxobf{G74T8<)aoKE{
z%3-~`*1S;R{Lu5@#=(7?(Zbxkho1h(v-?WV*ycG)=d9cC>9XLd4=(P*gP(5Q`<6*_
zV0>=LmZiB<0*%vGjIX+;zMwVDe8;(|_mK?`kq^Ihwr#L$u=kv#eq}NHo4*x~_nxF`
z|Iyib^7YNugV#!bQTiuewQpaS6ZiMjKRNKZyLrm_`g3cZU3T<E<J6u9HodygIOoE|
z3)|fbdXFqB=4ZG{=RW$d@#}TR+E%>O_{^6p<hq{iRb}lv9?hS;<#_(Kiq4J+9f=*L
z-*r#Blvn>!;U^8dPCfqo;l2}37Jbw-TR8C9w5s9-PUQXqYGwJWWAX}4>uVS8{o3-?
z`I44fuGoo-v(~@Bsh4(dEv~#?a;xk0`pzA4`xtW1&Es!Y&U>z_aZmjpzP-wwiFB!M
z2YMcFy7}9QPxZC;w>IRJsK=gp|9Epl`@q^iPDlFpF4%a{`OFj5uc(f^{1@Y;wI4L>
z5l`-J_<QmGNsn#xA6`ASxOfcb`1of7iMZH%_V%~J$*Z{)S7HO7FRJW-E!rZ$R;+U`
zWH>RbWqHvDw25#O_NYo_sZ2y!t_jG94+MmW6}|fIQ4|q)E4p0oCfrdQ2nx>D7+BI;
z<K<eLI5Uq{&ed2F3?v8xnMD%eP()%9Ry4`Wz_DV+Q6veGo2+P^+k@D|7(jGdotD5H
z3895T=V}m3jQ2CO_IYUvcxOd}vK(b_JRXm0<Fr<c1#r@AHsb_^Qxpafn6xS)vk5FB
zl_?Y{4m*&zm=Ki(F@h+ZtWRu~ttblTkxYK!sM|e6ACb~3Kt1pT8^uX2frrC*wudA;
zS|CX}p&#^+yl_Y3wLlV^V;pd_fQVd{O~G?R{?X=GC|M5A;UEOU5Gui_<gk$QU2e~i
zhay2h2uG7%Q0!qyS@7QxYglf|OtPHpM4<Z+?=W<x_9Pgh+-}A$a?MJ5F1r;~;xoL+
z2|SY=vO0n^Qg9VG1J7YPgMq-zdWytMtR5J7lIA(Wm__A^NHQDYfI<bywF2ZZakS42
z^ajjm;>?(i_8Bk};o~vVV4{7LUQe?|Ll(uNm;k$y4P|GgQ1OsTPZA^mB#)T@0GN)@
z)0mHDS&Zd)9py98I>MZy;yGrX7z?v-IfXDA0C+SKNXAeEXR16dD@tjJI~GrfmHp5G
z?g1gfi*f1BlvfCYB{Hk%Ng8RR(LmE?k~9z$l?s()Tn1ti>_vr@B(!vDMp+mJmII|_
zl}?2OnK~I;46w2o^NM1~iYgR{;yE<!h8v1!W!BEh0Fn}vo*_tv(tAmoA>hAhwlc`V
zi@e}pbvLxKeGp4((Vc<><F87ZQhRC%csg~Lx(x}*&4eJyZNacyssxE`0emt~$d#Jn
zf@~xJ;Q5hm*o<BHfo3r1IF6)A9|jwT#B`h<wi3x2FxF(IS)S%tKS)(I#4d?`InKsF
zRRHP;wSo<r)Cwt2FV)Q9*~EiDSqCT?1|tkfMv^F=E*4i#j7+r_{Cj#>l7Oru2KuFJ
z@aTdkAwF~zruBlk{*<5eI{cI&5M(sSi1ZzmYgDcgDKH}NXm^duH6jH@1Rm|K|C(Hy
zJ1<ip0-u25@KuQ*V+r`0mE&75->w=|{`S1Lza3ick2>oml}cNteAS89DqEm2S9ZA_
zxtG;bRb%rO7c`86rm3={PPU0*<(XWSd{6{D5*OqkqC6-re=dItWG`^ptGuQHbxzKW
z8*1n@?1TP(;ovaC|Ip33MTfpu&)w2jRQv8{8Z>;BWu(*N3oyWykqVTZ)V=s-<CVB~
z@#%*6dWg}$>%X#czUSeH70WRHSYqETBIBc6p&t(~JF{&+hR?=jV%{r0+jn$4s=)t=
i0&ts@-%F_wm#^TYZRL%j$rU}y*yX6PAE<t6-G2a-`ybf=

literal 0
HcmV?d00001

diff --git a/Larry the snail/larry_photos/legs2.png b/Larry the snail/larry_photos/legs2.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8702022f181fd2f1fa5a245256b13faf7e3d078
GIT binary patch
literal 5815
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

literal 0
HcmV?d00001

diff --git a/Larry the snail/larry_photos/legs4.png b/Larry the snail/larry_photos/legs4.png
new file mode 100644
index 0000000000000000000000000000000000000000..a798939dbfdc62ed3aa23e17cdb360e9a8452500
GIT binary patch
literal 5815
zcmeHLeNYtV8NbsA5rx(UwOG{U)QHBr-P^s-<xWHny+ckB0U==G-tK$Hmb=^YcHvH@
zNsJA2G?U2(F(Hg1nfQ@O6H{AloHo{4rA<OHQ>%Y8T7%YznQG9CEkUjH**(6)3^J2U
zrX7~q+xLCm-}8H)=l4GEJG*SlU%fIhVOD}lrAl-;9R=`Q26x<paqufvG`tIs&YHp!
zsep-Kp|IfP{RopbhY*IMyjP`)e)8PfvfxSL!Mk0HY94s(kN@7Z^G~@)kI#8(p~=;l
zyJI_No0>Fr{`Jnz^|$G~x8}F*tq~VC7%NY!7rV>qe80T-v(i1^&-}_UXR><lk*YfP
z)=!U56i;d!Z*<>VoV+C?|91}^jX!4VUXYOcpAG5B=^vju)DpkexgfbEuQL71;#T%h
zVDJ93cN%8gyjDK-!ZW!y<H~E_-njc*+O5O6KdSw*{A$k5o85)Yu4=R(TC{udzbvI0
z!Mga>;bpU5KJlxMN^BR`FQ4%8dlhFsfAq@7e?Omb`@}t}__k{PAF6+Ga7lQUN~Nyl
zv$OME+1Y)mL&7_scrn9yB0ssVXWw?s^!O<=Hhqx4>stH7swG8Bw$`35t~h)A@;<!c
z^b32ASP~9Qil6&zQT$BbhS}wlvTmp^=`4$W<J{KqaP18JoFnI-?`i7kxFD)uU(3Ap
zx9O7#+sJ~O4GnGYY`4FABkh^AKYNyY_BA<iZ+q$EU1!`?GcT52c;czzV=tLg+8^5X
zMxJ@erNm3GxmR_(zb2Ji;7VJ%aE|%v)|1bdzq;Ycf0Rol?XP9ct8ZMG^n+(xl3vMZ
z*fDiSw9)eRo3lQfQu^xjQ)N5P=011$iw__F;RltE@CW`nD=T%C6PrAZ{Biml6Q)f2
z#8{BG=c?_>#ktjYUEvQeFaGI^th(sU7g967ntQkDozjLzseXdK`A*BPGgmy_w4u55
zz3ZLqxnPs(p07Q(^3Kl_H-1rntEOzqT=m3rzi+83tMA&<IvcySXI0&0=aY{ve_i$d
z?!TBn+j6|DS!ml;_RrM)-&<DaJ^Yi2si_m#oR1#sibjNv^Y^avZJqHM*TP+A)@1I0
z8m;D`7E9cDG%Eyj3@3PyE*c0yk*ZX-%xH*VE0KhGkdF`AHJ!gbronj5u32w%6Yfwp
zs^Fb9VYIGhbs<|*$yzy0=F$XPlm>zTk{B!+@CQXYYS$>dG_++iuE7+DRB6|gxbv}W
zA&fAC&Y&Z-IZ?iv)GSTFY+=qz7dTe*Qb5nHsgR@)jpLC>L>HlSLfD7vtyU{ekT^+d
z0ihK)2PGz|4T|$*iWr9jiENk;NxTrmWKPB-R7rM?2F9_z_yQrfyPrNN_NoAS;87-o
z>vaSk2;hS~L@B2lNO}Vr(L*eREsYl-QK$;DD5n|)rFnxXIJVzER2B9s>2NHL{3rlW
z5oXm7nR2Deo!{>vOW@-JA;k;C9)gs3?|@iCVv|RdbOr|k?)|(&(0#cpU_iOuv_oL4
z<nUY$yGEX$<^-1KXr*Z}c&)4lnY0w4H)#zdY0!EQ>Cu`EUe4nsj1;mEgQ#3VQDTBD
zlBs}P#{-Ya%ursdhtQH%3Mdfe(J}^$M@#4l!i)@rm$7&TQLG8`P?d~-a8xoC2UKQ@
z0kIZfK_07FYcP;zEkiKSH5(|a$710aB1Xlr^a>#yU|>1<0OLb=DCkq>kOil+@?CZf
zsfz`!Sn~ah<OK)V1ALGZBH{qJkPo1B5+m!WH&bS_i87igD?w4DVE|N&!Xnh7%&I4J
zlt~$p7lwv(Kw3twR3K37kPV$3MvNqc3kAV%*T^AYvS<IW8#WZjNQ{G#5Rek2ktXys
zX)M%JG-0Ck<Rfxp5MJPT@8;3a^7g@Ou|;?CBFw*8F~#=OI<zU)i*^0HvY9YU*%maz
z#!?WOYQ!mV0#|H^tzd#a1m{O@!S>nt5sHCBCM#*8%vz3RELww=GeI#hRxM@ZybNPB
zkwzw#RX@8Zc%=vvMp-`45wwB=RkXs=dzWg_&}bqRNL~k!3@S`JAQ`2>dy~cG6QeI%
z8~!ytYzkn|5d(fP8ysD565{<wVXs~g>$iFJuEV!E1%{1JGA4b;<r<f3ObUz%JYHSn
za*auWF@eXc>l>3RVc=s51>p@S0-s8)McD!P%!>1@TIo>r$bZd8_t%4Ea>!XKs#Lmp
z@~w{U&8!AvyySA{#DA_%QN<;;OiGZzlp^JnNZCR_ekWHcFN%o6BD_?A$uElQpH7+y
z?9*J1tU}8)bzEF`w;G&=T$+P<JwputL!Aq24t*`3(ZYXV1OWV7LX`q!r|v_HQ-DP>
zb~w*yzQcnZ4G;WsTt>JL4xbD98sPhM1pBC_Q5A#0Qh?>kFeYlGq=#XYF8GtsXzLpr
zo_L>KevTwUA6||F+8w>PWO&|yTX(;4$bR4X1xciR=Ru4I@PDoVR9X7{qzJeq8Rt(W
UcD24lUY6UgoYjs4%QtTQFN0qyvH$=8

literal 0
HcmV?d00001

diff --git a/docs/index.html b/docs/index.html
index 477eae6..5181340 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -12,6 +12,7 @@
   <div id="toolbar">
     <br>
     <select id="shape-selector">
+      <option value="RaysInShape">Rays</option>
       <option value="NewWave">NewWave</option>
       <option value="EyePrototype">EyePrototype</option>
       <option value="Nodal_expanding">Nodal_expanding</option>
diff --git a/docs/js/helper.js b/docs/js/helper.js
index 7ab3e95..6168753 100644
--- a/docs/js/helper.js
+++ b/docs/js/helper.js
@@ -99,44 +99,97 @@ async function fetchConfig(className) {
       { type: "range", min: 1, max: 10, defaultValue: 4, property: "lineWidth" },
       { type: "range", min: 100, max: 1000, defaultValue: 100, property: "limiter" },
     ],
+    RaysInShape: [
+      { type: "range", min: 50, max: 1000, defaultValue: 300, property: "rays" },
+      { type: "range", min: 1, max: 1000, defaultValue: 2, property: "speed" },
+      { type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVert" },
+      { type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorr" },
+      { type: "range", min: 10, max: 2000, defaultValue: 800, property: "boxSize" },
+      { type: "range", min: 10, max: 200, defaultValue: 50, property: "trailLength" },
+
+    ],
   };
   return config[className];
 }
 
 function addControl(item, instance) {
-  // console.log(item);
   let parentDiv = document.getElementById("custom");
 
   let title = document.createElement("p");
   title.innerText = item.property + ": " + item.defaultValue;
   title.id = "elText" + item.property;
 
-  let control = document.createElement("input");
-  control.type = item.type;
+  let control;
 
   if (item.type === "range") {
+    control = document.createElement("input");
+    control.type = "range";
     control.min = item.min;
     control.max = item.max;
+    control.value = item.defaultValue;
+    control.addEventListener("input", (event) => {
+      const newValue = parseInt(event.target.value, 10);
+      instance[item.property] = newValue;
+      title.innerText = item.property + ": " + newValue;
+
+      if (item.callback) {
+        item.callback(instance, newValue);
+      }
+    });
+  } else if (item.type === "button") {
+    control = document.createElement("button");
+    control.innerText = item.label;
+    control.addEventListener("click", () => {
+      instance[item.method]();
+    });
+  } else if (item.type === "dropdown") {
+    control = document.createElement("select");
+    item.options.forEach(option => {
+      let optionElement = document.createElement("option");
+      optionElement.value = option.value;
+      optionElement.innerText = option.label;
+      control.appendChild(optionElement);
+    });
+    control.value = item.defaultValue;
+    control.addEventListener("change", (event) => {
+      const newValue = event.target.value;
+      instance[item.property] = newValue;
+      title.innerText = item.property + ": " + newValue;
+
+      if (item.callback) {
+        item.callback(instance, newValue);
+      }
+    });
+  } else if (item.type === "header") {
+    control = document.createElement("p");
+    control.innerText = item.text;
+    control.className = "header";
+    control.id = "elHeader" + item.text.replace(/\s+/g, '');
+  }
+  else if (item.type === "color") {
+    control = document.createElement("input");
+    control.type = item.type;
+    control.value = item.defaultValue;
+    control.id = "el" + item.property;
+    control.addEventListener("input", (event) => {
+      const newValue = event.target.value;
+      instance[item.property] = newValue;
+      title.innerText = item.property + ": " + newValue;
+    })
+
   }
 
-  control.value = item.defaultValue;
-  control.className = "control";
-  control.id = "el" + item.property;
+  if (item.type != "header") {
+    control.className = "control";
+    control.id = "el" + item.property;
+  }
 
-  const listener = (event) => {
-    const newValue = event.target.value;
-    instance[item.property] =
-      item.type === "range" ? parseInt(newValue, 10) : newValue;
-
-    title.innerText = item.property + ": " + newValue;
-  };
-
-  control.addEventListener("input", listener);
-
-  parentDiv.appendChild(title);
+  if (item.type != "button" && item.type != "header") {
+    parentDiv.appendChild(title);
+  }
   parentDiv.appendChild(control);
 
-  return { element: control, listener };
+  return { element: control };
 }
 
 function drawEyelid(width, x1, y1, colour) {
diff --git a/docs/js/index.js b/docs/js/index.js
index c8c4d75..d373345 100644
--- a/docs/js/index.js
+++ b/docs/js/index.js
@@ -12,13 +12,16 @@ let targetFps = 60;
 let frameDuration = 1000 / targetFps;
 
 let rotation = 0; //was = j = angle
-let paused = true;
+let paused = false;
+let elapsedTime = 0;
+let lastTimestamp = 0;
 render_clear();
 
 let drawObj = null;
 function createInstance(className, args) {
   const classMap = {
     NewWave: NewWave,
+    RaysInShape: RaysInShape,
     PolyTwistColourWidth: PolyTwistColourWidth,
     FloralPhyllo: FloralPhyllo,
     Spiral1: Spiral1,
@@ -65,15 +68,31 @@ updateDrawObj();
 
 function render() {
   setTimeout(() => {
-    requestAnimationFrame(() => {
-      render_clear();
-      if (drawObj) {
-        drawObj.draw(rotation);
-      }
-
+    requestAnimationFrame((timestamp) => {
+      if (!lastTimestamp) lastTimestamp = timestamp;
+      const deltaTime = timestamp - lastTimestamp;
+      const adjustedElapsed = elapsedTime / 100; // Convert to seconds
+      lastTimestamp = timestamp;
+      let adjustedDeltaTime;
       if (!paused) {
         rotation += deg_per_sec / targetFps;
+        elapsedTime += deltaTime;
+        adjustedDeltaTime = deltaTime / 100; // Convert to seconds
+        // console.log(adjustedDeltaTime)
       }
+      // console.log(deltaTime)
+      // console.log(elapsedTime)
+      render_clear();
+      if (drawObj) {
+        // drawObj.draw(rotation);
+
+        drawObj.draw(adjustedElapsed, adjustedDeltaTime);
+
+      }
+
+      ctx.font = "48px serif";
+      ctx.fillStyle = "white"
+      ctx.fillText(Math.floor(elapsedTime) + "ms", centerX - 100, centerY + 400);
       // drawCenter(300)
     });
     render();
diff --git a/docs/js/objects.js b/docs/js/objects.js
index 7d27e41..23b7483 100644
--- a/docs/js/objects.js
+++ b/docs/js/objects.js
@@ -10,7 +10,7 @@ class BaseShape {
       this.controls.push({ element, listener });
     }
 
-    const { element, listener } = addControl({ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier", }, this);
+    const { element, listener } = addControl({ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier" }, this);
     this.controls.push({ element, listener });
   }
 
@@ -18,11 +18,14 @@ class BaseShape {
     this.controls.forEach(({ element, listener }) => {
       if (element && listener) {
         element.removeEventListener("input", listener);
+        element.removeEventListener("click", listener);
       }
       if (element && element.parentElement) {
         element.parentElement.removeChild(element);
         const titleElement = document.getElementById("elText" + element.id.slice(2));
-        titleElement.parentElement.removeChild(titleElement);
+        if (titleElement) {
+          titleElement.parentElement.removeChild(titleElement);
+        }
       }
     });
     this.controls = [];
@@ -672,3 +675,302 @@ class NewWave extends BaseShape {
   }
 }
 
+class RaysInShape extends BaseShape {
+  constructor(rays, speed, speedVert, speedHorr, boxSize, trailLength = 50) {
+    super();
+    this.rays = rays;
+    this.speed = speed;
+    this.speedVert = speedVert;
+    this.speedHorr = speedHorr;
+    this.boxSize = boxSize;
+    this.trailLength = trailLength;
+    this.rayObjects = [];
+    this.centerRays = []; // New array for rays heading to center
+  }
+
+  initialise(config) {
+    for (let item of config) {
+      const { element, listener } = addControl(item, this);
+      this.controls.push({ element, listener });
+    }
+
+    // Add controls for speed multiplier and trail length
+    const { element: speedElement, listener: speedListener } = addControl({
+      type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier"
+    }, this);
+    this.controls.push({ element: speedElement, listener: speedListener });
+
+    const { element: trailElement, listener: trailListener } = addControl({
+      type: "range", min: 5, max: 200, defaultValue: this.trailLength, property: "trailLength"
+    }, this);
+    this.controls.push({ element: trailElement, listener: trailListener });
+
+    // Prepare rayObjects for the first draw
+    this.prepareRayObjects();
+  }
+
+  prepareRayObjects() {
+    this.rayObjects = [];
+    for (let i = 0; i < this.rays; i++) {
+      const angle = (360 / this.rays) * i;
+      this.rayObjects.push({
+        angle: angle,
+        lastX: centerX,
+        lastY: centerY,
+        positions: [{ x: centerX, y: centerY, angle: angle }]
+      });
+    }
+    this.centerRays = []; // Initialize centerRays array
+  }
+
+  createCenterRay(x, y) {
+    // Calculate angle towards center
+    const dx = centerX - x;
+    const dy = centerY - y;
+    const angleToCenter = Math.atan2(dy, dx) * 180 / Math.PI;
+
+    // Create new center-bound ray
+    this.centerRays.push({
+      positions: [{ x: x, y: y }],
+      angle: angleToCenter,
+      reachedCenter: false
+    });
+  }
+
+  updateCenterRays(deltaTime) {
+    const centerThreshold = 5; // Distance threshold to consider "reached center"
+    const maxDistance = 2000;
+
+    // Process each center-bound ray
+    for (let i = 0; i < this.centerRays.length; i++) {
+      const ray = this.centerRays[i];
+
+      // Skip rays that have reached the center
+      if (ray.reachedCenter) {
+        // Remove the oldest position from the trail
+        if (ray.positions.length > 0) {
+          ray.positions.shift();
+        }
+
+        // Remove ray if trail is empty
+        if (ray.positions.length <= 1) {
+          this.centerRays.splice(i, 1);
+          i--;
+          continue;
+        }
+      } else {
+        // Get current position
+        const currentPos = ray.positions[ray.positions.length - 1];
+
+        // Calculate new position
+        const dx = (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
+        const dy = (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
+        const newX = currentPos.x + dx;
+        const newY = currentPos.y + dy;
+
+        // Check if ray has gone too far from origin
+        const distFromOrigin = Math.sqrt(
+          Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
+        );
+
+        // Remove rays that have gone too far
+        if (distFromOrigin > maxDistance) {
+          this.centerRays.splice(i, 1);
+          i--;
+          continue;
+        }
+
+        // Add new position to ray
+        ray.positions.push({ x: newX, y: newY });
+
+        // Check if ray has reached center
+        const distToCenter = Math.sqrt(
+          Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
+        );
+
+        if (distToCenter <= centerThreshold) {
+          ray.reachedCenter = true;
+        }
+
+        // Remove positions beyond trail length
+        while (ray.positions.length > this.trailLength) {
+          ray.positions.shift();
+        }
+      }
+
+      // Draw all segments of the trail
+      ctx.lineWidth = 3;
+      for (let j = 1; j < ray.positions.length; j++) {
+        const prev = ray.positions[j - 1];
+        const curr = ray.positions[j];
+
+        // Fade color based on position in trail (newer = brighter)
+        const alpha = (j / ray.positions.length) * 0.8 + 0.2;
+
+        ctx.beginPath();
+        ctx.moveTo(prev.x, prev.y);
+        ctx.lineTo(curr.x, curr.y);
+
+        // Center-bound rays are pink
+        ctx.strokeStyle = `rgba(255, 51, 170, ${alpha})`;
+        ctx.stroke();
+      }
+    }
+  }
+
+  draw(elapsed, deltaTime) {
+    deltaTime *= this.speedMultiplier / 100;
+
+    // Define the box boundaries
+    const boxLeft = centerX - this.boxSize / 2;
+    const boxRight = centerX + this.boxSize / 2;
+    const boxTop = centerY - this.boxSize / 2;
+    const boxBottom = centerY + this.boxSize / 2;
+
+    // Draw the box boundary for visualization
+    ctx.strokeStyle = "white";
+    ctx.lineWidth = 1;
+    ctx.strokeRect(boxLeft, boxTop, this.boxSize, this.boxSize);
+
+    // Process ray movements and collisions
+    for (let j = 0; j < this.rayObjects.length; j++) {
+      const ray = this.rayObjects[j];
+
+      // Get current position
+      const currentPos = ray.positions[ray.positions.length - 1];
+
+      // Calculate potential new position
+      let dx = (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
+      let dy = (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
+      let newX = currentPos.x + dx;
+      let newY = currentPos.y + dy;
+      let collisionType = null;
+      const oldAngle = ray.angle;
+
+      // Check for horizontal collision
+      if (newX < boxLeft || newX > boxRight) {
+        // Calculate exact collision point with horizontal wall
+        const collisionX = newX < boxLeft ? boxLeft : boxRight;
+        const collisionRatio = (collisionX - currentPos.x) / dx;
+        const collisionY = currentPos.y + dy * collisionRatio;
+
+        // Add collision point to positions array
+        ray.positions.push({
+          x: collisionX,
+          y: collisionY,
+          angle: oldAngle,
+          collision: 'horizontal'
+        });
+
+        // Create a center-bound ray at the collision point
+        this.createCenterRay(collisionX, collisionY);
+
+        // Reflect horizontally
+        ray.angle = 180 - ray.angle;
+        // Normalize angle
+        ray.angle = ((ray.angle % 360) + 360) % 360;
+
+        // Calculate remaining movement after collision
+        const remainingRatio = 1 - collisionRatio;
+        dx = remainingRatio * (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
+        dy = remainingRatio * (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
+        newX = collisionX + dx;
+        newY = collisionY + dy;
+        collisionType = 'horizontal';
+      }
+
+      // Check for vertical collision
+      if (newY < boxTop || newY > boxBottom) {
+        if (collisionType === null) {
+          // Calculate exact collision point with vertical wall
+          const collisionY = newY < boxTop ? boxTop : boxBottom;
+          const collisionRatio = (collisionY - currentPos.y) / dy;
+          const collisionX = currentPos.x + dx * collisionRatio;
+
+          // Add collision point to positions array
+          ray.positions.push({
+            x: collisionX,
+            y: collisionY,
+            angle: oldAngle,
+            collision: 'vertical'
+          });
+
+          // Create a center-bound ray at the collision point
+          this.createCenterRay(collisionX, collisionY);
+
+          // Reflect vertically
+          ray.angle = 360 - ray.angle;
+          // Normalize angle
+          ray.angle = ((ray.angle % 360) + 360) % 360;
+
+          // Calculate remaining movement after collision
+          const remainingRatio = 1 - collisionRatio;
+          dx = remainingRatio * (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
+          dy = remainingRatio * (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
+          newX = collisionX + dx;
+          newY = collisionY + dy;
+        } else {
+          // Second collision in the same frame (corner case)
+          // Simply ensure we stay inside the box
+          newX = Math.max(boxLeft, Math.min(newX, boxRight));
+          newY = Math.max(boxTop, Math.min(newY, boxBottom));
+          ray.positions.push({
+            x: newX,
+            y: newY,
+            angle: ray.angle,
+            collision: 'corner'
+          });
+
+          // Create a center-bound ray at the collision point (corner)
+          this.createCenterRay(newX, newY);
+        }
+      }
+
+      // Ensure rays stay inside the box
+      newX = Math.max(boxLeft, Math.min(newX, boxRight));
+      newY = Math.max(boxTop, Math.min(newY, boxBottom));
+
+      // Add new position to history if there was no collision yet
+      if (collisionType === null) {
+        ray.positions.push({
+          x: newX,
+          y: newY,
+          angle: ray.angle
+        });
+      }
+
+      // Limit positions array to trail length
+      while (ray.positions.length > this.trailLength) {
+        ray.positions.shift();
+      }
+
+      // Draw the trail
+      ctx.lineWidth = 3;
+
+      // Draw all segments of the trail
+      for (let i = 1; i < ray.positions.length; i++) {
+        const prev = ray.positions[i - 1];
+        const curr = ray.positions[i];
+
+        // Fade color based on position in trail (newer = brighter)
+        const alpha = (i / ray.positions.length) * 0.8 + 0.2;
+
+        ctx.beginPath();
+        ctx.moveTo(prev.x, prev.y);
+        ctx.lineTo(curr.x, curr.y);
+
+        // Highlight collision points with different color
+        if (curr.collision) {
+          ctx.strokeStyle = `rgba(255, 255, 0, ${alpha})`;
+        } else {
+          ctx.strokeStyle = `rgba(50, 50, 50, ${alpha})`;  // Changed from pink to gray
+        }
+
+        ctx.stroke();
+      }
+    }
+
+    // Update and draw center-bound rays
+    this.updateCenterRays(deltaTime);
+  }
+}
\ No newline at end of file