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 @@
- - @@ -99,31 +94,17 @@ 1.0
-
-
- - - +
+
+ 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).

Active Color
@@ -447,7 +428,7 @@ Show wireframe for empty spots
@@ -464,7 +445,7 @@ Erase
-

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);