Files
2026-10-06 10:28:24 +03:00

471 lines
14 KiB
JavaScript

const canvas = document.getElementById('graphCanvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const center = width / 2;
// Элементы формы
const form = document.getElementById('point-form');
const yInput = document.getElementById('y-input');
const yError = document.getElementById('y-error');
const funcInput = document.getElementById('func-input');
const funcError = document.getElementById('func-error');
const previewFuncBtn = document.getElementById('preview-func-btn');
const rHidden = document.getElementById('r-hidden');
const rError = document.getElementById('r-error');
const rButtons = document.querySelectorAll('.r-button');
const checkboxes = document.querySelectorAll('input[name="xval"]');
const submitBtn = document.getElementById('submit-btn');
const globalError = document.getElementById('global-error');
const resultsBody = document.getElementById('results-body');
const clearHistoryBtn = document.getElementById('clear-history-btn');
// Б - безопасность
const STORAGE_KEY = 'web_lab1_history';
let history = [];
let currentR = null;
let formSubmitted = false;
let customFunction = null;
try {
const rawData = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (Array.isArray(rawData)) {
// Проверяем LocalStorage (вдруг врёт)
history = rawData.filter(rec => {
if (typeof rec !== 'object' || rec === null) return false;
if (![1, 2, 3, 4, 5].includes(rec.r)) return false;
if (![-2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2].includes(rec.x)) return false;
if (typeof rec.y !== 'number' || isNaN(rec.y) || ((rec.y < -3 || rec.y > 5) && rec.y !== 81)) return false;
return true;
});
}
} catch (e) {
history = []; // Миша, всё херня, начинай сначала.
}
// Пошло поехало
window.addEventListener('DOMContentLoaded', () => {
// Очистка истории
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', () => {
history = [];
localStorage.removeItem(STORAGE_KEY);
renderHistory();
drawGraph();
});
}
renderHistory();
drawGraph();
validateForm();
});
rButtons.forEach(btn => {
btn.addEventListener('click', () => {
rButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
rHidden.value = btn.getAttribute('data-val');
currentR = parseFloat(rHidden.value);
if (formSubmitted) {
rError.textContent = '';
}
validateForm();
drawGraph();
});
});
checkboxes.forEach(cb => {
cb.addEventListener('change', () => {
if (formSubmitted) {
globalError.textContent = '';
}
validateForm();
});
});
yInput.addEventListener('input', () => {
if (formSubmitted) {
yError.textContent = '';
}
validateForm();
});
funcInput.addEventListener('input', () => {
if (formSubmitted) {
funcError.textContent = '';
}
validateForm();
});
previewFuncBtn.addEventListener('click', () => {
const funcStr = funcInput.value.trim();
if (funcStr === '') {
customFunction = null;
funcError.textContent = '';
drawGraph();
return;
}
if (!currentR) {
funcError.textContent = 'Сначала выберите значение R';
return;
}
try {
customFunction = parseFunction(funcStr);
funcError.textContent = '';
drawGraph();
} catch (e) {
funcError.textContent = e.message;
customFunction = null;
}
});
function checkHit(x, y, r) {
if (customFunction) {
try {
return customFunction(x, y, r);
} catch (e) {
return false;
}
}
// Первая четверть (X >= 0, Y >= 0): Четверть круга (R)
if (x >= 0 && y >= 0) {
return (x * x + y * y) <= (r * r);
}
// Вторая четверть (X <= 0, Y >= 0): Прямоугольник (ширина R, высота R/2)
if (x <= 0 && y >= 0) {
return x >= -r && y <= (r / 2);
}
// Третья четверть (X <= 0, Y <= 0): Пустота
if (x <= 0 && y <= 0) {
return false;
}
// Четвертая четверть (X >= 0, Y <= 0): Треугольник (проходит через (R, 0) и (0, -R/2))
// Уравнение прямой: y = x/2 - r/2 => точка внутри если y >= x/2 - r/2
if (x >= 0 && y <= 0) {
return y >= (x / 2) - (r / 2);
}
return false;
}
function parseFunction(funcStr) {
if (!funcStr || funcStr.trim() === '') {
return null;
}
const normalized = funcStr.trim()
.replace(/abs\(/g, 'Math.abs(')
.replace(/sin\(/g, 'Math.sin(')
.replace(/cos\(/g, 'Math.cos(')
.replace(/tan\(/g, 'Math.tan(')
.replace(/sqrt\(/g, 'Math.sqrt(')
.replace(/\^/g, '**');
const allowedChars = /^[xyrRMathabsincoqrt.()+\-*/<>=\s\d.]+$/;
if (!allowedChars.test(normalized)) {
throw new Error('Недопустимые символы в функции');
}
try {
const func = new Function('x', 'y', 'r', 'R', `return ${normalized};`);
func(0, 0, 1, 1);
return (x, y, r) => func(x, y, r, r);
} catch (e) {
throw new Error('Синтаксическая ошибка в функции');
}
}
function validateFunction(showErrors = false) {
const funcStr = funcInput.value.trim();
if (funcStr === '') {
return true;
}
try {
parseFunction(funcStr);
return true;
} catch (e) {
if (showErrors) {
funcError.textContent = e.message;
}
return false;
}
}
function validateY(showErrors = false) {
const valStr = yInput.value.trim().replace(',', '.');
if (valStr === '') {
if (showErrors) {
yError.textContent = 'Поле Y обязательно к заполнению.';
}
return false;
}
const y = Number(valStr);
if (isNaN(y)) {
if (showErrors) {
yError.textContent = 'Y должно быть числом (буквы недопустимы).';
}
return false;
}
if ((y < -3 || y > 5) && y !== 81) {
if (showErrors) {
yError.textContent = 'Значение Y должно принадлежать диапазону от -3 до 5 включительно.';
}
return false;
}
return true;
}
function validateForm(showErrors = false) {
let isValid = true;
if (!validateY(showErrors)) isValid = false;
if (!validateFunction(showErrors)) isValid = false;
const validRValues = [1, 2, 3, 4, 5];
if (!rHidden.value) {
if (showErrors) {
rError.textContent = 'Необходимо выбрать значение R.';
}
isValid = false;
} else if (!validRValues.includes(parseFloat(rHidden.value))) {
if (showErrors) {
rError.textContent = 'Обнаружена подмена значения R!';
}
isValid = false;
}
const validXValues = [-2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2];
let xChecked = false;
let xSpoofed = false;
checkboxes.forEach(cb => {
if (cb.checked) {
xChecked = true;
if (!validXValues.includes(parseFloat(cb.value))) {
xSpoofed = true;
}
}
});
if (!xChecked) {
if (showErrors) {
globalError.textContent = 'Выберите хотя бы одно значение X.';
}
isValid = false;
} else if (xSpoofed) {
if (showErrors) {
globalError.textContent = 'Ошибка: передано недопустимое значение X!';
}
isValid = false;
}
submitBtn.disabled = !isValid;
return isValid;
}
form.addEventListener('submit', (e) => {
e.preventDefault();
formSubmitted = true;
if (!validateForm(true)) return;
const y = parseFloat(yInput.value.trim().replace(',', '.'));
// пасхалка
if (y === 81) {
const egg = document.getElementById('oscar-easter-egg');
if (egg) {
egg.classList.add('show');
setTimeout(() => egg.classList.remove('show'), 1000);
}
yInput.value = '';
validateForm();
return;
}
const r = parseFloat(rHidden.value);
const funcStr = funcInput.value.trim();
try {
customFunction = parseFunction(funcStr);
} catch (e) {
funcError.textContent = e.message;
return;
}
// Дата, время и тд
const dateString = new Date().toLocaleString("ru-RU", {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
checkboxes.forEach(cb => {
if (cb.checked) {
const x = parseFloat(cb.value);
const hit = checkHit(x, y, r);
history.push({
x: x,
y: y,
r: r,
hit: hit,
time: dateString
});
}
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
renderHistory();
drawGraph();
});
function renderHistory() {
resultsBody.innerHTML = '';
[...history].reverse().forEach(rec => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${rec.x}</td>
<td>${rec.y}</td>
<td>${rec.r}</td>
<td style="color: ${rec.hit ? 'green' : 'red'}; font-weight: bold;">
${rec.hit ? 'Попадание' : 'Промах'}
</td>
<td>${rec.time}</td>
`;
resultsBody.appendChild(tr);
});
}
function drawGraph() {
ctx.clearRect(0, 0, width, height);
const R_PIXELS = currentR ? (100 * currentR / 3) : 100;
if (customFunction && currentR) {
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const x = ((px - center) / R_PIXELS) * currentR;
const y = ((center - py) / R_PIXELS) * currentR;
const hit = checkHit(x, y, currentR);
const index = (py * width + px) * 4;
if (hit) {
data[index] = 51;
data[index + 1] = 153;
data[index + 2] = 255;
data[index + 3] = 255;
} else {
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = 255;
}
}
}
ctx.putImageData(imageData, 0, 0);
} else {
ctx.fillStyle = '#3399FF';
// Прямоугольник
ctx.fillRect(center - R_PIXELS, center - R_PIXELS / 2, R_PIXELS, R_PIXELS / 2);
// Четверть круга
ctx.beginPath();
ctx.moveTo(center, center);
ctx.arc(center, center, R_PIXELS, -Math.PI / 2, 0, false);
ctx.closePath();
ctx.fill();
// Треугольник
ctx.beginPath();
ctx.moveTo(center, center);
ctx.lineTo(center + R_PIXELS, center);
ctx.lineTo(center, center + R_PIXELS / 2);
ctx.closePath();
ctx.fill();
}
// Оси
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.beginPath();
// Ось X
ctx.moveTo(0, center);
ctx.lineTo(width, center);
ctx.lineTo(width - 5, center - 5);
ctx.moveTo(width, center);
ctx.lineTo(width - 5, center + 5);
// Ось Y
ctx.moveTo(center, height);
ctx.lineTo(center, 0);
ctx.lineTo(center - 5, 5);
ctx.moveTo(center, 0);
ctx.lineTo(center + 5, 5);
ctx.stroke();
// Обозначения
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
// Ось X
ctx.fillText('R', center + R_PIXELS - 5, center + 15);
ctx.fillText('R/2', center + R_PIXELS / 2 - 10, center + 15);
ctx.fillText('-R', center - R_PIXELS - 5, center + 15);
ctx.fillText('-R/2', center - R_PIXELS / 2 - 10, center + 15);
ctx.fillText('x', width - 10, center - 10);
// Ось Y
ctx.fillText('R', center + 10, center - R_PIXELS + 4);
ctx.fillText('R/2', center + 10, center - R_PIXELS / 2 + 4);
ctx.fillText('-R', center + 10, center + R_PIXELS + 4);
ctx.fillText('-R/2', center + 10, center + R_PIXELS / 2 + 4);
ctx.fillText('y', center + 10, 10);
// Засечки
ctx.beginPath();
[R_PIXELS, R_PIXELS/2, -R_PIXELS, -R_PIXELS/2].forEach(p => {
// по X
ctx.moveTo(center + p, center - 3);
ctx.lineTo(center + p, center + 3);
// по Y
ctx.moveTo(center - 3, center + p);
ctx.lineTo(center + 3, center + p);
});
ctx.stroke();
if (currentR && currentR > 0) {
history.forEach(rec => {
let drawX = center + (rec.x / rec.r) * R_PIXELS;
let drawY = center - (rec.y / rec.r) * R_PIXELS;
ctx.beginPath();
ctx.arc(drawX, drawY, 4, 0, 2 * Math.PI, false);
ctx.fillStyle = rec.hit ? '#4CAF50' : '#F44336';
ctx.fill();
ctx.strokeStyle = '#222';
ctx.stroke();
});
}
}