From 99ebaceda3e30aa165977b57ca44804efe1bc851 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Wed, 24 Jun 2026 07:59:52 +0500 Subject: [PATCH] feat: load widget image flow --- index.html | 8 +++++- js/preview.js | 18 +++++++++++++ js/state.js | 7 +++-- js/ui.js | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 41 +++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index d38fb0f..5a5c3e5 100644 --- a/index.html +++ b/index.html @@ -93,12 +93,18 @@ -
+

Widget

+
+ + + +
+
diff --git a/js/preview.js b/js/preview.js index 8ed4b78..16840cf 100644 --- a/js/preview.js +++ b/js/preview.js @@ -12,6 +12,22 @@ function applyBgToField(field, state) { } } +function applyWidgetImage(widget, state) { + if (state.widgetImage) { + widget.style.backgroundImage = `url(${state.widgetImage})`; + widget.style.backgroundSize = '100% 100%'; + widget.style.backgroundPosition = 'center'; + widget.style.backgroundRepeat = 'no-repeat'; + widget.querySelector('span').style.display = 'none'; + } else { + widget.style.backgroundImage = ''; + widget.style.backgroundSize = ''; + widget.style.backgroundPosition = ''; + widget.style.backgroundRepeat = ''; + widget.querySelector('span').style.display = ''; + } +} + export function updateLivePreview(state) { const container = $('fieldContainer'); const cw = container.clientWidth; @@ -34,6 +50,8 @@ export function updateLivePreview(state) { widget.style.width = state.widgetW + 'px'; widget.style.height = state.widgetH + 'px'; + applyWidgetImage(widget, state); + const H = computeHomography(state); if (H) { const n = v => parseFloat(v.toFixed(6)); diff --git a/js/state.js b/js/state.js index 9291717..c888eee 100644 --- a/js/state.js +++ b/js/state.js @@ -8,7 +8,8 @@ export const defaultState = { { x: 340, y: 950 } ], bgImage: null, - objectFit: 'contain' + objectFit: 'contain', + widgetImage: null }; export const state = { @@ -21,7 +22,8 @@ export const state = { { x: 340, y: 950 } ], bgImage: null, - objectFit: 'contain' + objectFit: 'contain', + widgetImage: null }; export function resetState() { @@ -31,6 +33,7 @@ export function resetState() { 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 }; }); diff --git a/js/ui.js b/js/ui.js index a05e236..e97e34f 100644 --- a/js/ui.js +++ b/js/ui.js @@ -139,11 +139,13 @@ export function initUI(state, updateAll) { }); $('inpWidgetW').addEventListener('input', () => { + if (state.widgetImage) return; const v = parseInt($('inpWidgetW').value); if (v > 0) { state.widgetW = v; updateAll(); } }); $('inpWidgetH').addEventListener('input', () => { + if (state.widgetImage) return; const v = parseInt($('inpWidgetH').value); if (v > 0) { state.widgetH = v; updateAll(); } }); @@ -173,6 +175,9 @@ export function initUI(state, updateAll) { buildCornerInputs(state); $('btnBgRemove').style.display = 'none'; $('bgObjectFit').value = state.objectFit; + $('inpWidgetW').disabled = false; + $('inpWidgetH').disabled = false; + $('btnWidgetRemove').style.display = 'none'; updateAll(); }); @@ -202,7 +207,58 @@ 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]); + e.target.value = ''; + }); + + $('btnWidgetRemove').addEventListener('click', () => { + state.widgetImage = null; + setWidgetImageLock(false); + updateAll(); + }); + + $('widgetPasteInput').addEventListener('paste', (e) => { + const items = e.clipboardData?.items; + if (!items) return; + for (const item of items) { + if (item.type.startsWith('image/')) { + const file = item.getAsFile(); + if (file) loadWidgetFile(file); + break; + } + } + }); + document.addEventListener('paste', (e) => { + if (document.activeElement?.id === 'widgetPasteInput') return; const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; const items = e.clipboardData?.items; @@ -262,4 +318,21 @@ export function initUI(state, updateAll) { 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]); + }); } diff --git a/style.css b/style.css index ea9552c..94407ab 100644 --- a/style.css +++ b/style.css @@ -406,6 +406,11 @@ main { cursor: not-allowed; } +.field-row input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .field-edit-options { display: none; margin-top: 10px; @@ -493,6 +498,17 @@ body.file-dragging #bgPanel.drag-over { background: rgba(88, 166, 255, 0.08); } +body.file-dragging #widgetPanel { + outline: 2px dashed rgba(88, 166, 255, 0.7); + outline-offset: -2px; +} + +body.file-dragging #widgetPanel.drag-over { + outline: 3px dashed var(--accent); + outline-offset: -3px; + background: rgba(88, 166, 255, 0.08); +} + #widget { position: absolute; top: 0; @@ -558,6 +574,31 @@ body.mode-preview .left-col { gap: 8px; } +.widget-img-controls { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +} + +.widget-paste-input { + width: 100%; + padding: 6px 8px; + margin-top: 8px; + background: var(--input-bg); + border: 1px dashed var(--border); + border-radius: 4px; + color: #8b949e; + font-size: 13px; + outline: none; + cursor: pointer; + transition: border-color 0.15s; +} +.widget-paste-input:focus { + border-color: var(--accent); + border-style: solid; +} + #bgObjectFit { padding: 4px 8px; background: var(--input-bg);