added pixel art editor

This commit is contained in:
thePR0M3TH3AN committed 2026-05-22 16:06:36 -04:00
1 parent b24511ad80
commit 8a2c146408
2 files changed
+232 -34

No files matched your search

+7 -2
View File
@@ -95,8 +95,13 @@ template, it is *any design*.
Reachable via the **▦** tool-palette button ("Add Layer → Pixel Art")
and an "Edit pixels" button / double-click on a selected pixel layer.
Single static frame only.
- **Phase A2 — standalone Pixel Art design** (`templateMode: 'pixelart'`,
publish/fork as `casewrap-pixelart`, `pixel-art` category): not started.
- **Phase A2 — standalone Pixel Art design: built (pending in-browser
testing).** `templateMode: 'pixelart'` — a "Pixel Art" layout on the
Designer tab where the `#pixelEditorView` is the whole canvas area,
bound to `state.pixelArt`. Save / load / publish / fork as
`casewrap-pixelart`; appears in the community feed with a crisp
preview; the `pixel-art` category was added. Single static 32×32 grid
(grid resize is Phase B).
- **Phases B / C / D:** not started. (Note: a `drawPixelArtToCanvas`
export path is already wired, so pixel layers appear in PDF/JPEG
export — Phase B's dedicated PNG export is still pending.)
+225 -32
View File
@@ -430,11 +430,13 @@
#designerLayoutFieldset { display: none; }
body.app-designer #designerLayoutFieldset,
body.template-customart #designerLayoutFieldset,
body.template-slide #designerLayoutFieldset { display: block; }
/* Custom Art and Slide Designer live on the Designer tab, so the
Template-tab Layout panel hides while either is active. */
body.template-slide #designerLayoutFieldset,
body.template-pixelart #designerLayoutFieldset { display: block; }
/* Custom Art / Slide / Pixel Art live on the Designer tab, so the
Template-tab Layout panel hides while any is active. */
body.template-customart #layoutFieldset,
body.template-slide #layoutFieldset { display: none !important; }
body.template-slide #layoutFieldset,
body.template-pixelart #layoutFieldset { display: none !important; }
/* Editing a deck's slide opens the slide editor; suppress the layout
panels so the only way out is the "Back to deck" control. */
body.editing-deck-slide #designerLayoutFieldset,
@@ -713,16 +715,19 @@
/* ---- Pixel Editor ---- */
#pixelEditorView { display: none; }
body.pixel-editing #pixelEditorView {
body.pixel-editing #pixelEditorView,
body.template-pixelart #pixelEditorView {
display: flex; flex-direction: column; width: 100%;
min-height: calc(100vh - 160px);
}
body.pixel-editing #sheetWrap,
body.pixel-editing #discDesignerView,
body.pixel-editing #deckSorterView,
body.pixel-editing #toolPalette,
body.pixel-editing #toolOptions,
body.pixel-editing #canvasToolbar { display: none !important; }
body.pixel-editing #sheetWrap, body.template-pixelart #sheetWrap,
body.pixel-editing #discDesignerView, body.template-pixelart #discDesignerView,
body.pixel-editing #deckSorterView, body.template-pixelart #deckSorterView,
body.pixel-editing #toolPalette, body.template-pixelart #toolPalette,
body.pixel-editing #toolOptions, body.template-pixelart #toolOptions,
body.pixel-editing #canvasToolbar, body.template-pixelart #canvasToolbar { display: none !important; }
/* A standalone pixel design — the editor IS the project, so no "Done". */
body.template-pixelart #pixelEditorDoneBtn { display: none; }
.pixelEditorBar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--panel);
@@ -1154,6 +1159,15 @@
body.template-deck #exportFieldset,
body.template-deck #layersFieldset,
body.template-deck #printExportRow { display: none !important; }
/* Standalone Pixel Art: only Layout, Metadata and Print/Save apply. */
body.template-pixelart #coverSizeFieldset,
body.template-pixelart #coverArtworkFieldset,
body.template-pixelart #discArtworkFieldset,
body.template-pixelart #jewelArtworkFieldset,
body.template-pixelart #customArtFieldset,
body.template-pixelart #exportFieldset,
body.template-pixelart #layersFieldset,
body.template-pixelart #printExportRow { display: none !important; }
.templateLayerPanels {
display: grid;
@@ -1957,6 +1971,9 @@
@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; height:280px; overflow:hidden; }
/* Stop flexbox from horizontally shrinking the fixed-width mini-preview
(it would squish the design); fitMiniPreview scales it uniformly. */
.publishConfirmPreview > * { flex: 0 0 auto; transform-origin: center center; }
.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; }
@@ -2714,6 +2731,7 @@
<div class="layoutTabs" role="tablist" aria-label="Designer layout">
<button type="button" class="layoutTab" id="mode-customart" data-layout="customart" role="tab">Custom art</button>
<button type="button" class="layoutTab" id="mode-slide" data-layout="slide" role="tab">Slide</button>
<button type="button" class="layoutTab" id="mode-pixelart" data-layout="pixelart" role="tab">Pixel art</button>
<button type="button" class="layoutTab" id="mode-discdesign" data-layout="discdesign" role="tab">Disc design</button>
</div>
</fieldset>
@@ -3400,7 +3418,7 @@
<p class="subhead" style="margin:0 0 14px">Browse community templates or start your own.</p>
<div class="welcomeSearchRow">
<div><label for="welcomeQuery" style="font-size:.78rem">Title or IMDb ID</label><input id="welcomeQuery" type="text" placeholder="e.g. The Matrix or tt0133093" /></div>
<div><label for="welcomeType" style="font-size:.78rem">Type</label><select id="welcomeType"><option value="any">Any type</option><option value="cover">Case cover</option><option value="disc">Disc labels</option><option value="jewel">CD jewel inserts</option><option value="disc-design">Disc designs</option><option value="customart">Custom art</option><option value="slide">Slide</option><option value="deck">Slide deck</option></select></div>
<div><label for="welcomeType" style="font-size:.78rem">Type</label><select id="welcomeType"><option value="any">Any type</option><option value="cover">Case cover</option><option value="disc">Disc labels</option><option value="jewel">CD jewel inserts</option><option value="disc-design">Disc designs</option><option value="customart">Custom art</option><option value="slide">Slide</option><option value="deck">Slide deck</option><option value="pixelart">Pixel art</option></select></div>
<div><label for="welcomeCategory" style="font-size:.78rem">Category</label><select id="welcomeCategory" class="categorySelect"></select></div>
<div class="buttonRow" style="margin:0"><button type="button" id="welcomeSearchBtn" class="primary">Search</button></div>
</div>
@@ -3552,7 +3570,7 @@
<div id="panelSearch" style="display:none">
<div class="searchGrid">
<div><label for="nostrQuery">Title or IMDb ID</label><input id="nostrQuery" type="text" placeholder="e.g. The Matrix or tt0133093" /></div>
<div><label for="nostrType">Type</label><select id="nostrType"><option value="any">Any</option><option value="cover">Case cover</option><option value="disc">Disc sheet</option><option value="jewel">CD jewel inserts</option><option value="disc-design">Disc design</option><option value="customart">Custom art</option><option value="slide">Slide</option><option value="deck">Slide deck</option></select></div>
<div><label for="nostrType">Type</label><select id="nostrType"><option value="any">Any</option><option value="cover">Case cover</option><option value="disc">Disc sheet</option><option value="jewel">CD jewel inserts</option><option value="disc-design">Disc design</option><option value="customart">Custom art</option><option value="slide">Slide</option><option value="deck">Slide deck</option><option value="pixelart">Pixel art</option></select></div>
<div><label for="nostrCategory">Category</label><select id="nostrCategory" class="categorySelect"></select></div>
<div class="buttonRow" style="margin:0"><button type="button" id="nostrRunSearch" class="primary">Search</button></div>
</div>
@@ -4090,7 +4108,8 @@
{ value: 'audio-cd', label: 'Audio CD' },
{ value: 'custom-art', label: 'Custom art' },
{ value: 'slide', label: 'Slide design' },
{ value: 'slide-deck', label: 'Slide deck' }
{ value: 'slide-deck', label: 'Slide deck' },
{ value: 'pixel-art', label: 'Pixel art' }
];
const DEFAULT_NOSTR_RELAYS = [
@@ -4165,6 +4184,8 @@
// Slide deck: an ordered list of self-contained slides. `editingIndex` is
// non-null while one of the deck's slides is open in the canvas editor.
deck: { theme: { fontFamily: '', background: '' }, slides: [], editingIndex: null },
// Standalone Pixel Art design (templateMode 'pixelart') — one grid.
pixelArt: null,
discSpineLabels: { top: '', bottom: '' },
jewelSpineLabels: { left: '', right: '' },
jewelInserts: {
@@ -4385,7 +4406,8 @@
'audio-cd': new Set(['upc', 'mb']),
'custom-art': new Set(),
'slide': new Set(),
'slide-deck': new Set()
'slide-deck': new Set(),
'pixel-art': new Set()
};
function relevantIdentifiersForCategory(category) {
@@ -4404,7 +4426,7 @@
// media-identifier panel and show a single optional "Custom tag" field
// instead — IMDb / UPC / TheTVDB / MusicBrainz don't apply to generic
// art / wallpaper / slide / deck work.
const isCustomArt = isCanvasMode(state.templateMode) || state.templateMode === 'deck';
const isCustomArt = isCanvasMode(state.templateMode) || state.templateMode === 'deck' || state.templateMode === 'pixelart';
const idWrap = $('mediaIdentifiersWrap');
const customWrap = $('customTagWrap');
if (idWrap) idWrap.style.display = isCustomArt ? 'none' : '';
@@ -4526,9 +4548,11 @@
if (raw === 'customart' || raw === 'custom-art') return 'customart';
if (raw === 'slide') return 'slide';
if (raw === 'deck') return 'deck';
if (raw === 'pixelart' || raw === 'pixel-art') return 'pixelart';
if (raw === 'cover' || raw === 'disc' || raw === 'jewel') return raw;
if (payload.deck) return 'deck';
if (payload.slide) return 'slide';
if (payload.pixelArt) return 'pixelart';
if (payload.customArt) return 'customart';
if (payload.discTemplate || payload.discMeta) return 'disc';
if (payload.discDesign && !payload.discTemplate) return 'disc-design';
@@ -4557,6 +4581,7 @@
if (mode === 'disc') return 'disc-label-sheet';
if (mode === 'slide') return 'slide';
if (mode === 'deck') return 'deck';
if (mode === 'pixelart') return 'pixelart';
if (mode === 'customart') return 'customart';
const preset = state.preset === 'jewel8693' ? 'dvd' : (state.preset || 'custom');
if (preset === 'dvd' || preset === 'bluray') return preset;
@@ -5024,9 +5049,10 @@
deck: !isDesignerApp && state.templateMode === 'deck',
customart: !isDesignerApp && state.templateMode === 'customart',
slide: !isDesignerApp && state.templateMode === 'slide',
pixelart: !isDesignerApp && state.templateMode === 'pixelart',
discdesign: isDesignerApp
};
for (const m of ['cover', 'disc', 'deck', 'customart', 'slide', 'discdesign']) {
for (const m of ['cover', 'disc', 'deck', 'customart', 'slide', 'pixelart', 'discdesign']) {
const button = $(`mode-${m}`);
if (!button) continue;
button.classList.toggle('active', !!layoutActive[m]);
@@ -5146,6 +5172,7 @@
customart: ['designerLayoutFieldset', 'customArtFieldset', 'layersFieldset', 'metadataFieldset', 'exportFieldset', 'printSaveFieldset'],
slide: ['designerLayoutFieldset', 'customArtFieldset', 'layersFieldset', 'metadataFieldset', 'exportFieldset', 'printSaveFieldset'],
deck: ['layoutFieldset', 'deckFieldset', 'deckThemeFieldset', 'metadataFieldset', 'printSaveFieldset'],
pixelart: ['designerLayoutFieldset', 'metadataFieldset', 'printSaveFieldset'],
designer: ['designerLayoutFieldset', 'designerFieldset', 'layersFieldset', 'designerMetadataFieldset', 'designerPrintSaveFieldset']
};
@@ -5170,6 +5197,7 @@
document.body.classList.toggle('template-disc', state.templateMode === 'disc');
document.body.classList.toggle('template-jewel', state.templateMode === 'jewel');
document.body.classList.toggle('template-customart', state.templateMode === 'customart');
document.body.classList.toggle('template-pixelart', state.templateMode === 'pixelart');
document.body.classList.toggle('template-slide', state.templateMode === 'slide');
document.body.classList.toggle('template-deck', state.templateMode === 'deck');
document.body.classList.toggle('editing-deck-slide', !!state.deck && state.deck.editingIndex !== null);
@@ -5242,6 +5270,12 @@
: 'Add slides to build your deck.';
$('trimReadout').textContent = n === 1 ? '1 slide' : `${n} slides`;
$('sheetReadout').textContent = '16:9 slides';
} else if (state.templateMode === 'pixelart') {
const pa = state.pixelArt || { width: 32, height: 32 };
$('projectTitle').textContent = 'Pixel art';
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 === 'jewel') {
$('projectTitle').textContent = 'CD jewel insert preview';
document.querySelector('.printHelp').textContent = 'Avery 8693 / 8943 layout: front and tray inserts on Letter paper.';
@@ -6602,6 +6636,7 @@
syncDesignerInputs();
updateBodyClasses();
renderDeckSorter();
if (state.templateMode === 'pixelart') ensureStandalonePixelEditor();
// After updateBodyClasses so the designer view is displayed/laid out —
// the disc image's cover-pan math needs the wrapper's real size.
renderDesignerDisc();
@@ -7792,14 +7827,18 @@
function closePixelEditor(commit = true) {
if (!_pixelEditActive) return;
if (commit && _pixelEdit) {
const layer = getLayer(_pixelEdit.layerId);
if (layer) {
pushHistory('edit pixel art');
layer.pixelArt = {
width: _pixelEdit.w, height: _pixelEdit.h,
palette: _pixelEdit.palette.slice(),
frames: [{ id: 'f1', pixels: encodePixelRLE(_pixelEdit.indices) }]
};
if (_pixelEdit.standalone) {
pixelCommitIfStandalone();
} else {
const layer = getLayer(_pixelEdit.layerId);
if (layer) {
pushHistory('edit pixel art');
layer.pixelArt = {
width: _pixelEdit.w, height: _pixelEdit.h,
palette: _pixelEdit.palette.slice(),
frames: [{ id: 'f1', pixels: encodePixelRLE(_pixelEdit.indices) }]
};
}
}
}
_pixelEditActive = false;
@@ -7875,6 +7914,7 @@
if (!_pixelEdit || !_pixelEdit.undo.length) return;
_pixelEdit.indices = _pixelEdit.undo.pop();
renderPixelGrid();
pixelCommitIfStandalone();
}
function pixelPaintCell(x, y, idx) {
@@ -7967,6 +8007,72 @@
openPixelEditor(layer.id);
}
// Commit the standalone pixel editor's working grid into state.pixelArt.
function pixelCommitIfStandalone() {
if (!_pixelEdit || !_pixelEdit.standalone) return;
state.pixelArt = {
width: _pixelEdit.w, height: _pixelEdit.h,
palette: _pixelEdit.palette.slice(),
frames: [{ id: 'f1', pixels: encodePixelRLE(_pixelEdit.indices) }]
};
}
// Commit whatever pixel editor is open (layer or standalone) and close it.
function pixelEditTearDown() {
if (!_pixelEditActive || !_pixelEdit) return;
if (_pixelEdit.standalone) {
pixelCommitIfStandalone();
} else {
const layer = getLayer(_pixelEdit.layerId);
if (layer) {
layer.pixelArt = {
width: _pixelEdit.w, height: _pixelEdit.h,
palette: _pixelEdit.palette.slice(),
frames: [{ id: 'f1', pixels: encodePixelRLE(_pixelEdit.indices) }]
};
}
}
_pixelEditActive = false;
_pixelEdit = null;
document.body.classList.remove('pixel-editing');
}
// Standalone Pixel Art mode: the #pixelEditorView is the whole canvas
// area, bound directly to state.pixelArt. Set up by render() whenever
// templateMode === 'pixelart'.
function ensureStandalonePixelEditor() {
if (_pixelEditActive && _pixelEdit && _pixelEdit.standalone) return;
if (!state.pixelArt) state.pixelArt = defaultPixelArt(32, 32);
const pa = state.pixelArt;
const w = (pa.width | 0) || 32, h = (pa.height | 0) || 32;
const frame = (pa.frames && pa.frames[0]) || { pixels: [] };
const palette = (pa.palette && pa.palette.length) ? pa.palette.slice() : DEFAULT_PIXEL_PALETTE.slice();
_pixelEdit = {
standalone: true, layerId: null, w, h,
indices: decodePixelRLE(frame, w, h), palette,
current: Math.min(1, palette.length - 1),
tool: 'pencil', zoom: 12, gridOn: true, mirrorX: false, mirrorY: false,
undo: [], last: null
};
_pixelEditActive = true;
pixelSyncToolButtons();
const gt = $('pixelGridToggle'); if (gt) gt.checked = true;
const mx = $('pixelMirrorX'); if (mx) mx.checked = false;
const my = $('pixelMirrorY'); if (my) my.checked = false;
const dims = $('pixelEditorDims'); if (dims) dims.textContent = `${w} × ${h} px`;
renderPixelPalette();
renderPixelGrid();
requestAnimationFrame(() => {
if (!_pixelEdit || !_pixelEdit.standalone) return;
const wrap = $('pixelEditorCanvasWrap');
const aw = (wrap?.clientWidth || 640) - 60, ah = (wrap?.clientHeight || 420) - 60;
let z = PIXEL_ZOOMS[0];
for (const c of PIXEL_ZOOMS) { if (c * w <= aw && c * h <= ah) z = c; }
_pixelEdit.zoom = z;
renderPixelGrid();
});
}
// ---- Vector shape layers (Phase A: primitives + solid fill) ----
const SHAPE_KINDS = ['rect', 'rounded-rect', 'circle', 'ellipse', 'triangle', 'polygon', 'star', 'line', 'path'];
@@ -9752,6 +9858,7 @@
if (mode === 'deck') return 'casewrap-deck';
if (mode === 'slide') return 'casewrap-slide';
if (mode === 'customart') return 'casewrap-customart';
if (mode === 'pixelart') return 'casewrap-pixelart';
if (mode === 'disc-design') return 'casewrap-disc-design';
return 'casewrap-cover';
}
@@ -10008,6 +10115,13 @@
if (!(payload.deck?.slides || []).length) {
throw new Error('Add at least one slide before publishing the deck.');
}
} else if (state.templateMode === 'pixelart') {
payload = pixelArtPayload();
typeTag = 'casewrap-pixelart';
const runs = payload.pixelArt?.frames?.[0]?.pixels || [];
if (!runs.some(r => (r[0] | 0) !== 0)) {
throw new Error('Draw something on the pixel canvas before publishing.');
}
} else if (isCanvasMode(state.templateMode)) {
payload = customArtPayload();
typeTag = state.templateMode === 'slide' ? 'casewrap-slide' : 'casewrap-customart';
@@ -11364,6 +11478,18 @@
};
}
// A standalone pixel-art design — one grid, published as casewrap-pixelart.
function pixelArtPayload() {
pixelCommitIfStandalone();
return {
version: SCHEMA_VERSION,
templateMode: 'pixelart',
templateType: 'pixelart',
meta: state.meta,
pixelArt: state.pixelArt || defaultPixelArt(32, 32)
};
}
function exportPayload() {
const templateType = templateTypeForMode();
return {
@@ -11918,6 +12044,9 @@
} else if (state.templateMode === 'deck') {
const n = (state.deck?.slides || []).length;
triggerDownload(deckPayload(), projectFilename('slide_deck', `${n}slides`));
} else if (state.templateMode === 'pixelart') {
const pa = state.pixelArt || defaultPixelArt(32, 32);
triggerDownload(pixelArtPayload(), projectFilename('pixel_art', `${pa.width}x${pa.height}`));
} else if (isCanvasMode(state.templateMode)) {
const ca = state.customArt || { width: 1920, height: 1080 };
const stem = state.templateMode === 'slide' ? 'slide' : 'custom_art';
@@ -11979,6 +12108,10 @@
state.jewelSpineLabels = { left: '', right: '' };
state.customArt = { width: 1920, height: 1080, background: '#ffffff', preset: '1920x1080' };
state.deck = { theme: { fontFamily: '', background: '' }, slides: [], editingIndex: null };
state.pixelArt = null;
_pixelEditActive = false;
_pixelEdit = null;
document.body.classList.remove('pixel-editing');
state.visibility = 'public';
state.privateDTag = '';
state.meta = blankMeta();
@@ -12092,7 +12225,8 @@
'disc-design': 'Disc design',
customart: 'Custom art',
slide: 'Slide',
deck: 'Slide deck'
deck: 'Slide deck',
pixelart: 'Pixel art'
};
let _publishConfirmTarget = null; // { onConfirm }
@@ -12133,6 +12267,7 @@
else if (mode === 'jewel') renderJewelPreviewDOM(previewEl, payload, { compact: true });
else if (mode === 'customart' || mode === 'slide') renderCustomArtPreviewDOM(previewEl, payload, { compact: true });
else if (mode === 'deck') renderDeckPreviewDOM(previewEl, payload, { compact: true });
else if (mode === 'pixelart') renderPixelArtPreviewDOM(previewEl, payload, { compact: true });
else renderTemplatePreviewDOM(previewEl, payload, { compact: true });
fitMiniPreview(previewEl);
} catch (err) {
@@ -12388,6 +12523,18 @@
return;
}
if (mode === 'pixelart' || (parsed.pixelArt && parsed.templateMode === 'pixelart')) {
Object.assign(state, {
templateMode: 'pixelart',
meta: { ...state.meta, ...(parsed.meta || {}) },
pixelArt: parsed.pixelArt || defaultPixelArt(32, 32)
});
_pixelEditActive = false;
_pixelEdit = null;
syncInputs(); render();
return;
}
if (mode === 'deck' || parsed.deck) {
const d = parsed.deck || {};
Object.assign(state, {
@@ -14425,6 +14572,8 @@
// The whole embedded slide list is the deck's identity.
fields.push(j(payload.deck?.slides || []));
fields.push(j(payload.deck?.theme || {}));
} else if (mode === 'pixelart') {
fields.push(j(payload.pixelArt || {}));
}
// Layers: strip the per-layer `id` and `_generatedDiscLayer` since
// those vary across fork sessions but the visual content is the same.
@@ -14482,6 +14631,7 @@
else if (row.mode === 'jewel') renderJewelPreviewDOM(previewWrap, payload, { compact: true });
else if (row.mode === 'customart' || row.mode === 'slide') renderCustomArtPreviewDOM(previewWrap, payload, { compact: true });
else if (row.mode === 'deck') renderDeckPreviewDOM(previewWrap, payload, { compact: true });
else if (row.mode === 'pixelart') renderPixelArtPreviewDOM(previewWrap, payload, { compact: true });
else renderTemplatePreviewDOM(previewWrap, payload, { compact: true });
} catch {}
thumb.appendChild(previewWrap);
@@ -14974,6 +15124,8 @@
return '';
}
if (mode === 'pixelart') return '';
// Cover: front → combined wrap → back (in that priority)
return (
payload.coverTemplate?.slots?.front?.src ||
@@ -15027,7 +15179,7 @@
return btn;
}
const MODE_LABELS = { cover: 'Case cover', disc: 'Disc sheet', jewel: 'CD jewel inserts', 'disc-design': 'Disc design', customart: 'Custom art', slide: 'Slide', deck: 'Slide deck' };
const MODE_LABELS = { cover: 'Case cover', disc: 'Disc sheet', jewel: 'CD jewel inserts', 'disc-design': 'Disc design', customart: 'Custom art', slide: 'Slide', deck: 'Slide deck', pixelart: 'Pixel art' };
function buildDiscSheetThumb(thumb, payload, compact = false) {
const slots = payload?.discTemplate?.discs || payload?.discs || {};
@@ -15437,6 +15589,8 @@
renderCustomArtPreviewDOM(content, p);
} else if (mode === 'deck') {
renderDeckPreviewDOM(content, p);
} else if (mode === 'pixelart') {
renderPixelArtPreviewDOM(content, p);
} else {
renderTemplatePreviewDOM(content, p);
}
@@ -16216,6 +16370,30 @@
container.appendChild(badge);
}
// Preview a standalone pixel-art design — the grid rendered crisp.
function renderPixelArtPreviewDOM(container, payload, opts = {}) {
const compact = !!opts.compact;
const pa = (payload && payload.pixelArt) || {};
const w = (pa.width | 0) || 32, h = (pa.height | 0) || 32;
const naturalW = PREVIEW_BASE_PX;
const naturalH = PREVIEW_BASE_PX * h / w;
const box = document.createElement('div');
box.style.cssText = [
'position:relative',
compact ? `width:${naturalW}px` : `width:min(100%, ${PREVIEW_BASE_PX}px)`,
compact ? `height:${naturalH}px` : `aspect-ratio:${w} / ${h}`,
compact ? 'margin:0;transform-origin:center center' : 'margin:0 auto',
'overflow:hidden',
'background:repeating-conic-gradient(#e2e8f0 0% 25%, #f8fafc 0% 50%) 0 0 / 18px 18px'
].join(';');
try {
const cv = renderPixelGridToCanvas(pa);
cv.style.cssText = 'width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges';
box.appendChild(cv);
} catch (e) {}
container.appendChild(box);
}
function renderCustomArtPreviewDOM(container, payload, opts = {}) {
const compact = !!opts.compact;
// Slides and Custom Art share this flat-canvas preview; a slide payload
@@ -16448,7 +16626,7 @@
function syncModeTabsUI() {
// The Designer tab hosts both Custom Art (a template mode) and Disc
// Design (the designer appMode).
const designerTab = state.appMode === 'designer' || isCanvasMode(state.templateMode);
const designerTab = state.appMode === 'designer' || isCanvasMode(state.templateMode) || state.templateMode === 'pixelart';
$('modeTabTemplate').classList.toggle('active', !designerTab);
$('modeTabDesigner').classList.toggle('active', designerTab);
}
@@ -16965,6 +17143,10 @@
// If a deck slide is open in the editor, save it back before any
// layout change carries us out of the slide editor.
persistEditingDeckSlide();
// Any open pixel editor (layer or standalone) commits before a
// layout change; a standalone one is re-created by render() if we
// land back in pixelart mode.
pixelEditTearDown();
if (layout === 'discdesign') {
if (state.appMode !== 'designer') switchAppMode('designer');
state._lastDesignerLayout = 'discdesign';
@@ -16993,7 +17175,9 @@
// Art just gets a default the user can override.
if (layout === 'slide') state.meta.category = 'slide';
else if (layout === 'deck') state.meta.category = 'slide-deck';
else if (layout === 'pixelart') state.meta.category = 'pixel-art';
else if (layout === 'customart' && !state.meta.category) state.meta.category = 'custom-art';
if (layout === 'pixelart' && !state.pixelArt) state.pixelArt = defaultPixelArt(32, 32);
if (layout === 'slide') {
const ca = state.customArt || {};
if (Math.abs((ca.width || 0) / (ca.height || 1) - 16 / 9) > 0.01) {
@@ -17006,7 +17190,7 @@
state.deck = state.deck || { theme: { fontFamily: '', background: '' }, slides: [], editingIndex: null };
state.deck.editingIndex = null;
}
if (isCanvasMode(layout)) state._lastDesignerLayout = layout;
if (isCanvasMode(layout) || layout === 'pixelart') state._lastDesignerLayout = layout;
else state._lastTemplateLayout = layout;
syncInputs();
render();
@@ -17118,7 +17302,7 @@
function attachEvents() {
// Layout tabs: cover/disc live on the Template tab; customart/discdesign
// on the Designer tab. applyLayout() sorts out the appMode crossing.
for (const layout of ['cover', 'disc', 'deck', 'customart', 'slide', 'discdesign']) {
for (const layout of ['cover', 'disc', 'deck', 'customart', 'slide', 'pixelart', 'discdesign']) {
const btn = $(`mode-${layout}`);
if (btn) btn.addEventListener('click', () => applyLayout(layout));
}
@@ -17202,7 +17386,11 @@
const cell = pixelCellFromEvent(e);
if (cell) pixelApplyTool(cell, false);
});
const _endPaint = () => { _painting = false; if (_pixelEdit) _pixelEdit.last = null; };
const _endPaint = () => {
_painting = false;
if (_pixelEdit) _pixelEdit.last = null;
pixelCommitIfStandalone();
};
_pgc.addEventListener('pointerup', _endPaint);
_pgc.addEventListener('pointercancel', _endPaint);
}
@@ -17210,7 +17398,7 @@
if (!_pixelEditActive) return;
const k = (e.key || '').toLowerCase();
if ((e.ctrlKey || e.metaKey) && k === 'z') { e.preventDefault(); e.stopPropagation(); pixelUndo(); }
else if (k === 'escape') { e.preventDefault(); e.stopPropagation(); closePixelEditor(true); }
else if (k === 'escape' && _pixelEdit && !_pixelEdit.standalone) { e.preventDefault(); e.stopPropagation(); closePixelEditor(true); }
else if (!e.ctrlKey && !e.metaKey && !isEditableFocus()) {
const map = { p: 'pencil', e: 'eraser', f: 'fill', i: 'eyedropper' };
if (map[k]) { e.preventDefault(); e.stopPropagation(); _pixelEdit.tool = map[k]; pixelSyncToolButtons(); }
@@ -17504,6 +17692,11 @@
payload = deckPayload();
mode = 'deck';
if (!(payload.deck?.slides || []).length) throw new Error('Add at least one slide before publishing the deck.');
} else if (state.templateMode === 'pixelart') {
payload = pixelArtPayload();
mode = 'pixelart';
const runs = payload.pixelArt?.frames?.[0]?.pixels || [];
if (!runs.some(r => (r[0] | 0) !== 0)) throw new Error('Draw something on the pixel canvas before publishing.');
} else if (isCanvasMode(state.templateMode)) {
payload = customArtPayload();
mode = state.templateMode;