diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f4c5c8 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Web Programming: Lab 1 + +This is a client-side web app that checks if a given 2D point (X, Y) falls inside a specific graphed area based on a given radius (R). + +## Functions +You basically pick your coordinates, set a radius, and hit submit. The app calculates whether your point landed directly inside the colored shapes on the graph or missed them entirely. + +## How to Run It +1. Download the files. +2. Double-click `index.html` to open it in your browser. +3. That's pretty much it. + +## Project Structure +- `index.html` — The main layout holding the UI and embedded CSS styled. +- `script.js` — Vanilla JavaScript: Canvas rendering, math checking, and validations. +- `assets/` — Try to guess. +- `docs/` — Assignment instructions. diff --git a/assets/oscar.png b/assets/oscar.png new file mode 100644 index 0000000..8eb48a0 Binary files /dev/null and b/assets/oscar.png differ diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..9642e21 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,22 @@ +Частые ошибки: + +1. Использование alert для вывода сообщений об ошибках. +2. Отсутствие человекочитаемых сообщений об ошибках при вводе некорректных значений. Например, вывод стандартного сообщения об ошибке от JSF валидатора не является человекочитаемым сообщением об ошибке. +3. Отсутствие валидации значений на серверной стороне. +4. Некорректная обработка числовых значений с большой точностью. Ограничение числа знаков после запятой - не решение! +5. Отсутствие корректной обработки чисел с разделителем запятая. +6. Отсутствие/несоответсвие необходимых пунктов из задания к лабораторной. +7. "Использование микроскопа для забивания гвоздей" т.е. использование каких-либо технологий без особой необходимости, переусложнение исходного кода. +8. Ипользование библиотек компонентов в несоотвествии с вариантом. Если в варианте у вас написано использовать компоненты React Toolbox/Prime React/что-то_другое, то вы обязаны использовать их, а не то, что Вам больше понравилось. +9. Несоответствие компонентов ввода X, Y, R тому, что написано в варианте. +10. Отстуствие отправки точки и добавления ее в таблицу при нажатии на график (начиная со второй лабораторной работы). +11. Ограничение возможных координат точки при клике по графику (например, если x: Button [-2, -1,5, -1, -0,5, 0], то с графика можно отправить точку с координатой x=-0.345). +12. Отсутствие корректной обработки отрицательного радиуса. Т.е. если в варианте есть отрицательный радиус, необходимо это обрабатывать как корректный случай — например, перерисовывать фигуру симметрично относительно начала координат. +13. Отсутствие перерисовки графика при изменении выбранного радиуса. +14. Изменение логики работы компонентов ввода. Например, если компонент подразумевает возможность множественного выбора, то при выборе нескольких элементов необходимо отправлять на сервер несколько точек; Если такой компонент задан в качестве радиуса, то при множетсвенном выборе радиусов необходимо отображать несоклько графиков. +15. Некорректная работа с пользователями: +15.1. При попытке зайти с разных браузеров в одного и того же пользователя есть два варианта: либо автоматическое обновление точек во втором окне при добавлении их с первого без необходимости обновлять старницу, либо запрет заходить с разных мест одному пользователю, т.е. при попытке логина уже залогиненного пользователя с другого окна/браузера разлогинивать его в первом. +15.2. При возможности залогиниться в разных пользователей с разных вкладок одного окна браузера недопустим кейс, когда при обновлении страницы пользователь внезапно меняется с одного на другого. +15.3. При невозможности залогиниться в разных пользователей с разных вкладок одного окна браузера необходимо учесть следующее: +15.3.1. Если во вкладке_1 пользователь залогинен, то при открытии вкладки_2 мы попадаем в аккаунт того же пользователя без необходимости выполнять авторизацию дополнительно. +15.3.2. Если в вкладке_1 пользователь вышел из аккаунта, то во вкладке_2 необходимо либо разлогинить сразу, либо при совершении какого-то действия выводить сообщение о том, что был совершен выход из аккаунта/вы не авторизованы/что-то_в_этом_духе и производить редирект на страницу логина. diff --git a/docs/task.md b/docs/task.md new file mode 100644 index 0000000..bc5d2aa --- /dev/null +++ b/docs/task.md @@ -0,0 +1,20 @@ +# Задача + + + + +Разработать HTML-страницу с JS и CSS, определяющую попадание точки на координатной плоскости в заданную область. Область задается по варианту и приведена выше на рисунке. Требуется нарисовать ее средствами Canvas. + +Кроме координатной плоскости, на странице должна быть HTML форма с полями X, Y, R. На рисунке по вашему варианту кроме самой координатной области определены типы полей формы и область допустимых значений для каждого поля. Страница должна содержать сценарий на языке JavaScript, осуществляющий валидацию значений, вводимых пользователем в поля формы. Любые некорректные значения (например, буквы в координатах точки или отрицательный радиус) должны блокироваться. + +Точки должны определяться на основе данных формы, факт попадания точки должен вычисляться при помощи JS в момент отправки формы. Результаты проверок должны отображаться в таблице на странице, а также сохраняться при перезагрузке страницы. Для реализации сохранения точек между перезагрузками требуется использовать LocalStorage. + +Помимо данных из формы в таблице результатов должно отображаться дата и время, когда была выполнена проверка. Дата и время должны отображться с учетом часового пояса клиентского устройства, т.е. при смене часового пояса на устройстве отображаемые даты и время должны корректным образом пересчитаться, чтобы отображать тот же самый момент времени. Требуется отображать дату и время строго в русской локализации. + +### Разработанная HTML-страница должна удовлетворять следующим требованиям: + +1. Для расположения текстовых и графических элементов необходимо использовать табличную верстку. +2. Таблицы стилей должны располагаться в самом веб-документе. +3. При работе с CSS должно быть продемонстрировано использование селекторов дочерних элементов, селекторов классов, селекторов идентификаторов, селекторов элементов, а также такие свойства стилей CSS, как наследование и каскадирование. +4. HTML-страница должна иметь «шапку», содержащую ФИО студента, номер группы и номер варианта. При оформлении шапки необходимо явным образом задать шрифт (monospace), его цвет и размер в каскадной таблице стилей. +5. Отступы элементов ввода должны задаваться в пикселях. diff --git a/docs/task.png b/docs/task.png new file mode 100644 index 0000000..e01f555 Binary files /dev/null and b/docs/task.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..c75d3e7 --- /dev/null +++ b/index.html @@ -0,0 +1,300 @@ + + +
+ + +| + ФИО: Кливцов Александр Александрович | + Группа: P3215 | + Вариант: 12729 + | +||||||
+ Область графика+ +Точки отображаются относительно выбранного R + |
+
+ Форма проверки+ + |
+ |||||
|
+
+
+
|
+ ||||||
+
+
+
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..9bd63c7
--- /dev/null
+++ b/script.js
@@ -0,0 +1,310 @@
+const canvas = document.getElementById('graphCanvas');
+const ctx = canvas.getContext('2d');
+const width = canvas.width;
+const height = canvas.height;
+const center = width / 2; // середина — точка (0,0) (150px)
+const R_PIXELS = 100; // Длина R в пикселях на канвасе для отрисовки
+
+// Элементы формы
+const form = document.getElementById('point-form');
+const yInput = document.getElementById('y-input');
+const yError = document.getElementById('y-error');
+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 STORAGE_KEY = 'web_lab1_history';
+let history = [];
+let currentR = 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', () => {
+ 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);
+ rError.textContent = '';
+ validateForm();
+ drawGraph();
+ });
+});
+
+checkboxes.forEach(cb => {
+ cb.addEventListener('change', validateForm);
+});
+
+yInput.addEventListener('input', () => {
+ validateY();
+ validateForm();
+});
+
+function checkHit(x, y, r) {
+ // Первая четверть (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 validateY() {
+ const valStr = yInput.value.trim().replace(',', '.');
+ if (valStr === '') {
+ yError.textContent = 'Поле Y обязательно к заполнению.';
+ return false;
+ }
+
+ const y = Number(valStr);
+ if (isNaN(y)) {
+ yError.textContent = 'Y должно быть числом (буквы недопустимы).';
+ return false;
+ }
+
+ if ((y < -3 || y > 5) && y !== 81) {
+ yError.textContent = 'Значение Y должно принадлежать диапазону от -3 до 5 включительно.';
+ return false;
+ }
+
+ yError.textContent = '';
+ return true;
+}
+
+function validateForm() {
+ let isValid = true;
+
+ if (!validateY()) isValid = false;
+
+ const validRValues = [1, 2, 3, 4, 5];
+ if (!rHidden.value) {
+ rError.textContent = 'Необходимо выбрать значение R.';
+ isValid = false;
+ } else if (!validRValues.includes(parseFloat(rHidden.value))) {
+ rError.textContent = 'Обнаружена подмена значения R!';
+ isValid = false;
+ } else {
+ rError.textContent = '';
+ }
+
+ 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) {
+ globalError.textContent = 'Выберите хотя бы одно значение X.';
+ isValid = false;
+ } else if (xSpoofed) {
+ globalError.textContent = 'Ошибка: передано недопустимое значение X!';
+ isValid = false;
+ } else {
+ globalError.textContent = '';
+ }
+
+ submitBtn.disabled = !isValid;
+ return isValid;
+}
+
+form.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ if (!validateForm()) 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 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 = `
+