commit c8f62d169abc81285ab61d9dfdad2ae3d5364cf0 Author: AK Date: Tue Sep 22 01:24:42 2026 +0300 initial commit. Ver 1.0 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 @@ +# Задача + +![картинка задания](task.png) + + +Разработать 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 @@ + + + + + + Лабораторная работа №1 + + + + + + + + + + + + + + + +
+

Область графика

+ +

Точки отображаются относительно выбранного R

+
+

Форма проверки

+
+ +
+ +
+
+
+ + + + +
+ + + + +
+
+ +
+ + + +
+ +
+
+ + + + + + + +
+ + +
+
+
+ + + + + + + + + + + + + +
XYRРезультатДата и время
+
+
+ + +Toasty + + + 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 = ` + ${rec.x} + ${rec.y} + ${rec.r} + + ${rec.hit ? 'Попадание' : 'Промах'} + + ${rec.time} + `; + resultsBody.appendChild(tr); + }); +} + +function drawGraph() { + ctx.clearRect(0, 0, width, height); + 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 / currentR) * R_PIXELS; + let drawY = center - (rec.y / currentR) * 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(); + }); + } +}