diff --git a/src/index.html b/src/index.html index 1f61f25..697ce15 100644 --- a/src/index.html +++ b/src/index.html @@ -12638,33 +12638,96 @@ `).join(''); - // Spread preview. Scale to fit in ~720 px wide; min 200 px. + // Spread preview. Paint the actual cover layers into a single + // composite canvas the size of the full wrap-around spread, + // overlaid with subtle panel labels + fold-line dividers + the + // red trim guide. Layers are stored in panel-local space + // (book-cover-back / book-spine / book-cover-front, each at + // x=0 = left edge of that panel's trim area), so we remap each + // layer's target to 'canvas' and add the panel's offset within + // the spread + the outer bleed. const previewMaxPx = 720; const pxPerIn = totalW > 0 ? Math.min(previewMaxPx / totalW, 60) : 60; - const heightPx = Math.max(40, totalH * pxPerIn); + const renderWidth = Math.max(40, Math.round(totalW * pxPerIn)); wrap.innerHTML = ''; - const addPanel = (cls, label, widthIn, dim) => { - const panel = document.createElement('div'); - panel.className = `bookCoverPanel ${cls}`; - panel.style.width = `${widthIn * pxPerIn}px`; - panel.style.height = `${heightPx}px`; - const lab = document.createElement('div'); - lab.className = 'bookCoverPanelLabel'; - lab.textContent = label; - panel.appendChild(lab); - if (dim) { - const dimEl = document.createElement('div'); - dimEl.className = 'bookCoverPanelDim'; - dimEl.textContent = dim; - panel.appendChild(dimEl); - } - wrap.appendChild(panel); - }; + const backX = bleed; + const spineX = bleed + (hasBack ? trimW : 0); + const frontX = bleed + (hasBack ? trimW : 0) + (hasSpine ? spineW : 0); + const panelY = bleed; - if (hasBack) addPanel('back', 'Back', trimW + bleed, `${fmt(trimW)} in`); - if (hasSpine && spineW > 0) addPanel('spine', 'Spine', spineW, `${fmt(spineW)} in`); - addPanel('front', 'Front', trimW + bleed, `${fmt(trimW)} in`); + const compositeLayers = []; + for (const l of (state.layers || [])) { + if (l.hidden) continue; + const t = String(l.target || ''); + let xo, yo; + if (t === 'book-cover-back' && hasBack) { xo = backX; yo = panelY; } + else if (t === 'book-spine' && hasSpine) { xo = spineX; yo = panelY; } + else if (t === 'book-cover-front') { xo = frontX; yo = panelY; } + else continue; + compositeLayers.push({ + ...l, + target: 'canvas', + x: (Number(l.x) || 0) + xo, + y: (Number(l.y) || 0) + yo, + }); + } + compositeLayers.sort((a, b) => (a.z || 0) - (b.z || 0)); + + const container = document.createElement('div'); + container.style.cssText = 'position:relative;display:inline-block;border:1px solid var(--line);box-shadow:0 1px 3px rgba(0,0,0,.08);background:#fff'; + try { + renderCustomArtPreviewDOM(container, { + customArt: { + width: Math.round(totalW * 96), + height: Math.round(totalH * 96), + background: book.cover?.background || s.background || '#ffffff', + }, + layers: compositeLayers, + }, { compact: true, renderWidth }); + } catch (err) { + console.warn('Cover preview paint failed:', err); + } + + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:absolute;inset:0;pointer-events:none'; + + // Trim guide — dashed red rectangle around the printable trim. + const trimGuide = document.createElement('div'); + trimGuide.style.cssText = `position:absolute;left:${(bleed / totalW) * 100}%;top:${(bleed / totalH) * 100}%;width:${((totalW - 2 * bleed) / totalW) * 100}%;height:${((totalH - 2 * bleed) / totalH) * 100}%;border:1px dashed rgba(220,38,38,.55);box-sizing:border-box`; + overlay.appendChild(trimGuide); + + // Vertical fold lines at each visible panel boundary. + const addFold = (xIn) => { + const fold = document.createElement('div'); + fold.style.cssText = `position:absolute;top:${(bleed / totalH) * 100}%;height:${((totalH - 2 * bleed) / totalH) * 100}%;width:0;border-left:1px dashed rgba(15,23,42,.35);left:${(xIn / totalW) * 100}%`; + overlay.appendChild(fold); + }; + if (hasBack) addFold(spineX); // back / spine boundary + if (hasSpine) addFold(spineX + spineW); // spine / front boundary + + // Panel labels — bottom of each panel. + const labelTopPct = ((bleed + trimH) / totalH) * 100 - 4; + const addLabel = (text, centerXIn) => { + const tag = document.createElement('div'); + tag.textContent = text; + tag.style.cssText = `position:absolute;left:${(centerXIn / totalW) * 100}%;top:${labelTopPct}%;transform:translate(-50%, -50%);background:rgba(255,255,255,.85);color:var(--muted);font-size:.58rem;letter-spacing:.08em;text-transform:uppercase;padding:2px 6px;border-radius:3px;font-family:inherit`; + overlay.appendChild(tag); + }; + if (hasBack) addLabel('Back', backX + trimW / 2); + if (hasSpine && spineW > 0) addLabel('Spine', spineX + spineW / 2); + addLabel('Front', frontX + trimW / 2); + + // Empty-state hint, centered, when no cover layers exist yet. + if (!compositeLayers.length) { + const empty = document.createElement('div'); + empty.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:.85rem;text-align:center;padding:0 16px'; + empty.textContent = 'No cover artwork yet — click Edit cover to design the spread.'; + overlay.appendChild(empty); + } + + container.appendChild(overlay); + wrap.appendChild(container); // Ref status hint — published / unpublished / publication target. const refHint = document.getElementById('bookCoverRefHint'); @@ -14288,6 +14351,20 @@ document.querySelector('.printHelp').textContent = 'Paint a pixel-art sprite — publishes as its own design.'; $('trimReadout').textContent = `${pa.width} × ${pa.height} px`; $('sheetReadout').textContent = 'Indexed pixel grid'; + } else if (state.templateMode === 'book') { + const spec = state.book?.spec || {}; + const tW = Number(spec.trimW) || 0; + const tH = Number(spec.trimH) || 0; + const pages = bookPageCount(); + const editing = !!state.book?.editingCover; + $('projectTitle').textContent = editing ? 'Book cover spread' : 'Book'; + document.querySelector('.printHelp').textContent = editing + ? `Back · spine · front laid out for ${spec.bookSize || `${fmt(tW)} × ${fmt(tH)} in`}.` + : `${spec.bookSize || `${fmt(tW)} × ${fmt(tH)} in`} · ${pages} page${pages === 1 ? '' : 's'}.`; + $('trimReadout').textContent = `Trim: ${fmt(tW)} × ${fmt(tH)} in`; + $('sheetReadout').textContent = editing + ? `Spread: ${fmt(sheetW())} × ${fmt(sheetH())} in (with bleed)` + : `Page: ${fmt(sheetW())} × ${fmt(sheetH())} in (with bleed)`; } else if (state.templateMode === 'jewel') { $('projectTitle').textContent = 'CD jewel insert preview'; document.querySelector('.printHelp').textContent = 'Avery 8693 / 8943 layout: front and tray inserts on Letter paper.';