Интерактивный веб-проект — симулятор поста охраны КПП с моделью работы шлагбаума.
Запись 1. Общая концепция проекта Задача. Разработать одностраничное интерактивное приложение по теме «Моделирование работы шлагбаума». Пользователь должен выступать в роли оператора КПП: сравнивать пропуска, принимать решение о пропуске или отказе, управлять шлагбаумом. Оформление — служебная система контроля доступа, тёмный интерфейс, без внешних библиотек. Всё в одном HTML-файле.
Решение. Создан каркас: HTML-структура с шапкой (логотип, часы, системный индикатор), основной сценой на Canvas и правой панелью управления. Сцена — псевдо-3D вид на КПП: небо с градиентом, здания, две полосы движения, будка охраны, охранник, шлагбаум с красно-белой стрелой. Стилистика выдержана в тёмных тонах с янтарными и голубыми акцентами, шрифт Courier New.
Запись 2. Системные часы и индикатор состояния Задача. Добавить часы реального времени в шапке и цветной индикатор состояния поста, который меняет цвет и текст в зависимости от ситуации.
Решение. Часы обновляются через setInterval каждую секунду. Индикатор реализован как переключаемый класс: «ПОСТ СВОБОДЕН» (зелёный), «ПРИБЛИЖАЕТСЯ АВТО» / «ОЖИДАНИЕ РЕШЕНИЯ» (жёлтый), «МАШИНА ЖДЁТ» (синий), «СТАРШИЙ ВЫЗВАН» (голубой), «НАРУШИТЕЛЬ !» / «ТРЕВОГА !» (красный, мигающий).
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);
function setStatus(text, cls) { document.getElementById('sysStatusText').textContent = text; document.getElementById('sysStatus').className = 'sys-status' + (cls ? ' ' + cls : ''); }
Запись 3. Сравнение пропусков Задача. Сделать две карточки для сравнения: эталонный пропуск «В БАЗЕ» и предъявленный водителем. Пропуска генерируются случайно: иногда совпадают, иногда отличаются номером, иногда предъявленный отозван.
Решение. Функция randomPass() генерирует случайный пропуск (номер, ФИО, подразделение, признак valid). Функция generateScenario() определяет, совпадает ли предъявленный с эталонным, и должно ли решение оператора быть «разрешить» или «запретить». Карточки рисуются в правой панели.
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
}; }
Запись 4. Анимация шлагбаума Задача. Шлагбаум должен плавно открываться и закрываться, с реалистичной красно-белой стрелой, лампой-индикатором и вращающейся осью. Угол стрелы — от 0 (закрыт) до 90° (открыт).
Решение. Уровень шлагбаума хранится в state.barrier.level от 0 до 1 и плавно интерполируется к target. Стрела рисуется через ctx.translate/rotate — угол равен Math.PI + level * Math.PI/2. Лампа на верху столба меняет цвет: красный при закрытом, жёлтый при движении, зелёный при открытом. Стрела состоит из 14 чередующихся сегментов.
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.restore();
Запись 5. Движение автомобилей Задача. Реализовать несколько типов машин: обычный легковой автомобиль и тёмный тонированный автомобиль для сценария «силовой проезд». Машины подъезжают к шлагбауму, останавливаются, ждут решения оператора, потом проезжают или разворачиваются.
Решение. Нарисованы две процедурные машины на Canvas. Легковой автомобиль — цветной, с градиентом, фарами и стёклами. Тонированный автомобиль — тёмный, с угловатыми фарами, решёткой радиатора, серебряными пятиспицевыми дисками. Колёса вращаются вместе с движением.
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(); }
Запись 6. Сценарий силового проезда и столкновения Задача. Добавить сценарий, в котором машина не сбрасывает скорость и врезается в закрытый шлагбаум. При столкновении — искры, тревога, приезд полиции и эвакуатора.
Решение. Кнопка «Силовой проезд» запускает тонированный автомобиль со скоростью в два раза выше обычной. Если шлагбаум закрыт — срабатывает функция crash(): красная пульсирующая рамка, надпись «!!! ТРЕВОГА !!!», 40 частиц искр. Через 700 мс приезжает полицейский автомобиль с мигалками, через 1400 мс — эвакуатор с водителем.
function crash() { logEvent('!!! СТОЛКНОВЕНИЕ СО ШЛАГБАУМОМ !!!', 'alarm'); setStatus('ТРЕВОГА !', 'alarm'); state.alarm = true; state.alarmEnd = state.time + 5; state.phase = 'crash_scene'; state.stats.alarms++; state.stats.breaches++; for (let i = 0; i < 40; i++) {
state.sparks.push({
x: l.pivotX - l.armLen - 30, y: l.topLaneY - 40,
vx: (Math.random() - .5) * 400, vy: (Math.random() - .5) * 300 - 80,
life: 1
});
} }
Запись 7. Работа водителя эвакуатора Задача. Водитель эвакуатора должен выйти из кабины, дойти до машины, прицепить её тросом, вернуться в кабину и увезти. Всё это — с анимацией ходьбы.
Решение. Реализован конечный автомат из 9 состояний: exiting → walking_out_down → walking_right → walking_up → attaching → returning_down → returning_left → returning_up → boarding → gone. Персонаж нарисован детально: оранжевый жилет со светоотражающими полосами, каска, трос крепится квадратичной кривой Безье.
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 === 'attaching') { td.attachTimer += dt; td.walking = false; if (td.attachTimer > 0.6 && !state.towCable) {
state.towCable = true;
logEvent('Автомобиль зацеплен', 'info');
} }
Запись 8. Старший смены Задача. При отказе в проезде оператор может вызвать старшего смены. Старший должен прийти к машине, проверить документы и самостоятельно принять верное решение.
Решение. По кнопке «Вызвать старшего» создаётся объект state.senior с координатами за правым краем экрана. Он идёт к машине по нижней полосе, затем поднимается к верхней, стоит 2 секунды, «проверяет документы» и выносит верный вердикт. Отдельная функция drawSenior() рисует фигуру в форме охраны — тёмно-синий китель, фуражка, погоны.
Запись 9. Погода и время суток Задача. Добавить переключение времени суток (день/ночь) и погоды (ясно, дождь, снег, туман). Освещение, цвета неба, поведение машин и видимость должны меняться.
Решение. Каждой комбинации соответствует свой режим освещения: day, dawn, dusk, overcast, night, wetnight, snownight. Функция skyColors() возвращает цвет неба, drawWeatherParticles() — рисует дождь, снег или туман. Дождь и снег снижают скорость машин через speedMult(): дождь — 0.75, снег — 0.6, туман — 0.85. Ночью включается ИК-режим камеры.
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; }
Запись 10. Журнал событий Задача. Все действия в системе должны фиксироваться в журнале с отметкой времени и цветовой кодировкой.
Решение. Массив logEntries хранит до 80 последних записей. Каждая запись имеет тип: info (голубой), ok (зелёный), warn (жёлтый), alarm (красный), senior (голубой жирный). Журнал автоматически прокручивается вниз.
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 > 80) logEntries.shift(); renderLog(); }
Запись 11. Отчёт о смене Задача. В конце работы оператор нажимает «Закончить смену» — должно открыться модальное окно с итогами: сколько впущено, отказано, верных решений, ошибок, тревог, силовых проездов, вызовов старшего. Оценка — по проценту верных решений.
Решение. Собраны все счётчики в state.stats. Оценка рассчитывается: ≥ 95% — «ОТЛИЧНО», ≥ 80% — «ХОРОШО», ≥ 60% — «УДОВЛЕТВОРИТЕЛЬНО», иначе — «ТРЕБУЕТСЯ ПОДГОТОВКА».
const pct = s.correct / total; if (pct >= 0.95) grade = 'ОТЛИЧНО'; else if (pct >= 0.8) grade = 'ХОРОШО'; else if (pct >= 0.6) grade = 'УДОВЛЕТВОРИТЕЛЬНО'; else grade = 'ТРЕБУЕТСЯ ПОДГОТОВКА';
Запись 12. Отметка в журнале вручную Задача. Дать оператору возможность добавить собственную запись в журнал.
Решение. Кнопка «✎ Отметить в журнале» вызывает prompt, введённый текст добавляется в журнал с пометкой [ОПЕРАТОР].
document.getElementById('btnNote').onclick = () => { const txt = prompt('Введите отметку для журнала:', ''); if (txt && txt.trim()) logEvent('[ОПЕРАТОР] ' + txt.trim(), 'info'); };
Запись 13. Чёткость Canvas Задача. Картинка на экране размытая, особенно на Retina-дисплеях. Сделать рендер чётким.
Решение. Внутренний размер канваса увеличивается пропорционально devicePixelRatio, а рендер идёт в логических координатах через ctx.setTransform(). Это убирает размытие и сохраняет единую систему координат.
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);
Запись 14. Сброс состояния Задача. Добавить кнопку полного сброса — вернуть систему в исходное состояние, очистить журнал, убрать все машины и объекты.
Решение. Функция resetAll() обнуляет машину, шлагбаум, полицию, эвакуатор, старшего, очищает журнал, перерисовывает карточки пропусков.
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.senior = null; logEntries.length = 0; renderLog(); renderPasses(); setStatus('ПОСТ СВОБОДЕН'); setHint('Нажмите «Следующая машина».'); }
Запись 15. Единый игровой цикл Задача. Обеспечить плавную работу всех анимаций с привязкой к реальному времени.
Решение. Один requestAnimationFrame с delta-time (не более 0.05 с на кадр). Отдельно обновляется сцена (updateScene) и отрисовывается (drawScene). Состояние игры хранится в одном объекте state.
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); } requestAnimationFrame(loop);
Как запустить Просто открыть index.html в браузере. Все зависимости встроены, внешних библиотек нет.