233 lines
6.7 KiB
JavaScript
233 lines
6.7 KiB
JavaScript
import { resetState } from './state.js';
|
|
import { computeHomography, toMatrix3dCSS } from './homography.js';
|
|
import { buildCornerInputs, syncCornerInputs, readCornerInputs, syncFieldInputs, syncWidgetInputs } from './inputs.js';
|
|
import { updateLivePreview } from './preview.js';
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
let fieldEditing = false;
|
|
let fieldEditW, fieldEditH;
|
|
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, state) {
|
|
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');
|
|
}
|
|
}
|
|
|
|
function setMode(mode) {
|
|
document.body.className = mode === 'preview' ? 'mode-preview' : 'mode-edit';
|
|
$('btnEdit').classList.toggle('active', mode === 'edit');
|
|
$('btnPreview').classList.toggle('active', mode === 'preview');
|
|
$('modeHint').textContent = mode === 'edit'
|
|
? 'Drag corners on the field'
|
|
: 'matrix3d() transform result';
|
|
}
|
|
|
|
let updateAllRef;
|
|
|
|
function loadBgFile(file, state) {
|
|
if (!file || !file.type.startsWith('image/')) return;
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
state.bgImage = reader.result;
|
|
$('btnBgRemove').style.display = '';
|
|
updateAllRef();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function clearBg(state) {
|
|
state.bgImage = null;
|
|
$('btnBgRemove').style.display = 'none';
|
|
updateAllRef();
|
|
}
|
|
|
|
export function updateCSSOutput(state) {
|
|
const H = computeHomography(state);
|
|
$('cssOutput').value = toMatrix3dCSS(H);
|
|
}
|
|
|
|
export function initUI(state, updateAll) {
|
|
updateAllRef = updateAll;
|
|
|
|
$('chainLink').addEventListener('click', () => {
|
|
keepAspect = !keepAspect;
|
|
updateChainIcon();
|
|
});
|
|
|
|
$('btnEditField').addEventListener('click', () => {
|
|
setFieldEditing(true, state);
|
|
$('inpFieldW').focus();
|
|
});
|
|
|
|
$('btnCancelField').addEventListener('click', () => {
|
|
setFieldEditing(false, state);
|
|
});
|
|
|
|
$('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', () => {
|
|
if (!fieldEditing) return;
|
|
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, state);
|
|
buildCornerInputs(state);
|
|
updateAll();
|
|
});
|
|
|
|
$('inpWidgetW').addEventListener('input', () => {
|
|
const v = parseInt($('inpWidgetW').value);
|
|
if (v > 0) { state.widgetW = v; updateAll(); }
|
|
});
|
|
|
|
$('inpWidgetH').addEventListener('input', () => {
|
|
const v = parseInt($('inpWidgetH').value);
|
|
if (v > 0) { state.widgetH = v; updateAll(); }
|
|
});
|
|
|
|
document.addEventListener('input', (e) => {
|
|
if (e.target.id && /^c\d[xy]$/.test(e.target.id)) {
|
|
readCornerInputs(state);
|
|
updateAll();
|
|
}
|
|
});
|
|
|
|
$('copyBtn').addEventListener('click', () => {
|
|
const text = $('cssOutput').value;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const btn = $('copyBtn');
|
|
const orig = btn.textContent;
|
|
btn.textContent = 'Copied!';
|
|
setTimeout(() => btn.textContent = orig, 1500);
|
|
});
|
|
});
|
|
|
|
$('resetBtn').addEventListener('click', () => {
|
|
setFieldEditing(false, state);
|
|
resetState();
|
|
syncFieldInputs(state, state.fieldW, state.fieldH);
|
|
syncWidgetInputs(state);
|
|
buildCornerInputs(state);
|
|
$('btnBgRemove').style.display = 'none';
|
|
$('bgObjectFit').value = state.objectFit;
|
|
updateAll();
|
|
});
|
|
|
|
$('btnEdit').addEventListener('click', () => {
|
|
setMode('edit');
|
|
requestAnimationFrame(() => updateLivePreview(state));
|
|
});
|
|
|
|
$('btnPreview').addEventListener('click', () => {
|
|
setMode('preview');
|
|
requestAnimationFrame(() => updateLivePreview(state));
|
|
});
|
|
|
|
window.addEventListener('resize', () => updateLivePreview(state));
|
|
|
|
$('btnBgLoad').addEventListener('click', () => $('bgFileInput').click());
|
|
|
|
$('bgFileInput').addEventListener('change', (e) => {
|
|
if (e.target.files[0]) loadBgFile(e.target.files[0], state);
|
|
e.target.value = '';
|
|
});
|
|
|
|
$('btnBgRemove').addEventListener('click', () => clearBg(state));
|
|
|
|
$('bgObjectFit').addEventListener('change', (e) => {
|
|
state.objectFit = e.target.value;
|
|
updateAll();
|
|
});
|
|
|
|
document.addEventListener('paste', (e) => {
|
|
const tag = document.activeElement?.tagName;
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
const items = e.clipboardData?.items;
|
|
if (!items) return;
|
|
for (const item of items) {
|
|
if (item.type.startsWith('image/')) {
|
|
const file = item.getAsFile();
|
|
if (file) loadBgFile(file, state);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
const lc = document.querySelector('.left-col');
|
|
lc.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
lc.classList.add('drag-over');
|
|
});
|
|
lc.addEventListener('dragleave', () => {
|
|
lc.classList.remove('drag-over');
|
|
});
|
|
lc.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
lc.classList.remove('drag-over');
|
|
if (e.dataTransfer.files[0]) loadBgFile(e.dataTransfer.files[0], state);
|
|
});
|
|
}
|