| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390 |
- <!DOCTYPE html>
- <html lang="ru">
- <head>
- <meta charset="UTF-8">
- <title>КПП-3 · СКУД · Пост охраны</title>
- <style>
- * { box-sizing: border-box; margin: 0; padding: 0; }
- html, body { height: 100%; overflow: hidden; background: #050810;
- color: #a8b8c8; font-family: "Courier New", monospace; font-size: 13px; }
- .app { height: 100vh; display: flex; flex-direction: column; }
- header { display: flex; align-items: center; gap: 20px; padding: 8px 20px;
- background: linear-gradient(180deg, #202830, #10161e);
- border-bottom: 1px solid #000; flex-shrink: 0; }
- .brand { color: #ffd060; font-weight: bold; letter-spacing: 2px;
- font-size: 14px; text-shadow: 0 0 10px rgba(255,208,96,.5); }
- .brand small { color: #506080; font-weight: normal; letter-spacing: 1px;
- font-size: 10px; margin-left: 8px; }
- .model-badge { color: #80e0ff; font-size: 11px; letter-spacing: 1px;
- border: 1px solid #2a4a6a; padding: 3px 8px; border-radius: 2px;
- background: rgba(64,160,224,.08); }
- .header-status { display: flex; align-items: center; gap: 10px; margin-left: auto; }
- .clock { color: #6090c0; letter-spacing: 2px; font-size: 13px; }
- .sys-status { display: flex; align-items: center; gap: 8px;
- color: #40d080; font-weight: bold; font-size: 12px; letter-spacing: 1px; }
- .sys-status .led { width: 10px; height: 10px; border-radius: 50%;
- background: #40d080; box-shadow: 0 0 10px #40d080; }
- .sys-status.alarm { color: #ff4040; }
- .sys-status.alarm .led { background: #ff4040;
- box-shadow: 0 0 15px #ff4040; animation: blink .4s infinite; }
- .sys-status.wait { color: #ffb040; }
- .sys-status.wait .led { background: #ffb040; box-shadow: 0 0 10px #ffb040; }
- .sys-status.pause { color: #60a0e0; }
- .sys-status.pause .led { background: #60a0e0; box-shadow: 0 0 10px #60a0e0; }
- .sys-status.senior { color: #a0c8ff; }
- .sys-status.senior .led { background: #a0c8ff; box-shadow: 0 0 10px #a0c8ff; }
- @keyframes blink { 50% { opacity: .2; } }
- .log-toggle {
- background: linear-gradient(180deg, #1a2028, #101418);
- color: #a0c0e0; border: 1px solid #304050;
- border-radius: 3px; padding: 5px 12px;
- font-family: inherit; font-size: 11px; letter-spacing: 1.2px;
- cursor: pointer; transition: all .15s; margin-left: 8px;
- }
- .log-toggle:hover { border-color: #4080c0; color: #c0e0ff;
- box-shadow: 0 0 10px rgba(96,160,224,.3); }
- .log-toggle.has-alarm { border-color: #702020; color: #ffb0b0;
- animation: blink 1s infinite; }
- main { flex: 1; display: grid; grid-template-columns: 1fr 420px; min-height: 0; }
- .stage { position: relative; background: #02040a; overflow: hidden; }
- .stage canvas { display: block; width: 100%; height: 100%; }
- aside { background: #0a0e14; border-left: 1px solid #1a2028;
- display: flex; flex-direction: column; overflow-y: auto; }
- aside .section { border-bottom: 1px solid #1a2028; flex-shrink: 0; }
- aside .section h3 { padding: 8px 14px; background: #080c12; color: #6070a0;
- font-size: 10.5px; letter-spacing: 2px; font-weight: normal;
- border-bottom: 1px solid #1a2028; }
- aside .content { padding: 10px 12px; }
- .model-switcher { display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
- padding: 10px 12px; }
- .model-btn {
- background: linear-gradient(180deg, #10161e, #0a0e14);
- border: 1px solid #2a3442; border-radius: 3px;
- padding: 8px 8px; cursor: pointer; transition: all .15s;
- font-family: inherit; color: #8090a8; text-align: left;
- font-size: 10.5px; line-height: 1.35;
- }
- .model-btn:hover { border-color: #4080c0; background: #0f1822; color: #c0d0e0; }
- .model-btn.active {
- border-color: #40d080; color: #c0f0d0;
- background: linear-gradient(180deg, #10201a, #0a1a14);
- box-shadow: 0 0 12px rgba(64,208,128,.25), inset 0 0 8px rgba(64,208,128,.08);
- }
- .model-btn .mb-code { display: block; color: #ffd060; font-weight: bold;
- font-size: 12px; letter-spacing: 1.5px; margin-bottom: 2px; }
- .model-btn .mb-name { display: block; color: #80e0ff; font-size: 10px;
- margin-bottom: 3px; }
- .model-btn .mb-spec { display: block; color: #506080; font-size: 9.5px; }
- .model-btn.active .mb-spec { color: #80a0c0; }
- .passport-grid { display: grid; grid-template-columns: 1fr 1fr;
- gap: 3px 12px; font-size: 11px; }
- .passport-row { display: flex; justify-content: space-between;
- padding: 3px 0; border-bottom: 1px dashed #1a2028; }
- .passport-row .pk { color: #607890; }
- .passport-row .pv { color: #a0d0ff; font-weight: bold; }
- .passport-title { color: #ffd060; font-size: 12.5px;
- font-weight: bold; letter-spacing: 1.5px; margin-bottom: 8px; }
- .passport-sub { color: #80e0ff; font-size: 10.5px;
- letter-spacing: 1px; margin-bottom: 8px; }
- .passport-desc { color: #8090a8; font-size: 10.5px;
- line-height: 1.5; margin-bottom: 10px;
- padding: 6px 8px; background: #060a10; border-left: 2px solid #2a4a6a;
- border-radius: 2px; }
- .terminal-list { display: flex; flex-wrap: wrap; gap: 4px;
- margin-top: 8px; }
- .terminal-chip { font-size: 9.5px; padding: 2px 6px;
- border-radius: 2px; background: #060a10; border: 1px solid #2a3442;
- color: #8090a8; letter-spacing: .5px; }
- .terminal-chip.on { background: rgba(64,208,128,.12); color: #80e0a0;
- border-color: rgba(64,208,128,.5); }
- .terminal-chip.alarm { background: rgba(255,64,64,.12); color: #ff8080;
- border-color: rgba(255,64,64,.5); }
- .skud-group { margin-bottom: 12px; }
- .skud-group:last-child { margin-bottom: 0; }
- .skud-group-title { color: #4a5a70; font-size: 9.5px;
- letter-spacing: 1.5px; margin-bottom: 6px;
- padding-bottom: 3px; border-bottom: 1px solid #141c24; }
- .skud-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px;
- font-size: 11px; }
- .skud-row { display: flex; align-items: center; gap: 6px; padding: 3px 0;
- color: #8090a0; }
- .skud-led { width: 9px; height: 9px; border-radius: 50%;
- background: #1a2028; border: 1px solid #2a3442; flex-shrink: 0;
- transition: all .1s; }
- .skud-led.on { background: #40d080; box-shadow: 0 0 8px #40d080; border-color: #40d080; }
- .skud-led.red { background: #ff4040; box-shadow: 0 0 8px #ff4040; border-color: #ff4040; }
- .skud-led.yel { background: #ffb040; box-shadow: 0 0 8px #ffb040; border-color: #ffb040; }
- .skud-led.blu { background: #60a0e0; box-shadow: 0 0 8px #60a0e0; border-color: #60a0e0; }
- .skud-led.blink { animation: blink .5s infinite; }
- .fsm-table { background: #060a10; border: 1px solid #1a2028;
- border-radius: 2px; padding: 8px 10px; }
- .fsm-row { display: flex; justify-content: space-between;
- padding: 3px 0; font-size: 11px; }
- .fsm-k { color: #607890; }
- .fsm-v { color: #80e0ff; font-weight: bold; letter-spacing: .5px; }
- .scope-canvas { display: block; width: 100%;
- background: #04060a; border: 1px solid #1a2028; border-radius: 2px;
- image-rendering: pixelated; }
- .passes-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
- .pass-card { background: #060a10; border: 1px solid #202836;
- border-radius: 3px; padding: 8px 10px; font-size: 11.5px; }
- .pass-card .head { color: #506080; font-size: 9.5px; letter-spacing: 1.5px;
- margin-bottom: 6px; text-transform: uppercase; }
- .pass-card .num { color: #ffd060; font-weight: bold; font-size: 15px;
- letter-spacing: 1px; margin-bottom: 4px; }
- .pass-card .name { color: #c0d0e0; margin-bottom: 4px; font-size: 11px; line-height: 1.3; }
- .pass-card .dept { color: #607890; font-size: 10px; margin-bottom: 6px; }
- .pass-card .status { padding: 3px 6px; border-radius: 2px; font-size: 10px;
- letter-spacing: 1px; text-align: center; display: inline-block; }
- .pass-card .status.ok { background: rgba(64,208,128,.15); color: #40d080;
- border: 1px solid rgba(64,208,128,.4); }
- .pass-card .status.bad { background: rgba(255,64,64,.15); color: #ff6060;
- border: 1px solid rgba(255,64,64,.4); }
- .pass-card .status.empty { background: rgba(80,90,110,.1); color: #506080;
- border: 1px dashed #303848; }
- .decision-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
- padding: 10px 12px; border-bottom: 1px solid #1a2028; }
- button.big { background: linear-gradient(180deg, #1a2028, #101418);
- color: #c0d0e0; border: 1px solid #304050;
- border-radius: 3px; padding: 12px 10px;
- font-family: inherit; font-size: 12.5px; letter-spacing: 1.5px;
- cursor: pointer; transition: all .15s; font-weight: bold; }
- button.big.allow { border-color: #206030; color: #80e0a0; }
- button.big.allow:hover:not(:disabled) {
- background: linear-gradient(180deg, #204830, #103020); border-color: #40d080; }
- button.big.deny { border-color: #702020; color: #ffb0b0; }
- button.big.deny:hover:not(:disabled) {
- background: linear-gradient(180deg, #401818, #200808); border-color: #ff4040; }
- button.big:disabled { opacity: .3; cursor: not-allowed; }
- .action-buttons { padding: 10px 12px; display: flex; flex-direction: column;
- gap: 6px; border-bottom: 1px solid #1a2028; }
- button.ctrl { background: linear-gradient(180deg, #1a2028, #101418);
- color: #c0d0e0; border: 1px solid #304050;
- border-radius: 2px; padding: 8px 12px;
- font-family: inherit; font-size: 11.5px; letter-spacing: .5px;
- cursor: pointer; transition: all .15s; }
- button.ctrl:hover:not(:disabled) { background: linear-gradient(180deg, #243040, #182028);
- border-color: #4080c0; }
- button.ctrl:disabled { opacity: .35; cursor: not-allowed; }
- button.ctrl.primary { border-color: #4060a0; color: #a0d0ff; }
- button.ctrl.danger { border-color: #702020; color: #ffb0b0; }
- button.ctrl.green { border-color: #206030; color: #80e0a0; }
- button.ctrl.yel { border-color: #706020; color: #ffd060; }
- button.ctrl.red { border-color: #702020; color: #ffb0b0; }
- button.ctrl.active { background: linear-gradient(180deg, #2a3a4a, #1a2028);
- border-color: #60a0e0; color: #c0e0ff; box-shadow: 0 0 8px rgba(96,160,224,.4); }
- .ctrl-panel-grid { display: grid; grid-template-columns: 1fr 1fr 1fr;
- gap: 6px; padding: 10px 12px; }
- .ctrl-panel-grid button { padding: 10px 4px; font-size: 11px;
- letter-spacing: 1px; font-weight: bold; }
- .mode-row { display: flex; gap: 6px; padding: 0 12px 10px; }
- .mode-row button { flex: 1; padding: 7px 4px; font-size: 10.5px; }
- .cond-row { display: flex; align-items: center; gap: 8px;
- padding: 6px 0; font-size: 11.5px; color: #8090a0; }
- .cond-row label { min-width: 60px; color: #607890; }
- .cond-row select {
- flex: 1; background: #060a10; color: #c0d0e0;
- border: 1px solid #304050; border-radius: 2px;
- padding: 5px 8px; font-family: inherit; font-size: 11.5px;
- cursor: pointer; }
- .log { overflow-y: auto; padding: 8px 12px;
- font-size: 10.5px; line-height: 1.7; }
- .log .entry { color: #8090a8; }
- .log .entry.warn { color: #ffb040; }
- .log .entry.alarm { color: #ff4040; font-weight: bold; }
- .log .entry.ok { color: #40d080; }
- .log .entry.info { color: #60a0e0; }
- .log .entry.senior { color: #a0c8ff; font-weight: bold; }
- .log .entry.skud { color: #80e0ff; }
- .log .entry .time { color: #405060; margin-right: 6px; }
- .log-full { max-height: 60vh; min-height: 300px;
- background: #060a10; border: 1px solid #1a2028; border-radius: 3px;
- padding: 12px 14px; font-size: 11.5px; line-height: 1.8; }
- .hint { padding: 8px 12px; background: rgba(96,160,224,.08);
- border-left: 2px solid #4080c0; color: #a0c0e0;
- font-size: 11px; line-height: 1.5; }
- .hint b { color: #ffd060; }
- .modal { position: fixed; inset: 0; background: rgba(0,0,0,.85);
- display: none; align-items: center; justify-content: center;
- z-index: 100; padding: 20px; }
- .modal.on { display: flex; }
- .modal-content {
- background: linear-gradient(180deg, #1a2230, #0d141c);
- border: 2px solid #3a4858;
- border-radius: 4px;
- box-shadow: 0 0 0 1px #000, 0 20px 60px rgba(0,0,0,.9);
- padding: 24px 28px; width: min(640px, 100%);
- max-height: 90vh; overflow-y: auto;
- color: #c0d0e0; font-family: "Courier New", monospace; }
- .modal-content h2 {
- color: #ffd060; font-size: 16px; letter-spacing: 2px;
- margin-bottom: 20px; text-align: center;
- text-shadow: 0 0 10px rgba(255,208,96,.4); }
- .modal-content .row {
- display: flex; justify-content: space-between;
- padding: 8px 0; border-bottom: 1px solid #1a2028;
- font-size: 12.5px; }
- .modal-content .row:last-of-type { border-bottom: none; }
- .modal-content .row .k { color: #8090a0; }
- .modal-content .row .v { color: #a0d0ff; font-weight: bold; }
- .modal-content .row .v.good { color: #40d080; }
- .modal-content .row .v.bad { color: #ff6060; }
- .modal-content button {
- display: block; margin: 20px auto 0;
- padding: 10px 30px; font-family: inherit;
- font-size: 13px; letter-spacing: 1.5px;
- background: linear-gradient(180deg, #1a2028, #101418);
- color: #c0d0e0; border: 1px solid #4060a0;
- border-radius: 3px; cursor: pointer; }
- .modal-content button:hover { background: linear-gradient(180deg, #243040, #182028); }
- .refuse-menu {
- display: none; padding: 10px 12px;
- background: rgba(200,80,80,.08);
- border-top: 1px solid rgba(255,64,64,.3);
- border-bottom: 1px solid #1a2028; }
- .refuse-menu.on { display: block; }
- .refuse-menu .title {
- font-size: 10.5px; color: #ffb040; letter-spacing: 1.5px;
- margin-bottom: 8px; }
- .refuse-menu button.ctrl {
- width: 100%; margin-bottom: 5px;
- text-align: left; font-size: 11.5px; padding: 7px 10px; }
- .refuse-menu button.ctrl:last-child { margin-bottom: 0; }
- </style>
- </head>
- <body>
- <div class="app">
- <header>
- <div class="brand">◤ КПП-3<small>СКУД · ПОСТ ОХРАНЫ</small></div>
- <div class="model-badge" id="modelBadge">МОДЕЛЬ: —</div>
- <div class="header-status">
- <div class="clock" id="clock">--:--:--</div>
- <div class="sys-status" id="sysStatus"><span class="led"></span><span id="sysStatusText">ПОСТ СВОБОДЕН</span></div>
- <button class="log-toggle" id="btnLogToggle" title="Журнал событий (F2)">☰ ЖУРНАЛ</button>
- </div>
- </header>
- <main>
- <div class="stage"><canvas id="cv"></canvas></div>
- <aside>
- <div class="section">
- <h3>ПАСПОРТ МОДЕЛИ · ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ</h3>
- <div class="content" id="modelPassport"></div>
- </div>
- <div class="section">
- <h3>МОДЕЛЬ ШЛАГБАУМА · ПЕРЕКЛЮЧАТЕЛЬ</h3>
- <div class="model-switcher" id="modelSwitcher"></div>
- </div>
- <div class="section">
- <h3>СТАНЦИЯ СКУД · ИНДИКАЦИЯ</h3>
- <div class="content">
- <div class="skud-group">
- <div class="skud-group-title">ПИТАНИЕ И ПРИВОД</div>
- <div class="skud-grid">
- <div class="skud-row"><span class="skud-led" id="ledPower"></span>ПИТАНИЕ 24V</div>
- <div class="skud-row"><span class="skud-led" id="ledArmState"></span>СОСТОЯНИЕ ПРИВОДА</div>
- </div>
- </div>
- <div class="skud-group">
- <div class="skud-group-title">КОНЦЕВЫЕ ВЫКЛЮЧАТЕЛИ</div>
- <div class="skud-grid">
- <div class="skud-row"><span class="skud-led" id="ledKW1"></span>КВ-1 · СТРЕЛА ВНИЗ</div>
- <div class="skud-row"><span class="skud-led" id="ledKW2"></span>КВ-2 · СТРЕЛА ВВЕРХ</div>
- </div>
- </div>
- <div class="skud-group">
- <div class="skud-group-title">ДАТЧИКИ И ПЕТЛИ</div>
- <div class="skud-grid">
- <div class="skud-row"><span class="skud-led" id="ledLoop1"></span>ПЕТЛЯ 1 · ВХОД</div>
- <div class="skud-row"><span class="skud-led" id="ledLoop2"></span>ПЕТЛЯ 2 · ВЫХОД</div>
- <div class="skud-row"><span class="skud-led" id="ledPhoto"></span>ФОТОЭЛЕМЕНТ</div>
- <div class="skud-row"><span class="skud-led" id="ledImpact"></span>ДАТЧИК УДАРА</div>
- </div>
- </div>
- <div class="skud-group">
- <div class="skud-group-title">РЕЛЕ КОНТРОЛЛЕРА</div>
- <div class="skud-grid">
- <div class="skud-row"><span class="skud-led" id="ledRelayOpen"></span>РЕЛЕ 1 · OPEN</div>
- <div class="skud-row"><span class="skud-led" id="ledRelayClose"></span>РЕЛЕ 2 · CLOSE</div>
- <div class="skud-row"><span class="skud-led" id="ledRelayStop"></span>РЕЛЕ 3 · STOP</div>
- <div class="skud-row"><span class="skud-led" id="ledPass"></span>СИГНАЛ ПРОПУСКА</div>
- </div>
- </div>
- <div class="skud-group">
- <div class="skud-group-title">ТАБЛО СОСТОЯНИЙ КОНТРОЛЛЕРА</div>
- <div class="fsm-table">
- <div class="fsm-row"><span class="fsm-k">Состояние:</span>
- <span class="fsm-v" id="fsmState">IDLE</span></div>
- <div class="fsm-row"><span class="fsm-k">Таймер автозакрытия:</span>
- <span class="fsm-v" id="fsmTimer">0.0 / 0.0 с</span></div>
- <div class="fsm-row"><span class="fsm-k">Позиция стрелы:</span>
- <span class="fsm-v" id="fsmLevel">0 %</span></div>
- <div class="fsm-row"><span class="fsm-k">Активных тревог:</span>
- <span class="fsm-v" id="fsmAlarms">0</span></div>
- </div>
- </div>
- <div class="skud-group">
- <div class="skud-group-title">ОСЦИЛЛОГРАММА СИГНАЛОВ · REALTIME</div>
- <canvas id="scopeCanvas" class="scope-canvas" width="360" height="90"></canvas>
- </div>
- </div>
- </div>
- <div class="section">
- <h3>ПУЛЬТ ОПЕРАТОРА</h3>
- <div class="ctrl-panel-grid">
- <button class="ctrl green" id="btnOpen">▲ ОТКР</button>
- <button class="ctrl red" id="btnClose">▼ ЗАКР</button>
- <button class="ctrl yel" id="btnStop">■ СТОП</button>
- </div>
- <div class="mode-row">
- <button class="ctrl" id="btnHold">УДЕРЖАНИЕ</button>
- <button class="ctrl" id="btnMode">РЕЖИМ: АВТО</button>
- <button class="ctrl" id="btnResetAlarm">СБРОС АВАРИИ</button>
- </div>
- </div>
- <div class="section">
- <h3>СРАВНЕНИЕ ПРОПУСКОВ</h3>
- <div class="content"><div class="passes-compare" id="passesCompare"></div></div>
- </div>
- <div class="hint" id="hint">Нажмите «Следующая машина».</div>
- <div class="decision-buttons">
- <button class="big allow" id="btnAllow" disabled>✓ РАЗРЕШИТЬ</button>
- <button class="big deny" id="btnDeny" disabled>✕ ЗАПРЕТИТЬ</button>
- </div>
- <div class="refuse-menu" id="refuseMenu">
- <div class="title">МАШИНА ЖДЁТ РЕШЕНИЯ</div>
- <button class="ctrl" data-refuse="explain">📢 Объяснить причину отказа</button>
- <button class="ctrl" data-refuse="senior">📞 Вызвать старшего смены</button>
- </div>
- <div class="action-buttons">
- <button class="ctrl primary" id="btnNext">▸ Следующая машина</button>
- <button class="ctrl danger" id="btnBreach">⚠ Силовой проезд</button>
- <button class="ctrl" id="btnNote">✎ Отметить в журнале</button>
- <button class="ctrl" id="btnEndShift">⏹ Закончить смену</button>
- <button class="ctrl" id="btnReset">↻ Сброс</button>
- </div>
- <div class="section">
- <h3>ВНЕШНИЕ УСЛОВИЯ</h3>
- <div class="content">
- <div class="cond-row">
- <label>Время</label>
- <select id="selTimeOfDay">
- <option value="day">☀ День</option>
- <option value="night" selected>🌙 Ночь</option>
- </select>
- </div>
- <div class="cond-row">
- <label>Погода</label>
- <select id="selWeather">
- <option value="clear" selected>☀ Ясно</option>
- <option value="rain">🌧 Дождь</option>
- <option value="snow">❄ Снег</option>
- <option value="fog">🌫 Туман</option>
- </select>
- </div>
- </div>
- </div>
- </aside>
- </main>
- </div>
- <div class="modal" id="logModal">
- <div class="modal-content" style="width: min(900px, 100%);">
- <h2>ЖУРНАЛ СОБЫТИЙ · СКУД</h2>
- <div class="log log-full" id="logContentFull"></div>
- <div style="display:flex; gap:8px; justify-content:center; margin-top:16px;">
- <button id="btnLogClear" style="margin:0;">ОЧИСТИТЬ</button>
- <button id="btnLogClose" style="margin:0;">ЗАКРЫТЬ</button>
- </div>
- </div>
- </div>
- <div class="modal" id="shiftModal">
- <div class="modal-content">
- <h2>СМЕНА ЗАВЕРШЕНА</h2>
- <div class="row"><span class="k">Впущено автомобилей:</span><span class="v good" id="statIn">0</span></div>
- <div class="row"><span class="k">Отказано в проезде:</span><span class="v" id="statOut">0</span></div>
- <div class="row"><span class="k">Верных решений:</span><span class="v good" id="statCorrect">0</span></div>
- <div class="row"><span class="k">Ошибок оператора:</span><span class="v bad" id="statWrong">0</span></div>
- <div class="row"><span class="k">Сработало тревог:</span><span class="v bad" id="statAlarms">0</span></div>
- <div class="row"><span class="k">Срабатываний датчиков:</span><span class="v" id="statSensors">0</span></div>
- <div class="row"><span class="k">Силовых проездов:</span><span class="v bad" id="statBreaches">0</span></div>
- <div class="row"><span class="k">Вызовов старшего:</span><span class="v" id="statSenior">0</span></div>
- <div class="row"><span class="k">Оценка смены:</span><span class="v" id="statGrade">—</span></div>
- <button id="btnCloseShift">ЗАКРЫТЬ И СБРОСИТЬ</button>
- </div>
- </div>
- <script>
- const cv = document.getElementById('cv');
- const ctx = cv.getContext('2d');
- let W = 0, H = 0;
- function resizeCanvas() {
- const stage = cv.parentElement;
- const dpr = window.devicePixelRatio || 1;
- W = stage.clientWidth; H = stage.clientHeight;
- cv.width = Math.floor(W * dpr); cv.height = Math.floor(H * dpr);
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- }
- window.addEventListener('resize', resizeCanvas);
- function L() {
- return {
- horizonY: H * 0.32,
- topLaneY: H * 0.58, topLaneTop: H * 0.52, topLaneBot: H * 0.66,
- botLaneY: H * 0.78, botLaneTop: H * 0.72, botLaneBot: H * 0.84,
- boothX: W * 0.40, boothW: 150, boothH: 165, boothBaseY: H * 0.55,
- pillarX: W * 0.72, pillarW: 30, pillarH: 260,
- pivotX: W * 0.72, pivotY: H * 0.58 - 80,
- armLen: 240, armH: 16
- };
- }
- /* ===== МОДЕЛИ ===== */
- const BARRIER_MODELS = {
- A: {
- code:'A', name:'СКОРОСТНОЙ', short:'«Спринт-90»',
- vendor:'СКУД-Системы', series:'SP-90',
- desc:'Высокоскоростная модель для офисных КПП и паркингов. Полный цикл ~1 с.',
- openTime:0.9, closeTime:1.2, autoClose:3, armLen:180,
- hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:true,
- driveType:'Линейный', driveVoltage:'24 В DC', drivePower:'120 Вт',
- ingressProtection:'IP54', operatingTemp:'−40…+55 °C',
- dutyCycle:'80 %', mttr:'~15 мин',
- terminals:['+24V','GND','OPEN','CLOSE','STOP','KW1','KW2','LOOP1','LOOP2','PHOTO']
- },
- B: {
- code:'B', name:'МАГИСТРАЛЬНЫЙ', short:'«Стандарт-240»',
- vendor:'СКУД-Системы', series:'ST-240',
- desc:'Для въезда на территорию предприятий. Петля + фотоэлемент.',
- openTime:3.5, closeTime:3.5, autoClose:8, armLen:240,
- hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:false, hasKW2:true,
- driveType:'Рычажный', driveVoltage:'230 В AC', drivePower:'250 Вт',
- ingressProtection:'IP65', operatingTemp:'−40…+60 °C',
- dutyCycle:'60 %', mttr:'~30 мин',
- terminals:['L','N','PE','OPEN','CLOSE','STOP','KW1','KW2','LOOP1','LOOP2','PHOTO','BUZZ']
- },
- C: {
- code:'C', name:'ПРОМЫШЛЕННЫЙ', short:'«Терминал-280»',
- vendor:'ПромАвтоматика', series:'IND-280',
- desc:'Для грузового транспорта. Две петли, фотоэлемент, датчик удара.',
- openTime:6, closeTime:6, autoClose:15, armLen:280,
- hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:true, hasKW2:true,
- driveType:'Редукторный', driveVoltage:'380 В AC', drivePower:'750 Вт',
- ingressProtection:'IP66', operatingTemp:'−50…+60 °C',
- dutyCycle:'100 %', mttr:'~45 мин',
- terminals:['L1','L2','L3','N','PE','OPEN','CLOSE','STOP','KW1','KW2',
- 'LOOP1','LOOP2','PHOTO','IMPACT','ALARM','READY']
- },
- D: {
- code:'D', name:'БЮДЖЕТНЫЙ', short:'«Мини-200»',
- vendor:'ЭкономСервис', series:'EC-200',
- desc:'Для внутренних КПП. Без автоматики, ручное управление с пульта.',
- openTime:4, closeTime:4, autoClose:0, armLen:200,
- hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:false,
- driveType:'Линейный', driveVoltage:'24 В DC', drivePower:'90 Вт',
- ingressProtection:'IP44', operatingTemp:'−25…+50 °C',
- dutyCycle:'50 %', mttr:'~20 мин',
- terminals:['+24V','GND','OPEN','CLOSE','STOP','KW1']
- }
- };
- let selectedModelCode = 'B';
- const state = {
- time: 0,
- model: null,
- barrier: { level: 0, target: 0, moving: false },
- holdMode: false,
- manualMode: false,
- autoCloseTimer: 0,
- car: null,
- correctPass: null, presentedPass: null,
- phase: 'idle',
- userDecision: null, correctDecision: null,
- alarm: false, alarmEnd: 0, sparks: [],
- police: null, tow: null, towDriver: null, towCable: false, leaving: false,
- timeOfDay: 'night',
- weather: 'clear',
- weatherParticles: [],
- senior: null,
- skud: {
- power: true, kw1: true, kw2: false,
- loop1: false, loop2: false,
- photo: false, impact: false,
- relayOpen: false, relayClose: false, relayStop: false,
- passValid: false, alarm: false
- },
- stats: { in:0, out:0, correct:0, wrong:0, alarms:0, breaches:0, senior:0, sensors:0 }
- };
- /* ===== ЖУРНАЛ ===== */
- const logEntries = [];
- function logEvent(text, cls) {
- const now = new Date();
- const t = String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
- logEntries.push({ t, text, cls: cls||'info' });
- if (logEntries.length > 200) logEntries.shift();
- renderLog();
- }
- function logSkud(text, cls) { logEvent('[СКУД] ' + text, cls || 'skud'); }
- function renderLog() {
- const html = logEntries.map(e =>
- `<div class="entry ${e.cls}"><span class="time">${e.t}</span>${e.text}</div>`
- ).join('');
- const el = document.getElementById('logContentFull');
- if (el) {
- const wasBottom = (el.scrollHeight - el.scrollTop - el.clientHeight) < 30;
- el.innerHTML = html;
- if (wasBottom) el.scrollTop = el.scrollHeight;
- }
- updateLogBadge();
- }
- function updateLogBadge() {
- const btn = document.getElementById('btnLogToggle');
- if (!btn) return;
- const modal = document.getElementById('logModal');
- const modalOpen = modal && modal.classList.contains('on');
- const hasAlarm = logEntries.slice(-5).some(e => e.cls === 'alarm');
- btn.classList.toggle('has-alarm', hasAlarm && !modalOpen);
- }
- function setStatus(text, cls) {
- document.getElementById('sysStatusText').textContent = text;
- document.getElementById('sysStatus').className = 'sys-status' + (cls ? ' ' + cls : '');
- }
- function setHint(text) { document.getElementById('hint').innerHTML = text; }
- /* ===== ОСВЕЩЕНИЕ ===== */
- function lighting() {
- const isDay = state.timeOfDay === 'day';
- const w = state.weather;
- if (w === 'fog') return isDay ? { mode: 'dawn' } : { mode: 'dusk' };
- if (w === 'rain') return isDay ? { mode: 'overcast' } : { mode: 'wetnight' };
- if (w === 'snow') return isDay ? { mode: 'overcast' } : { mode: 'snownight' };
- return isDay ? { mode: 'day' } : { mode: 'night' };
- }
- function skyColors() {
- const m = lighting().mode;
- switch (m) {
- case 'day': return ['#3d5d80', '#98c4dc', '#d8e8f2'];
- case 'dawn': return ['#2a3a5c', '#7a94b4', '#c4b8b0'];
- case 'dusk': return ['#1a1428', '#382a48', '#605060'];
- case 'overcast': return ['#48505c', '#6a7280', '#8a94a4'];
- case 'night': return ['#02040a', '#0a1220', '#182230'];
- case 'wetnight': return ['#060a14', '#0e1626', '#1a2032'];
- case 'snownight': return ['#0a0e1a', '#1a2234', '#2a3044'];
- }
- return ['#02040a', '#0a1220', '#182230'];
- }
- function isSunVisible() {
- const m = lighting().mode;
- return m === 'day' || m === 'dawn';
- }
- function isStarVisible() {
- const m = lighting().mode;
- return m === 'night' || m === 'wetnight' || m === 'snownight' || m === 'dusk';
- }
- function isWet() {
- return state.weather === 'rain';
- }
- function weatherLabel() {
- const w = { clear:'ясно', rain:'дождь', snow:'снег', fog:'туман' }[state.weather];
- const t = state.timeOfDay === 'night' ? 'ночь' : 'день';
- return t + ', ' + w;
- }
- /* ===== ПРОПУСКА ===== */
- const firstNames = ['Иванов','Петров','Сидоров','Кузнецов','Морозов','Волков','Соколов','Лебедев'];
- const initials = ['И.И.','П.П.','С.С.','А.В.','Д.А.','М.Н.'];
- const departments = ['Администрация','Служба охраны','Бухгалтерия','Отдел кадров','ИТ-отдел','Логистика'];
- const series = ['А','Б','В','Г'];
- function randomPass() {
- return {
- num: series[Math.floor(Math.random()*series.length)]+'-'+String(Math.floor(Math.random()*900)+100),
- name: firstNames[Math.floor(Math.random()*firstNames.length)]+' '+initials[Math.floor(Math.random()*initials.length)],
- dept: departments[Math.floor(Math.random()*departments.length)],
- valid: Math.random() > 0.25
- };
- }
- function generateScenario(mode) {
- const correct = randomPass(); correct.valid = true;
- if (mode === 'breach') return { correct, presented: null, shouldAllow: false };
- const r = Math.random();
- let presented, shouldAllow;
- if (r < 0.4) { presented = { ...correct }; shouldAllow = true; }
- else if (r < 0.65) { presented = randomPass(); while (presented.num === correct.num) presented = randomPass(); shouldAllow = false; }
- else { presented = { ...correct, valid: false }; shouldAllow = false; }
- return { correct, presented, shouldAllow };
- }
- function renderPasses() {
- const el = document.getElementById('passesCompare');
- const c = state.correctPass, p = state.presentedPass;
- const card = (head, pass) => {
- 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>`;
- 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>`;
- };
- el.innerHTML = card('В БАЗЕ', c) + card('ПРЕДЪЯВЛЕН', p);
- const active = state.phase === 'waiting' && state.userDecision === null;
- document.getElementById('btnAllow').disabled = !active;
- document.getElementById('btnDeny').disabled = !active;
- state.skud.passValid = !!(c && c.valid);
- }
- function showRefuseMenu() { document.getElementById('refuseMenu').classList.add('on'); }
- function hideRefuseMenu() { document.getElementById('refuseMenu').classList.remove('on'); }
- function speedMult() {
- if (state.weather === 'rain') return 0.75;
- if (state.weather === 'snow') return 0.6;
- if (state.weather === 'fog') return 0.85;
- return 1;
- }
- /* ===== ПЕРЕКЛЮЧАТЕЛЬ МОДЕЛИ ===== */
- function renderModelSwitcher() {
- const el = document.getElementById('modelSwitcher');
- el.innerHTML = Object.values(BARRIER_MODELS).map(m => `
- <button class="model-btn ${m.code===selectedModelCode?'active':''}" data-model="${m.code}">
- <span class="mb-code">МОДЕЛЬ ${m.code}</span>
- <span class="mb-name">${m.name}</span>
- <span class="mb-spec">▸ ${m.openTime}с/${m.closeTime}с · стрела ${m.armLen}</span>
- </button>`).join('');
- el.querySelectorAll('.model-btn').forEach(b => {
- b.onclick = () => switchModel(b.dataset.model);
- });
- }
- function switchModel(code) {
- if (!BARRIER_MODELS[code]) return;
- selectedModelCode = code;
- state.model = BARRIER_MODELS[code];
- document.getElementById('modelBadge').textContent = 'МОДЕЛЬ: ' + state.model.code + ' · ' + state.model.name;
- document.getElementById('btnHold').style.display = state.model.hasHold ? '' : 'none';
- document.getElementById('btnHold').classList.remove('active');
- state.holdMode = false;
- state.skud.kw1 = state.barrier.level < 0.05;
- state.skud.kw2 = state.barrier.level > 0.95;
- if (!state.model.hasLoop) state.skud.loop1 = false;
- if (!state.model.hasPhoto) state.skud.photo = false;
- if (!state.model.hasImpact) state.skud.impact = false;
- renderModelSwitcher();
- renderModelPassport();
- logSkud('▸ ПЕРЕКЛЮЧЕНИЕ МОДЕЛИ: ' + state.model.code + ' · ' + state.model.name, 'warn');
- logSkud('Открытие/закрытие: ' + state.model.openTime + 'с / ' + state.model.closeTime + 'с');
- logSkud('Автозакрытие: ' + (state.model.autoClose ? state.model.autoClose + ' с' : 'отключено'));
- setHint('Модель <b>' + state.model.code + '</b> активна. Нажмите «Следующая машина».');
- }
- /* ===== ПАСПОРТ МОДЕЛИ ===== */
- function isTerminalActive(t) {
- const s = state.skud;
- const power = t === '+24V' || t === 'GND' || t === 'L' || t === 'N' ||
- t === 'PE' || t === 'L1' || t === 'L2' || t === 'L3';
- if (power) return s.power ? 'on' : null;
- if (t === 'OPEN') return s.relayOpen ? 'on' : null;
- if (t === 'CLOSE') return s.relayClose ? 'on' : null;
- if (t === 'STOP') return s.relayStop ? 'alarm' : null;
- if (t === 'KW1') return s.kw1 ? 'on' : null;
- if (t === 'KW2') return s.kw2 ? 'on' : null;
- if (t === 'LOOP1') return s.loop1 ? 'on' : null;
- if (t === 'LOOP2') return s.loop2 ? 'on' : null;
- if (t === 'PHOTO') return s.photo ? 'alarm' : 'on';
- if (t === 'IMPACT') return s.impact ? 'alarm' : null;
- if (t === 'ALARM') return s.alarm ? 'alarm' : null;
- if (t === 'READY') return !s.alarm ? 'on' : null;
- return null;
- }
- function renderModelPassport() {
- const m = state.model;
- if (!m) return;
- const el = document.getElementById('modelPassport');
- const rows = [
- ['Производитель', m.vendor],
- ['Серия', m.series],
- ['Тип привода', m.driveType],
- ['Питание', m.driveVoltage],
- ['Мощность', m.drivePower],
- ['Открытие', m.openTime + ' с'],
- ['Закрытие', m.closeTime + ' с'],
- ['Автозакрытие', m.autoClose ? m.autoClose + ' с' : 'нет'],
- ['Длина стрелы', m.armLen + ' см'],
- ['Защита', m.ingressProtection],
- ['Температура', m.operatingTemp],
- ['Режим работы', m.dutyCycle],
- ];
- const term = (m.terminals || []).map(t => {
- const active = isTerminalActive(t);
- const cls = active === 'on' ? 'on' : (active === 'alarm' ? 'alarm' : '');
- return `<span class="terminal-chip ${cls}">${t}</span>`;
- }).join('');
- el.innerHTML = `
- <div class="passport-title">МОДЕЛЬ ${m.code} · ${m.short}</div>
- <div class="passport-sub">${m.name} · ${m.vendor} ${m.series}</div>
- <div class="passport-desc">${m.desc}</div>
- <div class="passport-grid">
- ${rows.map(([k,v]) => `<div class="passport-row"><span class="pk">${k}</span><span class="pv">${v}</span></div>`).join('')}
- </div>
- <div style="color:#607890;font-size:10px;letter-spacing:1.5px;margin-top:12px;">КЛЕММЫ ПОДКЛЮЧЕНИЯ</div>
- <div class="terminal-list">${term}</div>
- `;
- }
- /* ===== СЦЕНАРИИ ===== */
- function nextCar() {
- if (state.phase !== 'idle') return;
- const scen = generateScenario('normal');
- state.correctPass = scen.correct;
- state.presentedPass = scen.presented;
- state.correctDecision = scen.shouldAllow ? 'allow' : 'deny';
- state.userDecision = null;
- state.senior = null;
- state.phase = 'approaching';
- hideRefuseMenu();
- const colors = ['#3a5a8a','#8a3a3a','#2a2a30','#3a6a4a','#7a6a2a','#5a3a6a'];
- state.car = {
- x: -250, speed: 160 * speedMult(),
- color: colors[Math.floor(Math.random()*colors.length)],
- wheelAngle: 0, isPriora: false
- };
- state.skud.loop1 = false; state.skud.loop2 = false;
- setStatus('ПРИБЛИЖАЕТСЯ АВТО', 'wait');
- logEvent('Приближается автомобиль' + (state.weather !== 'clear' || state.timeOfDay === 'night' ? ` (${weatherLabel()})` : ''), 'info');
- renderPasses();
- setHint('Автомобиль подъезжает.');
- }
- function breachCar() {
- if (state.phase !== 'idle') return;
- const scen = generateScenario('breach');
- state.correctPass = scen.correct;
- state.presentedPass = null;
- state.correctDecision = 'deny';
- state.userDecision = null;
- state.senior = null;
- state.phase = 'approaching';
- hideRefuseMenu();
- state.car = { x: -250, speed: 340 * speedMult(), color: '#0a0a0a', wheelAngle: 0, isPriora: true };
- setStatus('НАРУШИТЕЛЬ !', 'alarm');
- logEvent('Тёмный автомобиль не сбрасывает скорость!', 'alarm');
- renderPasses();
- setHint('Нарушитель не останавливается.');
- }
- function decide(decision) {
- if (state.phase !== 'waiting' || state.userDecision !== null) return;
- state.userDecision = decision;
- const correct = (decision === state.correctDecision);
- if (correct) state.stats.correct++; else state.stats.wrong++;
- logEvent(`Решение «${decision==='allow'?'РАЗРЕШИТЬ':'ЗАПРЕТИТЬ'}» — ${correct?'верно':'ОШИБКА'}`,
- correct?'ok':'warn');
- if (decision === 'allow') {
- state.car.action = 'pass';
- openBarrier();
- state.phase = 'resolved';
- setStatus('ПРОЕЗД РАЗРЕШЁН');
- setHint('Шлагбаум открывается, автомобиль проезжает.');
- hideRefuseMenu();
- } else {
- state.car.action = 'wait';
- state.phase = 'refuse_wait';
- setStatus('МАШИНА ЖДЁТ', 'pause');
- setHint('Объясните причину отказа или вызовите старшего смены.');
- showRefuseMenu();
- }
- renderPasses();
- }
- /* ===== ПУЛЬТ ОПЕРАТОРА ===== */
- function openBarrier() {
- if (state.skud.alarm && !state.manualMode) { logSkud('Отказ: авария не сброшена', 'warn'); return; }
- state.barrier.target = 1;
- state.barrier.moving = true;
- state.skud.relayOpen = true;
- state.skud.relayClose = false;
- state.skud.relayStop = false;
- logSkud('РЕЛЕ «ОТКРЫТЬ»: вкл');
- state.autoCloseTimer = 0;
- }
- function closeBarrier() {
- if (state.skud.alarm && !state.manualMode) { logSkud('Отказ: авария не сброшена', 'warn'); return; }
- if (state.skud.photo) {
- logSkud('Отказ закрытия: фотоэлемент разорван', 'warn');
- setHint('Фотоэлемент разорван — стрела не опускается.');
- return;
- }
- if (state.holdMode) { logSkud('Отказ: включено удержание', 'warn'); return; }
- state.barrier.target = 0;
- state.barrier.moving = true;
- state.skud.relayClose = true;
- state.skud.relayOpen = false;
- state.skud.relayStop = false;
- logSkud('РЕЛЕ «ЗАКРЫТЬ»: вкл');
- }
- function stopBarrier() {
- state.barrier.target = state.barrier.level;
- state.barrier.moving = false;
- state.skud.relayStop = true;
- state.skud.relayOpen = false;
- state.skud.relayClose = false;
- logSkud('РЕЛЕ «СТОП»: вкл — привод остановлен', 'warn');
- setHint('Шлагбаум остановлен оператором.');
- }
- function resetAlarm() {
- state.skud.alarm = false;
- state.alarm = false;
- state.skud.impact = false;
- logSkud('СБРОС АВАРИИ выполнен', 'ok');
- }
- document.getElementById('btnOpen').onclick = openBarrier;
- document.getElementById('btnClose').onclick = closeBarrier;
- document.getElementById('btnStop').onclick = stopBarrier;
- document.getElementById('btnResetAlarm').onclick = resetAlarm;
- document.getElementById('btnHold').onclick = () => {
- if (!state.model.hasHold) return;
- state.holdMode = !state.holdMode;
- document.getElementById('btnHold').classList.toggle('active', state.holdMode);
- logSkud('УДЕРЖАНИЕ: ' + (state.holdMode ? 'включено' : 'выключено'), state.holdMode ? 'warn' : 'info');
- };
- document.getElementById('btnMode').onclick = () => {
- state.manualMode = !state.manualMode;
- document.getElementById('btnMode').textContent = 'РЕЖИМ: ' + (state.manualMode ? 'РУЧНОЙ' : 'АВТО');
- document.getElementById('btnMode').classList.toggle('active', state.manualMode);
- logSkud('Режим: ' + (state.manualMode ? 'РУЧНОЙ' : 'АВТО'));
- };
- document.querySelectorAll('[data-refuse]').forEach(b => {
- b.onclick = () => {
- const action = b.dataset.refuse;
- if (action === 'explain') {
- logEvent('Оператор объяснил причину отказа водителю', 'info');
- state.stats.out++;
- state.car.action = 'turn';
- state.phase = 'resolved';
- setStatus('ОТКАЗ', 'wait');
- setHint('Машина уезжает в обратном направлении.');
- hideRefuseMenu();
- renderPasses();
- }
- if (action === 'senior') {
- state.stats.senior++;
- logEvent('Оператор вызвал старшего смены', 'senior');
- state.phase = 'senior_walk';
- const carX = state.car ? state.car.x : 0;
- state.senior = { x: W + 40, y: L().botLaneY, targetX: carX + 130, targetY: L().topLaneY,
- state: 'walk_to_lane', walking: true, alpha: 0, decideTimer: 0 };
- hideRefuseMenu();
- setStatus('СТАРШИЙ ВЫЗВАН', 'senior');
- setHint('Старший смены направляется к машине.');
- }
- };
- });
- function resetAll() {
- state.car = null; state.correctPass = null; state.presentedPass = null;
- state.userDecision = null; state.correctDecision = null;
- state.phase = 'idle'; state.barrier.target = 0; state.barrier.level = 0;
- state.alarm = false; state.sparks = []; state.police = null;
- state.tow = null; state.towDriver = null; state.towCable = false; state.leaving = false;
- state.senior = null; state.holdMode = false; state.autoCloseTimer = 0;
- state.skud = { power:true, kw1:true, kw2:false, loop1:false, loop2:false,
- photo:false, impact:false, relayOpen:false, relayClose:false, relayStop:false,
- passValid:false, alarm:false };
- document.getElementById('btnHold').classList.remove('active');
- hideRefuseMenu();
- logEntries.length = 0; renderLog(); renderPasses();
- setStatus('ПОСТ СВОБОДЕН'); setHint('Нажмите <b>«Следующая машина»</b>.');
- logEvent('Система сброшена', 'ok');
- }
- document.getElementById('btnNext').onclick = nextCar;
- document.getElementById('btnBreach').onclick = breachCar;
- document.getElementById('btnReset').onclick = resetAll;
- document.getElementById('btnAllow').onclick = () => decide('allow');
- document.getElementById('btnDeny').onclick = () => decide('deny');
- document.getElementById('btnNote').onclick = () => {
- const txt = prompt('Введите отметку для журнала:', '');
- if (txt && txt.trim()) logEvent('[ОПЕРАТОР] ' + txt.trim(), 'info');
- };
- document.getElementById('btnEndShift').onclick = () => {
- const s = state.stats;
- document.getElementById('statIn').textContent = s.in;
- document.getElementById('statOut').textContent = s.out;
- document.getElementById('statCorrect').textContent = s.correct;
- document.getElementById('statWrong').textContent = s.wrong;
- document.getElementById('statAlarms').textContent = s.alarms;
- document.getElementById('statSensors').textContent = s.sensors;
- document.getElementById('statBreaches').textContent = s.breaches;
- document.getElementById('statSenior').textContent = s.senior;
- const total = s.correct + s.wrong;
- let grade = '—';
- if (total > 0) {
- const pct = s.correct / total;
- if (pct >= 0.95) grade = 'ОТЛИЧНО';
- else if (pct >= 0.8) grade = 'ХОРОШО';
- else if (pct >= 0.6) grade = 'УДОВЛЕТВОРИТЕЛЬНО';
- else grade = 'ТРЕБУЕТСЯ ПОДГОТОВКА';
- }
- document.getElementById('statGrade').textContent = grade;
- document.getElementById('shiftModal').classList.add('on');
- logEvent('Смена завершена. Открыт отчёт.', 'info');
- };
- document.getElementById('btnCloseShift').onclick = () => {
- document.getElementById('shiftModal').classList.remove('on');
- state.stats = { in:0, out:0, correct:0, wrong:0, alarms:0, breaches:0, senior:0, sensors:0 };
- logEntries.length = 0; renderLog();
- logEvent('Начата новая смена', 'ok');
- };
- /* ===== ЖУРНАЛ: кнопки ===== */
- document.getElementById('btnLogToggle').onclick = () => {
- document.getElementById('logModal').classList.add('on');
- const el = document.getElementById('logContentFull');
- el.scrollTop = el.scrollHeight;
- updateLogBadge();
- };
- document.getElementById('btnLogClose').onclick = () => {
- document.getElementById('logModal').classList.remove('on');
- updateLogBadge();
- };
- document.getElementById('btnLogClear').onclick = () => {
- logEntries.length = 0;
- renderLog();
- logEvent('Журнал очищен оператором', 'info');
- };
- document.addEventListener('keydown', (e) => {
- if (e.key === 'F2') {
- e.preventDefault();
- document.getElementById('btnLogToggle').click();
- }
- if (e.key === 'Escape') {
- document.getElementById('logModal').classList.remove('on');
- updateLogBadge();
- }
- });
- document.getElementById('selTimeOfDay').onchange = (e) => {
- state.timeOfDay = e.target.value;
- logEvent('Время суток: ' + (state.timeOfDay === 'day' ? 'день' : 'ночь'), 'info');
- if (state.timeOfDay === 'night') logEvent('Камеры переведены в ИК-режим', 'info');
- };
- document.getElementById('selWeather').onchange = (e) => {
- state.weather = e.target.value;
- const names = { clear:'ясно', rain:'дождь', snow:'снег', fog:'туман' };
- logEvent('Погода: ' + names[state.weather], 'info');
- if (state.weather === 'fog') logEvent('Туман — сумеречный режим', 'warn');
- if (state.weather === 'rain') logEvent('Мокрое покрытие — снижение скорости ТС', 'warn');
- if (state.weather === 'snow') logEvent('Снегопад — ограничение скорости ТС', 'warn');
- state.weatherParticles = [];
- };
- /* ===== LED ===== */
- function setLed(id, on, cls) {
- const el = document.getElementById(id);
- if (!el) return;
- el.className = 'skud-led' + (on ? ' ' + (cls || 'on') : '');
- }
- function updateSkudLeds() {
- const s = state.skud;
- setLed('ledPower', s.power);
- setLed('ledArmState', true,
- state.barrier.moving ? 'yel blink' :
- (state.barrier.level > 0.9 ? 'on' : 'red'));
- setLed('ledKW1', s.kw1);
- setLed('ledKW2', s.kw2);
- setLed('ledLoop1', s.loop1, 'blu');
- setLed('ledLoop2', s.loop2, 'blu');
- setLed('ledPhoto', s.photo, 'yel');
- setLed('ledImpact', s.impact, 'red blink');
- setLed('ledRelayOpen', s.relayOpen);
- setLed('ledRelayClose', s.relayClose);
- setLed('ledRelayStop', s.relayStop, 'yel');
- setLed('ledPass', s.passValid);
- renderModelPassport();
- updateFsmTable();
- }
- /* ===== ТАБЛО FSM ===== */
- function updateFsmTable() {
- const s = state.skud;
- let fsm = 'IDLE';
- if (state.phase === 'approaching') fsm = 'APPROACH';
- else if (state.phase === 'waiting') fsm = 'WAIT_OPERATOR';
- else if (state.phase === 'resolved') fsm = 'RESOLVED';
- else if (state.phase === 'refuse_wait') fsm = 'REFUSE_WAIT';
- else if (state.phase === 'senior_walk') fsm = 'SENIOR_CALL';
- else if (state.phase === 'crash_scene') fsm = 'EMERGENCY';
- else if (state.barrier.moving) fsm = state.barrier.target > 0 ? 'OPENING' : 'CLOSING';
- else if (state.barrier.level > 0.95) fsm = 'OPEN';
- else if (state.barrier.level < 0.05) fsm = 'CLOSED';
- const el = document.getElementById('fsmState');
- if (el) el.textContent = fsm;
- const tEl = document.getElementById('fsmTimer');
- if (tEl) {
- const max = state.model ? state.model.autoClose : 0;
- tEl.textContent = max > 0
- ? state.autoCloseTimer.toFixed(1) + ' / ' + max.toFixed(1) + ' с'
- : '— отключено —';
- }
- const lEl = document.getElementById('fsmLevel');
- if (lEl) lEl.textContent = Math.round(state.barrier.level * 100) + ' %';
- const aEl = document.getElementById('fsmAlarms');
- if (aEl) {
- const count = (s.alarm ? 1 : 0) + (s.impact ? 1 : 0) +
- (s.photo && state.barrier.moving && state.barrier.target === 0 ? 1 : 0);
- aEl.textContent = count;
- aEl.style.color = count > 0 ? '#ff6060' : '#80e0ff';
- }
- }
- /* ===== ОСЦИЛЛОГРАММА ===== */
- const scopeCv = document.getElementById('scopeCanvas');
- const scopeCtx = scopeCv.getContext('2d');
- const scopeSignals = [
- { id:'relayOpen', color:'#40d080', label:'OPEN' },
- { id:'relayClose', color:'#ff4040', label:'CLOSE' },
- { id:'relayStop', color:'#ffb040', label:'STOP' },
- { id:'photo', color:'#60a0e0', label:'PHOTO' },
- { id:'loop1', color:'#a0c8ff', label:'LOOP1' }
- ];
- const scopeHistory = scopeSignals.map(() => new Array(120).fill(0));
- let scopeFrame = 0;
- function drawScope() {
- const SW = scopeCv.width, SH = scopeCv.height;
- scopeCtx.fillStyle = '#04060a';
- scopeCtx.fillRect(0, 0, SW, SH);
- scopeCtx.strokeStyle = 'rgba(40,60,80,.35)';
- scopeCtx.lineWidth = 1;
- for (let x = 0; x < SW; x += 30) {
- scopeCtx.beginPath(); scopeCtx.moveTo(x, 0); scopeCtx.lineTo(x, SH); scopeCtx.stroke();
- }
- const rowH = SH / scopeSignals.length;
- scopeSignals.forEach((sig, i) => {
- const y = rowH * i + rowH * 0.7;
- scopeCtx.strokeStyle = 'rgba(40,60,80,.35)';
- scopeCtx.beginPath(); scopeCtx.moveTo(0, y); scopeCtx.lineTo(SW, y); scopeCtx.stroke();
- const hist = scopeHistory[i];
- scopeCtx.strokeStyle = sig.color;
- scopeCtx.lineWidth = 1.6;
- scopeCtx.beginPath();
- for (let x = 0; x < hist.length; x++) {
- const px = (x / hist.length) * SW;
- const py = y - hist[x] * (rowH * 0.55);
- if (x === 0) scopeCtx.moveTo(px, py);
- else scopeCtx.lineTo(px, py);
- }
- scopeCtx.stroke();
- scopeCtx.fillStyle = sig.color;
- scopeCtx.font = 'bold 8px "Courier New"';
- scopeCtx.fillText(sig.label, 4, rowH * i + 9);
- });
- }
- function pushScopeSample() {
- scopeFrame++;
- if (scopeFrame % 4 !== 0) return;
- scopeSignals.forEach((sig, i) => {
- const val = state.skud[sig.id] ? 1 : 0;
- scopeHistory[i].shift();
- scopeHistory[i].push(val);
- });
- }
- /* ===== ФОН ===== */
- function drawSky() {
- const l = L();
- const [c1, c2, c3] = skyColors();
- const grad = ctx.createLinearGradient(0, 0, 0, l.horizonY);
- grad.addColorStop(0, c1); grad.addColorStop(0.6, c2); grad.addColorStop(1, c3);
- ctx.fillStyle = grad; ctx.fillRect(0, 0, W, l.horizonY);
- if (isStarVisible()) {
- for (let i = 0; i < 90; i++) {
- const x = (i * 173.5) % W, y = (i * 97.3) % (l.horizonY - 15);
- const a = (0.15 + 0.4 * Math.abs(Math.sin(state.time * 0.5 + i))) *
- (state.weather === 'clear' ? 1 : 0.35);
- ctx.fillStyle = `rgba(200,220,255,${a})`;
- ctx.fillRect(x, y, 1.5, 1.5);
- }
- const mx = W * 0.78, my = l.horizonY * 0.30;
- const moonG = ctx.createRadialGradient(mx, my, 3, mx, my, 55);
- moonG.addColorStop(0, 'rgba(220,230,255,.55)');
- moonG.addColorStop(0.4, 'rgba(180,200,230,.10)');
- moonG.addColorStop(1, 'rgba(180,200,230,0)');
- ctx.fillStyle = moonG; ctx.fillRect(mx - 60, my - 60, 120, 120);
- ctx.beginPath(); ctx.arc(mx, my, 12, 0, Math.PI * 2);
- ctx.fillStyle = 'rgba(240,240,220,.85)'; ctx.fill();
- ctx.beginPath(); ctx.arc(mx - 4, my - 3, 10, 0, Math.PI * 2);
- ctx.fillStyle = 'rgba(230,235,220,.75)'; ctx.fill();
- }
- if (isSunVisible()) {
- const isDawn = lighting().mode === 'dawn';
- const sy = l.horizonY * (isDawn ? 0.55 : 0.32);
- const sx = W * 0.22;
- const sg = ctx.createRadialGradient(sx, sy, 6, sx, sy, 130);
- sg.addColorStop(0, isDawn ? 'rgba(255,220,180,.9)' : 'rgba(255,250,210,.95)');
- sg.addColorStop(0.3, isDawn ? 'rgba(255,200,140,.25)' : 'rgba(255,240,180,.25)');
- sg.addColorStop(1, 'rgba(255,240,180,0)');
- ctx.fillStyle = sg; ctx.fillRect(sx - 140, sy - 140, 280, 280);
- ctx.beginPath(); ctx.arc(sx, sy, 22, 0, Math.PI * 2);
- ctx.fillStyle = isDawn ? 'rgba(255,220,180,.95)' : 'rgba(255,252,220,.98)'; ctx.fill();
- ctx.save();
- ctx.globalCompositeOperation = 'lighter';
- for (let i = 0; i < 8; i++) {
- const a = -0.4 + i * 0.1 + Math.sin(state.time * 0.3 + i) * 0.02;
- const len = 180 + (i % 3) * 40;
- ctx.beginPath();
- ctx.moveTo(sx, sy);
- ctx.lineTo(sx + Math.cos(a) * len, sy + Math.sin(a) * len);
- ctx.lineTo(sx + Math.cos(a + 0.04) * len, sy + Math.sin(a + 0.04) * len);
- ctx.closePath();
- ctx.fillStyle = isDawn ? 'rgba(255,220,180,.06)' : 'rgba(255,250,220,.05)';
- ctx.fill();
- }
- ctx.restore();
- }
- const m = lighting().mode;
- let bcol = '#0a1220';
- if (m === 'day') bcol = '#3a4a5a';
- else if (m === 'overcast') bcol = '#454e58';
- else if (m === 'dawn') bcol = '#3a4050';
- else if (m === 'dusk') bcol = '#1c1a28';
- ctx.fillStyle = bcol; ctx.globalAlpha = 0.55;
- for (let i = 0; i < 30; i++) {
- const bx = i * (W / 30), bw = 20 + (i % 5) * 14, bh = 20 + (i % 7) * 12;
- ctx.fillRect(bx, l.horizonY - bh, bw, bh);
- }
- ctx.globalAlpha = 1;
- for (let i = 0; i < 24; i++) {
- const bx = i * (W / 24), bw = 18 + (i % 4) * 12, bh = 30 + (i % 5) * 12;
- ctx.fillRect(bx, l.horizonY - bh, bw, bh);
- if (isStarVisible() && (i * 13) % 5 === 0) {
- ctx.fillStyle = 'rgba(255,200,120,.35)';
- ctx.fillRect(bx + 4, l.horizonY - bh + 8, 3, 3);
- ctx.fillRect(bx + 12, l.horizonY - bh + 20, 3, 3);
- ctx.fillStyle = bcol;
- }
- }
- }
- function drawGround() {
- const l = L();
- const m = lighting().mode;
- let g;
- if (m === 'day' || m === 'overcast') {
- g = ctx.createLinearGradient(0, l.horizonY, 0, H);
- g.addColorStop(0, '#2a3a28'); g.addColorStop(1, '#101810');
- } else if (m === 'dawn' || m === 'dusk') {
- g = ctx.createLinearGradient(0, l.horizonY, 0, H);
- g.addColorStop(0, '#1a2018'); g.addColorStop(1, '#0a0e08');
- } else {
- g = ctx.createLinearGradient(0, l.horizonY, 0, H);
- g.addColorStop(0, '#0a1410'); g.addColorStop(1, '#03060a');
- }
- ctx.fillStyle = g; ctx.fillRect(0, l.horizonY, W, H - l.horizonY);
- const haze = ctx.createLinearGradient(0, l.horizonY - 4, 0, l.horizonY + 30);
- haze.addColorStop(0, 'rgba(80,110,140,.35)');
- haze.addColorStop(1, 'rgba(80,110,140,0)');
- ctx.fillStyle = haze;
- ctx.fillRect(0, l.horizonY - 4, W, 34);
- }
- function drawRoads() {
- const l = L();
- const m = lighting().mode;
- let c1, c2, edge, dash;
- if (m === 'day') {
- c1 = ['#3a3e44', '#25282c']; c2 = ['#3a3e44', '#25282c'];
- edge = 'rgba(180,180,180,.7)'; dash = 'rgba(220,200,140,.6)';
- } else if (m === 'overcast' || m === 'dawn') {
- c1 = ['#2a2e34', '#1a1d22']; c2 = ['#2a2e34', '#1a1d22'];
- edge = 'rgba(140,150,160,.6)'; dash = 'rgba(200,180,140,.5)';
- } else if (m === 'dusk') {
- c1 = ['#1a1d24', '#0d1014']; c2 = ['#1a1d24', '#0d1014'];
- edge = 'rgba(120,130,140,.5)'; dash = 'rgba(200,180,140,.4)';
- } else {
- c1 = ['#1a1e24', '#0d1014']; c2 = ['#1a1e24', '#0d1014'];
- edge = 'rgba(120,130,140,.5)'; dash = 'rgba(220,200,140,.4)';
- }
- const g1 = ctx.createLinearGradient(0, l.topLaneTop, 0, l.topLaneBot);
- g1.addColorStop(0, c1[0]); g1.addColorStop(1, c1[1]);
- ctx.fillStyle = g1; ctx.fillRect(0, l.topLaneTop, W, l.topLaneBot - l.topLaneTop);
- const g2 = ctx.createLinearGradient(0, l.botLaneTop, 0, l.botLaneBot);
- g2.addColorStop(0, c2[0]); g2.addColorStop(1, c2[1]);
- ctx.fillStyle = g2; ctx.fillRect(0, l.botLaneTop, W, l.botLaneBot - l.botLaneTop);
- ctx.strokeStyle = edge; ctx.lineWidth = 2;
- ctx.beginPath(); ctx.moveTo(0, (l.topLaneBot + l.botLaneTop) / 2);
- ctx.lineTo(W, (l.topLaneBot + l.botLaneTop) / 2); ctx.stroke();
- ctx.strokeStyle = dash; ctx.lineWidth = 3;
- ctx.setLineDash([30, 25]);
- ctx.beginPath(); ctx.moveTo(0, (l.topLaneBot + l.botLaneTop) / 2 + 6);
- ctx.lineTo(W, (l.topLaneBot + l.botLaneTop) / 2 + 6); ctx.stroke();
- ctx.setLineDash([]);
- if (isWet()) {
- ctx.fillStyle = 'rgba(120,150,200,.06)';
- ctx.fillRect(0, l.topLaneTop, W, l.topLaneBot - l.topLaneTop);
- ctx.fillRect(0, l.botLaneTop, W, l.botLaneBot - l.botLaneTop);
- if (state.car) {
- const cx = state.car.x;
- const refl = ctx.createLinearGradient(cx, 0, cx, l.topLaneBot);
- refl.addColorStop(0, 'rgba(255,240,180,.22)');
- refl.addColorStop(1, 'rgba(255,240,180,0)');
- ctx.fillStyle = refl;
- ctx.fillRect(cx - 90, l.topLaneTop, 180, l.topLaneBot - l.topLaneTop);
- }
- }
- }
- function drawBooth() {
- const l = L();
- const bx = l.boothX, by = l.boothBaseY, bw = l.boothW, bh = l.boothH;
- const m = lighting().mode;
- const bright = (m === 'day' || m === 'overcast' || m === 'dawn');
- ctx.fillStyle = 'rgba(0,0,0,.55)';
- ctx.beginPath(); ctx.ellipse(bx + bw/2, by + 6, bw/2 + 14, 9, 0, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#0a0f14'; ctx.fillRect(bx - 8, by, bw + 16, 12);
- const wg = ctx.createLinearGradient(bx, 0, bx + bw, 0);
- if (bright) { wg.addColorStop(0, '#6a7482'); wg.addColorStop(0.5, '#8a94a4'); wg.addColorStop(1, '#4a5462'); }
- else { wg.addColorStop(0, '#2a3442'); wg.addColorStop(0.5, '#4a5a6a'); wg.addColorStop(1, '#182028'); }
- ctx.fillStyle = wg; ctx.fillRect(bx, by - bh, bw, bh);
- ctx.strokeStyle = '#4a5a70'; ctx.lineWidth = 2; ctx.strokeRect(bx, by - bh, bw, bh);
- ctx.fillStyle = '#0a0f14'; ctx.fillRect(bx - 12, by - bh - 14, bw + 24, 14);
- ctx.strokeStyle = '#3a4452'; ctx.strokeRect(bx - 12, by - bh - 14, bw + 24, 14);
- ctx.fillStyle = 'rgba(255,255,255,.08)';
- ctx.fillRect(bx - 12, by - bh - 14, bw + 24, 4);
- const glow = bright ? 0.4 : 0.8 + 0.15 * Math.sin(state.time * 1.5);
- ctx.fillStyle = `rgba(255,220,140,${glow})`;
- ctx.fillRect(bx + 15, by - bh + 25, 60, 60);
- const wgl = ctx.createLinearGradient(bx + 15, by - bh + 25, bx + 75, by - bh + 85);
- wgl.addColorStop(0, 'rgba(255,255,255,.28)');
- wgl.addColorStop(0.5, 'rgba(255,255,255,0)');
- wgl.addColorStop(1, 'rgba(255,255,255,.10)');
- ctx.fillStyle = wgl; ctx.fillRect(bx + 15, by - bh + 25, 60, 60);
- ctx.strokeStyle = '#4a5a70'; ctx.strokeRect(bx + 15, by - bh + 25, 60, 60);
- ctx.beginPath(); ctx.moveTo(bx + 45, by - bh + 25); ctx.lineTo(bx + 45, by - bh + 85); ctx.stroke();
- ctx.beginPath(); ctx.moveTo(bx + 15, by - bh + 55); ctx.lineTo(bx + 75, by - bh + 55); ctx.stroke();
- const oGlow = ctx.createRadialGradient(bx + 45, by - bh + 55, 8, bx + 45, by - bh + 55, 60);
- oGlow.addColorStop(0, `rgba(255,220,140,${0.18 * glow + 0.08})`);
- oGlow.addColorStop(1, 'rgba(255,220,140,0)');
- ctx.fillStyle = oGlow; ctx.fillRect(bx + 15 - 60, by - bh + 25 - 60, 180, 180);
- ctx.fillStyle = '#1a2028'; ctx.fillRect(bx + bw - 45, by - bh + 25, 35, bh - 25);
- ctx.strokeStyle = '#4a5a70'; ctx.strokeRect(bx + bw - 45, by - bh + 25, 35, bh - 25);
- ctx.fillStyle = '#e8c070';
- ctx.beginPath(); ctx.arc(bx + bw - 40, by - bh/2 - 5, 2.6, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = 'rgba(255,255,255,.5)';
- ctx.beginPath(); ctx.arc(bx + bw - 41, by - bh/2 - 6, 1.1, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#80a0c0'; ctx.font = 'bold 11px "Courier New"';
- ctx.textAlign = 'center';
- ctx.fillText('ОХРАНА', bx + bw/2, by - bh - 22);
- ctx.textAlign = 'left';
- }
- function drawGuard(x, y) {
- ctx.fillStyle = 'rgba(0,0,0,.55)';
- ctx.beginPath(); ctx.ellipse(x, y + 2, 14, 4, 0, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#1a2230';
- ctx.fillRect(x - 8, y - 35, 7, 35); ctx.fillRect(x + 1, y - 35, 7, 35);
- ctx.fillStyle = '#0a0a0a';
- ctx.fillRect(x - 10, y - 3, 10, 5); ctx.fillRect(x, y - 3, 10, 5);
- const bg = ctx.createLinearGradient(x - 11, 0, x + 11, 0);
- bg.addColorStop(0, '#1a3a5e'); bg.addColorStop(0.4, '#3a6a9a'); bg.addColorStop(1, '#1a3050');
- ctx.fillStyle = bg; ctx.fillRect(x - 11, y - 68, 22, 36);
- ctx.strokeStyle = '#0a1a30'; ctx.lineWidth = 1; ctx.strokeRect(x - 11, y - 68, 22, 36);
- ctx.fillStyle = '#e8e8f0';
- ctx.fillRect(x - 11, y - 68, 5, 3); ctx.fillRect(x + 6, y - 68, 5, 3);
- ctx.fillStyle = '#0a0a0a'; ctx.fillRect(x - 11, y - 38, 22, 4);
- ctx.fillStyle = '#1a3a5e';
- ctx.fillRect(x - 16, y - 66, 6, 30); ctx.fillRect(x + 10, y - 66, 6, 30);
- ctx.fillStyle = '#d0a880';
- ctx.fillRect(x - 16, y - 38, 6, 7); ctx.fillRect(x + 10, y - 38, 6, 7);
- ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 72, 6, 4);
- ctx.beginPath(); ctx.arc(x, y - 81, 9, 0, Math.PI * 2);
- const fg = ctx.createRadialGradient(x - 3, y - 83, 2, x, y - 81, 9);
- fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#b08868');
- ctx.fillStyle = fg; ctx.fill();
- ctx.fillStyle = '#1a1a1a';
- ctx.beginPath(); ctx.arc(x - 3, y - 82, 1, 0, Math.PI * 2); ctx.fill();
- ctx.beginPath(); ctx.arc(x + 3, y - 82, 1, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#0f2340';
- ctx.beginPath(); ctx.arc(x, y - 88, 10, Math.PI, Math.PI * 2); ctx.fill();
- ctx.fillRect(x - 11, y - 88, 22, 3);
- ctx.fillStyle = '#050a15'; ctx.fillRect(x - 12, y - 85, 24, 3);
- ctx.fillStyle = '#c0a040'; ctx.beginPath(); ctx.arc(x, y - 90, 2, 0, Math.PI * 2); ctx.fill();
- 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();
- }
- function drawBarrier() {
- const l = L();
- const pillarX = l.pillarX, pillarW = l.pillarW, pillarH = l.pillarH;
- const groundBase = l.topLaneY;
- const pillarTopY = groundBase - pillarH;
- const pivotX = pillarX, pivotY = groundBase - 80;
- const armLen = (state.model ? state.model.armLen : 240);
- const armH = l.armH;
- const level = state.barrier.level;
- const m = lighting().mode;
- const bright = (m === 'day' || m === 'overcast' || m === 'dawn');
- ctx.fillStyle = 'rgba(0,0,0,.55)';
- ctx.beginPath(); ctx.ellipse(pillarX + pillarW/2, groundBase + 4, 26, 8, 0, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#1a1e24'; ctx.fillRect(pillarX - 10, groundBase - 10, pillarW + 20, 16);
- ctx.strokeStyle = '#3a4858'; ctx.lineWidth = 1.5; ctx.strokeRect(pillarX - 10, groundBase - 10, pillarW + 20, 16);
- ctx.fillStyle = 'rgba(255,255,255,.06)';
- ctx.fillRect(pillarX - 10, groundBase - 10, pillarW + 20, 3);
- const g = ctx.createLinearGradient(pillarX, 0, pillarX + pillarW, 0);
- if (bright) { g.addColorStop(0, '#3a4858'); g.addColorStop(0.35, '#7a8898'); g.addColorStop(0.6, '#5a6878'); g.addColorStop(1, '#2a3442'); }
- else { g.addColorStop(0, '#1a2028'); g.addColorStop(0.35, '#4a5a6a'); g.addColorStop(0.6, '#2a3442'); g.addColorStop(1, '#0a1018'); }
- ctx.fillStyle = g; ctx.fillRect(pillarX, pillarTopY, pillarW, pillarH);
- ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.strokeRect(pillarX, pillarTopY, pillarW, pillarH);
- const shine = ctx.createLinearGradient(pillarX + 4, 0, pillarX + 12, 0);
- shine.addColorStop(0, 'rgba(255,255,255,.18)');
- shine.addColorStop(1, 'rgba(255,255,255,0)');
- ctx.fillStyle = shine; ctx.fillRect(pillarX + 4, pillarTopY + 4, 8, pillarH - 8);
- for (let i = 0; i < 4; i++) {
- ctx.fillStyle = i % 2 === 0 ? '#a08030' : '#1a1a1a';
- ctx.fillRect(pillarX, pillarTopY + pillarH - 90 + i * 20, pillarW, 20);
- }
- ctx.fillStyle = 'rgba(255,220,140,.12)';
- ctx.fillRect(pillarX, pillarTopY + pillarH - 90, pillarW, 80);
- ctx.fillStyle = '#505860';
- ctx.beginPath();
- ctx.moveTo(pillarX - 5, pillarTopY); ctx.lineTo(pillarX + pillarW + 5, pillarTopY);
- ctx.lineTo(pillarX + pillarW, pillarTopY - 12); ctx.lineTo(pillarX, pillarTopY - 12);
- ctx.closePath(); ctx.fill();
- ctx.strokeStyle = '#000'; ctx.stroke();
- ctx.fillStyle = 'rgba(255,255,255,.12)';
- ctx.fillRect(pillarX, pillarTopY - 12, pillarW, 3);
- const blink = Math.sin(state.time * 4) > 0;
- const lampColor = level > 0.9 ? '#40d080' : (level > 0.1 ? '#ffb040' : '#ff2020');
- const lampX = pillarX + pillarW/2, lampY = pillarTopY - 26;
- if (blink) {
- const lampGlow = ctx.createRadialGradient(lampX, lampY, 3, lampX, lampY, 40);
- lampGlow.addColorStop(0, lampColor + 'cc');
- lampGlow.addColorStop(0.4, lampColor + '44');
- lampGlow.addColorStop(1, lampColor + '00');
- ctx.fillStyle = lampGlow;
- ctx.fillRect(lampX - 44, lampY - 44, 88, 88);
- }
- ctx.beginPath(); ctx.arc(lampX, lampY, 9, 0, Math.PI * 2);
- const lampGrad = ctx.createRadialGradient(lampX - 3, lampY - 3, 2, lampX, lampY, 9);
- if (blink) {
- lampGrad.addColorStop(0, '#ffffff');
- lampGrad.addColorStop(0.4, lampColor);
- lampGrad.addColorStop(1, lampColor);
- } else {
- lampGrad.addColorStop(0, '#3a3a3a');
- lampGrad.addColorStop(1, '#1a1a1a');
- }
- ctx.fillStyle = lampGrad; ctx.fill();
- ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
- ctx.beginPath(); ctx.arc(lampX - 3, lampY - 3, 2.5, 0, Math.PI * 2);
- ctx.fillStyle = 'rgba(255,255,255,.6)'; ctx.fill();
- ctx.beginPath(); ctx.arc(pivotX, pivotY, 15, 0, Math.PI * 2);
- ctx.fillStyle = '#2a3442'; ctx.fill();
- ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
- ctx.beginPath(); ctx.arc(pivotX, pivotY, 6, 0, Math.PI * 2);
- const hg = ctx.createRadialGradient(pivotX - 2, pivotY - 2, 1, pivotX, pivotY, 6);
- hg.addColorStop(0, '#c8d4e0'); hg.addColorStop(1, '#506070');
- ctx.fillStyle = hg; ctx.fill();
- ctx.beginPath(); ctx.arc(pivotX - 3, pivotY - 3, 2, 0, Math.PI * 2);
- ctx.fillStyle = 'rgba(255,255,255,.5)'; ctx.fill();
- const angle = Math.PI + level * Math.PI / 2;
- ctx.save();
- ctx.translate(pivotX, pivotY); ctx.rotate(angle);
- const seg = 14;
- for (let i = 0; i < seg; i++) {
- ctx.fillStyle = i % 2 === 0 ? '#e04040' : '#e8e8e8';
- ctx.fillRect(i * (armLen / seg), -armH/2, armLen / seg, armH);
- }
- ctx.fillStyle = 'rgba(0,0,0,.35)';
- ctx.fillRect(0, armH/2 - 2, armLen, 2);
- ctx.fillStyle = 'rgba(255,255,255,.4)';
- ctx.fillRect(0, -armH/2, armLen, 2);
- ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.strokeRect(0, -armH/2, armLen, armH);
- ctx.beginPath(); ctx.arc(armLen, 0, 12, 0, Math.PI * 2);
- const ng = ctx.createRadialGradient(armLen - 3, -3, 2, armLen, 0, 12);
- ng.addColorStop(0, '#ffffff'); ng.addColorStop(1, '#a8b0b8');
- ctx.fillStyle = ng; ctx.fill();
- ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
- ctx.beginPath(); ctx.arc(armLen, 0, 5, 0, Math.PI * 2);
- ctx.fillStyle = '#8090a0'; ctx.fill();
- ctx.restore();
- if (level < 0.9) {
- const endX = pivotX + Math.cos(angle) * armLen;
- const endY = pivotY + Math.sin(angle) * armLen;
- const blinkE = Math.sin(state.time * 6) > 0;
- if (blinkE) {
- const eGlow = ctx.createRadialGradient(endX, endY, 2, endX, endY, 26);
- eGlow.addColorStop(0, 'rgba(255,64,64,.55)');
- eGlow.addColorStop(1, 'rgba(255,64,64,0)');
- ctx.fillStyle = eGlow;
- ctx.fillRect(endX - 28, endY - 28, 56, 56);
- }
- ctx.beginPath(); ctx.arc(endX, endY, 6, 0, Math.PI * 2);
- ctx.fillStyle = blinkE ? '#ff4040' : '#601010'; ctx.fill();
- ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
- }
- if (state.model && state.model.hasLoop) {
- const loopX = l.pivotX - 280;
- const loopY = l.topLaneY + 10;
- const loopW = 200, loopH = 18;
- ctx.strokeStyle = state.skud.loop1 ? 'rgba(96,180,255,.85)' : 'rgba(96,160,224,.35)';
- ctx.lineWidth = 2;
- ctx.strokeRect(loopX, loopY, loopW, loopH);
- if (state.skud.loop1) {
- ctx.fillStyle = 'rgba(96,180,255,.18)';
- ctx.fillRect(loopX, loopY, loopW, loopH);
- const lg = ctx.createRadialGradient(loopX + loopW/2, loopY + loopH/2, 5, loopX + loopW/2, loopY + loopH/2, loopW/2);
- lg.addColorStop(0, 'rgba(120,200,255,.18)');
- lg.addColorStop(1, 'rgba(120,200,255,0)');
- ctx.fillStyle = lg;
- ctx.fillRect(loopX - 30, loopY - 30, loopW + 60, loopH + 60);
- }
- }
- if (state.model && state.model.hasPhoto) {
- const px1 = l.pivotX - state.model.armLen - 5;
- const px2 = l.pivotX + 55;
- const py = l.topLaneY - 4;
- ctx.fillStyle = '#304050';
- ctx.fillRect(px1 - 3, py - 44, 6, 44);
- ctx.fillRect(px2 - 3, py - 44, 6, 44);
- ctx.strokeStyle = '#506070'; ctx.lineWidth = 1;
- ctx.strokeRect(px1 - 3, py - 44, 6, 44);
- ctx.strokeRect(px2 - 3, py - 44, 6, 44);
- const on = !state.skud.photo;
- const dCol = on ? '#40d080' : '#ff4040';
- if (on) {
- const dg = ctx.createRadialGradient(px1, py - 36, 2, px1, py - 36, 16);
- dg.addColorStop(0, dCol + 'aa'); dg.addColorStop(1, dCol + '00');
- ctx.fillStyle = dg;
- ctx.fillRect(px1 - 18, py - 54, 36, 36);
- ctx.fillRect(px2 - 18, py - 54, 36, 36);
- }
- ctx.fillStyle = dCol;
- ctx.beginPath(); ctx.arc(px1, py - 36, 3.5, 0, Math.PI * 2); ctx.fill();
- ctx.beginPath(); ctx.arc(px2, py - 36, 3.5, 0, Math.PI * 2); ctx.fill();
- ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
- ctx.beginPath(); ctx.arc(px1, py - 36, 3.5, 0, Math.PI * 2); ctx.stroke();
- ctx.beginPath(); ctx.arc(px2, py - 36, 3.5, 0, Math.PI * 2); ctx.stroke();
- if (on) {
- ctx.strokeStyle = 'rgba(64,208,128,.6)';
- ctx.setLineDash([4,4]); ctx.lineWidth = 1.2;
- ctx.beginPath(); ctx.moveTo(px1, py - 36); ctx.lineTo(px2, py - 36); ctx.stroke();
- ctx.setLineDash([]);
- } else {
- ctx.strokeStyle = 'rgba(255,64,64,.5)';
- ctx.setLineDash([2,6]);
- ctx.beginPath(); ctx.moveTo(px1, py - 36); ctx.lineTo(px2, py - 36); ctx.stroke();
- ctx.setLineDash([]);
- }
- }
- }
- /* ===== МАШИНЫ ===== */
- function drawWheels(x, y, dxRear, dxFront, radius, rim, angle) {
- [x - dxRear, x + dxFront].forEach(wx => {
- ctx.beginPath(); ctx.arc(wx, y, radius, 0, Math.PI * 2);
- const tg = ctx.createRadialGradient(wx - radius*0.3, y - radius*0.3, 1, wx, y, radius);
- tg.addColorStop(0, '#2a2a2a'); tg.addColorStop(1, '#000');
- ctx.fillStyle = tg; ctx.fill();
- ctx.beginPath(); ctx.arc(wx, y, radius * 0.62, 0, Math.PI * 2);
- ctx.fillStyle = '#1a2028'; ctx.fill();
- ctx.strokeStyle = rim; ctx.lineWidth = 1.5; ctx.stroke();
- ctx.save(); ctx.translate(wx, y); ctx.rotate(angle);
- ctx.strokeStyle = rim; ctx.lineWidth = 1.4; ctx.lineCap = 'round';
- for (let i = 0; i < 5; i++) {
- const a = (i / 5) * Math.PI * 2;
- ctx.beginPath(); ctx.moveTo(0, 0);
- ctx.lineTo(Math.cos(a) * radius * 0.5, Math.sin(a) * radius * 0.5); ctx.stroke();
- }
- ctx.beginPath(); ctx.arc(0, 0, radius * 0.16, 0, Math.PI * 2);
- ctx.fillStyle = rim; ctx.fill();
- ctx.strokeStyle = '#303840'; ctx.lineWidth = 1; ctx.stroke();
- ctx.restore();
- ctx.beginPath(); ctx.arc(wx - radius*0.35, y - radius*0.35, radius*0.18, 0, Math.PI * 2);
- ctx.fillStyle = 'rgba(255,255,255,.10)'; ctx.fill();
- });
- }
- function drawSedan(x, y, color, wheelAngle) {
- ctx.fillStyle = 'rgba(0,0,0,.55)';
- ctx.beginPath(); ctx.ellipse(x, y + 4, 92, 7, 0, 0, Math.PI * 2); ctx.fill();
- const bg = ctx.createLinearGradient(0, y - 55, 0, y);
- bg.addColorStop(0, color); bg.addColorStop(0.55, color); bg.addColorStop(1, '#050505');
- ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(x + 85, y - 8); ctx.lineTo(x + 78, y - 26); ctx.lineTo(x + 40, y - 30);
- ctx.lineTo(x + 15, y - 52); ctx.lineTo(x - 25, y - 54); ctx.lineTo(x - 55, y - 42);
- ctx.lineTo(x - 78, y - 24); ctx.lineTo(x - 85, y - 8); ctx.lineTo(x - 85, y);
- ctx.lineTo(x + 85, y); ctx.closePath(); ctx.fill(); ctx.stroke();
- ctx.fillStyle = 'rgba(255,255,255,.18)';
- ctx.beginPath();
- ctx.moveTo(x - 70, y - 22); ctx.lineTo(x + 70, y - 22);
- ctx.lineTo(x + 65, y - 26); ctx.lineTo(x - 65, y - 26);
- ctx.closePath(); ctx.fill();
- const gk = ctx.createLinearGradient(0, y - 54, 0, y - 30);
- gk.addColorStop(0, 'rgba(90,140,180,.85)');
- gk.addColorStop(1, 'rgba(30,60,90,.75)');
- ctx.fillStyle = gk;
- ctx.beginPath();
- ctx.moveTo(x + 12, y - 50); ctx.lineTo(x - 22, y - 52); ctx.lineTo(x - 48, y - 40);
- ctx.lineTo(x - 48, y - 30); ctx.lineTo(x + 12, y - 30); ctx.closePath(); ctx.fill();
- ctx.fillStyle = 'rgba(255,255,255,.20)';
- ctx.beginPath();
- ctx.moveTo(x + 8, y - 48); ctx.lineTo(x - 18, y - 50);
- ctx.lineTo(x - 30, y - 44); ctx.lineTo(x - 20, y - 44);
- ctx.closePath(); ctx.fill();
- ctx.strokeStyle = 'rgba(0,0,0,.4)'; ctx.lineWidth = 1;
- ctx.beginPath(); ctx.moveTo(x - 8, y - 30); ctx.lineTo(x - 8, y - 8); ctx.stroke();
- ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 18;
- ctx.beginPath(); ctx.arc(x + 80, y - 14, 4.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
- ctx.fillStyle = 'rgba(255,255,255,.9)';
- ctx.beginPath(); ctx.arc(x + 79, y - 15, 1.8, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 10;
- ctx.fillRect(x - 85, y - 22, 4, 10); ctx.shadowBlur = 0;
- ctx.fillStyle = 'rgba(255,140,140,.7)';
- ctx.fillRect(x - 84, y - 21, 2, 3);
- drawWheels(x, y, 55, 55, 14, '#a0a8b0', wheelAngle);
- }
- function drawDarkCar(x, y, wheelAngle) {
- ctx.fillStyle = 'rgba(0,0,0,.65)';
- ctx.beginPath(); ctx.ellipse(x, y + 4, 108, 8, 0, 0, Math.PI * 2); ctx.fill();
- const bg = ctx.createLinearGradient(0, y - 58, 0, y);
- bg.addColorStop(0, '#3a3a3a');
- bg.addColorStop(0.25, '#1a1a1a');
- bg.addColorStop(0.7, '#050505');
- bg.addColorStop(1, '#000');
- ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(x - 105, y - 10); ctx.lineTo(x - 100, y - 28);
- ctx.lineTo(x - 80, y - 34); ctx.lineTo(x - 55, y - 42);
- ctx.lineTo(x - 35, y - 54); ctx.lineTo(x + 25, y - 54);
- ctx.lineTo(x + 45, y - 42); ctx.lineTo(x + 55, y - 34);
- ctx.lineTo(x + 92, y - 30); ctx.lineTo(x + 105, y - 12);
- ctx.lineTo(x + 105, y); ctx.lineTo(x - 105, y);
- ctx.closePath(); ctx.fill(); ctx.stroke();
- ctx.strokeStyle = 'rgba(220,230,240,.7)'; ctx.lineWidth = 1.5;
- ctx.beginPath(); ctx.moveTo(x - 100, y - 22); ctx.lineTo(x + 100, y - 22); ctx.stroke();
- ctx.strokeStyle = 'rgba(255,255,255,.25)'; ctx.lineWidth = 1;
- ctx.beginPath(); ctx.moveTo(x - 100, y - 23); ctx.lineTo(x + 100, y - 23); ctx.stroke();
- const gk = ctx.createLinearGradient(0, y - 54, 0, y - 28);
- gk.addColorStop(0, 'rgba(20,40,60,.95)');
- gk.addColorStop(0.5, 'rgba(5,10,20,.95)');
- gk.addColorStop(1, 'rgba(2,4,8,.95)');
- ctx.fillStyle = gk;
- ctx.beginPath();
- ctx.moveTo(x + 45, y - 40); ctx.lineTo(x + 18, y - 40);
- ctx.lineTo(x + 18, y - 30); ctx.lineTo(x + 52, y - 30);
- ctx.closePath(); ctx.fill();
- ctx.beginPath();
- ctx.moveTo(x - 55, y - 40); ctx.lineTo(x - 35, y - 50);
- ctx.lineTo(x - 10, y - 50); ctx.lineTo(x - 10, y - 30);
- ctx.lineTo(x - 55, y - 30); ctx.closePath(); ctx.fill();
- ctx.fillStyle = 'rgba(120,180,220,.35)';
- ctx.beginPath();
- ctx.moveTo(x - 50, y - 42); ctx.lineTo(x - 40, y - 48);
- ctx.lineTo(x - 30, y - 48); ctx.lineTo(x - 40, y - 42);
- ctx.closePath(); ctx.fill();
- ctx.fillStyle = 'rgba(120,180,220,.30)';
- ctx.beginPath();
- ctx.moveTo(x + 22, y - 38); ctx.lineTo(x + 30, y - 38);
- ctx.lineTo(x + 30, y - 32); ctx.lineTo(x + 22, y - 32);
- ctx.closePath(); ctx.fill();
- ctx.fillStyle = '#0a0a0a'; ctx.fillRect(x + 15, y - 50, 4, 20);
- ctx.fillStyle = '#050505';
- ctx.strokeStyle = '#404040';
- ctx.lineWidth = 1;
- ctx.fillRect(x + 82, y - 26, 20, 12);
- ctx.strokeRect(x + 82, y - 26, 20, 12);
- for (let i = 0; i < 3; i++) {
- ctx.strokeStyle = '#505860';
- ctx.beginPath();
- ctx.moveTo(x + 83, y - 24 + i * 4);
- ctx.lineTo(x + 101, y - 24 + i * 4);
- ctx.stroke();
- }
- ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 22;
- ctx.beginPath();
- ctx.moveTo(x + 84, y - 22); ctx.lineTo(x + 100, y - 22);
- ctx.lineTo(x + 100, y - 12); ctx.lineTo(x + 84, y - 12);
- ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0;
- ctx.fillStyle = '#c0e8ff'; ctx.fillRect(x + 86, y - 20, 12, 2);
- ctx.fillStyle = 'rgba(255,255,255,.7)';
- ctx.fillRect(x + 90, y - 21, 8, 1.5);
- ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 18;
- ctx.fillRect(x - 105, y - 24, 6, 14); ctx.shadowBlur = 0;
- ctx.fillStyle = 'rgba(255,160,160,.6)';
- ctx.fillRect(x - 104, y - 23, 3, 4);
- drawWheels(x, y, 62, 58, 17, '#c8d0d8', wheelAngle);
- }
- function drawPoliceCar(x, y, wheelAngle) {
- ctx.fillStyle = 'rgba(0,0,0,.6)';
- ctx.beginPath(); ctx.ellipse(x, y + 4, 105, 8, 0, 0, Math.PI * 2); ctx.fill();
- const bg = ctx.createLinearGradient(0, y - 58, 0, y);
- bg.addColorStop(0, '#f04444');
- bg.addColorStop(0.35, '#c82020');
- bg.addColorStop(0.75, '#700a0a');
- bg.addColorStop(1, '#400404');
- ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(x + 105, y - 10); ctx.lineTo(x + 100, y - 28);
- ctx.lineTo(x + 80, y - 34); ctx.lineTo(x + 55, y - 42);
- ctx.lineTo(x + 35, y - 54); ctx.lineTo(x - 25, y - 54);
- ctx.lineTo(x - 45, y - 42); ctx.lineTo(x - 55, y - 34);
- ctx.lineTo(x - 92, y - 30); ctx.lineTo(x - 105, y - 12);
- ctx.lineTo(x - 105, y); ctx.lineTo(x + 105, y);
- ctx.closePath(); ctx.fill(); ctx.stroke();
- ctx.fillStyle = 'rgba(255,255,255,.20)';
- ctx.beginPath();
- ctx.moveTo(x - 90, y - 20); ctx.lineTo(x + 90, y - 20);
- ctx.lineTo(x + 85, y - 24); ctx.lineTo(x - 85, y - 24);
- ctx.closePath(); ctx.fill();
- const gk = ctx.createLinearGradient(0, y - 54, 0, y - 30);
- gk.addColorStop(0, 'rgba(70,120,170,.85)');
- gk.addColorStop(1, 'rgba(20,40,70,.85)');
- ctx.fillStyle = gk;
- ctx.beginPath();
- ctx.moveTo(x - 45, y - 40); ctx.lineTo(x - 18, y - 40);
- ctx.lineTo(x - 18, y - 30); ctx.lineTo(x - 52, y - 30);
- ctx.closePath(); ctx.fill();
- ctx.beginPath();
- ctx.moveTo(x + 55, y - 40); ctx.lineTo(x + 35, y - 50);
- ctx.lineTo(x + 10, y - 50); ctx.lineTo(x + 10, y - 30);
- ctx.lineTo(x + 55, y - 30); ctx.closePath(); ctx.fill();
- ctx.fillRect(x - 12, y - 30, 25, 10);
- ctx.save();
- ctx.fillStyle = '#ffffff';
- ctx.font = 'bold 11px "Courier New"';
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
- ctx.shadowColor = 'rgba(0,0,0,.9)'; ctx.shadowBlur = 3;
- ctx.fillText('ПОЛИЦИЯ', x - 40, y - 12);
- ctx.fillText('ПОЛИЦИЯ', x + 30, y - 12);
- ctx.restore();
- ctx.fillStyle = '#1a3a80'; ctx.fillRect(x - 95, y - 6, 190, 3);
- ctx.fillStyle = 'rgba(120,180,255,.4)'; ctx.fillRect(x - 95, y - 6, 190, 1);
- ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 20;
- ctx.fillRect(x - 100, y - 20, 14, 6); ctx.shadowBlur = 0;
- ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 14;
- ctx.fillRect(x + 100, y - 24, 5, 14); ctx.shadowBlur = 0;
- const blink = Math.sin(state.time * 12) > 0;
- ctx.fillStyle = '#1a1a1a';
- ctx.fillRect(x - 30, y - 60, 60, 5);
- if (blink) {
- const rg = ctx.createRadialGradient(x - 15, y - 68, 4, x - 15, y - 68, 50);
- rg.addColorStop(0, 'rgba(255,50,50,.9)');
- rg.addColorStop(1, 'rgba(255,50,50,0)');
- ctx.fillStyle = rg; ctx.fillRect(x - 65, y - 118, 100, 100);
- }
- ctx.fillStyle = blink ? '#ff2020' : '#401010';
- ctx.beginPath();
- ctx.moveTo(x - 30, y - 60); ctx.lineTo(x, y - 60);
- ctx.lineTo(x, y - 70); ctx.lineTo(x - 30, y - 70);
- ctx.closePath(); ctx.fill();
- ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
- if (!blink) {
- const bg2 = ctx.createRadialGradient(x + 15, y - 68, 4, x + 15, y - 68, 50);
- bg2.addColorStop(0, 'rgba(50,120,255,.9)');
- bg2.addColorStop(1, 'rgba(50,120,255,0)');
- ctx.fillStyle = bg2; ctx.fillRect(x - 35, y - 118, 100, 100);
- }
- ctx.fillStyle = !blink ? '#2060ff' : '#101040';
- ctx.beginPath();
- ctx.moveTo(x, y - 60); ctx.lineTo(x + 30, y - 60);
- ctx.lineTo(x + 30, y - 70); ctx.lineTo(x, y - 70);
- ctx.closePath(); ctx.fill();
- ctx.strokeStyle = '#000'; ctx.stroke();
- const beamGrad = ctx.createLinearGradient(x - 105, y - 14, x - 300, y + 10);
- beamGrad.addColorStop(0, 'rgba(255,240,180,.5)');
- beamGrad.addColorStop(1, 'rgba(255,240,180,0)');
- ctx.fillStyle = beamGrad;
- ctx.beginPath();
- ctx.moveTo(x - 105, y - 20); ctx.lineTo(x - 300, y - 28);
- ctx.lineTo(x - 300, y + 20); ctx.lineTo(x - 105, y - 4);
- ctx.closePath(); ctx.fill();
- drawWheels(x, y, 65, 60, 15, '#606060', wheelAngle);
- }
- function drawTowTruck(x, y, wheelAngle, doorOpen) {
- ctx.fillStyle = 'rgba(0,0,0,.55)';
- ctx.beginPath(); ctx.ellipse(x, y + 4, 105, 7, 0, 0, Math.PI * 2); ctx.fill();
- const bg = ctx.createLinearGradient(0, y - 60, 0, y);
- bg.addColorStop(0, '#e8c840'); bg.addColorStop(0.4, '#c8a020'); bg.addColorStop(1, '#705010');
- ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(x - 100, y - 8); ctx.lineTo(x - 100, y - 55);
- ctx.lineTo(x - 55, y - 55); ctx.lineTo(x - 40, y - 25);
- ctx.lineTo(x - 20, y - 25); ctx.lineTo(x - 20, y - 8);
- ctx.lineTo(x - 20, y); ctx.lineTo(x - 100, y);
- ctx.closePath(); ctx.fill(); ctx.stroke();
- const gk = ctx.createLinearGradient(0, y - 50, 0, y - 28);
- gk.addColorStop(0, 'rgba(90,140,180,.85)');
- gk.addColorStop(1, 'rgba(30,60,90,.8)');
- ctx.fillStyle = gk;
- ctx.beginPath();
- ctx.moveTo(x - 95, y - 50); ctx.lineTo(x - 60, y - 50);
- ctx.lineTo(x - 48, y - 28); ctx.lineTo(x - 95, y - 28);
- ctx.closePath(); ctx.fill();
- ctx.fillStyle = 'rgba(255,255,255,.20)';
- ctx.beginPath();
- ctx.moveTo(x - 90, y - 48); ctx.lineTo(x - 75, y - 48);
- ctx.lineTo(x - 80, y - 32); ctx.lineTo(x - 90, y - 32);
- ctx.closePath(); ctx.fill();
- const pg = ctx.createLinearGradient(0, y - 20, 0, y - 8);
- pg.addColorStop(0, '#d0b040'); pg.addColorStop(1, '#705010');
- ctx.fillStyle = pg; ctx.fillRect(x + 20, y - 18, 85, 10);
- ctx.strokeStyle = '#000'; ctx.strokeRect(x + 20, y - 18, 85, 10);
- ctx.fillStyle = 'rgba(255,255,255,.15)';
- ctx.fillRect(x + 20, y - 18, 85, 2);
- ctx.strokeStyle = '#806010'; ctx.lineWidth = 5;
- ctx.beginPath();
- ctx.moveTo(x + 5, y - 18); ctx.lineTo(x + 40, y - 55); ctx.lineTo(x + 80, y - 55); ctx.stroke();
- ctx.strokeStyle = '#c0a020'; ctx.lineWidth = 2;
- ctx.beginPath(); ctx.moveTo(x + 80, y - 55); ctx.lineTo(x + 90, y - 32); ctx.stroke();
- ctx.strokeStyle = '#a0a8b0';
- ctx.beginPath(); ctx.arc(x + 90, y - 28, 4, 0, Math.PI * 1.5); ctx.stroke();
- const blink = Math.sin(state.time * 8) > 0;
- if (blink) {
- const mg = ctx.createRadialGradient(x - 59, y - 59, 3, x - 59, y - 59, 40);
- mg.addColorStop(0, 'rgba(255,180,70,.9)');
- mg.addColorStop(1, 'rgba(255,180,70,0)');
- ctx.fillStyle = mg;
- ctx.fillRect(x - 100, y - 100, 80, 80);
- }
- ctx.fillStyle = blink ? '#ffb040' : '#604010';
- ctx.fillRect(x - 65, y - 62, 12, 6);
- ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 15;
- ctx.beginPath(); ctx.arc(x - 95, y - 14, 4, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
- ctx.fillStyle = 'rgba(255,255,255,.7)';
- ctx.beginPath(); ctx.arc(x - 96, y - 15, 1.5, 0, Math.PI * 2); ctx.fill();
- drawWheels(x, y, 70, 25, 14, '#606060', wheelAngle);
- const wx = x + 60;
- ctx.beginPath(); ctx.arc(wx, y, 14, 0, Math.PI * 2);
- ctx.fillStyle = '#0a0a0a'; ctx.fill();
- ctx.beginPath(); ctx.arc(wx, y, 5, 0, Math.PI * 2);
- ctx.fillStyle = '#606060'; ctx.fill();
- }
- function drawTowDriver(x, y, walking, alpha) {
- ctx.save();
- ctx.globalAlpha = alpha !== undefined ? Math.max(0, Math.min(1, alpha)) : 1;
- const wobble = walking ? Math.sin(state.time * 10) * 2 : 0;
- ctx.fillStyle = 'rgba(0,0,0,.5)';
- ctx.beginPath(); ctx.ellipse(x, y + 2, 13, 4, 0, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#252c38';
- ctx.fillRect(x - 8, y - 33, 7, 29 + wobble);
- ctx.fillRect(x + 1, y - 33, 7, 29 - wobble);
- ctx.fillStyle = '#0a0a0a';
- ctx.fillRect(x - 10, y + 1 + wobble, 11, 3);
- ctx.fillRect(x, y + 1 - wobble, 11, 3);
- const jg = ctx.createLinearGradient(x - 11, 0, x + 11, 0);
- jg.addColorStop(0, '#b06818'); jg.addColorStop(0.4, '#f09030'); jg.addColorStop(1, '#a85810');
- ctx.fillStyle = jg; ctx.fillRect(x - 10, y - 63, 20, 33);
- ctx.strokeStyle = '#703810'; ctx.lineWidth = 1; ctx.strokeRect(x - 10, y - 63, 20, 33);
- ctx.fillStyle = '#f0f0f0';
- ctx.fillRect(x - 10, y - 56, 20, 3);
- ctx.fillRect(x - 10, y - 44, 20, 3);
- ctx.fillStyle = '#d0a880';
- ctx.fillRect(x - 16, y - 60, 6, 23);
- ctx.fillRect(x + 10, y - 60, 6, 23);
- ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 67, 6, 5);
- ctx.beginPath(); ctx.arc(x, y - 76, 9, 0, Math.PI * 2);
- const fg = ctx.createRadialGradient(x - 3, y - 78, 2, x, y - 76, 9);
- fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#c09870');
- ctx.fillStyle = fg; ctx.fill();
- ctx.fillStyle = '#e8c020';
- ctx.beginPath(); ctx.arc(x, y - 82, 11, Math.PI, 0); ctx.fill();
- ctx.fillRect(x - 11, y - 83, 22, 4);
- ctx.strokeStyle = '#8a6a10'; ctx.lineWidth = 1;
- ctx.beginPath(); ctx.arc(x, y - 82, 11, Math.PI, 0); ctx.stroke();
- ctx.fillStyle = '#c0a010';
- ctx.fillRect(x - 1.5, y - 93, 3, 12);
- ctx.restore();
- }
- function drawSenior(x, y, walking, alpha) {
- ctx.save();
- ctx.globalAlpha = alpha !== undefined ? Math.max(0, Math.min(1, alpha)) : 1;
- const wobble = walking ? Math.sin(state.time * 9) * 2 : 0;
- ctx.fillStyle = 'rgba(0,0,0,.55)';
- ctx.beginPath(); ctx.ellipse(x, y + 2, 15, 5, 0, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#1a2340';
- ctx.fillRect(x - 8, y - 35, 7, 31 + wobble);
- ctx.fillRect(x + 1, y - 35, 7, 31 - wobble);
- ctx.fillStyle = '#0a0a0a';
- ctx.fillRect(x - 10, y - 3 + wobble, 11, 7);
- ctx.fillRect(x + 1, y - 3 - wobble, 11, 7);
- const un = ctx.createLinearGradient(x - 12, 0, x + 12, 0);
- un.addColorStop(0, '#1a2a4a'); un.addColorStop(0.4, '#2a3a5a'); un.addColorStop(1, '#141e38');
- ctx.fillStyle = un; ctx.fillRect(x - 12, y - 70, 24, 38);
- ctx.strokeStyle = '#0a1428'; ctx.lineWidth = 1; ctx.strokeRect(x - 12, y - 70, 24, 38);
- ctx.fillStyle = '#c0a040';
- for (let i = 0; i < 3; i++) {
- ctx.beginPath(); ctx.arc(x, y - 60 + i * 10, 1.2, 0, Math.PI * 2); ctx.fill();
- }
- ctx.fillStyle = '#e0e0f0';
- ctx.fillRect(x - 13, y - 70, 6, 3);
- ctx.fillRect(x + 7, y - 70, 6, 3);
- ctx.fillStyle = '#ffd060';
- ctx.beginPath(); ctx.arc(x - 10, y - 71, 1.5, 0, Math.PI * 2); ctx.fill();
- ctx.beginPath(); ctx.arc(x + 10, y - 71, 1.5, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = '#2a3a5a';
- ctx.fillRect(x - 18, y - 68, 6, 32);
- ctx.fillRect(x + 12, y - 68, 6, 32);
- ctx.fillStyle = '#0a0a0a';
- ctx.fillRect(x - 18, y - 38, 7, 8);
- ctx.fillRect(x + 12, y - 38, 7, 8);
- ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 74, 6, 5);
- ctx.beginPath(); ctx.arc(x, y - 83, 10, 0, Math.PI * 2);
- const fg = ctx.createRadialGradient(x - 3, y - 85, 2, x, y - 83, 10);
- fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#c09870');
- ctx.fillStyle = fg; ctx.fill();
- ctx.fillStyle = '#1a1a1a';
- ctx.beginPath(); ctx.arc(x - 3, y - 84, 1.3, 0, Math.PI * 2); ctx.fill();
- ctx.beginPath(); ctx.arc(x + 3, y - 84, 1.3, 0, Math.PI * 2); ctx.fill();
- const cap = ctx.createLinearGradient(x - 13, 0, x + 13, 0);
- cap.addColorStop(0, '#0f1a35'); cap.addColorStop(0.5, '#1a2c52'); cap.addColorStop(1, '#0a1428');
- ctx.fillStyle = cap;
- ctx.beginPath(); ctx.arc(x, y - 90, 13, Math.PI, 0); ctx.fill();
- ctx.fillRect(x - 13, y - 90, 26, 5);
- ctx.strokeStyle = '#0a1428'; ctx.lineWidth = 1.5;
- ctx.beginPath(); ctx.arc(x, y - 90, 13, Math.PI, 0); ctx.stroke();
- ctx.beginPath(); ctx.moveTo(x - 13, y - 85); ctx.lineTo(x + 13, y - 85); ctx.stroke();
- ctx.fillStyle = '#05080f';
- ctx.beginPath();
- ctx.moveTo(x - 14, y - 85); ctx.lineTo(x + 14, y - 85);
- ctx.lineTo(x + 12, y - 81); ctx.lineTo(x - 12, y - 81);
- ctx.closePath(); ctx.fill();
- ctx.strokeStyle = '#000'; ctx.stroke();
- ctx.fillStyle = '#c0a040';
- ctx.beginPath(); ctx.arc(x, y - 96, 2.5, 0, Math.PI * 2); ctx.fill();
- ctx.fillStyle = 'rgba(255,220,140,.7)';
- ctx.beginPath(); ctx.arc(x - 0.8, y - 96.8, 1, 0, Math.PI * 2); ctx.fill();
- ctx.restore();
- }
- const CAR_SCALE = 1.56;
- function drawCarScaled(drawFn, x, y, ...args) {
- ctx.save();
- ctx.translate(x, y);
- ctx.scale(CAR_SCALE, CAR_SCALE);
- ctx.translate(-x, -y);
- drawFn(x, y, ...args);
- ctx.restore();
- }
- /* ===== ПОГОДА ===== */
- function updateWeatherParticles(dt) {
- if (state.weather === 'clear' || state.weather === 'fog') {
- state.weatherParticles = []; return;
- }
- const targetCount = state.weather === 'rain' ? 240 : 140;
- while (state.weatherParticles.length < targetCount) {
- state.weatherParticles.push({
- x: Math.random() * W, y: Math.random() * H,
- vx: state.weather === 'rain' ? 60 + Math.random() * 40 : -10 + Math.random() * 20,
- vy: state.weather === 'rain' ? 600 + Math.random() * 200 : 30 + Math.random() * 40,
- size: state.weather === 'rain' ? 1 : 1.5 + Math.random() * 1.5,
- alpha: state.weather === 'rain' ? 0.5 + Math.random() * 0.4 : 0.7 + Math.random() * 0.3
- });
- }
- if (state.weatherParticles.length > targetCount) state.weatherParticles.length = targetCount;
- state.weatherParticles.forEach(p => {
- p.x += p.vx * dt; p.y += p.vy * dt;
- if (p.y > H) { p.y = -10; p.x = Math.random() * W; }
- });
- }
- function drawWeatherParticles() {
- if (state.weather === 'rain') {
- ctx.strokeStyle = 'rgba(180,200,220,.55)';
- ctx.lineWidth = 1;
- state.weatherParticles.forEach(p => {
- ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - 1, p.y - 12); ctx.stroke();
- });
- } else if (state.weather === 'snow') {
- state.weatherParticles.forEach(p => {
- ctx.fillStyle = `rgba(240,248,255,${p.alpha})`;
- ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
- });
- } else if (state.weather === 'fog') {
- const fogGrad = ctx.createLinearGradient(0, 0, 0, H);
- fogGrad.addColorStop(0, 'rgba(200,205,215,.30)');
- fogGrad.addColorStop(1, 'rgba(200,205,215,.35)');
- ctx.fillStyle = fogGrad; ctx.fillRect(0, 0, W, H);
- }
- }
- /* ===== ЛОГИКА ===== */
- let lastTs = 0;
- function updateScene(dt) {
- const l = L();
- if (!state.model) return;
- if (state.alarm && state.time > state.alarmEnd) state.alarm = false;
- const diff = state.barrier.target - state.barrier.level;
- if (Math.abs(diff) > 0.001) {
- const speedUp = 1 / state.model.openTime;
- const speedDown = 1 / state.model.closeTime;
- const spd = diff > 0 ? speedUp : speedDown;
- const step = Math.min(Math.abs(diff), dt * spd);
- state.barrier.level += Math.sign(diff) * step;
- state.barrier.moving = true;
- if (Math.abs(diff - Math.sign(diff)*step) < 0.005) {
- state.barrier.level = state.barrier.target;
- state.barrier.moving = false;
- }
- } else {
- state.barrier.moving = false;
- }
- state.skud.kw1 = state.barrier.level < 0.05;
- state.skud.kw2 = state.barrier.level > 0.95;
- if (state.skud.kw1) { state.skud.relayClose = false; }
- if (state.skud.kw2) { state.skud.relayOpen = false; }
- if (state.barrier.level > 0.95 && !state.holdMode && state.model.autoClose > 0 && !state.manualMode) {
- state.autoCloseTimer += dt;
- if (state.autoCloseTimer >= state.model.autoClose && !state.skud.photo && !state.skud.loop1 && !state.skud.loop2) {
- closeBarrier();
- state.autoCloseTimer = 0;
- }
- } else if (state.barrier.level < 0.95) {
- state.autoCloseTimer = 0;
- }
- const stopX = l.pivotX - (state.model.armLen) - 60;
- if (state.car) {
- const c = state.car;
- if (state.phase === 'approaching') {
- if (c.x < stopX) {
- c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
- } else if (c.isPriora) {
- c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
- if (c.x >= l.pivotX - state.model.armLen - 20 && state.barrier.level < 0.5) {
- c.x = l.pivotX - state.model.armLen - 20; crash();
- } else if (c.x > W + 300) {
- state.car = null; state.phase = 'idle';
- }
- } else {
- state.phase = 'waiting';
- setStatus('ОЖИДАНИЕ РЕШЕНИЯ', 'wait');
- setHint('Сравните пропуска и примите решение.');
- if (state.model.hasLoop) {
- state.skud.loop1 = true;
- logSkud('ПЕТЛЯ 1: занята', 'blu');
- state.stats.sensors++;
- }
- renderPasses();
- }
- }
- else if (state.phase === 'resolved') {
- if (c.action === 'pass') {
- if (state.barrier.level > 0.9) {
- c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
- if (state.model.hasPhoto) {
- if (!state.skud.photo && c.x > l.pivotX - state.model.armLen - 40 && c.x < l.pivotX + 40) {
- state.skud.photo = true;
- logSkud('ФОТОЭЛЕМЕНТ: разрыв луча', 'yel');
- state.stats.sensors++;
- }
- if (state.skud.photo && (c.x < l.pivotX - state.model.armLen - 60 || c.x > l.pivotX + 60)) {
- state.skud.photo = false;
- }
- }
- if (c.x > W + 300) {
- logEvent('Автомобиль проехал КПП', 'ok');
- state.stats.in++;
- state.car = null; state.phase = 'idle';
- if (state.model.hasLoop) { state.skud.loop1 = false; logSkud('ПЕТЛЯ 1: свободна'); }
- if (state.skud.photo) state.skud.photo = false;
- state.barrier.target = 0;
- setStatus('ПОСТ СВОБОДЕН');
- setHint('Нажмите <b>«Следующая машина»</b>.');
- state.senior = null;
- }
- }
- } else if (c.action === 'turn') {
- c.x -= c.speed * dt; c.wheelAngle -= c.speed * dt * 0.06;
- if (c.x < -350) {
- logEvent('Автомобиль уехал', 'info');
- state.car = null; state.phase = 'idle';
- if (state.model.hasLoop) { state.skud.loop1 = false; logSkud('ПЕТЛЯ 1: свободна'); }
- setStatus('ПОСТ СВОБОДЕН');
- setHint('Нажмите <b>«Следующая машина»</b>.');
- state.senior = null;
- }
- }
- }
- }
- if (state.senior) {
- const s = state.senior;
- const SPEED = 90;
- if (s.state === 'walk_to_lane') {
- s.alpha = Math.min(1, (s.alpha || 0) + dt * 1.5);
- s.walking = true; s.y = l.botLaneY;
- const targetX = (state.car ? state.car.x : 0) + 140;
- if (s.x > targetX) s.x -= SPEED * dt;
- else s.state = 'walk_up';
- } else if (s.state === 'walk_up') {
- s.walking = true;
- if (s.y > l.topLaneY + 2) s.y -= SPEED * 0.7 * dt;
- else {
- s.y = l.topLaneY; s.walking = false; s.state = 'review'; s.decideTimer = 0;
- setStatus('СТАРШИЙ У МАШИНЫ', 'senior');
- setHint('Старший смены проверяет документы...');
- logEvent('Старший смены подошёл к машине', 'senior');
- }
- } else if (s.state === 'review') {
- s.decideTimer += dt; s.walking = false;
- if (s.decideTimer > 2.0) {
- const allow = (state.correctDecision === 'allow');
- if (allow) {
- logEvent('Старший смены: пропуск действителен — пропускаем', 'senior');
- setStatus('СТАРШИЙ ПРОПУСКАЕТ', 'ok');
- state.car.action = 'pass';
- openBarrier();
- state.phase = 'resolved';
- state.userDecision = 'allow';
- renderPasses();
- } else {
- logEvent('Старший смены: пропуск недействителен — отказ', 'senior');
- setStatus('СТАРШИЙ ОТКАЗЫВАЕТ', 'wait');
- state.car.action = 'turn';
- state.phase = 'resolved';
- state.userDecision = 'deny';
- state.stats.out++;
- renderPasses();
- }
- s.state = 'returning';
- }
- } else if (s.state === 'returning') {
- s.walking = true;
- if (s.y < l.botLaneY - 2) s.y += SPEED * 0.7 * dt;
- else {
- s.y = l.botLaneY; s.x += SPEED * dt;
- if (s.x > W + 60) state.senior = null;
- }
- }
- }
- if (state.phase === 'crash_scene') {
- if (state.police) {
- const p = state.police;
- if (p.state === 'arriving') {
- p.x -= p.speed * dt; p.wheelAngle -= p.speed * dt * 0.06;
- if (p.x <= p.targetX) { p.x = p.targetX; p.state = 'stopped'; }
- } else if (p.state === 'leaving') {
- p.x += p.speed * dt; p.wheelAngle += p.speed * dt * 0.06;
- }
- }
- if (state.tow) {
- const t2 = state.tow;
- if (t2.state === 'arriving') {
- t2.x += t2.speed * dt; t2.wheelAngle -= t2.speed * dt * 0.06;
- if (t2.x >= t2.targetX) { t2.x = t2.targetX; t2.state = 'stopped'; t2.stopTimer = 0; }
- } else if (t2.state === 'stopped') {
- t2.stopTimer = (t2.stopTimer || 0) + dt;
- } else if (t2.state === 'leaving') {
- t2.x -= t2.speed * dt; t2.wheelAngle += t2.speed * dt * 0.06;
- }
- }
- if (state.car && state.car.towed && state.tow) {
- state.car.x = state.tow.x + 220;
- state.car.wheelAngle += state.tow.speed * dt * 0.06;
- }
- if (state.tow && state.tow.state === 'stopped' && state.tow.stopTimer > 1.2 && !state.towDriver && !state.leaving && state.car) {
- const doorX = state.tow.x - 150;
- state.towDriver = { x: doorX, y: l.topLaneY, doorX: doorX,
- carApproachX: state.car.x - 160,
- state: 'exiting', exitTimer: 0, alpha: 0, walking: false, attachTimer: 0, boardTimer: 0 };
- state.tow.doorOpen = true;
- logEvent('Водитель выходит из кабины эвакуатора', 'info');
- }
- if (state.towDriver) {
- const td = state.towDriver;
- const underY = l.topLaneBot + 25;
- const SPD = 55;
- if (td.state === 'exiting') {
- td.exitTimer += dt; td.alpha = Math.min(1, td.exitTimer / 0.8);
- if (td.exitTimer > 0.8) { td.alpha = 1; td.state = 'walking_out_down'; }
- } else if (td.state === 'walking_out_down') {
- if (td.y < underY - 2) { td.y += SPD * dt; td.walking = true; }
- else { td.y = underY; td.state = 'walking_right'; }
- } else if (td.state === 'walking_right') {
- if (td.x < td.carApproachX - 2) { td.x += SPD * dt; td.walking = true; }
- else { td.x = td.carApproachX; td.state = 'walking_up'; }
- } else if (td.state === 'walking_up') {
- if (td.y > l.topLaneY + 2) { td.y -= SPD * dt; td.walking = true; }
- else { td.y = l.topLaneY; td.state = 'attaching'; td.attachTimer = 0; }
- } else if (td.state === 'attaching') {
- td.attachTimer += dt; td.walking = false;
- if (td.attachTimer > 0.6 && !state.towCable) { state.towCable = true; logEvent('Автомобиль зацеплен', 'info'); }
- if (td.attachTimer > 1.4) td.state = 'returning_down';
- } else if (td.state === 'returning_down') {
- if (td.y < underY - 2) { td.y += SPD * dt; td.walking = true; }
- else { td.y = underY; td.state = 'returning_left'; }
- } else if (td.state === 'returning_left') {
- if (td.x > td.doorX + 2) { td.x -= SPD * dt; td.walking = true; }
- else { td.x = td.doorX; td.state = 'returning_up'; }
- } else if (td.state === 'returning_up') {
- if (td.y > l.topLaneY + 2) { td.y -= SPD * dt; td.walking = true; }
- else { td.y = l.topLaneY; td.state = 'boarding'; td.boardTimer = 0; td.walking = false; }
- } else if (td.state === 'boarding') {
- td.boardTimer += dt; td.walking = false;
- td.alpha = Math.max(0, 1 - td.boardTimer / 0.8);
- td.x += 20 * dt;
- if (td.boardTimer > 0.8) {
- td.state = 'gone'; td.alpha = 0;
- if (state.tow) state.tow.doorOpen = false;
- if (!state.leaving) {
- state.leaving = true;
- setTimeout(() => {
- logEvent('Эвакуатор увозит автомобиль', 'info');
- if (state.car) state.car.towed = true;
- if (state.police) state.police.state = 'leaving';
- if (state.tow) state.tow.state = 'leaving';
- }, 600);
- }
- }
- }
- }
- if (state.tow && state.tow.state === 'leaving' && state.tow.x < -600) {
- state.car = null; state.police = null; state.tow = null;
- state.towDriver = null; state.towCable = false; state.leaving = false;
- state.phase = 'idle'; state.barrier.target = 0;
- state.alarm = false; state.skud.alarm = false;
- setStatus('ПОСТ СВОБОДЕН');
- setHint('Инцидент исчерпан. Нажмите «Следующая машина».');
- logEvent('Инцидент закрыт', 'ok');
- }
- }
- updateWeatherParticles(dt);
- updateSkudLeds();
- }
- function crash() {
- logEvent('!!! СТОЛКНОВЕНИЕ СО ШЛАГБАУМОМ !!!', 'alarm');
- setStatus('ТРЕВОГА !', 'alarm');
- setHint('Нарушитель врезался. Вызваны полиция и эвакуатор.');
- state.alarm = true;
- state.alarmEnd = state.time + 5;
- state.phase = 'crash_scene';
- state.leaving = false;
- state.stats.alarms++;
- state.stats.breaches++;
- state.skud.alarm = true;
- if (state.model.hasImpact) {
- state.skud.impact = true;
- logSkud('ДАТЧИК УДАРА: сработал', 'alarm');
- state.stats.sensors++;
- }
- if (state.car) { state.car.action = 'crashed'; state.car.speed = 0; }
- const l = L();
- for (let i = 0; i < 40; i++) {
- state.sparks.push({
- x: l.pivotX - state.model.armLen - 30, y: l.topLaneY - 40,
- vx: (Math.random() - .5) * 400, vy: (Math.random() - .5) * 300 - 80,
- life: 1
- });
- }
- setTimeout(() => {
- state.police = { x: W + 300, targetX: l.pivotX - 100, speed: 260, state: 'arriving', wheelAngle: 0 };
- logEvent('Прибывает полицейский автомобиль', 'warn');
- }, 700);
- setTimeout(() => {
- const carX = state.car ? state.car.x : (l.pivotX - state.model.armLen - 20);
- state.tow = { x: -300, targetX: carX - 340, speed: 200, state: 'arriving',
- wheelAngle: 0, stopTimer: 0, doorOpen: false };
- logEvent('Прибывает эвакуатор', 'warn');
- }, 1400);
- }
- /* ===== ОТРИСОВКА ===== */
- function drawScene() {
- const l = L();
- const t = state.time;
- ctx.fillStyle = '#02040a'; ctx.fillRect(0, 0, W, H);
- if (!state.model) return;
- drawSky(); drawGround(); drawRoads();
- drawBooth();
- drawGuard(l.boothX + l.boothW + 25, l.boothBaseY);
- if (state.towCable && state.tow && state.car) {
- const fromX = state.tow.x + 140;
- const fromY = l.topLaneY - 20;
- const toX = state.car.x - 140;
- const toY = l.topLaneY - 15;
- ctx.strokeStyle = 'rgba(200,200,200,.85)'; ctx.lineWidth = 2;
- ctx.beginPath(); ctx.moveTo(fromX, fromY);
- ctx.quadraticCurveTo((fromX + toX) / 2, Math.max(fromY, toY) + 22, toX, toY); ctx.stroke();
- }
- drawBarrier();
- if (state.car) {
- if (state.car.isPriora) drawCarScaled(drawDarkCar, state.car.x, l.topLaneY, state.car.wheelAngle);
- else drawCarScaled(drawSedan, state.car.x, l.topLaneY, state.car.color, state.car.wheelAngle);
- }
- if (state.police) drawCarScaled(drawPoliceCar, state.police.x, l.botLaneY, state.police.wheelAngle);
- if (state.tow) drawCarScaled(drawTowTruck, state.tow.x, l.topLaneY, state.tow.wheelAngle, state.tow.doorOpen);
- if (state.towDriver && state.towDriver.state !== 'gone') {
- drawTowDriver(state.towDriver.x, state.towDriver.y, state.towDriver.walking, state.towDriver.alpha);
- }
- if (state.senior) {
- drawSenior(state.senior.x, state.senior.y, state.senior.walking, state.senior.alpha);
- }
- state.sparks = state.sparks.filter(s => s.life > 0);
- state.sparks.forEach(s => {
- s.x += s.vx / 60; s.y += s.vy / 60; s.vy += 500 / 60; s.life -= 1.2 / 60;
- ctx.fillStyle = `rgba(255,${Math.floor(160 + Math.random()*90)},60,${Math.max(0, s.life)})`;
- ctx.beginPath(); ctx.arc(s.x, s.y, 2 + s.life * 3, 0, Math.PI * 2); ctx.fill();
- });
- drawWeatherParticles();
- if (state.alarm) {
- const pulse = 0.5 + Math.sin(t * 8) * 0.5;
- ctx.strokeStyle = `rgba(255,40,40,${pulse * 0.5})`; ctx.lineWidth = 12;
- ctx.strokeRect(6, 6, W - 12, H - 12);
- ctx.fillStyle = `rgba(255,60,60,${0.5 + 0.3 * Math.abs(Math.sin(t * 5))})`;
- ctx.font = 'bold 36px "Courier New"'; ctx.textAlign = 'center';
- ctx.fillText('!!! ТРЕВОГА !!!', W / 2, 60); ctx.textAlign = 'left';
- }
- const hintText = {
- idle: 'ПОСТ СВОБОДЕН · НАЖМИТЕ «СЛЕДУЮЩАЯ МАШИНА»',
- approaching: 'АВТОМОБИЛЬ ПРИБЛИЖАЕТСЯ',
- waiting: 'ОЖИДАНИЕ РЕШЕНИЯ ОПЕРАТОРА',
- refuse_wait: 'МАШИНА ЖДЁТ · ОБЪЯСНИТЕ ПРИЧИНУ ИЛИ ВЫЗОВИТЕ СТАРШЕГО',
- senior_walk: 'СТАРШИЙ СМЕНЫ ИДЁТ К МАШИНЕ',
- resolved: 'ОБРАБОТКА РЕЗУЛЬТАТА',
- crash_scene: 'НА МЕСТЕ ПРОИСШЕСТВИЯ'
- }[state.phase] || '';
- if (hintText) {
- ctx.font = 'bold 12px "Courier New"';
- const tw = ctx.measureText(hintText).width + 40;
- ctx.fillStyle = 'rgba(10,14,20,.78)';
- ctx.fillRect(W / 2 - tw / 2, 20, tw, 28);
- ctx.strokeStyle = 'rgba(96,160,224,.5)'; ctx.lineWidth = 1;
- ctx.strokeRect(W / 2 - tw / 2, 20, tw, 28);
- ctx.fillStyle = '#a0c0e0'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
- ctx.fillText(hintText, W / 2, 34); ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
- }
- }
- function loop(ts) {
- if (!lastTs) lastTs = ts;
- const dt = Math.min(0.05, (ts - lastTs) / 1000);
- lastTs = ts;
- state.time += dt;
- updateScene(dt);
- drawScene();
- drawScope();
- pushScopeSample();
- requestAnimationFrame(loop);
- }
- function updateClock() {
- const d = new Date();
- document.getElementById('clock').textContent =
- String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0');
- }
- setInterval(updateClock, 1000); updateClock();
- /* ===== СТАРТ ===== */
- resizeCanvas(); renderPasses(); renderLog();
- renderModelSwitcher();
- switchModel('B');
- renderModelPassport();
- drawScope();
- logSkud('Запуск СКУД КПП-3...', 'ok');
- logSkud('Контроллер инициализирован, клеммы опрошены');
- requestAnimationFrame(loop);
- </script>
- </body>
- </html>
|