diff --git a/index.html b/index.html
index 0e78867..c0de395 100644
--- a/index.html
+++ b/index.html
@@ -41,11 +41,6 @@
@@ -79,11 +74,11 @@
-
-
-
-
-
+
+
+ Main Colors
+
+
+
Tap a chip to change it. You can add up to 10 main colors.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Accent
+
+
@@ -138,7 +119,7 @@
-
Drag balloons to reposition. Use keyboard arrows for fine nudges.
+
Drag balloons to reposition. Use arrows/touches for fine nudges.
Resize
@@ -185,7 +166,7 @@
Color Library
-
Alt+click on canvas to sample a balloon’s color.
+
Tap or click on canvas to sample a balloon’s color (use the eyedropper).
-
Paint applies the active color; Erase clears. Hold Shift/Ctrl for temporary erase.
+
Paint applies the active color; Erase clears. Hold modifier on desktop to erase temporarily.
@@ -477,7 +458,7 @@
- Tap a swatch to set. Tap a balloon to paint; tap again (same color) to clear. Alt+click (desktop) to pick.
+ Tap a swatch to set. Tap a balloon to paint; tap again (same color) to clear. Use the eyedropper to pick from the canvas.
Used Colors
diff --git a/organic.js b/organic.js
index fb82234..d310ee5 100644
--- a/organic.js
+++ b/organic.js
@@ -138,16 +138,10 @@
const fitViewBtn = document.getElementById('fit-view-btn');
const garlandDensityInput = document.getElementById('garland-density');
const garlandDensityLabel = document.getElementById('garland-density-label');
- const garlandColorMain1Sel = document.getElementById('garland-color-main1');
- const garlandColorMain2Sel = document.getElementById('garland-color-main2');
- const garlandColorMain3Sel = document.getElementById('garland-color-main3');
- const garlandColorMain4Sel = document.getElementById('garland-color-main4');
- 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 garlandMainChips = document.getElementById('garland-main-chips');
+ const garlandAddColorBtn = document.getElementById('garland-add-color');
+ const garlandAccentChip = document.getElementById('garland-accent-chip');
+ const garlandAccentClearBtn = document.getElementById('garland-accent-clear');
const garlandControls = document.getElementById('garland-controls');
const sizePresetGroup = document.getElementById('size-preset-group');
@@ -214,8 +208,8 @@
let usedSortDesc = true;
let garlandPath = [];
let garlandDensity = parseFloat(garlandDensityInput?.value || '1') || 1;
- let garlandMainIdx = [0, 0, 0, 0];
- let garlandAccentIdx = 0;
+ let garlandMainIdx = [0];
+ let garlandAccentIdx = -1;
let lastCommitMode = '';
let lastAddStatus = '';
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;
if (toolUndoBtn) {
toolUndoBtn.disabled = !canUndo;
- toolUndoBtn.title = canUndo ? 'Undo (Ctrl+Z)' : 'Nothing to undo';
+ toolUndoBtn.title = canUndo ? 'Undo' : 'Nothing to undo';
}
if (toolRedoBtn) {
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 (Array.isArray(s.garlandMainIdx)) {
- garlandMainIdx = s.garlandMainIdx.slice(0, 4).map(v => Number(v) || -1);
- while (garlandMainIdx.length < 4) garlandMainIdx.push(-1);
+ garlandMainIdx = s.garlandMainIdx.slice(0, 10).map(v => Number.isInteger(v) ? v : -1).filter((v, i) => i < 10);
+ if (!garlandMainIdx.length) garlandMainIdx = [selectedColorIdx];
}
if (typeof s.garlandAccentIdx === 'number') garlandAccentIdx = s.garlandAccentIdx;
if (typeof s.isBorderEnabled === 'boolean') isBorderEnabled = s.isBorderEnabled;
@@ -1010,6 +1004,109 @@
loadAppState();
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) ======
function renderAllowedPalette() {
if (!paletteBox) return;
@@ -1789,31 +1886,13 @@
if (mode === 'garland') requestDraw();
persist();
});
- const handleGarlandColorChange = () => {
- updateGarlandSwatches();
- persist();
+ const refreshGarlandColors = () => {
+ renderGarlandMainChips();
+ updateAccentChip();
if (mode === 'garland') requestDraw();
+ persist();
};
- garlandColorMain1Sel?.addEventListener('change', e => {
- 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();
- });
+ refreshGarlandColors();
deleteSelectedBtn?.addEventListener('click', deleteSelected);
duplicateSelectedBtn?.addEventListener('click', duplicateSelected);
@@ -1957,26 +2036,6 @@
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 fromHex = replaceFromSel?.value;
const toIdx = parseInt(replaceToSel?.value || '-1', 10);
diff --git a/script.js b/script.js
index db4a325..c7432df 100644
--- a/script.js
+++ b/script.js
@@ -7,7 +7,7 @@
// Ensure shared helpers are ready
if (!window.shared) return;
- const { clamp, clamp01 } = window.shared;
+ const { clamp, clamp01, SWATCH_TEXTURE_ZOOM } = window.shared;
const { FLAT_COLORS } = window.shared;
// Modal helpers
@@ -58,9 +58,10 @@
const setChipStyle = (el, meta) => {
if (!el || !meta) return;
if (meta.image) {
+ const zoom = Math.max(1, meta.imageZoom ?? SWATCH_TEXTURE_ZOOM ?? 2.5);
el.style.backgroundImage = `url("${meta.image}")`;
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}%`;
} else {
el.style.backgroundImage = 'none';
@@ -526,7 +527,8 @@
const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches;
const updateMobileActionBarVisibility = () => {
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);
};
const wireMobileActionButtons = () => {
diff --git a/style.css b/style.css
index cf49ab9..e2930f0 100644
--- a/style.css
+++ b/style.css
@@ -446,7 +446,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
display: block;
}
.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 */
.tool-btn,
@@ -480,7 +480,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
display: flex;
align-items: center;
gap: 0.5rem;
- z-index: 45;
+ z-index: 20; /* below control sheets (30) and modals (60) */
}
.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);
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 {
flex: 1 1 0;
display: flex;
diff --git a/wall.js b/wall.js
index de43af5..2e85109 100644
--- a/wall.js
+++ b/wall.js
@@ -92,7 +92,7 @@
function wallDefaultState() {
// 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)
@@ -439,8 +439,8 @@
const meta = wallColorMeta(gapIdx);
const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex);
- const stroke = 'none';
- const strokeW = 0;
+ const stroke = invisible || isEmpty ? 'none' : (showOutline ? '#111827' : 'none');
+ const strokeW = invisible || isEmpty ? 0 : (showOutline ? 0.6 : 0);
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82; // slightly smaller 11" gap balloon
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
@@ -498,9 +498,9 @@
const patId = ensurePattern(meta);
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);
- // Always outline X-pattern link ovals; thicken when outline toggle is on.
- const stroke = invisibleLink ? 'none' : (showOutline ? '#111827' : '#cbd5e1');
- const strokeW = invisibleLink ? 0 : (showOutline ? 0.8 : 0.6);
+ // Outline only when filled; light wireframe when empty and wireframes shown.
+ const stroke = invisibleLink ? 'none' : (linkIsEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
+ const strokeW = invisibleLink ? 0 : (linkIsEmpty ? (showWireframes ? 1.2 : 0) : (showOutline ? 0.8 : 0));
const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);