diff --git a/classic.js b/classic.js
index 73a8df8..f2a7a23 100644
--- a/classic.js
+++ b/classic.js
@@ -16,6 +16,8 @@
const PALETTE_KEY = 'classic:colors:v2';
const TOPPER_COLOR_KEY = 'classic:topperColor:v2';
+ const MAX_SLOTS = 20;
+ const SLOT_COUNT_KEY = 'classic:slotCount:v1';
const defaultColors = () => [
{ hex: '#d92e3a', image: null }, { hex: '#ffffff', image: null },
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', image: null },
@@ -31,18 +33,19 @@
const saved = JSON.parse(savedJSON);
if (Array.isArray(saved) && saved.length > 0) {
if (typeof saved[0] === 'string') {
- arr = saved.slice(0, 5).map(hex => ({ hex: normHex(hex), image: null }));
+ arr = saved.slice(0, MAX_SLOTS).map(hex => ({ hex: normHex(hex), image: null }));
} else if (typeof saved[0] === 'object' && saved[0] !== null) {
- arr = saved.slice(0, 5);
+ arr = saved.slice(0, MAX_SLOTS);
}
- while (arr.length < 5) arr.push({ hex: '#ffffff', image: null });
}
+ while (arr.length < 5) arr.push({ hex: '#ffffff', image: null });
+ if (arr.length > MAX_SLOTS) arr = arr.slice(0, MAX_SLOTS);
} catch (e) { console.error('Failed to parse classic colors:', e); }
return arr;
}
function setClassicColors(arr) {
- const clean = (arr || []).slice(0, 5).map(c => ({
+ const clean = (arr || []).slice(0, MAX_SLOTS).map(c => ({
hex: normHex(c.hex), image: c.image || null
}));
while (clean.length < 5) clean.push({ hex: '#ffffff', image: null });
@@ -196,6 +199,11 @@ function distinctPaletteSlots(palette) {
const balloonsPerCluster = pattern.balloonsPerCluster || 4;
const reversed = !!(pattern._reverse || (pattern.parent && pattern.parent._reverse));
const rowColorPatterns = {};
+ const stackedSlots = (() => {
+ const slots = distinctPaletteSlots(model.palette);
+ const limit = Math.max(1, Math.min(slots.length, balloonsPerCluster));
+ return slots.slice(0, limit);
+ })();
const colorBlock4 = [[1, 2, 3, 4], [3, 1, 4, 2], [4, 3, 2, 1], [2, 4, 1, 3]];
const colorBlock5 =
@@ -222,10 +230,16 @@ function distinctPaletteSlots(palette) {
const rowIndex = cell.y;
if (!rowColorPatterns[rowIndex]) {
- const qEff = rowIndex + 1;
+ const totalRows = model.rowCount * (pattern.cellsPerRow || 1);
+ const isRightHalf = false; // mirror mode removed
+ const baseRow = rowIndex;
+ const qEff = baseRow + 1;
let pat;
- if (balloonsPerCluster === 5) {
+ if (pattern.colorMode === 'stacked') {
+ const slot = stackedSlots[(rowIndex) % stackedSlots.length] || stackedSlots[0] || 1;
+ pat = new Array(balloonsPerCluster).fill(slot);
+ } else if (balloonsPerCluster === 5) {
const base = (qEff - 1) % 5;
pat = colorBlock5[base].slice();
} else {
@@ -236,22 +250,21 @@ function distinctPaletteSlots(palette) {
}
}
- if (reversed && pat.length > 1) {
- pat.reverse();
+ // Swap left/right emphasis every 5 clusters to break repetition (per template override)
+ if (balloonsPerCluster === 5) {
+ const SWAP_EVERY = 5;
+ const blockIndex = Math.floor(rowIndex / SWAP_EVERY);
+ if (blockIndex % 2 === 1) {
+ [pat[0], pat[4]] = [pat[4], pat[0]];
+ }
}
- // --- NEW: swap left/right after every 5 clusters ---
-const SWAP_EVERY = 5; // clusters per block
-const blockIndex = Math.floor(rowIndex / SWAP_EVERY);
+ if (pat.length > 1) {
+ let shouldReverse;
+ shouldReverse = reversed;
+ if (shouldReverse) pat.reverse();
+ }
-// swap on blocks #2, #4, #6, ... (i.e., rows 6–10, 16–20, ...)
-if (blockIndex % 2 === 1) {
- if (balloonsPerCluster === 5) {
- // [leftMid, leftBack, front, rightBack, rightMid]
- [pat[0], pat[4]] = [pat[4], pat[0]];
- // [pat[1], pat[3]] = [pat[3], pat[1]];
- }
-}
rowColorPatterns[rowIndex] = pat;
}
@@ -314,7 +327,7 @@ if (blockIndex % 2 === 1) {
if (cellData) model.cells.push({ ...cellData, x, y, balloonIndexInCluster: balloonIndexInCluster++ });
}
}
- if (name === 'Column 4' && topperEnabled) {
+ if (name.toLowerCase().includes('column') && topperEnabled) {
const shapeName = `topper-${topperType}`;
const originalShape = pattern.balloonShapes[shapeName];
if (originalShape) {
@@ -374,64 +387,47 @@ if (blockIndex % 2 === 1) {
return { x: -r*Math.cos(a), y: -r*Math.sin(a) };
}
};
-
- // --- START: MODIFIED SECTION ---
- // This is the new 'Column 5' definition, adapted from your template file.
+ // --- Column 5 (template geometry) ---
patterns['Column 5'] = {
baseBalloonSize: 25,
_reverse: false,
- balloonsPerCluster: 5, // Kept this from classic.js to ensure 5-color spiral
+ balloonsPerCluster: 5,
tile: { size: { x: 5, y: 1 } },
cellsPerRow: 1,
cellsPerColumn: 5,
-
- // Balloon shapes from your template, converted to classic.js format
- // (type: "qlink" is approx size: 3.0)
balloonShapes: {
"front": { zIndex:5, base:{radius:0.5}, size:3.0 },
"front2": { zIndex:4, base:{radius:0.5}, size:3.0 },
"middle": { zIndex:3, base:{radius:0.5}, size:3.0 },
"middle2": { zIndex:2, base:{radius:0.5}, size:3.0 },
"back": { zIndex:1, base:{radius:0.5}, size:3.0 },
- "back2": { zIndex:0, base:{radius:0.5}, size:3.0 }
+ "back2": { zIndex:0, base:{radius:0.5}, size:3.0 },
+ 'topper-round':{base:{type:'ellipse', radius:0.5}, size:8},
+ 'topper-star':{base:{type:'path', d:roundedStarPath({}), radius:0.5}, size:8},
+ 'topper-heart':{base:{type:'path', d:'M0,0.35 C-0.5,0, -0.14,-0.35, 0,-0.14 C0.14,-0.35, 0.5,0, 0,0.35 Z', radius:0.5}, size:20}
},
-
- // gridX function from your template
- // (I've hard-coded `this.exploded` to false, as it's not in classic.js)
gridX(row, col) {
- var mid = 0.6; // this.exploded ? 0.2 : 0.6
- return (0.9) * (col + (0 === col % 5 && -0.5) + (1 === col % 5 && -mid) + (3 === col % 5 && mid) + (4 === col % 5 && 0.5) - 0.5);
+ var mid = 0.6;
+ return (0.9) * (col + (0 === col % 5 && -0.5) + (1 === col % 5 && -mid) + (3 === col % 5 && mid) + (4 === col % 5 && 0.5) - 0.5);
},
-
- // gridY function is inherited from Column 4 via `deriveFrom` in your template.
- // So, we use the gridY function from this file's 'Column 4'.
gridY(row, col){
return 2.2 * (1 - 1/5) * (Math.floor(row/2) + Math.floor((row+1)/2));
},
-
- // createCell function from your template, adapted for classic.js
createCell(x, y) {
- var yOdd = !!(y % 2);
-
- // Re-created logic from template's createCell
- const shapePattern = yOdd ?
- ['middle', 'back', 'front', 'back', 'middle'] :
- ['middle2', 'front2', 'back2', 'front2', 'middle2'];
-
- var shapeName = shapePattern[x % 5];
- var shape = this.balloonShapes[shapeName];
-
- // Return in classic.js format
- return shape ? { shape: {...shape} } : null;
+ var yOdd = !!(y % 2);
+ const shapePattern = yOdd
+ ? ['middle', 'back', 'front', 'back', 'middle']
+ : ['middle2', 'front2', 'back2', 'front2', 'middle2'];
+ var shapeName = shapePattern[x % 5];
+ var shape = this.balloonShapes[shapeName];
+ return shape ? { shape: {...shape} } : null;
}
};
- // This is the new 'Arch 5' definition.
- // It derives from the new 'Column 5' and uses the same arching logic as 'Arch 4'.
+ // Arch 5 derives from Column 5
patterns['Arch 5'] = {
deriveFrom: 'Column 5',
transform(point, col, row, model){
- // This transform logic is standard and will work with the new Column 5's gridY
const len = this.gridY(model.rowCount * this.tile.size.y, 0) - this.gridY(0, 0);
const r = (len / Math.PI) + point.x;
const y = point.y - this.gridY(0, 0);
@@ -441,53 +437,75 @@ if (blockIndex % 2 === 1) {
};
// --- END: MODIFIED SECTION ---
+ // --- Stacked variants (same geometry, single-color clusters alternating rows) ---
+ patterns['Arch 4 Stacked'] = { deriveFrom: 'Arch 4', colorMode: 'stacked' };
+ patterns['Arch 5 Stacked'] = { deriveFrom: 'Arch 5', colorMode: 'stacked' };
+ patterns['Column 4 Stacked'] = { deriveFrom: 'Column 4', colorMode: 'stacked' };
+ patterns['Column 5 Stacked'] = { deriveFrom: 'Column 5', colorMode: 'stacked' };
+
Object.keys(patterns).forEach(n => extend(patterns[n]));
return api;
}
const patternSlotCount = (name) => ((name || '').includes('5') ? 5 : 4);
+ function getStoredSlotCount() {
+ try {
+ const saved = parseInt(localStorage.getItem(SLOT_COUNT_KEY), 10);
+ if (Number.isFinite(saved) && saved > 0) return Math.min(saved, MAX_SLOTS);
+ } catch {}
+ return 5;
+ }
+ function setStoredSlotCount(n) {
+ const v = Math.max(1, Math.min(MAX_SLOTS, n|0));
+ try { localStorage.setItem(SLOT_COUNT_KEY, String(v)); } catch {}
+ return v;
+ }
function initClassicColorPicker(onColorChange) {
- const slots = Array.from(document.querySelectorAll('#classic-slots .slot-btn')), 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'), dockColorBtn = document.getElementById('dock-classic-color');
- if (!slots.length || !topperSwatch || !swatchGrid || !activeLabel) return;
+ 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');
+ if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
topperSwatch.classList.add('tab-btn');
- let classicColors = getClassicColors(), activeTarget = '1';
-
- const syncDockColor = (color) => {
- if (!dockColorBtn) return;
- const next = color?.hex ? color : { hex: '#2563eb', image: null };
- if (next.image) {
- dockColorBtn.style.backgroundImage = `url("${next.image}")`;
- dockColorBtn.style.backgroundSize = '200%';
- dockColorBtn.style.backgroundColor = 'transparent';
- } else {
- dockColorBtn.style.backgroundImage = 'none';
- dockColorBtn.style.backgroundColor = next.hex;
- }
- };
+ let classicColors = getClassicColors(), activeTarget = '1', slotCount = getStoredSlotCount();
function visibleSlotCount() {
const patSelect = document.getElementById('classic-pattern');
const name = patSelect?.value || 'Arch 4';
- return patternSlotCount(name);
+ const baseCount = patternSlotCount(name);
+ const isStacked = (name || '').toLowerCase().includes('stacked');
+ if (!isStacked) return baseCount;
+ const lengthInp = document.getElementById('classic-length-ft');
+ const clusters = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
+ const maxSlots = Math.min(MAX_SLOTS, clusters);
+ return Math.min(Math.max(baseCount, slotCount), maxSlots);
+ }
+
+ function renderSlots() {
+ slotsContainer.innerHTML = '';
+ const count = visibleSlotCount();
+ for (let i = 1; i <= count; i++) {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'slot-btn tab-btn';
+ btn.dataset.slot = String(i);
+ btn.textContent = `#${i}`;
+ btn.addEventListener('click', () => { activeTarget = String(i); updateUI(); });
+ slotsContainer.appendChild(btn);
+ }
}
function enforceSlotVisibility() {
const count = visibleSlotCount();
- slots.forEach((slot, i) => {
- const show = i < count;
- slot.classList.toggle('hidden', !show);
- if (!show && activeTarget === slot.dataset.slot) activeTarget = '1';
- });
if (parseInt(activeTarget, 10) > count) activeTarget = '1';
+ renderSlots();
}
function updateUI() {
enforceSlotVisibility();
- [...slots, topperSwatch].forEach(el => { const id = el.dataset.slot || 'T'; el.classList.toggle('tab-active', activeTarget === id); el.classList.toggle('tab-idle', activeTarget !== id); });
+ const buttons = Array.from(slotsContainer.querySelectorAll('.slot-btn'));
+ [...buttons, topperSwatch].forEach(el => { const id = el.dataset.slot || 'T'; el.classList.toggle('tab-active', activeTarget === id); el.classList.toggle('tab-idle', activeTarget !== id); });
- slots.forEach((slot, i) => {
+ buttons.forEach((slot, i) => {
const color = classicColors[i];
if (!color) return; // Safeguard against errors
slot.style.backgroundImage = color.image ? `url("${color.image}")` : 'none';
@@ -502,9 +520,17 @@ if (blockIndex % 2 === 1) {
topperSwatch.style.backgroundSize = '200%';
topperSwatch.style.backgroundPosition = 'center';
+ const patSelect = document.getElementById('classic-pattern');
+ const isStacked = (patSelect?.value || '').toLowerCase().includes('stacked');
+ if (addSlotBtn) {
+ const lengthInp = document.getElementById('classic-length-ft');
+ const clusters = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
+ const maxSlots = Math.min(MAX_SLOTS, clusters);
+ addSlotBtn.classList.toggle('hidden', !isStacked);
+ addSlotBtn.disabled = !isStacked || slotCount >= maxSlots;
+ }
+
activeLabel.textContent = activeTarget === 'T' ? 'Topper' : `Slot #${activeTarget}`;
- const displayColor = activeTarget === 'T' ? topperColor : classicColors[(parseInt(activeTarget, 10) || 1) - 1] || classicColors[0];
- syncDockColor(displayColor);
}
const allPaletteColors = flattenPalette(); swatchGrid.innerHTML = '';
@@ -525,7 +551,7 @@ if (blockIndex % 2 === 1) {
if (activeTarget === 'T') setTopperColor(selectedColor);
else {
const index = parseInt(activeTarget, 10) - 1;
- if (index >= 0 && index < 5) { classicColors[index] = selectedColor; setClassicColors(classicColors); }
+ if (index >= 0 && index < MAX_SLOTS) { classicColors[index] = selectedColor; setClassicColors(classicColors); }
}
updateUI(); onColorChange();
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
@@ -534,7 +560,6 @@ if (blockIndex % 2 === 1) {
});
swatchGrid.appendChild(row);
});
- slots.forEach(slot => { slot.addEventListener('click', () => { activeTarget = slot.dataset.slot; updateUI(); }); });
topperSwatch.addEventListener('click', () => { activeTarget = 'T'; updateUI(); });
randomizeBtn?.addEventListener('click', () => {
const pool = allPaletteColors.slice(); const picks = [];
@@ -544,153 +569,137 @@ if (blockIndex % 2 === 1) {
updateUI(); onColorChange();
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
});
+ addSlotBtn?.addEventListener('click', () => {
+ const patSelect = document.getElementById('classic-pattern');
+ const name = patSelect?.value || '';
+ const isStacked = name.toLowerCase().includes('stacked');
+ if (!isStacked) return;
+ const lengthInp = document.getElementById('classic-length-ft');
+ const clusters = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
+ const maxSlots = Math.min(MAX_SLOTS, clusters);
+ if (slotCount >= maxSlots) return;
+ slotCount = setStoredSlotCount(slotCount + 1);
+ while (classicColors.length < slotCount) {
+ const fallback = allPaletteColors[Math.floor(Math.random() * allPaletteColors.length)] || { hex: '#ffffff', image: null };
+ classicColors.push({ hex: fallback.hex, image: fallback.image });
+ }
+ setClassicColors(classicColors);
+ updateUI(); onColorChange();
+ if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
+ });
updateUI();
}
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'), topperTypeSelect = document.getElementById('classic-topper-type'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), lengthPresetWrap = document.getElementById('classic-length-presets'), lengthLabel = document.getElementById('classic-length-label');
- const ARCH_LENGTHS = [20, 25, 30, 35, 40];
- const COLUMN_LENGTHS = [3,4,5,6,7,8,9,10,11,12,13,14,15];
- const ARCH_DEFAULT = 20;
- const COLUMN_DEFAULT = 5;
- const lengthDialDrawer = document.getElementById('classic-length-drawer');
+ 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');
+ const patternShapeBtns = Array.from(document.querySelectorAll('[data-pattern-shape]'));
+ const patternCountBtns = Array.from(document.querySelectorAll('[data-pattern-count]'));
+ const patternLayoutBtns = Array.from(document.querySelectorAll('[data-pattern-layout]'));
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
- const slotButtons = Array.from(document.querySelectorAll('#classic-slots .slot-btn'));
- const patternBtns = Array.from(document.querySelectorAll('.classic-pattern-btn'));
- const variantBtns = Array.from(document.querySelectorAll('.classic-variant-btn'));
- const topperBtns = Array.from(document.querySelectorAll('.classic-topper-btn'));
- const topperInline = document.getElementById('topper-inline');
- const topperTypeInline = document.getElementById('classic-topper-type-inline');
- const topperSizeInline = document.getElementById('classic-topper-size-inline');
- const topperColorInline = document.getElementById('classic-topper-color-swatch-inline');
+ const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
+ const slotsContainer = document.getElementById('classic-slots');
let topperOffsetX = 0, topperOffsetY = 0;
- let userTopperChoice = false;
+ let lastPresetKey = null; // 'custom' means user-tweaked; otherwise `${pattern}:${type}`
+ const topperPresets = {
+ 'Column 4:heart': { enabled: true, offsetX: 3, offsetY: -10.5, size: 1.05 },
+ 'Column 4:star': { enabled: true, offsetX: 3, offsetY: -7.5, size: 1.65 },
+ 'Column 4:round': { enabled: true, offsetX: 3, offsetY: -2, size: 1.25 },
+ 'Column 5:heart': { enabled: true, offsetX: 2, offsetY: -10, size: 1.15 },
+ 'Column 5:star': { enabled: true, offsetX: 2.5, offsetY: -7.5, size: 1.75 },
+ 'Column 5:round': { enabled: true, offsetX: 2.5, offsetY: -2, size: 1.3 }
+ };
if (!display) return fail('#classic-display not found');
const GC = GridCalculator(), ctrl = GC.controller(display);
- const syncDockPatternButtons = (patternName) => {
- const isArch = (patternName || '').toLowerCase().includes('arch');
- const isFive = (patternName || '').includes('5');
- patternBtns.forEach(btn => {
- const base = (btn.dataset.patternBase || '').toLowerCase();
- const active = isArch ? base === 'arch' : base === 'column';
- btn.classList.toggle('active', active);
+ const getTopperType = () => topperTypeButtons.find(btn => btn.getAttribute('aria-pressed') === 'true')?.dataset.type || 'round';
+ const setTopperType = (type) => {
+ topperTypeButtons.forEach(btn => {
+ const active = btn.dataset.type === type;
btn.setAttribute('aria-pressed', String(active));
- });
- variantBtns.forEach(btn => {
- const active = btn.dataset.patternVariant === (isFive ? '5' : '4');
- btn.classList.toggle('active', active);
- btn.setAttribute('aria-pressed', String(active));
- });
- };
- const syncDockTopperButton = () => {
- const on = !!topperEnabledCb?.checked;
- topperBtns.forEach(btn => {
- btn.classList.toggle('active', on);
- btn.setAttribute('aria-pressed', String(on));
+ btn.classList.toggle('tab-active', active);
+ btn.classList.toggle('tab-idle', !active);
});
};
- const syncTopperInline = () => {
- const on = !!topperEnabledCb?.checked;
- if (topperInline) topperInline.classList.toggle('hidden', !on);
- if (topperTypeInline && topperTypeSelect) topperTypeInline.value = topperTypeSelect.value;
- if (topperSizeInline && topperSizeInp) topperSizeInline.value = topperSizeInp.value;
- if (topperColorInline) {
- const tc = getTopperColor();
- topperColorInline.style.backgroundImage = tc.image ? `url("${tc.image}")` : 'none';
- topperColorInline.style.backgroundColor = tc.hex;
- }
+ function applyTopperPreset(patternName, type) {
+ const key = `${patternName}:${type}`;
+ const preset = topperPresets[key];
+ if (!preset) return;
+ if (lastPresetKey === key || lastPresetKey === 'custom') return;
+ topperOffsetX = preset.offsetX;
+ topperOffsetY = preset.offsetY;
+ if (topperSizeInp) topperSizeInp.value = preset.size;
+ if (topperEnabledCb) topperEnabledCb.checked = preset.enabled;
+ setTopperType(type);
+ lastPresetKey = key;
+ }
+
+ let patternShape = 'arch', patternCount = 4, patternLayout = 'spiral';
+ const computePatternName = () => {
+ const base = patternShape === 'column' ? 'Column' : 'Arch';
+ const count = patternCount === 5 ? '5' : '4';
+ const layout = patternLayout === 'stacked' ? ' Stacked' : '';
+ return `${base} ${count}${layout}`;
};
-
- const renderLengthPresets = (patternName) => {
- if (!lengthInp) return;
- const isArch = (patternName || '').toLowerCase().includes('arch');
- const list = isArch ? ARCH_LENGTHS : COLUMN_LENGTHS;
- const current = parseFloat(lengthInp.value) || (isArch ? ARCH_DEFAULT : COLUMN_DEFAULT);
- if (lengthLabel) lengthLabel.textContent = `${current} ft`;
-
- const targets = [lengthPresetWrap, lengthDialDrawer];
- targets.forEach(container => {
- if (!container) return;
- container.innerHTML = '';
- list.forEach(len => {
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'dock-pill';
- if (Math.abs(current - len) < 1e-6) btn.classList.add('active');
- btn.textContent = `${len} ft`;
- btn.dataset.len = len;
- btn.addEventListener('click', () => {
- lengthInp.value = len;
- if (lengthLabel) lengthLabel.textContent = `${len} ft`;
- updateClassicDesign();
- btn.classList.add('ping');
- btn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
- setTimeout(() => btn.classList.remove('ping'), 280);
- document.getElementById('classic-drawer-pattern')?.classList.add('hidden');
- window.__dockActiveMenu = null;
- });
- container.appendChild(btn);
- });
+ const syncPatternStateFromSelect = () => {
+ const val = (patSel?.value || '').toLowerCase();
+ patternShape = val.includes('column') ? 'column' : 'arch';
+ patternCount = val.includes('5') ? 5 : 4;
+ patternLayout = val.includes('stacked') ? 'stacked' : 'spiral';
+ };
+ const applyPatternButtons = () => {
+ const setActive = (btns, attr, val) => btns.forEach(b => {
+ const active = b.dataset[attr] === val;
+ b.classList.toggle('tab-active', active);
+ b.classList.toggle('tab-idle', !active);
+ b.setAttribute('aria-pressed', String(active));
});
+ setActive(patternShapeBtns, 'patternShape', patternShape);
+ setActive(patternCountBtns, 'patternCount', String(patternCount));
+ setActive(patternLayoutBtns, 'patternLayout', patternLayout);
};
-
- const ensureLengthForPattern = (patternName) => {
- if (!lengthInp) return;
- const isArch = (patternName || '').toLowerCase().includes('arch');
- const list = isArch ? ARCH_LENGTHS : COLUMN_LENGTHS;
- const cur = parseFloat(lengthInp.value);
- if (!list.includes(cur)) {
- const def = isArch ? ARCH_DEFAULT : COLUMN_DEFAULT;
- lengthInp.value = def;
- }
- };
+ syncPatternStateFromSelect();
+ applyPatternButtons();
function updateClassicDesign() {
if (!lengthInp || !patSel) return;
+ patSel.value = computePatternName();
const patternName = patSel.value || 'Arch 4';
- ensureLengthForPattern(patternName);
const isColumn = patternName.toLowerCase().includes('column');
const hasTopper = patternName.includes('4') || patternName.includes('5');
const showToggle = isColumn && hasTopper;
- if (showToggle && topperEnabledCb && !userTopperChoice) {
- topperEnabledCb.checked = true;
+ if (patternName.toLowerCase().includes('column')) {
+ const baseName = patternName.includes('5') ? 'Column 5' : 'Column 4';
+ applyTopperPreset(baseName, getTopperType());
}
if (topperToggleRow) topperToggleRow.classList.toggle('hidden', !showToggle);
const showTopper = showToggle && topperEnabledCb?.checked;
- slotButtons.forEach((btn, i) => {
- const count = patternSlotCount(patternName);
- const show = i < count;
- btn.classList.toggle('hidden', !show);
- });
-
topperControls.classList.toggle('hidden', !showTopper);
- topperTypeSelect.disabled = !showTopper;
GC.setTopperEnabled(showTopper);
GC.setClusters(Math.round((parseFloat(lengthInp.value) || 0) * 2));
GC.setReverse(!!reverseCb?.checked);
- GC.setTopperType(topperTypeSelect.value);
+ GC.setTopperType(getTopperType());
GC.setTopperOffsetX(topperOffsetX);
GC.setTopperOffsetY(topperOffsetY);
GC.setTopperSize(topperSizeInp?.value);
GC.setShineEnabled(!!shineEnabledCb?.checked);
+ if (document.body) {
+ if (showTopper) document.body.dataset.topperOverlay = '1';
+ else delete document.body.dataset.topperOverlay;
+ }
+ window.__updateFloatingNudge?.();
if(clusterHint) clusterHint.textContent = `≈ ${Math.round((parseFloat(lengthInp.value) || 0) * 2)} clusters (rule: 2 clusters/ft)`;
ctrl.selectPattern(patternName);
- syncDockPatternButtons(patternName);
- syncDockTopperButton();
- renderLengthPresets(patternName);
- syncTopperInline();
}
const setLengthForPattern = () => {
if (!lengthInp || !patSel) return;
- const isArch = (patSel.value || '').toLowerCase().includes('arch');
- lengthInp.value = isArch ? ARCH_DEFAULT : COLUMN_DEFAULT;
+ const isArch = (computePatternName()).toLowerCase().includes('arch');
+ lengthInp.value = isArch ? 20 : 5;
};
window.ClassicDesigner = window.ClassicDesigner || {};
@@ -701,45 +710,38 @@ if (blockIndex % 2 === 1) {
document.querySelector('#mode-tabs')?.addEventListener('click', () => setTimeout(() => { if (window.updateExportButtonVisibility) window.updateExportButtonVisibility() }, 50));
patSel?.addEventListener('change', () => {
- ensureLengthForPattern(patSel.value);
+ lastPresetKey = null;
+ syncPatternStateFromSelect();
+ applyPatternButtons();
+ setLengthForPattern();
updateClassicDesign();
- renderLengthPresets(patSel.value);
});
+ patternShapeBtns.forEach(btn => btn.addEventListener('click', () => { patternShape = btn.dataset.patternShape; lastPresetKey = null; applyPatternButtons(); setLengthForPattern(); updateClassicDesign(); }));
+ patternCountBtns.forEach(btn => btn.addEventListener('click', () => { patternCount = Number(btn.dataset.patternCount) === 5 ? 5 : 4; lastPresetKey = null; applyPatternButtons(); setLengthForPattern(); updateClassicDesign(); }));
+ patternLayoutBtns.forEach(btn => btn.addEventListener('click', () => { patternLayout = btn.dataset.patternLayout === 'stacked' ? 'stacked' : 'spiral'; lastPresetKey = null; applyPatternButtons(); updateClassicDesign(); }));
topperNudgeBtns.forEach(btn => btn.addEventListener('click', () => {
const dx = Number(btn.dataset.dx || 0);
const dy = Number(btn.dataset.dy || 0);
topperOffsetX += dx;
topperOffsetY += dy;
+ lastPresetKey = 'custom';
GC.setTopperOffsetX(topperOffsetX);
GC.setTopperOffsetY(topperOffsetY);
updateClassicDesign();
}));
- [lengthInp, reverseCb, topperEnabledCb, topperTypeSelect, topperSizeInp]
- .forEach(el => { if (!el) return; const eventType = (el.type === 'range' || el.type === 'number') ? 'input' : 'change'; el.addEventListener(eventType, updateClassicDesign); });
+ topperTypeButtons.forEach(btn => btn.addEventListener('click', () => {
+ setTopperType(btn.dataset.type);
+ lastPresetKey = null;
+ updateClassicDesign();
+ }));
+ [lengthInp, reverseCb, topperEnabledCb, topperSizeInp]
+ .forEach(el => { if (!el) return; const eventType = (el.type === 'range' || el.type === 'number') ? 'input' : 'change'; el.addEventListener(eventType, () => { if (el === topperSizeInp || el === topperEnabledCb) lastPresetKey = 'custom'; updateClassicDesign(); }); });
topperEnabledCb?.addEventListener('change', updateClassicDesign);
- topperEnabledCb?.addEventListener('change', (e) => {
- if (e.isTrusted) userTopperChoice = true;
- syncDockTopperButton();
- syncTopperInline();
- });
- topperTypeInline?.addEventListener('change', () => {
- if (topperTypeSelect) topperTypeSelect.value = topperTypeInline.value;
- updateClassicDesign();
- });
- topperSizeInline?.addEventListener('input', () => {
- if (topperSizeInp) topperSizeInp.value = topperSizeInline.value;
- updateClassicDesign();
- });
- topperColorInline?.addEventListener('click', () => {
- const sw = document.getElementById('classic-topper-color-swatch');
- sw?.click();
- });
shineEnabledCb?.addEventListener('change', (e) => { const on = !!e.target.checked; GC.setShineEnabled(on); updateClassicDesign(); window.syncAppShine?.(on); });
initClassicColorPicker(updateClassicDesign);
try { const saved = localStorage.getItem('app:shineEnabled:v1'); if (saved !== null && shineEnabledCb) shineEnabledCb.checked = JSON.parse(saved); } catch {}
setLengthForPattern();
updateClassicDesign();
- renderLengthPresets(patSel?.value || '');
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
log('Classic ready');
} catch (e) { fail(e.message || e); }
diff --git a/index.html b/index.html
index 3e32795..770f593 100644
--- a/index.html
+++ b/index.html
@@ -13,6 +13,7 @@
+
-
+
+
-
-
+
+
-
Balloon Studio
+
Balloon Studio
+
Professional Design Tool
-
-
-
-
-
Balloon Size
+
Size & Shine
+
Global scale lives in PX_PER_INCH (see script.js).
+
- Palette in Use
+
+
Used Colors
- Colors currently on your canvas.
+ Built from the current design. Click a swatch to select that color.
-
Color Library
+
Allowed Colors
-
Pick a color to draw with.
+
Alt+Click a balloon on canvas to pick its color.
-
Swap Colors
+
Replace Color
-
+
-
+
-
+
@@ -175,32 +192,45 @@