Интерактивный веб-проект — симулятор поста охраны КПП с моделью работы шлагбаума.
Запись 1. Общая концепция проекта
Задача. Разработать одностраничное интерактивное приложение по теме «Моделирование работы шлагбаума». Пользователь должен выступать в роли оператора КПП: сравнивать пропуска, принимать решение о пропуске или отказе, управлять шлагбаумом. Оформление — служебная система контроля доступа, тёмный интерфейс, без внешних библиотек. Всё в одном HTML-файле.
Решение. Создан каркас: HTML-структура с шапкой (логотип, часы, системный индикатор), основной сценой на Canvas и правой панелью управления. Сцена — псевдо-3D вид на КПП: небо с градиентом, здания, две полосы движения, будка охраны, охранник, шлагбаум с красно-белой стрелой. Стилистика выдержана в тёмных тонах с янтарными и голубыми акцентами, шрифт Courier New.
◤ КПП-3СИСТЕМА КОНТРОЛЯ ДОСТУПА
--:--:--
ПОСТ СВОБОДЕН
Запись 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);
новые правки:
1. Постоянный переключатель моделей
1.1. Удалено стартовое модальное окно
Убрать из HTML:
html
...
1.2. Добавлена панель-переключатель в сайдбар
HTML (первый блок в