initial commit. Ver 1.0

This commit is contained in:
AK
2026-09-22 01:24:42 +03:00
commit c8f62d169a
7 changed files with 669 additions and 0 deletions
+17
View File
@@ -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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

+22
View File
@@ -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 необходимо либо разлогинить сразу, либо при совершении какого-то действия выводить сообщение о том, что был совершен выход из аккаунта/вы не авторизованы/что-то_в_этом_духе и производить редирект на страницу логина.
+20
View File
@@ -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. Отступы элементов ввода должны задаваться в пикселях.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+300
View File
@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Лабораторная работа №1</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #F5F5F7;
margin: 0;
padding: 20px;
color: #1D1D1F;
-webkit-font-smoothing: antialiased;
}
#layout-table {
width: 100%;
max-width: 900px;
margin: 0 auto;
background: transparent;
border-collapse: separate;
border-spacing: 20px;
}
table, th, td {
border: none;
}
#header {
background-color: #FFFFFF;
color: #000000;
padding: 20px;
text-align: center;
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
font-size: 16px;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.04);
}
#header span.variant {
font-weight: 600;
color: #007AFF;
}
.content-cell {
background: #FFFFFF;
border-radius: 16px;
padding: 30px;
vertical-align: top;
width: 50%;
box-shadow: 0 4px 24px rgba(0,0,0,0.04);
}
h3 {
margin-top: 0;
font-weight: 600;
letter-spacing: -0.5px;
}
#point-form > .form-group {
background: #F5F5F7;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
border: none;
}
.checkbox-group label {
margin-right: 15px;
display: inline-block;
font-size: 14px;
cursor: pointer;
}
#point-form input[type="text"] {
margin-left: 10px;
margin-right: 10px;
padding: 10px 14px;
width: 120px;
background: #E5E5EA;
border: 1px solid transparent;
border-radius: 8px;
font-size: 15px;
outline: none;
transition: all 0.2s ease;
}
#point-form input[type="text"]:focus {
background: #FFFFFF;
border: 1px solid #007AFF;
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.2);
}
.r-button {
margin-right: 8px;
margin-top: 10px;
margin-bottom: 5px;
padding: 8px 16px;
background: #E5E5EA;
color: #1D1D1F;
border: none;
cursor: pointer;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
}
.r-button:hover {
background: #D1D1D6;
}
.r-button.active {
background: #007AFF;
color: #FFFFFF;
}
#submit-btn {
margin-top: 10px;
margin-bottom: 5px;
padding: 12px 24px;
background-color: #007AFF;
color: #FFFFFF;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
width: 100%;
transition: all 0.2s ease;
}
#submit-btn:disabled {
background-color: #E5E5EA;
color: #8E8E93;
cursor: not-allowed;
}
#submit-btn:hover:not(:disabled) {
transform: scale(0.98);
background-color: #0066CC;
}
.error {
color: #FF3B30;
font-size: 13px;
display: block;
margin-top: 8px;
}
.global-error {
color: #FF3B30;
font-weight: 600;
margin-bottom: 12px;
font-size: 14px;
}
.table-scroll-container {
max-height: 250px;
overflow-y: auto;
margin-top: 0px;
border-radius: 12px;
background: #FFFFFF;
border: 1px solid #E5E5EA;
box-sizing: border-box;
}
#results-table {
width: 100%;
border-collapse: collapse;
text-align: center;
font-size: 14px;
}
#results-table th {
background-color: rgba(245, 245, 247, 0.9);
backdrop-filter: blur(10px);
color: #8E8E93;
padding: 12px;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid #E5E5EA;
}
#results-table td {
padding: 12px;
border-bottom: 1px solid #E5E5EA;
color: #1D1D1F;
}
#results-table tr:last-child td {
border-bottom: none;
}
.canvas-container {
text-align: center;
}
/* Oscar Piastri */
#oscar-easter-egg {
position: fixed;
bottom: -350px;
left: 0;
max-width: 250px;
z-index: 9999;
transition: bottom 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
}
#oscar-easter-egg.show {
bottom: 0;
}
</style>
</head>
<body>
<table id="layout-table">
<tr>
<td colspan="2" id="header">
<span>ФИО: <b>Кливцов Александр Александрович</b></span> |
<span>Группа: <b>P3215</b></span> |
<span class="variant">Вариант: 12729</span>
</td>
</tr>
<tr>
<td class="content-cell canvas-container">
<h3>Область графика</h3>
<canvas id="graphCanvas" width="300" height="300"></canvas>
<p style="font-size: 13px; color: #8E8E93; margin-top: 15px;">Точки отображаются относительно выбранного R</p>
</td>
<td class="content-cell">
<h3>Форма проверки</h3>
<form id="point-form">
<div class="global-error" id="global-error"></div>
<div class="form-group" id="x-group">
<label><b>Изменение X (выберите хотя бы один):</b></label><br>
<div class="checkbox-group" style="margin-top: 10px;">
<label><input type="checkbox" name="xval" value="-2"> -2</label>
<label><input type="checkbox" name="xval" value="-1.5"> -1.5</label>
<label><input type="checkbox" name="xval" value="-1"> -1</label>
<label><input type="checkbox" name="xval" value="-0.5"> -0.5</label>
<label><input type="checkbox" name="xval" value="0"> 0</label><br>
<label><input type="checkbox" name="xval" value="0.5"> 0.5</label>
<label><input type="checkbox" name="xval" value="1"> 1</label>
<label><input type="checkbox" name="xval" value="1.5"> 1.5</label>
<label><input type="checkbox" name="xval" value="2"> 2</label>
</div>
</div>
<div class="form-group" id="y-group">
<label for="y-input"><b>Изменение Y:</b></label>
<input type="text" id="y-input" name="yval" placeholder="-3 ... 5">
<span class="error" id="y-error"></span>
</div>
<div class="form-group" id="r-group">
<label><b>Изменение R:</b></label><br>
<button type="button" class="r-button" data-val="1">1</button>
<button type="button" class="r-button" data-val="2">2</button>
<button type="button" class="r-button" data-val="3">3</button>
<button type="button" class="r-button" data-val="4">4</button>
<button type="button" class="r-button" data-val="5">5</button>
<input type="hidden" id="r-hidden" name="rval" value="">
<span class="error" id="r-error"></span>
</div>
<button type="submit" id="submit-btn" disabled>Проверить попадание</button>
</form>
</td>
</tr>
<tr>
<td colspan="2" class="content-cell" style="width: 100%; padding: 20px;">
<div class="table-scroll-container">
<table id="results-table">
<thead>
<tr>
<th>X</th>
<th>Y</th>
<th>R</th>
<th>Результат</th>
<th>Дата и время</th>
</tr>
</thead>
<tbody id="results-body">
<!-- Данные из LocalStorage -->
</tbody>
</table>
</div>
</td>
</tr>
</table>
<!-- Пасхалка -->
<img id="oscar-easter-egg" src="assets/oscar.png" alt="Toasty">
<script src="script.js"></script>
</body>
</html>
+310
View File
@@ -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 = `
<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);
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();
});
}
}