exploded-classic #1
383
classic.js
383
classic.js
@ -39,20 +39,25 @@
|
|||||||
});
|
});
|
||||||
return 0.2126 * norm[0] + 0.7152 * norm[1] + 0.0722 * norm[2];
|
return 0.2126 * norm[0] + 0.7152 * norm[1] + 0.0722 * norm[2];
|
||||||
}
|
}
|
||||||
|
let manualModeState = false;
|
||||||
let classicZoom = 1;
|
let classicZoom = 1;
|
||||||
const clampZoom = (z) => Math.min(2.2, Math.max(0.5, z));
|
const clampZoom = (z) => Math.min(2.2, Math.max(0.5, z));
|
||||||
|
let currentPatternName = '';
|
||||||
|
let currentRowCount = 0;
|
||||||
|
let manualUndoStack = [];
|
||||||
|
let manualRedoStack = [];
|
||||||
function classicShineStyle(colorInfo) {
|
function classicShineStyle(colorInfo) {
|
||||||
const hex = normHex(colorInfo?.hex || colorInfo?.colour || '');
|
const hex = normHex(colorInfo?.hex || colorInfo?.colour || '');
|
||||||
if (hex.startsWith('#')) {
|
if (hex.startsWith('#')) {
|
||||||
const lum = luminance(hex);
|
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) {
|
if (lum > 0.7) {
|
||||||
const t = clamp01((lum - 0.7) / 0.3);
|
// Slightly stronger highlight for bright hues while staying neutral
|
||||||
const fillAlpha = 0.22 + (0.10 - 0.22) * t;
|
return { fill: 'rgba(255,255,255,0.4)', opacity: 1, stroke: null };
|
||||||
return {
|
}
|
||||||
fill: `rgba(0,0,0,${fillAlpha})`,
|
// Deep shades keep a stronger white highlight.
|
||||||
opacity: 1,
|
if (lum < 0.2) {
|
||||||
stroke: null
|
return { fill: 'rgba(255,255,255,0.55)', opacity: 1, stroke: null };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { fill: '#ffffff', opacity: 0.45, stroke: null };
|
return { fill: '#ffffff', opacity: 0.45, stroke: null };
|
||||||
@ -222,6 +227,33 @@
|
|||||||
saveManualOverrides(manualOverrides);
|
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 };
|
let manualActiveColorGlobal = (window.shared?.getActiveColor?.()) || { hex: '#ffffff', image: null };
|
||||||
function getTopperTypeSafe() {
|
function getTopperTypeSafe() {
|
||||||
try { return (window.ClassicDesigner?.lastTopperType) || null; } catch { return null; }
|
try { return (window.ClassicDesigner?.lastTopperType) || null; } catch { return null; }
|
||||||
@ -455,7 +487,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
|
|
||||||
function newGrid(pattern, cells, container, model){
|
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 floatingAnchor = null;
|
||||||
let overrideCount = manualOverrideCount(model.patternName, model.rowCount);
|
let overrideCount = manualOverrideCount(model.patternName, model.rowCount);
|
||||||
const balloonsPerCluster = pattern.balloonsPerCluster || 4;
|
const balloonsPerCluster = pattern.balloonsPerCluster || 4;
|
||||||
@ -585,6 +617,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
const depthLift = expandedOn ? ((cell.shape.zIndex || 0) * 1.8) : 0;
|
const depthLift = expandedOn ? ((cell.shape.zIndex || 0) * 1.8) : 0;
|
||||||
const floatingOut = model.manualMode && model.manualFloatingQuad === cell.y;
|
const floatingOut = model.manualMode && model.manualFloatingQuad === cell.y;
|
||||||
if (floatingOut) {
|
if (floatingOut) {
|
||||||
|
if (!resetDots.has(cell.y)) resetDots.set(cell.y, { x: c.x, y: c.y });
|
||||||
const isArch = (model.patternName || '').toLowerCase().includes('arch');
|
const isArch = (model.patternName || '').toLowerCase().includes('arch');
|
||||||
let slideX = 80;
|
let slideX = 80;
|
||||||
let slideY = 0;
|
let slideY = 0;
|
||||||
@ -610,8 +643,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (isArch) {
|
if (isArch) {
|
||||||
// no fan/scale for arches; preserve layout
|
// no fan/scale for arches; preserve layout
|
||||||
} else {
|
} else {
|
||||||
tx += spread * 12;
|
tx += spread * 4;
|
||||||
ty += spread * 10;
|
ty += spread * 4;
|
||||||
}
|
}
|
||||||
const fanScale = 1;
|
const fanScale = 1;
|
||||||
// Nudge the top pair down slightly in columns so they remain easily clickable.
|
// 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)));
|
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.
|
// 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 margin = (model.manualMode && model.manualFloatingQuad !== null) ? 40 : 20;
|
||||||
const focusValid = isFinite(focusBox.min.x) && isFinite(focusBox.min.y) && focusBox.w() > 0 && focusBox.h() > 0;
|
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) {
|
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 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 numberTintSlider = document.getElementById('classic-number-tint');
|
||||||
const topperBlock = document.getElementById('classic-topper-color-block');
|
const topperBlock = document.getElementById('classic-topper-color-block');
|
||||||
if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
|
if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
|
||||||
@ -1033,6 +1082,187 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (parseInt(activeTarget, 10) > count) activeTarget = '1';
|
if (parseInt(activeTarget, 10) > count) activeTarget = '1';
|
||||||
renderSlots();
|
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 = '<div class="hint text-xs">Enter Manual paint to see colors used.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const used = manualUsedColorsFor(currentPatternName, currentRowCount);
|
||||||
|
if (!used.length) {
|
||||||
|
projectPaletteBox.innerHTML = '<div class="hint text-xs">Paint to build a project palette.</div>';
|
||||||
|
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() {
|
function updateUI() {
|
||||||
enforceSlotVisibility();
|
enforceSlotVisibility();
|
||||||
@ -1120,9 +1350,11 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (activeChip) {
|
if (activeChip) {
|
||||||
activeChip.style.display = manualModeOn ? '' : 'none';
|
activeChip.style.display = manualModeOn ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
if (projectPaletteBox) {
|
||||||
|
projectPaletteBox.parentElement?.classList.toggle('hidden', !manualModeOn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPaletteColors = flattenPalette();
|
|
||||||
swatchGrid.innerHTML = '';
|
swatchGrid.innerHTML = '';
|
||||||
swatchGrid.style.display = 'none'; // hide inline list; use modal picker instead
|
swatchGrid.style.display = 'none'; // hide inline list; use modal picker instead
|
||||||
|
|
||||||
@ -1173,6 +1405,10 @@ function distinctPaletteSlots(palette) {
|
|||||||
openPalettePicker();
|
openPalettePicker();
|
||||||
});
|
});
|
||||||
randomizeBtn?.addEventListener('click', () => {
|
randomizeBtn?.addEventListener('click', () => {
|
||||||
|
if (isManual() && window.ClassicDesigner?.randomizeManualFromPalette) {
|
||||||
|
const applied = window.ClassicDesigner.randomizeManualFromPalette();
|
||||||
|
if (applied) return;
|
||||||
|
}
|
||||||
const pool = allPaletteColors.slice(); const picks = [];
|
const pool = allPaletteColors.slice(); const picks = [];
|
||||||
const colorCount = visibleSlotCount();
|
const colorCount = visibleSlotCount();
|
||||||
for (let i = 0; i < colorCount && pool.length; i++) { picks.push(pool.splice(Math.floor(Math.random() * pool.length), 1)[0]); }
|
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();
|
updateUI(); onColorChange();
|
||||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
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();
|
updateUI();
|
||||||
return updateUI;
|
updateReplaceChips();
|
||||||
|
return () => { updateUI(); updateReplaceChips(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
function initClassic() {
|
function initClassic() {
|
||||||
try {
|
try {
|
||||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
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');
|
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 numberTintRow = document.getElementById('classic-number-tint-row'), numberTintSlider = document.getElementById('classic-number-tint');
|
||||||
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
||||||
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
||||||
@ -1224,21 +1505,24 @@ function distinctPaletteSlots(palette) {
|
|||||||
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
||||||
const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
|
const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
|
||||||
const slotsContainer = document.getElementById('classic-slots');
|
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 topperOffsetX = 0, topperOffsetY = 0;
|
||||||
let lastPresetKey = null; // 'custom' means user-tweaked; otherwise `${pattern}:${type}`
|
let lastPresetKey = null; // 'custom' means user-tweaked; otherwise `${pattern}:${type}`
|
||||||
window.ClassicDesigner = window.ClassicDesigner || {};
|
window.ClassicDesigner = window.ClassicDesigner || {};
|
||||||
window.ClassicDesigner.lastTopperType = window.ClassicDesigner.lastTopperType || 'round';
|
window.ClassicDesigner.lastTopperType = window.ClassicDesigner.lastTopperType || 'round';
|
||||||
|
window.ClassicDesigner.resetFloatingQuad = () => { manualFloatingQuad = null; updateClassicDesign(); };
|
||||||
let patternShape = 'arch', patternCount = 4, patternLayout = 'spiral', lastNonManualLayout = 'spiral';
|
let patternShape = 'arch', patternCount = 4, patternLayout = 'spiral', lastNonManualLayout = 'spiral';
|
||||||
let manualModeState = loadManualMode();
|
manualModeState = loadManualMode();
|
||||||
let manualExpandedState = loadManualExpanded();
|
let manualExpandedState = loadManualExpanded();
|
||||||
let manualFocusEnabled = false; // start with full design visible; focus toggles when user targets a cluster
|
let manualFocusEnabled = false; // start with full design visible; focus toggles when user targets a cluster
|
||||||
manualActiveColorGlobal = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
manualActiveColorGlobal = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
||||||
let currentPatternName = '';
|
currentPatternName = '';
|
||||||
let currentRowCount = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
|
currentRowCount = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
|
||||||
let manualFocusStart = 0;
|
let manualFocusStart = 0;
|
||||||
const manualFocusSize = 8;
|
const manualFocusSize = 8;
|
||||||
const manualUndoStack = [];
|
manualUndoStack = [];
|
||||||
const manualRedoStack = [];
|
manualRedoStack = [];
|
||||||
let manualTool = 'paint'; // paint | pick | erase
|
let manualTool = 'paint'; // paint | pick | erase
|
||||||
let manualFloatingQuad = null;
|
let manualFloatingQuad = null;
|
||||||
let quadModalRow = null;
|
let quadModalRow = null;
|
||||||
@ -1246,6 +1530,33 @@ function distinctPaletteSlots(palette) {
|
|||||||
let manualDetailRow = 0;
|
let manualDetailRow = 0;
|
||||||
let manualDetailFrame = null;
|
let manualDetailFrame = null;
|
||||||
classicZoom = 1;
|
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
|
// Force UI to reflect initial manual state
|
||||||
if (manualModeState) patternLayout = 'manual';
|
if (manualModeState) patternLayout = 'manual';
|
||||||
if (numberTintSlider) numberTintSlider.value = getNumberTintOpacity();
|
if (numberTintSlider) numberTintSlider.value = getNumberTintOpacity();
|
||||||
@ -1732,6 +2043,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
refreshClassicPaletteUi?.();
|
refreshClassicPaletteUi?.();
|
||||||
ctrl.selectPattern(patternName);
|
ctrl.selectPattern(patternName);
|
||||||
syncManualUi();
|
syncManualUi();
|
||||||
|
renderProjectPalette();
|
||||||
scheduleManualDetail();
|
scheduleManualDetail();
|
||||||
persistState();
|
persistState();
|
||||||
}
|
}
|
||||||
@ -1909,6 +2221,25 @@ function distinctPaletteSlots(palette) {
|
|||||||
debug('manual full view');
|
debug('manual full view');
|
||||||
});
|
});
|
||||||
manualFocusBtn?.addEventListener('click', () => setManualTargetRow(manualDetailRow));
|
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
|
// Keep detail view in sync after initial render when manual mode is pre-enabled
|
||||||
if (manualModeState) {
|
if (manualModeState) {
|
||||||
scheduleManualDetail();
|
scheduleManualDetail();
|
||||||
@ -1937,6 +2268,16 @@ function distinctPaletteSlots(palette) {
|
|||||||
document.querySelector('[data-export="png"]')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
document.querySelector('[data-export="png"]')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
});
|
});
|
||||||
floatingUndo?.addEventListener('click', undoLastManual);
|
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
|
// Zoom: wheel and pinch on the display
|
||||||
const handleZoom = (factor) => {
|
const handleZoom = (factor) => {
|
||||||
classicZoom = clampZoom(classicZoom * factor);
|
classicZoom = clampZoom(classicZoom * factor);
|
||||||
|
|||||||
@ -12,7 +12,8 @@ const PALETTE = [
|
|||||||
]},
|
]},
|
||||||
{ family: "Oranges & Browns & Yellows", colors: [
|
{ family: "Oranges & Browns & Yellows", colors: [
|
||||||
{name:"Pastel Yellow",hex:"#fcfd96"},{name:"Yellow",hex:"#f5e812"},{name:"Goldenrod",hex:"#f7b615"},
|
{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: [
|
{ family: "Greens", colors: [
|
||||||
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},
|
{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.CLASSIC_COLORS = ['#D92E3A', '#FFFFFF', '#0055A4', '#40E0D0'];
|
||||||
window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []);
|
window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []);
|
||||||
|
|||||||
106
index.html
106
index.html
@ -24,7 +24,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="p-0 md:p-6 flex flex-col items-center justify-start min-h-screen bg-[conic-gradient(at_top_left,_var(--tw-gradient-stops))] from-indigo-100 via-white to-pink-100 text-slate-800 overflow-hidden">
|
<body class="p-0 md:p-6 flex flex-col items-center justify-start min-h-screen bg-[conic-gradient(at_top_left,_var(--tw-gradient-stops))] from-indigo-100 via-white to-pink-100 text-slate-800 overflow-hidden">
|
||||||
<div class="container mx-auto mt-2 p-4 lg:p-6 bg-white/80 lg:backdrop-blur-xl rounded-3xl border border-white/50 shadow-2xl flex flex-col gap-4 max-w-7xl lg:h-[calc(100vh-2rem)] overflow-hidden ring-1 ring-black/5">
|
<div class="container mx-auto mt-2 p-4 lg:p-6 bg-white/80 lg:backdrop-blur-xl rounded-3xl border border-white/50 shadow-2xl flex flex-col gap-4 max-w-7xl lg:h-[calc(97vh-2rem)] overflow-hidden ring-1 ring-black/5">
|
||||||
|
|
||||||
<header id="app-header" class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
|
<header id="app-header" class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@ -251,9 +251,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2 space-y-2">
|
<div class="md:col-span-2 space-y-2">
|
||||||
<div class="text-sm font-medium text-gray-700">Layout</div>
|
<div class="text-sm font-medium text-gray-700">Layout</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-layout="spiral" aria-pressed="true">Spiral</button>
|
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-layout="spiral" aria-pressed="true">Spiral</button>
|
||||||
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="stacked" aria-pressed="false">Stacked</button>
|
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="stacked" aria-pressed="false">Stacked</button>
|
||||||
|
<button type="button" class="tab-btn tab-idle" id="classic-manual-btn" aria-pressed="false">Manual paint</button>
|
||||||
|
</div>
|
||||||
|
<div id="classic-expanded-row" class="flex items-center gap-2 hidden">
|
||||||
|
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
||||||
|
<input id="classic-expanded-toggle" type="checkbox" class="align-middle" checked>
|
||||||
|
Expanded spacing
|
||||||
|
</label>
|
||||||
|
<p class="hint m-0">Separate clusters for easier taps.</p>
|
||||||
|
</div>
|
||||||
|
<div id="classic-focus-row" class="flex items-center gap-2 hidden">
|
||||||
|
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-prev" aria-hidden="true" tabindex="-1">◀ Prev</button>
|
||||||
|
<span id="classic-focus-label" class="text-sm text-gray-700">Clusters 1–8</span>
|
||||||
|
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-next" aria-hidden="true" tabindex="-1">Next ▶</button>
|
||||||
|
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-zoomout" aria-hidden="true" tabindex="-1">Zoom Out</button>
|
||||||
|
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-quad-reset" aria-hidden="true" tabindex="-1">Reset Quad</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
||||||
@ -344,15 +359,39 @@
|
|||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
<div class="panel-heading">Classic Colors</div>
|
<div class="panel-heading">Classic Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div id="classic-slots" class="flex items-center gap-2"></div>
|
<div id="classic-slots" class="flex items-center gap-2"></div>
|
||||||
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<span class="text-sm font-semibold text-gray-700">Active color:</span>
|
||||||
|
<button id="classic-active-chip" type="button" class="slot-swatch border border-gray-300" title="Tap to scroll to palette">
|
||||||
|
<span class="color-dot" id="classic-active-dot"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading mt-2">Project Palette</div>
|
||||||
|
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
|
||||||
|
<div class="panel-heading mt-4">Replace Color (Manual)</div>
|
||||||
|
<div class="panel-card space-y-3">
|
||||||
|
<div class="flex items-center gap-2 replace-row">
|
||||||
|
<button type="button" class="replace-chip" id="classic-replace-from-chip" aria-label="Pick color to replace"></button>
|
||||||
|
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||||
|
<button type="button" class="replace-chip" id="classic-replace-to-chip" aria-label="Pick replacement color"></button>
|
||||||
|
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
|
<div class="grid grid-cols-1 gap-2">
|
||||||
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
<p class="hint text-xs">Manual paint only. “From” lists colors already used on canvas; “To” comes from the Classic library.</p>
|
||||||
<div class="flex flex-wrap gap-2 mt-3">
|
<select id="classic-replace-from" class="sr-only"></select>
|
||||||
<button id="classic-randomize-colors" class="btn-dark">Randomize</button>
|
<select id="classic-replace-to" class="sr-only"></select>
|
||||||
|
<button id="classic-replace-btn" class="btn-blue">Replace</button>
|
||||||
|
<p id="classic-replace-msg" class="hint"></p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
|
||||||
|
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-3">
|
||||||
|
<button id="classic-randomize-colors" class="btn-dark">Randomize</button>
|
||||||
|
</div>
|
||||||
<div id="classic-topper-color-block" class="mt-3 hidden">
|
<div id="classic-topper-color-block" class="mt-3 hidden">
|
||||||
<div class="panel-heading">Topper Color</div>
|
<div class="panel-heading">Topper Color</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@ -377,10 +416,41 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section id="classic-canvas-panel"
|
<section id="classic-canvas-panel"
|
||||||
class="order-1 w-full lg:flex-1 flex flex-col items-stretch shadow-x3 rounded-2xl overflow-hidden bg-white">
|
class="order-1 w-full lg:flex-1 grid grid-rows-[1fr] lg:grid-rows-[minmax(0,1fr)] gap-2 shadow-x3 rounded-2xl overflow-hidden bg-white">
|
||||||
<div id="classic-display"
|
<div id="classic-display"
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
|
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
|
||||||
|
<div id="classic-mobile-bar" class="mobile-action-bar hidden">
|
||||||
|
<div class="mobile-action-chip" id="classic-active-chip-floating" title="Active">
|
||||||
|
<span class="color-dot" id="classic-active-dot-floating"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-action-row">
|
||||||
|
<button type="button" class="mobile-action-btn" id="classic-undo-manual" aria-label="Undo">
|
||||||
|
<i class="fa-solid fa-rotate-left" aria-hidden="true"></i>
|
||||||
|
<span>Undo</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-action-btn" id="classic-redo-manual" aria-label="Redo">
|
||||||
|
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
|
||||||
|
<span>Redo</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-action-btn" id="classic-pick-manual" aria-label="Eyedropper" aria-pressed="false">
|
||||||
|
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
|
||||||
|
<span>Pick</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-action-btn" id="classic-erase-manual" aria-label="Toggle Erase" aria-pressed="false">
|
||||||
|
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
|
||||||
|
<span>Erase</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-action-btn danger" id="classic-clear-manual" aria-label="Clear">
|
||||||
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
|
<span>Clear</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-action-btn" id="classic-export-manual" aria-label="Export">
|
||||||
|
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
||||||
|
<span>Export</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
||||||
<div class="floating-nudge-header">
|
<div class="floating-nudge-header">
|
||||||
<div class="panel-heading">Nudge Topper</div>
|
<div class="panel-heading">Nudge Topper</div>
|
||||||
@ -516,7 +586,8 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section id="wall-canvas-panel"
|
<section id="wall-canvas-panel"
|
||||||
class="order-1 lg:order-2 w-full lg:flex-1 flex flex-col items-stretch rounded-2xl overflow-hidden bg-white/50 shadow-inner ring-1 ring-black/5">
|
class="order-1 lg:order-2 w-full lg:flex-1 flex flex-col items-stretch rounded-2xl overflow-hidden bg-white/50 shadow-inner ring-1 ring-black/5"
|
||||||
|
style="height:92%;">
|
||||||
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-white/70">
|
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-white/70">
|
||||||
<div class="text-base font-semibold text-slate-700">Balloon Wall</div>
|
<div class="text-base font-semibold text-slate-700">Balloon Wall</div>
|
||||||
<div class="text-sm text-gray-500">Columns/Rows: <span id="wall-grid-label">9 × 7</span></div>
|
<div class="text-sm text-gray-500">Columns/Rows: <span id="wall-grid-label">9 × 7</span></div>
|
||||||
@ -625,5 +696,18 @@
|
|||||||
<script src="wall.js" defer></script>
|
<script src="wall.js" defer></script>
|
||||||
<script src="classic.js" defer></script>
|
<script src="classic.js" defer></script>
|
||||||
|
|
||||||
|
<div id="classic-quad-modal" class="quad-modal hidden" aria-hidden="true">
|
||||||
|
<div class="quad-modal-backdrop"></div>
|
||||||
|
<div class="quad-modal-panel" role="dialog" aria-modal="true" aria-label="Quad detail">
|
||||||
|
<div class="quad-modal-header">
|
||||||
|
<div class="quad-modal-title">Quad Detail</div>
|
||||||
|
<button type="button" id="classic-quad-modal-close" class="btn-dark text-xs px-3 py-2">Close</button>
|
||||||
|
</div>
|
||||||
|
<div class="quad-modal-body">
|
||||||
|
<div id="classic-quad-modal-display" class="quad-modal-display"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
22
shared.js
22
shared.js
@ -13,6 +13,8 @@
|
|||||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||||
const clamp01 = v => clamp(v, 0, 1);
|
const clamp01 = v => clamp(v, 0, 1);
|
||||||
const normalizeHex = h => (h || '').toLowerCase();
|
const normalizeHex = h => (h || '').toLowerCase();
|
||||||
|
const ACTIVE_COLOR_KEY = 'app:activeColor:v1';
|
||||||
|
let ACTIVE_COLOR_CACHE = null;
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
const h = normalizeHex(hex).replace('#','');
|
const h = normalizeHex(hex).replace('#','');
|
||||||
if (h.length === 3) {
|
if (h.length === 3) {
|
||||||
@ -29,6 +31,24 @@
|
|||||||
}
|
}
|
||||||
return { r: 0, g: 0, b: 0 };
|
return { r: 0, g: 0, b: 0 };
|
||||||
}
|
}
|
||||||
|
function getActiveColor() {
|
||||||
|
if (ACTIVE_COLOR_CACHE) return ACTIVE_COLOR_CACHE;
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem(ACTIVE_COLOR_KEY));
|
||||||
|
if (saved && saved.hex) {
|
||||||
|
ACTIVE_COLOR_CACHE = { hex: normalizeHex(saved.hex), image: saved.image || null };
|
||||||
|
return ACTIVE_COLOR_CACHE;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
ACTIVE_COLOR_CACHE = { hex: '#ff6b6b', image: null };
|
||||||
|
return ACTIVE_COLOR_CACHE;
|
||||||
|
}
|
||||||
|
function setActiveColor(color) {
|
||||||
|
const clean = { hex: normalizeHex(color?.hex || '#ffffff'), image: color?.image || null };
|
||||||
|
ACTIVE_COLOR_CACHE = clean;
|
||||||
|
try { localStorage.setItem(ACTIVE_COLOR_KEY, JSON.stringify(clean)); } catch {}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
function luminance(hex) {
|
function luminance(hex) {
|
||||||
const { r, g, b } = hexToRgb(hex || '#000');
|
const { r, g, b } = hexToRgb(hex || '#000');
|
||||||
const norm = [r,g,b].map(v => {
|
const norm = [r,g,b].map(v => {
|
||||||
@ -206,6 +226,8 @@
|
|||||||
imageToDataUrl,
|
imageToDataUrl,
|
||||||
imageUrlToDataUrl,
|
imageUrlToDataUrl,
|
||||||
download,
|
download,
|
||||||
|
getActiveColor,
|
||||||
|
setActiveColor
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
10
style.css
10
style.css
@ -29,7 +29,8 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
|||||||
box-shadow: 0 8px 24px rgba(15,23,42,0.08);
|
box-shadow: 0 8px 24px rgba(15,23,42,0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
#balloon-canvas { touch-action: none; }
|
#balloon-canvas { touch-action: none;
|
||||||
|
height: 95%}
|
||||||
|
|
||||||
.classic-expanded-canvas {
|
.classic-expanded-canvas {
|
||||||
height: 130vh !important;
|
height: 130vh !important;
|
||||||
@ -395,6 +396,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
height: 92%;
|
||||||
}
|
}
|
||||||
.control-sheet.hidden { display: none; }
|
.control-sheet.hidden { display: none; }
|
||||||
.control-sheet.minimized { transform: translateY(100%); }
|
.control-sheet.minimized { transform: translateY(100%); }
|
||||||
@ -685,6 +687,9 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
|||||||
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; }
|
.mobile-tabbar.hidden { display: none; }
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.mobile-tabbar { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
/* Tuck canvases above the tabbar */
|
/* Tuck canvases above the tabbar */
|
||||||
@ -695,6 +700,9 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
|||||||
height: calc(100vh - 190px) !important; /* tie to viewport minus header/controls */
|
height: calc(100vh - 190px) !important; /* tie to viewport minus header/controls */
|
||||||
max-height: calc(100vh - 190px) !important;
|
max-height: calc(100vh - 190px) !important;
|
||||||
}
|
}
|
||||||
|
#classic-display{
|
||||||
|
height: 92%;
|
||||||
|
}
|
||||||
/* Keep the main canvas panels above the tabbar/action bar */
|
/* Keep the main canvas panels above the tabbar/action bar */
|
||||||
#canvas-panel,
|
#canvas-panel,
|
||||||
#classic-canvas-panel {
|
#classic-canvas-panel {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user