diff --git a/index.html b/index.html
index d38fb0f..5a5c3e5 100644
--- a/index.html
+++ b/index.html
@@ -93,12 +93,18 @@
-
+
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);