feat: load widget image flow
This commit is contained in:
+7
-1
@@ -93,12 +93,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel" id="widgetPanel">
|
||||||
<h2>Widget</h2>
|
<h2>Widget</h2>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<label><span>W</span><input id="inpWidgetW" type="number" value="400" min="1" /></label>
|
<label><span>W</span><input id="inpWidgetW" type="number" value="400" min="1" /></label>
|
||||||
<label><span>H</span><input id="inpWidgetH" type="number" value="300" min="1" /></label>
|
<label><span>H</span><input id="inpWidgetH" type="number" value="300" min="1" /></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="widget-img-controls">
|
||||||
|
<input type="file" id="widgetFileInput" accept="image/*" hidden />
|
||||||
|
<button class="btn btn-edit-field" id="btnWidgetLoad">Load Image</button>
|
||||||
|
<button class="btn btn-cancel-field" id="btnWidgetRemove" style="display:none">Remove</button>
|
||||||
|
</div>
|
||||||
|
<input id="widgetPasteInput" type="text" class="widget-paste-input" placeholder="Ctrl+V to paste image" readonly />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|||||||
@@ -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) {
|
export function updateLivePreview(state) {
|
||||||
const container = $('fieldContainer');
|
const container = $('fieldContainer');
|
||||||
const cw = container.clientWidth;
|
const cw = container.clientWidth;
|
||||||
@@ -34,6 +50,8 @@ export function updateLivePreview(state) {
|
|||||||
widget.style.width = state.widgetW + 'px';
|
widget.style.width = state.widgetW + 'px';
|
||||||
widget.style.height = state.widgetH + 'px';
|
widget.style.height = state.widgetH + 'px';
|
||||||
|
|
||||||
|
applyWidgetImage(widget, state);
|
||||||
|
|
||||||
const H = computeHomography(state);
|
const H = computeHomography(state);
|
||||||
if (H) {
|
if (H) {
|
||||||
const n = v => parseFloat(v.toFixed(6));
|
const n = v => parseFloat(v.toFixed(6));
|
||||||
|
|||||||
+5
-2
@@ -8,7 +8,8 @@ export const defaultState = {
|
|||||||
{ x: 340, y: 950 }
|
{ x: 340, y: 950 }
|
||||||
],
|
],
|
||||||
bgImage: null,
|
bgImage: null,
|
||||||
objectFit: 'contain'
|
objectFit: 'contain',
|
||||||
|
widgetImage: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
@@ -21,7 +22,8 @@ export const state = {
|
|||||||
{ x: 340, y: 950 }
|
{ x: 340, y: 950 }
|
||||||
],
|
],
|
||||||
bgImage: null,
|
bgImage: null,
|
||||||
objectFit: 'contain'
|
objectFit: 'contain',
|
||||||
|
widgetImage: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetState() {
|
export function resetState() {
|
||||||
@@ -31,6 +33,7 @@ export function resetState() {
|
|||||||
state.widgetH = defaultState.widgetH;
|
state.widgetH = defaultState.widgetH;
|
||||||
state.bgImage = null;
|
state.bgImage = null;
|
||||||
state.objectFit = defaultState.objectFit;
|
state.objectFit = defaultState.objectFit;
|
||||||
|
state.widgetImage = null;
|
||||||
defaultState.corners.forEach((c, i) => {
|
defaultState.corners.forEach((c, i) => {
|
||||||
state.corners[i] = { x: c.x, y: c.y };
|
state.corners[i] = { x: c.x, y: c.y };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -139,11 +139,13 @@ export function initUI(state, updateAll) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('inpWidgetW').addEventListener('input', () => {
|
$('inpWidgetW').addEventListener('input', () => {
|
||||||
|
if (state.widgetImage) return;
|
||||||
const v = parseInt($('inpWidgetW').value);
|
const v = parseInt($('inpWidgetW').value);
|
||||||
if (v > 0) { state.widgetW = v; updateAll(); }
|
if (v > 0) { state.widgetW = v; updateAll(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
$('inpWidgetH').addEventListener('input', () => {
|
$('inpWidgetH').addEventListener('input', () => {
|
||||||
|
if (state.widgetImage) return;
|
||||||
const v = parseInt($('inpWidgetH').value);
|
const v = parseInt($('inpWidgetH').value);
|
||||||
if (v > 0) { state.widgetH = v; updateAll(); }
|
if (v > 0) { state.widgetH = v; updateAll(); }
|
||||||
});
|
});
|
||||||
@@ -173,6 +175,9 @@ export function initUI(state, updateAll) {
|
|||||||
buildCornerInputs(state);
|
buildCornerInputs(state);
|
||||||
$('btnBgRemove').style.display = 'none';
|
$('btnBgRemove').style.display = 'none';
|
||||||
$('bgObjectFit').value = state.objectFit;
|
$('bgObjectFit').value = state.objectFit;
|
||||||
|
$('inpWidgetW').disabled = false;
|
||||||
|
$('inpWidgetH').disabled = false;
|
||||||
|
$('btnWidgetRemove').style.display = 'none';
|
||||||
updateAll();
|
updateAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,7 +207,58 @@ export function initUI(state, updateAll) {
|
|||||||
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) => {
|
document.addEventListener('paste', (e) => {
|
||||||
|
if (document.activeElement?.id === 'widgetPasteInput') return;
|
||||||
const tag = document.activeElement?.tagName;
|
const tag = document.activeElement?.tagName;
|
||||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
||||||
const items = e.clipboardData?.items;
|
const items = e.clipboardData?.items;
|
||||||
@@ -262,4 +318,21 @@ export function initUI(state, updateAll) {
|
|||||||
bp.classList.remove('drag-over');
|
bp.classList.remove('drag-over');
|
||||||
if (e.dataTransfer.files[0]) loadBgFile(e.dataTransfer.files[0], state);
|
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]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,6 +406,11 @@ main {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-row input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.field-edit-options {
|
.field-edit-options {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -493,6 +498,17 @@ body.file-dragging #bgPanel.drag-over {
|
|||||||
background: rgba(88, 166, 255, 0.08);
|
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 {
|
#widget {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -558,6 +574,31 @@ body.mode-preview .left-col {
|
|||||||
gap: 8px;
|
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 {
|
#bgObjectFit {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
|
|||||||
Reference in New Issue
Block a user