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];
|
||||
}
|
||||
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 = '<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() {
|
||||
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);
|
||||
|
||||
@ -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 : []);
|
||||
window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []);
|
||||
|
||||
106
index.html
106
index.html
@ -24,7 +24,7 @@
|
||||
</style>
|
||||
</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">
|
||||
<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">
|
||||
<div class="flex items-center gap-3">
|
||||
@ -251,9 +251,24 @@
|
||||
</div>
|
||||
<div class="md:col-span-2 space-y-2">
|
||||
<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-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>
|
||||
<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="panel-heading">Classic Colors</div>
|
||||
<div class="panel-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<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>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<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>
|
||||
</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 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 class="grid grid-cols-1 gap-2">
|
||||
<p class="hint text-xs">Manual paint only. “From” lists colors already used on canvas; “To” comes from the Classic library.</p>
|
||||
<select id="classic-replace-from" class="sr-only"></select>
|
||||
<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 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 class="panel-heading">Topper Color</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@ -377,10 +416,41 @@
|
||||
</aside>
|
||||
|
||||
<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"
|
||||
class="rounded-xl"
|
||||
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 class="floating-nudge-header">
|
||||
<div class="panel-heading">Nudge Topper</div>
|
||||
@ -516,7 +586,8 @@
|
||||
</aside>
|
||||
|
||||
<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="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>
|
||||
@ -625,5 +696,18 @@
|
||||
<script src="wall.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>
|
||||
</html>
|
||||
|
||||
22
shared.js
22
shared.js
@ -13,6 +13,8 @@
|
||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||
const clamp01 = v => clamp(v, 0, 1);
|
||||
const normalizeHex = h => (h || '').toLowerCase();
|
||||
const ACTIVE_COLOR_KEY = 'app:activeColor:v1';
|
||||
let ACTIVE_COLOR_CACHE = null;
|
||||
function hexToRgb(hex) {
|
||||
const h = normalizeHex(hex).replace('#','');
|
||||
if (h.length === 3) {
|
||||
@ -29,6 +31,24 @@
|
||||
}
|
||||
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) {
|
||||
const { r, g, b } = hexToRgb(hex || '#000');
|
||||
const norm = [r,g,b].map(v => {
|
||||
@ -206,6 +226,8 @@
|
||||
imageToDataUrl,
|
||||
imageUrlToDataUrl,
|
||||
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);
|
||||
}
|
||||
|
||||
#balloon-canvas { touch-action: none; }
|
||||
#balloon-canvas { touch-action: none;
|
||||
height: 95%}
|
||||
|
||||
.classic-expanded-canvas {
|
||||
height: 130vh !important;
|
||||
@ -395,6 +396,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
||||
z-index: 30;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
height: 92%;
|
||||
}
|
||||
.control-sheet.hidden { display: none; }
|
||||
.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);
|
||||
}
|
||||
.mobile-tabbar.hidden { display: none; }
|
||||
@media (min-width: 1024px) {
|
||||
.mobile-tabbar { display: none !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
/* 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 */
|
||||
max-height: calc(100vh - 190px) !important;
|
||||
}
|
||||
#classic-display{
|
||||
height: 92%;
|
||||
}
|
||||
/* Keep the main canvas panels above the tabbar/action bar */
|
||||
#canvas-panel,
|
||||
#classic-canvas-panel {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user