commit 994e2af1664becd33bba8566e8327cfa00c3406f Author: Gleb O. Ivaniczkij Date: Sun Jul 28 19:52:17 2024 +0300 Разработано веб-приложение diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e4c7aa5 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +from .main import app diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..8fd3c0f --- /dev/null +++ b/app/main.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from fastapi.responses import HTMLResponse +from jinja2 import Environment, FileSystemLoader + + +app = FastAPI( + openapi_url=None, + docs_url=None, + redoc_url=None, +) + +env = Environment( + loader=FileSystemLoader( + searchpath='./templates', + ), + enable_async=True, +) + + +@app.get('/') +async def function(): + template = env.get_template('main.jinja2') + return HTMLResponse( + content=await template.render_async( + title='Калькулятор рисков', + description='Матрица n×m: игры с природой. Матрица 2×2: аналитический и графический методы.', + keywords='калькулятор,теория рисков,теория игр,матрица,игры с природой,критерии,аналитический метод,графический метод', + robots='all', + ), + status_code=200, + ) diff --git a/main.py b/main.py new file mode 100644 index 0000000..74929ed --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +from app import app + + +if __name__ == '__main__': + app.run() diff --git a/nginx/main.conf b/nginx/main.conf new file mode 100644 index 0000000..347403f --- /dev/null +++ b/nginx/main.conf @@ -0,0 +1,27 @@ +server { + listen 212.109.196.217:443 ssl; + listen [2a01:230:4:700::6969]:443 ssl; + + server_name risk-calc.csasq.ru; + server_tokens off; + + ssl_certificate /etc/letsencrypt/live/csasq.ru/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/csasq.ru/privkey.pem; + + keepalive_timeout 5; + + location / { + include /opt/risk-calc.csasq.ru/nginx/secure-headers.conf; + include ./conf.d/http-proxy.conf; + + proxy_pass http://risk-calc.csasq.ru; + } + + location /static/ { + include /opt/risk-calc.csasq.ru/nginx/secure-headers.conf; + include ./conf.d/gzip.conf; + + root /opt/risk-calc.csasq.ru; + expires 1d; + } +} diff --git a/nginx/secure-headers.conf b/nginx/secure-headers.conf new file mode 100644 index 0000000..c1520c8 --- /dev/null +++ b/nginx/secure-headers.conf @@ -0,0 +1,6 @@ +add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'"; +add_header X-Frame-Options "DENY"; +add_header X-Content-Type-Options "nosniff"; +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; +add_header Referrer-Policy "origin"; +add_header X-XSS-Protection "1; mode=block; report=mailto:security@csasq.ru"; diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cdea98f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi +gunicorn +jinja2 +uvicorn diff --git a/static/fonts/cyrillic-ext.woff2 b/static/fonts/cyrillic-ext.woff2 new file mode 100644 index 0000000..c70a9d3 Binary files /dev/null and b/static/fonts/cyrillic-ext.woff2 differ diff --git a/static/fonts/cyrillic.woff2 b/static/fonts/cyrillic.woff2 new file mode 100644 index 0000000..f4b21e9 Binary files /dev/null and b/static/fonts/cyrillic.woff2 differ diff --git a/static/fonts/latin-ext.woff2 b/static/fonts/latin-ext.woff2 new file mode 100644 index 0000000..9b7465b Binary files /dev/null and b/static/fonts/latin-ext.woff2 differ diff --git a/static/fonts/latin.woff2 b/static/fonts/latin.woff2 new file mode 100644 index 0000000..5c5fab5 Binary files /dev/null and b/static/fonts/latin.woff2 differ diff --git a/static/fonts/vietnamese.woff2 b/static/fonts/vietnamese.woff2 new file mode 100644 index 0000000..420761f Binary files /dev/null and b/static/fonts/vietnamese.woff2 differ diff --git a/static/icons/risk-calc.svg b/static/icons/risk-calc.svg new file mode 100644 index 0000000..edd212e --- /dev/null +++ b/static/icons/risk-calc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/scripts/main.js b/static/scripts/main.js new file mode 100644 index 0000000..6e01685 --- /dev/null +++ b/static/scripts/main.js @@ -0,0 +1,4 @@ +document.addEventListener('DOMContentLoaded', () => { + window.application = new Application(); + application.open(Application.DEFAULT_FRAGMENT); +}); diff --git a/static/scripts/vendor.js b/static/scripts/vendor.js new file mode 100644 index 0000000..c3b5005 --- /dev/null +++ b/static/scripts/vendor.js @@ -0,0 +1,1174 @@ +Math.randint = (min, max) => Math.floor(Math.random() * (max - min + 1) + min); + +String.prototype.subscript = function () { + return this + .replaceAll(/0/g, '\u2080') + .replaceAll(/1/g, '\u2081') + .replaceAll(/2/g, '\u2082') + .replaceAll(/3/g, '\u2083') + .replaceAll(/4/g, '\u2084') + .replaceAll(/5/g, '\u2085') + .replaceAll(/6/g, '\u2086') + .replaceAll(/7/g, '\u2087') + .replaceAll(/8/g, '\u2088') + .replaceAll(/9/g, '\u2089'); +}; + +class Menu { + #application; + #root; + #items; + + constructor(application) { + this.#application = application; + this.#root = document.querySelector('#menu'); + this.#items = this.#root.querySelectorAll('input[data-fragment]'); + this.#items.forEach(item => { + item.addEventListener('click', () => this.#application.open(item.dataset.fragment)); + }); + }; + + setActive(fragmentId) { + this.#items.forEach(item => item.classList.remove('active')); + this.#items.forEach(item => item.dataset.fragment === fragmentId ? item.classList.add('active') : null); + }; +} + +class BottomBar { + #copyrightYears; + + constructor() { + this.#copyrightYears = document.querySelector('#copyright-years'); + if (this.copyrightToYear > this.copyrightFromYear) this.copyrightYears = `${this.copyrightFromYear}—${this.copyrightToYear}`; + }; + + get copyrightFromYear() { + return Number(this.#copyrightYears.dataset.from); + }; + + get copyrightToYear() { + const date = new Date(); + return date.getFullYear(); + }; + + set copyrightYears(value) { + this.#copyrightYears.innerHTML = value; + }; +} + +class Fragment1 { + #matrixTable; + #widthInput; + #heightInput; + #generateMatrixButton; + #minRandomValueInput; + #maxRandomValueInput; + #fillRandomValuesButton; + #clearMatrixButton; + #optimismCriterionInput; + #fractionDigitsInput; + #lowestPriceInput; + #highestPriceInput; + #saddlePointInput; + #laplaceCriterionInput; + #waldCriterionInput; + #maxOfMaxCriterionInput; + #minOfMinCriterionInput; + #savageCriterionInput; + #hurwitzCriterionInput; + #regretMatrixTable; + #extra; + + constructor() { + this.#matrixTable = document.querySelector('#fragment-1-matrix'); + this.#widthInput = document.querySelector('#fragment-1-width'); + this.#heightInput = document.querySelector('#fragment-1-height'); + this.#generateMatrixButton = document.querySelector('#fragment-1-generate-matrix'); + this.#minRandomValueInput = document.querySelector('#fragment-1-min-random-value'); + this.#maxRandomValueInput = document.querySelector('#fragment-1-max-random-value'); + this.#fillRandomValuesButton = document.querySelector('#fragment-1-fill-random-values'); + this.#clearMatrixButton = document.querySelector('#fragment-1-clear-matrix'); + this.#optimismCriterionInput = document.querySelector('#fragment-1-optimism-criterion'); + this.#fractionDigitsInput = document.querySelector('#fragment-1-fraction-digits'); + this.#lowestPriceInput = document.querySelector('#fragment-1-lowest-price'); + this.#highestPriceInput = document.querySelector('#fragment-1-highest-price'); + this.#saddlePointInput = document.querySelector('#fragment-1-saddle-point'); + this.#laplaceCriterionInput = document.querySelector('#fragment-1-laplace-criterion'); + this.#waldCriterionInput = document.querySelector('#fragment-1-wald-criterion'); + this.#maxOfMaxCriterionInput = document.querySelector('#fragment-1-max-of-max-criterion'); + this.#minOfMinCriterionInput = document.querySelector('#fragment-1-min-of-min-criterion'); + this.#savageCriterionInput = document.querySelector('#fragment-1-savage-criterion'); + this.#hurwitzCriterionInput = document.querySelector('#fragment-1-hurwitz-criterion'); + this.#regretMatrixTable = document.querySelector('#fragment-1-regret-matrix'); + this.#extra = document.querySelectorAll('.extra'); + this.setMatrixSize(); + this.generateMatrix(); + this.#generateMatrixButton.addEventListener('click', () => { + this.setMatrixSize(); + this.generateMatrix(); + }); + this.#fillRandomValuesButton.addEventListener('click', () => this.generateMatrix(true)); + this.#clearMatrixButton.addEventListener('click', () => this.generateMatrix()); + [ + this.#lowestPriceInput, + this.#highestPriceInput, + this.#saddlePointInput, + this.#laplaceCriterionInput, + this.#waldCriterionInput, + this.#maxOfMaxCriterionInput, + this.#minOfMinCriterionInput, + this.#savageCriterionInput, + this.#hurwitzCriterionInput, + ].forEach(input => input.addEventListener('input', () => input.value = input.dataset.value)); + }; + + getMatrixWidth() { + return Number(this.#widthInput.dataset.value); + }; + + getMatrixHeight() { + return Number(this.#heightInput.dataset.value); + }; + + getMinRandomValue() { + return Number(this.#minRandomValueInput.value); + }; + + getMaxRandomValue() { + return Number(this.#maxRandomValueInput.value); + }; + + getOptimismCriterion() { + return Number(this.#optimismCriterionInput.value); + } + + getFractionDigits() { + return Number(this.#fractionDigitsInput.value); + }; + + getRows() { + const rows = []; + const width = this.getMatrixWidth(); + const height = this.getMatrixHeight(); + for (let i = 0; i < height; i++) { + const row = { + index: this.#matrixTable.querySelector(`tr:nth-child(${i + 2}) > th`).textContent, + cols: [], + }; + for (let j = 0; j < width; j++) { + row.cols.push({ + index: this.#matrixTable.querySelector(`tr:first-child > th:nth-child(${j + 2})`).textContent, + value: Number(this.#matrixTable.querySelector(`tr:nth-child(${i + 2}) > td:nth-child(${j + 2}) > input`).value), + }); + } + rows.push(row); + } + return rows; + }; + + transposeMatrix(rows) { + return rows[0].cols.map((col, i) => { + return { + index: col.index, + rows: rows.map(row => { + return { + index: row.index, + value: row.cols[i].value, + }; + }), + }; + }); + }; + + getRegretMatrix(cols, fractionDigits) { + const maxes = cols.map(col => col.rows.reduce((row1, row2) => { + return { + index: col.index, + value: Math.max(row1.value, row2.value), + }; + })); + const transposedRegretMatrix = cols.map((col, i) => { + return { + index: col.index, + rows: col.rows.map(row => { + return { + index: row.index, + value: parseFloat((maxes[i].value - row.value).toFixed(fractionDigits)), + }; + }), + }; + }); + return transposedRegretMatrix[0].rows.map((row, i) => { + return { + index: row.index, + cols: transposedRegretMatrix.map(col => { + return { + index: col.index, + value: col.rows[i].value, + }; + }), + }; + }); + }; + + getLowestPrice(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.min(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set lowestPrice(lowestPrise) { + const value = `${lowestPrise.index} (${lowestPrise.value})`; + this.#lowestPriceInput.dataset.value = String(value); + this.#lowestPriceInput.value = String(value); + }; + + getHighestPrice(cols, fractionDigits) { + return cols.map(col => col.rows.reduce((row1, row2) => { + return { + index: col.index, + value: Math.max(row1.value, row2.value), + }; + })).reduce((col1, col2) => { + return { + index: col1.value < col2.value ? col1.index : col1.value > col2.value ? col2.index : `${col1.index}, ${col2.index}`, + value: parseFloat(Math.min(col1.value, col2.value).toFixed(fractionDigits)), + }; + }); + }; + + set highestPrice(highestPrice) { + const value = `${highestPrice.index} (${highestPrice.value})`; + this.#highestPriceInput.dataset.value = String(value); + this.#highestPriceInput.value = String(value); + }; + + getSaddlePoint(lowestPrice, highestPrice) { + return lowestPrice.value === highestPrice.value ? lowestPrice.value : null; + }; + + set saddlePoint(value) { + this.#saddlePointInput.dataset.value = String(value); + this.#saddlePointInput.value = String(value); + }; + + getLaplaceCriterion(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: col1.value + col2.value, + length: row.cols.length, + }; + })).map(row => { + return { + index: row.index, + value: row.value / row.length, + }; + }).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + } + }); + }; + + set laplaceCriterion(criterion) { + const value = `${criterion.index} (${criterion.value})`; + this.#laplaceCriterionInput.dataset.value = value; + this.#laplaceCriterionInput.value = value; + }; + + getWaldCriterion(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.min(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set waldCriterion(criterion) { + const value = `${criterion.index} (${criterion.value})`; + this.#waldCriterionInput.dataset.value = String(value); + this.#waldCriterionInput.value = `${value}`; + }; + + getMaxOfMaxCriterion(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.max(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set maxOfMaxCriterion(criterion) { + const value = `${criterion.index} (${criterion.value})`; + this.#maxOfMaxCriterionInput.dataset.value = String(value); + this.#maxOfMaxCriterionInput.value = `${value}`; + }; + + getMinOfMinCriterion(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.min(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value < row2.value ? row1.index : row1.value > row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.min(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set minOfMinCriterion(criterion) { + const value = `${criterion.index} (${criterion.value})`; + this.#minOfMinCriterionInput.dataset.value = String(value); + this.#minOfMinCriterionInput.value = `${value}`; + }; + + getSavageCriterion(regretMatrix, fractionDigits) { + return regretMatrix.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.max(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value < row2.value ? row1.index : row1.value > row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.min(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set savageCriterion(criterion) { + const value = `${criterion.index} (${criterion.value})`; + this.#savageCriterionInput.dataset.value = String(value); + this.#savageCriterionInput.value = `${value}`; + }; + + getHurwitzCriterion(rows, optimismCriterion, fractionDigits) { + return rows.map(row => row.cols.map(col => { + col.min = col.value; + col.max = col.value; + return col; + }).reduce((col1, col2) => { + return { + index: row.index, + min: Math.min(col1.min, col2.min), + max: Math.max(col1.max, col2.max), + }; + })).map(row => { + return { + index: row.index, + value: optimismCriterion * row.min + (1 - optimismCriterion) * row.max, + }; + }).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set hurwitzCriterion(criterion) { + const value = `${criterion.index} (${criterion.value})`; + this.#hurwitzCriterionInput.dataset.value = String(value); + this.#hurwitzCriterionInput.value = `${value}`; + }; + + set regretMatrix(rows) { + this.#regretMatrixTable.replaceChildren(); + const width = this.getMatrixWidth(); + const height = this.getMatrixHeight(); + const tr = document.createElement('tr'); + const th = document.createElement('th'); + tr.appendChild(th); + for (let i = 0; i < width; i++) { + const th = document.createElement('th'); + th.textContent = `B${i + 1}`.subscript(); + tr.appendChild(th); + } + this.#regretMatrixTable.appendChild(tr); + for (let i = 0; i < height; i++) { + const tr = document.createElement('tr'); + const th = document.createElement('th'); + th.textContent = `A${i + 1}`.subscript(); + tr.appendChild(th); + this.#regretMatrixTable.appendChild(tr); + for (let j = 0; j < width; j++) { + const td = document.createElement('td'); + const input = document.createElement('input'); + input.type = 'number'; + input.value = rows[i].cols[j].value; + input.dataset.value = rows[i].cols[j].value; + input.ariaLabel = `Выделить ячейку A${i + 1}B${j + 1}`; + input.addEventListener('input', () => input.value = input.dataset.value); + input.dataset.a = String(i); + input.dataset.b = String(j); + input.addEventListener('keydown', (event) => { + const selectCell = (a, b) => this.#regretMatrixTable.querySelector(`input[type="number"][data-a="${a}"][data-b="${b}"]`).select(); + switch (event.code) { + case 'ArrowUp': + event.preventDefault(); + event.stopPropagation(); + selectCell((Number(input.dataset.a) - 1 + height) % height, input.dataset.b); + break; + + case 'ArrowDown': + event.preventDefault(); + event.stopPropagation(); + selectCell((Number(input.dataset.a) + 1 + height) % height, input.dataset.b); + break; + + case 'ArrowLeft': + event.preventDefault(); + event.stopPropagation(); + selectCell(input.dataset.a, (Number(input.dataset.b) - 1 + width) % width); + break; + + case 'ArrowRight': + event.preventDefault(); + event.stopPropagation(); + selectCell(input.dataset.a, (Number(input.dataset.b) + 1 + width) % width); + break; + } + }); + td.appendChild(input); + tr.appendChild(td); + } + } + }; + + setMatrixSize() { + this.#widthInput.dataset.value = this.#widthInput.value; + this.#heightInput.dataset.value = this.#heightInput.value; + }; + + generateMatrix(randomValues) { + this.#matrixTable.replaceChildren(); + const width = this.getMatrixWidth(); + const height = this.getMatrixHeight(); + const minRandomValue = this.getMinRandomValue(); + const maxRandomValue = this.getMaxRandomValue(); + const tr = document.createElement('tr'); + const th = document.createElement('th'); + tr.appendChild(th); + for (let i = 0; i < width; i++) { + const th = document.createElement('th'); + th.textContent = `B${i + 1}`.subscript(); + tr.appendChild(th); + } + this.#matrixTable.appendChild(tr); + for (let i = 0; i < height; i++) { + const tr = document.createElement('tr'); + const th = document.createElement('th'); + th.textContent = `A${i + 1}`.subscript(); + tr.appendChild(th); + this.#matrixTable.appendChild(tr); + for (let j = 0; j < width; j++) { + const td = document.createElement('td'); + const input = document.createElement('input'); + input.type = 'number'; + input.ariaLabel = `Изменить ячейку A${i + 1}B${j + 1}`; + input.dataset.a = String(i); + input.dataset.b = String(j); + input.addEventListener('keydown', (event) => { + const selectCell = (a, b) => this.#matrixTable.querySelector(`input[type="number"][data-a="${a}"][data-b="${b}"]`).select(); + switch (event.code) { + case 'ArrowUp': + event.preventDefault(); + event.stopPropagation(); + selectCell((Number(input.dataset.a) - 1 + height) % height, input.dataset.b); + break; + + case 'ArrowDown': + event.preventDefault(); + event.stopPropagation(); + selectCell((Number(input.dataset.a) + 1 + height) % height, input.dataset.b); + break; + + case 'ArrowLeft': + event.preventDefault(); + event.stopPropagation(); + selectCell(input.dataset.a, (Number(input.dataset.b) - 1 + width) % width); + break; + + case 'ArrowRight': + event.preventDefault(); + event.stopPropagation(); + selectCell(input.dataset.a, (Number(input.dataset.b) + 1 + width) % width); + break; + } + }); + if (randomValues) input.value = `${Math.randint(minRandomValue, maxRandomValue)}`; + td.appendChild(input); + tr.appendChild(td); + } + } + }; + + solve() { + const rows = this.getRows(); + const cols = this.transposeMatrix(rows); + const fractionDigits = this.getFractionDigits(); + const regretMatrix = this.getRegretMatrix(cols, fractionDigits); + const optimismCriterion = this.getOptimismCriterion(); + const lowestPrice = this.getLowestPrice(rows, fractionDigits); + const highestPrice = this.getHighestPrice(cols, fractionDigits); + const saddlePoint = this.getSaddlePoint(lowestPrice, highestPrice); + this.lowestPrice = lowestPrice; + this.highestPrice = highestPrice; + this.saddlePoint = saddlePoint === null ? 'Нет' : saddlePoint; + this.laplaceCriterion = this.getLaplaceCriterion(rows, fractionDigits); + this.waldCriterion = this.getWaldCriterion(rows, fractionDigits); + this.maxOfMaxCriterion = this.getMaxOfMaxCriterion(rows, fractionDigits); + this.minOfMinCriterion = this.getMinOfMinCriterion(rows, fractionDigits); + this.savageCriterion = this.getSavageCriterion(regretMatrix, fractionDigits); + this.hurwitzCriterion = this.getHurwitzCriterion(rows, optimismCriterion, fractionDigits); + this.regretMatrix = regretMatrix; + }; +} + +class Fragment2 { + #matrixTable; + #minRandomValueInput; + #maxRandomValueInput; + #fillRandomValuesButton; + #clearMatrixButton; + #fractionDigitsInput; + #graphSizeInput; + #lowestPriceInput; + #highestPriceInput; + #saddlePointInput; + #p1Input; + #p2Input; + #q1Input; + #q2Input; + #vInput; + #aPlayerGraph; + #bPlayerGraph; + #extra; + + constructor() { + this.#matrixTable = document.querySelector('#fragment-2-matrix'); + this.#minRandomValueInput = document.querySelector('#fragment-2-min-random-value'); + this.#maxRandomValueInput = document.querySelector('#fragment-2-max-random-value'); + this.#fillRandomValuesButton = document.querySelector('#fragment-2-fill-random-values'); + this.#clearMatrixButton = document.querySelector('#fragment-2-clear-matrix'); + this.#fractionDigitsInput = document.querySelector('#fragment-2-fraction-digits'); + this.#graphSizeInput = document.querySelector('#fragment-2-graph-size'); + this.#lowestPriceInput = document.querySelector('#fragment-2-lowest-price'); + this.#highestPriceInput = document.querySelector('#fragment-2-highest-price'); + this.#saddlePointInput = document.querySelector('#fragment-2-saddle-point'); + this.#p1Input = document.querySelector('#fragment-2-p1'); + this.#p2Input = document.querySelector('#fragment-2-p2'); + this.#q1Input = document.querySelector('#fragment-2-q1'); + this.#q2Input = document.querySelector('#fragment-2-q2'); + this.#vInput = document.querySelector('#fragment-2-v'); + this.#aPlayerGraph = new Graph(document.querySelector('#fragment-2-a-player-graph')); + this.#bPlayerGraph = new Graph(document.querySelector('#fragment-2-b-player-graph')); + this.#extra = document.querySelectorAll('.extra'); + this.generateMatrix(); + this.#fillRandomValuesButton.addEventListener('click', () => this.generateMatrix(true)); + this.#clearMatrixButton.addEventListener('click', () => this.generateMatrix()); + [ + this.#lowestPriceInput, + this.#highestPriceInput, + this.#saddlePointInput, + this.#p1Input, + this.#p2Input, + this.#q1Input, + this.#q2Input, + this.#vInput, + ].forEach(input => input.addEventListener('input', () => input.value = input.dataset.value)); + }; + + getMatrixWidth() { + return 2; + }; + + getMatrixHeight() { + return 2; + }; + + getMinRandomValue() { + return Number(this.#minRandomValueInput.value); + }; + + getMaxRandomValue() { + return Number(this.#maxRandomValueInput.value); + }; + + getFractionDigits() { + return Number(this.#fractionDigitsInput.value); + }; + + getGraphSize() { + return Number(this.#graphSizeInput.value); + }; + + getRows() { + const rows = []; + const width = this.getMatrixWidth(); + const height = this.getMatrixHeight(); + for (let i = 0; i < height; i++) { + const row = { + index: this.#matrixTable.querySelector(`tr:nth-child(${i + 2}) > th`).textContent, + cols: [], + }; + for (let j = 0; j < width; j++) { + row.cols.push({ + index: this.#matrixTable.querySelector(`tr:first-child > th:nth-child(${j + 2})`).textContent, + value: Number(this.#matrixTable.querySelector(`tr:nth-child(${i + 2}) > td:nth-child(${j + 2}) > input`).value), + }); + } + rows.push(row); + } + return rows; + }; + + transposeMatrix(rows) { + return rows[0].cols.map((col, i) => { + return { + index: col.index, + rows: rows.map(row => { + return { + index: row.index, + value: row.cols[i].value, + }; + }), + }; + }); + }; + + getLowestPrice(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.min(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + set lowestPrice(lowestPrise) { + const value = `${lowestPrise.index} (${lowestPrise.value})`; + this.#lowestPriceInput.dataset.value = String(value); + this.#lowestPriceInput.value = String(value); + }; + + getHighestPrice(cols, fractionDigits) { + return cols.map(col => col.rows.reduce((row1, row2) => { + return { + index: col.index, + value: Math.max(row1.value, row2.value), + }; + })).reduce((col1, col2) => { + return { + index: col1.value < col2.value ? col1.index : col1.value > col2.value ? col2.index : `${col1.index}, ${col2.index}`, + value: parseFloat(Math.min(col1.value, col2.value).toFixed(fractionDigits)), + }; + }); + }; + + set highestPrice(highestPrice) { + const value = `${highestPrice.index} (${highestPrice.value})`; + this.#highestPriceInput.dataset.value = String(value); + this.#highestPriceInput.value = String(value); + }; + + getSaddlePoint(lowestPrice, highestPrice) { + return lowestPrice.value === highestPrice.value ? lowestPrice.value : null; + }; + + set saddlePoint(value) { + this.#saddlePointInput.dataset.value = String(value); + this.#saddlePointInput.value = String(value); + }; + + showExtra() { + this.#extra.forEach(element => element.classList.remove('hidden')); + }; + + hideExtra() { + this.#extra.forEach(element => element.classList.add('hidden')); + }; + + getDenominator(rows, fractionDigits) { + return parseFloat((rows[0].cols[0].value + rows[1].cols[1].value - rows[1].cols[0].value - rows[0].cols[1].value).toFixed(fractionDigits)); + }; + + getP1(rows, denominator) { + return (rows[1].cols[1].value - rows[1].cols[0].value) / denominator; + }; + + setP1(p1, fractionDigits) { + const value = String(parseFloat(p1.toFixed(fractionDigits))); + this.#p1Input.dataset.value = value; + this.#p1Input.value = value; + }; + + getP2(rows, denominator) { + return (rows[0].cols[0].value - rows[0].cols[1].value) / denominator; + }; + + setP2(p2, fractionDigits) { + const value = String(parseFloat(p2.toFixed(fractionDigits))); + this.#p2Input.dataset.value = value; + this.#p2Input.value = value; + }; + + getQ1(rows, denominator) { + return (rows[1].cols[1].value - rows[0].cols[1].value) / denominator; + }; + + setQ1(q1, fractionDigits) { + const value = String(parseFloat(q1.toFixed(fractionDigits))); + this.#q1Input.dataset.value = value; + this.#q1Input.value = value; + }; + + getQ2(rows, denominator) { + return (rows[0].cols[0].value - rows[1].cols[0].value) / denominator; + }; + + setQ2(q2, fractionDigits) { + const value = String(parseFloat(q2.toFixed(fractionDigits))); + this.#q2Input.dataset.value = value; + this.#q2Input.value = value; + }; + + getV(rows, denominator) { + return (rows[0].cols[0].value * rows[1].cols[1].value - rows[0].cols[1].value * rows[1].cols[0].value) / denominator; + }; + + setV(v, fractionDigits) { + const value = String(parseFloat(v.toFixed(fractionDigits))); + this.#vInput.dataset.value = value; + this.#vInput.value = value; + }; + + getMaxOfMaxCriterion(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.max(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value > row2.value ? row1.index : row1.value < row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.max(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + getMinOfMinCriterion(rows, fractionDigits) { + return rows.map(row => row.cols.reduce((col1, col2) => { + return { + index: row.index, + value: Math.min(col1.value, col2.value), + }; + })).reduce((row1, row2) => { + return { + index: row1.value < row2.value ? row1.index : row1.value > row2.value ? row2.index : `${row1.index}, ${row2.index}`, + value: parseFloat(Math.min(row1.value, row2.value).toFixed(fractionDigits)), + }; + }); + }; + + generateMatrix(randomValues) { + this.#matrixTable.replaceChildren(); + const width = this.getMatrixWidth(); + const height = this.getMatrixHeight(); + const minRandomValue = this.getMinRandomValue(); + const maxRandomValue = this.getMaxRandomValue(); + const tr = document.createElement('tr'); + const th = document.createElement('th'); + tr.appendChild(th); + for (let i = 0; i < width; i++) { + const th = document.createElement('th'); + th.textContent = `B${i + 1}`.subscript(); + tr.appendChild(th); + } + this.#matrixTable.appendChild(tr); + for (let i = 0; i < height; i++) { + const tr = document.createElement('tr'); + const th = document.createElement('th'); + th.textContent = `A${i + 1}`.subscript(); + tr.appendChild(th); + this.#matrixTable.appendChild(tr); + for (let j = 0; j < width; j++) { + const td = document.createElement('td'); + const input = document.createElement('input'); + input.type = 'number'; + input.ariaLabel = `Изменить ячейку A${i + 1}B${j + 1}`; + input.dataset.a = String(i); + input.dataset.b = String(j); + input.addEventListener('keydown', (event) => { + const selectCell = (a, b) => this.#matrixTable.querySelector(`input[type="number"][data-a="${a}"][data-b="${b}"]`).select(); + switch (event.code) { + case 'ArrowUp': + event.preventDefault(); + event.stopPropagation(); + selectCell((Number(input.dataset.a) - 1 + height) % height, input.dataset.b); + break; + + case 'ArrowDown': + event.preventDefault(); + event.stopPropagation(); + selectCell((Number(input.dataset.a) + 1 + height) % height, input.dataset.b); + break; + + case 'ArrowLeft': + event.preventDefault(); + event.stopPropagation(); + selectCell(input.dataset.a, (Number(input.dataset.b) - 1 + width) % width); + break; + + case 'ArrowRight': + event.preventDefault(); + event.stopPropagation(); + selectCell(input.dataset.a, (Number(input.dataset.b) + 1 + width) % width); + break; + } + }); + if (randomValues) input.value = `${Math.randint(minRandomValue, maxRandomValue)}`; + td.appendChild(input); + tr.appendChild(td); + } + } + }; + + solve() { + const rows = this.getRows(); + const cols = this.transposeMatrix(rows); + const fractionDigits = this.getFractionDigits(); + const graphSize = this.getGraphSize(); + const lowestPrice = this.getLowestPrice(rows, fractionDigits); + const highestPrice = this.getHighestPrice(cols, fractionDigits); + const saddlePoint = this.getSaddlePoint(lowestPrice, highestPrice); + const denominator = this.getDenominator(rows, fractionDigits); + const p1 = this.getP1(rows, denominator); + const p2 = this.getP2(rows, denominator); + const q1 = this.getQ1(rows, denominator); + const q2 = this.getQ2(rows, denominator); + const v = this.getV(rows, denominator); + const maxOfMaxCriterion = this.getMaxOfMaxCriterion(rows, fractionDigits); + const minOfMinCriterion = this.getMinOfMinCriterion(rows, fractionDigits); + this.lowestPrice = lowestPrice; + this.highestPrice = highestPrice; + this.saddlePoint = saddlePoint === null ? 'Нет' : saddlePoint; + if (saddlePoint === null) { + this.setP1(p1, fractionDigits); + this.setP2(p2, fractionDigits); + this.setQ1(q1, fractionDigits); + this.setQ2(q2, fractionDigits); + this.setV(v, fractionDigits); + this.#aPlayerGraph.size = graphSize; + this.#aPlayerGraph.min = minOfMinCriterion.value; + this.#aPlayerGraph.max = maxOfMaxCriterion.value; + this.#aPlayerGraph.clear(); + const aPlayerGraphWorkspace = this.#aPlayerGraph.getWorkspace(); + this.#aPlayerGraph.drawLine(aPlayerGraphWorkspace, cols[0].rows[0].value, cols[0].rows[1].value, Graph.blue, 'B\u2081'); + this.#aPlayerGraph.drawLine(aPlayerGraphWorkspace, cols[1].rows[0].value, cols[1].rows[1].value, Graph.red, 'B\u2082'); + this.#aPlayerGraph.drawShadows(aPlayerGraphWorkspace, p1, p2, v, fractionDigits, 'p\u2081', 'p\u2082', 'V'); + this.#bPlayerGraph.size = graphSize; + this.#bPlayerGraph.min = minOfMinCriterion.value; + this.#bPlayerGraph.max = maxOfMaxCriterion.value; + this.#bPlayerGraph.clear(); + const bPlayerGraphWorkspace = this.#bPlayerGraph.getWorkspace(); + this.#bPlayerGraph.drawLine(bPlayerGraphWorkspace, rows[0].cols[0].value, rows[0].cols[1].value, Graph.blue, 'A\u2081'); + this.#bPlayerGraph.drawLine(bPlayerGraphWorkspace, rows[1].cols[0].value, rows[1].cols[1].value, Graph.red, 'A\u2082'); + this.#bPlayerGraph.drawShadows(bPlayerGraphWorkspace, q1, q2, v, fractionDigits, 'q\u2081', 'q\u2082', 'V'); + this.showExtra(); + } else + this.hideExtra(); + }; +} + +class Graph { + static white = '#FFF'; + static black = '#1E1F22'; + static gray = '#575757'; + static blue = '#3574F0'; + static red = '#C94F4F'; + + min = 0; + max = 10; + + #canvas; + #context; + + constructor(canvas) { + this.#canvas = canvas; + this.#context = this.#canvas.getContext('2d'); + } + + set size(value) { + this.#canvas.width = value; + this.#canvas.height = value * 9 / 16; + }; + + getWidth() { + return this.#canvas.width; + }; + + getHeight() { + return this.#canvas.height; + }; + + getPenSize(width) { + return Math.floor(Math.max(width / 500, 1)); + }; + + getScalePitch(width) { + return width / 100; + }; + + getPadding(scalePitch) { + return scalePitch * 5; + }; + + getWorkspace() { + const canvasWidth = this.getWidth(); + const canvasHeight = this.getHeight(); + const scalePitch = this.getScalePitch(canvasWidth); + const padding = this.getPadding(scalePitch); + const bottomPadding = scalePitch * 3; + const x1 = padding + scalePitch; + const y1 = padding + scalePitch * 5; + const width = canvasWidth - padding * 2 - scalePitch * 6; + const height = canvasHeight - padding * 2 - scalePitch * 6; + const x2 = x1 + width; + const y2 = y1 + height; + return { + x1: x1, + y1: y2, + x2: x2, + y2: y2, + width: width, + height: height, + scalePitch: scalePitch, + bottomPadding: bottomPadding, + }; + }; + + clear() { + const width = this.getWidth(); + const height = this.getHeight(); + const scalePitch = this.getScalePitch(width); + const penSize = this.getPenSize(width); + const padding = this.getPadding(scalePitch); + + this.#context.fillStyle = Graph.white; + this.#context.beginPath(); + this.#context.rect(0, 0, width, height); + this.#context.fill(); + + this.drawAxes(width, height, padding, scalePitch, penSize); + }; + + drawLine(workspace, y1, y2, color, title) { + const getHeight = (value) => (workspace.height - workspace.bottomPadding) * (value - this.min) / (this.max - this.min) + workspace.bottomPadding; + + this.#context.beginPath(); + this.#context.strokeStyle = color; + + y1 = workspace.y2 - getHeight(y1); + y2 = workspace.y2 - getHeight(y2); + + this.#context.moveTo(workspace.x1, y1); + this.#context.lineTo(workspace.x2, y2); + + this.#context.stroke(); + + this.#context.beginPath(); + this.#context.fillStyle = color; + this.#context.arc(workspace.x1, y1, workspace.scalePitch * .5, 0, 2 * Math.PI); + this.#context.arc(workspace.x2, y2, workspace.scalePitch * .5, 0, 2 * Math.PI); + this.#context.fill(); + + this.#context.beginPath(); + this.#context.fillStyle = Graph.black; + this.#context.font = `${workspace.scalePitch * .15}rem "Exo 2", sans-serif`; + this.#context.textAlign = 'end'; + this.#context.fillText(title, workspace.x1 - workspace.scalePitch * .75, y1 + workspace.scalePitch * .8); + + this.#context.textAlign = 'start'; + this.#context.fillText(`${title}'`, workspace.x2 + workspace.scalePitch * .75, y2 + workspace.scalePitch * .8); + + this.#context.stroke(); + }; + + drawShadows(workspace, p1, p2, v, fractionDigits, p1Text, p2Text, vText) { + const getHeight = (value) => (workspace.height - workspace.bottomPadding) * (value - this.min) / (this.max - this.min) + workspace.bottomPadding; + + this.#context.beginPath(); + this.#context.strokeStyle = Graph.gray; + this.#context.setLineDash([workspace.scalePitch * .75, workspace.scalePitch * .75]); + + const a = workspace.x1 + workspace.width * p2; + const b = workspace.y2 - getHeight(v); + + this.#context.moveTo(a, b); + this.#context.lineTo(a, workspace.y2); + this.#context.moveTo(workspace.x1, b); + this.#context.lineTo(a, b); + this.#context.stroke(); + + this.#context.fillStyle = Graph.gray; + + this.#context.beginPath(); + this.#context.arc(a, b, workspace.scalePitch * .5, 0, 2 * Math.PI); + this.#context.fill(); + + this.#context.beginPath(); + this.#context.arc(a, workspace.y2, workspace.scalePitch * .5, 0, 2 * Math.PI); + this.#context.fill(); + + this.#context.beginPath(); + this.#context.arc(workspace.x1, b, workspace.scalePitch * .5, 0, 2 * Math.PI); + this.#context.fill(); + + this.#context.fillStyle = Graph.black; + this.#context.font = `${workspace.scalePitch * .1}rem "Exo 2", sans-serif`; + + this.#context.beginPath(); + this.#context.textAlign = 'end'; + this.#context.fillText(String(parseFloat(v.toFixed(fractionDigits))), workspace.x1 - workspace.scalePitch * .75, b + workspace.scalePitch * .5); + this.#context.textAlign = 'center'; + this.#context.fillText(String(parseFloat(p2.toFixed(fractionDigits))), a, workspace.y2 + workspace.scalePitch * 2); + + this.#context.font = `${workspace.scalePitch * .15}rem "Exo 2", sans-serif`; + this.#context.textAlign = 'center'; + this.#context.fillText(vText, a, b - workspace.scalePitch); + this.#context.fillText(p1Text, a + (workspace.width * p1) * .5, workspace.y2 + workspace.scalePitch * 2); + this.#context.fillText(p2Text, workspace.x1 + (workspace.width * p2) * .5, workspace.y2 + workspace.scalePitch * 2); + + this.#context.stroke(); + }; + + drawAxes(width, height, padding, scalePitch, penSize) { + this.#context.strokeStyle = Graph.black; + this.#context.fillStyle = Graph.black; + this.#context.lineWidth = penSize; + + const a = padding + scalePitch; + const b = width - padding; + const c = height - padding; + const d = padding * .7; + const e = height - d; + const f = width - d; + + this.#context.beginPath(); + this.#context.moveTo(a, d + scalePitch * 2.5); + this.#context.lineTo(a, e); + this.#context.stroke(); + + this.#context.beginPath(); + this.#context.moveTo(a, d); + this.#context.lineTo(padding + scalePitch * .25, d + scalePitch * 3); + this.#context.lineTo(padding + scalePitch, d + scalePitch * 2.5); + this.#context.lineTo(padding + scalePitch * 1.75, d + scalePitch * 3); + this.#context.fill(); + + this.#context.beginPath(); + this.#context.font = `${scalePitch * .15}rem "Exo 2", sans-serif`; + this.#context.textAlign = 'start'; + this.#context.fillText('y\u2081', padding + scalePitch * 2.25, d + scalePitch * 2); + + this.#context.beginPath(); + this.#context.moveTo(b - scalePitch * 5, d + scalePitch * 2.5); + this.#context.lineTo(b - scalePitch * 5, e); + this.#context.stroke(); + + this.#context.beginPath(); + this.#context.moveTo(b - scalePitch * 5, d); + this.#context.lineTo(b - scalePitch * 5.75, d + scalePitch * 3); + this.#context.lineTo(b - scalePitch * 5, d + scalePitch * 2.5); + this.#context.lineTo(b - scalePitch * 4.25, d + scalePitch * 3); + this.#context.fill(); + + this.#context.beginPath(); + this.#context.font = `${scalePitch * .15}rem "Exo 2", sans-serif`; + this.#context.textAlign = 'start'; + this.#context.fillText('y\u2082', b - scalePitch * 3.75, d + scalePitch * 2); + + this.#context.beginPath(); + this.#context.moveTo(d, c - scalePitch); + this.#context.lineTo(f - scalePitch * 2.5, c - scalePitch); + this.#context.stroke(); + + this.#context.beginPath(); + this.#context.moveTo(f, c - scalePitch); + this.#context.lineTo(f - scalePitch * 3, c - scalePitch * .25); + this.#context.lineTo(f - scalePitch * 2.5, c - scalePitch); + this.#context.lineTo(f - scalePitch * 3, c - scalePitch * 1.75); + this.#context.fill(); + + this.#context.beginPath(); + this.#context.font = `${scalePitch * .15}rem "Exo 2", sans-serif`; + this.#context.textAlign = 'center'; + this.#context.fillText('x', f - scalePitch * 1.5, c - scalePitch + scalePitch * 2.25); + }; +} + +class Application { + static DEFAULT_FRAGMENT = '1'; + + #inputContainer; + #outputContainer; + #solveButton; + #menu; + #bottomBar; + #fragmentId; + #fragments = { + '1': new Fragment1(), + '2': new Fragment2(), + }; + + constructor() { + this.#inputContainer = document.querySelector('#input'); + this.#outputContainer = document.querySelector('#output'); + this.#solveButton = document.querySelector('#solve'); + this.#menu = new Menu(this); + this.#bottomBar = new BottomBar(); + + this.#solveButton.addEventListener('click', () => { + this.#fragments[this.#fragmentId].solve(); + document.querySelector(`#output [data-fragment="${this.#fragmentId}"]`).classList.add('active'); + }); + }; + + open(fragmentId) { + this.#fragmentId = fragmentId; + document.querySelectorAll('#input [data-fragment]').forEach(fragment => fragment.classList.remove('active')); + document.querySelector(`#input [data-fragment="${fragmentId}"]`).classList.add('active'); + document.querySelectorAll('#output [data-fragment]').forEach(fragment => fragment.classList.remove('active')); + this.#menu.setActive(fragmentId); + }; +} diff --git a/static/styles/main.css b/static/styles/main.css new file mode 100644 index 0000000..26be35c --- /dev/null +++ b/static/styles/main.css @@ -0,0 +1,306 @@ +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('/static/fonts/cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('/static/fonts/cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('/static/fonts/vietnamese.woff2') format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('/static/fonts/latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('/static/fonts/latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --color-1: #2E436E; + --color-2: #1E1F22; + --color-3: #2B2D30; + --color-4: #43454A; + --color-5: #575757; + --color-6: #CED0D6; + --color-7: #569ACC; + --color-8: #3574F0; + --color-9: #528D58; + --color-10: #C94F4F; +} + +html, +input, +button, +textarea, +select { + font-family: 'Exo 2', sans-serif; +} + +html, +body { + height: 100%; + color: var(--color-6); + background-color: var(--color-2); + color-scheme: dark light; +} + +body { + display: flex; + flex-direction: column; + margin: 0; +} + +input[type="text"], +input[type="number"] { + color: var(--color-6); +} + +a { + color: var(--color-7); + text-decoration: none; + outline: none; +} + +a:hover, +a:focus { + text-decoration: underline; +} + +.hidden { + display: none; +} + +#top-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: .25rem .5rem; + background-color: var(--color-3); + border-bottom: solid 2px var(--color-2); +} + +#logo { + display: flex; + align-items: center; +} + +#icon { + width: 1.5rem; + height: 1.5rem; +} + +#title { + margin-left: .5rem; + font-size: 1.2rem; + font-weight: bold; +} + +#actions { + display: flex; + align-items: center; +} + +#actions button { + display: flex; + justify-content: center; + align-items: center; + padding: .4rem .6rem; + color: var(--color-6); + background: none; + border: none; + border-radius: .25rem; + outline: none; + font-size: 1rem; + cursor: pointer; +} + +#actions button:hover, +#actions button:focus { + background: var(--color-4); +} + +#actions .icon { + width: 1.2rem; + height: 1.2rem; +} + +#content { + position: sticky; + display: flex; + height: 100%; + overflow-y: auto; +} + +#menu { + display: flex; + flex-direction: column; + justify-content: stretch; + max-width: 16rem; + background-color: var(--color-3); +} + +#menu .header { + padding: .75rem; + font-weight: bold; +} + +#menu .item { + display: flex; + align-items: stretch; +} + +#menu input[type="button"] { + padding: .5rem .75rem; + width: 100%; + color: var(--color-6); + background: none; + border: none; + outline: none; + white-space: pre-wrap; + text-align: start; + cursor: pointer; +} + +#menu input[type="button"]:hover, +#menu input[type="button"]:focus { + color: white; +} + +#menu input[type="button"].active { + color: white; +} + +#menu .marker { + width: 3px; + background: none; + border-radius: 3px; +} + +#menu input[type="button"].active + .marker { + background: var(--color-8); +} + +#main { + position: sticky; + display: flex; + width: 100%; + overflow: auto; +} + +#input { + flex: 1 1 auto; + padding: .75rem; + min-width: 24rem; + overflow: auto; +} + +#input [data-fragment]:not(.active), +#output [data-fragment]:not(.active) { + display: none; +} + +#main input { + width: 10rem; + background: none; + border: none; + outline: solid 1px var(--color-5); +} + +#main input:focus { + outline: solid 2px var(--color-8); +} + +#main input[type="button"] { + padding: .25rem 1.5rem; + width: 100%; + color: white; + border-radius: .25rem; + outline: none; + cursor: pointer; +} + +#main input[type="button"].primary { + padding: .25rem 1.5rem; + background: var(--color-8); + border: solid 1px var(--color-2); +} + +#main input[type="button"].secondary { + padding: .25rem 1.5rem; + background: none; + border: solid 1px var(--color-4); +} + +#main input[type="button"]:focus { + outline: solid 2px var(--color-8); +} + +#output { + flex: 1 1 auto; + padding: .75rem; + border-left: solid 1px var(--color-3); + overflow: auto; +} + +#bottom-bar { + display: flex; + justify-content: end; + align-items: center; + padding: .5rem; + background-color: var(--color-3); + border-top: solid .15rem var(--color-2); + font-size: .8rem; +} + +table.properties th { + font-weight: normal; + text-align: start; +} + +table.properties th[colspan="2"] { + padding: 1rem 0 .5rem 0; + font-weight: bold; +} + +table.properties td:nth-child(2) { + padding-left: 1rem; +} + +table.properties td[colspan="2"] { + padding: .5rem 0 .5rem 0; +} + +table.properties td[colspan="2"] > div { + display: flex; +} + +table.properties td[colspan="2"] > div > :not(:first-child) { + margin-left: .5rem; +} + +table.matrix input { + width: 3rem !important; +} diff --git a/templates/main.jinja2 b/templates/main.jinja2 new file mode 100644 index 0000000..872eec3 --- /dev/null +++ b/templates/main.jinja2 @@ -0,0 +1,364 @@ + + + + + + + + + + {{ title }} + + + + + + + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Размер матрицы
+ + + +
+ + + +
+ +
Заполнить случайными значениями
+ + + +
+ + + +
+
+ + +
+
Прочее
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Заполнить случайными значениями
+ + + +
+ + + +
+
+ + +
+
Прочее
+ + + +
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Седловая точка
+ + + +
+ + + +
+ + + +
Критерии
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
Матрица сожалений
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Седловая точка
+ + + +
+ + + +
+ + + +
Вероятности стратегий и цена игры
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + + + + + + + + + + +
График стратегий игрока A
+ +
График стратегий игрока B
+ +
+
+
+
+
+ + +