From 2298512dce015c149d4d3e1a671012dec6bf1f51 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 20 May 2026 22:44:55 -0400 Subject: [PATCH] Optional background image, disc-image placement fixes, browser polish - Publishing no longer requires a background image: cover/jewel/disc designs publish with layers-only content. "Save design (JSON)" works for layer-only disc designs (removed a silent return + a payload throw). - Disc background image now lands correctly: renderDesignerDisc() runs after the designer view is laid out, and the publish/feed preview uses the same cover-pan math as the editor instead of a naive translate(%). - Publish-modal preview no longer squishes the disc vertically (dropped axis-independent max-width/height clamps; fitMiniPreview scales it). - Template browser: "Fork off!" button renamed to "Use/Fork". Co-Authored-By: Claude Opus 4.7 --- src/index.html | 58 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/index.html b/src/index.html index 477e85d..e905f37 100644 --- a/src/index.html +++ b/src/index.html @@ -1657,8 +1657,7 @@ .toast.toastOut { opacity:0; transition:opacity .2s; } @keyframes toastIn { from { transform:translateY(8px); opacity:0; } to { transform:translateY(0); opacity:1; } } - .publishConfirmPreview { background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:10px; margin:0 0 12px; display:flex; justify-content:center; align-items:center; max-height:280px; overflow:hidden; } - .publishConfirmPreview > * { max-width:100%; max-height:260px; } + .publishConfirmPreview { background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:10px; margin:0 0 12px; display:flex; justify-content:center; align-items:center; height:280px; overflow:hidden; } .publishConfirmMeta { margin:0 0 12px; font-size:.82rem; } .publishConfirmMetaRow { display:flex; gap:10px; padding:5px 0; border-bottom:1px solid var(--line); } .publishConfirmMetaRow:last-child { border-bottom:0; } @@ -4014,7 +4013,6 @@ function discDesignPayload() { persistCurrentDiscDesign(); const design = state.discDesign; - if (!design?.src) throw new Error('Load an image URL before publishing this disc design.'); const layers = currentDesignerLayers(); return { version: SCHEMA_VERSION, @@ -6090,10 +6088,12 @@ renderJewelSlots(); renderCombined(); renderLayers(); - renderDesignerDisc(); syncLayerInputs(); syncDesignerInputs(); updateBodyClasses(); + // After updateBodyClasses so the designer view is displayed/laid out — + // the disc image's cover-pan math needs the wrapper's real size. + renderDesignerDisc(); refreshPenOverlayGeometry(); refreshPenOverlayContent(); if (_editorTool !== 'select') syncToolOptionInputs(); @@ -8960,8 +8960,8 @@ } else if (state.templateMode === 'jewel') { payload = jewelTemplatePayload(); typeTag = 'casewrap-jewel'; - if (!previewFromPayload(payload, 'jewel')) { - throw new Error('Load jewel insert artwork before publishing.'); + if (!previewFromPayload(payload, 'jewel') && !(payload.layers || []).length) { + throw new Error('Add jewel insert artwork or at least one layer before publishing.'); } } else if (state.templateMode === 'customart') { payload = customArtPayload(); @@ -8974,8 +8974,8 @@ } else { payload = coverTemplatePayload(); typeTag = 'casewrap-cover'; - if (!previewFromPayload(payload, 'cover')) { - throw new Error('Load cover artwork before publishing.'); + if (!previewFromPayload(payload, 'cover') && !(payload.layers || []).length) { + throw new Error('Add cover artwork or at least one layer before publishing.'); } } const mode = normalizeTemplateMode(payload); @@ -8999,11 +8999,13 @@ async function publishDiscDesignToNostr() { const design = state.discDesign; - if (!design?.src) throw new Error('Load an image URL before publishing this disc design.'); if (!state.meta.category?.trim()) { throw new Error('Select a category in Project Metadata before publishing.'); } const payload = discDesignPayload(); + if (!design?.src && !(payload.layers || []).length) { + throw new Error('Load an image or add at least one layer before publishing this disc design.'); + } const designName = (design.name || '').trim() || state.meta.title; if (!designName?.trim()) { throw new Error('Add a title in Project Metadata or a custom design name before publishing.'); @@ -10709,7 +10711,6 @@ if (!title) { toastError('Please enter a title in Project Metadata before saving.'); return; } if (!category) { toastError('Please select a category in Project Metadata before saving.'); return; } const design = state.discDesign; - if (!design?.src) return; const payload = discDesignPayload(); const name = (design.name || 'disc_design') .toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); @@ -13260,7 +13261,7 @@ prevBtn.type = 'button'; prevBtn.textContent = 'Preview'; prevBtn.addEventListener('click', e => { e.stopPropagation(); openDesignPreview(row); }); const btn = document.createElement('button'); - btn.type = 'button'; btn.textContent = 'Fork off!'; + btn.type = 'button'; btn.textContent = 'Use/Fork'; btn.title = 'Fork this design — opens it in the editor so you can save your own version'; btn.addEventListener('click', e => { e.stopPropagation(); onUse(); }); const tipBtn = makeFeedCardTipButton(row); @@ -13803,7 +13804,7 @@ prevBtn.addEventListener('click', e => { e.stopPropagation(); openDesignPreview(row); }); const useBtn = document.createElement('button'); useBtn.type = 'button'; - useBtn.textContent = 'Fork off!'; + useBtn.textContent = 'Use/Fork'; useBtn.title = 'Fork this design — opens it in the editor so you can save your own version'; useBtn.addEventListener('click', e => { e.stopPropagation(); onUse(); }); actions.append(makeReactionBtn(row), prevBtn, useBtn); @@ -13957,7 +13958,7 @@ const forkBtn = document.createElement('button'); forkBtn.type = 'button'; - forkBtn.textContent = 'Fork off!'; + forkBtn.textContent = 'Use/Fork'; forkBtn.title = 'Fork this design — opens it in the editor so you can save your own version'; forkBtn.addEventListener('click', (e) => { e.stopPropagation(); @@ -14177,8 +14178,37 @@ if (disc.src) { const img = document.createElement('img'); img.src = disc.src; - img.style.cssText = `position:absolute;left:50%;top:50%;width:100%;height:100%;object-fit:${disc.fit || 'cover'};transform:translate(-50%,-50%) translate(${disc.x||0}%,${disc.y||0}%) rotate(${disc.rotate||0}deg) scale(${disc.zoom||1});z-index:1;`; + img.alt = ''; + img.style.cssText = 'position:absolute;left:50%;top:50%;width:100%;height:100%;object-fit:fill;transform-origin:center center;user-select:none;pointer-events:none;z-index:1;'; discWrap.appendChild(img); + // Match the editor's cover-pan math (applyRotationCover) so the + // background image sits exactly where the user placed it — the old + // translate(x%) model panned by a different, much larger range. + const updateDiscArt = () => { + const fit = disc.fit || 'cover'; + const cW = discWrap.clientWidth, cH = discWrap.clientHeight; + if (!cW || !cH) return; + const nW = img.naturalWidth || cW, nH = img.naturalHeight || cH; + let baseW = cW, baseH = cH; + if (fit === 'none') { baseW = nW; baseH = nH; } + else if (fit !== 'fill') { + const fs = fit === 'contain' ? Math.min(cW / nW, cH / nH) : Math.max(cW / nW, cH / nH); + baseW = nW * fs; baseH = nH * fs; + } + const rotate = Number(disc.rotate) || 0, zoom = Number(disc.zoom) || 1; + const rad = rotate * Math.PI / 180, ac = Math.abs(Math.cos(rad)), as = Math.abs(Math.sin(rad)); + const rotatedW = baseW * ac + baseH * as, rotatedH = baseW * as + baseH * ac; + const coverScale = fit === 'cover' + ? Math.max(cW / Math.max(rotatedW, 1), cH / Math.max(rotatedH, 1), 1) : 1; + const finalScale = coverScale * zoom; + const xPx = ((Number(disc.x) || 0) / 100) * Math.max(0, (rotatedW * finalScale - cW) / 2); + const yPx = ((Number(disc.y) || 0) / 100) * Math.max(0, (rotatedH * finalScale - cH) / 2); + img.style.width = `${baseW}px`; + img.style.height = `${baseH}px`; + img.style.transform = `translate(-50%,-50%) translate(${xPx}px,${yPx}px) rotate(${rotate}deg) scale(${finalScale})`; + }; + if (img.complete) requestAnimationFrame(updateDiscArt); + img.addEventListener('load', updateDiscArt, { once: true }); } layers.forEach((layer, i) => {