Refine mobile UI and simplify tool help

This commit is contained in:
chris 2025-12-04 13:10:48 -05:00
parent 7e6ac4cf4b
commit a5016b4fa7
5 changed files with 157 additions and 105 deletions

View File

@ -41,11 +41,6 @@
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button> <button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
</nav> </nav>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="flex items-center gap-1 px-2 py-1 rounded-xl bg-white/70 border border-gray-200 shadow-sm" title="Active Color">
<div id="current-color-chip-global" class="current-color-chip">
<span id="current-color-label-global" class="text-[10px] font-semibold text-slate-700"></span>
</div>
</div>
<button id="app-fullscreen-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Toggle fullscreen">Fullscreen</button> <button id="app-fullscreen-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Toggle fullscreen">Fullscreen</button>
<button id="clear-canvas-btn-top" class="btn-danger text-xs px-3 py-2">Start Fresh</button> <button id="clear-canvas-btn-top" class="btn-danger text-xs px-3 py-2">Start Fresh</button>
</div> </div>
@ -79,11 +74,11 @@
</button> </button>
</div> </div>
<div class="grid grid-cols-3 gap-2 mb-3"> <div class="grid grid-cols-3 gap-2 mb-3">
<button id="tool-undo" class="tool-btn" title="Ctrl+Z" aria-label="Undo"> <button id="tool-undo" class="tool-btn" aria-label="Undo">
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg> <svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
<span class="hidden sm:inline">Undo</span> <span class="hidden sm:inline">Undo</span>
</button> </button>
<button id="tool-redo" class="tool-btn" title="Ctrl+Y" aria-label="Redo"> <button id="tool-redo" class="tool-btn" aria-label="Redo">
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg> <svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
<span class="hidden sm:inline">Redo</span> <span class="hidden sm:inline">Redo</span>
</button> </button>
@ -99,31 +94,17 @@
<input id="garland-density" type="range" min="0.6" max="1.6" step="0.1" value="1" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> <input id="garland-density" type="range" min="0.6" max="1.6" step="0.1" value="1" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<span id="garland-density-label" class="w-10 text-right text-xs text-gray-500">1.0</span> <span id="garland-density-label" class="w-10 text-right text-xs text-gray-500">1.0</span>
</div> </div>
<div class="grid grid-cols-1 gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center justify-between">
<label for="garland-color-main1" class="font-medium w-24">Main A</label> <span class="font-medium text-sm text-gray-700">Main Colors</span>
<select id="garland-color-main1" class="select text-sm flex-1"></select> <button type="button" id="garland-add-color" class="btn-blue text-xs px-3 py-1">+ Add</button>
<span id="garland-swatch-main1" class="swatch tiny"></span>
</div> </div>
<div id="garland-main-chips" class="flex flex-wrap gap-2"></div>
<p class="hint text-xs">Tap a chip to change it. You can add up to 10 main colors.</p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="garland-color-main2" class="font-medium w-24">Main B</label> <span class="font-medium text-sm text-gray-700">Accent</span>
<select id="garland-color-main2" class="select text-sm flex-1"></select> <button type="button" id="garland-accent-chip" class="replace-chip" aria-label="Pick accent color"></button>
<span id="garland-swatch-main2" class="swatch tiny"></span> <button type="button" id="garland-accent-clear" class="btn-yellow text-xs px-3 py-1">Clear</button>
</div>
<div class="flex items-center gap-2">
<label for="garland-color-main3" class="font-medium w-24">Main C</label>
<select id="garland-color-main3" class="select text-sm flex-1"></select>
<span id="garland-swatch-main3" class="swatch tiny"></span>
</div>
<div class="flex items-center gap-2">
<label for="garland-color-main4" class="font-medium w-24">Main D</label>
<select id="garland-color-main4" class="select text-sm flex-1"></select>
<span id="garland-swatch-main4" class="swatch tiny"></span>
</div>
<div class="flex items-center gap-2">
<label for="garland-color-accent" class="font-medium w-24">5&quot; Accent</label>
<select id="garland-color-accent" class="select text-sm flex-1"></select>
<span id="garland-swatch-accent" class="swatch tiny"></span>
</div> </div>
</div> </div>
</div> </div>
@ -138,7 +119,7 @@
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button> <button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<p class="hint">Drag balloons to reposition. Use keyboard arrows for fine nudges.</p> <p class="hint">Drag balloons to reposition. Use arrows/touches for fine nudges.</p>
</div> </div>
<div class="mt-2 flex items-center gap-2 text-xs text-gray-600"> <div class="mt-2 flex items-center gap-2 text-xs text-gray-600">
<span class="font-semibold">Resize</span> <span class="font-semibold">Resize</span>
@ -185,7 +166,7 @@
<div class="panel-heading mt-4">Color Library</div> <div class="panel-heading mt-4">Color Library</div>
<div class="panel-card"> <div class="panel-card">
<p class="hint mb-2">Alt+click on canvas to sample a balloons color.</p> <p class="hint mb-2">Tap or click on canvas to sample a balloons color (use the eyedropper).</p>
<div class="flex items-center gap-3 mb-2"> <div class="flex items-center gap-3 mb-2">
<span class="text-sm font-medium text-gray-700">Active Color</span> <span class="text-sm font-medium text-gray-700">Active Color</span>
<div id="current-color-chip" class="current-color-chip"> <div id="current-color-chip" class="current-color-chip">
@ -447,7 +428,7 @@
Show wireframe for empty spots Show wireframe for empty spots
</label> </label>
<label class="text-sm font-medium inline-flex items-center gap-2 col-span-2"> <label class="text-sm font-medium inline-flex items-center gap-2 col-span-2">
<input id="wall-outline" type="checkbox" class="align-middle"> <input id="wall-outline" type="checkbox" class="align-middle" checked>
Outline balloons Outline balloons
</label> </label>
</div> </div>
@ -464,7 +445,7 @@
<span>Erase</span> <span>Erase</span>
</button> </button>
</div> </div>
<p class="hint mt-2 text-xs">Paint applies the active color; Erase clears. Hold Shift/Ctrl for temporary erase.</p> <p class="hint mt-2 text-xs">Paint applies the active color; Erase clears. Hold modifier on desktop to erase temporarily.</p>
</div> </div>
</div> </div>
@ -477,7 +458,7 @@
<span id="wall-active-color-label" class="text-[10px] font-semibold text-slate-700"></span> <span id="wall-active-color-label" class="text-[10px] font-semibold text-slate-700"></span>
</div> </div>
</div> </div>
<p class="hint mt-2">Tap a swatch to set. Tap a balloon to paint; tap again (same color) to clear. Alt+click (desktop) to pick.</p> <p class="hint mt-2">Tap a swatch to set. Tap a balloon to paint; tap again (same color) to clear. Use the eyedropper to pick from the canvas.</p>
</div> </div>
<div class="panel-heading mt-4">Used Colors</div> <div class="panel-heading mt-4">Used Colors</div>
<div class="panel-card"> <div class="panel-card">

View File

@ -138,16 +138,10 @@
const fitViewBtn = document.getElementById('fit-view-btn'); const fitViewBtn = document.getElementById('fit-view-btn');
const garlandDensityInput = document.getElementById('garland-density'); const garlandDensityInput = document.getElementById('garland-density');
const garlandDensityLabel = document.getElementById('garland-density-label'); const garlandDensityLabel = document.getElementById('garland-density-label');
const garlandColorMain1Sel = document.getElementById('garland-color-main1'); const garlandMainChips = document.getElementById('garland-main-chips');
const garlandColorMain2Sel = document.getElementById('garland-color-main2'); const garlandAddColorBtn = document.getElementById('garland-add-color');
const garlandColorMain3Sel = document.getElementById('garland-color-main3'); const garlandAccentChip = document.getElementById('garland-accent-chip');
const garlandColorMain4Sel = document.getElementById('garland-color-main4'); const garlandAccentClearBtn = document.getElementById('garland-accent-clear');
const garlandColorAccentSel = document.getElementById('garland-color-accent');
const garlandSwatchMain1 = document.getElementById('garland-swatch-main1');
const garlandSwatchMain2 = document.getElementById('garland-swatch-main2');
const garlandSwatchMain3 = document.getElementById('garland-swatch-main3');
const garlandSwatchMain4 = document.getElementById('garland-swatch-main4');
const garlandSwatchAccent = document.getElementById('garland-swatch-accent');
const garlandControls = document.getElementById('garland-controls'); const garlandControls = document.getElementById('garland-controls');
const sizePresetGroup = document.getElementById('size-preset-group'); const sizePresetGroup = document.getElementById('size-preset-group');
@ -214,8 +208,8 @@
let usedSortDesc = true; let usedSortDesc = true;
let garlandPath = []; let garlandPath = [];
let garlandDensity = parseFloat(garlandDensityInput?.value || '1') || 1; let garlandDensity = parseFloat(garlandDensityInput?.value || '1') || 1;
let garlandMainIdx = [0, 0, 0, 0]; let garlandMainIdx = [0];
let garlandAccentIdx = 0; let garlandAccentIdx = -1;
let lastCommitMode = ''; let lastCommitMode = '';
let lastAddStatus = ''; let lastAddStatus = '';
let evtStats = { down: 0, up: 0, cancel: 0, touchEnd: 0, addBalloon: 0, addGarland: 0, lastType: '' }; let evtStats = { down: 0, up: 0, cancel: 0, touchEnd: 0, addBalloon: 0, addGarland: 0, lastType: '' };
@ -234,11 +228,11 @@
const canRedo = historyPointer < historyStack.length - 1; const canRedo = historyPointer < historyStack.length - 1;
if (toolUndoBtn) { if (toolUndoBtn) {
toolUndoBtn.disabled = !canUndo; toolUndoBtn.disabled = !canUndo;
toolUndoBtn.title = canUndo ? 'Undo (Ctrl+Z)' : 'Nothing to undo'; toolUndoBtn.title = canUndo ? 'Undo' : 'Nothing to undo';
} }
if (toolRedoBtn) { if (toolRedoBtn) {
toolRedoBtn.disabled = !canRedo; toolRedoBtn.disabled = !canRedo;
toolRedoBtn.title = canRedo ? 'Redo (Ctrl+Y)' : 'Nothing to redo'; toolRedoBtn.title = canRedo ? 'Redo' : 'Nothing to redo';
} }
} }
@ -997,8 +991,8 @@
if (garlandDensityLabel) garlandDensityLabel.textContent = garlandDensity.toFixed(1); if (garlandDensityLabel) garlandDensityLabel.textContent = garlandDensity.toFixed(1);
} }
if (Array.isArray(s.garlandMainIdx)) { if (Array.isArray(s.garlandMainIdx)) {
garlandMainIdx = s.garlandMainIdx.slice(0, 4).map(v => Number(v) || -1); garlandMainIdx = s.garlandMainIdx.slice(0, 10).map(v => Number.isInteger(v) ? v : -1).filter((v, i) => i < 10);
while (garlandMainIdx.length < 4) garlandMainIdx.push(-1); if (!garlandMainIdx.length) garlandMainIdx = [selectedColorIdx];
} }
if (typeof s.garlandAccentIdx === 'number') garlandAccentIdx = s.garlandAccentIdx; if (typeof s.garlandAccentIdx === 'number') garlandAccentIdx = s.garlandAccentIdx;
if (typeof s.isBorderEnabled === 'boolean') isBorderEnabled = s.isBorderEnabled; if (typeof s.isBorderEnabled === 'boolean') isBorderEnabled = s.isBorderEnabled;
@ -1010,6 +1004,109 @@
loadAppState(); loadAppState();
resetHistory(); // establish initial history state for undo/redo controls resetHistory(); // establish initial history state for undo/redo controls
// ====== Garland color UI (dynamic chips) ======
const styleChip = (el, meta) => {
if (!el || !meta) return;
if (meta.image) {
el.style.backgroundImage = `url("${meta.image}")`;
el.style.backgroundColor = meta.hex || '#fff';
el.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
el.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else {
el.style.backgroundImage = 'none';
el.style.backgroundColor = meta.hex || '#f1f5f9';
}
};
const garlandMaxColors = 10;
function renderGarlandMainChips() {
if (!garlandMainChips) return;
garlandMainChips.innerHTML = '';
const items = garlandMainIdx.length ? garlandMainIdx : [selectedColorIdx];
items.forEach((idx, i) => {
const wrap = document.createElement('div');
wrap.className = 'flex items-center gap-1';
const chip = document.createElement('button');
chip.type = 'button';
chip.className = 'replace-chip garland-chip';
const meta = FLAT_COLORS[idx] || FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
styleChip(chip, meta);
chip.title = meta?.name || meta?.hex || 'Color';
chip.addEventListener('click', () => {
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Path color',
subtitle: 'Pick a main color',
items: (FLAT_COLORS || []).map((c, ci) => ({ label: c.name || c.hex, metaText: c.family || '', idx: ci })),
onSelect: (item) => {
garlandMainIdx[i] = item.idx;
renderGarlandMainChips();
if (mode === 'garland') requestDraw();
persist();
}
});
});
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn-yellow text-xs px-2 py-1';
removeBtn.textContent = '×';
removeBtn.title = 'Remove color';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
garlandMainIdx.splice(i, 1);
if (!garlandMainIdx.length) garlandMainIdx.push(selectedColorIdx);
renderGarlandMainChips();
if (mode === 'garland') requestDraw();
persist();
});
wrap.appendChild(chip);
wrap.appendChild(removeBtn);
garlandMainChips.appendChild(wrap);
});
}
garlandAddColorBtn?.addEventListener('click', () => {
if (garlandMainIdx.length >= garlandMaxColors) { showModal(`Max ${garlandMaxColors} colors.`); return; }
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Add path color',
subtitle: 'Choose a main color',
items: (FLAT_COLORS || []).map((c, ci) => ({ label: c.name || c.hex, metaText: c.family || '', idx: ci })),
onSelect: (item) => {
garlandMainIdx.push(item.idx);
renderGarlandMainChips();
if (mode === 'garland') requestDraw();
persist();
}
});
});
const updateAccentChip = () => {
if (!garlandAccentChip) return;
const meta = garlandAccentIdx >= 0 ? FLAT_COLORS[garlandAccentIdx] : null;
styleChip(garlandAccentChip, meta || { hex: '#f8fafc' });
};
garlandAccentChip?.addEventListener('click', () => {
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Accent color',
subtitle: 'Choose a 5" accent color',
items: (FLAT_COLORS || []).map((c, ci) => ({ label: c.name || c.hex, metaText: c.family || '', idx: ci })),
onSelect: (item) => {
garlandAccentIdx = item.idx;
updateAccentChip();
if (mode === 'garland') requestDraw();
persist();
}
});
});
garlandAccentClearBtn?.addEventListener('click', () => {
garlandAccentIdx = -1;
updateAccentChip();
if (mode === 'garland') requestDraw();
persist();
});
// ====== UI Rendering (Palettes) ====== // ====== UI Rendering (Palettes) ======
function renderAllowedPalette() { function renderAllowedPalette() {
if (!paletteBox) return; if (!paletteBox) return;
@ -1789,31 +1886,13 @@
if (mode === 'garland') requestDraw(); if (mode === 'garland') requestDraw();
persist(); persist();
}); });
const handleGarlandColorChange = () => { const refreshGarlandColors = () => {
updateGarlandSwatches(); renderGarlandMainChips();
persist(); updateAccentChip();
if (mode === 'garland') requestDraw(); if (mode === 'garland') requestDraw();
persist();
}; };
garlandColorMain1Sel?.addEventListener('change', e => { refreshGarlandColors();
garlandMainIdx[0] = parseInt(e.target.value, 10) || -1;
handleGarlandColorChange();
});
garlandColorMain2Sel?.addEventListener('change', e => {
garlandMainIdx[1] = parseInt(e.target.value, 10) || -1;
handleGarlandColorChange();
});
garlandColorMain3Sel?.addEventListener('change', e => {
garlandMainIdx[2] = parseInt(e.target.value, 10) || -1;
handleGarlandColorChange();
});
garlandColorMain4Sel?.addEventListener('change', e => {
garlandMainIdx[3] = parseInt(e.target.value, 10) || -1;
handleGarlandColorChange();
});
garlandColorAccentSel?.addEventListener('change', e => {
garlandAccentIdx = parseInt(e.target.value, 10) || -1;
handleGarlandColorChange();
});
deleteSelectedBtn?.addEventListener('click', deleteSelected); deleteSelectedBtn?.addEventListener('click', deleteSelected);
duplicateSelectedBtn?.addEventListener('click', duplicateSelected); duplicateSelectedBtn?.addEventListener('click', duplicateSelected);
@ -1957,26 +2036,6 @@
updateGarlandSwatches(); updateGarlandSwatches();
} }
function updateGarlandSwatches() {
const setSw = (sw, idx) => {
if (!sw) return;
const meta = idx >= 0 ? FLAT_COLORS[idx] : null;
if (meta?.image) {
sw.style.backgroundImage = `url("${meta.image}")`;
sw.style.backgroundColor = meta.hex || '#fff';
sw.style.backgroundSize = 'cover';
} else {
sw.style.backgroundImage = 'none';
sw.style.backgroundColor = meta?.hex || '#f1f5f9';
}
};
setSw(garlandSwatchMain1, garlandMainIdx[0]);
setSw(garlandSwatchMain2, garlandMainIdx[1]);
setSw(garlandSwatchMain3, garlandMainIdx[2]);
setSw(garlandSwatchMain4, garlandMainIdx[3]);
setSw(garlandSwatchAccent, garlandAccentIdx);
}
const updateReplaceChips = () => { const updateReplaceChips = () => {
const fromHex = replaceFromSel?.value; const fromHex = replaceFromSel?.value;
const toIdx = parseInt(replaceToSel?.value || '-1', 10); const toIdx = parseInt(replaceToSel?.value || '-1', 10);

View File

@ -7,7 +7,7 @@
// Ensure shared helpers are ready // Ensure shared helpers are ready
if (!window.shared) return; if (!window.shared) return;
const { clamp, clamp01 } = window.shared; const { clamp, clamp01, SWATCH_TEXTURE_ZOOM } = window.shared;
const { FLAT_COLORS } = window.shared; const { FLAT_COLORS } = window.shared;
// Modal helpers // Modal helpers
@ -58,9 +58,10 @@
const setChipStyle = (el, meta) => { const setChipStyle = (el, meta) => {
if (!el || !meta) return; if (!el || !meta) return;
if (meta.image) { if (meta.image) {
const zoom = Math.max(1, meta.imageZoom ?? SWATCH_TEXTURE_ZOOM ?? 2.5);
el.style.backgroundImage = `url("${meta.image}")`; el.style.backgroundImage = `url("${meta.image}")`;
el.style.backgroundColor = meta.hex || '#fff'; el.style.backgroundColor = meta.hex || '#fff';
el.style.backgroundSize = 'cover'; el.style.backgroundSize = `${100 * zoom}%`;
el.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`; el.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else { } else {
el.style.backgroundImage = 'none'; el.style.backgroundImage = 'none';
@ -526,7 +527,8 @@
const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches; const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches;
const updateMobileActionBarVisibility = () => { const updateMobileActionBarVisibility = () => {
if (!mobileActionBar) return; if (!mobileActionBar) return;
const shouldShow = current === '#tab-organic' && isMobileView(); const modalOpen = !!document.querySelector('.color-modal:not(.hidden)');
const shouldShow = current === '#tab-organic' && isMobileView() && !modalOpen;
mobileActionBar.classList.toggle('hidden', !shouldShow); mobileActionBar.classList.toggle('hidden', !shouldShow);
}; };
const wireMobileActionButtons = () => { const wireMobileActionButtons = () => {

View File

@ -446,7 +446,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
display: block; display: block;
} }
.control-sheet { bottom: 4.5rem; max-height: 55vh; } .control-sheet { bottom: 4.5rem; max-height: 55vh; }
.control-sheet.minimized { transform: translateY(95%); } .control-sheet.minimized { transform: translateY(115%); }
/* Larger tap targets and spacing */ /* Larger tap targets and spacing */
.tool-btn, .tool-btn,
@ -480,7 +480,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
z-index: 45; z-index: 20; /* below control sheets (30) and modals (60) */
} }
.color-modal { .color-modal {
@ -624,6 +624,16 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
box-shadow: 0 -6px 30px rgba(15, 23, 42, 0.12); box-shadow: 0 -6px 30px rgba(15, 23, 42, 0.12);
border-top: 1px solid rgba(148, 163, 184, 0.25); border-top: 1px solid rgba(148, 163, 184, 0.25);
} }
.mobile-tabbar.hidden { display: none; }
@media (max-width: 1023px) {
/* Tuck canvases above the tabbar */
#classic-display,
#wall-display,
#balloon-canvas {
margin-bottom: 5.5rem;
}
}
.mobile-tabbar .mobile-tab-btn { .mobile-tabbar .mobile-tab-btn {
flex: 1 1 0; flex: 1 1 0;
display: flex; display: flex;

12
wall.js
View File

@ -92,7 +92,7 @@
function wallDefaultState() { function wallDefaultState() {
// Default to wireframes on so empty cells are visible/clickable. // Default to wireframes on so empty cells are visible/clickable.
return { rows: 7, cols: 9, spacing: 75, bigSize: 52, pattern: 'grid', fillGaps: false, showWireframes: true, outline: false, colors: [], customColors: {}, patternStore: {}, activeColorIdx: 0 }; return { rows: 7, cols: 9, spacing: 75, bigSize: 52, pattern: 'grid', fillGaps: false, showWireframes: true, outline: true, colors: [], customColors: {}, patternStore: {}, activeColorIdx: 0 };
} }
// Build FLAT_COLORS locally if shared failed to populate (e.g., palette not ready) // Build FLAT_COLORS locally if shared failed to populate (e.g., palette not ready)
@ -439,8 +439,8 @@
const meta = wallColorMeta(gapIdx); const meta = wallColorMeta(gapIdx);
const patId = ensurePattern(meta); const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex); const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex);
const stroke = 'none'; const stroke = invisible || isEmpty ? 'none' : (showOutline ? '#111827' : 'none');
const strokeW = 0; const strokeW = invisible || isEmpty ? 0 : (showOutline ? 0.6 : 0);
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`; const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82; // slightly smaller 11" gap balloon const rGap = bigR * 0.82; // slightly smaller 11" gap balloon
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex); const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
@ -498,9 +498,9 @@
const patId = ensurePattern(meta); const patId = ensurePattern(meta);
const fill = invisibleLink ? 'rgba(0,0,0,0.001)' : (linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex)); const fill = invisibleLink ? 'rgba(0,0,0,0.001)' : (linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
console.log(`l#-r-c: keyId: ${linkKey}, customIdx: ${linkCustomIdx}, isEmpty: ${linkIsEmpty}, invisible: ${invisibleLink}, fill: ${fill}, meta:`, meta); console.log(`l#-r-c: keyId: ${linkKey}, customIdx: ${linkCustomIdx}, isEmpty: ${linkIsEmpty}, invisible: ${invisibleLink}, fill: ${fill}, meta:`, meta);
// Always outline X-pattern link ovals; thicken when outline toggle is on. // Outline only when filled; light wireframe when empty and wireframes shown.
const stroke = invisibleLink ? 'none' : (showOutline ? '#111827' : '#cbd5e1'); const stroke = invisibleLink ? 'none' : (linkIsEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
const strokeW = invisibleLink ? 0 : (showOutline ? 0.8 : 0.6); const strokeW = invisibleLink ? 0 : (linkIsEmpty ? (showWireframes ? 1.2 : 0) : (showOutline ? 0.8 : 0));
const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`; const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex); const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);