From 346f6ff9175346a919299be2c36ac973c5cf34ee Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 18 Dec 2025 10:05:04 -0500 Subject: [PATCH] Add manual replace palette and fix shine for bright colors --- classic.js | 383 ++++++++++++++++++++++++++++++++++++++++++++++++++--- colors.js | 5 +- index.html | 106 +++++++++++++-- shared.js | 22 +++ style.css | 10 +- 5 files changed, 491 insertions(+), 35 deletions(-) diff --git a/classic.js b/classic.js index 00949b6..11cafb5 100644 --- a/classic.js +++ b/classic.js @@ -39,20 +39,25 @@ }); return 0.2126 * norm[0] + 0.7152 * norm[1] + 0.0722 * norm[2]; } + let manualModeState = false; let classicZoom = 1; const clampZoom = (z) => Math.min(2.2, Math.max(0.5, z)); + let currentPatternName = ''; + let currentRowCount = 0; + let manualUndoStack = []; + let manualRedoStack = []; function classicShineStyle(colorInfo) { const hex = normHex(colorInfo?.hex || colorInfo?.colour || ''); if (hex.startsWith('#')) { const lum = luminance(hex); + // For bright hues (yellows, pastels) avoid darkening which can skew green; use a light, soft highlight instead. if (lum > 0.7) { - const t = clamp01((lum - 0.7) / 0.3); - const fillAlpha = 0.22 + (0.10 - 0.22) * t; - return { - fill: `rgba(0,0,0,${fillAlpha})`, - opacity: 1, - stroke: null - }; + // Slightly stronger highlight for bright hues while staying neutral + return { fill: 'rgba(255,255,255,0.4)', opacity: 1, stroke: null }; + } + // Deep shades keep a stronger white highlight. + if (lum < 0.2) { + return { fill: 'rgba(255,255,255,0.55)', opacity: 1, stroke: null }; } } return { fill: '#ffffff', opacity: 0.45, stroke: null }; @@ -222,6 +227,33 @@ saveManualOverrides(manualOverrides); } } + function manualUsedColorsFor(patternName, rowCount) { + const key = manualKey(patternName, rowCount); + const overrides = manualOverrides[key] || {}; + const palette = buildClassicPalette(); + const out = []; + const seen = new Set(); + Object.values(overrides).forEach(val => { + let hex = null, image = null; + if (val && typeof val === 'object') { + hex = normHex(val.hex || val.colour || ''); + image = val.image || null; + } else if (typeof val === 'number') { + const info = palette[val] || null; + hex = normHex(info?.colour || info?.hex || ''); + image = info?.image || null; + } + if (!hex && !image) return; + const keyStr = `${image || ''}|${hex || ''}`; + if (seen.has(keyStr)) return; + seen.add(keyStr); + out.push({ hex, image, label: hex || (image ? 'Texture' : 'Color') }); + }); + return out; + } + // Manual palette (used in Manual mode project palette) + let projectPaletteBox = null; + let renderProjectPalette = () => {}; let manualActiveColorGlobal = (window.shared?.getActiveColor?.()) || { hex: '#ffffff', image: null }; function getTopperTypeSafe() { try { return (window.ClassicDesigner?.lastTopperType) || null; } catch { return null; } @@ -455,7 +487,7 @@ function distinctPaletteSlots(palette) { function newGrid(pattern, cells, container, model){ - const kids = [], layers = [], bbox = new BBox(), focusBox = new BBox(); + const kids = [], layers = [], bbox = new BBox(), focusBox = new BBox(), resetDots = new Map(); let floatingAnchor = null; let overrideCount = manualOverrideCount(model.patternName, model.rowCount); const balloonsPerCluster = pattern.balloonsPerCluster || 4; @@ -585,6 +617,7 @@ function distinctPaletteSlots(palette) { const depthLift = expandedOn ? ((cell.shape.zIndex || 0) * 1.8) : 0; const floatingOut = model.manualMode && model.manualFloatingQuad === cell.y; if (floatingOut) { + if (!resetDots.has(cell.y)) resetDots.set(cell.y, { x: c.x, y: c.y }); const isArch = (model.patternName || '').toLowerCase().includes('arch'); let slideX = 80; let slideY = 0; @@ -610,8 +643,8 @@ function distinctPaletteSlots(palette) { if (isArch) { // no fan/scale for arches; preserve layout } else { - tx += spread * 12; - ty += spread * 10; + tx += spread * 4; + ty += spread * 4; } const fanScale = 1; // Nudge the top pair down slightly in columns so they remain easily clickable. @@ -676,6 +709,15 @@ function distinctPaletteSlots(palette) { }; layers.forEach(layer => layer && layer.forEach(v => kids.push(v))); + // Add reset dots for floated quads (one per floating row) at their original position. + if (resetDots.size) { + resetDots.forEach(({ x, y }) => { + kids.push(svg('g', { transform: `translate(${x},${y})`, style: 'cursor:pointer' , onclick: 'window.ClassicDesigner?.resetFloatingQuad?.()' }, [ + svg('circle', { cx: 0, cy: 0, r: 10, fill: 'rgba(37,99,235,0.12)', stroke: '#2563eb', 'stroke-width': 2 }), + svg('circle', { cx: 0, cy: 0, r: 3.5, fill: '#2563eb' }) + ])); + }); + } // Keep a modest margin when a quad is floated so the design doesn’t shrink too much. const margin = (model.manualMode && model.manualFloatingQuad !== null) ? 40 : 20; const focusValid = isFinite(focusBox.min.x) && isFinite(focusBox.min.y) && focusBox.w() > 0 && focusBox.h() > 0; @@ -968,6 +1010,13 @@ function distinctPaletteSlots(palette) { function initClassicColorPicker(onColorChange) { const slotsContainer = document.getElementById('classic-slots'), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors'), addSlotBtn = document.getElementById('classic-add-slot'), activeChip = document.getElementById('classic-active-chip'), floatingChip = document.getElementById('classic-active-chip-floating'), activeDot = document.getElementById('classic-active-dot'), floatingDot = document.getElementById('classic-active-dot-floating'); + const replaceFromSel = document.getElementById('classic-replace-from'); + const replaceToSel = document.getElementById('classic-replace-to'); + const replaceBtn = document.getElementById('classic-replace-btn'); + const replaceMsg = document.getElementById('classic-replace-msg'); + const replaceFromChip = document.getElementById('classic-replace-from-chip'); + const replaceToChip = document.getElementById('classic-replace-to-chip'); + const replaceCountLabel = document.getElementById('classic-replace-count'); const numberTintSlider = document.getElementById('classic-number-tint'); const topperBlock = document.getElementById('classic-topper-color-block'); if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return; @@ -1033,6 +1082,187 @@ function distinctPaletteSlots(palette) { if (parseInt(activeTarget, 10) > count) activeTarget = '1'; renderSlots(); } + const allPaletteColors = flattenPalette(); + + const colorKeyFromVal = (val) => { + const palette = buildClassicPalette(); + let hex = null, image = null; + if (val && typeof val === 'object') { + hex = normHex(val.hex || val.colour || ''); + image = val.image || null; + } else if (typeof val === 'number') { + const info = palette[val] || null; + hex = normHex(info?.colour || info?.hex || ''); + image = info?.image || null; + } + const cleanedHex = (hex === 'transparent' || hex === 'none') ? '' : (hex || ''); + const key = (image || cleanedHex) ? `${image || ''}|${cleanedHex}` : ''; + return { hex: cleanedHex, image: image || null, key }; + }; + + const manualUsage = () => { + if (!manualModeState) return []; + const palette = buildClassicPalette(); + const map = new Map(); + const cells = Array.from(document.querySelectorAll('#classic-display g[id^="balloon_"]')); + cells.forEach(g => { + const match = g.id.match(/balloon_(\d+)_(\d+)/); + if (!match) return; + const x = parseInt(match[1], 10); + const y = parseInt(match[2], 10); + const override = getManualOverride(currentPatternName, currentRowCount, x, y); + const code = parseInt(g.getAttribute('data-color-code') || '0', 10); + const base = palette[code] || { hex: '#ffffff', image: null }; + const fill = override || base; + const { hex, image, key: k } = colorKeyFromVal(fill); + if (!k) return; + const existing = map.get(k) || { hex, image, count: 0 }; + existing.count += 1; + map.set(k, existing); + }); + return Array.from(map.values()); + }; + + const setReplaceChip = (chip, color) => { + if (!chip) return; + if (color?.image) { + chip.style.backgroundImage = `url("${color.image}")`; + chip.style.backgroundSize = 'cover'; + chip.style.backgroundColor = color.hex || '#fff'; + } else { + chip.style.backgroundImage = 'none'; + chip.style.backgroundColor = color?.hex || '#f1f5f9'; + } + }; + + const populateReplaceTo = () => { + if (!replaceToSel) return; + replaceToSel.innerHTML = ''; + allPaletteColors.forEach((c, idx) => { + const opt = document.createElement('option'); + opt.value = String(idx); + opt.textContent = c.name || c.hex || (c.image ? 'Texture' : 'Color'); + replaceToSel.appendChild(opt); + }); + }; + + const updateReplaceChips = () => { + if (!replaceFromSel || !replaceToSel) return 0; + if (!manualModeState) { + replaceFromSel.innerHTML = ''; + setReplaceChip(replaceFromChip, { hex: '#f8fafc' }); + setReplaceChip(replaceToChip, { hex: '#f8fafc' }); + if (replaceCountLabel) replaceCountLabel.textContent = ''; + if (replaceMsg) replaceMsg.textContent = 'Manual paint only.'; + return 0; + } + const usage = manualUsage(); + replaceFromSel.innerHTML = ''; + usage.forEach(u => { + const opt = document.createElement('option'); + opt.value = `${u.image || ''}|${u.hex || ''}`; + const labelHex = u.hex || (u.image ? 'Texture' : 'Color'); + opt.textContent = `${labelHex} (${u.count})`; + replaceFromSel.appendChild(opt); + }); + if (!replaceFromSel.value && usage.length) replaceFromSel.value = `${usage[0].image || ''}|${usage[0].hex || ''}`; + if (!replaceToSel.value && replaceToSel.options.length) replaceToSel.value = replaceToSel.options[0].value; + + const toIdx = parseInt(replaceToSel.value || '-1', 10); + const toMeta = Number.isInteger(toIdx) && toIdx >= 0 ? allPaletteColors[toIdx] : null; + const fromVal = replaceFromSel.value || ''; + const fromParts = fromVal.split('|'); + const fromColor = { image: fromParts[0] || null, hex: fromParts[1] || '' }; + setReplaceChip(replaceFromChip, fromColor); + setReplaceChip(replaceToChip, toMeta ? { hex: toMeta.hex || '#f1f5f9', image: toMeta.image || null } : { hex: '#f1f5f9' }); + + // count matches + let count = 0; + if (fromVal) { + const usage = manualUsage(); + usage.forEach(u => { if (`${u.image || ''}|${u.hex || ''}` === fromVal) count += u.count; }); + } + if (replaceCountLabel) replaceCountLabel.textContent = count ? `${count} match${count === 1 ? '' : 'es'}` : '0 matches'; + if (replaceMsg) replaceMsg.textContent = usage.length ? '' : 'Paint something first to replace.'; + return count; + }; + + const openReplacePicker = (mode = 'from') => { + if (!window.openColorPicker) return; + if (mode === 'from') { + const usage = manualUsage(); + const items = usage.map(u => ({ + label: u.hex || (u.image ? 'Texture' : 'Color'), + metaText: `${u.count} in design`, + value: `${u.image || ''}|${u.hex || ''}`, + hex: u.hex || '#ffffff', + meta: { image: u.image, hex: u.hex || '#ffffff' }, + image: u.image + })); + window.openColorPicker({ + title: 'Replace: From color', + subtitle: 'Pick a color already on canvas', + items, + onSelect: (item) => { + if (!replaceFromSel) return; + replaceFromSel.value = item.value; + updateReplaceChips(); + } + }); + } else { + const items = allPaletteColors.map((c, idx) => ({ + label: c.name || c.hex || (c.image ? 'Texture' : 'Color'), + metaText: c.family || '', + idx + })); + window.openColorPicker({ + title: 'Replace: To color', + subtitle: 'Choose a library color', + items, + onSelect: (item) => { + if (!replaceToSel) return; + replaceToSel.value = String(item.idx); + updateReplaceChips(); + } + }); + } + }; + + renderProjectPalette = function renderProjectPaletteFn() { + if (!projectPaletteBox) return; + projectPaletteBox.innerHTML = ''; + if (!manualModeState) { + projectPaletteBox.innerHTML = '
Enter Manual paint to see colors used.
'; + return; + } + const used = manualUsedColorsFor(currentPatternName, currentRowCount); + if (!used.length) { + projectPaletteBox.innerHTML = '
Paint to build a project palette.
'; + return; + } + const row = document.createElement('div'); + row.className = 'swatch-row'; + used.forEach(item => { + const sw = document.createElement('button'); + sw.type = 'button'; + sw.className = 'swatch'; + if (item.image) { + sw.style.backgroundImage = `url("${item.image}")`; + sw.style.backgroundSize = '500%'; + sw.style.backgroundPosition = 'center'; + sw.style.backgroundColor = item.hex || '#fff'; + } else { + sw.style.backgroundColor = item.hex || '#fff'; + } + sw.title = item.label || item.hex || 'Color'; + sw.addEventListener('click', () => { + manualActiveColorGlobal = window.shared?.setActiveColor?.({ hex: item.hex || '#ffffff', image: item.image || null }) || { hex: item.hex || '#ffffff', image: item.image || null }; + updateClassicDesign(); + }); + row.appendChild(sw); + }); + projectPaletteBox.appendChild(row); + } function updateUI() { enforceSlotVisibility(); @@ -1120,9 +1350,11 @@ function distinctPaletteSlots(palette) { if (activeChip) { activeChip.style.display = manualModeOn ? '' : 'none'; } + if (projectPaletteBox) { + projectPaletteBox.parentElement?.classList.toggle('hidden', !manualModeOn); + } } - const allPaletteColors = flattenPalette(); swatchGrid.innerHTML = ''; swatchGrid.style.display = 'none'; // hide inline list; use modal picker instead @@ -1173,6 +1405,10 @@ function distinctPaletteSlots(palette) { openPalettePicker(); }); randomizeBtn?.addEventListener('click', () => { + if (isManual() && window.ClassicDesigner?.randomizeManualFromPalette) { + const applied = window.ClassicDesigner.randomizeManualFromPalette(); + if (applied) return; + } const pool = allPaletteColors.slice(); const picks = []; const colorCount = visibleSlotCount(); for (let i = 0; i < colorCount && pool.length; i++) { picks.push(pool.splice(Math.floor(Math.random() * pool.length), 1)[0]); } @@ -1198,14 +1434,59 @@ function distinctPaletteSlots(palette) { updateUI(); onColorChange(); if (window.updateExportButtonVisibility) window.updateExportButtonVisibility(); }); + replaceFromChip?.addEventListener('click', () => openReplacePicker('from')); + replaceToChip?.addEventListener('click', () => openReplacePicker('to')); + replaceFromSel?.addEventListener('change', updateReplaceChips); + replaceToSel?.addEventListener('change', updateReplaceChips); + replaceBtn?.addEventListener('click', () => { + if (!manualModeState) { if (replaceMsg) replaceMsg.textContent = 'Manual paint only.'; return; } + const fromKey = replaceFromSel?.value || ''; + const toIdx = parseInt(replaceToSel?.value || '-1', 10); + if (!fromKey || Number.isNaN(toIdx) || toIdx < 0 || toIdx >= allPaletteColors.length) { if (replaceMsg) replaceMsg.textContent = 'Pick both colors.'; return; } + const toMeta = allPaletteColors[toIdx]; + const key = manualKey(currentPatternName, currentRowCount); + const prevSnapshot = manualOverrides[key] ? { ...manualOverrides[key] } : null; + if (!manualOverrides[key]) manualOverrides[key] = {}; + const cells = Array.from(document.querySelectorAll('#classic-display g[id^="balloon_"]')); + const palette = buildClassicPalette(); + let count = 0; + cells.forEach(g => { + const match = g.id.match(/balloon_(\d+)_(\d+)/); + if (!match) return; + const x = parseInt(match[1], 10); + const y = parseInt(match[2], 10); + const override = getManualOverride(currentPatternName, currentRowCount, x, y); + const code = parseInt(g.getAttribute('data-color-code') || '0', 10); + const base = palette[code] || { hex: '#ffffff', image: null }; + const fill = override || base; + if (colorKeyFromVal(fill).key === fromKey) { + manualOverrides[key][`${x},${y}`] = { + hex: normHex(toMeta.hex || toMeta.colour || '#ffffff'), + image: toMeta.image || null + }; + count++; + } + }); + if (!count) { if (replaceMsg) replaceMsg.textContent = 'Nothing to replace.'; return; } + saveManualOverrides(manualOverrides); + manualUndoStack.push({ clear: true, pattern: currentPatternName, rows: currentRowCount, snapshot: prevSnapshot }); + manualRedoStack.length = 0; + if (replaceMsg) replaceMsg.textContent = `Replaced ${count} balloon${count === 1 ? '' : 's'}.`; + onColorChange(); + updateReplaceChips(); + }); + + populateReplaceTo(); updateUI(); - return updateUI; + updateReplaceChips(); + return () => { updateUI(); updateReplaceChips(); }; } - function initClassic() { - try { - if (typeof window.m === 'undefined') return fail('Mithril not loaded'); - const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display'); + function initClassic() { + try { + if (typeof window.m === 'undefined') return fail('Mithril not loaded'); + projectPaletteBox = null; + const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display'); const numberTintRow = document.getElementById('classic-number-tint-row'), numberTintSlider = document.getElementById('classic-number-tint'); const nudgeOpenBtn = document.getElementById('classic-nudge-open'); const fullscreenBtn = document.getElementById('app-fullscreen-toggle'); @@ -1224,21 +1505,24 @@ function distinctPaletteSlots(palette) { const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper')); const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn')); const slotsContainer = document.getElementById('classic-slots'); + projectPaletteBox = document.getElementById('classic-project-palette'); + const manualPaletteBtn = document.getElementById('classic-manual-palette'); let topperOffsetX = 0, topperOffsetY = 0; let lastPresetKey = null; // 'custom' means user-tweaked; otherwise `${pattern}:${type}` window.ClassicDesigner = window.ClassicDesigner || {}; window.ClassicDesigner.lastTopperType = window.ClassicDesigner.lastTopperType || 'round'; + window.ClassicDesigner.resetFloatingQuad = () => { manualFloatingQuad = null; updateClassicDesign(); }; let patternShape = 'arch', patternCount = 4, patternLayout = 'spiral', lastNonManualLayout = 'spiral'; - let manualModeState = loadManualMode(); + manualModeState = loadManualMode(); let manualExpandedState = loadManualExpanded(); let manualFocusEnabled = false; // start with full design visible; focus toggles when user targets a cluster manualActiveColorGlobal = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null }; - let currentPatternName = ''; - let currentRowCount = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2)); + currentPatternName = ''; + currentRowCount = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2)); let manualFocusStart = 0; const manualFocusSize = 8; - const manualUndoStack = []; - const manualRedoStack = []; + manualUndoStack = []; + manualRedoStack = []; let manualTool = 'paint'; // paint | pick | erase let manualFloatingQuad = null; let quadModalRow = null; @@ -1246,6 +1530,33 @@ function distinctPaletteSlots(palette) { let manualDetailRow = 0; let manualDetailFrame = null; classicZoom = 1; + window.ClassicDesigner = window.ClassicDesigner || {}; + window.ClassicDesigner.randomizeManualFromPalette = () => { + if (!manualModeState) return false; + const used = manualUsedColorsFor(currentPatternName, currentRowCount); + const source = (used.length ? used : flattenPalette().map(c => ({ hex: c.hex, image: c.image || null }))).filter(Boolean); + if (!source.length) return false; + const cells = Array.from(document.querySelectorAll('#classic-display g[id^="balloon_"]')); + if (!cells.length) return false; + const key = manualKey(currentPatternName, currentRowCount); + const prevSnapshot = manualOverrides[key] ? { ...manualOverrides[key] } : null; + manualUndoStack.push({ clear: true, pattern: currentPatternName, rows: currentRowCount, snapshot: prevSnapshot }); + manualRedoStack.length = 0; + manualOverrides[key] = {}; + cells.forEach(g => { + const match = g.id.match(/balloon_(\d+)_(\d+)/); + if (!match) return; + const pick = source[Math.floor(Math.random() * source.length)] || { hex: '#ffffff', image: null }; + manualOverrides[key][`${parseInt(match[1], 10)},${parseInt(match[2], 10)}`] = { + hex: normHex(pick.hex || pick.colour || '#ffffff'), + image: pick.image || null + }; + }); + saveManualOverrides(manualOverrides); + updateClassicDesign(); + scheduleManualDetail(); + return true; + }; // Force UI to reflect initial manual state if (manualModeState) patternLayout = 'manual'; if (numberTintSlider) numberTintSlider.value = getNumberTintOpacity(); @@ -1732,6 +2043,7 @@ function distinctPaletteSlots(palette) { refreshClassicPaletteUi?.(); ctrl.selectPattern(patternName); syncManualUi(); + renderProjectPalette(); scheduleManualDetail(); persistState(); } @@ -1909,6 +2221,25 @@ function distinctPaletteSlots(palette) { debug('manual full view'); }); manualFocusBtn?.addEventListener('click', () => setManualTargetRow(manualDetailRow)); + manualPaletteBtn?.addEventListener('click', () => { + if (!window.openColorPicker) return; + const items = flattenPalette().map(c => ({ + label: c.name || c.hex, + hex: c.hex, + meta: c, + metaText: c.family || '' + })); + window.openColorPicker({ + title: 'Manual paint color', + subtitle: 'Applies to manual paint tool', + items, + onSelect: (item) => { + const meta = item.meta || {}; + manualActiveColorGlobal = window.shared?.setActiveColor?.({ hex: meta.hex || item.hex, image: meta.image || null }) || { hex: meta.hex || item.hex, image: meta.image || null }; + updateClassicDesign(); + } + }); + }); // Keep detail view in sync after initial render when manual mode is pre-enabled if (manualModeState) { scheduleManualDetail(); @@ -1937,6 +2268,16 @@ function distinctPaletteSlots(palette) { document.querySelector('[data-export="png"]')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); floatingUndo?.addEventListener('click', undoLastManual); + // Reset floated quad by clicking empty canvas area. + display?.addEventListener('click', (e) => { + if (!manualModeState) return; + const hit = e.target?.closest?.('g[id^="balloon_"], [data-quad-number]'); + if (hit) return; + if (manualFloatingQuad !== null) { + manualFloatingQuad = null; + updateClassicDesign(); + } + }); // Zoom: wheel and pinch on the display const handleZoom = (factor) => { classicZoom = clampZoom(classicZoom * factor); diff --git a/colors.js b/colors.js index 73a5692..c7f8401 100644 --- a/colors.js +++ b/colors.js @@ -12,7 +12,8 @@ const PALETTE = [ ]}, { family: "Oranges & Browns & Yellows", colors: [ {name:"Pastel Yellow",hex:"#fcfd96"},{name:"Yellow",hex:"#f5e812"},{name:"Goldenrod",hex:"#f7b615"}, - {name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"} + {name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"}, + {name:"Blended Brown",hex:"#c9aea0"} ]}, { family: "Greens", colors: [ {name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"}, @@ -63,4 +64,4 @@ const PALETTE = [ ]; window.CLASSIC_COLORS = ['#D92E3A', '#FFFFFF', '#0055A4', '#40E0D0']; - window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []); \ No newline at end of file + window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []); diff --git a/index.html b/index.html index c0de395..7bbfb1a 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,7 @@ -
+
@@ -251,9 +251,24 @@
Layout
-
+
+ +
+ +
+ + +

+
+
Pick a color for Slot #1 (from colors.js):
+
+
+ +