|
|
@@ -0,0 +1,2123 @@
|
|
|
+<!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 400px; 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; }
|
|
|
+
|
|
|
+ .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; }
|
|
|
+
|
|
|
+ .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="model-switcher" id="modelSwitcher"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section">
|
|
|
+ <h3>СТАНЦИЯ СКУД · ИНДИКАЦИЯ</h3>
|
|
|
+ <div class="content">
|
|
|
+ <div class="skud-grid">
|
|
|
+ <div class="skud-row"><span class="skud-led" id="ledPower"></span>ПИТАНИЕ</div>
|
|
|
+ <div class="skud-row"><span class="skud-led" id="ledArmState"></span>СТРЕЛА</div>
|
|
|
+ <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 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 class="skud-row"><span class="skud-led" id="ledRelayOpen"></span>РЕЛЕ «ОТКР»</div>
|
|
|
+ <div class="skud-row"><span class="skud-led" id="ledRelayClose"></span>РЕЛЕ «ЗАКР»</div>
|
|
|
+ <div class="skud-row"><span class="skud-led" id="ledRelayStop"></span>РЕЛЕ «СТОП»</div>
|
|
|
+ <div class="skud-row"><span class="skud-led" id="ledPass"></span>ПРОПУСК</div>
|
|
|
+ </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:'«Быстрый»',
|
|
|
+ desc:'Для офисных КПП. Полный цикл ~1 с.',
|
|
|
+ openTime:0.9, closeTime:1.2, autoClose:3, armLen:180,
|
|
|
+ hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:true },
|
|
|
+ B: { code:'B', name:'МАГИСТРАЛЬНЫЙ', short:'«Стандарт»',
|
|
|
+ desc:'Для въезда на территорию. Петля + фотоэлемент.',
|
|
|
+ openTime:3.5, closeTime:3.5, autoClose:8, armLen:240,
|
|
|
+ hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:false, hasKW2:true },
|
|
|
+ C: { code:'C', name:'ПРОМЫШЛЕННЫЙ', short:'«Терминал»',
|
|
|
+ desc:'Грузовой транспорт. 2 петли + фотоэлемент + датчик удара.',
|
|
|
+ openTime:6, closeTime:6, autoClose:15, armLen:280,
|
|
|
+ hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:true, hasKW2:true },
|
|
|
+ D: { code:'D', name:'БЮДЖЕТНЫЙ', short:'«Мини»',
|
|
|
+ desc:'Для внутренних КПП. Без автоматики.',
|
|
|
+ openTime:4, closeTime:4, autoClose:0, armLen:200,
|
|
|
+ hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:false }
|
|
|
+};
|
|
|
+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();
|
|
|
+ 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 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);
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== ФОН ===== */
|
|
|
+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();
|
|
|
+ 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');
|
|
|
+logSkud('Запуск СКУД КПП-3...', 'ok');
|
|
|
+requestAnimationFrame(loop);
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|