mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-07 22:20:09 +00:00
Mini-preview cards, fixed scaling, welcome close button, danger styling
Template browser cards now render WYSIWYG mini-previews instead of just the background image. Each card shows the full layered composition (color fills, text, image layers, slot artwork) at thumbnail scale, with the design's natural silhouette preserved — discs stay circular, sheets stay rectangular, cover stays wide. Preview renderers (4 total): - Added a `compact` option to renderTemplatePreviewDOM, renderDiscSheetPreviewDOM, renderJewelPreviewDOM, renderDiscPreviewDOM. When compact: drop labels (BACK/SPINE/FRONT, Disc 1/2, FRONT/TRAY, sheet titles), drop shadow, drop border lines. - Compact mode renders each preview at a fixed natural pixel size (720 wide for sheets, 380 for disc-design); fitMiniPreview applies a CSS transform: scale() to fit the thumb. Everything — text, layer positions, slot art — scales together so the mini is a true miniature of what `Preview` shows. Disc-design preview specific fixes: - Was the only renderer not sorting layers by z, so they rendered in array order and a higher-z layer added earlier could be hidden behind a lower-z layer added later. Now sorted ascending and stacked in DOM. - Added explicit z-index values matching the disc-sheet renderer (bg img z:1, layers z:3+sortedIndex, hole z:12) to avoid stacking context surprises from the per-layer transform: rotate(). - Used Number(layer.x) etc. for defensive coercion of legacy payloads. - Made the center hole percentage-sized so it scales with the disc. Cover preview fix: - Dropped the unnecessary `row` wrapper that broke mini-preview sizing. preview is appended directly to container, same shape as the other three renderers. The `!hasArtwork` early-return now only fires in non-compact mode so empty covers still show their silhouette in the card thumb. UI polish: - Close button (×) in the top-right corner of the welcome modal. - Both "Close project" buttons (template-mode and disc-designer) now use the .danger class to match the styling of the other destructive buttons in their panels. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
d94eff9671
commit
20cbe78805
1 file changed
+187
-148
+187
-148
@@ -1513,7 +1513,11 @@
|
||||
.feedTrustBadge{font-size:.62rem;background:rgba(0,184,230,.12);color:var(--accent);border:1px solid rgba(0,184,230,.25);border-radius:4px;padding:1px 5px;margin-left:5px;white-space:nowrap;flex-shrink:0}
|
||||
body.dark .feedTrustBadge{background:rgba(0,217,255,.1);border-color:rgba(0,217,255,.2)}
|
||||
.feedCard.feedCardBlurred .feedCardThumb img,
|
||||
.feedCard.feedCardBlurred .feedCardThumb canvas{filter:blur(14px) saturate(.5);transition:filter .15s}
|
||||
.feedCard.feedCardBlurred .feedCardThumb canvas,
|
||||
.feedCard.feedCardBlurred .miniPreviewWrap{filter:blur(14px) saturate(.5);transition:filter .15s}
|
||||
.miniPreviewWrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;contain:layout style paint;z-index:2}
|
||||
.miniPreviewWrap > *{margin:0!important;box-shadow:none!important;flex:0 0 auto;transform-origin:center center}
|
||||
.feedCardThumb .miniPreviewWrap ~ .feedCardThumbLabel{display:none}
|
||||
.feedCard.feedCardBroken{opacity:.55}
|
||||
.feedCard.feedCardBroken .feedCardThumb{background:repeating-linear-gradient(45deg,#f1f5f9,#f1f5f9 10px,#e2e8f0 10px,#e2e8f0 20px)}
|
||||
.feedCardBrokenBadge{position:absolute;top:6px;right:6px;background:rgba(220,38,38,.92);color:#fff;font-size:.62rem;font-weight:700;border-radius:4px;padding:2px 6px;z-index:6;letter-spacing:.03em}
|
||||
@@ -1527,6 +1531,8 @@
|
||||
body.dark .feedTypeBtn.active{color:#fff}
|
||||
.feedCategorySelect{width:auto;min-width:130px;padding:5px 8px;font-size:.78rem}
|
||||
.welcomeCard { max-width: 900px; width: min(900px, 92vw); }
|
||||
.welcomeCloseBtn { position:absolute; top:8px; right:10px; width:32px; height:32px; border:0; background:transparent; color:var(--muted); font-size:24px; line-height:1; cursor:pointer; padding:0; border-radius:6px; display:flex; align-items:center; justify-content:center; }
|
||||
.welcomeCloseBtn:hover { background:rgba(127,127,127,.15); color:var(--ink); }
|
||||
.welcomeSearchRow { display:grid; grid-template-columns: 1fr 160px 160px auto; gap:8px; align-items:end; margin-bottom:12px; }
|
||||
.welcomeTabs { display:flex; gap:6px; border-bottom:1px solid var(--line); margin:4px 0 10px; flex-wrap:wrap; }
|
||||
.welcomeTab { background:transparent; border:0; padding:8px 14px; cursor:pointer; font-weight:700; color:var(--muted); border-bottom:2px solid transparent; font-size:.85rem; }
|
||||
@@ -1917,7 +1923,7 @@
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="saveProjectBtn">Save project (JSON)</button>
|
||||
<button id="closeProjectBtn">Close project</button>
|
||||
<button type="button" class="danger" id="closeProjectBtn">Close project</button>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Nostr</summary>
|
||||
@@ -2246,7 +2252,7 @@
|
||||
<div class="buttonRow">
|
||||
<button type="button" id="designerSaveProjectBtn">Save design (JSON)</button>
|
||||
<button type="button" id="designerExportJpegBtn">Export JPEG</button>
|
||||
<button type="button" id="designerCloseProjectBtn">Close project</button>
|
||||
<button type="button" class="danger" id="designerCloseProjectBtn">Close project</button>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Nostr</summary>
|
||||
@@ -2369,7 +2375,8 @@
|
||||
|
||||
|
||||
<div class="modal" id="welcomeModal" aria-hidden="true">
|
||||
<div class="modalCard welcomeCard">
|
||||
<div class="modalCard welcomeCard" style="position:relative">
|
||||
<button type="button" id="welcomeCloseBtn" class="welcomeCloseBtn" aria-label="Close">×</button>
|
||||
<h2 style="margin:0 0 4px">Welcome to CaseWrap Studio</h2>
|
||||
<p class="subhead" style="margin:0 0 14px">Browse community templates or start your own.</p>
|
||||
<div class="welcomeSearchRow">
|
||||
@@ -7354,20 +7361,37 @@
|
||||
const isDisc = row.mode === 'disc' || row.mode === 'disc-design';
|
||||
const modeLabel = MODE_LABELS[row.mode] || row.mode;
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'feedCardThumb' + (isDisc ? ' feedCardThumbDisc' : '');
|
||||
thumb.className = 'feedCardThumb';
|
||||
|
||||
// Mini-preview: same DOM as the full preview rendered at its natural size,
|
||||
// then CSS-scaled via transform to fit the thumb. Layers, text, and slot
|
||||
// art all scale together so it's WYSIWYG with the click-Preview pane.
|
||||
const previewWrap = document.createElement('div');
|
||||
previewWrap.className = 'miniPreviewWrap';
|
||||
try {
|
||||
const payload = row.payload || {};
|
||||
if (row.mode === 'disc-design') renderDiscPreviewDOM(previewWrap, payload, { compact: true });
|
||||
else if (row.mode === 'disc') renderDiscSheetPreviewDOM(previewWrap, payload, { compact: true });
|
||||
else if (row.mode === 'jewel') renderJewelPreviewDOM(previewWrap, payload, { compact: true });
|
||||
else renderTemplatePreviewDOM(previewWrap, payload, { compact: true });
|
||||
} catch {}
|
||||
thumb.appendChild(previewWrap);
|
||||
fitMiniPreview(previewWrap);
|
||||
|
||||
// Fallback label so empty designs still show their mode (and as a placeholder
|
||||
// if the preview produced nothing visible).
|
||||
const lbl = document.createElement('span');
|
||||
lbl.className = 'feedCardThumbLabel';
|
||||
lbl.textContent = modeLabel;
|
||||
if (row.mode === 'disc') {
|
||||
buildDiscSheetThumb(thumb, row.payload, false);
|
||||
} else {
|
||||
thumb.appendChild(lbl);
|
||||
}
|
||||
if (row.preview && row.mode !== 'disc') {
|
||||
const img = document.createElement('img');
|
||||
img.alt = ''; img.loading = 'lazy'; img.src = row.preview;
|
||||
img.addEventListener('error', () => { img.remove(); markImageBroken(row.preview); });
|
||||
thumb.insertBefore(img, lbl);
|
||||
thumb.appendChild(lbl);
|
||||
|
||||
// Hidden detector image to keep broken-flag/sort behavior consistent.
|
||||
// We can't read pixels from cross-origin images but we CAN listen for
|
||||
// their onerror (display-only path) to know if the main artwork URL works.
|
||||
if (row.preview) {
|
||||
const detector = new Image();
|
||||
detector.onerror = () => markImageBroken(row.preview);
|
||||
detector.src = row.preview;
|
||||
}
|
||||
const isBroken = isPreviewBroken(row);
|
||||
if (isBroken) {
|
||||
@@ -8012,56 +8036,106 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderDiscPreviewDOM(container, payload) {
|
||||
// Shared by all preview renderers. Mirrors the editor's `.textLayer` CSS
|
||||
// so text rendering in the preview matches what the editor shows.
|
||||
// pxPerInch is the preview's CSS-px per real inch (e.g., 720/sheetW or
|
||||
// PREVIEW_PX/DISC_D), so the rendered metrics scale with the preview size.
|
||||
function applyPreviewLayerTextStyle(el, layer, pxPerInch) {
|
||||
const fontPx = (Number(layer.fontSize) || 14) * pxPerInch / 72;
|
||||
const padPx = 0.04 * pxPerInch; // matches .textLayer padding: .04in
|
||||
const align = layer.align || 'left';
|
||||
const family = layer.fontFamily && layer.fontFamily !== 'inherit'
|
||||
? layer.fontFamily
|
||||
: 'inherit';
|
||||
el.style.cssText += `font-family:${family};color:${layer.color || '#111827'};font-size:${fontPx}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};padding:${padPx}px;box-sizing:border-box;line-height:1.2;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;`;
|
||||
el.innerHTML = layer.html || '';
|
||||
}
|
||||
|
||||
const PREVIEW_BASE_PX = 720;
|
||||
|
||||
function fitMiniPreview(wrap) {
|
||||
const preview = wrap?.firstElementChild;
|
||||
if (!preview) return;
|
||||
const naturalW = parseFloat(preview.style.width) || preview.offsetWidth || 0;
|
||||
const naturalH = parseFloat(preview.style.height) || preview.offsetHeight || 0;
|
||||
if (!naturalW || !naturalH) return;
|
||||
let attempts = 0;
|
||||
const apply = () => {
|
||||
const wrapW = wrap.clientWidth;
|
||||
const wrapH = wrap.clientHeight;
|
||||
if (!wrapW || !wrapH) {
|
||||
if (attempts++ < 10) requestAnimationFrame(apply);
|
||||
return;
|
||||
}
|
||||
const scale = Math.min(wrapW / naturalW, wrapH / naturalH);
|
||||
preview.style.transform = `scale(${scale})`;
|
||||
};
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
apply();
|
||||
if (window.ResizeObserver) {
|
||||
const ro = new ResizeObserver(apply);
|
||||
ro.observe(wrap);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderDiscPreviewDOM(container, payload, opts = {}) {
|
||||
const compact = !!opts.compact;
|
||||
const disc = payload.discDesign || {};
|
||||
const allLayers = payload.layers || disc.layers || [];
|
||||
const layers = allLayers.filter(l => !l.target || l.target === 'designer-disc' || l._generatedDiscLayer);
|
||||
const layers = allLayers
|
||||
.filter(l => !l.target || l.target === 'designer-disc' || l._generatedDiscLayer)
|
||||
.slice()
|
||||
.sort((a, b) => (a.z || 0) - (b.z || 0));
|
||||
|
||||
const DISC_REAL_PX = 4.625 * 96;
|
||||
const PREVIEW_PX = 380;
|
||||
const SCALE = PREVIEW_PX / DISC_REAL_PX;
|
||||
const DISC_D = 4.625;
|
||||
const HOLE_D = 0.69;
|
||||
const holePx = (HOLE_D / DISC_D) * PREVIEW_PX;
|
||||
const holePct = (HOLE_D / DISC_D) * 100;
|
||||
|
||||
const outer = document.createElement('div');
|
||||
outer.style.cssText = `position:relative;width:${PREVIEW_PX}px;height:${PREVIEW_PX}px;margin:0 auto;`;
|
||||
outer.style.cssText = `position:relative;width:${PREVIEW_PX}px;height:${PREVIEW_PX}px;margin:0 auto;${compact ? 'transform-origin:center center;' : ''}`;
|
||||
|
||||
const discWrap = document.createElement('div');
|
||||
discWrap.style.cssText = `position:absolute;inset:0;border-radius:50%;overflow:hidden;background:#111;border:1px solid rgba(17,24,39,.18);`;
|
||||
discWrap.style.cssText = `position:absolute;inset:0;border-radius:50%;overflow:hidden;background:#111;${compact ? '' : 'border:1px solid rgba(17,24,39,.18);'}`;
|
||||
|
||||
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});`;
|
||||
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;`;
|
||||
discWrap.appendChild(img);
|
||||
}
|
||||
|
||||
layers.forEach(layer => {
|
||||
layers.forEach((layer, i) => {
|
||||
const el = document.createElement('div');
|
||||
const lx = (layer.x || 0) / DISC_D * 100;
|
||||
const ly = (layer.y || 0) / DISC_D * 100;
|
||||
const lw = (layer.w || DISC_D) / DISC_D * 100;
|
||||
const lh = (layer.h || DISC_D) / DISC_D * 100;
|
||||
el.style.cssText = `position:absolute;left:${lx}%;top:${ly}%;width:${lw}%;height:${lh}%;transform:rotate(${layer.rotate||0}deg);opacity:${layer.opacity !== undefined ? layer.opacity : 1};overflow:hidden;pointer-events:none;box-sizing:border-box;`;
|
||||
const lx = (Number(layer.x) || 0) / DISC_D * 100;
|
||||
const ly = (Number(layer.y) || 0) / DISC_D * 100;
|
||||
const lw = (Number(layer.w) || DISC_D) / DISC_D * 100;
|
||||
const lh = (Number(layer.h) || DISC_D) / DISC_D * 100;
|
||||
// z-index from the sorted index so each layer sits cleanly above the
|
||||
// last (and above the disc bg image at z:1). Hole is at z:12.
|
||||
const zi = 3 + i;
|
||||
el.style.cssText = `position:absolute;left:${lx}%;top:${ly}%;width:${lw}%;height:${lh}%;transform:rotate(${Number(layer.rotate)||0}deg);opacity:${layer.opacity !== undefined ? layer.opacity : 1};overflow:hidden;pointer-events:none;box-sizing:border-box;z-index:${zi};`;
|
||||
|
||||
if (layer.type === 'text') {
|
||||
const fontPx = (layer.fontSize || 14) * (96/72) * SCALE;
|
||||
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color||'#fff'};font-size:${fontPx}px;font-weight:${layer.bold?'bold':'normal'};font-style:${layer.italic?'italic':'normal'};text-align:${layer.align||'left'};display:flex;align-items:flex-start;`;
|
||||
el.innerHTML = layer.html || '';
|
||||
applyPreviewLayerTextStyle(el, layer, PREVIEW_PX / DISC_D);
|
||||
} else if (layer.type === 'color') {
|
||||
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
|
||||
} else if (layer.type === 'image' && layer.src) {
|
||||
const img = document.createElement('img');
|
||||
img.src = layer.src;
|
||||
img.style.cssText = 'width:100%;height:100%;object-fit:contain;';
|
||||
img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
|
||||
img.addEventListener('error', () => img.remove());
|
||||
el.appendChild(img);
|
||||
}
|
||||
discWrap.appendChild(el);
|
||||
});
|
||||
|
||||
const hole = document.createElement('div');
|
||||
hole.style.cssText = `position:absolute;left:50%;top:50%;width:${holePx}px;height:${holePx}px;border-radius:50%;background:#fff;border:1px solid rgba(17,24,39,.28);transform:translate(-50%,-50%);z-index:12;pointer-events:none;`;
|
||||
hole.style.cssText = `position:absolute;left:50%;top:50%;width:${holePct}%;height:${holePct}%;border-radius:50%;background:#fff;${compact ? '' : 'border:1px solid rgba(17,24,39,.28);'}transform:translate(-50%,-50%);z-index:12;pointer-events:none;`;
|
||||
discWrap.appendChild(hole);
|
||||
|
||||
outer.appendChild(discWrap);
|
||||
@@ -8069,7 +8143,8 @@
|
||||
|
||||
}
|
||||
|
||||
function renderTemplatePreviewDOM(container, payload) {
|
||||
function renderTemplatePreviewDOM(container, payload, opts = {}) {
|
||||
const compact = !!opts.compact;
|
||||
const ct = payload.coverTemplate || {};
|
||||
const slots = ct.slots || payload.slots || {};
|
||||
const preset = presets[ct.preset] || presets.dvd;
|
||||
@@ -8082,15 +8157,16 @@
|
||||
const combined = ct.combined || payload.combined || {};
|
||||
|
||||
const preview = document.createElement('div');
|
||||
const _coverNaturalH = PREVIEW_BASE_PX * trimH / totalW;
|
||||
preview.style.cssText = [
|
||||
'position:relative',
|
||||
'width:min(100%, 720px)',
|
||||
`aspect-ratio:${totalW} / ${trimH}`,
|
||||
'margin:0 auto',
|
||||
compact ? `width:${PREVIEW_BASE_PX}px` : `width:min(100%, ${PREVIEW_BASE_PX}px)`,
|
||||
compact ? `height:${_coverNaturalH}px` : `aspect-ratio:${totalW} / ${trimH}`,
|
||||
compact ? 'margin:0;transform-origin:center center' : 'margin:0 auto',
|
||||
'background:#fff',
|
||||
'border:1px solid var(--line)',
|
||||
compact ? 'border:0' : 'border:1px solid var(--line)',
|
||||
'overflow:hidden',
|
||||
'box-shadow:0 2px 10px rgba(15,23,42,.12)'
|
||||
compact ? 'box-shadow:none' : 'box-shadow:0 2px 10px rgba(15,23,42,.12)'
|
||||
].join(';');
|
||||
|
||||
const renderPreviewArt = (wrap, art) => {
|
||||
@@ -8190,7 +8266,7 @@
|
||||
addRegion('Spine', panelW, spineW, slots.spine || {});
|
||||
addRegion('Front cover', panelW + spineW, panelW, slots.front || {});
|
||||
|
||||
const labels = [
|
||||
const labels = compact ? [] : [
|
||||
['BACK', 0, panelW],
|
||||
['SPINE', panelW, spineW],
|
||||
['FRONT', panelW + spineW, panelW]
|
||||
@@ -8249,10 +8325,7 @@
|
||||
'z-index:4'
|
||||
].join(';');
|
||||
if (layer.type === 'text') {
|
||||
const scale = preview.clientWidth ? (preview.clientWidth / (totalW * 96)) : 1;
|
||||
const align = layer.align || 'left';
|
||||
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
|
||||
el.innerHTML = layer.html || '';
|
||||
applyPreviewLayerTextStyle(el, layer, PREVIEW_BASE_PX / totalW);
|
||||
} else if (layer.type === 'color') {
|
||||
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
|
||||
} else if (layer.type === 'image' && layer.src) {
|
||||
@@ -8268,11 +8341,8 @@
|
||||
|
||||
coverLayers.forEach(layer => renderCoverLayer(preview, layer));
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.appendChild(preview);
|
||||
|
||||
const hasArtwork = (isCombined && combined.src) || ['back','spine','front'].some(key => slots[key]?.src) || coverLayers.length > 0;
|
||||
if (!hasArtwork) {
|
||||
if (!hasArtwork && !compact) {
|
||||
if (payload.preview) {
|
||||
const img = document.createElement('img');
|
||||
img.src = payload.preview;
|
||||
@@ -8287,10 +8357,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
container.appendChild(row);
|
||||
container.appendChild(preview);
|
||||
}
|
||||
|
||||
function renderDiscSheetPreviewDOM(container, payload) {
|
||||
function renderDiscSheetPreviewDOM(container, payload, opts = {}) {
|
||||
const compact = !!opts.compact;
|
||||
const template = payload.discTemplate || {};
|
||||
const slots = template.discs || payload.discs || {};
|
||||
const meta = template.meta || payload.discMeta || {};
|
||||
@@ -8328,64 +8399,18 @@
|
||||
].join(';');
|
||||
|
||||
if (layer.type === 'text') {
|
||||
const scale = wrap.clientWidth && layerScale ? (wrap.clientWidth / (layerScale * 96)) : 1;
|
||||
const align = layer.align || 'left';
|
||||
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
|
||||
el.innerHTML = layer.html || '';
|
||||
// px/in within this disc wrap = preview_width(720)/sheetW (sheet's
|
||||
// CSS px per real inch, same scale everywhere on the sheet).
|
||||
applyPreviewLayerTextStyle(el, layer, PREVIEW_BASE_PX / sheetW);
|
||||
} else if (layer.type === 'color') {
|
||||
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
|
||||
} else if (layer.type === 'image' && layer.src) {
|
||||
const img = document.createElement('img');
|
||||
img.src = layer.src;
|
||||
img.alt = layer.name || '';
|
||||
img.style.cssText = 'position:absolute;left:50%;top:50%;transform-origin:center center;user-select:none;pointer-events:none;display:block;';
|
||||
img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
|
||||
img.addEventListener('error', () => img.remove());
|
||||
el.appendChild(img);
|
||||
|
||||
const update = () => {
|
||||
const cW = el.clientWidth;
|
||||
const cH = el.clientHeight;
|
||||
if (!cW || !cH) return;
|
||||
const fit = layer.fit || 'contain';
|
||||
const nW = img.naturalWidth || cW;
|
||||
const nH = img.naturalHeight || cH;
|
||||
let baseW = cW;
|
||||
let baseH = cH;
|
||||
if (fit === 'fill') {
|
||||
baseW = cW;
|
||||
baseH = cH;
|
||||
} else if (fit === 'none') {
|
||||
baseW = nW;
|
||||
baseH = nH;
|
||||
} else {
|
||||
const fitScale = fit === 'contain'
|
||||
? Math.min(cW / nW, cH / nH)
|
||||
: Math.max(cW / nW, cH / nH);
|
||||
baseW = nW * fitScale;
|
||||
baseH = nH * fitScale;
|
||||
}
|
||||
const rotate = Number(layer.rotate) || 0;
|
||||
const zoom = Number(layer.zoom) || 1;
|
||||
const rad = rotate * Math.PI / 180;
|
||||
const ac = Math.abs(Math.cos(rad));
|
||||
const as = Math.abs(Math.sin(rad));
|
||||
const rotatedW = (baseW * ac) + (baseH * as);
|
||||
const 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 panRangeX = Math.max(0, (rotatedW * finalScale - cW) / 2);
|
||||
const panRangeY = Math.max(0, (rotatedH * finalScale - cH) / 2);
|
||||
const xPx = ((Number(layer.x) || 0) / 100) * panRangeX;
|
||||
const yPx = ((Number(layer.y) || 0) / 100) * panRangeY;
|
||||
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(update);
|
||||
img.addEventListener('load', update, { once: true });
|
||||
window.addEventListener('resize', update, { once: true });
|
||||
}
|
||||
|
||||
wrap.appendChild(el);
|
||||
@@ -8453,15 +8478,16 @@
|
||||
};
|
||||
|
||||
const sheet = document.createElement('div');
|
||||
const _sheetNaturalH = PREVIEW_BASE_PX * sheetH / sheetW;
|
||||
sheet.style.cssText = [
|
||||
'position:relative',
|
||||
'width:min(100%, 720px)',
|
||||
`aspect-ratio:${sheetW} / ${sheetH}`,
|
||||
'margin:0 auto',
|
||||
compact ? `width:${PREVIEW_BASE_PX}px` : `width:min(100%, ${PREVIEW_BASE_PX}px)`,
|
||||
compact ? `height:${_sheetNaturalH}px` : `aspect-ratio:${sheetW} / ${sheetH}`,
|
||||
compact ? 'margin:0;transform-origin:center center' : 'margin:0 auto',
|
||||
'background:#fff',
|
||||
'border:1px solid var(--line)',
|
||||
compact ? 'border:0' : 'border:1px solid var(--line)',
|
||||
'overflow:hidden',
|
||||
'box-shadow:0 2px 10px rgba(15,23,42,.12)'
|
||||
compact ? 'box-shadow:none' : 'box-shadow:0 2px 10px rgba(15,23,42,.12)'
|
||||
].join(';');
|
||||
|
||||
const addDisc = (slotKey) => {
|
||||
@@ -8500,19 +8526,21 @@
|
||||
].join(';');
|
||||
wrap.appendChild(hole);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = [
|
||||
'position:absolute',
|
||||
'left:50%',
|
||||
'bottom:-1.25rem',
|
||||
'transform:translateX(-50%)',
|
||||
'font-size:.72rem',
|
||||
'font-weight:700',
|
||||
'color:#64748b',
|
||||
'white-space:nowrap'
|
||||
].join(';');
|
||||
label.textContent = meta?.[slotKey]?.title || (slotKey === 'top' ? 'Disk 1' : 'Disk 2');
|
||||
wrap.appendChild(label);
|
||||
if (!compact) {
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = [
|
||||
'position:absolute',
|
||||
'left:50%',
|
||||
'bottom:-1.25rem',
|
||||
'transform:translateX(-50%)',
|
||||
'font-size:.72rem',
|
||||
'font-weight:700',
|
||||
'color:#64748b',
|
||||
'white-space:nowrap'
|
||||
].join(';');
|
||||
label.textContent = meta?.[slotKey]?.title || (slotKey === 'top' ? 'Disk 1' : 'Disk 2');
|
||||
wrap.appendChild(label);
|
||||
}
|
||||
|
||||
sheet.appendChild(wrap);
|
||||
};
|
||||
@@ -8520,26 +8548,29 @@
|
||||
addDisc('top');
|
||||
addDisc('bottom');
|
||||
|
||||
const sheetLabel = document.createElement('div');
|
||||
sheetLabel.style.cssText = [
|
||||
'position:absolute',
|
||||
'left:0',
|
||||
'right:0',
|
||||
'top:8px',
|
||||
'text-align:center',
|
||||
'font-size:.72rem',
|
||||
'font-weight:700',
|
||||
'color:#94a3b8',
|
||||
'pointer-events:none',
|
||||
'z-index:3'
|
||||
].join(';');
|
||||
sheetLabel.textContent = 'Disc sheet';
|
||||
sheet.appendChild(sheetLabel);
|
||||
if (!compact) {
|
||||
const sheetLabel = document.createElement('div');
|
||||
sheetLabel.style.cssText = [
|
||||
'position:absolute',
|
||||
'left:0',
|
||||
'right:0',
|
||||
'top:8px',
|
||||
'text-align:center',
|
||||
'font-size:.72rem',
|
||||
'font-weight:700',
|
||||
'color:#94a3b8',
|
||||
'pointer-events:none',
|
||||
'z-index:3'
|
||||
].join(';');
|
||||
sheetLabel.textContent = 'Disc sheet';
|
||||
sheet.appendChild(sheetLabel);
|
||||
}
|
||||
|
||||
container.appendChild(sheet);
|
||||
}
|
||||
|
||||
function renderJewelPreviewDOM(container, payload) {
|
||||
function renderJewelPreviewDOM(container, payload, opts = {}) {
|
||||
const compact = !!opts.compact;
|
||||
const template = payload.jewelTemplate || {};
|
||||
const inserts = template.inserts || payload.jewelInserts || {};
|
||||
const spineLabels = template.spineLabels || payload.jewelSpineLabels || {};
|
||||
@@ -8602,10 +8633,8 @@
|
||||
const lh = (Number(layer.h) || slotH) / slotH * 100;
|
||||
el.style.cssText = `position:absolute;left:${lx}%;top:${ly}%;width:${lw}%;height:${lh}%;transform:rotate(${Number(layer.rotate)||0}deg);opacity:${layer.opacity !== undefined ? layer.opacity : 1};overflow:hidden;pointer-events:none;box-sizing:border-box;z-index:3;`;
|
||||
if (layer.type === 'text') {
|
||||
const scale = wrap.clientWidth ? (wrap.clientWidth / (slotW * 96)) : 1;
|
||||
const align = layer.align || 'left';
|
||||
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#111'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
|
||||
el.innerHTML = layer.html || '';
|
||||
// px/in within this insert = sheet_width(720)/sheetW (uniform across sheet).
|
||||
applyPreviewLayerTextStyle(el, layer, PREVIEW_BASE_PX / sheetW);
|
||||
} else if (layer.type === 'color') {
|
||||
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
|
||||
} else if (layer.type === 'image' && layer.src) {
|
||||
@@ -8613,13 +8642,17 @@
|
||||
img.src = layer.src;
|
||||
img.alt = layer.name || '';
|
||||
img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
|
||||
img.addEventListener('error', () => img.remove());
|
||||
el.appendChild(img);
|
||||
}
|
||||
wrap.appendChild(el);
|
||||
};
|
||||
|
||||
const sheet = document.createElement('div');
|
||||
sheet.style.cssText = `position:relative;width:min(100%, 720px);aspect-ratio:${sheetW} / ${sheetH};margin:0 auto;background:#fff;border:1px solid var(--line);overflow:hidden;box-shadow:0 2px 10px rgba(15,23,42,.12);`;
|
||||
const _jewelNaturalH = PREVIEW_BASE_PX * sheetH / sheetW;
|
||||
sheet.style.cssText = compact
|
||||
? `position:relative;width:${PREVIEW_BASE_PX}px;height:${_jewelNaturalH}px;margin:0;transform-origin:center center;background:#fff;border:0;overflow:hidden;box-shadow:none;`
|
||||
: `position:relative;width:min(100%, ${PREVIEW_BASE_PX}px);aspect-ratio:${sheetW} / ${sheetH};margin:0 auto;background:#fff;border:1px solid var(--line);overflow:hidden;box-shadow:0 2px 10px rgba(15,23,42,.12);`;
|
||||
|
||||
slots.forEach((slotDef) => {
|
||||
const insert = inserts[slotDef.key] || {};
|
||||
@@ -8652,17 +8685,21 @@
|
||||
buildSpine('right', spineLabels.right || '');
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = `position:absolute;left:50%;bottom:-1.25rem;transform:translateX(-50%);font-size:.72rem;font-weight:700;color:#64748b;white-space:nowrap;`;
|
||||
label.textContent = slotDef.label || slotDef.key;
|
||||
wrap.appendChild(label);
|
||||
if (!compact) {
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = `position:absolute;left:50%;bottom:-1.25rem;transform:translateX(-50%);font-size:.72rem;font-weight:700;color:#64748b;white-space:nowrap;`;
|
||||
label.textContent = slotDef.label || slotDef.key;
|
||||
wrap.appendChild(label);
|
||||
}
|
||||
sheet.appendChild(wrap);
|
||||
});
|
||||
|
||||
const sheetLabel = document.createElement('div');
|
||||
sheetLabel.style.cssText = `position:absolute;left:0;right:0;top:8px;text-align:center;font-size:.72rem;font-weight:700;color:#94a3b8;pointer-events:none;z-index:3;`;
|
||||
sheetLabel.textContent = 'Jewel case (front + tray with spines)';
|
||||
sheet.appendChild(sheetLabel);
|
||||
if (!compact) {
|
||||
const sheetLabel = document.createElement('div');
|
||||
sheetLabel.style.cssText = `position:absolute;left:0;right:0;top:8px;text-align:center;font-size:.72rem;font-weight:700;color:#94a3b8;pointer-events:none;z-index:3;`;
|
||||
sheetLabel.textContent = 'Jewel case (front + tray with spines)';
|
||||
sheet.appendChild(sheetLabel);
|
||||
}
|
||||
|
||||
container.appendChild(sheet);
|
||||
}
|
||||
@@ -9377,6 +9414,8 @@
|
||||
});
|
||||
$('welcomeSearchBtn').addEventListener('click', runWelcomeSearch);
|
||||
$('welcomeSkipBtn').addEventListener('click', dismissWelcomeModal);
|
||||
const welcomeCloseBtn = $('welcomeCloseBtn');
|
||||
if (welcomeCloseBtn) welcomeCloseBtn.addEventListener('click', dismissWelcomeModal);
|
||||
$('welcomeBrowseAllBtn').addEventListener('click', openWelcomeBrowseAll);
|
||||
$('welcomeTabNew').addEventListener('click', () => loadWelcomeTab('New'));
|
||||
$('welcomeTabPopular').addEventListener('click', () => loadWelcomeTab('Popular'));
|
||||
|
||||
Reference in new issue
Block a user