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 @@
-