mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-04 20:50:39 +00:00
update
This commit is contained in:
1 parent
88cf2bceab
commit
f61ab7729f
1 file changed
+423
-82
+423
-82
@@ -1506,6 +1506,7 @@
|
||||
.feedTrustToggle select{font-size:.75rem;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:4px;padding:3px 6px;cursor:pointer}
|
||||
body.dark .feedTrustToggle select{background:#0b1220}
|
||||
body.nostr-logged-in .feedTrustToggle{display:flex}
|
||||
.layerLockTag{display:inline-block;font-size:.58rem;font-weight:800;letter-spacing:.06em;color:var(--muted);background:rgba(127,127,127,.15);border:1px solid var(--line);border-radius:3px;padding:0 4px;margin-right:6px;vertical-align:1px}
|
||||
.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,
|
||||
@@ -1540,7 +1541,43 @@
|
||||
.previewShell { min-height: 420px; }
|
||||
}
|
||||
|
||||
body.exporting .slot,
|
||||
body.exporting .discSlot,
|
||||
body.exporting .jewelSlot {
|
||||
border-color: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
body.exporting .placeholder,
|
||||
body.exporting .slot::after, body.exporting .slot::before,
|
||||
body.exporting .discSlot::before,
|
||||
body.exporting .jewelSlot::before, body.exporting .jewelSlot::after,
|
||||
body.exporting .jewelSpineGuide,
|
||||
body.exporting .foldLine, body.exporting .trimArea, body.exporting .bleedFill,
|
||||
body.exporting .spineText, body.exporting .discSpineStrip {
|
||||
display: none !important;
|
||||
}
|
||||
body.exporting .freeLayer.selected {
|
||||
outline: none !important;
|
||||
}
|
||||
body.exporting .freeLayer {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
body.exporting .sheet,
|
||||
body.exporting .sheet *,
|
||||
body.exporting .designerDiscWrap,
|
||||
body.exporting .designerDiscWrap * {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
color-adjust: exact !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
html, body, .sheet, .sheet *, .freeLayer, .freeLayer * {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
color-adjust: exact !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: var(--sheet-w) !important;
|
||||
height: var(--sheet-h) !important;
|
||||
@@ -1566,16 +1603,21 @@
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
html:has(body.template-cover) {
|
||||
width: 11in !important;
|
||||
height: 8.5in !important;
|
||||
}
|
||||
|
||||
body.template-cover {
|
||||
width: 8.5in !important;
|
||||
height: 11in !important;
|
||||
width: 11in !important;
|
||||
height: 8.5in !important;
|
||||
}
|
||||
|
||||
body.template-cover .app,
|
||||
body.template-cover main,
|
||||
body.template-cover .previewShell {
|
||||
width: 8.5in !important;
|
||||
height: 11in !important;
|
||||
width: 11in !important;
|
||||
height: 8.5in !important;
|
||||
}
|
||||
|
||||
body.template-cover .previewShell {
|
||||
@@ -1602,8 +1644,8 @@
|
||||
|
||||
body.template-cover .sheetWrap {
|
||||
position: relative !important;
|
||||
width: var(--sheet-h) !important;
|
||||
height: var(--sheet-w) !important;
|
||||
width: var(--sheet-w) !important;
|
||||
height: var(--sheet-h) !important;
|
||||
margin: 0 auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
@@ -1619,13 +1661,13 @@
|
||||
}
|
||||
|
||||
body.template-cover .sheet {
|
||||
position: absolute !important;
|
||||
position: fixed !important;
|
||||
left: 50% !important;
|
||||
top: 50% !important;
|
||||
width: var(--sheet-w) !important;
|
||||
height: var(--sheet-h) !important;
|
||||
transform: translate(-50%, -50%) rotate(90deg) !important;
|
||||
transform-origin: center center !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.slot, .discSlot, .jewelSlot {
|
||||
@@ -1845,6 +1887,7 @@
|
||||
<legend data-step="6">Print & Save</legend>
|
||||
<div class="buttonRow">
|
||||
<button class="primary" id="printBtn">Print / Save PDF</button>
|
||||
<button id="exportJpegBtn">Export JPEG</button>
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="saveProjectBtn">Save project (JSON)</button>
|
||||
@@ -2100,6 +2143,12 @@
|
||||
</div>
|
||||
|
||||
<div id="textLayerControls">
|
||||
<label for="textLayerContent">Text</label>
|
||||
<textarea id="textLayerContent" rows="2" placeholder="Type your text here…" style="resize:vertical;width:100%"></textarea>
|
||||
|
||||
<label for="textFontFamily">Font</label>
|
||||
<select id="textFontFamily"></select>
|
||||
|
||||
<label for="textFontSize">Text size, pt</label>
|
||||
<input id="textFontSize" type="number" min="4" max="200" step="1" />
|
||||
|
||||
@@ -2119,9 +2168,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="colorLayerControls" style="display:none">
|
||||
<label for="colorLayerFill">Fill color</label>
|
||||
<input id="colorLayerFill" type="color" />
|
||||
</div>
|
||||
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;cursor:pointer;font-size:.84rem">
|
||||
<input type="checkbox" id="layerLocked" style="accent-color:var(--accent)" />
|
||||
Lock layer (prevent dragging on canvas)
|
||||
</label>
|
||||
|
||||
<div class="buttonRow">
|
||||
<button type="button" id="layerForward">Move forward</button>
|
||||
<button type="button" id="layerBackward">Move back</button>
|
||||
<button type="button" id="layerDuplicate">Duplicate</button>
|
||||
<button type="button" class="danger" id="deleteLayer">Delete layer</button>
|
||||
</div>
|
||||
</details>
|
||||
@@ -2159,6 +2219,7 @@
|
||||
<legend data-step="4">Save & Export</legend>
|
||||
<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>
|
||||
</div>
|
||||
<details>
|
||||
@@ -2503,6 +2564,26 @@
|
||||
const SCHEMA_VERSION = 5;
|
||||
const MIN_SUPPORTED_SCHEMA_VERSION = 4;
|
||||
|
||||
const COLOR_LAYER_DEFAULT_FILL = '#3b82f6';
|
||||
const COLOR_LAYER_DEFAULT_OPACITY = 0.6;
|
||||
const WEB_SAFE_FONTS = [
|
||||
{ label: 'System default', value: 'inherit' },
|
||||
{ label: 'Sans · Arial', value: 'Arial, Helvetica, sans-serif' },
|
||||
{ label: 'Sans · Helvetica', value: 'Helvetica, Arial, sans-serif' },
|
||||
{ label: 'Sans · Verdana', value: 'Verdana, Geneva, sans-serif' },
|
||||
{ label: 'Sans · Trebuchet', value: '"Trebuchet MS", sans-serif' },
|
||||
{ label: 'Sans · Tahoma', value: 'Tahoma, Geneva, sans-serif' },
|
||||
{ label: 'Sans · Arial Black', value: '"Arial Black", sans-serif' },
|
||||
{ label: 'Sans · Impact', value: 'Impact, "Arial Narrow Bold", sans-serif' },
|
||||
{ label: 'Serif · Times', value: '"Times New Roman", Times, serif' },
|
||||
{ label: 'Serif · Georgia', value: 'Georgia, serif' },
|
||||
{ label: 'Serif · Palatino', value: '"Palatino Linotype", "Book Antiqua", Palatino, serif' },
|
||||
{ label: 'Mono · Courier', value: '"Courier New", Courier, monospace' },
|
||||
{ label: 'Mono · Consolas', value: 'Consolas, "Courier New", monospace' },
|
||||
{ label: 'Cursive · Comic Sans', value: '"Comic Sans MS", "Comic Sans", cursive' },
|
||||
{ label: 'Cursive · Brush Script', value: '"Brush Script MT", cursive' }
|
||||
];
|
||||
|
||||
const presets = {
|
||||
dvd: {
|
||||
name: 'DVD case wrap',
|
||||
@@ -3334,25 +3415,25 @@
|
||||
function preferredDiscProjectTitle() {
|
||||
const topTitle = state.discMeta?.top?.title?.trim();
|
||||
const bottomTitle = state.discMeta?.bottom?.title?.trim();
|
||||
return topTitle || bottomTitle || '';
|
||||
return topTitle || bottomTitle || (state.meta?.title || '');
|
||||
}
|
||||
|
||||
function preferredDiscProjectImdb() {
|
||||
const topImdb = state.discMeta?.top?.imdbId?.trim();
|
||||
const bottomImdb = state.discMeta?.bottom?.imdbId?.trim();
|
||||
return topImdb || bottomImdb || '';
|
||||
return topImdb || bottomImdb || (state.meta?.imdbId || '');
|
||||
}
|
||||
|
||||
function preferredDiscProjectCategory() {
|
||||
const topCategory = state.discMeta?.top?.category?.trim();
|
||||
const bottomCategory = state.discMeta?.bottom?.category?.trim();
|
||||
return topCategory || bottomCategory || '';
|
||||
return topCategory || bottomCategory || (state.meta?.category || '');
|
||||
}
|
||||
|
||||
function preferredDiscProjectLanguage() {
|
||||
const topLanguage = state.discMeta?.top?.language?.trim();
|
||||
const bottomLanguage = state.discMeta?.bottom?.language?.trim();
|
||||
return topLanguage || bottomLanguage || 'en';
|
||||
return topLanguage || bottomLanguage || (state.meta?.language || 'en');
|
||||
}
|
||||
|
||||
function syncProjectMetaFromDiscMeta(force = false) {
|
||||
@@ -3517,8 +3598,8 @@
|
||||
root.style.setProperty('--bleed', inches(state.bleed));
|
||||
root.style.setProperty('--safe', inches(state.safe));
|
||||
|
||||
const pageW = state.templateMode === 'cover' ? 8.5 : sheetW();
|
||||
const pageH = state.templateMode === 'cover' ? 11 : sheetH();
|
||||
const pageW = state.templateMode === 'cover' ? 11 : sheetW();
|
||||
const pageH = state.templateMode === 'cover' ? 8.5 : sheetH();
|
||||
$('dynamicPageStyle').textContent = `@page { size: ${fmt(pageW)}in ${fmt(pageH)}in; margin: 0; }`;
|
||||
|
||||
if (state.templateMode === 'disc') {
|
||||
@@ -4477,6 +4558,7 @@
|
||||
rotate: 0,
|
||||
opacity: 1,
|
||||
z: nextLayerZ(),
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 24,
|
||||
color: '#111827',
|
||||
align: 'center',
|
||||
@@ -4485,6 +4567,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
function addColorLayer(targetOverride = '') {
|
||||
const target = targetOverride || $('layerTarget')?.value || 'page';
|
||||
const bounds = targetBounds(target);
|
||||
applyTargetMode(target);
|
||||
addLayer({
|
||||
id: makeLayerId(),
|
||||
type: 'color',
|
||||
name: 'Color block',
|
||||
target,
|
||||
color: COLOR_LAYER_DEFAULT_FILL,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
w: bounds.w,
|
||||
h: bounds.h,
|
||||
rotate: 0,
|
||||
opacity: COLOR_LAYER_DEFAULT_OPACITY,
|
||||
locked: true,
|
||||
z: nextLayerZ()
|
||||
});
|
||||
}
|
||||
|
||||
function renderLayers() {
|
||||
const overlay = $('layerOverlay');
|
||||
const designerOverlay = $('designerLayerOverlay');
|
||||
@@ -4515,18 +4618,25 @@
|
||||
el.style.opacity = layer.opacity;
|
||||
el.style.zIndex = layer.z || 1;
|
||||
el.style.transform = `rotate(${layer.rotate || 0}deg)`;
|
||||
if (layer.locked) {
|
||||
el.style.pointerEvents = 'none';
|
||||
el.classList.add('layerLocked');
|
||||
}
|
||||
|
||||
if (layer.type === 'image') {
|
||||
const img = document.createElement('img');
|
||||
img.src = layer.src;
|
||||
img.alt = layer.name || 'Image layer';
|
||||
el.appendChild(img);
|
||||
} else if (layer.type === 'color') {
|
||||
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
|
||||
} else {
|
||||
const text = document.createElement('div');
|
||||
text.className = 'textLayer';
|
||||
text.contentEditable = 'true';
|
||||
text.spellcheck = false;
|
||||
text.innerHTML = layer.html || '';
|
||||
text.style.fontFamily = layer.fontFamily || 'inherit';
|
||||
text.style.fontSize = `${layer.fontSize || 24}pt`;
|
||||
text.style.color = layer.color || '#111827';
|
||||
text.style.textAlign = layer.align || 'left';
|
||||
@@ -4576,9 +4686,12 @@
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = `layerItem${layer.id === state.selectedLayerId ? ' active' : ''}`;
|
||||
const rawName = layer.name || layer.type;
|
||||
const rawName = layer.type === 'text'
|
||||
? (layerHtmlToPlainText(layer.html).split('\n')[0].trim() || layer.name || 'Text layer')
|
||||
: (layer.name || layer.type);
|
||||
const displayName = rawName.length > 32 ? rawName.slice(0, 29) + '...' : rawName;
|
||||
item.innerHTML = `<span title="${rawName}">${displayName}</span><span>${layer.type}</span>`;
|
||||
const lockTag = layer.locked ? '<span class="layerLockTag" title="Locked">LOCK</span>' : '';
|
||||
item.innerHTML = `<span title="${rawName}">${lockTag}${displayName}</span><span>${layer.type}</span>`;
|
||||
item.addEventListener('click', () => {
|
||||
state.selectedLayerId = layer.id;
|
||||
render();
|
||||
@@ -4635,7 +4748,11 @@
|
||||
addTextBtn.type = 'button';
|
||||
addTextBtn.textContent = 'Add text layer';
|
||||
addTextBtn.addEventListener('click', () => addTextLayer(targetDef.key));
|
||||
buttonRow.append(addImageBtn, addTextBtn);
|
||||
const addColorBtn = document.createElement('button');
|
||||
addColorBtn.type = 'button';
|
||||
addColorBtn.textContent = 'Add color block';
|
||||
addColorBtn.addEventListener('click', () => addColorLayer(targetDef.key));
|
||||
buttonRow.append(addImageBtn, addTextBtn, addColorBtn);
|
||||
|
||||
actions.append(urlRow, buttonRow);
|
||||
panel.appendChild(actions);
|
||||
@@ -4655,9 +4772,12 @@
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = `layerItem${layer.id === state.selectedLayerId ? ' active' : ''}`;
|
||||
const rawName = layer.name || layer.type;
|
||||
const rawName = layer.type === 'text'
|
||||
? (layerHtmlToPlainText(layer.html).split('\n')[0].trim() || layer.name || 'Text layer')
|
||||
: (layer.name || layer.type);
|
||||
const displayName = rawName.length > 32 ? rawName.slice(0, 29) + '...' : rawName;
|
||||
item.innerHTML = `<span title="${rawName}">${displayName}</span><span>${layer.type}</span>`;
|
||||
const lockTag = layer.locked ? '<span class="layerLockTag" title="Locked">LOCK</span>' : '';
|
||||
item.innerHTML = `<span title="${rawName}">${lockTag}${displayName}</span><span>${layer.type}</span>`;
|
||||
item.addEventListener('click', () => {
|
||||
state.selectedLayerId = layer.id;
|
||||
render();
|
||||
@@ -5524,20 +5644,48 @@
|
||||
return getLayer(state.selectedLayerId);
|
||||
}
|
||||
|
||||
function layerHtmlToPlainText(html) {
|
||||
if (!html) return '';
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = String(html).replace(/<br\s*\/?>/gi, '\n').replace(/<\/(?:div|p)>/gi, '\n').replace(/<(?:div|p)[^>]*>/gi, '');
|
||||
return (tmp.textContent || '').replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
function layerPlainTextToHtml(text) {
|
||||
return escapeHtml(String(text || '')).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
function syncLayerInputs() {
|
||||
const layer = selectedLayer();
|
||||
$('layerControls').open = Boolean(layer);
|
||||
$('textLayerControls').style.display = layer && layer.type === 'text' ? 'block' : 'none';
|
||||
$('layerImageControls').style.display = layer && layer.type === 'image' ? 'block' : 'none';
|
||||
const colorPanel = $('colorLayerControls');
|
||||
if (colorPanel) colorPanel.style.display = layer && layer.type === 'color' ? 'block' : 'none';
|
||||
const layerNameRow = $('layerName');
|
||||
const layerNameLabel = layerNameRow?.previousElementSibling;
|
||||
const hideName = layer && layer.type === 'text';
|
||||
if (layerNameRow) layerNameRow.style.display = hideName ? 'none' : '';
|
||||
if (layerNameLabel && layerNameLabel.tagName === 'LABEL' && layerNameLabel.getAttribute('for') === 'layerName') {
|
||||
layerNameLabel.style.display = hideName ? 'none' : '';
|
||||
}
|
||||
for (const id of ['layerName', 'layerX', 'layerY', 'layerW', 'layerH', 'layerRotate', 'layerOpacity']) {
|
||||
$(id).disabled = !layer;
|
||||
}
|
||||
$('layerUrl').disabled = !layer || layer.type !== 'image';
|
||||
for (const id of ['textFontSize', 'textColor', 'textAlign', 'textBold', 'textItalic']) {
|
||||
$(id).disabled = !layer || layer.type !== 'text';
|
||||
for (const id of ['textLayerContent', 'textFontFamily', 'textFontSize', 'textColor', 'textAlign', 'textBold', 'textItalic']) {
|
||||
const el = $(id);
|
||||
if (el) el.disabled = !layer || layer.type !== 'text';
|
||||
}
|
||||
for (const id of ['layerForward', 'layerBackward', 'deleteLayer']) {
|
||||
$(id).disabled = !layer;
|
||||
const fillEl = $('colorLayerFill');
|
||||
if (fillEl) fillEl.disabled = !layer || layer.type !== 'color';
|
||||
for (const id of ['layerForward', 'layerBackward', 'layerDuplicate', 'deleteLayer']) {
|
||||
const el = $(id);
|
||||
if (el) el.disabled = !layer;
|
||||
}
|
||||
const lockEl = $('layerLocked');
|
||||
if (lockEl) {
|
||||
lockEl.disabled = !layer;
|
||||
lockEl.checked = !!(layer && layer.locked);
|
||||
}
|
||||
if (!layer) return;
|
||||
|
||||
@@ -5554,10 +5702,18 @@
|
||||
}
|
||||
|
||||
if (layer.type === 'text') {
|
||||
const content = $('textLayerContent');
|
||||
if (content && document.activeElement !== content) content.value = layerHtmlToPlainText(layer.html);
|
||||
const ff = $('textFontFamily');
|
||||
if (ff) ff.value = layer.fontFamily || 'inherit';
|
||||
$('textFontSize').value = layer.fontSize || 24;
|
||||
$('textColor').value = layer.color || '#111827';
|
||||
$('textAlign').value = layer.align || 'left';
|
||||
}
|
||||
|
||||
if (layer.type === 'color' && fillEl) {
|
||||
fillEl.value = layer.color || COLOR_LAYER_DEFAULT_FILL;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedLayer(prop, value) {
|
||||
@@ -5571,6 +5727,7 @@
|
||||
const el = event.currentTarget;
|
||||
const layer = getLayer(el.dataset.layerId);
|
||||
if (!layer) return;
|
||||
if (layer.locked) { state.selectedLayerId = layer.id; render(); return; }
|
||||
state.selectedLayerId = layer.id;
|
||||
|
||||
const textTarget = event.target.closest('.textLayer');
|
||||
@@ -5792,6 +5949,82 @@
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
let _html2canvasPromise = null;
|
||||
function loadHtml2Canvas() {
|
||||
if (window.html2canvas) return Promise.resolve(window.html2canvas);
|
||||
if (_html2canvasPromise) return _html2canvasPromise;
|
||||
_html2canvasPromise = new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js';
|
||||
s.onload = () => resolve(window.html2canvas);
|
||||
s.onerror = () => { _html2canvasPromise = null; reject(new Error('Could not load the image-export library. Check your network connection.')); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return _html2canvasPromise;
|
||||
}
|
||||
|
||||
function jpegFilenameStem() {
|
||||
const parts = [
|
||||
filenamePart(state.meta?.title) || 'design',
|
||||
filenamePart(state.meta?.category),
|
||||
state.appMode === 'designer' ? 'disc_design' : state.templateMode
|
||||
].filter(Boolean);
|
||||
return parts.join('_');
|
||||
}
|
||||
|
||||
async function exportArtboardAsJpeg(button) {
|
||||
const originalLabel = button?.textContent;
|
||||
const originalSelectedId = state.selectedLayerId;
|
||||
const sheetWrapEl = $('sheetWrap');
|
||||
const originalSheetWrapTransform = sheetWrapEl?.style.transform || '';
|
||||
const targetEl = state.appMode === 'designer' ? $('designerDiscWrap') : $('sheet');
|
||||
if (!targetEl) return;
|
||||
try {
|
||||
if (button) { button.disabled = true; button.textContent = 'Rendering…'; }
|
||||
if (originalSelectedId) { state.selectedLayerId = ''; }
|
||||
if (sheetWrapEl) sheetWrapEl.style.transform = 'none';
|
||||
document.body.classList.add('exporting');
|
||||
render();
|
||||
await new Promise(res => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(res, 100))));
|
||||
await Promise.all([...targetEl.querySelectorAll('img')].map(img => {
|
||||
if (img.complete && img.naturalHeight) return Promise.resolve();
|
||||
return new Promise(res => {
|
||||
img.addEventListener('load', () => res(), { once: true });
|
||||
img.addEventListener('error', () => res(), { once: true });
|
||||
});
|
||||
}));
|
||||
const html2canvas = await loadHtml2Canvas();
|
||||
const rect = targetEl.getBoundingClientRect();
|
||||
const scale = Math.max(2, Math.min(4, 1800 / Math.max(rect.width, rect.height)));
|
||||
const canvas = await html2canvas(targetEl, {
|
||||
scale,
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
backgroundColor: '#ffffff',
|
||||
logging: false,
|
||||
imageTimeout: 10000
|
||||
});
|
||||
const blob = await new Promise(res => canvas.toBlob(res, 'image/jpeg', 0.92));
|
||||
if (!blob) throw new Error('Could not encode JPEG. One of your images is from a server that blocks cross-origin reads — try a different image host.');
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${jpegFilenameStem()}.jpg`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch (err) {
|
||||
alert(err.message || 'Could not export JPEG.');
|
||||
} finally {
|
||||
document.body.classList.remove('exporting');
|
||||
if (sheetWrapEl) sheetWrapEl.style.transform = originalSheetWrapTransform;
|
||||
if (originalSelectedId) { state.selectedLayerId = originalSelectedId; }
|
||||
render();
|
||||
if (button) { button.disabled = false; button.textContent = originalLabel; }
|
||||
}
|
||||
}
|
||||
|
||||
function downloadProject() {
|
||||
const title = (state.meta.title || '').trim();
|
||||
const category = (state.meta.category || '').trim();
|
||||
@@ -5823,62 +6056,74 @@
|
||||
triggerDownload(payload, filename);
|
||||
}
|
||||
|
||||
function closeDiscDesignerProject() {
|
||||
const hasContent = state.discDesign?.src || (state.layers && state.layers.some(l => l.target === 'designer-disc'));
|
||||
if (!hasContent) {
|
||||
state.discDesign = { name: '', src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 };
|
||||
state.layers = state.layers.filter(l => l.target !== 'designer-disc');
|
||||
state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const action = confirm('Save this design before closing?\n\nOK = Save to file\nCancel = Close without saving');
|
||||
if (action) {
|
||||
try {
|
||||
downloadSingleDesign();
|
||||
} catch (e) {
|
||||
const publishAnyway = confirm(`Could not save: ${e.message}\n\nPublish to Nostr instead?`);
|
||||
if (publishAnyway) {
|
||||
try {
|
||||
publishDiscDesignToNostr();
|
||||
} catch (e2) {
|
||||
alert(`Error: ${e2.message}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
function resetDiscDesignerState() {
|
||||
state.discDesign = { name: '', src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 };
|
||||
state.layers = state.layers.filter(l => l.target !== 'designer-disc');
|
||||
state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
|
||||
state.layers = (state.layers || []).filter(l => l.target !== 'designer-disc');
|
||||
state.meta = { title: '', imdbId: '', omdbApiKey: '', category: '', language: 'en' };
|
||||
state.selectedLayerId = '';
|
||||
syncInputs();
|
||||
syncDesignerInputs();
|
||||
render();
|
||||
}
|
||||
|
||||
function closeTemplateProject() {
|
||||
const hasContent = state.discs.top?.src || state.discs.bottom?.src || state.combined?.src || Object.values(state.slots || {}).some(s => s?.src);
|
||||
if (!hasContent) {
|
||||
state.discs = { top: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, bottom: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
|
||||
state.discMeta = normalizeDiscMeta();
|
||||
state.slots = { back: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, spine: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, front: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
|
||||
state.combined = { src: '' };
|
||||
state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const action = confirm('Save this project before closing?\n\nOK = Save to file\nCancel = Close without saving');
|
||||
if (action) {
|
||||
downloadProject();
|
||||
function closeDiscDesignerProject() {
|
||||
const hasContent = state.discDesign?.src || (state.layers && state.layers.some(l => l.target === 'designer-disc'));
|
||||
if (hasContent) {
|
||||
const action = confirm('Save this design before closing?\n\nOK = Save to file\nCancel = Close without saving');
|
||||
if (action) {
|
||||
try {
|
||||
downloadSingleDesign();
|
||||
} catch (e) {
|
||||
const publishAnyway = confirm(`Could not save: ${e.message}\n\nPublish to Nostr instead?`);
|
||||
if (publishAnyway) {
|
||||
try {
|
||||
publishDiscDesignToNostr();
|
||||
} catch (e2) {
|
||||
alert(`Error: ${e2.message}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resetDiscDesignerState();
|
||||
}
|
||||
|
||||
function resetTemplateProjectState() {
|
||||
state.discs = { top: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, bottom: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
|
||||
state.discMeta = normalizeDiscMeta();
|
||||
state.slots = { back: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, spine: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, front: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
|
||||
state.combined = { src: '' };
|
||||
state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
|
||||
state.jewelInserts = { front: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, tray: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
|
||||
state.discSpineLabels = { top: '', bottom: '' };
|
||||
state.jewelSpineLabels = { left: '', right: '' };
|
||||
state.meta = { title: '', imdbId: '', omdbApiKey: '', category: '', language: 'en' };
|
||||
state.layers = (state.layers || []).filter(l => l.target === 'designer-disc');
|
||||
state.selectedLayerId = '';
|
||||
syncInputs();
|
||||
syncAllSlotInputs();
|
||||
syncAllDiscInputs();
|
||||
syncAllJewelInputs();
|
||||
syncCombinedInputs();
|
||||
render();
|
||||
}
|
||||
|
||||
function closeTemplateProject() {
|
||||
const hasSlotContent = state.discs.top?.src || state.discs.bottom?.src || state.combined?.src || Object.values(state.slots || {}).some(s => s?.src);
|
||||
const hasJewel = Object.values(state.jewelInserts || {}).some(i => i?.src);
|
||||
const hasLayers = (state.layers || []).some(l => l.target !== 'designer-disc' && !l._generatedDiscLayer);
|
||||
const hasSpine = !!(state.discSpineLabels?.top || state.discSpineLabels?.bottom || state.jewelSpineLabels?.left || state.jewelSpineLabels?.right);
|
||||
const hasTitle = !!(state.meta?.title?.trim());
|
||||
const hasContent = hasSlotContent || hasJewel || hasLayers || hasSpine || hasTitle;
|
||||
if (hasContent) {
|
||||
const action = confirm('Save this project before closing?\n\nOK = Save to file\nCancel = Close without saving');
|
||||
if (action) downloadProject();
|
||||
}
|
||||
resetTemplateProjectState();
|
||||
}
|
||||
|
||||
function loadProjectFromText(text) {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed.version && parsed.templateMode === 'disc-design' && parsed.discDesign) {
|
||||
@@ -7205,8 +7450,10 @@
|
||||
|
||||
if (layer.type === 'text') {
|
||||
const fontPx = (layer.fontSize || 14) * (96/72) * SCALE;
|
||||
el.style.cssText += `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.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 || '';
|
||||
} 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;
|
||||
@@ -7377,10 +7624,57 @@
|
||||
preview.appendChild(el);
|
||||
});
|
||||
|
||||
const coverLayers = (Array.isArray(payload.layers) ? payload.layers : [])
|
||||
.filter(l => {
|
||||
const t = String(l?.target || '');
|
||||
return !t.startsWith('disc') && !t.startsWith('jewel') && !t.startsWith('designer-') && !l._generatedDiscLayer;
|
||||
})
|
||||
.slice()
|
||||
.sort((a, b) => (a.z || 0) - (b.z || 0));
|
||||
|
||||
const renderCoverLayer = (wrap, layer) => {
|
||||
const el = document.createElement('div');
|
||||
const lx = ((Number(layer.x) || 0) - bleed) / totalW * 100;
|
||||
const ly = ((Number(layer.y) || 0) - bleed) / trimH * 100;
|
||||
const lw = (Number(layer.w) || totalW) / totalW * 100;
|
||||
const lh = (Number(layer.h) || trimH) / trimH * 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: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 || '';
|
||||
} 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 = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
|
||||
img.addEventListener('error', () => img.remove());
|
||||
el.appendChild(img);
|
||||
}
|
||||
wrap.appendChild(el);
|
||||
};
|
||||
|
||||
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);
|
||||
const hasArtwork = (isCombined && combined.src) || ['back','spine','front'].some(key => slots[key]?.src) || coverLayers.length > 0;
|
||||
if (!hasArtwork) {
|
||||
if (payload.preview) {
|
||||
const img = document.createElement('img');
|
||||
@@ -7439,8 +7733,10 @@
|
||||
if (layer.type === 'text') {
|
||||
const scale = wrap.clientWidth && layerScale ? (wrap.clientWidth / (layerScale * 96)) : 1;
|
||||
const align = layer.align || 'left';
|
||||
el.style.cssText += `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.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 || '';
|
||||
} 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;
|
||||
@@ -7711,8 +8007,10 @@
|
||||
if (layer.type === 'text') {
|
||||
const scale = wrap.clientWidth ? (wrap.clientWidth / (slotW * 96)) : 1;
|
||||
const align = layer.align || 'left';
|
||||
el.style.cssText += `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.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 || '';
|
||||
} 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;
|
||||
@@ -8248,6 +8546,10 @@
|
||||
|
||||
$('saveProjectBtn').addEventListener('click', downloadProject);
|
||||
$('closeProjectBtn').addEventListener('click', closeTemplateProject);
|
||||
const exportJpegBtn = $('exportJpegBtn');
|
||||
if (exportJpegBtn) exportJpegBtn.addEventListener('click', () => exportArtboardAsJpeg(exportJpegBtn));
|
||||
const designerExportJpegBtn = $('designerExportJpegBtn');
|
||||
if (designerExportJpegBtn) designerExportJpegBtn.addEventListener('click', () => exportArtboardAsJpeg(designerExportJpegBtn));
|
||||
$('designerSaveProjectBtn').addEventListener('click', () => {
|
||||
try {
|
||||
downloadSingleDesign();
|
||||
@@ -8569,9 +8871,27 @@
|
||||
$(id).addEventListener('input', (event) => updateSelectedLayer(prop, Number(event.target.value)));
|
||||
}
|
||||
|
||||
const textContentEl = $('textLayerContent');
|
||||
if (textContentEl) {
|
||||
textContentEl.addEventListener('input', (event) => {
|
||||
const layer = selectedLayer();
|
||||
if (!layer || layer.type !== 'text') return;
|
||||
layer.html = layerPlainTextToHtml(event.target.value);
|
||||
render();
|
||||
});
|
||||
}
|
||||
const fontFamilyEl = $('textFontFamily');
|
||||
if (fontFamilyEl) {
|
||||
fontFamilyEl.innerHTML = WEB_SAFE_FONTS.map(f => `<option value="${escapeHtml(f.value)}" style="font-family:${f.value === 'inherit' ? 'inherit' : escapeHtml(f.value)}">${escapeHtml(f.label)}</option>`).join('');
|
||||
fontFamilyEl.addEventListener('change', (event) => updateSelectedLayer('fontFamily', event.target.value));
|
||||
}
|
||||
$('textFontSize').addEventListener('input', (event) => updateSelectedLayer('fontSize', Number(event.target.value)));
|
||||
$('textColor').addEventListener('input', (event) => updateSelectedLayer('color', event.target.value));
|
||||
$('textAlign').addEventListener('change', (event) => updateSelectedLayer('align', event.target.value));
|
||||
const colorFillEl = $('colorLayerFill');
|
||||
if (colorFillEl) colorFillEl.addEventListener('input', (event) => updateSelectedLayer('color', event.target.value));
|
||||
const lockEl = $('layerLocked');
|
||||
if (lockEl) lockEl.addEventListener('change', (event) => updateSelectedLayer('locked', event.target.checked));
|
||||
|
||||
$('textBold').addEventListener('click', () => {
|
||||
const layer = selectedLayer();
|
||||
@@ -8587,20 +8907,25 @@
|
||||
render();
|
||||
});
|
||||
|
||||
$('layerForward').addEventListener('click', () => {
|
||||
function moveLayerStep(direction) {
|
||||
const layer = selectedLayer();
|
||||
if (!layer) return;
|
||||
layer.z = nextLayerZ();
|
||||
const curZ = layer.z || 0;
|
||||
const peers = state.layers
|
||||
.filter(l => l !== layer && l.target === layer.target && !l._generatedDiscLayer)
|
||||
.sort((a, b) => (a.z || 0) - (b.z || 0));
|
||||
const swap = direction === 'forward'
|
||||
? peers.find(l => (l.z || 0) > curZ)
|
||||
: [...peers].reverse().find(l => (l.z || 0) < curZ);
|
||||
if (!swap) return;
|
||||
const swapZ = swap.z || 0;
|
||||
swap.z = curZ;
|
||||
layer.z = swapZ;
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
$('layerBackward').addEventListener('click', () => {
|
||||
const layer = selectedLayer();
|
||||
if (!layer) return;
|
||||
const minZ = state.layers.reduce((min, item) => Math.min(min, item.z || 0), layer.z || 0);
|
||||
layer.z = minZ - 1;
|
||||
render();
|
||||
});
|
||||
$('layerForward').addEventListener('click', () => moveLayerStep('forward'));
|
||||
$('layerBackward').addEventListener('click', () => moveLayerStep('backward'));
|
||||
|
||||
$('deleteLayer').addEventListener('click', () => {
|
||||
if (!state.selectedLayerId) return;
|
||||
@@ -8609,6 +8934,22 @@
|
||||
render();
|
||||
});
|
||||
|
||||
const duplicateBtn = $('layerDuplicate');
|
||||
if (duplicateBtn) duplicateBtn.addEventListener('click', () => {
|
||||
const layer = selectedLayer();
|
||||
if (!layer) return;
|
||||
const OFFSET = 0.1;
|
||||
const copy = {
|
||||
...layer,
|
||||
id: makeLayerId(),
|
||||
x: (Number(layer.x) || 0) + OFFSET,
|
||||
y: (Number(layer.y) || 0) + OFFSET,
|
||||
z: nextLayerZ(),
|
||||
name: layer.name ? `${layer.name} copy` : layer.name
|
||||
};
|
||||
addLayer(copy);
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'p') {
|
||||
render();
|
||||
|
||||
Reference in new issue
Block a user