From fe74ab9f56cab112c6cff134449f01b7b0527bbc Mon Sep 17 00:00:00 2001 From: Nicky Case Date: Wed, 22 Apr 2020 18:48:22 -0400 Subject: [PATCH] sandbox very modified wow --- icons/e.png | Bin 0 -> 1631 bytes icons/i.png | Bin 0 -> 1760 bytes icons/r.png | Bin 0 -> 1449 bytes icons/s.png | Bin 0 -> 1487 bytes index.html | 2 +- sim/index.html | 296 +++++++++++++++------------ sim/sim.css | 164 ++++++++++++--- sim/sim.js | 528 +++++++++++++++++++++++++++++++++++++++---------- 8 files changed, 729 insertions(+), 261 deletions(-) create mode 100644 icons/e.png create mode 100644 icons/i.png create mode 100644 icons/r.png create mode 100644 icons/s.png diff --git a/icons/e.png b/icons/e.png new file mode 100644 index 0000000000000000000000000000000000000000..909bdf58a1b34151aaf48d9704ddfaf9f3066d09 GIT binary patch literal 1631 zcmV-l2B7(gP)AoFwv>!i(;i|MRGKj!3I!Xfha#=3x23(fY+1am`*mHqJuS4w9?Q}~ z_E6}aD(xa&Xlq*N;({@=)E=TBjcv`eB_W!C9}s2_nMucF%%?_|{e$4lydVF6-uHRm z=Xsx3U=JWk(wThUgTdgl-R@JUv;mLD)0|4BMgcG2296b3Ro4FeR;zU~5D2VR!lP^< zk|do}RdpN~2JDrP0OEj85QLk-U@%-x10@U51l%M&0vTLzxoEZ7=oLjyw6xGzS7%y3 zvAIbo7GpLNVI`U5;o@R;kx4-i#!D4ck`TAsJpqjC&mM<^A(xAxj*e2A$-#q#1s*Ie z5>OQVRUEh}%ks~~wo#N2%?|veZGeuJ7OwX7apKUS5-I0LC>rIb=g(P)#q?LQ)oS$? zV?PCjXj91#*lo47T?70F8BZObiTQtE~lar&1}u$Kz=>D{Cqwl}h>0MBE-6q_wee z@7NYVYhxp~2M3K}d}igW*)QB~_cv%u>@WTO^ortM@+=C68yYxnxAW)H62NB;ha;{i z%HPH^+k{Axk^%pxkxzyEFJ+=jK+-@hawZ+Si9|2B@qIgeHl#IP>n_8->`p{OW zF-fW}(;_e?Nr*NZfL-I7K}Lw?v3dcn_Vw+HaTh{kT^(2Z`t(Py(aX*VQB`#m(r(8= zu#270D~fm=4&W1K(1d79+9<$~%T;}LCBndQR9}uQ09rhOt+uve2e29#>gdpy;|zCw zYIyRTd0fy^tlOSDSnP1sJ$`0W6XvokWZ94ziJF!@$)pNzzFRhA?$MU6E3e z#noP;s;U>rENNy4y?;+~a}$7$BS&bguirz8kl5H@;ll?2Jnii}mh(6q1QZ2TRlPNZ z8uc$O^27c6nP;)7iHBojyFk|*iH!{|`g}yz)-wHlfA%ci0^5SA_iKbd>u?~lw#LJk zFDsy$A2*&p&4|$ZyYb{n!FmOS{4qbD^Um1OZX>dq8#fixUqK;aQ&Umzb_-@UQA~dp z(nl|>q+{b8Tpt<1W^f?ScXe^0ySq%9h4J<2)0{Vi4`Ndj*GEQ7%NjVx1xb?5Xs09u zzxv9&!s=>f$n&ityNST^GLV0jlzZIN7ILt;One>Cs84N?H*$jT$`EU z!Q!HB6Gdfq|3B$zBeE|Hc-z|<>*=XjP$(MZ*4!L^Etn!5wT{cO{9CbYlzWB4hc-Hp z9-)4xlYXbOXi=ePl<9Do>2TOM0swyP3P;|&dN#_wRO5ELKL@U05CrKH{Sz%MdFzH^ zv5fa>fL{=VE9G8+s`#3dcBw$WC3LLB>N4p|G`@0g1?i|*h!L77H(8^U)2Sf%Ef$Mh d=^ZU2{sp<>bMFy|evtqG002ovPDHLkV1j2V2h;!n literal 0 HcmV?d00001 diff --git a/icons/i.png b/icons/i.png new file mode 100644 index 0000000000000000000000000000000000000000..0fe144158c6612598831a585aa5add5dc5be5512 GIT binary patch literal 1760 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 D3l++t literal 0 HcmV?d00001 diff --git a/icons/s.png b/icons/s.png new file mode 100644 index 0000000000000000000000000000000000000000..1041901fcacdb9b36754fd7f18de91116dda9437 GIT binary patch literal 1487 zcmV;=1u*)FP)@89n|!98FY#*?`3 zP1F2q(eD)VK45ou_rcB0%^{Elbl}m1MJ@l+OeT{buCK4Z&!b~zBZgsUp649{&w=zj zB7iPX6+*l*P4nw45187BZ{Q968VK3n-$zwdG)-e?XNUFm^`Yfm*F{Q+ZQD4G!}jXA&#dtG?fuu*UP|haII+?`}_Oka=B?9bB9Kw!TI?)wrvNuy1*ODvi_L#850@t z+d-3l8(@2Tn^LL7&d$!1lH-w5^7{2_+U<65%StAb*-6^ZSR;NYHGy<0mEz#wfP6kb zpW-ppYBlQhI=x;GpeuyPnx<*Tm5DWC7{)`-^DH2(C<>>irwgxzQAjB{IXR)z=|JC% z9@lP08i|I}_VzaAa(QF}zAs$YrC2Nmp%xovLybgDq*5u)&dyL(_5Kvc;5ZJ2LLmsV zk!BLZ3g6t^RPjxe%jK1s08mwxa=A<@l>*RxZFfix-q3aZcl<8)e*7&#J0+9mn}Q`kPxu48wQ`yaRrE@!|ys2M5bjJBF>TEnL^-^XJb1zie)9 zzH=NW46$3G<#}EezlXZ-E_N33`8j_e+)Z7!m zWsu9|g6=rf9)wmFU~g}46}o03qBRQxMuOP;FA&k11xOf%q2aIae&j|zNJMMeFbpk0 zWSP3ksmQbpXicwqo|lFAu#!^Zx-LNMGImh}%VCI*5>3;vZ5z+?vTNfW+G@3U`SN9W zttbj_-@aXd+~K&cOQBE*)*&d0!qL&u*p6uE>^I8Q)zz)*PNzeo(U?POJe5i%G!eY7 zQmKqBH`d6IEXS=EEgHvhW_sUPBmN|9?7c-XtW8Yvz6AZ$%g}!U;Pmt~97TbJq<`Yb z=kvFwNkOh)7!l1~VHm~}KTE>J#l^@1L7`=hHI`)o@xA2Jr%wS8LS)x~FTod%;|v{3 zR8<{0##ojWO{~pCkfn?=?hhM?pf78tX@1ppy$hr*%bGfDY}=+*t4&pA5KYsj9*lz; z1G=VZehtQvCh*+0?Wv6@iZb^cZI%Hx5(Q20(?Pnf=YeV}mEz;ak8_Dy7SEnN3sOXf zmSxov0Lf(X13<6Wqt$AyQr|?hTCKoDs67GTC%mcv@87>)k-W)?Xsiawx^O@gLR6t2 zdy$6p!x8Nz1I-XI;_vGCo-7~8yg!-6B>t z8l`Dj)~|!V89Mt-CX>&BZl}|sSS*IC!&MOMkpvq)zP39IHFAA@{apx=1-k8an?j)w zErAlK{l(iayOJbwImX>CSINwHYu+qZ83 zUxX03xP8R2o1ufdSGin{qobq6DIP+lQlZgkjNPS*^8eYMjxu;)Ad|_E&*$ehB&DQQ zt3?wXUEtWVtclkLvmW87;@1xJYiMt8k4z>ru_-AfS65fGTCHde05tt098;~-ulND+ej4O pl+Hjc-Fc}H%|s$$&3i^G%0Jc{d90iUC6oXF002ovPDHLkV1j^u+?oIY literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 0adcd65..e431999 100644 --- a/index.html +++ b/index.html @@ -69,7 +69,7 @@

Click "Start" play the simulation! You can then change the "generation time", and see how that changes the simulation:

- +

Starts small ("it's just a flu"), then explodes ("oh right, flus don't break hospitals in rich countries"). This is the "J-shaped" exponential growth curve.

diff --git a/sim/index.html b/sim/index.html index 961b15c..053603a 100644 --- a/sim/index.html +++ b/sim/index.html @@ -11,166 +11,200 @@
-
+
+
+ + On average, + each ... +

+ +
+ Infects a every N days +
+ (when almost everyone's still ) +
+ +
+ +
+ + Takes N days to go from to +
+ +
+ +
+ + Recovers in N days +
+ +
+ +
+ + Loses immunity in N years +
+ +
+ +
- On average, - each infected person... -

- -
- Infects a new person every N days - (in the beginning) -
- -
- -
- - Recovers in N days -
- -
- -
- - Loses immunity in N years -
-
-
+
-
+ R0 is + +
-
+ + But R is changed by... +
+ Susceptible population + +
+
- R0 is - -
+ + Personal Hygiene +
+ +
+
+ + Physical Distancing +
+ +
+
+ + Isolating Cases +
+ +
+ Quarantining Contacts +
+ +
+
+ + Deep Cleaning +
+ +
+ Face Masks +
+ +
+
+ + Summer +
+ +
+
+ + Vaccinations +
+ +
+
- - But R is changed by...
- Susceptible population - -
-
- - Personal Hygiene -
- -
-
- - Physical Distancing -
- -
-
- - Isolating Cases -
- -
- Quarantining Contacts -
- -
-
- - Deep Cleaning -
- -
- Face Masks -
- -
-
- - Summer -
- -
-
- - Vaccinations -
- -
-
+ + R is now + + -
+ + Hospital capacity at N% +
+ +
- - R is now - - +
- - Hospital capacity at N% +
+ +
+ + Simulate N years + +
+ +
+
+ in N seconds
- - - -
- -
- -
- - Simulate N years... -
- -
- ...in N seconds -
- - -
+ +
- - - +
+
▶ Start
+
❙❙ Pause
+
▶ Continue
+
↺ Reset
+
+
+
↺ Reset
+
↺ Replay what you just did
+
↺ Reset all sliders
+
- +
+
+ + Susceptible +
Exposed
+
Infectious +
Removed
+ +
+ + + - - - Herd Immunity + + +
+ + +   ––– Healthcare Capacity + +
- --> -
+ + jan + feb + mar + apr + may + jun + jul + aug + sep + oct + nov + dec + + \ No newline at end of file diff --git a/sim/sim.css b/sim/sim.css index 6e4b813..fd1dad8 100644 --- a/sim/sim.css +++ b/sim/sim.css @@ -2,12 +2,19 @@ body{ margin: 0; font-family: Helvetica, Arial, sans-serif; font-size: 15px; + font-weight: normal; background: #ddd; } +div{ + -webkit-user-select: none; /* Chrome all / Safari all */ + -moz-user-select: none; /* Firefox all */ + -ms-user-select: none; /* IE 10+ */ + user-select: none; /* Likely future */ +} #sandbox{ width: 800px; - height: 520px; + height: 540px; overflow: hidden; background: #fff; position: absolute; @@ -24,6 +31,77 @@ body{ float:left; position: relative; } + +#sim_controls{ + margin-top: 10px; +} + +.big_button{ + font-weight: 100; + background: #ff4040; + color: #fff; + padding:10px; + border-radius: 10px; + border-bottom: 4px solid rgba(0,0,0,0.2); + font-size: 30px; + cursor: pointer; +} +.big_button:hover{ + background: #ff6060; +} +.big_button > div{ + display: none; +} +.big_button[label='start'] > #bb_start{ + display: block; +} +.big_button:not([label='start']){ + background: #dddddd; + color: #555; +} +.big_button:not([label='start']):hover{ + background: #dddddd; +} +.big_button[label='continue'] > #bb_continue{ + display: block; +} +.big_button[label='pause'] > #bb_pause{ + display: block; +} +.big_button[label='reset'] > #bb_reset{ + display: block; +} +.small_button{ + text-align: right; + margin-top: 3px; + color: #999; + font-size: 17px; + font-weight: 100; + cursor: pointer; +} +.small_button:hover{ + color: #aaa; +} +.small_button > div{ + display: none; +} +.small_button[label='reset'] > #sb_reset{ + display: block; +} +.small_button[label='replay'] > #sb_replay{ + display: block; +} +.small_button[label='params'] > #sb_params{ + display: block; +} + +hr{ + border:none; + border-bottom: 2px dashed #ddd; + margin: 15px 0; +} + + /* #controls > div{ overflow: hidden; @@ -111,34 +189,66 @@ body{ height:500px; border: 1px solid #ccc; } -#graph > #months{ - color: #bbb; - font-size: 10px; - width: 562.5px; - height: 50px; - text-align: right; - position: relative; - left: -70.5px; - top: 2px; -} -#graph > #months > div{ - color: #bbb; - font-size: 10px; - width: 62.5px; - float: left; - transform-origin: top right; - transform: rotate(-30deg); -} + #graph > #month_ticks{ - width: 562.5px; - height: 5px; - position: relative; - left: -61.5px; - top: -50px; + width: 500px; + height: 15px; + position: absolute; + left: 0; + top: 501px; + color: #bbb; + font-size: 14px; + font-weight: 100; } #graph > #month_ticks > div{ - width: 61.5px; + width: 100px; height: 100%; - border-right: 1px solid #bbb; - float: left; + border-left: 1px solid #bbb; + position: absolute; +} +#graph > #month_ticks > div > span{ + position: absolute; + left:7px; + top:5px; +} + +icon{ + display: inline-block; + width: 1em; + height: 1em; + position: relative; + top:0.1em; + background-size: 100% 100%; +} +icon[s]{ + background-image: url(../icons/s.png); +} +icon[e]{ + background-image: url(../icons/e.png); +} +icon[i]{ + background-image: url(../icons/i.png); +} +icon[r]{ + background-image: url(../icons/r.png); +} + +#legend{ + position: absolute; + top: 10px; + left: 10px; + /* font-size: 17px; */ + font-weight: 100; + line-height: 1.4em; +} + +#hide_on_first_playthrough{ + transition: height 0.5s ease-in-out; + overflow: hidden; + height:0px; + padding-right: 2px; +} + +#month_names{ + display:none; } \ No newline at end of file diff --git a/sim/sim.js b/sim/sim.js index fb66746..48a96bc 100644 --- a/sim/sim.js +++ b/sim/sim.js @@ -8,61 +8,6 @@ window.$all = (query,el=document)=>{ return [...el.querySelectorAll(query)]; }; -///////////////////////////////////// -// PARAMETERS /////////////////////// -///////////////////////////////////// - -let S,I,R; - -let params = {}; - -// Sliders -let DONT_RECORD_HISTORY = true; -$all('.sim_input').forEach((slider)=>{ - let id = slider.id; - let label = $('#label_'+id); - let isRecordable = slider.classList.contains('recordable'); - let onChange = ()=>{ - - // Change label & param - let val = parseFloat(slider.value); - if(label){ - let digits = parseInt(label.getAttribute('toFixed')); - label.innerHTML = digits ? val.toFixed(digits) : val; - } - params[id] = val; - - // Record history (@ this day) - if(isRecordable){ - if(!DONT_RECORD_HISTORY){ - recordedHistory.push([id, val, Math.round(daysCurrent)]); - } - } - - }; - slider.oninput = onChange; - onChange(); -}); -DONT_RECORD_HISTORY = false; - -// Checkboxes -$all('.sim_checkbox').forEach((checkbox)=>{ - let id = checkbox.id; - let label = $('#label_'+id); - let onChange = ()=>{ - if(label){ - if(!checkbox.checked){ - label.setAttribute("strikethrough","yes"); - }else{ - label.removeAttribute("strikethrough"); - } - } - params[id] = checkbox.checked; - }; - checkbox.oninput = onChange; - onChange(); -}); - ///////////////////////////////////// // UPDATE /////////////////////////// ///////////////////////////////////// @@ -78,18 +23,23 @@ let int = { vaccines: 0 }; +let daysCurrent, daysDrawn, daysTotal, daysPerFrame; +let r0_dom = $('#p_r0'); +let s_dom = $('#p_s'); +let re_dom = $('#p_re'); + let updateModel = (days, fake)=>{ - let real_S=S, real_I=I, real_R=R; + let real_S=S, real_E=E, real_I=I, real_R=R; let transmissionRate = 1/params.p_transmission, + incubationRate = 1/params.p_exposed, recoveryRate = 1/params.p_recovery, immunityLossRate = 1/(params.p_waning*365); // R0 - let r0_dom = $('#p_r0'); r0_dom.value = transmissionRate/recoveryRate; - r0_dom.oninput(); + if(r0_dom.oninput) r0_dom.oninput(); // Transmission affected by interventions @@ -125,23 +75,32 @@ let updateModel = (days, fake)=>{ int.vaccines = params.p_vaccines; // S... - let s_dom = $('#p_s'); if(!s_dom.disabled){ S = parseFloat(s_dom.value); } // Update Model - let newlyInfected; + let newlyExposed; if(params.EXPONENTIAL){ - newlyInfected = I*transmissionRate*days; + newlyExposed = I*transmissionRate*days; }else{ - newlyInfected = S*I*transmissionRate*days; + newlyExposed = S*I*transmissionRate*days; } + + let newlyInfectious = params.c_exposed ? (E*incubationRate*days) : 0; let newlyRecovered = params.c_recovery ? (I*recoveryRate*days) : 0; let newlySusceptible = params.c_waning ? (R*immunityLossRate*days) : 0; - S -= newlyInfected; - I += newlyInfected; + S -= newlyExposed; + E += newlyExposed; + + if(params.c_exposed){ + E -= newlyInfectious; + I += newlyInfectious; + }else{ + I += E; // instant transfer + E = 0; + } I -= newlyRecovered; R += newlyRecovered; @@ -157,19 +116,160 @@ let updateModel = (days, fake)=>{ if(s_dom.disabled){ s_dom.value = S; } - let re_dom = $('#p_re'); - re_dom.value = newlyInfected/newlyRecovered; - re_dom.oninput(); + re_dom.value = newlyExposed/newlyRecovered; + if(re_dom.oninput) re_dom.oninput(); // IF FAKE, UNDO EVERYTHING if(fake){ S = real_S; + E = real_E; I = real_I; R = real_R; } } +///////////////////////////////////// +// PARAMETERS /////////////////////// +///////////////////////////////////// + +let S,E,I,R; + +let params = {}; + +// Update Month Ticks! +let mn = [...$('#month_names').children].map((dom)=>{ + return dom.innerHTML; +}); +let updateMonthTicks = ()=>{ + + let ticks; + + switch(params.p_years){ + case 0.5: + ticks = [ + mn[0]+" 2020", + mn[1]+" 2020", + mn[2]+" 2020", + mn[3]+" 2020", + mn[4]+" 2020", + mn[5]+" 2020", + null + ]; + break; + case 1.0: + ticks = [ + mn[0]+" 2020", + mn[3]+" 2020", + mn[6]+" 2020", + mn[9]+" 2020", + null + ]; + break; + default: + ticks = []; + for(let time=0; time<=params.p_years; time+=0.5){ + if(time%1 != 0){ + ticks.push(null); + }else{ + ticks.push(time+2020); + } + } + ticks[ticks.length-1] = null; + break; + } + + let monthTicksDOM = $('#month_ticks'); + monthTicksDOM.innerHTML = ''; + + ticks.forEach((tick,i)=>{ + + if(i==ticks.length-1 || !tick) return; + + let tickDOM = document.createElement('div'); + let tickSpanDOM = document.createElement('span'); + tickSpanDOM.innerHTML = tick; + tickDOM.style.left = ((i/(ticks.length-1))*500)+"px"; + tickDOM.appendChild(tickSpanDOM); + monthTicksDOM.appendChild(tickDOM); + + }); + + console.log(JSON.stringify(ticks)); + +} + +// Sliders +let DONT_RECORD_HISTORY = true; +let _DO_NOT_RECURSE = true; +let yearsSlider = $('#p_years'); +$all('.sim_input').forEach((slider)=>{ + let id = slider.id; + let label = $('#label_'+id); + let isRecordable = slider.classList.contains('recordable'); + let onChange = ()=>{ + + // Change label & param + let val = parseFloat(slider.value); + if(label){ + let digits = parseInt(label.getAttribute('toFixed')); + label.innerHTML = digits ? val.toFixed(digits) : val; + } + params[id] = val; + + // Record history (@ this day) + if(isRecordable){ + if(!DONT_RECORD_HISTORY){ + recordedHistory.push([id, val, Math.round(daysCurrent)]); + } + } + + // Just to update other random crap + if(!_DO_NOT_RECURSE){ + _DO_NOT_RECURSE = true; + updateModel(1, true); // one day + _DO_NOT_RECURSE = false; + } + + // HACK: If this is years, RESET SIM & CHANGE THE MONTHS + if(slider==yearsSlider){ + if(daysCurrent>0){ + _resetTheSim(); + _updateButtons(); + } + updateMonthTicks(); + } + + // MORE HAX + if(daysCurrent==0){ + sbDOM.setAttribute('label','params'); + } + + }; + slider.oninput = onChange; + onChange(); +}); +DONT_RECORD_HISTORY = false; +_DO_NOT_RECURSE = false; + +// Checkboxes +$all('.sim_checkbox').forEach((checkbox)=>{ + let id = checkbox.id; + let label = $('#label_'+id); + let onChange = ()=>{ + if(label){ + if(!checkbox.checked){ + label.setAttribute("strikethrough","yes"); + }else{ + label.removeAttribute("strikethrough"); + } + } + params[id] = checkbox.checked; + }; + checkbox.oninput = onChange; + onChange(); +}); + ///////////////////////////////////// // DRAW ///////////////////////////// ///////////////////////////////////// @@ -181,7 +281,6 @@ canvas.height = 1000; canvas.style.width = (canvas.width/2)+"px"; canvas.style.height = (canvas.height/2)+"px"; -let daysCurrent, daysDrawn, daysTotal, daysPerFrame; let interventionColors = [ ['hygiene', 'hsl(230,100%,63%)', 0.1], @@ -198,12 +297,28 @@ let draw = ()=>{ // Redraw requestAnimationFrame(draw); - updateModel(1, true); // one day + + // Update SI + if(params._HACK_SHOW_SI_PERCENTS){ + $('#show_percent_s').innerHTML = ': '+(S*100).toFixed(5)+'%'; + $('#show_percent_i').innerHTML = ': '+(I*100).toFixed(5)+'%'; + } // Paused? At the end? if(!IS_PLAYING) return; if(params.FROZEN_IN_TIME) return; - if(daysCurrent > daysTotal) return; + if(daysCurrent > daysTotal){ + IS_PLAYING = false; + _updateButtons(); + return; + } + + // HACK + if(params._HACK_RESET_WHEN_I_100=="ready" && I>=0.99999999){ + params._HACK_RESET_WHEN_I_100 = "go"; + bbDOM.setAttribute('label','reset'); + sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay'); + } // Replay History! if(IS_REPLAYING_HISTORY){ @@ -254,6 +369,12 @@ let draw = ()=>{ ctx.fillStyle = "#cccccc"; ctx.fillRect(0,y,w,h); + // E + y += h; + h = E * canvas.height; + ctx.fillStyle = "#FF9393"; + ctx.fillRect(0,y,w,h); + // I y += h; h = I * canvas.height; @@ -271,10 +392,26 @@ let draw = ()=>{ }); // ICU bed capacity - y = (1-((params.p_hospital/100)*0.02))*canvas.height; - h = 2; - ctx.fillStyle = "#000000"; - ctx.fillRect(0,y,w,h); + if(params.p_hospital){ + y = (1-((params.p_hospital/100)*0.02))*canvas.height; + h = 2; + ctx.fillStyle = "#000000"; + ctx.fillRect(0,y,w,h); + } + + // Herd Immunity + if(params.c_recovery && !params.DO_NOT_SHOW_HERD_IMMUNITY){ + if((daysDrawn/daysTotal)*100 % 1 < 0.5){ + let transmissionRate = 1/params.p_transmission, + recoveryRate = 1/params.p_recovery, + r0 = transmissionRate/recoveryRate; + let herdImmunity = 1 - (1/r0); + y = (1-herdImmunity)*canvas.height; + h = 2; + ctx.fillStyle = "#000000"; + ctx.fillRect(0,y,w,h); + } + } // RESET ctx.setTransform(1,0,0,1,0,0); @@ -291,15 +428,12 @@ requestAnimationFrame(draw); ///////////////////////////////////// let IS_PLAYING = false; -let togglePlaying = ()=>{ - IS_PLAYING = !IS_PLAYING; -}; -$('#start').onclick = togglePlaying; let recordedHistory = []; let IS_REPLAYING_HISTORY = false; let START_S = 0.999, + START_E = 0.001, START_I = 0.001, START_R = 0.000; @@ -307,6 +441,7 @@ let restart = ()=>{ // Model S = START_S; + E = START_E; I = START_I; R = START_R; @@ -320,6 +455,119 @@ let restart = ()=>{ ctx.clearRect(0,0,canvas.width,canvas.height); }; + + + +///////////////////////////////////// +// BIG & SMALL BUTTONS ////////////// +///////////////////////////////////// + +let bbDOM = $('.big_button'); +let sbDOM = $('.small_button'); + +bbDOM.onclick = ()=>{ + if(daysCurrent>daysTotal || params._HACK_RESET_WHEN_I_100=="go"){ + _resetTheSim(); + params._HACK_RESET_WHEN_I_100 = undefined; + }else{ + if(daysCurrent==0){ + restart(); + } + IS_PLAYING = !IS_PLAYING; + } + _updateButtons(); +}; + +let defaultParams = [ + ["p_transmission", 4], + ["p_exposed", 3], + ["p_recovery", 14], + ["p_waning", 1], + ["p_hospital", 100], + ["p_years", 2], + ["p_speed", 30], +]; + +sbDOM.onclick = ()=>{ + if(daysCurrent==0){ + + changeSliders(defaultParams); + changeSliders(CURRENT_STAGE.inputs); + + }else if(daysCurrent>daysTotal){ + _replayTheSim(); + }else{ + _resetTheSim(); + } + _updateButtons(); +}; + +let _updateButtons = ()=>{ + + if(daysCurrent > daysTotal){ + + bbDOM.setAttribute('label','reset'); + sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay'); + + }else if(IS_PLAYING){ + + bbDOM.setAttribute('label','pause'); + sbDOM.setAttribute('label','reset'); + + }else{ + + if(daysCurrent==0){ + bbDOM.setAttribute('label','start'); + sbDOM.setAttribute('label','NONE'); + }else{ + bbDOM.setAttribute('label','continue'); + sbDOM.setAttribute('label','reset'); + } + + } + +}; + +let hofp = $('#hide_on_first_playthrough'); +let _showAllControls = ()=>{ + + let originalHeight = hofp.getBoundingClientRect().height; + if(originalHeight>0) return; + + hofp.style.height = "auto"; + hofp.style.position = "absolute"; + hofp.style.top = "-1000px"; + setTimeout(()=>{ + let newHeight = hofp.getBoundingClientRect().height; + hofp.style.position = ""; + hofp.style.top = ""; + hofp.style.height = originalHeight+"px"; + setTimeout(()=>{ + hofp.style.marginTop = ""; + hofp.style.height = newHeight+"px"; + },20); + },20); +}; +let _hideAllControls = ()=>{ + hofp.style.height = "0px"; +}; + +let _resetTheSim = ()=>{ + _showAllControls(); + IS_REPLAYING_HISTORY = false; + recordedHistory = []; + restart(); + IS_PLAYING = false; +}; +let _replayTheSim = ()=>{ + _hideAllControls(); + IS_REPLAYING_HISTORY = true; + restart(); + IS_PLAYING = true; +}; + + +/* $('#restart').onclick = ()=>{ IS_REPLAYING_HISTORY = false; recordedHistory = []; @@ -329,6 +577,7 @@ $('#replay').onclick = ()=>{ IS_REPLAYING_HISTORY = true; restart(); }; +*/ ///////////////////////////////////// // SIM STAGES /////////////////////// @@ -361,34 +610,71 @@ SB Full Sandbox const STAGES = { + "00": { + hide: [ + "section_dynamics", + "c_recovery","label_c_waning","section_meta_years","c_exposed", + "int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity" + ], + inputs: [ + ["p_transmission",4], + ["p_years",1], + ["p_speed",5], + ["TIME_DELTA", 0.5], + //["EXPONENTIAL",true], + ["CANNOT_REPLAY_HISTORY", true] + ], + checkboxes: [ + ["c_recovery", true], + ["c_exposed",true] + ], + }, + ////////////////////////////////////////// // THE SIMULATION //////////////////////// ////////////////////////////////////////// "01": { - hide: ["section_r","section_meta","label_c_recovery","label_c_waning"], + hide: [ + "section_r","label_c_recovery","label_c_waning","section_meta_years","label_c_exposed", + "label_exposed","label_removed", + "label_herd_immunity","label_capacity" + ], inputs: [ ["p_years",0.5], - ["p_speed",10], + ["p_speed",20], + ["p_hospital", 0], ["TIME_DELTA", 0.1], - ["EXPONENTIAL",true] - ] + ["EXPONENTIAL",true], + ["CANNOT_REPLAY_HISTORY", true], + ["_HACK_RESET_WHEN_I_100","ready"], + ["_HACK_SHOW_SI_PERCENTS",true] + ], + SIR: [0.9999999,0.0000001,0] }, "02": { - hide: ["section_r","section_meta","label_c_recovery","label_c_waning"], + hide: [ + "section_r","section_meta","label_c_recovery","label_c_waning", + "label_exposed","label_removed", + "label_herd_immunity","label_capacity" + ], inputs: [ ["p_years",0.5], - ["p_speed",10], + ["p_speed",20], ["TIME_DELTA", 0.1], - ] + ["p_hospital", 0], + ["_HACK_RESET_WHEN_I_100","ready"], + ["_HACK_SHOW_SI_PERCENTS",true] + ], + SIR: [0.9999999,0.0000001,0] }, "03": { hide: ["section_r","section_meta","label_transmission","label_c_waning","c_recovery"], inputs: [ ["p_years",0.5], - ["p_speed",10], + ["p_speed",20], ["TIME_DELTA", 0.1], ], checkboxes: [ @@ -398,15 +684,25 @@ const STAGES = { }, "04": { - hide: ["section_r","section_meta","label_c_waning","c_recovery"], + hide: [ + "section_r","label_c_waning","c_recovery","label_c_exposed","section_meta_years", + //"section_meta", + "label_exposed", + "label_herd_immunity","label_capacity" + ], inputs: [ ["p_years",0.5], - ["p_speed",10], - ["TIME_DELTA", 0.1], + ["p_speed",20], + ["p_hospital", 0], + //["TIME_DELTA", 0.1], + ["CANNOT_REPLAY_HISTORY", true], + ["DO_NOT_SHOW_HERD_IMMUNITY", true] ], checkboxes: [ ["c_recovery", true] - ] + ], + SIR: [0.9999999,0.0000001,0], + //SHOW_ALL_AT_START: true, }, "05a": { @@ -427,7 +723,7 @@ const STAGES = { "05b": { hide: [ - "section_meta","label_c_waning","c_recovery", + "section_meta","label_c_waning","c_recovery","label_c_exposed", "int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity", "graph", "sim_controls" @@ -661,9 +957,32 @@ const STAGES = { }; +let changeSliders = (idValPair)=>{ + + DONT_RECORD_HISTORY = true; + _DO_NOT_RECURSE = true; + + idValPair.forEach((idValPair)=>{ + let [id,val] = idValPair; + let slider = $('#'+id); + if(slider){ + slider.value = val; + slider.oninput(); + } + params[id] = val; + }); + + DONT_RECORD_HISTORY = false; + _DO_NOT_RECURSE = false; + +}; + +let CURRENT_STAGE; + let setStage = (stageID)=>{ let stage = STAGES[stageID]; + CURRENT_STAGE = stage; // Hide what stage.hide = stage.hide || []; @@ -673,17 +992,7 @@ let setStage = (stageID)=>{ // Sliders stage.inputs = stage.inputs || []; - DONT_RECORD_HISTORY = true; - stage.inputs.forEach((idValPair)=>{ - let [id,val] = idValPair; - let slider = $('#'+id); - if(slider){ - slider.value = val; - slider.oninput(); - } - params[id] = val; - }); - DONT_RECORD_HISTORY = false; + changeSliders(stage.inputs); // Checkboxes stage.checkboxes = stage.checkboxes || []; @@ -706,13 +1015,28 @@ let setStage = (stageID)=>{ // Start SIR if(stage.SIR){ START_S = stage.SIR[0]; + START_E = 0; START_I = stage.SIR[1]; START_R = stage.SIR[2]; } + // Show all? + if(stage.SHOW_ALL_AT_START){ + _showAllControls(); + } + }; let stageParams = new URLSearchParams(location.search); if(stageParams.has('stage')) setStage(stageParams.get('stage')); + + + +///////////////////////////////////// +// INIT! //////////////////////////// +///////////////////////////////////// + restart(); +s_dom.oninput(); +_updateButtons(); \ No newline at end of file