diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b31f87
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.mimocode/
diff --git a/.mimocode/plans/1782265108554-gentle-rocket.md b/.mimocode/plans/1782265108554-gentle-rocket.md
deleted file mode 100644
index 27d5d3f..0000000
--- a/.mimocode/plans/1782265108554-gentle-rocket.md
+++ /dev/null
@@ -1,92 +0,0 @@
-# Plan: Background Image Upload for CSS Matrix Calculator
-
-## Overview
-Add ability to load a background image onto the main `#field` element for visual reference when positioning corners. The image is stored as a data URL in memory (no server). Three upload methods: Ctrl+V paste, file picker, drag & drop onto the field. Adjustable via `object-fit` options.
-
-## Files to Modify
-
-### 1. `js/state.js` — Add background state
-- Add `bgImage: null` (data URL string) and `objectFit: 'contain'` to `defaultState` and `state`
-- Reset these in `resetState()`
-
-### 2. `index.html` — New "Background" panel + hidden file input
-Add a new panel in `right-col` (between Field and Widget panels, after line 78):
-```html
-
-
Background
-
-
- Load Image
- Remove
-
- contain
- cover
- fill
- none
- scale-down
-
-
-
-```
-
-Add drag & drop hint overlay on `#fieldContainer` (drop zone indicator).
-
-### 3. `style.css` — New styles
-- `.bg-controls` — flex row layout for controls
-- `#bgObjectFit` — styled select matching existing dark theme
-- `#fieldContainer.drag-over` — dashed border highlight for drop feedback
-- `#field` — add `background-size`, `background-position`, `background-repeat` properties
-- `.bg-hint` — overlay text on field when no image loaded ("Drop image or Ctrl+V")
-
-### 4. `js/ui.js` — Event handlers
-- `btnBgLoad` click → trigger hidden file input
-- `bgFileInput` change → read file as data URL via `FileReader` → `state.bgImage = dataUrl`
-- `document` paste event → detect image in `clipboardData.files` → read as data URL
-- `#fieldContainer` dragover/drop → read dropped file as data URL
-- `btnBgRemove` click → clear `state.bgImage`
-- `bgObjectFit` change → `state.objectFit = value`
-- Toggle remove button visibility based on whether image is loaded
-- On state change: call `updateAll()`
-
-### 5. `js/preview.js` — Apply background to field
-- If `state.bgImage`: set `field.style.backgroundImage = url(...)`, `field.style.backgroundSize`, `field.style.backgroundPosition`, `field.style.backgroundRepeat` based on objectFit
-- If no image: clear background properties
-
-### 6. `js/svg-renderer.js` — Show background in SVG editor
-- If `state.bgImage`: add an `` element as first child of SVG (behind grid) with `href`, `width`, `height`, and `preserveAspectRatio` matching the objectFit mapping
-
-### 7. `js/main.js` — No changes needed (updateAll already propagates)
-
-## Implementation Details
-
-### Image loading flow
-1. User triggers upload (paste/file/drop) → get `File` object
-2. `FileReader.readAsDataURL(file)` → get data URL string
-3. Store in `state.bgImage`
-4. Call `updateAll()` to propagate
-
-### object-fit mapping to SVG preserveAspectRatio
-| objectFit | SVG preserveAspectRatio |
-|---------------|------------------------|
-| contain | xMidYMid meet |
-| cover | xMidYMid slice |
-| fill | none |
-| none | xMinYMin meet |
-| scale-down | xMidYMid meet |
-
-### Keyboard shortcut
-- Ctrl+V paste listener on `document` — check for image in clipboard items
-- Only active when no input/textarea is focused (to avoid conflicts)
-
-### Drag & drop
-- Listen on `#fieldContainer` for `dragover` (prevent default, show visual hint) and `drop` (read file)
-
-## Verification
-1. Open `index.html` in browser
-2. **File picker**: Click "Load Image" → select image → verify it appears as background on the field in both edit (SVG) and preview modes
-3. **Ctrl+V**: Copy an image to clipboard → press Ctrl+V → verify it loads
-4. **Drag & drop**: Drag an image file onto the field area → verify it loads
-5. **object-fit**: Change dropdown → verify background resizes correctly (contain fits, cover fills, etc.)
-6. **Remove**: Click "Remove" → verify background clears
-7. **Reset**: Click "Reset" → verify background clears along with other state
-8. **Corner drag**: Load background → drag corners → verify background stays in place, corners move independently
diff --git a/js/constants.js b/js/constants.js
new file mode 100644
index 0000000..464935a
--- /dev/null
+++ b/js/constants.js
@@ -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));
diff --git a/js/dom.js b/js/dom.js
new file mode 100644
index 0000000..40000d4
--- /dev/null
+++ b/js/dom.js
@@ -0,0 +1 @@
+export const $ = id => document.getElementById(id);
diff --git a/js/drop-zone.js b/js/drop-zone.js
new file mode 100644
index 0000000..3b02ad4
--- /dev/null
+++ b/js/drop-zone.js
@@ -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));
+}
diff --git a/js/file-loader.js b/js/file-loader.js
new file mode 100644
index 0000000..398c096
--- /dev/null
+++ b/js/file-loader.js
@@ -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);
+}
diff --git a/js/homography.js b/js/homography.js
index c5e4c54..13d3363 100644
--- a/js/homography.js
+++ b/js/homography.js
@@ -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');
}
diff --git a/js/inputs.js b/js/inputs.js
index c0fdf93..a4351f0 100644
--- a/js/inputs.js
+++ b/js/inputs.js
@@ -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;
});
diff --git a/js/mode.js b/js/mode.js
new file mode 100644
index 0000000..c495a9b
--- /dev/null
+++ b/js/mode.js
@@ -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';
+}
diff --git a/js/preview.js b/js/preview.js
index 16840cf..abfdcea 100644
--- a/js/preview.js
+++ b/js/preview.js
@@ -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';
diff --git a/js/state.js b/js/state.js
index c888eee..ca8e37d 100644
--- a/js/state.js
+++ b/js/state.js
@@ -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);
}
diff --git a/js/svg-renderer.js b/js/svg-renderer.js
index 8ed7028..ee089b7 100644
--- a/js/svg-renderer.js
+++ b/js/svg-renderer.js
@@ -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);
+}
diff --git a/js/ui.js b/js/ui.js
index e97e34f..ce57e46 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -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);
}
diff --git a/style.css b/style.css
index 94407ab..2649eb3 100644
--- a/style.css
+++ b/style.css
@@ -246,6 +246,10 @@ main {
.corner-input-row input:focus {
border-color: var(--accent);
}
+.corner-input-row input.input-error {
+ border-color: #f85149;
+ background: rgba(248, 81, 73, 0.1);
+}
#cssOutput {
width: 100%;