From 1f9ac90ce2724795d95afdf5821834a4c8941232 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Wed, 24 Jun 2026 08:11:22 +0500 Subject: [PATCH] refactor: update js - separate files, modules --- .gitignore | 1 + .../plans/1782265108554-gentle-rocket.md | 92 --------- js/constants.js | 15 ++ js/dom.js | 1 + js/drop-zone.js | 40 ++++ js/file-loader.js | 49 +++++ js/homography.js | 9 +- js/inputs.js | 15 +- js/mode.js | 79 +++++++ js/preview.js | 9 +- js/state.js | 30 +-- js/svg-renderer.js | 48 +++-- js/ui.js | 194 ++---------------- style.css | 4 + 14 files changed, 270 insertions(+), 316 deletions(-) create mode 100644 .gitignore delete mode 100644 .mimocode/plans/1782265108554-gentle-rocket.md create mode 100644 js/constants.js create mode 100644 js/dom.js create mode 100644 js/drop-zone.js create mode 100644 js/file-loader.js create mode 100644 js/mode.js 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

-
- - - - -
-
-``` - -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%;