Sklakbaym2.html 94 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123
  1. <!DOCTYPE html>
  2. <html lang="ru">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>КПП-3 · СКУД · Пост охраны</title>
  6. <style>
  7. * { box-sizing: border-box; margin: 0; padding: 0; }
  8. html, body { height: 100%; overflow: hidden; background: #050810;
  9. color: #a8b8c8; font-family: "Courier New", monospace; font-size: 13px; }
  10. .app { height: 100vh; display: flex; flex-direction: column; }
  11. header { display: flex; align-items: center; gap: 20px; padding: 8px 20px;
  12. background: linear-gradient(180deg, #202830, #10161e);
  13. border-bottom: 1px solid #000; flex-shrink: 0; }
  14. .brand { color: #ffd060; font-weight: bold; letter-spacing: 2px;
  15. font-size: 14px; text-shadow: 0 0 10px rgba(255,208,96,.5); }
  16. .brand small { color: #506080; font-weight: normal; letter-spacing: 1px;
  17. font-size: 10px; margin-left: 8px; }
  18. .model-badge { color: #80e0ff; font-size: 11px; letter-spacing: 1px;
  19. border: 1px solid #2a4a6a; padding: 3px 8px; border-radius: 2px;
  20. background: rgba(64,160,224,.08); }
  21. .header-status { display: flex; align-items: center; gap: 10px; margin-left: auto; }
  22. .clock { color: #6090c0; letter-spacing: 2px; font-size: 13px; }
  23. .sys-status { display: flex; align-items: center; gap: 8px;
  24. color: #40d080; font-weight: bold; font-size: 12px; letter-spacing: 1px; }
  25. .sys-status .led { width: 10px; height: 10px; border-radius: 50%;
  26. background: #40d080; box-shadow: 0 0 10px #40d080; }
  27. .sys-status.alarm { color: #ff4040; }
  28. .sys-status.alarm .led { background: #ff4040;
  29. box-shadow: 0 0 15px #ff4040; animation: blink .4s infinite; }
  30. .sys-status.wait { color: #ffb040; }
  31. .sys-status.wait .led { background: #ffb040; box-shadow: 0 0 10px #ffb040; }
  32. .sys-status.pause { color: #60a0e0; }
  33. .sys-status.pause .led { background: #60a0e0; box-shadow: 0 0 10px #60a0e0; }
  34. .sys-status.senior { color: #a0c8ff; }
  35. .sys-status.senior .led { background: #a0c8ff; box-shadow: 0 0 10px #a0c8ff; }
  36. @keyframes blink { 50% { opacity: .2; } }
  37. .log-toggle {
  38. background: linear-gradient(180deg, #1a2028, #101418);
  39. color: #a0c0e0; border: 1px solid #304050;
  40. border-radius: 3px; padding: 5px 12px;
  41. font-family: inherit; font-size: 11px; letter-spacing: 1.2px;
  42. cursor: pointer; transition: all .15s; margin-left: 8px;
  43. }
  44. .log-toggle:hover { border-color: #4080c0; color: #c0e0ff;
  45. box-shadow: 0 0 10px rgba(96,160,224,.3); }
  46. .log-toggle.has-alarm { border-color: #702020; color: #ffb0b0;
  47. animation: blink 1s infinite; }
  48. main { flex: 1; display: grid; grid-template-columns: 1fr 400px; min-height: 0; }
  49. .stage { position: relative; background: #02040a; overflow: hidden; }
  50. .stage canvas { display: block; width: 100%; height: 100%; }
  51. aside { background: #0a0e14; border-left: 1px solid #1a2028;
  52. display: flex; flex-direction: column; overflow-y: auto; }
  53. aside .section { border-bottom: 1px solid #1a2028; flex-shrink: 0; }
  54. aside .section h3 { padding: 8px 14px; background: #080c12; color: #6070a0;
  55. font-size: 10.5px; letter-spacing: 2px; font-weight: normal;
  56. border-bottom: 1px solid #1a2028; }
  57. aside .content { padding: 10px 12px; }
  58. .model-switcher { display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
  59. padding: 10px 12px; }
  60. .model-btn {
  61. background: linear-gradient(180deg, #10161e, #0a0e14);
  62. border: 1px solid #2a3442; border-radius: 3px;
  63. padding: 8px 8px; cursor: pointer; transition: all .15s;
  64. font-family: inherit; color: #8090a8; text-align: left;
  65. font-size: 10.5px; line-height: 1.35;
  66. }
  67. .model-btn:hover { border-color: #4080c0; background: #0f1822; color: #c0d0e0; }
  68. .model-btn.active {
  69. border-color: #40d080; color: #c0f0d0;
  70. background: linear-gradient(180deg, #10201a, #0a1a14);
  71. box-shadow: 0 0 12px rgba(64,208,128,.25), inset 0 0 8px rgba(64,208,128,.08);
  72. }
  73. .model-btn .mb-code { display: block; color: #ffd060; font-weight: bold;
  74. font-size: 12px; letter-spacing: 1.5px; margin-bottom: 2px; }
  75. .model-btn .mb-name { display: block; color: #80e0ff; font-size: 10px;
  76. margin-bottom: 3px; }
  77. .model-btn .mb-spec { display: block; color: #506080; font-size: 9.5px; }
  78. .model-btn.active .mb-spec { color: #80a0c0; }
  79. .skud-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px;
  80. font-size: 11px; }
  81. .skud-row { display: flex; align-items: center; gap: 6px; padding: 3px 0;
  82. color: #8090a0; }
  83. .skud-led { width: 9px; height: 9px; border-radius: 50%;
  84. background: #1a2028; border: 1px solid #2a3442; flex-shrink: 0;
  85. transition: all .1s; }
  86. .skud-led.on { background: #40d080; box-shadow: 0 0 8px #40d080; border-color: #40d080; }
  87. .skud-led.red { background: #ff4040; box-shadow: 0 0 8px #ff4040; border-color: #ff4040; }
  88. .skud-led.yel { background: #ffb040; box-shadow: 0 0 8px #ffb040; border-color: #ffb040; }
  89. .skud-led.blu { background: #60a0e0; box-shadow: 0 0 8px #60a0e0; border-color: #60a0e0; }
  90. .skud-led.blink { animation: blink .5s infinite; }
  91. .passes-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  92. .pass-card { background: #060a10; border: 1px solid #202836;
  93. border-radius: 3px; padding: 8px 10px; font-size: 11.5px; }
  94. .pass-card .head { color: #506080; font-size: 9.5px; letter-spacing: 1.5px;
  95. margin-bottom: 6px; text-transform: uppercase; }
  96. .pass-card .num { color: #ffd060; font-weight: bold; font-size: 15px;
  97. letter-spacing: 1px; margin-bottom: 4px; }
  98. .pass-card .name { color: #c0d0e0; margin-bottom: 4px; font-size: 11px; line-height: 1.3; }
  99. .pass-card .dept { color: #607890; font-size: 10px; margin-bottom: 6px; }
  100. .pass-card .status { padding: 3px 6px; border-radius: 2px; font-size: 10px;
  101. letter-spacing: 1px; text-align: center; display: inline-block; }
  102. .pass-card .status.ok { background: rgba(64,208,128,.15); color: #40d080;
  103. border: 1px solid rgba(64,208,128,.4); }
  104. .pass-card .status.bad { background: rgba(255,64,64,.15); color: #ff6060;
  105. border: 1px solid rgba(255,64,64,.4); }
  106. .pass-card .status.empty { background: rgba(80,90,110,.1); color: #506080;
  107. border: 1px dashed #303848; }
  108. .decision-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  109. padding: 10px 12px; border-bottom: 1px solid #1a2028; }
  110. button.big { background: linear-gradient(180deg, #1a2028, #101418);
  111. color: #c0d0e0; border: 1px solid #304050;
  112. border-radius: 3px; padding: 12px 10px;
  113. font-family: inherit; font-size: 12.5px; letter-spacing: 1.5px;
  114. cursor: pointer; transition: all .15s; font-weight: bold; }
  115. button.big.allow { border-color: #206030; color: #80e0a0; }
  116. button.big.allow:hover:not(:disabled) {
  117. background: linear-gradient(180deg, #204830, #103020); border-color: #40d080; }
  118. button.big.deny { border-color: #702020; color: #ffb0b0; }
  119. button.big.deny:hover:not(:disabled) {
  120. background: linear-gradient(180deg, #401818, #200808); border-color: #ff4040; }
  121. button.big:disabled { opacity: .3; cursor: not-allowed; }
  122. .action-buttons { padding: 10px 12px; display: flex; flex-direction: column;
  123. gap: 6px; border-bottom: 1px solid #1a2028; }
  124. button.ctrl { background: linear-gradient(180deg, #1a2028, #101418);
  125. color: #c0d0e0; border: 1px solid #304050;
  126. border-radius: 2px; padding: 8px 12px;
  127. font-family: inherit; font-size: 11.5px; letter-spacing: .5px;
  128. cursor: pointer; transition: all .15s; }
  129. button.ctrl:hover:not(:disabled) { background: linear-gradient(180deg, #243040, #182028);
  130. border-color: #4080c0; }
  131. button.ctrl:disabled { opacity: .35; cursor: not-allowed; }
  132. button.ctrl.primary { border-color: #4060a0; color: #a0d0ff; }
  133. button.ctrl.danger { border-color: #702020; color: #ffb0b0; }
  134. button.ctrl.green { border-color: #206030; color: #80e0a0; }
  135. button.ctrl.yel { border-color: #706020; color: #ffd060; }
  136. button.ctrl.red { border-color: #702020; color: #ffb0b0; }
  137. button.ctrl.active { background: linear-gradient(180deg, #2a3a4a, #1a2028);
  138. border-color: #60a0e0; color: #c0e0ff; box-shadow: 0 0 8px rgba(96,160,224,.4); }
  139. .ctrl-panel-grid { display: grid; grid-template-columns: 1fr 1fr 1fr;
  140. gap: 6px; padding: 10px 12px; }
  141. .ctrl-panel-grid button { padding: 10px 4px; font-size: 11px;
  142. letter-spacing: 1px; font-weight: bold; }
  143. .mode-row { display: flex; gap: 6px; padding: 0 12px 10px; }
  144. .mode-row button { flex: 1; padding: 7px 4px; font-size: 10.5px; }
  145. .cond-row { display: flex; align-items: center; gap: 8px;
  146. padding: 6px 0; font-size: 11.5px; color: #8090a0; }
  147. .cond-row label { min-width: 60px; color: #607890; }
  148. .cond-row select {
  149. flex: 1; background: #060a10; color: #c0d0e0;
  150. border: 1px solid #304050; border-radius: 2px;
  151. padding: 5px 8px; font-family: inherit; font-size: 11.5px;
  152. cursor: pointer; }
  153. .log { overflow-y: auto; padding: 8px 12px;
  154. font-size: 10.5px; line-height: 1.7; }
  155. .log .entry { color: #8090a8; }
  156. .log .entry.warn { color: #ffb040; }
  157. .log .entry.alarm { color: #ff4040; font-weight: bold; }
  158. .log .entry.ok { color: #40d080; }
  159. .log .entry.info { color: #60a0e0; }
  160. .log .entry.senior { color: #a0c8ff; font-weight: bold; }
  161. .log .entry.skud { color: #80e0ff; }
  162. .log .entry .time { color: #405060; margin-right: 6px; }
  163. .log-full { max-height: 60vh; min-height: 300px;
  164. background: #060a10; border: 1px solid #1a2028; border-radius: 3px;
  165. padding: 12px 14px; font-size: 11.5px; line-height: 1.8; }
  166. .hint { padding: 8px 12px; background: rgba(96,160,224,.08);
  167. border-left: 2px solid #4080c0; color: #a0c0e0;
  168. font-size: 11px; line-height: 1.5; }
  169. .hint b { color: #ffd060; }
  170. .modal { position: fixed; inset: 0; background: rgba(0,0,0,.85);
  171. display: none; align-items: center; justify-content: center;
  172. z-index: 100; padding: 20px; }
  173. .modal.on { display: flex; }
  174. .modal-content {
  175. background: linear-gradient(180deg, #1a2230, #0d141c);
  176. border: 2px solid #3a4858;
  177. border-radius: 4px;
  178. box-shadow: 0 0 0 1px #000, 0 20px 60px rgba(0,0,0,.9);
  179. padding: 24px 28px; width: min(640px, 100%);
  180. max-height: 90vh; overflow-y: auto;
  181. color: #c0d0e0; font-family: "Courier New", monospace; }
  182. .modal-content h2 {
  183. color: #ffd060; font-size: 16px; letter-spacing: 2px;
  184. margin-bottom: 20px; text-align: center;
  185. text-shadow: 0 0 10px rgba(255,208,96,.4); }
  186. .modal-content .row {
  187. display: flex; justify-content: space-between;
  188. padding: 8px 0; border-bottom: 1px solid #1a2028;
  189. font-size: 12.5px; }
  190. .modal-content .row:last-of-type { border-bottom: none; }
  191. .modal-content .row .k { color: #8090a0; }
  192. .modal-content .row .v { color: #a0d0ff; font-weight: bold; }
  193. .modal-content .row .v.good { color: #40d080; }
  194. .modal-content .row .v.bad { color: #ff6060; }
  195. .modal-content button {
  196. display: block; margin: 20px auto 0;
  197. padding: 10px 30px; font-family: inherit;
  198. font-size: 13px; letter-spacing: 1.5px;
  199. background: linear-gradient(180deg, #1a2028, #101418);
  200. color: #c0d0e0; border: 1px solid #4060a0;
  201. border-radius: 3px; cursor: pointer; }
  202. .modal-content button:hover { background: linear-gradient(180deg, #243040, #182028); }
  203. .refuse-menu {
  204. display: none; padding: 10px 12px;
  205. background: rgba(200,80,80,.08);
  206. border-top: 1px solid rgba(255,64,64,.3);
  207. border-bottom: 1px solid #1a2028; }
  208. .refuse-menu.on { display: block; }
  209. .refuse-menu .title {
  210. font-size: 10.5px; color: #ffb040; letter-spacing: 1.5px;
  211. margin-bottom: 8px; }
  212. .refuse-menu button.ctrl {
  213. width: 100%; margin-bottom: 5px;
  214. text-align: left; font-size: 11.5px; padding: 7px 10px; }
  215. .refuse-menu button.ctrl:last-child { margin-bottom: 0; }
  216. </style>
  217. </head>
  218. <body>
  219. <div class="app">
  220. <header>
  221. <div class="brand">◤ КПП-3<small>СКУД · ПОСТ ОХРАНЫ</small></div>
  222. <div class="model-badge" id="modelBadge">МОДЕЛЬ: —</div>
  223. <div class="header-status">
  224. <div class="clock" id="clock">--:--:--</div>
  225. <div class="sys-status" id="sysStatus"><span class="led"></span><span id="sysStatusText">ПОСТ СВОБОДЕН</span></div>
  226. <button class="log-toggle" id="btnLogToggle" title="Журнал событий (F2)">☰ ЖУРНАЛ</button>
  227. </div>
  228. </header>
  229. <main>
  230. <div class="stage"><canvas id="cv"></canvas></div>
  231. <aside>
  232. <div class="section">
  233. <h3>МОДЕЛЬ ШЛАГБАУМА · ПЕРЕКЛЮЧАТЕЛЬ</h3>
  234. <div class="model-switcher" id="modelSwitcher"></div>
  235. </div>
  236. <div class="section">
  237. <h3>СТАНЦИЯ СКУД · ИНДИКАЦИЯ</h3>
  238. <div class="content">
  239. <div class="skud-grid">
  240. <div class="skud-row"><span class="skud-led" id="ledPower"></span>ПИТАНИЕ</div>
  241. <div class="skud-row"><span class="skud-led" id="ledArmState"></span>СТРЕЛА</div>
  242. <div class="skud-row"><span class="skud-led" id="ledKW1"></span>КВ-1 (низ)</div>
  243. <div class="skud-row"><span class="skud-led" id="ledKW2"></span>КВ-2 (верх)</div>
  244. <div class="skud-row"><span class="skud-led" id="ledLoop1"></span>ПЕТЛЯ 1</div>
  245. <div class="skud-row"><span class="skud-led" id="ledLoop2"></span>ПЕТЛЯ 2</div>
  246. <div class="skud-row"><span class="skud-led" id="ledPhoto"></span>ФОТОЭЛЕМЕНТ</div>
  247. <div class="skud-row"><span class="skud-led" id="ledImpact"></span>ДАТЧИК УДАРА</div>
  248. <div class="skud-row"><span class="skud-led" id="ledRelayOpen"></span>РЕЛЕ «ОТКР»</div>
  249. <div class="skud-row"><span class="skud-led" id="ledRelayClose"></span>РЕЛЕ «ЗАКР»</div>
  250. <div class="skud-row"><span class="skud-led" id="ledRelayStop"></span>РЕЛЕ «СТОП»</div>
  251. <div class="skud-row"><span class="skud-led" id="ledPass"></span>ПРОПУСК</div>
  252. </div>
  253. </div>
  254. </div>
  255. <div class="section">
  256. <h3>ПУЛЬТ ОПЕРАТОРА</h3>
  257. <div class="ctrl-panel-grid">
  258. <button class="ctrl green" id="btnOpen">▲ ОТКР</button>
  259. <button class="ctrl red" id="btnClose">▼ ЗАКР</button>
  260. <button class="ctrl yel" id="btnStop">■ СТОП</button>
  261. </div>
  262. <div class="mode-row">
  263. <button class="ctrl" id="btnHold">УДЕРЖАНИЕ</button>
  264. <button class="ctrl" id="btnMode">РЕЖИМ: АВТО</button>
  265. <button class="ctrl" id="btnResetAlarm">СБРОС АВАРИИ</button>
  266. </div>
  267. </div>
  268. <div class="section">
  269. <h3>СРАВНЕНИЕ ПРОПУСКОВ</h3>
  270. <div class="content"><div class="passes-compare" id="passesCompare"></div></div>
  271. </div>
  272. <div class="hint" id="hint">Нажмите «Следующая машина».</div>
  273. <div class="decision-buttons">
  274. <button class="big allow" id="btnAllow" disabled>✓ РАЗРЕШИТЬ</button>
  275. <button class="big deny" id="btnDeny" disabled>✕ ЗАПРЕТИТЬ</button>
  276. </div>
  277. <div class="refuse-menu" id="refuseMenu">
  278. <div class="title">МАШИНА ЖДЁТ РЕШЕНИЯ</div>
  279. <button class="ctrl" data-refuse="explain">📢 Объяснить причину отказа</button>
  280. <button class="ctrl" data-refuse="senior">📞 Вызвать старшего смены</button>
  281. </div>
  282. <div class="action-buttons">
  283. <button class="ctrl primary" id="btnNext">▸ Следующая машина</button>
  284. <button class="ctrl danger" id="btnBreach">⚠ Силовой проезд</button>
  285. <button class="ctrl" id="btnNote">✎ Отметить в журнале</button>
  286. <button class="ctrl" id="btnEndShift">⏹ Закончить смену</button>
  287. <button class="ctrl" id="btnReset">↻ Сброс</button>
  288. </div>
  289. <div class="section">
  290. <h3>ВНЕШНИЕ УСЛОВИЯ</h3>
  291. <div class="content">
  292. <div class="cond-row">
  293. <label>Время</label>
  294. <select id="selTimeOfDay">
  295. <option value="day">☀ День</option>
  296. <option value="night" selected>🌙 Ночь</option>
  297. </select>
  298. </div>
  299. <div class="cond-row">
  300. <label>Погода</label>
  301. <select id="selWeather">
  302. <option value="clear" selected>☀ Ясно</option>
  303. <option value="rain">🌧 Дождь</option>
  304. <option value="snow">❄ Снег</option>
  305. <option value="fog">🌫 Туман</option>
  306. </select>
  307. </div>
  308. </div>
  309. </div>
  310. </aside>
  311. </main>
  312. </div>
  313. <div class="modal" id="logModal">
  314. <div class="modal-content" style="width: min(900px, 100%);">
  315. <h2>ЖУРНАЛ СОБЫТИЙ · СКУД</h2>
  316. <div class="log log-full" id="logContentFull"></div>
  317. <div style="display:flex; gap:8px; justify-content:center; margin-top:16px;">
  318. <button id="btnLogClear" style="margin:0;">ОЧИСТИТЬ</button>
  319. <button id="btnLogClose" style="margin:0;">ЗАКРЫТЬ</button>
  320. </div>
  321. </div>
  322. </div>
  323. <div class="modal" id="shiftModal">
  324. <div class="modal-content">
  325. <h2>СМЕНА ЗАВЕРШЕНА</h2>
  326. <div class="row"><span class="k">Впущено автомобилей:</span><span class="v good" id="statIn">0</span></div>
  327. <div class="row"><span class="k">Отказано в проезде:</span><span class="v" id="statOut">0</span></div>
  328. <div class="row"><span class="k">Верных решений:</span><span class="v good" id="statCorrect">0</span></div>
  329. <div class="row"><span class="k">Ошибок оператора:</span><span class="v bad" id="statWrong">0</span></div>
  330. <div class="row"><span class="k">Сработало тревог:</span><span class="v bad" id="statAlarms">0</span></div>
  331. <div class="row"><span class="k">Срабатываний датчиков:</span><span class="v" id="statSensors">0</span></div>
  332. <div class="row"><span class="k">Силовых проездов:</span><span class="v bad" id="statBreaches">0</span></div>
  333. <div class="row"><span class="k">Вызовов старшего:</span><span class="v" id="statSenior">0</span></div>
  334. <div class="row"><span class="k">Оценка смены:</span><span class="v" id="statGrade">—</span></div>
  335. <button id="btnCloseShift">ЗАКРЫТЬ И СБРОСИТЬ</button>
  336. </div>
  337. </div>
  338. <script>
  339. const cv = document.getElementById('cv');
  340. const ctx = cv.getContext('2d');
  341. let W = 0, H = 0;
  342. function resizeCanvas() {
  343. const stage = cv.parentElement;
  344. const dpr = window.devicePixelRatio || 1;
  345. W = stage.clientWidth; H = stage.clientHeight;
  346. cv.width = Math.floor(W * dpr); cv.height = Math.floor(H * dpr);
  347. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  348. }
  349. window.addEventListener('resize', resizeCanvas);
  350. function L() {
  351. return {
  352. horizonY: H * 0.32,
  353. topLaneY: H * 0.58, topLaneTop: H * 0.52, topLaneBot: H * 0.66,
  354. botLaneY: H * 0.78, botLaneTop: H * 0.72, botLaneBot: H * 0.84,
  355. boothX: W * 0.40, boothW: 150, boothH: 165, boothBaseY: H * 0.55,
  356. pillarX: W * 0.72, pillarW: 30, pillarH: 260,
  357. pivotX: W * 0.72, pivotY: H * 0.58 - 80,
  358. armLen: 240, armH: 16
  359. };
  360. }
  361. /* ===== МОДЕЛИ ===== */
  362. const BARRIER_MODELS = {
  363. A: { code:'A', name:'СКОРОСТНОЙ', short:'«Быстрый»',
  364. desc:'Для офисных КПП. Полный цикл ~1 с.',
  365. openTime:0.9, closeTime:1.2, autoClose:3, armLen:180,
  366. hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:true },
  367. B: { code:'B', name:'МАГИСТРАЛЬНЫЙ', short:'«Стандарт»',
  368. desc:'Для въезда на территорию. Петля + фотоэлемент.',
  369. openTime:3.5, closeTime:3.5, autoClose:8, armLen:240,
  370. hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:false, hasKW2:true },
  371. C: { code:'C', name:'ПРОМЫШЛЕННЫЙ', short:'«Терминал»',
  372. desc:'Грузовой транспорт. 2 петли + фотоэлемент + датчик удара.',
  373. openTime:6, closeTime:6, autoClose:15, armLen:280,
  374. hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:true, hasKW2:true },
  375. D: { code:'D', name:'БЮДЖЕТНЫЙ', short:'«Мини»',
  376. desc:'Для внутренних КПП. Без автоматики.',
  377. openTime:4, closeTime:4, autoClose:0, armLen:200,
  378. hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:false }
  379. };
  380. let selectedModelCode = 'B';
  381. const state = {
  382. time: 0,
  383. model: null,
  384. barrier: { level: 0, target: 0, moving: false },
  385. holdMode: false,
  386. manualMode: false,
  387. autoCloseTimer: 0,
  388. car: null,
  389. correctPass: null, presentedPass: null,
  390. phase: 'idle',
  391. userDecision: null, correctDecision: null,
  392. alarm: false, alarmEnd: 0, sparks: [],
  393. police: null, tow: null, towDriver: null, towCable: false, leaving: false,
  394. timeOfDay: 'night',
  395. weather: 'clear',
  396. weatherParticles: [],
  397. senior: null,
  398. skud: {
  399. power: true, kw1: true, kw2: false,
  400. loop1: false, loop2: false,
  401. photo: false, impact: false,
  402. relayOpen: false, relayClose: false, relayStop: false,
  403. passValid: false, alarm: false
  404. },
  405. stats: { in:0, out:0, correct:0, wrong:0, alarms:0, breaches:0, senior:0, sensors:0 }
  406. };
  407. /* ===== ЖУРНАЛ ===== */
  408. const logEntries = [];
  409. function logEvent(text, cls) {
  410. const now = new Date();
  411. const t = String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
  412. logEntries.push({ t, text, cls: cls||'info' });
  413. if (logEntries.length > 200) logEntries.shift();
  414. renderLog();
  415. }
  416. function logSkud(text, cls) { logEvent('[СКУД] ' + text, cls || 'skud'); }
  417. function renderLog() {
  418. const html = logEntries.map(e =>
  419. `<div class="entry ${e.cls}"><span class="time">${e.t}</span>${e.text}</div>`
  420. ).join('');
  421. const el = document.getElementById('logContentFull');
  422. if (el) {
  423. const wasBottom = (el.scrollHeight - el.scrollTop - el.clientHeight) < 30;
  424. el.innerHTML = html;
  425. if (wasBottom) el.scrollTop = el.scrollHeight;
  426. }
  427. updateLogBadge();
  428. }
  429. function updateLogBadge() {
  430. const btn = document.getElementById('btnLogToggle');
  431. if (!btn) return;
  432. const modal = document.getElementById('logModal');
  433. const modalOpen = modal && modal.classList.contains('on');
  434. const hasAlarm = logEntries.slice(-5).some(e => e.cls === 'alarm');
  435. btn.classList.toggle('has-alarm', hasAlarm && !modalOpen);
  436. }
  437. function setStatus(text, cls) {
  438. document.getElementById('sysStatusText').textContent = text;
  439. document.getElementById('sysStatus').className = 'sys-status' + (cls ? ' ' + cls : '');
  440. }
  441. function setHint(text) { document.getElementById('hint').innerHTML = text; }
  442. /* ===== ОСВЕЩЕНИЕ ===== */
  443. function lighting() {
  444. const isDay = state.timeOfDay === 'day';
  445. const w = state.weather;
  446. if (w === 'fog') return isDay ? { mode: 'dawn' } : { mode: 'dusk' };
  447. if (w === 'rain') return isDay ? { mode: 'overcast' } : { mode: 'wetnight' };
  448. if (w === 'snow') return isDay ? { mode: 'overcast' } : { mode: 'snownight' };
  449. return isDay ? { mode: 'day' } : { mode: 'night' };
  450. }
  451. function skyColors() {
  452. const m = lighting().mode;
  453. switch (m) {
  454. case 'day': return ['#3d5d80', '#98c4dc', '#d8e8f2'];
  455. case 'dawn': return ['#2a3a5c', '#7a94b4', '#c4b8b0'];
  456. case 'dusk': return ['#1a1428', '#382a48', '#605060'];
  457. case 'overcast': return ['#48505c', '#6a7280', '#8a94a4'];
  458. case 'night': return ['#02040a', '#0a1220', '#182230'];
  459. case 'wetnight': return ['#060a14', '#0e1626', '#1a2032'];
  460. case 'snownight': return ['#0a0e1a', '#1a2234', '#2a3044'];
  461. }
  462. return ['#02040a', '#0a1220', '#182230'];
  463. }
  464. function isSunVisible() {
  465. const m = lighting().mode;
  466. return m === 'day' || m === 'dawn';
  467. }
  468. function isStarVisible() {
  469. const m = lighting().mode;
  470. return m === 'night' || m === 'wetnight' || m === 'snownight' || m === 'dusk';
  471. }
  472. function isWet() {
  473. return state.weather === 'rain';
  474. }
  475. function weatherLabel() {
  476. const w = { clear:'ясно', rain:'дождь', snow:'снег', fog:'туман' }[state.weather];
  477. const t = state.timeOfDay === 'night' ? 'ночь' : 'день';
  478. return t + ', ' + w;
  479. }
  480. /* ===== ПРОПУСКА ===== */
  481. const firstNames = ['Иванов','Петров','Сидоров','Кузнецов','Морозов','Волков','Соколов','Лебедев'];
  482. const initials = ['И.И.','П.П.','С.С.','А.В.','Д.А.','М.Н.'];
  483. const departments = ['Администрация','Служба охраны','Бухгалтерия','Отдел кадров','ИТ-отдел','Логистика'];
  484. const series = ['А','Б','В','Г'];
  485. function randomPass() {
  486. return {
  487. num: series[Math.floor(Math.random()*series.length)]+'-'+String(Math.floor(Math.random()*900)+100),
  488. name: firstNames[Math.floor(Math.random()*firstNames.length)]+' '+initials[Math.floor(Math.random()*initials.length)],
  489. dept: departments[Math.floor(Math.random()*departments.length)],
  490. valid: Math.random() > 0.25
  491. };
  492. }
  493. function generateScenario(mode) {
  494. const correct = randomPass(); correct.valid = true;
  495. if (mode === 'breach') return { correct, presented: null, shouldAllow: false };
  496. const r = Math.random();
  497. let presented, shouldAllow;
  498. if (r < 0.4) { presented = { ...correct }; shouldAllow = true; }
  499. else if (r < 0.65) { presented = randomPass(); while (presented.num === correct.num) presented = randomPass(); shouldAllow = false; }
  500. else { presented = { ...correct, valid: false }; shouldAllow = false; }
  501. return { correct, presented, shouldAllow };
  502. }
  503. function renderPasses() {
  504. const el = document.getElementById('passesCompare');
  505. const c = state.correctPass, p = state.presentedPass;
  506. const card = (head, pass) => {
  507. if (!pass) return `<div class="pass-card"><div class="head">${head}</div><div class="num">—</div><div class="name">не предъявлен</div><div class="dept">—</div><div class="status empty">НЕТ ДАННЫХ</div></div>`;
  508. return `<div class="pass-card"><div class="head">${head}</div><div class="num">${pass.num}</div><div class="name">${pass.name}</div><div class="dept">${pass.dept}</div><div class="status ${pass.valid?'ok':'bad'}">${pass.valid?'АКТИВЕН':'ОТОЗВАН'}</div></div>`;
  509. };
  510. el.innerHTML = card('В БАЗЕ', c) + card('ПРЕДЪЯВЛЕН', p);
  511. const active = state.phase === 'waiting' && state.userDecision === null;
  512. document.getElementById('btnAllow').disabled = !active;
  513. document.getElementById('btnDeny').disabled = !active;
  514. state.skud.passValid = !!(c && c.valid);
  515. }
  516. function showRefuseMenu() { document.getElementById('refuseMenu').classList.add('on'); }
  517. function hideRefuseMenu() { document.getElementById('refuseMenu').classList.remove('on'); }
  518. function speedMult() {
  519. if (state.weather === 'rain') return 0.75;
  520. if (state.weather === 'snow') return 0.6;
  521. if (state.weather === 'fog') return 0.85;
  522. return 1;
  523. }
  524. /* ===== ПЕРЕКЛЮЧАТЕЛЬ МОДЕЛИ ===== */
  525. function renderModelSwitcher() {
  526. const el = document.getElementById('modelSwitcher');
  527. el.innerHTML = Object.values(BARRIER_MODELS).map(m => `
  528. <button class="model-btn ${m.code===selectedModelCode?'active':''}" data-model="${m.code}">
  529. <span class="mb-code">МОДЕЛЬ ${m.code}</span>
  530. <span class="mb-name">${m.name}</span>
  531. <span class="mb-spec">▸ ${m.openTime}с/${m.closeTime}с · стрела ${m.armLen}</span>
  532. </button>`).join('');
  533. el.querySelectorAll('.model-btn').forEach(b => {
  534. b.onclick = () => switchModel(b.dataset.model);
  535. });
  536. }
  537. function switchModel(code) {
  538. if (!BARRIER_MODELS[code]) return;
  539. selectedModelCode = code;
  540. state.model = BARRIER_MODELS[code];
  541. document.getElementById('modelBadge').textContent = 'МОДЕЛЬ: ' + state.model.code + ' · ' + state.model.name;
  542. document.getElementById('btnHold').style.display = state.model.hasHold ? '' : 'none';
  543. document.getElementById('btnHold').classList.remove('active');
  544. state.holdMode = false;
  545. state.skud.kw1 = state.barrier.level < 0.05;
  546. state.skud.kw2 = state.barrier.level > 0.95;
  547. if (!state.model.hasLoop) state.skud.loop1 = false;
  548. if (!state.model.hasPhoto) state.skud.photo = false;
  549. if (!state.model.hasImpact) state.skud.impact = false;
  550. renderModelSwitcher();
  551. logSkud('▸ ПЕРЕКЛЮЧЕНИЕ МОДЕЛИ: ' + state.model.code + ' · ' + state.model.name, 'warn');
  552. logSkud('Открытие/закрытие: ' + state.model.openTime + 'с / ' + state.model.closeTime + 'с');
  553. logSkud('Автозакрытие: ' + (state.model.autoClose ? state.model.autoClose + ' с' : 'отключено'));
  554. setHint('Модель <b>' + state.model.code + '</b> активна. Нажмите «Следующая машина».');
  555. }
  556. /* ===== СЦЕНАРИИ ===== */
  557. function nextCar() {
  558. if (state.phase !== 'idle') return;
  559. const scen = generateScenario('normal');
  560. state.correctPass = scen.correct;
  561. state.presentedPass = scen.presented;
  562. state.correctDecision = scen.shouldAllow ? 'allow' : 'deny';
  563. state.userDecision = null;
  564. state.senior = null;
  565. state.phase = 'approaching';
  566. hideRefuseMenu();
  567. const colors = ['#3a5a8a','#8a3a3a','#2a2a30','#3a6a4a','#7a6a2a','#5a3a6a'];
  568. state.car = {
  569. x: -250, speed: 160 * speedMult(),
  570. color: colors[Math.floor(Math.random()*colors.length)],
  571. wheelAngle: 0, isPriora: false
  572. };
  573. state.skud.loop1 = false; state.skud.loop2 = false;
  574. setStatus('ПРИБЛИЖАЕТСЯ АВТО', 'wait');
  575. logEvent('Приближается автомобиль' + (state.weather !== 'clear' || state.timeOfDay === 'night' ? ` (${weatherLabel()})` : ''), 'info');
  576. renderPasses();
  577. setHint('Автомобиль подъезжает.');
  578. }
  579. function breachCar() {
  580. if (state.phase !== 'idle') return;
  581. const scen = generateScenario('breach');
  582. state.correctPass = scen.correct;
  583. state.presentedPass = null;
  584. state.correctDecision = 'deny';
  585. state.userDecision = null;
  586. state.senior = null;
  587. state.phase = 'approaching';
  588. hideRefuseMenu();
  589. state.car = { x: -250, speed: 340 * speedMult(), color: '#0a0a0a', wheelAngle: 0, isPriora: true };
  590. setStatus('НАРУШИТЕЛЬ !', 'alarm');
  591. logEvent('Тёмный автомобиль не сбрасывает скорость!', 'alarm');
  592. renderPasses();
  593. setHint('Нарушитель не останавливается.');
  594. }
  595. function decide(decision) {
  596. if (state.phase !== 'waiting' || state.userDecision !== null) return;
  597. state.userDecision = decision;
  598. const correct = (decision === state.correctDecision);
  599. if (correct) state.stats.correct++; else state.stats.wrong++;
  600. logEvent(`Решение «${decision==='allow'?'РАЗРЕШИТЬ':'ЗАПРЕТИТЬ'}» — ${correct?'верно':'ОШИБКА'}`,
  601. correct?'ok':'warn');
  602. if (decision === 'allow') {
  603. state.car.action = 'pass';
  604. openBarrier();
  605. state.phase = 'resolved';
  606. setStatus('ПРОЕЗД РАЗРЕШЁН');
  607. setHint('Шлагбаум открывается, автомобиль проезжает.');
  608. hideRefuseMenu();
  609. } else {
  610. state.car.action = 'wait';
  611. state.phase = 'refuse_wait';
  612. setStatus('МАШИНА ЖДЁТ', 'pause');
  613. setHint('Объясните причину отказа или вызовите старшего смены.');
  614. showRefuseMenu();
  615. }
  616. renderPasses();
  617. }
  618. /* ===== ПУЛЬТ ОПЕРАТОРА ===== */
  619. function openBarrier() {
  620. if (state.skud.alarm && !state.manualMode) { logSkud('Отказ: авария не сброшена', 'warn'); return; }
  621. state.barrier.target = 1;
  622. state.barrier.moving = true;
  623. state.skud.relayOpen = true;
  624. state.skud.relayClose = false;
  625. state.skud.relayStop = false;
  626. logSkud('РЕЛЕ «ОТКРЫТЬ»: вкл');
  627. state.autoCloseTimer = 0;
  628. }
  629. function closeBarrier() {
  630. if (state.skud.alarm && !state.manualMode) { logSkud('Отказ: авария не сброшена', 'warn'); return; }
  631. if (state.skud.photo) {
  632. logSkud('Отказ закрытия: фотоэлемент разорван', 'warn');
  633. setHint('Фотоэлемент разорван — стрела не опускается.');
  634. return;
  635. }
  636. if (state.holdMode) { logSkud('Отказ: включено удержание', 'warn'); return; }
  637. state.barrier.target = 0;
  638. state.barrier.moving = true;
  639. state.skud.relayClose = true;
  640. state.skud.relayOpen = false;
  641. state.skud.relayStop = false;
  642. logSkud('РЕЛЕ «ЗАКРЫТЬ»: вкл');
  643. }
  644. function stopBarrier() {
  645. state.barrier.target = state.barrier.level;
  646. state.barrier.moving = false;
  647. state.skud.relayStop = true;
  648. state.skud.relayOpen = false;
  649. state.skud.relayClose = false;
  650. logSkud('РЕЛЕ «СТОП»: вкл — привод остановлен', 'warn');
  651. setHint('Шлагбаум остановлен оператором.');
  652. }
  653. function resetAlarm() {
  654. state.skud.alarm = false;
  655. state.alarm = false;
  656. state.skud.impact = false;
  657. logSkud('СБРОС АВАРИИ выполнен', 'ok');
  658. }
  659. document.getElementById('btnOpen').onclick = openBarrier;
  660. document.getElementById('btnClose').onclick = closeBarrier;
  661. document.getElementById('btnStop').onclick = stopBarrier;
  662. document.getElementById('btnResetAlarm').onclick = resetAlarm;
  663. document.getElementById('btnHold').onclick = () => {
  664. if (!state.model.hasHold) return;
  665. state.holdMode = !state.holdMode;
  666. document.getElementById('btnHold').classList.toggle('active', state.holdMode);
  667. logSkud('УДЕРЖАНИЕ: ' + (state.holdMode ? 'включено' : 'выключено'), state.holdMode ? 'warn' : 'info');
  668. };
  669. document.getElementById('btnMode').onclick = () => {
  670. state.manualMode = !state.manualMode;
  671. document.getElementById('btnMode').textContent = 'РЕЖИМ: ' + (state.manualMode ? 'РУЧНОЙ' : 'АВТО');
  672. document.getElementById('btnMode').classList.toggle('active', state.manualMode);
  673. logSkud('Режим: ' + (state.manualMode ? 'РУЧНОЙ' : 'АВТО'));
  674. };
  675. document.querySelectorAll('[data-refuse]').forEach(b => {
  676. b.onclick = () => {
  677. const action = b.dataset.refuse;
  678. if (action === 'explain') {
  679. logEvent('Оператор объяснил причину отказа водителю', 'info');
  680. state.stats.out++;
  681. state.car.action = 'turn';
  682. state.phase = 'resolved';
  683. setStatus('ОТКАЗ', 'wait');
  684. setHint('Машина уезжает в обратном направлении.');
  685. hideRefuseMenu();
  686. renderPasses();
  687. }
  688. if (action === 'senior') {
  689. state.stats.senior++;
  690. logEvent('Оператор вызвал старшего смены', 'senior');
  691. state.phase = 'senior_walk';
  692. const carX = state.car ? state.car.x : 0;
  693. state.senior = { x: W + 40, y: L().botLaneY, targetX: carX + 130, targetY: L().topLaneY,
  694. state: 'walk_to_lane', walking: true, alpha: 0, decideTimer: 0 };
  695. hideRefuseMenu();
  696. setStatus('СТАРШИЙ ВЫЗВАН', 'senior');
  697. setHint('Старший смены направляется к машине.');
  698. }
  699. };
  700. });
  701. function resetAll() {
  702. state.car = null; state.correctPass = null; state.presentedPass = null;
  703. state.userDecision = null; state.correctDecision = null;
  704. state.phase = 'idle'; state.barrier.target = 0; state.barrier.level = 0;
  705. state.alarm = false; state.sparks = []; state.police = null;
  706. state.tow = null; state.towDriver = null; state.towCable = false; state.leaving = false;
  707. state.senior = null; state.holdMode = false; state.autoCloseTimer = 0;
  708. state.skud = { power:true, kw1:true, kw2:false, loop1:false, loop2:false,
  709. photo:false, impact:false, relayOpen:false, relayClose:false, relayStop:false,
  710. passValid:false, alarm:false };
  711. document.getElementById('btnHold').classList.remove('active');
  712. hideRefuseMenu();
  713. logEntries.length = 0; renderLog(); renderPasses();
  714. setStatus('ПОСТ СВОБОДЕН'); setHint('Нажмите <b>«Следующая машина»</b>.');
  715. logEvent('Система сброшена', 'ok');
  716. }
  717. document.getElementById('btnNext').onclick = nextCar;
  718. document.getElementById('btnBreach').onclick = breachCar;
  719. document.getElementById('btnReset').onclick = resetAll;
  720. document.getElementById('btnAllow').onclick = () => decide('allow');
  721. document.getElementById('btnDeny').onclick = () => decide('deny');
  722. document.getElementById('btnNote').onclick = () => {
  723. const txt = prompt('Введите отметку для журнала:', '');
  724. if (txt && txt.trim()) logEvent('[ОПЕРАТОР] ' + txt.trim(), 'info');
  725. };
  726. document.getElementById('btnEndShift').onclick = () => {
  727. const s = state.stats;
  728. document.getElementById('statIn').textContent = s.in;
  729. document.getElementById('statOut').textContent = s.out;
  730. document.getElementById('statCorrect').textContent = s.correct;
  731. document.getElementById('statWrong').textContent = s.wrong;
  732. document.getElementById('statAlarms').textContent = s.alarms;
  733. document.getElementById('statSensors').textContent = s.sensors;
  734. document.getElementById('statBreaches').textContent = s.breaches;
  735. document.getElementById('statSenior').textContent = s.senior;
  736. const total = s.correct + s.wrong;
  737. let grade = '—';
  738. if (total > 0) {
  739. const pct = s.correct / total;
  740. if (pct >= 0.95) grade = 'ОТЛИЧНО';
  741. else if (pct >= 0.8) grade = 'ХОРОШО';
  742. else if (pct >= 0.6) grade = 'УДОВЛЕТВОРИТЕЛЬНО';
  743. else grade = 'ТРЕБУЕТСЯ ПОДГОТОВКА';
  744. }
  745. document.getElementById('statGrade').textContent = grade;
  746. document.getElementById('shiftModal').classList.add('on');
  747. logEvent('Смена завершена. Открыт отчёт.', 'info');
  748. };
  749. document.getElementById('btnCloseShift').onclick = () => {
  750. document.getElementById('shiftModal').classList.remove('on');
  751. state.stats = { in:0, out:0, correct:0, wrong:0, alarms:0, breaches:0, senior:0, sensors:0 };
  752. logEntries.length = 0; renderLog();
  753. logEvent('Начата новая смена', 'ok');
  754. };
  755. /* ===== ЖУРНАЛ: кнопки ===== */
  756. document.getElementById('btnLogToggle').onclick = () => {
  757. document.getElementById('logModal').classList.add('on');
  758. const el = document.getElementById('logContentFull');
  759. el.scrollTop = el.scrollHeight;
  760. updateLogBadge();
  761. };
  762. document.getElementById('btnLogClose').onclick = () => {
  763. document.getElementById('logModal').classList.remove('on');
  764. updateLogBadge();
  765. };
  766. document.getElementById('btnLogClear').onclick = () => {
  767. logEntries.length = 0;
  768. renderLog();
  769. logEvent('Журнал очищен оператором', 'info');
  770. };
  771. document.addEventListener('keydown', (e) => {
  772. if (e.key === 'F2') {
  773. e.preventDefault();
  774. document.getElementById('btnLogToggle').click();
  775. }
  776. if (e.key === 'Escape') {
  777. document.getElementById('logModal').classList.remove('on');
  778. updateLogBadge();
  779. }
  780. });
  781. document.getElementById('selTimeOfDay').onchange = (e) => {
  782. state.timeOfDay = e.target.value;
  783. logEvent('Время суток: ' + (state.timeOfDay === 'day' ? 'день' : 'ночь'), 'info');
  784. if (state.timeOfDay === 'night') logEvent('Камеры переведены в ИК-режим', 'info');
  785. };
  786. document.getElementById('selWeather').onchange = (e) => {
  787. state.weather = e.target.value;
  788. const names = { clear:'ясно', rain:'дождь', snow:'снег', fog:'туман' };
  789. logEvent('Погода: ' + names[state.weather], 'info');
  790. if (state.weather === 'fog') logEvent('Туман — сумеречный режим', 'warn');
  791. if (state.weather === 'rain') logEvent('Мокрое покрытие — снижение скорости ТС', 'warn');
  792. if (state.weather === 'snow') logEvent('Снегопад — ограничение скорости ТС', 'warn');
  793. state.weatherParticles = [];
  794. };
  795. /* ===== LED ===== */
  796. function setLed(id, on, cls) {
  797. const el = document.getElementById(id);
  798. if (!el) return;
  799. el.className = 'skud-led' + (on ? ' ' + (cls || 'on') : '');
  800. }
  801. function updateSkudLeds() {
  802. const s = state.skud;
  803. setLed('ledPower', s.power);
  804. setLed('ledArmState', true,
  805. state.barrier.moving ? 'yel blink' :
  806. (state.barrier.level > 0.9 ? 'on' : 'red'));
  807. setLed('ledKW1', s.kw1);
  808. setLed('ledKW2', s.kw2);
  809. setLed('ledLoop1', s.loop1, 'blu');
  810. setLed('ledLoop2', s.loop2, 'blu');
  811. setLed('ledPhoto', s.photo, 'yel');
  812. setLed('ledImpact', s.impact, 'red blink');
  813. setLed('ledRelayOpen', s.relayOpen);
  814. setLed('ledRelayClose', s.relayClose);
  815. setLed('ledRelayStop', s.relayStop, 'yel');
  816. setLed('ledPass', s.passValid);
  817. }
  818. /* ===== ФОН ===== */
  819. function drawSky() {
  820. const l = L();
  821. const [c1, c2, c3] = skyColors();
  822. const grad = ctx.createLinearGradient(0, 0, 0, l.horizonY);
  823. grad.addColorStop(0, c1); grad.addColorStop(0.6, c2); grad.addColorStop(1, c3);
  824. ctx.fillStyle = grad; ctx.fillRect(0, 0, W, l.horizonY);
  825. if (isStarVisible()) {
  826. for (let i = 0; i < 90; i++) {
  827. const x = (i * 173.5) % W, y = (i * 97.3) % (l.horizonY - 15);
  828. const a = (0.15 + 0.4 * Math.abs(Math.sin(state.time * 0.5 + i))) *
  829. (state.weather === 'clear' ? 1 : 0.35);
  830. ctx.fillStyle = `rgba(200,220,255,${a})`;
  831. ctx.fillRect(x, y, 1.5, 1.5);
  832. }
  833. const mx = W * 0.78, my = l.horizonY * 0.30;
  834. const moonG = ctx.createRadialGradient(mx, my, 3, mx, my, 55);
  835. moonG.addColorStop(0, 'rgba(220,230,255,.55)');
  836. moonG.addColorStop(0.4, 'rgba(180,200,230,.10)');
  837. moonG.addColorStop(1, 'rgba(180,200,230,0)');
  838. ctx.fillStyle = moonG; ctx.fillRect(mx - 60, my - 60, 120, 120);
  839. ctx.beginPath(); ctx.arc(mx, my, 12, 0, Math.PI * 2);
  840. ctx.fillStyle = 'rgba(240,240,220,.85)'; ctx.fill();
  841. ctx.beginPath(); ctx.arc(mx - 4, my - 3, 10, 0, Math.PI * 2);
  842. ctx.fillStyle = 'rgba(230,235,220,.75)'; ctx.fill();
  843. }
  844. if (isSunVisible()) {
  845. const isDawn = lighting().mode === 'dawn';
  846. const sy = l.horizonY * (isDawn ? 0.55 : 0.32);
  847. const sx = W * 0.22;
  848. const sg = ctx.createRadialGradient(sx, sy, 6, sx, sy, 130);
  849. sg.addColorStop(0, isDawn ? 'rgba(255,220,180,.9)' : 'rgba(255,250,210,.95)');
  850. sg.addColorStop(0.3, isDawn ? 'rgba(255,200,140,.25)' : 'rgba(255,240,180,.25)');
  851. sg.addColorStop(1, 'rgba(255,240,180,0)');
  852. ctx.fillStyle = sg; ctx.fillRect(sx - 140, sy - 140, 280, 280);
  853. ctx.beginPath(); ctx.arc(sx, sy, 22, 0, Math.PI * 2);
  854. ctx.fillStyle = isDawn ? 'rgba(255,220,180,.95)' : 'rgba(255,252,220,.98)'; ctx.fill();
  855. ctx.save();
  856. ctx.globalCompositeOperation = 'lighter';
  857. for (let i = 0; i < 8; i++) {
  858. const a = -0.4 + i * 0.1 + Math.sin(state.time * 0.3 + i) * 0.02;
  859. const len = 180 + (i % 3) * 40;
  860. ctx.beginPath();
  861. ctx.moveTo(sx, sy);
  862. ctx.lineTo(sx + Math.cos(a) * len, sy + Math.sin(a) * len);
  863. ctx.lineTo(sx + Math.cos(a + 0.04) * len, sy + Math.sin(a + 0.04) * len);
  864. ctx.closePath();
  865. ctx.fillStyle = isDawn ? 'rgba(255,220,180,.06)' : 'rgba(255,250,220,.05)';
  866. ctx.fill();
  867. }
  868. ctx.restore();
  869. }
  870. const m = lighting().mode;
  871. let bcol = '#0a1220';
  872. if (m === 'day') bcol = '#3a4a5a';
  873. else if (m === 'overcast') bcol = '#454e58';
  874. else if (m === 'dawn') bcol = '#3a4050';
  875. else if (m === 'dusk') bcol = '#1c1a28';
  876. ctx.fillStyle = bcol; ctx.globalAlpha = 0.55;
  877. for (let i = 0; i < 30; i++) {
  878. const bx = i * (W / 30), bw = 20 + (i % 5) * 14, bh = 20 + (i % 7) * 12;
  879. ctx.fillRect(bx, l.horizonY - bh, bw, bh);
  880. }
  881. ctx.globalAlpha = 1;
  882. for (let i = 0; i < 24; i++) {
  883. const bx = i * (W / 24), bw = 18 + (i % 4) * 12, bh = 30 + (i % 5) * 12;
  884. ctx.fillRect(bx, l.horizonY - bh, bw, bh);
  885. if (isStarVisible() && (i * 13) % 5 === 0) {
  886. ctx.fillStyle = 'rgba(255,200,120,.35)';
  887. ctx.fillRect(bx + 4, l.horizonY - bh + 8, 3, 3);
  888. ctx.fillRect(bx + 12, l.horizonY - bh + 20, 3, 3);
  889. ctx.fillStyle = bcol;
  890. }
  891. }
  892. }
  893. function drawGround() {
  894. const l = L();
  895. const m = lighting().mode;
  896. let g;
  897. if (m === 'day' || m === 'overcast') {
  898. g = ctx.createLinearGradient(0, l.horizonY, 0, H);
  899. g.addColorStop(0, '#2a3a28'); g.addColorStop(1, '#101810');
  900. } else if (m === 'dawn' || m === 'dusk') {
  901. g = ctx.createLinearGradient(0, l.horizonY, 0, H);
  902. g.addColorStop(0, '#1a2018'); g.addColorStop(1, '#0a0e08');
  903. } else {
  904. g = ctx.createLinearGradient(0, l.horizonY, 0, H);
  905. g.addColorStop(0, '#0a1410'); g.addColorStop(1, '#03060a');
  906. }
  907. ctx.fillStyle = g; ctx.fillRect(0, l.horizonY, W, H - l.horizonY);
  908. const haze = ctx.createLinearGradient(0, l.horizonY - 4, 0, l.horizonY + 30);
  909. haze.addColorStop(0, 'rgba(80,110,140,.35)');
  910. haze.addColorStop(1, 'rgba(80,110,140,0)');
  911. ctx.fillStyle = haze;
  912. ctx.fillRect(0, l.horizonY - 4, W, 34);
  913. }
  914. function drawRoads() {
  915. const l = L();
  916. const m = lighting().mode;
  917. let c1, c2, edge, dash;
  918. if (m === 'day') {
  919. c1 = ['#3a3e44', '#25282c']; c2 = ['#3a3e44', '#25282c'];
  920. edge = 'rgba(180,180,180,.7)'; dash = 'rgba(220,200,140,.6)';
  921. } else if (m === 'overcast' || m === 'dawn') {
  922. c1 = ['#2a2e34', '#1a1d22']; c2 = ['#2a2e34', '#1a1d22'];
  923. edge = 'rgba(140,150,160,.6)'; dash = 'rgba(200,180,140,.5)';
  924. } else if (m === 'dusk') {
  925. c1 = ['#1a1d24', '#0d1014']; c2 = ['#1a1d24', '#0d1014'];
  926. edge = 'rgba(120,130,140,.5)'; dash = 'rgba(200,180,140,.4)';
  927. } else {
  928. c1 = ['#1a1e24', '#0d1014']; c2 = ['#1a1e24', '#0d1014'];
  929. edge = 'rgba(120,130,140,.5)'; dash = 'rgba(220,200,140,.4)';
  930. }
  931. const g1 = ctx.createLinearGradient(0, l.topLaneTop, 0, l.topLaneBot);
  932. g1.addColorStop(0, c1[0]); g1.addColorStop(1, c1[1]);
  933. ctx.fillStyle = g1; ctx.fillRect(0, l.topLaneTop, W, l.topLaneBot - l.topLaneTop);
  934. const g2 = ctx.createLinearGradient(0, l.botLaneTop, 0, l.botLaneBot);
  935. g2.addColorStop(0, c2[0]); g2.addColorStop(1, c2[1]);
  936. ctx.fillStyle = g2; ctx.fillRect(0, l.botLaneTop, W, l.botLaneBot - l.botLaneTop);
  937. ctx.strokeStyle = edge; ctx.lineWidth = 2;
  938. ctx.beginPath(); ctx.moveTo(0, (l.topLaneBot + l.botLaneTop) / 2);
  939. ctx.lineTo(W, (l.topLaneBot + l.botLaneTop) / 2); ctx.stroke();
  940. ctx.strokeStyle = dash; ctx.lineWidth = 3;
  941. ctx.setLineDash([30, 25]);
  942. ctx.beginPath(); ctx.moveTo(0, (l.topLaneBot + l.botLaneTop) / 2 + 6);
  943. ctx.lineTo(W, (l.topLaneBot + l.botLaneTop) / 2 + 6); ctx.stroke();
  944. ctx.setLineDash([]);
  945. if (isWet()) {
  946. ctx.fillStyle = 'rgba(120,150,200,.06)';
  947. ctx.fillRect(0, l.topLaneTop, W, l.topLaneBot - l.topLaneTop);
  948. ctx.fillRect(0, l.botLaneTop, W, l.botLaneBot - l.botLaneTop);
  949. if (state.car) {
  950. const cx = state.car.x;
  951. const refl = ctx.createLinearGradient(cx, 0, cx, l.topLaneBot);
  952. refl.addColorStop(0, 'rgba(255,240,180,.22)');
  953. refl.addColorStop(1, 'rgba(255,240,180,0)');
  954. ctx.fillStyle = refl;
  955. ctx.fillRect(cx - 90, l.topLaneTop, 180, l.topLaneBot - l.topLaneTop);
  956. }
  957. }
  958. }
  959. function drawBooth() {
  960. const l = L();
  961. const bx = l.boothX, by = l.boothBaseY, bw = l.boothW, bh = l.boothH;
  962. const m = lighting().mode;
  963. const bright = (m === 'day' || m === 'overcast' || m === 'dawn');
  964. ctx.fillStyle = 'rgba(0,0,0,.55)';
  965. ctx.beginPath(); ctx.ellipse(bx + bw/2, by + 6, bw/2 + 14, 9, 0, 0, Math.PI * 2); ctx.fill();
  966. ctx.fillStyle = '#0a0f14'; ctx.fillRect(bx - 8, by, bw + 16, 12);
  967. const wg = ctx.createLinearGradient(bx, 0, bx + bw, 0);
  968. if (bright) { wg.addColorStop(0, '#6a7482'); wg.addColorStop(0.5, '#8a94a4'); wg.addColorStop(1, '#4a5462'); }
  969. else { wg.addColorStop(0, '#2a3442'); wg.addColorStop(0.5, '#4a5a6a'); wg.addColorStop(1, '#182028'); }
  970. ctx.fillStyle = wg; ctx.fillRect(bx, by - bh, bw, bh);
  971. ctx.strokeStyle = '#4a5a70'; ctx.lineWidth = 2; ctx.strokeRect(bx, by - bh, bw, bh);
  972. ctx.fillStyle = '#0a0f14'; ctx.fillRect(bx - 12, by - bh - 14, bw + 24, 14);
  973. ctx.strokeStyle = '#3a4452'; ctx.strokeRect(bx - 12, by - bh - 14, bw + 24, 14);
  974. ctx.fillStyle = 'rgba(255,255,255,.08)';
  975. ctx.fillRect(bx - 12, by - bh - 14, bw + 24, 4);
  976. const glow = bright ? 0.4 : 0.8 + 0.15 * Math.sin(state.time * 1.5);
  977. ctx.fillStyle = `rgba(255,220,140,${glow})`;
  978. ctx.fillRect(bx + 15, by - bh + 25, 60, 60);
  979. const wgl = ctx.createLinearGradient(bx + 15, by - bh + 25, bx + 75, by - bh + 85);
  980. wgl.addColorStop(0, 'rgba(255,255,255,.28)');
  981. wgl.addColorStop(0.5, 'rgba(255,255,255,0)');
  982. wgl.addColorStop(1, 'rgba(255,255,255,.10)');
  983. ctx.fillStyle = wgl; ctx.fillRect(bx + 15, by - bh + 25, 60, 60);
  984. ctx.strokeStyle = '#4a5a70'; ctx.strokeRect(bx + 15, by - bh + 25, 60, 60);
  985. ctx.beginPath(); ctx.moveTo(bx + 45, by - bh + 25); ctx.lineTo(bx + 45, by - bh + 85); ctx.stroke();
  986. ctx.beginPath(); ctx.moveTo(bx + 15, by - bh + 55); ctx.lineTo(bx + 75, by - bh + 55); ctx.stroke();
  987. const oGlow = ctx.createRadialGradient(bx + 45, by - bh + 55, 8, bx + 45, by - bh + 55, 60);
  988. oGlow.addColorStop(0, `rgba(255,220,140,${0.18 * glow + 0.08})`);
  989. oGlow.addColorStop(1, 'rgba(255,220,140,0)');
  990. ctx.fillStyle = oGlow; ctx.fillRect(bx + 15 - 60, by - bh + 25 - 60, 180, 180);
  991. ctx.fillStyle = '#1a2028'; ctx.fillRect(bx + bw - 45, by - bh + 25, 35, bh - 25);
  992. ctx.strokeStyle = '#4a5a70'; ctx.strokeRect(bx + bw - 45, by - bh + 25, 35, bh - 25);
  993. ctx.fillStyle = '#e8c070';
  994. ctx.beginPath(); ctx.arc(bx + bw - 40, by - bh/2 - 5, 2.6, 0, Math.PI * 2); ctx.fill();
  995. ctx.fillStyle = 'rgba(255,255,255,.5)';
  996. ctx.beginPath(); ctx.arc(bx + bw - 41, by - bh/2 - 6, 1.1, 0, Math.PI * 2); ctx.fill();
  997. ctx.fillStyle = '#80a0c0'; ctx.font = 'bold 11px "Courier New"';
  998. ctx.textAlign = 'center';
  999. ctx.fillText('ОХРАНА', bx + bw/2, by - bh - 22);
  1000. ctx.textAlign = 'left';
  1001. }
  1002. function drawGuard(x, y) {
  1003. ctx.fillStyle = 'rgba(0,0,0,.55)';
  1004. ctx.beginPath(); ctx.ellipse(x, y + 2, 14, 4, 0, 0, Math.PI * 2); ctx.fill();
  1005. ctx.fillStyle = '#1a2230';
  1006. ctx.fillRect(x - 8, y - 35, 7, 35); ctx.fillRect(x + 1, y - 35, 7, 35);
  1007. ctx.fillStyle = '#0a0a0a';
  1008. ctx.fillRect(x - 10, y - 3, 10, 5); ctx.fillRect(x, y - 3, 10, 5);
  1009. const bg = ctx.createLinearGradient(x - 11, 0, x + 11, 0);
  1010. bg.addColorStop(0, '#1a3a5e'); bg.addColorStop(0.4, '#3a6a9a'); bg.addColorStop(1, '#1a3050');
  1011. ctx.fillStyle = bg; ctx.fillRect(x - 11, y - 68, 22, 36);
  1012. ctx.strokeStyle = '#0a1a30'; ctx.lineWidth = 1; ctx.strokeRect(x - 11, y - 68, 22, 36);
  1013. ctx.fillStyle = '#e8e8f0';
  1014. ctx.fillRect(x - 11, y - 68, 5, 3); ctx.fillRect(x + 6, y - 68, 5, 3);
  1015. ctx.fillStyle = '#0a0a0a'; ctx.fillRect(x - 11, y - 38, 22, 4);
  1016. ctx.fillStyle = '#1a3a5e';
  1017. ctx.fillRect(x - 16, y - 66, 6, 30); ctx.fillRect(x + 10, y - 66, 6, 30);
  1018. ctx.fillStyle = '#d0a880';
  1019. ctx.fillRect(x - 16, y - 38, 6, 7); ctx.fillRect(x + 10, y - 38, 6, 7);
  1020. ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 72, 6, 4);
  1021. ctx.beginPath(); ctx.arc(x, y - 81, 9, 0, Math.PI * 2);
  1022. const fg = ctx.createRadialGradient(x - 3, y - 83, 2, x, y - 81, 9);
  1023. fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#b08868');
  1024. ctx.fillStyle = fg; ctx.fill();
  1025. ctx.fillStyle = '#1a1a1a';
  1026. ctx.beginPath(); ctx.arc(x - 3, y - 82, 1, 0, Math.PI * 2); ctx.fill();
  1027. ctx.beginPath(); ctx.arc(x + 3, y - 82, 1, 0, Math.PI * 2); ctx.fill();
  1028. ctx.fillStyle = '#0f2340';
  1029. ctx.beginPath(); ctx.arc(x, y - 88, 10, Math.PI, Math.PI * 2); ctx.fill();
  1030. ctx.fillRect(x - 11, y - 88, 22, 3);
  1031. ctx.fillStyle = '#050a15'; ctx.fillRect(x - 12, y - 85, 24, 3);
  1032. ctx.fillStyle = '#c0a040'; ctx.beginPath(); ctx.arc(x, y - 90, 2, 0, Math.PI * 2); ctx.fill();
  1033. ctx.fillStyle = 'rgba(255,220,140,.6)'; ctx.beginPath(); ctx.arc(x - 0.5, y - 90.5, 0.8, 0, Math.PI * 2); ctx.fill();
  1034. }
  1035. function drawBarrier() {
  1036. const l = L();
  1037. const pillarX = l.pillarX, pillarW = l.pillarW, pillarH = l.pillarH;
  1038. const groundBase = l.topLaneY;
  1039. const pillarTopY = groundBase - pillarH;
  1040. const pivotX = pillarX, pivotY = groundBase - 80;
  1041. const armLen = (state.model ? state.model.armLen : 240);
  1042. const armH = l.armH;
  1043. const level = state.barrier.level;
  1044. const m = lighting().mode;
  1045. const bright = (m === 'day' || m === 'overcast' || m === 'dawn');
  1046. ctx.fillStyle = 'rgba(0,0,0,.55)';
  1047. ctx.beginPath(); ctx.ellipse(pillarX + pillarW/2, groundBase + 4, 26, 8, 0, 0, Math.PI * 2); ctx.fill();
  1048. ctx.fillStyle = '#1a1e24'; ctx.fillRect(pillarX - 10, groundBase - 10, pillarW + 20, 16);
  1049. ctx.strokeStyle = '#3a4858'; ctx.lineWidth = 1.5; ctx.strokeRect(pillarX - 10, groundBase - 10, pillarW + 20, 16);
  1050. ctx.fillStyle = 'rgba(255,255,255,.06)';
  1051. ctx.fillRect(pillarX - 10, groundBase - 10, pillarW + 20, 3);
  1052. const g = ctx.createLinearGradient(pillarX, 0, pillarX + pillarW, 0);
  1053. if (bright) { g.addColorStop(0, '#3a4858'); g.addColorStop(0.35, '#7a8898'); g.addColorStop(0.6, '#5a6878'); g.addColorStop(1, '#2a3442'); }
  1054. else { g.addColorStop(0, '#1a2028'); g.addColorStop(0.35, '#4a5a6a'); g.addColorStop(0.6, '#2a3442'); g.addColorStop(1, '#0a1018'); }
  1055. ctx.fillStyle = g; ctx.fillRect(pillarX, pillarTopY, pillarW, pillarH);
  1056. ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.strokeRect(pillarX, pillarTopY, pillarW, pillarH);
  1057. const shine = ctx.createLinearGradient(pillarX + 4, 0, pillarX + 12, 0);
  1058. shine.addColorStop(0, 'rgba(255,255,255,.18)');
  1059. shine.addColorStop(1, 'rgba(255,255,255,0)');
  1060. ctx.fillStyle = shine; ctx.fillRect(pillarX + 4, pillarTopY + 4, 8, pillarH - 8);
  1061. for (let i = 0; i < 4; i++) {
  1062. ctx.fillStyle = i % 2 === 0 ? '#a08030' : '#1a1a1a';
  1063. ctx.fillRect(pillarX, pillarTopY + pillarH - 90 + i * 20, pillarW, 20);
  1064. }
  1065. ctx.fillStyle = 'rgba(255,220,140,.12)';
  1066. ctx.fillRect(pillarX, pillarTopY + pillarH - 90, pillarW, 80);
  1067. ctx.fillStyle = '#505860';
  1068. ctx.beginPath();
  1069. ctx.moveTo(pillarX - 5, pillarTopY); ctx.lineTo(pillarX + pillarW + 5, pillarTopY);
  1070. ctx.lineTo(pillarX + pillarW, pillarTopY - 12); ctx.lineTo(pillarX, pillarTopY - 12);
  1071. ctx.closePath(); ctx.fill();
  1072. ctx.strokeStyle = '#000'; ctx.stroke();
  1073. ctx.fillStyle = 'rgba(255,255,255,.12)';
  1074. ctx.fillRect(pillarX, pillarTopY - 12, pillarW, 3);
  1075. const blink = Math.sin(state.time * 4) > 0;
  1076. const lampColor = level > 0.9 ? '#40d080' : (level > 0.1 ? '#ffb040' : '#ff2020');
  1077. const lampX = pillarX + pillarW/2, lampY = pillarTopY - 26;
  1078. if (blink) {
  1079. const lampGlow = ctx.createRadialGradient(lampX, lampY, 3, lampX, lampY, 40);
  1080. lampGlow.addColorStop(0, lampColor + 'cc');
  1081. lampGlow.addColorStop(0.4, lampColor + '44');
  1082. lampGlow.addColorStop(1, lampColor + '00');
  1083. ctx.fillStyle = lampGlow;
  1084. ctx.fillRect(lampX - 44, lampY - 44, 88, 88);
  1085. }
  1086. ctx.beginPath(); ctx.arc(lampX, lampY, 9, 0, Math.PI * 2);
  1087. const lampGrad = ctx.createRadialGradient(lampX - 3, lampY - 3, 2, lampX, lampY, 9);
  1088. if (blink) {
  1089. lampGrad.addColorStop(0, '#ffffff');
  1090. lampGrad.addColorStop(0.4, lampColor);
  1091. lampGrad.addColorStop(1, lampColor);
  1092. } else {
  1093. lampGrad.addColorStop(0, '#3a3a3a');
  1094. lampGrad.addColorStop(1, '#1a1a1a');
  1095. }
  1096. ctx.fillStyle = lampGrad; ctx.fill();
  1097. ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
  1098. ctx.beginPath(); ctx.arc(lampX - 3, lampY - 3, 2.5, 0, Math.PI * 2);
  1099. ctx.fillStyle = 'rgba(255,255,255,.6)'; ctx.fill();
  1100. ctx.beginPath(); ctx.arc(pivotX, pivotY, 15, 0, Math.PI * 2);
  1101. ctx.fillStyle = '#2a3442'; ctx.fill();
  1102. ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
  1103. ctx.beginPath(); ctx.arc(pivotX, pivotY, 6, 0, Math.PI * 2);
  1104. const hg = ctx.createRadialGradient(pivotX - 2, pivotY - 2, 1, pivotX, pivotY, 6);
  1105. hg.addColorStop(0, '#c8d4e0'); hg.addColorStop(1, '#506070');
  1106. ctx.fillStyle = hg; ctx.fill();
  1107. ctx.beginPath(); ctx.arc(pivotX - 3, pivotY - 3, 2, 0, Math.PI * 2);
  1108. ctx.fillStyle = 'rgba(255,255,255,.5)'; ctx.fill();
  1109. const angle = Math.PI + level * Math.PI / 2;
  1110. ctx.save();
  1111. ctx.translate(pivotX, pivotY); ctx.rotate(angle);
  1112. const seg = 14;
  1113. for (let i = 0; i < seg; i++) {
  1114. ctx.fillStyle = i % 2 === 0 ? '#e04040' : '#e8e8e8';
  1115. ctx.fillRect(i * (armLen / seg), -armH/2, armLen / seg, armH);
  1116. }
  1117. ctx.fillStyle = 'rgba(0,0,0,.35)';
  1118. ctx.fillRect(0, armH/2 - 2, armLen, 2);
  1119. ctx.fillStyle = 'rgba(255,255,255,.4)';
  1120. ctx.fillRect(0, -armH/2, armLen, 2);
  1121. ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.strokeRect(0, -armH/2, armLen, armH);
  1122. ctx.beginPath(); ctx.arc(armLen, 0, 12, 0, Math.PI * 2);
  1123. const ng = ctx.createRadialGradient(armLen - 3, -3, 2, armLen, 0, 12);
  1124. ng.addColorStop(0, '#ffffff'); ng.addColorStop(1, '#a8b0b8');
  1125. ctx.fillStyle = ng; ctx.fill();
  1126. ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
  1127. ctx.beginPath(); ctx.arc(armLen, 0, 5, 0, Math.PI * 2);
  1128. ctx.fillStyle = '#8090a0'; ctx.fill();
  1129. ctx.restore();
  1130. if (level < 0.9) {
  1131. const endX = pivotX + Math.cos(angle) * armLen;
  1132. const endY = pivotY + Math.sin(angle) * armLen;
  1133. const blinkE = Math.sin(state.time * 6) > 0;
  1134. if (blinkE) {
  1135. const eGlow = ctx.createRadialGradient(endX, endY, 2, endX, endY, 26);
  1136. eGlow.addColorStop(0, 'rgba(255,64,64,.55)');
  1137. eGlow.addColorStop(1, 'rgba(255,64,64,0)');
  1138. ctx.fillStyle = eGlow;
  1139. ctx.fillRect(endX - 28, endY - 28, 56, 56);
  1140. }
  1141. ctx.beginPath(); ctx.arc(endX, endY, 6, 0, Math.PI * 2);
  1142. ctx.fillStyle = blinkE ? '#ff4040' : '#601010'; ctx.fill();
  1143. ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
  1144. }
  1145. if (state.model && state.model.hasLoop) {
  1146. const loopX = l.pivotX - 280;
  1147. const loopY = l.topLaneY + 10;
  1148. const loopW = 200, loopH = 18;
  1149. ctx.strokeStyle = state.skud.loop1 ? 'rgba(96,180,255,.85)' : 'rgba(96,160,224,.35)';
  1150. ctx.lineWidth = 2;
  1151. ctx.strokeRect(loopX, loopY, loopW, loopH);
  1152. if (state.skud.loop1) {
  1153. ctx.fillStyle = 'rgba(96,180,255,.18)';
  1154. ctx.fillRect(loopX, loopY, loopW, loopH);
  1155. const lg = ctx.createRadialGradient(loopX + loopW/2, loopY + loopH/2, 5, loopX + loopW/2, loopY + loopH/2, loopW/2);
  1156. lg.addColorStop(0, 'rgba(120,200,255,.18)');
  1157. lg.addColorStop(1, 'rgba(120,200,255,0)');
  1158. ctx.fillStyle = lg;
  1159. ctx.fillRect(loopX - 30, loopY - 30, loopW + 60, loopH + 60);
  1160. }
  1161. }
  1162. if (state.model && state.model.hasPhoto) {
  1163. const px1 = l.pivotX - state.model.armLen - 5;
  1164. const px2 = l.pivotX + 55;
  1165. const py = l.topLaneY - 4;
  1166. ctx.fillStyle = '#304050';
  1167. ctx.fillRect(px1 - 3, py - 44, 6, 44);
  1168. ctx.fillRect(px2 - 3, py - 44, 6, 44);
  1169. ctx.strokeStyle = '#506070'; ctx.lineWidth = 1;
  1170. ctx.strokeRect(px1 - 3, py - 44, 6, 44);
  1171. ctx.strokeRect(px2 - 3, py - 44, 6, 44);
  1172. const on = !state.skud.photo;
  1173. const dCol = on ? '#40d080' : '#ff4040';
  1174. if (on) {
  1175. const dg = ctx.createRadialGradient(px1, py - 36, 2, px1, py - 36, 16);
  1176. dg.addColorStop(0, dCol + 'aa'); dg.addColorStop(1, dCol + '00');
  1177. ctx.fillStyle = dg;
  1178. ctx.fillRect(px1 - 18, py - 54, 36, 36);
  1179. ctx.fillRect(px2 - 18, py - 54, 36, 36);
  1180. }
  1181. ctx.fillStyle = dCol;
  1182. ctx.beginPath(); ctx.arc(px1, py - 36, 3.5, 0, Math.PI * 2); ctx.fill();
  1183. ctx.beginPath(); ctx.arc(px2, py - 36, 3.5, 0, Math.PI * 2); ctx.fill();
  1184. ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
  1185. ctx.beginPath(); ctx.arc(px1, py - 36, 3.5, 0, Math.PI * 2); ctx.stroke();
  1186. ctx.beginPath(); ctx.arc(px2, py - 36, 3.5, 0, Math.PI * 2); ctx.stroke();
  1187. if (on) {
  1188. ctx.strokeStyle = 'rgba(64,208,128,.6)';
  1189. ctx.setLineDash([4,4]); ctx.lineWidth = 1.2;
  1190. ctx.beginPath(); ctx.moveTo(px1, py - 36); ctx.lineTo(px2, py - 36); ctx.stroke();
  1191. ctx.setLineDash([]);
  1192. } else {
  1193. ctx.strokeStyle = 'rgba(255,64,64,.5)';
  1194. ctx.setLineDash([2,6]);
  1195. ctx.beginPath(); ctx.moveTo(px1, py - 36); ctx.lineTo(px2, py - 36); ctx.stroke();
  1196. ctx.setLineDash([]);
  1197. }
  1198. }
  1199. }
  1200. /* ===== МАШИНЫ ===== */
  1201. function drawWheels(x, y, dxRear, dxFront, radius, rim, angle) {
  1202. [x - dxRear, x + dxFront].forEach(wx => {
  1203. ctx.beginPath(); ctx.arc(wx, y, radius, 0, Math.PI * 2);
  1204. const tg = ctx.createRadialGradient(wx - radius*0.3, y - radius*0.3, 1, wx, y, radius);
  1205. tg.addColorStop(0, '#2a2a2a'); tg.addColorStop(1, '#000');
  1206. ctx.fillStyle = tg; ctx.fill();
  1207. ctx.beginPath(); ctx.arc(wx, y, radius * 0.62, 0, Math.PI * 2);
  1208. ctx.fillStyle = '#1a2028'; ctx.fill();
  1209. ctx.strokeStyle = rim; ctx.lineWidth = 1.5; ctx.stroke();
  1210. ctx.save(); ctx.translate(wx, y); ctx.rotate(angle);
  1211. ctx.strokeStyle = rim; ctx.lineWidth = 1.4; ctx.lineCap = 'round';
  1212. for (let i = 0; i < 5; i++) {
  1213. const a = (i / 5) * Math.PI * 2;
  1214. ctx.beginPath(); ctx.moveTo(0, 0);
  1215. ctx.lineTo(Math.cos(a) * radius * 0.5, Math.sin(a) * radius * 0.5); ctx.stroke();
  1216. }
  1217. ctx.beginPath(); ctx.arc(0, 0, radius * 0.16, 0, Math.PI * 2);
  1218. ctx.fillStyle = rim; ctx.fill();
  1219. ctx.strokeStyle = '#303840'; ctx.lineWidth = 1; ctx.stroke();
  1220. ctx.restore();
  1221. ctx.beginPath(); ctx.arc(wx - radius*0.35, y - radius*0.35, radius*0.18, 0, Math.PI * 2);
  1222. ctx.fillStyle = 'rgba(255,255,255,.10)'; ctx.fill();
  1223. });
  1224. }
  1225. function drawSedan(x, y, color, wheelAngle) {
  1226. ctx.fillStyle = 'rgba(0,0,0,.55)';
  1227. ctx.beginPath(); ctx.ellipse(x, y + 4, 92, 7, 0, 0, Math.PI * 2); ctx.fill();
  1228. const bg = ctx.createLinearGradient(0, y - 55, 0, y);
  1229. bg.addColorStop(0, color); bg.addColorStop(0.55, color); bg.addColorStop(1, '#050505');
  1230. ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
  1231. ctx.beginPath();
  1232. ctx.moveTo(x + 85, y - 8); ctx.lineTo(x + 78, y - 26); ctx.lineTo(x + 40, y - 30);
  1233. ctx.lineTo(x + 15, y - 52); ctx.lineTo(x - 25, y - 54); ctx.lineTo(x - 55, y - 42);
  1234. ctx.lineTo(x - 78, y - 24); ctx.lineTo(x - 85, y - 8); ctx.lineTo(x - 85, y);
  1235. ctx.lineTo(x + 85, y); ctx.closePath(); ctx.fill(); ctx.stroke();
  1236. ctx.fillStyle = 'rgba(255,255,255,.18)';
  1237. ctx.beginPath();
  1238. ctx.moveTo(x - 70, y - 22); ctx.lineTo(x + 70, y - 22);
  1239. ctx.lineTo(x + 65, y - 26); ctx.lineTo(x - 65, y - 26);
  1240. ctx.closePath(); ctx.fill();
  1241. const gk = ctx.createLinearGradient(0, y - 54, 0, y - 30);
  1242. gk.addColorStop(0, 'rgba(90,140,180,.85)');
  1243. gk.addColorStop(1, 'rgba(30,60,90,.75)');
  1244. ctx.fillStyle = gk;
  1245. ctx.beginPath();
  1246. ctx.moveTo(x + 12, y - 50); ctx.lineTo(x - 22, y - 52); ctx.lineTo(x - 48, y - 40);
  1247. ctx.lineTo(x - 48, y - 30); ctx.lineTo(x + 12, y - 30); ctx.closePath(); ctx.fill();
  1248. ctx.fillStyle = 'rgba(255,255,255,.20)';
  1249. ctx.beginPath();
  1250. ctx.moveTo(x + 8, y - 48); ctx.lineTo(x - 18, y - 50);
  1251. ctx.lineTo(x - 30, y - 44); ctx.lineTo(x - 20, y - 44);
  1252. ctx.closePath(); ctx.fill();
  1253. ctx.strokeStyle = 'rgba(0,0,0,.4)'; ctx.lineWidth = 1;
  1254. ctx.beginPath(); ctx.moveTo(x - 8, y - 30); ctx.lineTo(x - 8, y - 8); ctx.stroke();
  1255. ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 18;
  1256. ctx.beginPath(); ctx.arc(x + 80, y - 14, 4.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
  1257. ctx.fillStyle = 'rgba(255,255,255,.9)';
  1258. ctx.beginPath(); ctx.arc(x + 79, y - 15, 1.8, 0, Math.PI * 2); ctx.fill();
  1259. ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 10;
  1260. ctx.fillRect(x - 85, y - 22, 4, 10); ctx.shadowBlur = 0;
  1261. ctx.fillStyle = 'rgba(255,140,140,.7)';
  1262. ctx.fillRect(x - 84, y - 21, 2, 3);
  1263. drawWheels(x, y, 55, 55, 14, '#a0a8b0', wheelAngle);
  1264. }
  1265. function drawDarkCar(x, y, wheelAngle) {
  1266. ctx.fillStyle = 'rgba(0,0,0,.65)';
  1267. ctx.beginPath(); ctx.ellipse(x, y + 4, 108, 8, 0, 0, Math.PI * 2); ctx.fill();
  1268. const bg = ctx.createLinearGradient(0, y - 58, 0, y);
  1269. bg.addColorStop(0, '#3a3a3a');
  1270. bg.addColorStop(0.25, '#1a1a1a');
  1271. bg.addColorStop(0.7, '#050505');
  1272. bg.addColorStop(1, '#000');
  1273. ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
  1274. ctx.beginPath();
  1275. ctx.moveTo(x - 105, y - 10); ctx.lineTo(x - 100, y - 28);
  1276. ctx.lineTo(x - 80, y - 34); ctx.lineTo(x - 55, y - 42);
  1277. ctx.lineTo(x - 35, y - 54); ctx.lineTo(x + 25, y - 54);
  1278. ctx.lineTo(x + 45, y - 42); ctx.lineTo(x + 55, y - 34);
  1279. ctx.lineTo(x + 92, y - 30); ctx.lineTo(x + 105, y - 12);
  1280. ctx.lineTo(x + 105, y); ctx.lineTo(x - 105, y);
  1281. ctx.closePath(); ctx.fill(); ctx.stroke();
  1282. ctx.strokeStyle = 'rgba(220,230,240,.7)'; ctx.lineWidth = 1.5;
  1283. ctx.beginPath(); ctx.moveTo(x - 100, y - 22); ctx.lineTo(x + 100, y - 22); ctx.stroke();
  1284. ctx.strokeStyle = 'rgba(255,255,255,.25)'; ctx.lineWidth = 1;
  1285. ctx.beginPath(); ctx.moveTo(x - 100, y - 23); ctx.lineTo(x + 100, y - 23); ctx.stroke();
  1286. const gk = ctx.createLinearGradient(0, y - 54, 0, y - 28);
  1287. gk.addColorStop(0, 'rgba(20,40,60,.95)');
  1288. gk.addColorStop(0.5, 'rgba(5,10,20,.95)');
  1289. gk.addColorStop(1, 'rgba(2,4,8,.95)');
  1290. ctx.fillStyle = gk;
  1291. ctx.beginPath();
  1292. ctx.moveTo(x + 45, y - 40); ctx.lineTo(x + 18, y - 40);
  1293. ctx.lineTo(x + 18, y - 30); ctx.lineTo(x + 52, y - 30);
  1294. ctx.closePath(); ctx.fill();
  1295. ctx.beginPath();
  1296. ctx.moveTo(x - 55, y - 40); ctx.lineTo(x - 35, y - 50);
  1297. ctx.lineTo(x - 10, y - 50); ctx.lineTo(x - 10, y - 30);
  1298. ctx.lineTo(x - 55, y - 30); ctx.closePath(); ctx.fill();
  1299. ctx.fillStyle = 'rgba(120,180,220,.35)';
  1300. ctx.beginPath();
  1301. ctx.moveTo(x - 50, y - 42); ctx.lineTo(x - 40, y - 48);
  1302. ctx.lineTo(x - 30, y - 48); ctx.lineTo(x - 40, y - 42);
  1303. ctx.closePath(); ctx.fill();
  1304. ctx.fillStyle = 'rgba(120,180,220,.30)';
  1305. ctx.beginPath();
  1306. ctx.moveTo(x + 22, y - 38); ctx.lineTo(x + 30, y - 38);
  1307. ctx.lineTo(x + 30, y - 32); ctx.lineTo(x + 22, y - 32);
  1308. ctx.closePath(); ctx.fill();
  1309. ctx.fillStyle = '#0a0a0a'; ctx.fillRect(x + 15, y - 50, 4, 20);
  1310. ctx.fillStyle = '#050505';
  1311. ctx.strokeStyle = '#404040';
  1312. ctx.lineWidth = 1;
  1313. ctx.fillRect(x + 82, y - 26, 20, 12);
  1314. ctx.strokeRect(x + 82, y - 26, 20, 12);
  1315. for (let i = 0; i < 3; i++) {
  1316. ctx.strokeStyle = '#505860';
  1317. ctx.beginPath();
  1318. ctx.moveTo(x + 83, y - 24 + i * 4);
  1319. ctx.lineTo(x + 101, y - 24 + i * 4);
  1320. ctx.stroke();
  1321. }
  1322. ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 22;
  1323. ctx.beginPath();
  1324. ctx.moveTo(x + 84, y - 22); ctx.lineTo(x + 100, y - 22);
  1325. ctx.lineTo(x + 100, y - 12); ctx.lineTo(x + 84, y - 12);
  1326. ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0;
  1327. ctx.fillStyle = '#c0e8ff'; ctx.fillRect(x + 86, y - 20, 12, 2);
  1328. ctx.fillStyle = 'rgba(255,255,255,.7)';
  1329. ctx.fillRect(x + 90, y - 21, 8, 1.5);
  1330. ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 18;
  1331. ctx.fillRect(x - 105, y - 24, 6, 14); ctx.shadowBlur = 0;
  1332. ctx.fillStyle = 'rgba(255,160,160,.6)';
  1333. ctx.fillRect(x - 104, y - 23, 3, 4);
  1334. drawWheels(x, y, 62, 58, 17, '#c8d0d8', wheelAngle);
  1335. }
  1336. function drawPoliceCar(x, y, wheelAngle) {
  1337. ctx.fillStyle = 'rgba(0,0,0,.6)';
  1338. ctx.beginPath(); ctx.ellipse(x, y + 4, 105, 8, 0, 0, Math.PI * 2); ctx.fill();
  1339. const bg = ctx.createLinearGradient(0, y - 58, 0, y);
  1340. bg.addColorStop(0, '#f04444');
  1341. bg.addColorStop(0.35, '#c82020');
  1342. bg.addColorStop(0.75, '#700a0a');
  1343. bg.addColorStop(1, '#400404');
  1344. ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
  1345. ctx.beginPath();
  1346. ctx.moveTo(x + 105, y - 10); ctx.lineTo(x + 100, y - 28);
  1347. ctx.lineTo(x + 80, y - 34); ctx.lineTo(x + 55, y - 42);
  1348. ctx.lineTo(x + 35, y - 54); ctx.lineTo(x - 25, y - 54);
  1349. ctx.lineTo(x - 45, y - 42); ctx.lineTo(x - 55, y - 34);
  1350. ctx.lineTo(x - 92, y - 30); ctx.lineTo(x - 105, y - 12);
  1351. ctx.lineTo(x - 105, y); ctx.lineTo(x + 105, y);
  1352. ctx.closePath(); ctx.fill(); ctx.stroke();
  1353. ctx.fillStyle = 'rgba(255,255,255,.20)';
  1354. ctx.beginPath();
  1355. ctx.moveTo(x - 90, y - 20); ctx.lineTo(x + 90, y - 20);
  1356. ctx.lineTo(x + 85, y - 24); ctx.lineTo(x - 85, y - 24);
  1357. ctx.closePath(); ctx.fill();
  1358. const gk = ctx.createLinearGradient(0, y - 54, 0, y - 30);
  1359. gk.addColorStop(0, 'rgba(70,120,170,.85)');
  1360. gk.addColorStop(1, 'rgba(20,40,70,.85)');
  1361. ctx.fillStyle = gk;
  1362. ctx.beginPath();
  1363. ctx.moveTo(x - 45, y - 40); ctx.lineTo(x - 18, y - 40);
  1364. ctx.lineTo(x - 18, y - 30); ctx.lineTo(x - 52, y - 30);
  1365. ctx.closePath(); ctx.fill();
  1366. ctx.beginPath();
  1367. ctx.moveTo(x + 55, y - 40); ctx.lineTo(x + 35, y - 50);
  1368. ctx.lineTo(x + 10, y - 50); ctx.lineTo(x + 10, y - 30);
  1369. ctx.lineTo(x + 55, y - 30); ctx.closePath(); ctx.fill();
  1370. ctx.fillRect(x - 12, y - 30, 25, 10);
  1371. ctx.save();
  1372. ctx.fillStyle = '#ffffff';
  1373. ctx.font = 'bold 11px "Courier New"';
  1374. ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  1375. ctx.shadowColor = 'rgba(0,0,0,.9)'; ctx.shadowBlur = 3;
  1376. ctx.fillText('ПОЛИЦИЯ', x - 40, y - 12);
  1377. ctx.fillText('ПОЛИЦИЯ', x + 30, y - 12);
  1378. ctx.restore();
  1379. ctx.fillStyle = '#1a3a80'; ctx.fillRect(x - 95, y - 6, 190, 3);
  1380. ctx.fillStyle = 'rgba(120,180,255,.4)'; ctx.fillRect(x - 95, y - 6, 190, 1);
  1381. ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 20;
  1382. ctx.fillRect(x - 100, y - 20, 14, 6); ctx.shadowBlur = 0;
  1383. ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 14;
  1384. ctx.fillRect(x + 100, y - 24, 5, 14); ctx.shadowBlur = 0;
  1385. const blink = Math.sin(state.time * 12) > 0;
  1386. ctx.fillStyle = '#1a1a1a';
  1387. ctx.fillRect(x - 30, y - 60, 60, 5);
  1388. if (blink) {
  1389. const rg = ctx.createRadialGradient(x - 15, y - 68, 4, x - 15, y - 68, 50);
  1390. rg.addColorStop(0, 'rgba(255,50,50,.9)');
  1391. rg.addColorStop(1, 'rgba(255,50,50,0)');
  1392. ctx.fillStyle = rg; ctx.fillRect(x - 65, y - 118, 100, 100);
  1393. }
  1394. ctx.fillStyle = blink ? '#ff2020' : '#401010';
  1395. ctx.beginPath();
  1396. ctx.moveTo(x - 30, y - 60); ctx.lineTo(x, y - 60);
  1397. ctx.lineTo(x, y - 70); ctx.lineTo(x - 30, y - 70);
  1398. ctx.closePath(); ctx.fill();
  1399. ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
  1400. if (!blink) {
  1401. const bg2 = ctx.createRadialGradient(x + 15, y - 68, 4, x + 15, y - 68, 50);
  1402. bg2.addColorStop(0, 'rgba(50,120,255,.9)');
  1403. bg2.addColorStop(1, 'rgba(50,120,255,0)');
  1404. ctx.fillStyle = bg2; ctx.fillRect(x - 35, y - 118, 100, 100);
  1405. }
  1406. ctx.fillStyle = !blink ? '#2060ff' : '#101040';
  1407. ctx.beginPath();
  1408. ctx.moveTo(x, y - 60); ctx.lineTo(x + 30, y - 60);
  1409. ctx.lineTo(x + 30, y - 70); ctx.lineTo(x, y - 70);
  1410. ctx.closePath(); ctx.fill();
  1411. ctx.strokeStyle = '#000'; ctx.stroke();
  1412. const beamGrad = ctx.createLinearGradient(x - 105, y - 14, x - 300, y + 10);
  1413. beamGrad.addColorStop(0, 'rgba(255,240,180,.5)');
  1414. beamGrad.addColorStop(1, 'rgba(255,240,180,0)');
  1415. ctx.fillStyle = beamGrad;
  1416. ctx.beginPath();
  1417. ctx.moveTo(x - 105, y - 20); ctx.lineTo(x - 300, y - 28);
  1418. ctx.lineTo(x - 300, y + 20); ctx.lineTo(x - 105, y - 4);
  1419. ctx.closePath(); ctx.fill();
  1420. drawWheels(x, y, 65, 60, 15, '#606060', wheelAngle);
  1421. }
  1422. function drawTowTruck(x, y, wheelAngle, doorOpen) {
  1423. ctx.fillStyle = 'rgba(0,0,0,.55)';
  1424. ctx.beginPath(); ctx.ellipse(x, y + 4, 105, 7, 0, 0, Math.PI * 2); ctx.fill();
  1425. const bg = ctx.createLinearGradient(0, y - 60, 0, y);
  1426. bg.addColorStop(0, '#e8c840'); bg.addColorStop(0.4, '#c8a020'); bg.addColorStop(1, '#705010');
  1427. ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
  1428. ctx.beginPath();
  1429. ctx.moveTo(x - 100, y - 8); ctx.lineTo(x - 100, y - 55);
  1430. ctx.lineTo(x - 55, y - 55); ctx.lineTo(x - 40, y - 25);
  1431. ctx.lineTo(x - 20, y - 25); ctx.lineTo(x - 20, y - 8);
  1432. ctx.lineTo(x - 20, y); ctx.lineTo(x - 100, y);
  1433. ctx.closePath(); ctx.fill(); ctx.stroke();
  1434. const gk = ctx.createLinearGradient(0, y - 50, 0, y - 28);
  1435. gk.addColorStop(0, 'rgba(90,140,180,.85)');
  1436. gk.addColorStop(1, 'rgba(30,60,90,.8)');
  1437. ctx.fillStyle = gk;
  1438. ctx.beginPath();
  1439. ctx.moveTo(x - 95, y - 50); ctx.lineTo(x - 60, y - 50);
  1440. ctx.lineTo(x - 48, y - 28); ctx.lineTo(x - 95, y - 28);
  1441. ctx.closePath(); ctx.fill();
  1442. ctx.fillStyle = 'rgba(255,255,255,.20)';
  1443. ctx.beginPath();
  1444. ctx.moveTo(x - 90, y - 48); ctx.lineTo(x - 75, y - 48);
  1445. ctx.lineTo(x - 80, y - 32); ctx.lineTo(x - 90, y - 32);
  1446. ctx.closePath(); ctx.fill();
  1447. const pg = ctx.createLinearGradient(0, y - 20, 0, y - 8);
  1448. pg.addColorStop(0, '#d0b040'); pg.addColorStop(1, '#705010');
  1449. ctx.fillStyle = pg; ctx.fillRect(x + 20, y - 18, 85, 10);
  1450. ctx.strokeStyle = '#000'; ctx.strokeRect(x + 20, y - 18, 85, 10);
  1451. ctx.fillStyle = 'rgba(255,255,255,.15)';
  1452. ctx.fillRect(x + 20, y - 18, 85, 2);
  1453. ctx.strokeStyle = '#806010'; ctx.lineWidth = 5;
  1454. ctx.beginPath();
  1455. ctx.moveTo(x + 5, y - 18); ctx.lineTo(x + 40, y - 55); ctx.lineTo(x + 80, y - 55); ctx.stroke();
  1456. ctx.strokeStyle = '#c0a020'; ctx.lineWidth = 2;
  1457. ctx.beginPath(); ctx.moveTo(x + 80, y - 55); ctx.lineTo(x + 90, y - 32); ctx.stroke();
  1458. ctx.strokeStyle = '#a0a8b0';
  1459. ctx.beginPath(); ctx.arc(x + 90, y - 28, 4, 0, Math.PI * 1.5); ctx.stroke();
  1460. const blink = Math.sin(state.time * 8) > 0;
  1461. if (blink) {
  1462. const mg = ctx.createRadialGradient(x - 59, y - 59, 3, x - 59, y - 59, 40);
  1463. mg.addColorStop(0, 'rgba(255,180,70,.9)');
  1464. mg.addColorStop(1, 'rgba(255,180,70,0)');
  1465. ctx.fillStyle = mg;
  1466. ctx.fillRect(x - 100, y - 100, 80, 80);
  1467. }
  1468. ctx.fillStyle = blink ? '#ffb040' : '#604010';
  1469. ctx.fillRect(x - 65, y - 62, 12, 6);
  1470. ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 15;
  1471. ctx.beginPath(); ctx.arc(x - 95, y - 14, 4, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
  1472. ctx.fillStyle = 'rgba(255,255,255,.7)';
  1473. ctx.beginPath(); ctx.arc(x - 96, y - 15, 1.5, 0, Math.PI * 2); ctx.fill();
  1474. drawWheels(x, y, 70, 25, 14, '#606060', wheelAngle);
  1475. const wx = x + 60;
  1476. ctx.beginPath(); ctx.arc(wx, y, 14, 0, Math.PI * 2);
  1477. ctx.fillStyle = '#0a0a0a'; ctx.fill();
  1478. ctx.beginPath(); ctx.arc(wx, y, 5, 0, Math.PI * 2);
  1479. ctx.fillStyle = '#606060'; ctx.fill();
  1480. }
  1481. function drawTowDriver(x, y, walking, alpha) {
  1482. ctx.save();
  1483. ctx.globalAlpha = alpha !== undefined ? Math.max(0, Math.min(1, alpha)) : 1;
  1484. const wobble = walking ? Math.sin(state.time * 10) * 2 : 0;
  1485. ctx.fillStyle = 'rgba(0,0,0,.5)';
  1486. ctx.beginPath(); ctx.ellipse(x, y + 2, 13, 4, 0, 0, Math.PI * 2); ctx.fill();
  1487. ctx.fillStyle = '#252c38';
  1488. ctx.fillRect(x - 8, y - 33, 7, 29 + wobble);
  1489. ctx.fillRect(x + 1, y - 33, 7, 29 - wobble);
  1490. ctx.fillStyle = '#0a0a0a';
  1491. ctx.fillRect(x - 10, y + 1 + wobble, 11, 3);
  1492. ctx.fillRect(x, y + 1 - wobble, 11, 3);
  1493. const jg = ctx.createLinearGradient(x - 11, 0, x + 11, 0);
  1494. jg.addColorStop(0, '#b06818'); jg.addColorStop(0.4, '#f09030'); jg.addColorStop(1, '#a85810');
  1495. ctx.fillStyle = jg; ctx.fillRect(x - 10, y - 63, 20, 33);
  1496. ctx.strokeStyle = '#703810'; ctx.lineWidth = 1; ctx.strokeRect(x - 10, y - 63, 20, 33);
  1497. ctx.fillStyle = '#f0f0f0';
  1498. ctx.fillRect(x - 10, y - 56, 20, 3);
  1499. ctx.fillRect(x - 10, y - 44, 20, 3);
  1500. ctx.fillStyle = '#d0a880';
  1501. ctx.fillRect(x - 16, y - 60, 6, 23);
  1502. ctx.fillRect(x + 10, y - 60, 6, 23);
  1503. ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 67, 6, 5);
  1504. ctx.beginPath(); ctx.arc(x, y - 76, 9, 0, Math.PI * 2);
  1505. const fg = ctx.createRadialGradient(x - 3, y - 78, 2, x, y - 76, 9);
  1506. fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#c09870');
  1507. ctx.fillStyle = fg; ctx.fill();
  1508. ctx.fillStyle = '#e8c020';
  1509. ctx.beginPath(); ctx.arc(x, y - 82, 11, Math.PI, 0); ctx.fill();
  1510. ctx.fillRect(x - 11, y - 83, 22, 4);
  1511. ctx.strokeStyle = '#8a6a10'; ctx.lineWidth = 1;
  1512. ctx.beginPath(); ctx.arc(x, y - 82, 11, Math.PI, 0); ctx.stroke();
  1513. ctx.fillStyle = '#c0a010';
  1514. ctx.fillRect(x - 1.5, y - 93, 3, 12);
  1515. ctx.restore();
  1516. }
  1517. function drawSenior(x, y, walking, alpha) {
  1518. ctx.save();
  1519. ctx.globalAlpha = alpha !== undefined ? Math.max(0, Math.min(1, alpha)) : 1;
  1520. const wobble = walking ? Math.sin(state.time * 9) * 2 : 0;
  1521. ctx.fillStyle = 'rgba(0,0,0,.55)';
  1522. ctx.beginPath(); ctx.ellipse(x, y + 2, 15, 5, 0, 0, Math.PI * 2); ctx.fill();
  1523. ctx.fillStyle = '#1a2340';
  1524. ctx.fillRect(x - 8, y - 35, 7, 31 + wobble);
  1525. ctx.fillRect(x + 1, y - 35, 7, 31 - wobble);
  1526. ctx.fillStyle = '#0a0a0a';
  1527. ctx.fillRect(x - 10, y - 3 + wobble, 11, 7);
  1528. ctx.fillRect(x + 1, y - 3 - wobble, 11, 7);
  1529. const un = ctx.createLinearGradient(x - 12, 0, x + 12, 0);
  1530. un.addColorStop(0, '#1a2a4a'); un.addColorStop(0.4, '#2a3a5a'); un.addColorStop(1, '#141e38');
  1531. ctx.fillStyle = un; ctx.fillRect(x - 12, y - 70, 24, 38);
  1532. ctx.strokeStyle = '#0a1428'; ctx.lineWidth = 1; ctx.strokeRect(x - 12, y - 70, 24, 38);
  1533. ctx.fillStyle = '#c0a040';
  1534. for (let i = 0; i < 3; i++) {
  1535. ctx.beginPath(); ctx.arc(x, y - 60 + i * 10, 1.2, 0, Math.PI * 2); ctx.fill();
  1536. }
  1537. ctx.fillStyle = '#e0e0f0';
  1538. ctx.fillRect(x - 13, y - 70, 6, 3);
  1539. ctx.fillRect(x + 7, y - 70, 6, 3);
  1540. ctx.fillStyle = '#ffd060';
  1541. ctx.beginPath(); ctx.arc(x - 10, y - 71, 1.5, 0, Math.PI * 2); ctx.fill();
  1542. ctx.beginPath(); ctx.arc(x + 10, y - 71, 1.5, 0, Math.PI * 2); ctx.fill();
  1543. ctx.fillStyle = '#2a3a5a';
  1544. ctx.fillRect(x - 18, y - 68, 6, 32);
  1545. ctx.fillRect(x + 12, y - 68, 6, 32);
  1546. ctx.fillStyle = '#0a0a0a';
  1547. ctx.fillRect(x - 18, y - 38, 7, 8);
  1548. ctx.fillRect(x + 12, y - 38, 7, 8);
  1549. ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 74, 6, 5);
  1550. ctx.beginPath(); ctx.arc(x, y - 83, 10, 0, Math.PI * 2);
  1551. const fg = ctx.createRadialGradient(x - 3, y - 85, 2, x, y - 83, 10);
  1552. fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#c09870');
  1553. ctx.fillStyle = fg; ctx.fill();
  1554. ctx.fillStyle = '#1a1a1a';
  1555. ctx.beginPath(); ctx.arc(x - 3, y - 84, 1.3, 0, Math.PI * 2); ctx.fill();
  1556. ctx.beginPath(); ctx.arc(x + 3, y - 84, 1.3, 0, Math.PI * 2); ctx.fill();
  1557. const cap = ctx.createLinearGradient(x - 13, 0, x + 13, 0);
  1558. cap.addColorStop(0, '#0f1a35'); cap.addColorStop(0.5, '#1a2c52'); cap.addColorStop(1, '#0a1428');
  1559. ctx.fillStyle = cap;
  1560. ctx.beginPath(); ctx.arc(x, y - 90, 13, Math.PI, 0); ctx.fill();
  1561. ctx.fillRect(x - 13, y - 90, 26, 5);
  1562. ctx.strokeStyle = '#0a1428'; ctx.lineWidth = 1.5;
  1563. ctx.beginPath(); ctx.arc(x, y - 90, 13, Math.PI, 0); ctx.stroke();
  1564. ctx.beginPath(); ctx.moveTo(x - 13, y - 85); ctx.lineTo(x + 13, y - 85); ctx.stroke();
  1565. ctx.fillStyle = '#05080f';
  1566. ctx.beginPath();
  1567. ctx.moveTo(x - 14, y - 85); ctx.lineTo(x + 14, y - 85);
  1568. ctx.lineTo(x + 12, y - 81); ctx.lineTo(x - 12, y - 81);
  1569. ctx.closePath(); ctx.fill();
  1570. ctx.strokeStyle = '#000'; ctx.stroke();
  1571. ctx.fillStyle = '#c0a040';
  1572. ctx.beginPath(); ctx.arc(x, y - 96, 2.5, 0, Math.PI * 2); ctx.fill();
  1573. ctx.fillStyle = 'rgba(255,220,140,.7)';
  1574. ctx.beginPath(); ctx.arc(x - 0.8, y - 96.8, 1, 0, Math.PI * 2); ctx.fill();
  1575. ctx.restore();
  1576. }
  1577. const CAR_SCALE = 1.56;
  1578. function drawCarScaled(drawFn, x, y, ...args) {
  1579. ctx.save();
  1580. ctx.translate(x, y);
  1581. ctx.scale(CAR_SCALE, CAR_SCALE);
  1582. ctx.translate(-x, -y);
  1583. drawFn(x, y, ...args);
  1584. ctx.restore();
  1585. }
  1586. /* ===== ПОГОДА ===== */
  1587. function updateWeatherParticles(dt) {
  1588. if (state.weather === 'clear' || state.weather === 'fog') {
  1589. state.weatherParticles = []; return;
  1590. }
  1591. const targetCount = state.weather === 'rain' ? 240 : 140;
  1592. while (state.weatherParticles.length < targetCount) {
  1593. state.weatherParticles.push({
  1594. x: Math.random() * W, y: Math.random() * H,
  1595. vx: state.weather === 'rain' ? 60 + Math.random() * 40 : -10 + Math.random() * 20,
  1596. vy: state.weather === 'rain' ? 600 + Math.random() * 200 : 30 + Math.random() * 40,
  1597. size: state.weather === 'rain' ? 1 : 1.5 + Math.random() * 1.5,
  1598. alpha: state.weather === 'rain' ? 0.5 + Math.random() * 0.4 : 0.7 + Math.random() * 0.3
  1599. });
  1600. }
  1601. if (state.weatherParticles.length > targetCount) state.weatherParticles.length = targetCount;
  1602. state.weatherParticles.forEach(p => {
  1603. p.x += p.vx * dt; p.y += p.vy * dt;
  1604. if (p.y > H) { p.y = -10; p.x = Math.random() * W; }
  1605. });
  1606. }
  1607. function drawWeatherParticles() {
  1608. if (state.weather === 'rain') {
  1609. ctx.strokeStyle = 'rgba(180,200,220,.55)';
  1610. ctx.lineWidth = 1;
  1611. state.weatherParticles.forEach(p => {
  1612. ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - 1, p.y - 12); ctx.stroke();
  1613. });
  1614. } else if (state.weather === 'snow') {
  1615. state.weatherParticles.forEach(p => {
  1616. ctx.fillStyle = `rgba(240,248,255,${p.alpha})`;
  1617. ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
  1618. });
  1619. } else if (state.weather === 'fog') {
  1620. const fogGrad = ctx.createLinearGradient(0, 0, 0, H);
  1621. fogGrad.addColorStop(0, 'rgba(200,205,215,.30)');
  1622. fogGrad.addColorStop(1, 'rgba(200,205,215,.35)');
  1623. ctx.fillStyle = fogGrad; ctx.fillRect(0, 0, W, H);
  1624. }
  1625. }
  1626. /* ===== ЛОГИКА ===== */
  1627. let lastTs = 0;
  1628. function updateScene(dt) {
  1629. const l = L();
  1630. if (!state.model) return;
  1631. if (state.alarm && state.time > state.alarmEnd) state.alarm = false;
  1632. const diff = state.barrier.target - state.barrier.level;
  1633. if (Math.abs(diff) > 0.001) {
  1634. const speedUp = 1 / state.model.openTime;
  1635. const speedDown = 1 / state.model.closeTime;
  1636. const spd = diff > 0 ? speedUp : speedDown;
  1637. const step = Math.min(Math.abs(diff), dt * spd);
  1638. state.barrier.level += Math.sign(diff) * step;
  1639. state.barrier.moving = true;
  1640. if (Math.abs(diff - Math.sign(diff)*step) < 0.005) {
  1641. state.barrier.level = state.barrier.target;
  1642. state.barrier.moving = false;
  1643. }
  1644. } else {
  1645. state.barrier.moving = false;
  1646. }
  1647. state.skud.kw1 = state.barrier.level < 0.05;
  1648. state.skud.kw2 = state.barrier.level > 0.95;
  1649. if (state.skud.kw1) { state.skud.relayClose = false; }
  1650. if (state.skud.kw2) { state.skud.relayOpen = false; }
  1651. if (state.barrier.level > 0.95 && !state.holdMode && state.model.autoClose > 0 && !state.manualMode) {
  1652. state.autoCloseTimer += dt;
  1653. if (state.autoCloseTimer >= state.model.autoClose && !state.skud.photo && !state.skud.loop1 && !state.skud.loop2) {
  1654. closeBarrier();
  1655. state.autoCloseTimer = 0;
  1656. }
  1657. } else if (state.barrier.level < 0.95) {
  1658. state.autoCloseTimer = 0;
  1659. }
  1660. const stopX = l.pivotX - (state.model.armLen) - 60;
  1661. if (state.car) {
  1662. const c = state.car;
  1663. if (state.phase === 'approaching') {
  1664. if (c.x < stopX) {
  1665. c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
  1666. } else if (c.isPriora) {
  1667. c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
  1668. if (c.x >= l.pivotX - state.model.armLen - 20 && state.barrier.level < 0.5) {
  1669. c.x = l.pivotX - state.model.armLen - 20; crash();
  1670. } else if (c.x > W + 300) {
  1671. state.car = null; state.phase = 'idle';
  1672. }
  1673. } else {
  1674. state.phase = 'waiting';
  1675. setStatus('ОЖИДАНИЕ РЕШЕНИЯ', 'wait');
  1676. setHint('Сравните пропуска и примите решение.');
  1677. if (state.model.hasLoop) {
  1678. state.skud.loop1 = true;
  1679. logSkud('ПЕТЛЯ 1: занята', 'blu');
  1680. state.stats.sensors++;
  1681. }
  1682. renderPasses();
  1683. }
  1684. }
  1685. else if (state.phase === 'resolved') {
  1686. if (c.action === 'pass') {
  1687. if (state.barrier.level > 0.9) {
  1688. c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
  1689. if (state.model.hasPhoto) {
  1690. if (!state.skud.photo && c.x > l.pivotX - state.model.armLen - 40 && c.x < l.pivotX + 40) {
  1691. state.skud.photo = true;
  1692. logSkud('ФОТОЭЛЕМЕНТ: разрыв луча', 'yel');
  1693. state.stats.sensors++;
  1694. }
  1695. if (state.skud.photo && (c.x < l.pivotX - state.model.armLen - 60 || c.x > l.pivotX + 60)) {
  1696. state.skud.photo = false;
  1697. }
  1698. }
  1699. if (c.x > W + 300) {
  1700. logEvent('Автомобиль проехал КПП', 'ok');
  1701. state.stats.in++;
  1702. state.car = null; state.phase = 'idle';
  1703. if (state.model.hasLoop) { state.skud.loop1 = false; logSkud('ПЕТЛЯ 1: свободна'); }
  1704. if (state.skud.photo) state.skud.photo = false;
  1705. state.barrier.target = 0;
  1706. setStatus('ПОСТ СВОБОДЕН');
  1707. setHint('Нажмите <b>«Следующая машина»</b>.');
  1708. state.senior = null;
  1709. }
  1710. }
  1711. } else if (c.action === 'turn') {
  1712. c.x -= c.speed * dt; c.wheelAngle -= c.speed * dt * 0.06;
  1713. if (c.x < -350) {
  1714. logEvent('Автомобиль уехал', 'info');
  1715. state.car = null; state.phase = 'idle';
  1716. if (state.model.hasLoop) { state.skud.loop1 = false; logSkud('ПЕТЛЯ 1: свободна'); }
  1717. setStatus('ПОСТ СВОБОДЕН');
  1718. setHint('Нажмите <b>«Следующая машина»</b>.');
  1719. state.senior = null;
  1720. }
  1721. }
  1722. }
  1723. }
  1724. if (state.senior) {
  1725. const s = state.senior;
  1726. const SPEED = 90;
  1727. if (s.state === 'walk_to_lane') {
  1728. s.alpha = Math.min(1, (s.alpha || 0) + dt * 1.5);
  1729. s.walking = true; s.y = l.botLaneY;
  1730. const targetX = (state.car ? state.car.x : 0) + 140;
  1731. if (s.x > targetX) s.x -= SPEED * dt;
  1732. else s.state = 'walk_up';
  1733. } else if (s.state === 'walk_up') {
  1734. s.walking = true;
  1735. if (s.y > l.topLaneY + 2) s.y -= SPEED * 0.7 * dt;
  1736. else {
  1737. s.y = l.topLaneY; s.walking = false; s.state = 'review'; s.decideTimer = 0;
  1738. setStatus('СТАРШИЙ У МАШИНЫ', 'senior');
  1739. setHint('Старший смены проверяет документы...');
  1740. logEvent('Старший смены подошёл к машине', 'senior');
  1741. }
  1742. } else if (s.state === 'review') {
  1743. s.decideTimer += dt; s.walking = false;
  1744. if (s.decideTimer > 2.0) {
  1745. const allow = (state.correctDecision === 'allow');
  1746. if (allow) {
  1747. logEvent('Старший смены: пропуск действителен — пропускаем', 'senior');
  1748. setStatus('СТАРШИЙ ПРОПУСКАЕТ', 'ok');
  1749. state.car.action = 'pass';
  1750. openBarrier();
  1751. state.phase = 'resolved';
  1752. state.userDecision = 'allow';
  1753. renderPasses();
  1754. } else {
  1755. logEvent('Старший смены: пропуск недействителен — отказ', 'senior');
  1756. setStatus('СТАРШИЙ ОТКАЗЫВАЕТ', 'wait');
  1757. state.car.action = 'turn';
  1758. state.phase = 'resolved';
  1759. state.userDecision = 'deny';
  1760. state.stats.out++;
  1761. renderPasses();
  1762. }
  1763. s.state = 'returning';
  1764. }
  1765. } else if (s.state === 'returning') {
  1766. s.walking = true;
  1767. if (s.y < l.botLaneY - 2) s.y += SPEED * 0.7 * dt;
  1768. else {
  1769. s.y = l.botLaneY; s.x += SPEED * dt;
  1770. if (s.x > W + 60) state.senior = null;
  1771. }
  1772. }
  1773. }
  1774. if (state.phase === 'crash_scene') {
  1775. if (state.police) {
  1776. const p = state.police;
  1777. if (p.state === 'arriving') {
  1778. p.x -= p.speed * dt; p.wheelAngle -= p.speed * dt * 0.06;
  1779. if (p.x <= p.targetX) { p.x = p.targetX; p.state = 'stopped'; }
  1780. } else if (p.state === 'leaving') {
  1781. p.x += p.speed * dt; p.wheelAngle += p.speed * dt * 0.06;
  1782. }
  1783. }
  1784. if (state.tow) {
  1785. const t2 = state.tow;
  1786. if (t2.state === 'arriving') {
  1787. t2.x += t2.speed * dt; t2.wheelAngle -= t2.speed * dt * 0.06;
  1788. if (t2.x >= t2.targetX) { t2.x = t2.targetX; t2.state = 'stopped'; t2.stopTimer = 0; }
  1789. } else if (t2.state === 'stopped') {
  1790. t2.stopTimer = (t2.stopTimer || 0) + dt;
  1791. } else if (t2.state === 'leaving') {
  1792. t2.x -= t2.speed * dt; t2.wheelAngle += t2.speed * dt * 0.06;
  1793. }
  1794. }
  1795. if (state.car && state.car.towed && state.tow) {
  1796. state.car.x = state.tow.x + 220;
  1797. state.car.wheelAngle += state.tow.speed * dt * 0.06;
  1798. }
  1799. if (state.tow && state.tow.state === 'stopped' && state.tow.stopTimer > 1.2 && !state.towDriver && !state.leaving && state.car) {
  1800. const doorX = state.tow.x - 150;
  1801. state.towDriver = { x: doorX, y: l.topLaneY, doorX: doorX,
  1802. carApproachX: state.car.x - 160,
  1803. state: 'exiting', exitTimer: 0, alpha: 0, walking: false, attachTimer: 0, boardTimer: 0 };
  1804. state.tow.doorOpen = true;
  1805. logEvent('Водитель выходит из кабины эвакуатора', 'info');
  1806. }
  1807. if (state.towDriver) {
  1808. const td = state.towDriver;
  1809. const underY = l.topLaneBot + 25;
  1810. const SPD = 55;
  1811. if (td.state === 'exiting') {
  1812. td.exitTimer += dt; td.alpha = Math.min(1, td.exitTimer / 0.8);
  1813. if (td.exitTimer > 0.8) { td.alpha = 1; td.state = 'walking_out_down'; }
  1814. } else if (td.state === 'walking_out_down') {
  1815. if (td.y < underY - 2) { td.y += SPD * dt; td.walking = true; }
  1816. else { td.y = underY; td.state = 'walking_right'; }
  1817. } else if (td.state === 'walking_right') {
  1818. if (td.x < td.carApproachX - 2) { td.x += SPD * dt; td.walking = true; }
  1819. else { td.x = td.carApproachX; td.state = 'walking_up'; }
  1820. } else if (td.state === 'walking_up') {
  1821. if (td.y > l.topLaneY + 2) { td.y -= SPD * dt; td.walking = true; }
  1822. else { td.y = l.topLaneY; td.state = 'attaching'; td.attachTimer = 0; }
  1823. } else if (td.state === 'attaching') {
  1824. td.attachTimer += dt; td.walking = false;
  1825. if (td.attachTimer > 0.6 && !state.towCable) { state.towCable = true; logEvent('Автомобиль зацеплен', 'info'); }
  1826. if (td.attachTimer > 1.4) td.state = 'returning_down';
  1827. } else if (td.state === 'returning_down') {
  1828. if (td.y < underY - 2) { td.y += SPD * dt; td.walking = true; }
  1829. else { td.y = underY; td.state = 'returning_left'; }
  1830. } else if (td.state === 'returning_left') {
  1831. if (td.x > td.doorX + 2) { td.x -= SPD * dt; td.walking = true; }
  1832. else { td.x = td.doorX; td.state = 'returning_up'; }
  1833. } else if (td.state === 'returning_up') {
  1834. if (td.y > l.topLaneY + 2) { td.y -= SPD * dt; td.walking = true; }
  1835. else { td.y = l.topLaneY; td.state = 'boarding'; td.boardTimer = 0; td.walking = false; }
  1836. } else if (td.state === 'boarding') {
  1837. td.boardTimer += dt; td.walking = false;
  1838. td.alpha = Math.max(0, 1 - td.boardTimer / 0.8);
  1839. td.x += 20 * dt;
  1840. if (td.boardTimer > 0.8) {
  1841. td.state = 'gone'; td.alpha = 0;
  1842. if (state.tow) state.tow.doorOpen = false;
  1843. if (!state.leaving) {
  1844. state.leaving = true;
  1845. setTimeout(() => {
  1846. logEvent('Эвакуатор увозит автомобиль', 'info');
  1847. if (state.car) state.car.towed = true;
  1848. if (state.police) state.police.state = 'leaving';
  1849. if (state.tow) state.tow.state = 'leaving';
  1850. }, 600);
  1851. }
  1852. }
  1853. }
  1854. }
  1855. if (state.tow && state.tow.state === 'leaving' && state.tow.x < -600) {
  1856. state.car = null; state.police = null; state.tow = null;
  1857. state.towDriver = null; state.towCable = false; state.leaving = false;
  1858. state.phase = 'idle'; state.barrier.target = 0;
  1859. state.alarm = false; state.skud.alarm = false;
  1860. setStatus('ПОСТ СВОБОДЕН');
  1861. setHint('Инцидент исчерпан. Нажмите «Следующая машина».');
  1862. logEvent('Инцидент закрыт', 'ok');
  1863. }
  1864. }
  1865. updateWeatherParticles(dt);
  1866. updateSkudLeds();
  1867. }
  1868. function crash() {
  1869. logEvent('!!! СТОЛКНОВЕНИЕ СО ШЛАГБАУМОМ !!!', 'alarm');
  1870. setStatus('ТРЕВОГА !', 'alarm');
  1871. setHint('Нарушитель врезался. Вызваны полиция и эвакуатор.');
  1872. state.alarm = true;
  1873. state.alarmEnd = state.time + 5;
  1874. state.phase = 'crash_scene';
  1875. state.leaving = false;
  1876. state.stats.alarms++;
  1877. state.stats.breaches++;
  1878. state.skud.alarm = true;
  1879. if (state.model.hasImpact) {
  1880. state.skud.impact = true;
  1881. logSkud('ДАТЧИК УДАРА: сработал', 'alarm');
  1882. state.stats.sensors++;
  1883. }
  1884. if (state.car) { state.car.action = 'crashed'; state.car.speed = 0; }
  1885. const l = L();
  1886. for (let i = 0; i < 40; i++) {
  1887. state.sparks.push({
  1888. x: l.pivotX - state.model.armLen - 30, y: l.topLaneY - 40,
  1889. vx: (Math.random() - .5) * 400, vy: (Math.random() - .5) * 300 - 80,
  1890. life: 1
  1891. });
  1892. }
  1893. setTimeout(() => {
  1894. state.police = { x: W + 300, targetX: l.pivotX - 100, speed: 260, state: 'arriving', wheelAngle: 0 };
  1895. logEvent('Прибывает полицейский автомобиль', 'warn');
  1896. }, 700);
  1897. setTimeout(() => {
  1898. const carX = state.car ? state.car.x : (l.pivotX - state.model.armLen - 20);
  1899. state.tow = { x: -300, targetX: carX - 340, speed: 200, state: 'arriving',
  1900. wheelAngle: 0, stopTimer: 0, doorOpen: false };
  1901. logEvent('Прибывает эвакуатор', 'warn');
  1902. }, 1400);
  1903. }
  1904. /* ===== ОТРИСОВКА ===== */
  1905. function drawScene() {
  1906. const l = L();
  1907. const t = state.time;
  1908. ctx.fillStyle = '#02040a'; ctx.fillRect(0, 0, W, H);
  1909. if (!state.model) return;
  1910. drawSky(); drawGround(); drawRoads();
  1911. drawBooth();
  1912. drawGuard(l.boothX + l.boothW + 25, l.boothBaseY);
  1913. if (state.towCable && state.tow && state.car) {
  1914. const fromX = state.tow.x + 140;
  1915. const fromY = l.topLaneY - 20;
  1916. const toX = state.car.x - 140;
  1917. const toY = l.topLaneY - 15;
  1918. ctx.strokeStyle = 'rgba(200,200,200,.85)'; ctx.lineWidth = 2;
  1919. ctx.beginPath(); ctx.moveTo(fromX, fromY);
  1920. ctx.quadraticCurveTo((fromX + toX) / 2, Math.max(fromY, toY) + 22, toX, toY); ctx.stroke();
  1921. }
  1922. drawBarrier();
  1923. if (state.car) {
  1924. if (state.car.isPriora) drawCarScaled(drawDarkCar, state.car.x, l.topLaneY, state.car.wheelAngle);
  1925. else drawCarScaled(drawSedan, state.car.x, l.topLaneY, state.car.color, state.car.wheelAngle);
  1926. }
  1927. if (state.police) drawCarScaled(drawPoliceCar, state.police.x, l.botLaneY, state.police.wheelAngle);
  1928. if (state.tow) drawCarScaled(drawTowTruck, state.tow.x, l.topLaneY, state.tow.wheelAngle, state.tow.doorOpen);
  1929. if (state.towDriver && state.towDriver.state !== 'gone') {
  1930. drawTowDriver(state.towDriver.x, state.towDriver.y, state.towDriver.walking, state.towDriver.alpha);
  1931. }
  1932. if (state.senior) {
  1933. drawSenior(state.senior.x, state.senior.y, state.senior.walking, state.senior.alpha);
  1934. }
  1935. state.sparks = state.sparks.filter(s => s.life > 0);
  1936. state.sparks.forEach(s => {
  1937. s.x += s.vx / 60; s.y += s.vy / 60; s.vy += 500 / 60; s.life -= 1.2 / 60;
  1938. ctx.fillStyle = `rgba(255,${Math.floor(160 + Math.random()*90)},60,${Math.max(0, s.life)})`;
  1939. ctx.beginPath(); ctx.arc(s.x, s.y, 2 + s.life * 3, 0, Math.PI * 2); ctx.fill();
  1940. });
  1941. drawWeatherParticles();
  1942. if (state.alarm) {
  1943. const pulse = 0.5 + Math.sin(t * 8) * 0.5;
  1944. ctx.strokeStyle = `rgba(255,40,40,${pulse * 0.5})`; ctx.lineWidth = 12;
  1945. ctx.strokeRect(6, 6, W - 12, H - 12);
  1946. ctx.fillStyle = `rgba(255,60,60,${0.5 + 0.3 * Math.abs(Math.sin(t * 5))})`;
  1947. ctx.font = 'bold 36px "Courier New"'; ctx.textAlign = 'center';
  1948. ctx.fillText('!!! ТРЕВОГА !!!', W / 2, 60); ctx.textAlign = 'left';
  1949. }
  1950. const hintText = {
  1951. idle: 'ПОСТ СВОБОДЕН · НАЖМИТЕ «СЛЕДУЮЩАЯ МАШИНА»',
  1952. approaching: 'АВТОМОБИЛЬ ПРИБЛИЖАЕТСЯ',
  1953. waiting: 'ОЖИДАНИЕ РЕШЕНИЯ ОПЕРАТОРА',
  1954. refuse_wait: 'МАШИНА ЖДЁТ · ОБЪЯСНИТЕ ПРИЧИНУ ИЛИ ВЫЗОВИТЕ СТАРШЕГО',
  1955. senior_walk: 'СТАРШИЙ СМЕНЫ ИДЁТ К МАШИНЕ',
  1956. resolved: 'ОБРАБОТКА РЕЗУЛЬТАТА',
  1957. crash_scene: 'НА МЕСТЕ ПРОИСШЕСТВИЯ'
  1958. }[state.phase] || '';
  1959. if (hintText) {
  1960. ctx.font = 'bold 12px "Courier New"';
  1961. const tw = ctx.measureText(hintText).width + 40;
  1962. ctx.fillStyle = 'rgba(10,14,20,.78)';
  1963. ctx.fillRect(W / 2 - tw / 2, 20, tw, 28);
  1964. ctx.strokeStyle = 'rgba(96,160,224,.5)'; ctx.lineWidth = 1;
  1965. ctx.strokeRect(W / 2 - tw / 2, 20, tw, 28);
  1966. ctx.fillStyle = '#a0c0e0'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  1967. ctx.fillText(hintText, W / 2, 34); ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
  1968. }
  1969. }
  1970. function loop(ts) {
  1971. if (!lastTs) lastTs = ts;
  1972. const dt = Math.min(0.05, (ts - lastTs) / 1000);
  1973. lastTs = ts;
  1974. state.time += dt;
  1975. updateScene(dt);
  1976. drawScene();
  1977. requestAnimationFrame(loop);
  1978. }
  1979. function updateClock() {
  1980. const d = new Date();
  1981. document.getElementById('clock').textContent =
  1982. String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0');
  1983. }
  1984. setInterval(updateClock, 1000); updateClock();
  1985. /* ===== СТАРТ ===== */
  1986. resizeCanvas(); renderPasses(); renderLog();
  1987. renderModelSwitcher();
  1988. switchModel('B');
  1989. logSkud('Запуск СКУД КПП-3...', 'ok');
  1990. requestAnimationFrame(loop);
  1991. </script>
  1992. </body>
  1993. </html>