initial commit. Ver 1.0
This commit is contained in:
@@ -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.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
@@ -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 необходимо либо разлогинить сразу, либо при совершении какого-то действия выводить сообщение о том, что был совершен выход из аккаунта/вы не авторизованы/что-то_в_этом_духе и производить редирект на страницу логина.
|
||||||
@@ -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. Отступы элементов ввода должны задаваться в пикселях.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
+300
@@ -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>
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user