feat: recalc coordinates flow
This commit is contained in:
+28
-4
@@ -37,10 +37,34 @@
|
|||||||
|
|
||||||
<div class="right-col">
|
<div class="right-col">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Основное поле</h2>
|
<div class="panel-header">
|
||||||
<div class="field-row">
|
<h2>Основное поле</h2>
|
||||||
<label><span>W</span><input id="inpFieldW" type="number" value="1920" min="1" /></label>
|
<button class="btn btn-edit-field" id="btnEditField">Редактировать</button>
|
||||||
<label><span>H</span><input id="inpFieldH" type="number" value="1080" min="1" /></label>
|
</div>
|
||||||
|
<div class="field-size-stack">
|
||||||
|
<div class="chain-link" id="chainLink" title="Keep aspect ratio">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path class="chain-locked" d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path class="chain-locked" d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
<path class="chain-unlocked" d="M13.5 10.5l3-3" style="display:none" />
|
||||||
|
<path class="chain-unlocked" d="M10.5 13.5l-3 3" style="display:none" />
|
||||||
|
<path class="chain-unlocked" d="M7.5 7.5l9 9" style="display:none" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="field-stack-inputs">
|
||||||
|
<label><span>W</span><input id="inpFieldW" type="number" value="1920" min="1" disabled /></label>
|
||||||
|
<label><span>H</span><input id="inpFieldH" type="number" value="1080" min="1" disabled /></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field-edit-options" id="fieldEditOptions">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="chkRescaleCoords" checked />
|
||||||
|
<span>Пересчитать координаты</span>
|
||||||
|
</label>
|
||||||
|
<div class="field-save-row">
|
||||||
|
<button class="btn btn-cancel-field" id="btnCancelField">Отменить</button>
|
||||||
|
<button class="btn btn-save-field" id="btnSaveField">Сохранить</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -286,14 +286,103 @@
|
|||||||
$('cssOutput').value = toMatrix3dCSS(H);
|
$('cssOutput').value = toMatrix3dCSS(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Input events =====
|
// ===== Field editing =====
|
||||||
$('inpFieldW').addEventListener('input', () => {
|
let fieldEditing = false;
|
||||||
const v = parseInt($('inpFieldW').value);
|
let fieldEditW = state.fieldW;
|
||||||
if (v > 0) { state.fieldW = v; updateAll(); }
|
let fieldEditH = state.fieldH;
|
||||||
|
let keepAspect = true;
|
||||||
|
|
||||||
|
function updateChainIcon() {
|
||||||
|
const locked = $('chainLink').querySelectorAll('.chain-locked');
|
||||||
|
const unlocked = $('chainLink').querySelectorAll('.chain-unlocked');
|
||||||
|
locked.forEach(el => el.style.display = keepAspect ? '' : 'none');
|
||||||
|
unlocked.forEach(el => el.style.display = keepAspect ? 'none' : '');
|
||||||
|
$('chainLink').classList.toggle('unlocked', !keepAspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFieldEditing(editing) {
|
||||||
|
fieldEditing = editing;
|
||||||
|
const inpW = $('inpFieldW');
|
||||||
|
const inpH = $('inpFieldH');
|
||||||
|
const options = $('fieldEditOptions');
|
||||||
|
const btnEdit = $('btnEditField');
|
||||||
|
const chain = $('chainLink');
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
fieldEditW = state.fieldW;
|
||||||
|
fieldEditH = state.fieldH;
|
||||||
|
inpW.disabled = false;
|
||||||
|
inpH.disabled = false;
|
||||||
|
options.classList.add('visible');
|
||||||
|
btnEdit.style.display = 'none';
|
||||||
|
chain.classList.add('visible');
|
||||||
|
updateChainIcon();
|
||||||
|
} else {
|
||||||
|
inpW.value = state.fieldW;
|
||||||
|
inpW.disabled = true;
|
||||||
|
inpH.value = state.fieldH;
|
||||||
|
inpH.disabled = true;
|
||||||
|
options.classList.remove('visible');
|
||||||
|
btnEdit.style.display = '';
|
||||||
|
chain.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('chainLink').addEventListener('click', () => {
|
||||||
|
keepAspect = !keepAspect;
|
||||||
|
updateChainIcon();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('btnEditField').addEventListener('click', () => {
|
||||||
|
setFieldEditing(true);
|
||||||
|
$('inpFieldW').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btnCancelField').addEventListener('click', () => {
|
||||||
|
setFieldEditing(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('inpFieldW').addEventListener('input', () => {
|
||||||
|
if (!fieldEditing) return;
|
||||||
|
const newW = parseInt($('inpFieldW').value);
|
||||||
|
if (newW <= 0) return;
|
||||||
|
if (keepAspect) {
|
||||||
|
fieldEditH = Math.round(newW * state.fieldH / state.fieldW);
|
||||||
|
$('inpFieldH').value = fieldEditH;
|
||||||
|
}
|
||||||
|
fieldEditW = newW;
|
||||||
|
});
|
||||||
|
|
||||||
$('inpFieldH').addEventListener('input', () => {
|
$('inpFieldH').addEventListener('input', () => {
|
||||||
const v = parseInt($('inpFieldH').value);
|
if (!fieldEditing) return;
|
||||||
if (v > 0) { state.fieldH = v; updateAll(); }
|
const newH = parseInt($('inpFieldH').value);
|
||||||
|
if (newH <= 0) return;
|
||||||
|
if (keepAspect) {
|
||||||
|
fieldEditW = Math.round(newH * state.fieldW / state.fieldH);
|
||||||
|
$('inpFieldW').value = fieldEditW;
|
||||||
|
}
|
||||||
|
fieldEditH = newH;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btnSaveField').addEventListener('click', () => {
|
||||||
|
const newW = parseInt($('inpFieldW').value);
|
||||||
|
const newH = parseInt($('inpFieldH').value);
|
||||||
|
if (newW <= 0 || newH <= 0) return;
|
||||||
|
|
||||||
|
if ($('chkRescaleCoords').checked && (newW !== state.fieldW || newH !== state.fieldH)) {
|
||||||
|
const sx = newW / state.fieldW;
|
||||||
|
const sy = newH / state.fieldH;
|
||||||
|
state.corners.forEach(c => {
|
||||||
|
c.x = Math.round(c.x * sx);
|
||||||
|
c.y = Math.round(c.y * sy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.fieldW = newW;
|
||||||
|
state.fieldH = newH;
|
||||||
|
setFieldEditing(false);
|
||||||
|
buildCornerInputs();
|
||||||
|
updateAll();
|
||||||
});
|
});
|
||||||
$('inpWidgetW').addEventListener('input', () => {
|
$('inpWidgetW').addEventListener('input', () => {
|
||||||
const v = parseInt($('inpWidgetW').value);
|
const v = parseInt($('inpWidgetW').value);
|
||||||
@@ -324,6 +413,7 @@
|
|||||||
|
|
||||||
// ===== Reset =====
|
// ===== Reset =====
|
||||||
$('resetBtn').addEventListener('click', () => {
|
$('resetBtn').addEventListener('click', () => {
|
||||||
|
setFieldEditing(false);
|
||||||
state.fieldW = defaultState.fieldW;
|
state.fieldW = defaultState.fieldW;
|
||||||
state.fieldH = defaultState.fieldH;
|
state.fieldH = defaultState.fieldH;
|
||||||
state.widgetW = defaultState.widgetW;
|
state.widgetW = defaultState.widgetW;
|
||||||
|
|||||||
@@ -107,6 +107,16 @@ main {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.panel-header h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.left-col {
|
.left-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -275,6 +285,145 @@ main {
|
|||||||
background: #484f58;
|
background: #484f58;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-edit-field {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-edit-field:hover {
|
||||||
|
background: rgba(88, 166, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save-field {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--btn-bg);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.btn-save-field:hover {
|
||||||
|
background: var(--btn-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-field {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.btn-cancel-field:hover {
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-size-stack {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-link {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
min-height: 56px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.chain-link:hover {
|
||||||
|
background: rgba(88, 166, 255, 0.1);
|
||||||
|
}
|
||||||
|
.chain-link.unlocked {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
.chain-link.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-stack-inputs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.field-stack-inputs label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
.field-stack-inputs label span {
|
||||||
|
min-width: 14px;
|
||||||
|
}
|
||||||
|
.field-stack-inputs input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--heading);
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: monospace;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.field-stack-inputs input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.field-stack-inputs input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-edit-options {
|
||||||
|
display: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.field-edit-options.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-save-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
#fieldContainer {
|
#fieldContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user