refactor: update js - separate files, modules

This commit is contained in:
2026-06-24 08:11:22 +05:00
parent 99ebaceda3
commit 1f9ac90ce2
14 changed files with 270 additions and 316 deletions
+15
View File
@@ -0,0 +1,15 @@
export const COLORS = {
fieldBg: '#0a0e14',
accent: '#58a6ff',
widgetFill: 'rgba(88,166,255,0.12)',
widgetStroke: 'rgba(88,166,255,0.4)',
polygonFill: 'rgba(88,166,255,0.1)',
gridLine: 'rgba(255,255,255,0.06)'
};
export const LABELS = ['TL', 'TR', 'BR', 'BL'];
export const FIELD_PADDING = 140;
export const ROUND_DIGITS = 6;
export const round = v => parseFloat(v.toFixed(ROUND_DIGITS));
+1
View File
@@ -0,0 +1 @@
export const $ = id => document.getElementById(id);
+40
View File
@@ -0,0 +1,40 @@
import { $ } from './dom.js';
import { loadBgFile, loadWidgetFile } from './file-loader.js';
function setupDropZone(el, onDrop) {
el.addEventListener('dragover', (e) => {
e.preventDefault();
el.classList.add('drag-over');
});
el.addEventListener('dragleave', (e) => {
if (!el.contains(e.relatedTarget)) {
el.classList.remove('drag-over');
}
});
el.addEventListener('drop', (e) => {
e.preventDefault();
document.body.classList.remove('file-dragging');
el.classList.remove('drag-over');
if (e.dataTransfer.files[0]) onDrop(e.dataTransfer.files[0]);
});
}
export function initDropZones(state) {
const lc = document.querySelector('.left-col');
document.addEventListener('dragover', (e) => {
e.preventDefault();
document.body.classList.add('file-dragging');
});
document.addEventListener('drop', () => {
document.body.classList.remove('file-dragging');
lc.classList.remove('drag-over');
});
setupDropZone(lc, (file) => loadBgFile(file, state));
setupDropZone($('bgPanel'), (file) => loadBgFile(file, state));
setupDropZone($('widgetPanel'), (file) => loadWidgetFile(file, state));
}
+49
View File
@@ -0,0 +1,49 @@
import { $ } from './dom.js';
let updateAllRef;
export function setUpdateAllRef(fn) {
updateAllRef = fn;
}
export 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);
}
export function clearBg(state) {
state.bgImage = null;
$('btnBgRemove').style.display = 'none';
updateAllRef();
}
export function setWidgetImageLock(locked) {
$('inpWidgetW').disabled = locked;
$('inpWidgetH').disabled = locked;
$('btnWidgetRemove').style.display = locked ? '' : 'none';
}
export function loadWidgetFile(file, state) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = () => {
state.widgetImage = reader.result;
const img = new Image();
img.onload = () => {
state.widgetW = img.naturalWidth;
state.widgetH = img.naturalHeight;
$('inpWidgetW').value = state.widgetW;
$('inpWidgetH').value = state.widgetH;
setWidgetImageLock(true);
updateAllRef();
};
img.src = reader.result;
};
reader.readAsDataURL(file);
}
+5 -4
View File
@@ -1,3 +1,5 @@
import { round } from './constants.js';
export function computeHomography(state) {
const w = state.widgetW, h = state.widgetH;
if (w <= 0 || h <= 0) return null;
@@ -29,14 +31,13 @@ export function computeHomography(state) {
export function toMatrix3dCSS(H) {
if (!H) return '/* degenerate configuration */';
const n = v => parseFloat(v.toFixed(8));
return [
'transform-origin: 0 0;',
'transform: matrix3d(',
` ${n(H.h00)}, ${n(H.h10)}, 0, ${n(H.h20)},`,
` ${n(H.h01)}, ${n(H.h11)}, 0, ${n(H.h21)},`,
` ${round(H.h00)}, ${round(H.h10)}, 0, ${round(H.h20)},`,
` ${round(H.h01)}, ${round(H.h11)}, 0, ${round(H.h21)},`,
` 0, 0, 1, 0,`,
` ${n(H.h02)}, ${n(H.h12)}, 0, ${n(H.h22)}`,
` ${round(H.h02)}, ${round(H.h12)}, 0, ${round(H.h22)}`,
');'
].join('\n');
}
+10 -5
View File
@@ -1,6 +1,5 @@
const LABELS = ['TL', 'TR', 'BR', 'BL'];
function $(id) { return document.getElementById(id); }
import { $ } from './dom.js';
import { LABELS } from './constants.js';
export function buildCornerInputs(state) {
const container = $('cornerInputs');
@@ -28,8 +27,14 @@ export function syncCornerInputs(state) {
export function readCornerInputs(state) {
state.corners.forEach((c, i) => {
const x = parseInt($(`c${i}x`).value);
const y = parseInt($(`c${i}y`).value);
const xi = $(`c${i}x`);
const yi = $(`c${i}y`);
const x = parseInt(xi.value);
const y = parseInt(yi.value);
xi.classList.toggle('input-error', isNaN(x));
yi.classList.toggle('input-error', isNaN(y));
if (!isNaN(x)) c.x = x;
if (!isNaN(y)) c.y = y;
});
+79
View File
@@ -0,0 +1,79 @@
import { $ } from './dom.js';
let fieldEditing = false;
let fieldEditW, fieldEditH;
let keepAspect = true;
export function isFieldEditing() {
return fieldEditing;
}
export function getFieldEditW() {
return fieldEditW;
}
export function getFieldEditH() {
return fieldEditH;
}
export function setFieldEditW(v) {
fieldEditW = v;
}
export function setFieldEditH(v) {
fieldEditH = v;
}
export function isKeepAspect() {
return keepAspect;
}
export function toggleKeepAspect() {
keepAspect = !keepAspect;
updateChainIcon();
}
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);
}
export 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');
}
}
export 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';
}
+4 -5
View File
@@ -1,6 +1,6 @@
import { $ } from './dom.js';
import { computeHomography } from './homography.js';
function $(id) { return document.getElementById(id); }
import { round, FIELD_PADDING } from './constants.js';
function applyBgToField(field, state) {
if (state.bgImage) {
@@ -33,7 +33,7 @@ export function updateLivePreview(state) {
const cw = container.clientWidth;
if (cw <= 0) return;
const maxH = window.innerHeight - 140;
const maxH = window.innerHeight - FIELD_PADDING;
const scaleW = cw / state.fieldW;
const scaleH = maxH / state.fieldH;
const scale = Math.min(scaleW, scaleH);
@@ -54,8 +54,7 @@ export function updateLivePreview(state) {
const H = computeHomography(state);
if (H) {
const n = v => parseFloat(v.toFixed(6));
widget.style.transform = `matrix3d(${n(H.h00)},${n(H.h10)},0,${n(H.h20)},${n(H.h01)},${n(H.h11)},0,${n(H.h21)},0,0,1,0,${n(H.h02)},${n(H.h12)},0,${n(H.h22)})`;
widget.style.transform = `matrix3d(${round(H.h00)},${round(H.h10)},0,${round(H.h20)},${round(H.h01)},${round(H.h11)},0,${round(H.h21)},0,0,1,0,${round(H.h02)},${round(H.h12)},0,${round(H.h22)})`;
widget.style.opacity = '1';
} else {
widget.style.transform = 'none';
+7 -23
View File
@@ -12,29 +12,13 @@ export const defaultState = {
widgetImage: null
};
export const state = {
fieldW: 1920, fieldH: 1080,
widgetW: 400, widgetH: 300,
corners: [
{ x: 300, y: 200 },
{ x: 1620, y: 150 },
{ x: 1580, y: 920 },
{ x: 340, y: 950 }
],
bgImage: null,
objectFit: 'contain',
widgetImage: null
};
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
export const state = deepClone(defaultState);
export function resetState() {
state.fieldW = defaultState.fieldW;
state.fieldH = defaultState.fieldH;
state.widgetW = defaultState.widgetW;
state.widgetH = defaultState.widgetH;
state.bgImage = null;
state.objectFit = defaultState.objectFit;
state.widgetImage = null;
defaultState.corners.forEach((c, i) => {
state.corners[i] = { x: c.x, y: c.y };
});
const clone = deepClone(defaultState);
Object.assign(state, clone);
}
+34 -14
View File
@@ -1,4 +1,5 @@
const LABELS = ['TL', 'TR', 'BR', 'BL'];
import { COLORS, LABELS } from './constants.js';
const ns = 'http://www.w3.org/2000/svg';
const OBJECT_FIT_MAP = {
@@ -22,15 +23,12 @@ function setAttrs(el, attrs) {
}
}
export function renderSVG(svg, state) {
svg.setAttribute('viewBox', `0 0 ${state.fieldW} ${state.fieldH}`);
svg.innerHTML = '';
function renderBackground(svg, state) {
const rect = document.createElementNS(ns, 'rect');
setAttrs(rect, {
width: state.fieldW,
height: state.fieldH,
fill: '#0a0e14'
fill: COLORS.fieldBg
});
svg.appendChild(rect);
@@ -44,21 +42,28 @@ export function renderSVG(svg, state) {
});
svg.appendChild(img);
}
}
function renderGrid(svg, state) {
const step = gridInterval(Math.max(state.fieldW, state.fieldH));
const gridG = document.createElementNS(ns, 'g');
for (let x = step; x < state.fieldW; x += step) {
const line = document.createElementNS(ns, 'line');
setAttrs(line, { x1: x, y1: 0, x2: x, y2: state.fieldH, stroke: 'rgba(255,255,255,0.06)', 'stroke-width': 1 });
setAttrs(line, { x1: x, y1: 0, x2: x, y2: state.fieldH, stroke: COLORS.gridLine, 'stroke-width': 1 });
gridG.appendChild(line);
}
for (let y = step; y < state.fieldH; y += step) {
const line = document.createElementNS(ns, 'line');
setAttrs(line, { x1: 0, y1: y, x2: state.fieldW, y2: y, stroke: 'rgba(255,255,255,0.06)', 'stroke-width': 1 });
setAttrs(line, { x1: 0, y1: y, x2: state.fieldW, y2: y, stroke: COLORS.gridLine, 'stroke-width': 1 });
gridG.appendChild(line);
}
svg.appendChild(gridG);
svg.appendChild(gridG);
}
function renderWidgetRect(svg, state) {
const cx = state.corners.reduce((s, c) => s + c.x, 0) / 4;
const cy = state.corners.reduce((s, c) => s + c.y, 0) / 4;
@@ -68,31 +73,35 @@ export function renderSVG(svg, state) {
y: cy - state.widgetH / 2,
width: state.widgetW,
height: state.widgetH,
fill: 'rgba(88,166,255,0.12)',
stroke: 'rgba(88,166,255,0.4)',
fill: COLORS.widgetFill,
stroke: COLORS.widgetStroke,
'stroke-width': 2,
'stroke-dasharray': '12,8'
});
svg.appendChild(widgetRect);
}
function renderPolygon(svg, state) {
const poly = document.createElementNS(ns, 'polygon');
const pts = state.corners.map(c => `${c.x},${c.y}`).join(' ');
setAttrs(poly, {
points: pts,
fill: 'rgba(88,166,255,0.1)',
stroke: '#58a6ff',
fill: COLORS.polygonFill,
stroke: COLORS.accent,
'stroke-width': 3,
'stroke-linejoin': 'round'
});
svg.appendChild(poly);
}
function renderCorners(svg, state) {
state.corners.forEach((c, i) => {
const g = document.createElementNS(ns, 'g');
const circle = document.createElementNS(ns, 'circle');
setAttrs(circle, {
cx: c.x, cy: c.y, r: 10,
fill: '#58a6ff',
fill: COLORS.accent,
stroke: '#ffffff',
'stroke-width': 2.5,
class: 'corner-circle'
@@ -116,3 +125,14 @@ export function renderSVG(svg, state) {
svg.appendChild(g);
});
}
export function renderSVG(svg, state) {
svg.setAttribute('viewBox', `0 0 ${state.fieldW} ${state.fieldH}`);
svg.innerHTML = '';
renderBackground(svg, state);
renderGrid(svg, state);
renderWidgetRect(svg, state);
renderPolygon(svg, state);
renderCorners(svg, state);
}
+21 -173
View File
@@ -1,77 +1,11 @@
import { $ } from './dom.js';
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();
}
import { setFieldEditing, setMode, toggleKeepAspect, isFieldEditing, getFieldEditW, getFieldEditH, setFieldEditW, setFieldEditH, isKeepAspect } from './mode.js';
import { setUpdateAllRef, loadBgFile, clearBg, loadWidgetFile, setWidgetImageLock } from './file-loader.js';
import { initDropZones } from './drop-zone.js';
export function updateCSSOutput(state) {
const H = computeHomography(state);
@@ -79,11 +13,10 @@ export function updateCSSOutput(state) {
}
export function initUI(state, updateAll) {
updateAllRef = updateAll;
setUpdateAllRef(updateAll);
$('chainLink').addEventListener('click', () => {
keepAspect = !keepAspect;
updateChainIcon();
toggleKeepAspect();
});
$('btnEditField').addEventListener('click', () => {
@@ -96,25 +29,27 @@ export function initUI(state, updateAll) {
});
$('inpFieldW').addEventListener('input', () => {
if (!fieldEditing) return;
if (!isFieldEditing()) return;
const newW = parseInt($('inpFieldW').value);
if (newW <= 0) return;
if (keepAspect) {
fieldEditH = Math.round(newW * state.fieldH / state.fieldW);
$('inpFieldH').value = fieldEditH;
if (isKeepAspect()) {
const newH = Math.round(newW * state.fieldH / state.fieldW);
$('inpFieldH').value = newH;
setFieldEditH(newH);
}
fieldEditW = newW;
setFieldEditW(newW);
});
$('inpFieldH').addEventListener('input', () => {
if (!fieldEditing) return;
if (!isFieldEditing()) return;
const newH = parseInt($('inpFieldH').value);
if (newH <= 0) return;
if (keepAspect) {
fieldEditW = Math.round(newH * state.fieldW / state.fieldH);
$('inpFieldW').value = fieldEditW;
if (isKeepAspect()) {
const newW = Math.round(newH * state.fieldW / state.fieldH);
$('inpFieldW').value = newW;
setFieldEditW(newW);
}
fieldEditH = newH;
setFieldEditH(newH);
});
$('btnSaveField').addEventListener('click', () => {
@@ -207,35 +142,10 @@ export function initUI(state, updateAll) {
updateAll();
});
function setWidgetImageLock(locked) {
$('inpWidgetW').disabled = locked;
$('inpWidgetH').disabled = locked;
$('btnWidgetRemove').style.display = locked ? '' : 'none';
}
function loadWidgetFile(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = () => {
state.widgetImage = reader.result;
const img = new Image();
img.onload = () => {
state.widgetW = img.naturalWidth;
state.widgetH = img.naturalHeight;
$('inpWidgetW').value = state.widgetW;
$('inpWidgetH').value = state.widgetH;
setWidgetImageLock(true);
updateAll();
};
img.src = reader.result;
};
reader.readAsDataURL(file);
}
$('btnWidgetLoad').addEventListener('click', () => $('widgetFileInput').click());
$('widgetFileInput').addEventListener('change', (e) => {
if (e.target.files[0]) loadWidgetFile(e.target.files[0]);
if (e.target.files[0]) loadWidgetFile(e.target.files[0], state);
e.target.value = '';
});
@@ -251,7 +161,7 @@ export function initUI(state, updateAll) {
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) loadWidgetFile(file);
if (file) loadWidgetFile(file, state);
break;
}
}
@@ -272,67 +182,5 @@ export function initUI(state, updateAll) {
}
});
const lc = document.querySelector('.left-col');
document.addEventListener('dragover', (e) => {
e.preventDefault();
document.body.classList.add('file-dragging');
});
document.addEventListener('drop', () => {
document.body.classList.remove('file-dragging');
lc.classList.remove('drag-over');
});
lc.addEventListener('dragover', (e) => {
e.preventDefault();
lc.classList.add('drag-over');
});
lc.addEventListener('dragleave', (e) => {
if (!lc.contains(e.relatedTarget)) {
lc.classList.remove('drag-over');
}
});
lc.addEventListener('drop', (e) => {
e.preventDefault();
document.body.classList.remove('file-dragging');
lc.classList.remove('drag-over');
if (e.dataTransfer.files[0]) loadBgFile(e.dataTransfer.files[0], state);
});
const bp = $('bgPanel');
bp.addEventListener('dragover', (e) => {
e.preventDefault();
bp.classList.add('drag-over');
});
bp.addEventListener('dragleave', (e) => {
if (!bp.contains(e.relatedTarget)) {
bp.classList.remove('drag-over');
}
});
bp.addEventListener('drop', (e) => {
e.preventDefault();
document.body.classList.remove('file-dragging');
bp.classList.remove('drag-over');
if (e.dataTransfer.files[0]) loadBgFile(e.dataTransfer.files[0], state);
});
const wp = $('widgetPanel');
wp.addEventListener('dragover', (e) => {
e.preventDefault();
wp.classList.add('drag-over');
});
wp.addEventListener('dragleave', (e) => {
if (!wp.contains(e.relatedTarget)) {
wp.classList.remove('drag-over');
}
});
wp.addEventListener('drop', (e) => {
e.preventDefault();
document.body.classList.remove('file-dragging');
wp.classList.remove('drag-over');
if (e.dataTransfer.files[0]) loadWidgetFile(e.dataTransfer.files[0]);
});
initDropZones(state);
}