From 70d29cefca87134cfb80d3e7a0de28a72488406d Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 1 Dec 2025 09:18:19 -0500 Subject: [PATCH] chore: snapshot v2 version --- classic.js | 414 +++++++++++++++++++++++++++-------------------------- index.html | 297 ++++++++++++++++++-------------------- script.js | 412 ++++++++++------------------------------------------ style.css | 260 +++++++++++---------------------- 4 files changed, 502 insertions(+), 881 deletions(-) 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 @@