chore: snapshot v2 version
This commit is contained in:
parent
9cabe15481
commit
70d29cefca
414
classic.js
414
classic.js
@ -16,6 +16,8 @@
|
|||||||
const PALETTE_KEY = 'classic:colors:v2';
|
const PALETTE_KEY = 'classic:colors:v2';
|
||||||
const TOPPER_COLOR_KEY = 'classic:topperColor:v2';
|
const TOPPER_COLOR_KEY = 'classic:topperColor:v2';
|
||||||
|
|
||||||
|
const MAX_SLOTS = 20;
|
||||||
|
const SLOT_COUNT_KEY = 'classic:slotCount:v1';
|
||||||
const defaultColors = () => [
|
const defaultColors = () => [
|
||||||
{ hex: '#d92e3a', image: null }, { hex: '#ffffff', image: null },
|
{ hex: '#d92e3a', image: null }, { hex: '#ffffff', image: null },
|
||||||
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', image: null },
|
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', image: null },
|
||||||
@ -31,18 +33,19 @@
|
|||||||
const saved = JSON.parse(savedJSON);
|
const saved = JSON.parse(savedJSON);
|
||||||
if (Array.isArray(saved) && saved.length > 0) {
|
if (Array.isArray(saved) && saved.length > 0) {
|
||||||
if (typeof saved[0] === 'string') {
|
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) {
|
} 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); }
|
} catch (e) { console.error('Failed to parse classic colors:', e); }
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setClassicColors(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
|
hex: normHex(c.hex), image: c.image || null
|
||||||
}));
|
}));
|
||||||
while (clean.length < 5) clean.push({ hex: '#ffffff', 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 balloonsPerCluster = pattern.balloonsPerCluster || 4;
|
||||||
const reversed = !!(pattern._reverse || (pattern.parent && pattern.parent._reverse));
|
const reversed = !!(pattern._reverse || (pattern.parent && pattern.parent._reverse));
|
||||||
const rowColorPatterns = {};
|
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 colorBlock4 = [[1, 2, 3, 4], [3, 1, 4, 2], [4, 3, 2, 1], [2, 4, 1, 3]];
|
||||||
const colorBlock5 =
|
const colorBlock5 =
|
||||||
@ -222,10 +230,16 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
const rowIndex = cell.y;
|
const rowIndex = cell.y;
|
||||||
if (!rowColorPatterns[rowIndex]) {
|
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;
|
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;
|
const base = (qEff - 1) % 5;
|
||||||
pat = colorBlock5[base].slice();
|
pat = colorBlock5[base].slice();
|
||||||
} else {
|
} else {
|
||||||
@ -236,22 +250,21 @@ function distinctPaletteSlots(palette) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reversed && pat.length > 1) {
|
// Swap left/right emphasis every 5 clusters to break repetition (per template override)
|
||||||
pat.reverse();
|
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 ---
|
if (pat.length > 1) {
|
||||||
const SWAP_EVERY = 5; // clusters per block
|
let shouldReverse;
|
||||||
const blockIndex = Math.floor(rowIndex / SWAP_EVERY);
|
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;
|
rowColorPatterns[rowIndex] = pat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +327,7 @@ if (blockIndex % 2 === 1) {
|
|||||||
if (cellData) model.cells.push({ ...cellData, x, y, balloonIndexInCluster: balloonIndexInCluster++ });
|
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 shapeName = `topper-${topperType}`;
|
||||||
const originalShape = pattern.balloonShapes[shapeName];
|
const originalShape = pattern.balloonShapes[shapeName];
|
||||||
if (originalShape) {
|
if (originalShape) {
|
||||||
@ -374,64 +387,47 @@ if (blockIndex % 2 === 1) {
|
|||||||
return { x: -r*Math.cos(a), y: -r*Math.sin(a) };
|
return { x: -r*Math.cos(a), y: -r*Math.sin(a) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// --- Column 5 (template geometry) ---
|
||||||
// --- START: MODIFIED SECTION ---
|
|
||||||
// This is the new 'Column 5' definition, adapted from your template file.
|
|
||||||
patterns['Column 5'] = {
|
patterns['Column 5'] = {
|
||||||
baseBalloonSize: 25,
|
baseBalloonSize: 25,
|
||||||
_reverse: false,
|
_reverse: false,
|
||||||
balloonsPerCluster: 5, // Kept this from classic.js to ensure 5-color spiral
|
balloonsPerCluster: 5,
|
||||||
tile: { size: { x: 5, y: 1 } },
|
tile: { size: { x: 5, y: 1 } },
|
||||||
cellsPerRow: 1,
|
cellsPerRow: 1,
|
||||||
cellsPerColumn: 5,
|
cellsPerColumn: 5,
|
||||||
|
|
||||||
// Balloon shapes from your template, converted to classic.js format
|
|
||||||
// (type: "qlink" is approx size: 3.0)
|
|
||||||
balloonShapes: {
|
balloonShapes: {
|
||||||
"front": { zIndex:5, base:{radius:0.5}, size:3.0 },
|
"front": { zIndex:5, base:{radius:0.5}, size:3.0 },
|
||||||
"front2": { zIndex:4, 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 },
|
"middle": { zIndex:3, base:{radius:0.5}, size:3.0 },
|
||||||
"middle2": { zIndex:2, 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 },
|
"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) {
|
gridX(row, col) {
|
||||||
var mid = 0.6; // this.exploded ? 0.2 : 0.6
|
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);
|
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){
|
gridY(row, col){
|
||||||
return 2.2 * (1 - 1/5) * (Math.floor(row/2) + Math.floor((row+1)/2));
|
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) {
|
createCell(x, y) {
|
||||||
var yOdd = !!(y % 2);
|
var yOdd = !!(y % 2);
|
||||||
|
const shapePattern = yOdd
|
||||||
// Re-created logic from template's createCell
|
? ['middle', 'back', 'front', 'back', 'middle']
|
||||||
const shapePattern = yOdd ?
|
: ['middle2', 'front2', 'back2', 'front2', 'middle2'];
|
||||||
['middle', 'back', 'front', 'back', 'middle'] :
|
var shapeName = shapePattern[x % 5];
|
||||||
['middle2', 'front2', 'back2', 'front2', 'middle2'];
|
var shape = this.balloonShapes[shapeName];
|
||||||
|
return shape ? { shape: {...shape} } : null;
|
||||||
var shapeName = shapePattern[x % 5];
|
|
||||||
var shape = this.balloonShapes[shapeName];
|
|
||||||
|
|
||||||
// Return in classic.js format
|
|
||||||
return shape ? { shape: {...shape} } : null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is the new 'Arch 5' definition.
|
// Arch 5 derives from Column 5
|
||||||
// It derives from the new 'Column 5' and uses the same arching logic as 'Arch 4'.
|
|
||||||
patterns['Arch 5'] = {
|
patterns['Arch 5'] = {
|
||||||
deriveFrom: 'Column 5',
|
deriveFrom: 'Column 5',
|
||||||
transform(point, col, row, model){
|
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 len = this.gridY(model.rowCount * this.tile.size.y, 0) - this.gridY(0, 0);
|
||||||
const r = (len / Math.PI) + point.x;
|
const r = (len / Math.PI) + point.x;
|
||||||
const y = point.y - this.gridY(0, 0);
|
const y = point.y - this.gridY(0, 0);
|
||||||
@ -441,53 +437,75 @@ if (blockIndex % 2 === 1) {
|
|||||||
};
|
};
|
||||||
// --- END: MODIFIED SECTION ---
|
// --- 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]));
|
Object.keys(patterns).forEach(n => extend(patterns[n]));
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
const patternSlotCount = (name) => ((name || '').includes('5') ? 5 : 4);
|
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) {
|
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');
|
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 (!slots.length || !topperSwatch || !swatchGrid || !activeLabel) return;
|
if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
|
||||||
topperSwatch.classList.add('tab-btn');
|
topperSwatch.classList.add('tab-btn');
|
||||||
let classicColors = getClassicColors(), activeTarget = '1';
|
let classicColors = getClassicColors(), activeTarget = '1', slotCount = getStoredSlotCount();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function visibleSlotCount() {
|
function visibleSlotCount() {
|
||||||
const patSelect = document.getElementById('classic-pattern');
|
const patSelect = document.getElementById('classic-pattern');
|
||||||
const name = patSelect?.value || 'Arch 4';
|
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() {
|
function enforceSlotVisibility() {
|
||||||
const count = visibleSlotCount();
|
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';
|
if (parseInt(activeTarget, 10) > count) activeTarget = '1';
|
||||||
|
renderSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
enforceSlotVisibility();
|
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];
|
const color = classicColors[i];
|
||||||
if (!color) return; // Safeguard against errors
|
if (!color) return; // Safeguard against errors
|
||||||
slot.style.backgroundImage = color.image ? `url("${color.image}")` : 'none';
|
slot.style.backgroundImage = color.image ? `url("${color.image}")` : 'none';
|
||||||
@ -502,9 +520,17 @@ if (blockIndex % 2 === 1) {
|
|||||||
topperSwatch.style.backgroundSize = '200%';
|
topperSwatch.style.backgroundSize = '200%';
|
||||||
topperSwatch.style.backgroundPosition = 'center';
|
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}`;
|
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 = '';
|
const allPaletteColors = flattenPalette(); swatchGrid.innerHTML = '';
|
||||||
@ -525,7 +551,7 @@ if (blockIndex % 2 === 1) {
|
|||||||
if (activeTarget === 'T') setTopperColor(selectedColor);
|
if (activeTarget === 'T') setTopperColor(selectedColor);
|
||||||
else {
|
else {
|
||||||
const index = parseInt(activeTarget, 10) - 1;
|
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();
|
updateUI(); onColorChange();
|
||||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||||
@ -534,7 +560,6 @@ if (blockIndex % 2 === 1) {
|
|||||||
});
|
});
|
||||||
swatchGrid.appendChild(row);
|
swatchGrid.appendChild(row);
|
||||||
});
|
});
|
||||||
slots.forEach(slot => { slot.addEventListener('click', () => { activeTarget = slot.dataset.slot; updateUI(); }); });
|
|
||||||
topperSwatch.addEventListener('click', () => { activeTarget = 'T'; updateUI(); });
|
topperSwatch.addEventListener('click', () => { activeTarget = 'T'; updateUI(); });
|
||||||
randomizeBtn?.addEventListener('click', () => {
|
randomizeBtn?.addEventListener('click', () => {
|
||||||
const pool = allPaletteColors.slice(); const picks = [];
|
const pool = allPaletteColors.slice(); const picks = [];
|
||||||
@ -544,153 +569,137 @@ if (blockIndex % 2 === 1) {
|
|||||||
updateUI(); onColorChange();
|
updateUI(); onColorChange();
|
||||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
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();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initClassic() {
|
function initClassic() {
|
||||||
try {
|
try {
|
||||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
||||||
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), 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 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 ARCH_LENGTHS = [20, 25, 30, 35, 40];
|
const patternShapeBtns = Array.from(document.querySelectorAll('[data-pattern-shape]'));
|
||||||
const COLUMN_LENGTHS = [3,4,5,6,7,8,9,10,11,12,13,14,15];
|
const patternCountBtns = Array.from(document.querySelectorAll('[data-pattern-count]'));
|
||||||
const ARCH_DEFAULT = 20;
|
const patternLayoutBtns = Array.from(document.querySelectorAll('[data-pattern-layout]'));
|
||||||
const COLUMN_DEFAULT = 5;
|
|
||||||
const lengthDialDrawer = document.getElementById('classic-length-drawer');
|
|
||||||
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
||||||
const slotButtons = Array.from(document.querySelectorAll('#classic-slots .slot-btn'));
|
const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
|
||||||
const patternBtns = Array.from(document.querySelectorAll('.classic-pattern-btn'));
|
const slotsContainer = document.getElementById('classic-slots');
|
||||||
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');
|
|
||||||
let topperOffsetX = 0, topperOffsetY = 0;
|
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');
|
if (!display) return fail('#classic-display not found');
|
||||||
const GC = GridCalculator(), ctrl = GC.controller(display);
|
const GC = GridCalculator(), ctrl = GC.controller(display);
|
||||||
|
|
||||||
const syncDockPatternButtons = (patternName) => {
|
const getTopperType = () => topperTypeButtons.find(btn => btn.getAttribute('aria-pressed') === 'true')?.dataset.type || 'round';
|
||||||
const isArch = (patternName || '').toLowerCase().includes('arch');
|
const setTopperType = (type) => {
|
||||||
const isFive = (patternName || '').includes('5');
|
topperTypeButtons.forEach(btn => {
|
||||||
patternBtns.forEach(btn => {
|
const active = btn.dataset.type === type;
|
||||||
const base = (btn.dataset.patternBase || '').toLowerCase();
|
|
||||||
const active = isArch ? base === 'arch' : base === 'column';
|
|
||||||
btn.classList.toggle('active', active);
|
|
||||||
btn.setAttribute('aria-pressed', String(active));
|
btn.setAttribute('aria-pressed', String(active));
|
||||||
});
|
btn.classList.toggle('tab-active', active);
|
||||||
variantBtns.forEach(btn => {
|
btn.classList.toggle('tab-idle', !active);
|
||||||
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));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncTopperInline = () => {
|
function applyTopperPreset(patternName, type) {
|
||||||
const on = !!topperEnabledCb?.checked;
|
const key = `${patternName}:${type}`;
|
||||||
if (topperInline) topperInline.classList.toggle('hidden', !on);
|
const preset = topperPresets[key];
|
||||||
if (topperTypeInline && topperTypeSelect) topperTypeInline.value = topperTypeSelect.value;
|
if (!preset) return;
|
||||||
if (topperSizeInline && topperSizeInp) topperSizeInline.value = topperSizeInp.value;
|
if (lastPresetKey === key || lastPresetKey === 'custom') return;
|
||||||
if (topperColorInline) {
|
topperOffsetX = preset.offsetX;
|
||||||
const tc = getTopperColor();
|
topperOffsetY = preset.offsetY;
|
||||||
topperColorInline.style.backgroundImage = tc.image ? `url("${tc.image}")` : 'none';
|
if (topperSizeInp) topperSizeInp.value = preset.size;
|
||||||
topperColorInline.style.backgroundColor = tc.hex;
|
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 syncPatternStateFromSelect = () => {
|
||||||
const renderLengthPresets = (patternName) => {
|
const val = (patSel?.value || '').toLowerCase();
|
||||||
if (!lengthInp) return;
|
patternShape = val.includes('column') ? 'column' : 'arch';
|
||||||
const isArch = (patternName || '').toLowerCase().includes('arch');
|
patternCount = val.includes('5') ? 5 : 4;
|
||||||
const list = isArch ? ARCH_LENGTHS : COLUMN_LENGTHS;
|
patternLayout = val.includes('stacked') ? 'stacked' : 'spiral';
|
||||||
const current = parseFloat(lengthInp.value) || (isArch ? ARCH_DEFAULT : COLUMN_DEFAULT);
|
};
|
||||||
if (lengthLabel) lengthLabel.textContent = `${current} ft`;
|
const applyPatternButtons = () => {
|
||||||
|
const setActive = (btns, attr, val) => btns.forEach(b => {
|
||||||
const targets = [lengthPresetWrap, lengthDialDrawer];
|
const active = b.dataset[attr] === val;
|
||||||
targets.forEach(container => {
|
b.classList.toggle('tab-active', active);
|
||||||
if (!container) return;
|
b.classList.toggle('tab-idle', !active);
|
||||||
container.innerHTML = '';
|
b.setAttribute('aria-pressed', String(active));
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
setActive(patternShapeBtns, 'patternShape', patternShape);
|
||||||
|
setActive(patternCountBtns, 'patternCount', String(patternCount));
|
||||||
|
setActive(patternLayoutBtns, 'patternLayout', patternLayout);
|
||||||
};
|
};
|
||||||
|
syncPatternStateFromSelect();
|
||||||
const ensureLengthForPattern = (patternName) => {
|
applyPatternButtons();
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateClassicDesign() {
|
function updateClassicDesign() {
|
||||||
if (!lengthInp || !patSel) return;
|
if (!lengthInp || !patSel) return;
|
||||||
|
patSel.value = computePatternName();
|
||||||
const patternName = patSel.value || 'Arch 4';
|
const patternName = patSel.value || 'Arch 4';
|
||||||
ensureLengthForPattern(patternName);
|
|
||||||
const isColumn = patternName.toLowerCase().includes('column');
|
const isColumn = patternName.toLowerCase().includes('column');
|
||||||
const hasTopper = patternName.includes('4') || patternName.includes('5');
|
const hasTopper = patternName.includes('4') || patternName.includes('5');
|
||||||
const showToggle = isColumn && hasTopper;
|
const showToggle = isColumn && hasTopper;
|
||||||
if (showToggle && topperEnabledCb && !userTopperChoice) {
|
if (patternName.toLowerCase().includes('column')) {
|
||||||
topperEnabledCb.checked = true;
|
const baseName = patternName.includes('5') ? 'Column 5' : 'Column 4';
|
||||||
|
applyTopperPreset(baseName, getTopperType());
|
||||||
}
|
}
|
||||||
if (topperToggleRow) topperToggleRow.classList.toggle('hidden', !showToggle);
|
if (topperToggleRow) topperToggleRow.classList.toggle('hidden', !showToggle);
|
||||||
const showTopper = showToggle && topperEnabledCb?.checked;
|
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);
|
topperControls.classList.toggle('hidden', !showTopper);
|
||||||
topperTypeSelect.disabled = !showTopper;
|
|
||||||
|
|
||||||
GC.setTopperEnabled(showTopper);
|
GC.setTopperEnabled(showTopper);
|
||||||
GC.setClusters(Math.round((parseFloat(lengthInp.value) || 0) * 2));
|
GC.setClusters(Math.round((parseFloat(lengthInp.value) || 0) * 2));
|
||||||
GC.setReverse(!!reverseCb?.checked);
|
GC.setReverse(!!reverseCb?.checked);
|
||||||
GC.setTopperType(topperTypeSelect.value);
|
GC.setTopperType(getTopperType());
|
||||||
GC.setTopperOffsetX(topperOffsetX);
|
GC.setTopperOffsetX(topperOffsetX);
|
||||||
GC.setTopperOffsetY(topperOffsetY);
|
GC.setTopperOffsetY(topperOffsetY);
|
||||||
GC.setTopperSize(topperSizeInp?.value);
|
GC.setTopperSize(topperSizeInp?.value);
|
||||||
GC.setShineEnabled(!!shineEnabledCb?.checked);
|
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)`;
|
if(clusterHint) clusterHint.textContent = `≈ ${Math.round((parseFloat(lengthInp.value) || 0) * 2)} clusters (rule: 2 clusters/ft)`;
|
||||||
ctrl.selectPattern(patternName);
|
ctrl.selectPattern(patternName);
|
||||||
syncDockPatternButtons(patternName);
|
|
||||||
syncDockTopperButton();
|
|
||||||
renderLengthPresets(patternName);
|
|
||||||
syncTopperInline();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setLengthForPattern = () => {
|
const setLengthForPattern = () => {
|
||||||
if (!lengthInp || !patSel) return;
|
if (!lengthInp || !patSel) return;
|
||||||
const isArch = (patSel.value || '').toLowerCase().includes('arch');
|
const isArch = (computePatternName()).toLowerCase().includes('arch');
|
||||||
lengthInp.value = isArch ? ARCH_DEFAULT : COLUMN_DEFAULT;
|
lengthInp.value = isArch ? 20 : 5;
|
||||||
};
|
};
|
||||||
|
|
||||||
window.ClassicDesigner = window.ClassicDesigner || {};
|
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));
|
document.querySelector('#mode-tabs')?.addEventListener('click', () => setTimeout(() => { if (window.updateExportButtonVisibility) window.updateExportButtonVisibility() }, 50));
|
||||||
patSel?.addEventListener('change', () => {
|
patSel?.addEventListener('change', () => {
|
||||||
ensureLengthForPattern(patSel.value);
|
lastPresetKey = null;
|
||||||
|
syncPatternStateFromSelect();
|
||||||
|
applyPatternButtons();
|
||||||
|
setLengthForPattern();
|
||||||
updateClassicDesign();
|
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', () => {
|
topperNudgeBtns.forEach(btn => btn.addEventListener('click', () => {
|
||||||
const dx = Number(btn.dataset.dx || 0);
|
const dx = Number(btn.dataset.dx || 0);
|
||||||
const dy = Number(btn.dataset.dy || 0);
|
const dy = Number(btn.dataset.dy || 0);
|
||||||
topperOffsetX += dx;
|
topperOffsetX += dx;
|
||||||
topperOffsetY += dy;
|
topperOffsetY += dy;
|
||||||
|
lastPresetKey = 'custom';
|
||||||
GC.setTopperOffsetX(topperOffsetX);
|
GC.setTopperOffsetX(topperOffsetX);
|
||||||
GC.setTopperOffsetY(topperOffsetY);
|
GC.setTopperOffsetY(topperOffsetY);
|
||||||
updateClassicDesign();
|
updateClassicDesign();
|
||||||
}));
|
}));
|
||||||
[lengthInp, reverseCb, topperEnabledCb, topperTypeSelect, topperSizeInp]
|
topperTypeButtons.forEach(btn => btn.addEventListener('click', () => {
|
||||||
.forEach(el => { if (!el) return; const eventType = (el.type === 'range' || el.type === 'number') ? 'input' : 'change'; el.addEventListener(eventType, updateClassicDesign); });
|
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', 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); });
|
shineEnabledCb?.addEventListener('change', (e) => { const on = !!e.target.checked; GC.setShineEnabled(on); updateClassicDesign(); window.syncAppShine?.(on); });
|
||||||
initClassicColorPicker(updateClassicDesign);
|
initClassicColorPicker(updateClassicDesign);
|
||||||
try { const saved = localStorage.getItem('app:shineEnabled:v1'); if (saved !== null && shineEnabledCb) shineEnabledCb.checked = JSON.parse(saved); } catch {}
|
try { const saved = localStorage.getItem('app:shineEnabled:v1'); if (saved !== null && shineEnabledCb) shineEnabledCb.checked = JSON.parse(saved); } catch {}
|
||||||
setLengthForPattern();
|
setLengthForPattern();
|
||||||
updateClassicDesign();
|
updateClassicDesign();
|
||||||
renderLengthPresets(patSel?.value || '');
|
|
||||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||||
log('Classic ready');
|
log('Classic ready');
|
||||||
} catch (e) { fail(e.message || e); }
|
} catch (e) { fail(e.message || e); }
|
||||||
|
|||||||
297
index.html
297
index.html
@ -13,6 +13,7 @@
|
|||||||
<script src="colors.js"></script>
|
<script src="colors.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<style>
|
<style>
|
||||||
.tab-btn{padding:.5rem .75rem;border-radius:.5rem;font-size:.875rem;font-weight:600;transition:background-color .2s,color .2s,box-shadow .2s}
|
.tab-btn{padding:.5rem .75rem;border-radius:.5rem;font-size:.875rem;font-weight:600;transition:background-color .2s,color .2s,box-shadow .2s}
|
||||||
.tab-active{background:#2563eb;color:#fff;box-shadow:0 2px 6px rgba(37,99,235,.35)}
|
.tab-active{background:#2563eb;color:#fff;box-shadow:0 2px 6px rgba(37,99,235,.35)}
|
||||||
@ -21,42 +22,59 @@
|
|||||||
.copy-message{opacity:0;pointer-events:none;transition:opacity .2s}.copy-message.show{opacity:1}
|
.copy-message{opacity:0;pointer-events:none;transition:opacity .2s}.copy-message.show{opacity:1}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="flex flex-col min-h-screen p-0 md:p-6 items-center justify-start bg-[conic-gradient(at_top_left,_var(--tw-gradient-stops))] from-indigo-100 via-white to-pink-100 text-slate-800">
|
<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">
|
||||||
|
|
||||||
|
<header class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
|
||||||
<header class="flex items-center justify-between gap-3 px-2 lg:px-0 py-2">
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-black text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-pink-600 tracking-tight filter drop-shadow-sm">Balloon Studio</div>
|
<div class="text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-pink-600 tracking-tight filter drop-shadow-sm">Balloon Studio</div>
|
||||||
|
<div class="text-xs text-indigo-500 font-bold uppercase tracking-wider">Professional Design Tool</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<nav id="mode-tabs" class="flex gap-2">
|
||||||
<!-- Mode Switcher Restored -->
|
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
||||||
<nav id="mode-tabs" class="flex gap-1 bg-slate-100 p-1 rounded-lg">
|
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic (Arch/Column)</button>
|
||||||
<button type="button" class="tab-btn tab-active text-xs !py-1 !px-2" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
|
||||||
<button type="button" class="tab-btn tab-idle text-xs !py-1 !px-2" data-target="#tab-classic" aria-pressed="false">Classic</button>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button id="header-undo" class="tool-btn !p-2" title="Undo">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
|
|
||||||
</button>
|
|
||||||
<button id="header-redo" class="tool-btn !p-2" title="Redo">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
|
|
||||||
</button>
|
|
||||||
<button id="header-export" class="btn-blue text-xs font-bold !py-2 !px-3" data-export="png">Export</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section id="tab-organic" class="flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
|
<section id="tab-organic" class="flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
|
||||||
<aside id="controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
|
<aside id="controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
|
||||||
<div class="panel-header-row">
|
<div class="panel-header-row">
|
||||||
<h2 class="panel-title">Organic Controls</h2>
|
<h2 class="panel-title">Organic Controls</h2>
|
||||||
<button type="button" class="sheet-close-btn" data-sheet-toggle="controls-panel">Hide</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="control-stack" data-mobile-tab="controls">
|
<div class="control-stack" data-mobile-tab="controls">
|
||||||
<div class="panel-heading">Selection Options</div>
|
<div class="panel-heading">Tools</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
|
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||||
|
<button id="tool-draw" class="tool-btn" aria-pressed="true" title="V">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||||
|
<span class="hidden sm:inline">Draw</span>
|
||||||
|
</button>
|
||||||
|
<button id="tool-erase" class="tool-btn" aria-pressed="false" title="E">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 0 1-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zM4.22 15.58l3.54 3.53c.78.79 2.04.79 2.83 0l8.48-8.48-3.54-3.54-8.48 8.48c-.79.79-.79 2.05 0 2.84z"/></svg>
|
||||||
|
<span class="hidden sm:inline">Erase</span>
|
||||||
|
</button>
|
||||||
|
<button id="tool-select" class="tool-btn" aria-pressed="false" title="S">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M7 2l12 11.2-5.8.5 3.3 7.3-2.2.9-3.2-7.4-4.4 4V2z"/></svg>
|
||||||
|
<span class="hidden sm:inline">Select</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||||
|
<button id="tool-undo" class="tool-btn" title="Ctrl+Z" aria-label="Undo">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
|
||||||
|
<span class="hidden sm:inline">Undo</span>
|
||||||
|
</button>
|
||||||
|
<button id="tool-redo" class="tool-btn" title="Ctrl+Y" aria-label="Redo">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
|
||||||
|
<span class="hidden sm:inline">Redo</span>
|
||||||
|
</button>
|
||||||
|
<button id="tool-eyedropper" class="tool-btn" title="Pick Color" aria-label="Eyedropper" aria-pressed="false">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M20.71 5.63l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-3.12 3.12-1.93-1.91-1.41 1.41 1.42 1.42L3 16.25V21h4.75l8.92-8.92 1.42 1.42 1.41-1.41-1.92-1.92 3.12-3.12c.4-.4.4-1.03.01-1.42zM6.92 19L5 17.08l8.06-8.06 1.92 1.92L6.92 19z"/></svg>
|
||||||
|
<span class="hidden sm:inline">Picker</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div id="eraser-controls" class="hidden flex flex-col gap-2">
|
<div id="eraser-controls" class="hidden flex flex-col gap-2">
|
||||||
<label class="text-sm font-medium text-gray-700">Eraser Size: <span id="eraser-size-label">30</span>px</label>
|
<label class="text-sm font-medium text-gray-700">Eraser Size: <span id="eraser-size-label">30</span>px</label>
|
||||||
<input type="range" id="eraser-size" min="10" max="120" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
<input type="range" id="eraser-size" min="10" max="120" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||||
@ -94,46 +112,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="hint">Click a balloon to select. Del/Backspace removes. Esc clears.</p>
|
<p class="hint">Click a balloon to select. Del/Backspace removes. Esc clears.</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Fallback hint if nothing selected/erasing -->
|
|
||||||
<p id="controls-empty-hint" class="hint mt-2 hidden">Select a balloon or choose the Eraser tool to see options.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="panel-heading mt-4">Size & Shine</div>
|
||||||
<div class="panel-heading">Balloon Size</div>
|
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div id="size-preset-group" class="grid grid-cols-5 gap-2 mb-2"></div>
|
<div id="size-preset-group" class="grid grid-cols-5 gap-2 mb-2"></div>
|
||||||
|
<p class="hint mb-3">Global scale lives in <code>PX_PER_INCH</code> (see <code>script.js</code>).</p>
|
||||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
||||||
<input id="toggle-shine-checkbox" type="checkbox" class="align-middle" checked>
|
<input id="toggle-shine-checkbox" type="checkbox" class="align-middle" checked>
|
||||||
Enable Shine
|
Enable Shine
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel-heading mt-4">Palette in Use</div>
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
|
<div class="panel-heading">Used Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span class="text-sm text-gray-600">Colors currently on your canvas.</span>
|
<span class="text-sm text-gray-600">Built from the current design. Click a swatch to select that color.</span>
|
||||||
<button id="sort-used-toggle" class="text-sm underline">Sort: Most → Least</button>
|
<button id="sort-used-toggle" class="text-sm underline">Sort: Most → Least</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-heading mt-4">Color Library</div>
|
<div class="panel-heading mt-4">Allowed Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<p class="hint mb-2">Pick a color to draw with.</p>
|
<p class="hint mb-2">Alt+Click a balloon on canvas to pick its color.</p>
|
||||||
<div id="color-palette" class="palette-box"></div>
|
<div id="color-palette" class="palette-box"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-heading mt-4">Swap Colors</div>
|
<div class="panel-heading mt-4">Replace Color</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="grid grid-cols-1 gap-2">
|
<div class="grid grid-cols-1 gap-2">
|
||||||
<label class="text-sm font-medium">Change this color:</label>
|
<label class="text-sm font-medium">From (used):</label>
|
||||||
<select id="replace-from" class="select"></select>
|
<select id="replace-from" class="select"></select>
|
||||||
|
|
||||||
<label class="text-sm font-medium">To this new color:</label>
|
<label class="text-sm font-medium">To (allowed):</label>
|
||||||
<select id="replace-to" class="select"></select>
|
<select id="replace-to" class="select"></select>
|
||||||
|
|
||||||
<button id="replace-btn" class="btn-blue">Swap All</button>
|
<button id="replace-btn" class="btn-blue">Replace</button>
|
||||||
<p id="replace-msg" class="hint"></p>
|
<p id="replace-msg" class="hint"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -175,32 +192,45 @@
|
|||||||
<aside id="classic-controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
|
<aside id="classic-controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
|
||||||
<div class="panel-header-row">
|
<div class="panel-header-row">
|
||||||
<h2 class="panel-title">Classic Controls</h2>
|
<h2 class="panel-title">Classic Controls</h2>
|
||||||
<button type="button" class="sheet-close-btn" data-sheet-toggle="classic-controls-panel">Hide</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="control-stack" data-mobile-tab="controls">
|
<div class="control-stack" data-mobile-tab="controls">
|
||||||
<div class="panel-heading">Pattern & Layout</div>
|
<div class="panel-heading">Pattern & Layout</div>
|
||||||
<div class="panel-card space-y-4">
|
<div class="panel-card space-y-4">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<span class="classic-quick-label">Colors</span>
|
<div class="space-y-2">
|
||||||
<div class="dock-pill-group">
|
<div class="text-sm font-medium text-gray-700">Shape</div>
|
||||||
<button type="button" class="dock-pill classic-variant-btn" data-pattern-variant="4">4 colors</button>
|
<div class="flex gap-2">
|
||||||
<button type="button" class="dock-pill classic-variant-btn" data-pattern-variant="5">5 colors</button>
|
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-shape="arch" aria-pressed="true">Arch</button>
|
||||||
|
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-shape="column" aria-pressed="false">Column</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="space-y-2">
|
||||||
<select id="classic-pattern" class="select hidden">
|
<div class="text-sm font-medium text-gray-700">Balloon Count</div>
|
||||||
<option value="Arch 4">Arch 4 (4-color spiral)</option>
|
<div class="flex gap-2">
|
||||||
<option value="Column 4">Column 4 (quad wrap)</option>
|
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-count="4" aria-pressed="true">4 Colors</button>
|
||||||
<option value="Arch 5">Arch 5 (5-color spiral)</option>
|
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-count="5" aria-pressed="false">5 Colors</button>
|
||||||
<option value="Column 5">Column 5 (5-balloon wrap)</option>
|
</div>
|
||||||
</select>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="classic-quick-label">Length</span>
|
|
||||||
<span class="text-xs text-gray-500" id="classic-length-label">ft</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="classic-length-presets" class="length-dial"></div>
|
<div class="md:col-span-2 space-y-2">
|
||||||
<input id="classic-length-ft" type="number" min="1" max="100" step="0.5" value="5" class="hidden">
|
<div class="text-sm font-medium text-gray-700">Layout</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
||||||
|
<option value="Arch 4">Arch 4 (4-color spiral)</option>
|
||||||
|
<option value="Column 4">Column 4 (quad wrap)</option>
|
||||||
|
<option value="Arch 5">Arch 5 (5-color spiral)</option>
|
||||||
|
<option value="Column 5">Column 5 (5-balloon wrap)</option>
|
||||||
|
<option value="Arch 4 Stacked">Arch 4 (stacked)</option>
|
||||||
|
<option value="Column 4 Stacked">Column 4 (stacked)</option>
|
||||||
|
<option value="Arch 5 Stacked">Arch 5 (stacked)</option>
|
||||||
|
<option value="Column 5 Stacked">Column 5 (stacked)</option>
|
||||||
|
</select>
|
||||||
|
<label class="text-sm">Length (ft):
|
||||||
|
<input id="classic-length-ft" type="number" min="1" max="100" step="0.5" value="5" class="w-full px-2 py-1 border rounded align-middle">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="classic-topper-toggle-row" class="flex items-center gap-3 pt-2 border-t border-gray-200 hidden">
|
<div id="classic-topper-toggle-row" class="flex items-center gap-3 pt-2 border-t border-gray-200 hidden">
|
||||||
@ -210,29 +240,19 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="topper-controls" class="hidden grid grid-cols-1 sm:grid-cols-4 gap-3 items-end pt-2">
|
<div id="topper-controls" class="hidden grid grid-cols-1 sm:grid-cols-4 gap-3 items-start pt-2">
|
||||||
<label class="text-sm sm:col-span-2">Topper Type:
|
<div class="sm:col-span-2">
|
||||||
<select id="classic-topper-type" class="select align-middle" disabled>
|
<div class="text-sm font-medium mb-2">Topper Shape</div>
|
||||||
<option value="round">24" Round</option>
|
<div id="topper-type-group" class="topper-type-group">
|
||||||
<option value="star">24" Star</option>
|
<button type="button" class="tab-btn topper-type-btn tab-active" data-type="round" aria-pressed="true"><i class="fa-regular fa-circle-dot"></i><span class="hidden sm:inline">Round</span></button>
|
||||||
<option value="heart">24" Heart</option>
|
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="star" aria-pressed="false"><i class="fa-solid fa-star"></i><span class="hidden sm:inline">Star</span></button>
|
||||||
</select>
|
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="heart" aria-pressed="false"><i class="fa-solid fa-heart"></i><span class="hidden sm:inline">Heart</span></button>
|
||||||
</label>
|
|
||||||
<label class="text-sm">Topper Color:
|
|
||||||
<div id="classic-topper-color-swatch" class="slot-swatch mx-auto" title="Click to change topper color">T</div>
|
|
||||||
</label>
|
|
||||||
<div class="col-span-full">
|
|
||||||
<div class="panel-heading mt-2 mb-2">Topper Nudge</div>
|
|
||||||
<div class="grid grid-cols-3 gap-2 max-w-xs justify-items-center">
|
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up">↑</button>
|
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="0.5" data-dy="0" aria-label="Move Topper Right">→</button>
|
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down">↓</button>
|
|
||||||
<button type="button" class="btn-nudge nudge-topper col-span-3" data-dx="-0.5" data-dy="0" aria-label="Move Topper Left">←</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="text-sm">Topper Size:
|
<div class="sm:col-span-2">
|
||||||
<input id="classic-topper-size" type="range" min="0.5" max="2" step="0.05" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
<div class="text-sm font-medium mb-1">Topper Size</div>
|
||||||
</label>
|
<input id="classic-topper-size" type="range" min="0.5" max="2" step="0.05" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
@ -255,18 +275,20 @@
|
|||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
<div class="panel-heading">Classic Colors</div>
|
<div class="panel-heading">Classic Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div id="classic-slots" class="flex items-center gap-2 mb-3">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<button type="button" class="slot-btn tab-btn" data-slot="1">#1</button>
|
<div id="classic-slots" class="flex items-center gap-2"></div>
|
||||||
<button type="button" class="slot-btn tab-btn" data-slot="2">#2</button>
|
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
||||||
<button type="button" class="slot-btn tab-btn" data-slot="3">#3</button>
|
|
||||||
<button type="button" class="slot-btn tab-btn" data-slot="4">#4</button>
|
|
||||||
<button type="button" class="slot-btn tab-btn" data-slot="5">#5</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
|
<div class="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 id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
||||||
<div class="flex flex-wrap gap-2 mt-3">
|
<div class="flex flex-wrap gap-2 mt-3">
|
||||||
<button id="classic-randomize-colors" class="btn-dark">Randomize 5</button>
|
<button id="classic-randomize-colors" class="btn-dark">Randomize 5</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-heading mt-3">Topper Color</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="classic-topper-color-swatch" class="slot-swatch" title="Click to change topper color">T</button>
|
||||||
|
<p class="hint">Select a color then click to apply.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -288,90 +310,43 @@
|
|||||||
<div id="classic-display"
|
<div id="classic-display"
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
|
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
|
||||||
</section>
|
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
||||||
|
<div class="floating-nudge-header">
|
||||||
</section>
|
<div class="panel-heading">Nudge Topper</div>
|
||||||
|
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2">Hide</button>
|
||||||
|
</div>
|
||||||
<div id="mobile-tabbar" class="mobile-tabbar justify-center gap-6 !py-4 !bg-slate-900/95 backdrop-blur-xl border-t border-white/10">
|
<div class="floating-nudge-body">
|
||||||
<div id="dock-organic" class="flex items-center gap-3">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<button id="dock-draw" class="mobile-tool-btn active" data-dock="organic" title="Draw">
|
<div></div>
|
||||||
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up">↑</button>
|
||||||
</button>
|
<div></div>
|
||||||
<button id="dock-erase" class="mobile-tool-btn" data-dock="organic" title="Erase">
|
<button type="button" class="btn-dark nudge-topper" data-dx="-0.5" data-dy="0" aria-label="Move Topper Left">←</button>
|
||||||
<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 0 1-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zM4.22 15.58l3.54 3.53c.78.79 2.04.79 2.83 0l8.48-8.48-3.54-3.54-8.48 8.48c-.79.79-.79 2.05 0 2.84z"/></svg>
|
<div></div>
|
||||||
</button>
|
<button type="button" class="btn-dark nudge-topper" data-dx="0.5" data-dy="0" aria-label="Move Topper Right">→</button>
|
||||||
<button id="dock-color-trigger" class="dock-color-btn" style="background-color: #2563eb;" aria-label="Open Colors"></button>
|
<div></div>
|
||||||
<button id="dock-select" class="mobile-tool-btn" data-dock="organic" title="Select">
|
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down">↓</button>
|
||||||
<svg viewBox="0 0 24 24"><path d="M7 2l12 11.2-5.8.5 3.3 7.3-2.2.9-3.2-7.4-4.4 4V2z"/></svg>
|
<div></div>
|
||||||
</button>
|
</div>
|
||||||
<button id="dock-picker" class="mobile-tool-btn" data-dock="organic" title="Picker">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M20.71 5.63l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-3.12 3.12-1.93-1.91-1.41 1.41 1.42 1.42L3 16.25V21h4.75l8.92-8.92 1.42 1.42 1.41-1.41-1.92-1.92 3.12-3.12c.4-.4.4-1.03.01-1.42zM6.92 19L5 17.08l8.06-8.06 1.92 1.92L6.92 19z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dock-classic" class="hidden flex items-center justify-center gap-3">
|
|
||||||
<button id="dock-arch" class="mobile-tool-btn classic-pattern-btn" data-pattern-base="Arch" title="Arch Pattern">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M4 18a8 8 0 0 1 16 0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="dock-classic-color" class="dock-color-btn" style="background-color:#2563eb;" aria-label="Open Colors"></button>
|
|
||||||
|
|
||||||
<button id="dock-column" class="mobile-tool-btn classic-pattern-btn" data-pattern-base="Column" title="Column Pattern">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M8 4v16M16 4v16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Classic drawers for mobile -->
|
|
||||||
<div id="classic-drawer-pattern" class="classic-drawer hidden">
|
|
||||||
<div class="drawer-row">
|
|
||||||
<div class="drawer-pill-group">
|
|
||||||
<button type="button" class="dock-pill classic-pattern-btn" data-pattern-base="Arch">Arch</button>
|
|
||||||
<button type="button" class="dock-pill classic-pattern-btn" data-pattern-base="Column">Column</button>
|
|
||||||
</div>
|
|
||||||
<div class="drawer-pill-group">
|
|
||||||
<button type="button" class="dock-pill classic-variant-btn" data-pattern-variant="4">4 colors</button>
|
|
||||||
<button type="button" class="dock-pill classic-variant-btn" data-pattern-variant="5">5 colors</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="drawer-row dial-row">
|
|
||||||
<span class="drawer-label">Length</span>
|
|
||||||
<div id="classic-length-drawer" class="length-dial"></div>
|
|
||||||
</div>
|
|
||||||
<div class="drawer-row topper-row">
|
|
||||||
<button type="button" class="dock-pill classic-topper-btn" data-topper-toggle>Topper</button>
|
|
||||||
<div class="topper-inline hidden" id="topper-inline">
|
|
||||||
<select id="classic-topper-type-inline" class="select select-xs">
|
|
||||||
<option value="round">Round</option>
|
|
||||||
<option value="star">Star</option>
|
|
||||||
<option value="heart">Heart</option>
|
|
||||||
</select>
|
|
||||||
<div id="classic-topper-color-swatch-inline" class="slot-swatch" title="Topper color">T</div>
|
|
||||||
<div class="nudge-pad">
|
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="0" data-dy="0.5">↑</button>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="-0.5" data-dy="0">←</button>
|
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="0.5" data-dy="0">→</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-nudge nudge-topper" data-dx="0" data-dy="-0.5">↓</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="topper-size-wrap">
|
</section>
|
||||||
<label class="text-xs text-slate-600">Size</label>
|
</section>
|
||||||
<input id="classic-topper-size-inline" type="range" min="0.5" max="2" step="0.05" value="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="classic-drawer-colors" class="classic-drawer hidden">
|
<div id="mobile-tabbar" class="mobile-tabbar">
|
||||||
<div class="drawer-row">
|
<button type="button" class="mobile-tab-btn" data-mobile-tab="controls" aria-pressed="true" aria-label="Tools">
|
||||||
<div id="classic-slots-drawer" class="flex items-center gap-2"></div>
|
<i class="mobile-tab-icon fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i>
|
||||||
<div class="flex-1 text-right">
|
<span class="sr-only">Tools</span>
|
||||||
<button id="classic-randomize-colors-inline" class="dock-pill">Shuffle 5</button>
|
</button>
|
||||||
</div>
|
<button type="button" class="mobile-tab-btn" data-mobile-tab="colors" aria-pressed="false" aria-label="Colors">
|
||||||
</div>
|
<i class="mobile-tab-icon fa-solid fa-palette" aria-hidden="true"></i>
|
||||||
<div id="classic-swatch-drawer" class="palette-box"></div>
|
<span class="sr-only">Colors</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-tab-btn" data-mobile-tab="save" aria-pressed="false" aria-label="Save and Share">
|
||||||
|
<i class="mobile-tab-icon fa-solid fa-cloud-arrow-up" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">Save</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="message-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
<div id="message-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
||||||
|
|||||||
412
script.js
412
script.js
@ -199,19 +199,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bind Undo/Redo Buttons
|
// Bind Undo/Redo Buttons
|
||||||
document.getElementById('tool-undo')?.addEventListener('click', () => {
|
document.getElementById('tool-undo')?.addEventListener('click', undo);
|
||||||
undo();
|
document.getElementById('tool-redo')?.addEventListener('click', redo);
|
||||||
// Auto-minimize on mobile to see changes
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
document.getElementById('controls-panel')?.classList.add('minimized');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('tool-redo')?.addEventListener('click', () => {
|
|
||||||
redo();
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
document.getElementById('controls-panel')?.classList.add('minimized');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Eyedropper Tool
|
// Eyedropper Tool
|
||||||
const toolEyedropperBtn = document.getElementById('tool-eyedropper');
|
const toolEyedropperBtn = document.getElementById('tool-eyedropper');
|
||||||
@ -221,10 +210,6 @@
|
|||||||
setMode('draw'); // toggle off
|
setMode('draw'); // toggle off
|
||||||
} else {
|
} else {
|
||||||
setMode('eyedropper');
|
setMode('eyedropper');
|
||||||
// Auto-minimize on mobile
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
document.getElementById('controls-panel')?.classList.add('minimized');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -304,53 +289,18 @@
|
|||||||
toolSelectBtn?.setAttribute('aria-pressed', String(mode === 'select'));
|
toolSelectBtn?.setAttribute('aria-pressed', String(mode === 'select'));
|
||||||
toolEyedropperBtn?.setAttribute('aria-pressed', String(mode === 'eyedropper'));
|
toolEyedropperBtn?.setAttribute('aria-pressed', String(mode === 'eyedropper'));
|
||||||
|
|
||||||
// Update Mobile Dock Active States
|
|
||||||
document.querySelectorAll('.mobile-tool-btn[data-dock="organic"]').forEach(btn => btn.classList.remove('active'));
|
|
||||||
if (mode === 'draw') document.getElementById('dock-draw')?.classList.add('active');
|
|
||||||
if (mode === 'erase') document.getElementById('dock-erase')?.classList.add('active');
|
|
||||||
if (mode === 'select') document.getElementById('dock-select')?.classList.add('active');
|
|
||||||
if (mode === 'eyedropper') document.getElementById('dock-picker')?.classList.add('active');
|
|
||||||
|
|
||||||
eraserControls?.classList.toggle('hidden', mode !== 'erase');
|
eraserControls?.classList.toggle('hidden', mode !== 'erase');
|
||||||
selectControls?.classList.toggle('hidden', mode !== 'select');
|
selectControls?.classList.toggle('hidden', mode !== 'select');
|
||||||
|
|
||||||
// Show/Hide empty hint in Selection Options panel
|
|
||||||
const emptyHint = document.getElementById('controls-empty-hint');
|
|
||||||
if (emptyHint) {
|
|
||||||
emptyHint.classList.toggle('hidden', mode === 'erase' || mode === 'select');
|
|
||||||
emptyHint.textContent = mode === 'draw' ? 'Switch to Select or Erase tool to see options.' : 'Select a tool...';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'erase') canvas.style.cursor = 'none';
|
if (mode === 'erase') canvas.style.cursor = 'none';
|
||||||
else if (mode === 'select') {
|
else if (mode === 'select') canvas.style.cursor = 'default'; // will be move over items
|
||||||
canvas.style.cursor = 'default';
|
|
||||||
}
|
|
||||||
else if (mode === 'eyedropper') canvas.style.cursor = 'cell';
|
else if (mode === 'eyedropper') canvas.style.cursor = 'cell';
|
||||||
else canvas.style.cursor = 'crosshair';
|
else canvas.style.cursor = 'crosshair';
|
||||||
|
|
||||||
// Contextual Tab Switching
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
if (mode === 'select' || mode === 'erase') {
|
|
||||||
setMobileTab('controls');
|
|
||||||
} else if (mode === 'draw') {
|
|
||||||
// Optional: switch to colors, or stay put?
|
|
||||||
// setMobileTab('colors');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimize drawer on tool switch to clear view
|
|
||||||
const panel = document.getElementById('controls-panel');
|
|
||||||
if (panel && !panel.classList.contains('minimized')) {
|
|
||||||
panel.classList.add('minimized');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw();
|
draw();
|
||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (rest of the file) ...
|
|
||||||
|
|
||||||
|
|
||||||
function updateSelectButtons() {
|
function updateSelectButtons() {
|
||||||
const has = !!selectedBalloonId;
|
const has = !!selectedBalloonId;
|
||||||
if (deleteSelectedBtn) deleteSelectedBtn.disabled = !has;
|
if (deleteSelectedBtn) deleteSelectedBtn.disabled = !has;
|
||||||
@ -466,31 +416,13 @@
|
|||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
// ====== Canvas & Drawing ======
|
// ====== Canvas & Drawing ======
|
||||||
let hasFittedView = false;
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
const rect = canvas.parentElement?.getBoundingClientRect?.() || canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const prevDpr = dpr || 1;
|
|
||||||
const prevCw = canvas.width / prevDpr;
|
|
||||||
const prevCh = canvas.height / prevDpr;
|
|
||||||
const prevCenter = {
|
|
||||||
x: (prevCw / 2) / (view.s || 1) - view.tx,
|
|
||||||
y: (prevCh / 2) / (view.s || 1) - view.ty
|
|
||||||
};
|
|
||||||
|
|
||||||
dpr = Math.max(1, window.devicePixelRatio || 1);
|
dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
canvas.width = Math.round(Math.min(rect.width, window.innerWidth) * dpr);
|
canvas.width = Math.round(rect.width * dpr);
|
||||||
canvas.height = Math.round(Math.min(rect.height, window.innerHeight) * dpr);
|
canvas.height = Math.round(rect.height * dpr);
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
fitView();
|
||||||
if (!hasFittedView) {
|
|
||||||
fitView();
|
|
||||||
hasFittedView = true;
|
|
||||||
} else if (prevCw > 0 && prevCh > 0) {
|
|
||||||
const cw = canvas.width / dpr;
|
|
||||||
const ch = canvas.height / dpr;
|
|
||||||
view.tx = (cw / (2 * (view.s || 1))) - prevCenter.x;
|
|
||||||
view.ty = (ch / (2 * (view.s || 1))) - prevCenter.y;
|
|
||||||
}
|
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
function clearCanvasArea() {
|
function clearCanvasArea() {
|
||||||
@ -631,22 +563,8 @@
|
|||||||
|
|
||||||
function saveAppState() {
|
function saveAppState() {
|
||||||
// Note: isShineEnabled is managed globally.
|
// Note: isShineEnabled is managed globally.
|
||||||
const state = { balloons, selectedColorIdx, currentDiameterInches, eraserRadius, mode, view, usedSortDesc };
|
const state = { balloons, selectedColorIdx, currentDiameterInches, eraserRadius, view, usedSortDesc };
|
||||||
try { localStorage.setItem(APP_STATE_KEY, JSON.stringify(state)); } catch {}
|
try { localStorage.setItem(APP_STATE_KEY, JSON.stringify(state)); } catch {}
|
||||||
|
|
||||||
// Update dock color trigger
|
|
||||||
const meta = FLAT_COLORS[selectedColorIdx];
|
|
||||||
const trig = document.getElementById('dock-color-trigger');
|
|
||||||
if (trig && meta) {
|
|
||||||
if (meta.image) {
|
|
||||||
trig.style.backgroundImage = `url("${meta.image}")`;
|
|
||||||
trig.style.backgroundSize = '200%';
|
|
||||||
trig.style.backgroundColor = 'transparent';
|
|
||||||
} else {
|
|
||||||
trig.style.backgroundImage = 'none';
|
|
||||||
trig.style.backgroundColor = meta.hex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const persist = (() => { let t; return () => { clearTimeout(t); t = setTimeout(saveAppState, 120); }; })();
|
const persist = (() => { let t; return () => { clearTimeout(t); t = setTimeout(saveAppState, 120); }; })();
|
||||||
|
|
||||||
@ -664,7 +582,6 @@
|
|||||||
if (eraserSizeInput) eraserSizeInput.value = eraserRadius;
|
if (eraserSizeInput) eraserSizeInput.value = eraserRadius;
|
||||||
if (eraserSizeLabel) eraserSizeLabel.textContent = eraserRadius;
|
if (eraserSizeLabel) eraserSizeLabel.textContent = eraserRadius;
|
||||||
}
|
}
|
||||||
if (typeof s.mode === 'string') mode = s.mode;
|
|
||||||
if (s.view && typeof s.view.s === 'number') view = s.view;
|
if (s.view && typeof s.view.s === 'number') view = s.view;
|
||||||
if (typeof s.usedSortDesc === 'boolean') {
|
if (typeof s.usedSortDesc === 'boolean') {
|
||||||
usedSortDesc = s.usedSortDesc;
|
usedSortDesc = s.usedSortDesc;
|
||||||
@ -1188,20 +1105,11 @@
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSheets(activeId) {
|
function updateSheets() {
|
||||||
const tab = activeId || detectCurrentTab();
|
const tab = detectCurrentTab();
|
||||||
// Panels should be visible if their tab is active.
|
const hide = !window.matchMedia('(min-width: 1024px)').matches && document.body?.dataset?.controlsHidden === '1';
|
||||||
// Mobile minimization is handled by the .minimized class, not .hidden.
|
if (orgSheet) orgSheet.classList.toggle('hidden', hide || tab !== '#tab-organic');
|
||||||
if (orgSheet) orgSheet.classList.toggle('hidden', tab !== '#tab-organic');
|
if (claSheet) claSheet.classList.toggle('hidden', hide || tab !== '#tab-classic');
|
||||||
if (claSheet) claSheet.classList.toggle('hidden', tab !== '#tab-classic');
|
|
||||||
|
|
||||||
// Ensure Dock is visible on both tabs (content managed by setTab)
|
|
||||||
const dock = document.getElementById('mobile-tabbar');
|
|
||||||
if (dock) dock.style.display = 'flex';
|
|
||||||
const dockOrg = document.getElementById('dock-organic');
|
|
||||||
const dockCla = document.getElementById('dock-classic');
|
|
||||||
dockOrg?.classList.toggle('hidden', tab === '#tab-classic');
|
|
||||||
dockCla?.classList.toggle('hidden', tab !== '#tab-classic');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportPng() {
|
async function exportPng() {
|
||||||
@ -1333,7 +1241,6 @@
|
|||||||
const worldH = ch / view.s;
|
const worldH = ch / view.s;
|
||||||
view.tx = (worldW - w) * 0.5 - box.minX;
|
view.tx = (worldW - w) * 0.5 - box.minX;
|
||||||
view.ty = (worldH - h) * 0.5 - box.minY;
|
view.ty = (worldH - h) * 0.5 - box.minY;
|
||||||
hasFittedView = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function balloonScreenBounds(b) {
|
function balloonScreenBounds(b) {
|
||||||
@ -1518,6 +1425,7 @@
|
|||||||
window.syncAppShine(on);
|
window.syncAppShine(on);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mode = 'draw'; // force default tool on load
|
||||||
renderAllowedPalette();
|
renderAllowedPalette();
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
loadFromUrl();
|
loadFromUrl();
|
||||||
@ -1561,17 +1469,12 @@
|
|||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
const stacks = Array.from(panel.querySelectorAll('.control-stack'));
|
const stacks = Array.from(panel.querySelectorAll('.control-stack'));
|
||||||
if (!stacks.length) return;
|
if (!stacks.length) return;
|
||||||
|
|
||||||
// If we passed 'all', show everything (Desktop mode)
|
|
||||||
const showAll = (tabName === 'all');
|
|
||||||
|
|
||||||
stacks.forEach(stack => {
|
stacks.forEach(stack => {
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
stack.style.display = 'none';
|
stack.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
const show = showAll ? true : stack.dataset.mobileTab === target;
|
const show = stack.dataset.mobileTab === target;
|
||||||
// Use flex to match CSS .control-stack
|
stack.style.display = show ? 'block' : 'none';
|
||||||
stack.style.display = show ? 'flex' : 'none';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1590,6 +1493,21 @@
|
|||||||
}
|
}
|
||||||
window.__setMobileTab = setMobileTab;
|
window.__setMobileTab = setMobileTab;
|
||||||
|
|
||||||
|
let floatingNudgeCollapsed = false;
|
||||||
|
function updateFloatingNudge() {
|
||||||
|
const el = document.getElementById('floating-topper-nudge');
|
||||||
|
if (!el) return;
|
||||||
|
const isMobile = !window.matchMedia('(min-width: 1024px)').matches;
|
||||||
|
const classicActive = document.body?.dataset.activeTab === '#tab-classic';
|
||||||
|
const topperActive = document.body?.dataset.topperOverlay === '1';
|
||||||
|
const shouldShow = isMobile && classicActive && topperActive;
|
||||||
|
el.classList.toggle('hidden', !shouldShow);
|
||||||
|
el.classList.toggle('collapsed', floatingNudgeCollapsed);
|
||||||
|
const toggle = document.getElementById('floating-nudge-toggle');
|
||||||
|
if (toggle) toggle.textContent = floatingNudgeCollapsed ? 'Show' : 'Hide';
|
||||||
|
}
|
||||||
|
window.__updateFloatingNudge = updateFloatingNudge;
|
||||||
|
|
||||||
if (orgSection && claSection && tabBtns.length > 0) {
|
if (orgSection && claSection && tabBtns.length > 0) {
|
||||||
let current = '#tab-organic';
|
let current = '#tab-organic';
|
||||||
|
|
||||||
@ -1605,11 +1523,8 @@
|
|||||||
|
|
||||||
orgSection.classList.toggle('hidden', id !== '#tab-organic');
|
orgSection.classList.toggle('hidden', id !== '#tab-organic');
|
||||||
claSection.classList.toggle('hidden', id !== '#tab-classic');
|
claSection.classList.toggle('hidden', id !== '#tab-classic');
|
||||||
updateSheets(id);
|
updateSheets();
|
||||||
|
updateFloatingNudge();
|
||||||
// Ensure Dock is visible
|
|
||||||
const dock = document.getElementById('mobile-tabbar');
|
|
||||||
if (dock) dock.style.display = 'flex';
|
|
||||||
|
|
||||||
tabBtns.forEach(btn => {
|
tabBtns.forEach(btn => {
|
||||||
const active = btn.dataset.target === id;
|
const active = btn.dataset.target === id;
|
||||||
@ -1624,7 +1539,9 @@
|
|||||||
|
|
||||||
if (document.body) delete document.body.dataset.controlsHidden;
|
if (document.body) delete document.body.dataset.controlsHidden;
|
||||||
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
||||||
|
orgSheet?.classList.toggle('hidden', id !== '#tab-organic');
|
||||||
|
claSheet?.classList.toggle('hidden', id !== '#tab-classic');
|
||||||
|
|
||||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1645,232 +1562,55 @@
|
|||||||
setMobileTab(document.body.dataset.mobileTab);
|
setMobileTab(document.body.dataset.mobileTab);
|
||||||
updateSheets();
|
updateSheets();
|
||||||
updateMobileStacks(document.body.dataset.mobileTab);
|
updateMobileStacks(document.body.dataset.mobileTab);
|
||||||
|
|
||||||
// Sheet toggle buttons (Hide/Show)
|
|
||||||
document.querySelectorAll('[data-sheet-toggle]').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const id = btn.dataset.sheetToggle;
|
|
||||||
const panel = document.getElementById(id);
|
|
||||||
if (!panel) return;
|
|
||||||
const now = panel.classList.contains('minimized');
|
|
||||||
panel.classList.toggle('minimized', !now);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// ===== Mobile Dock Logic =======
|
// ===== Mobile bottom tabs ======
|
||||||
// ===============================
|
// ===============================
|
||||||
(function initMobileDock() {
|
(function initMobileTabs() {
|
||||||
if (window.__dockInit) return;
|
const buttons = Array.from(document.querySelectorAll('#mobile-tabbar .mobile-tab-btn'));
|
||||||
window.__dockInit = true;
|
if (!buttons.length) return;
|
||||||
|
|
||||||
const dockOrganic = document.getElementById('dock-organic');
|
buttons.forEach(btn => {
|
||||||
const dockClassic = document.getElementById('dock-classic');
|
btn.addEventListener('click', () => {
|
||||||
const patternBtns = Array.from(document.querySelectorAll('.classic-pattern-btn'));
|
const tab = btn.dataset.mobileTab || 'controls';
|
||||||
const variantBtns = Array.from(document.querySelectorAll('.classic-variant-btn'));
|
const panel = (!document.getElementById('tab-classic')?.classList.contains('hidden')
|
||||||
const topperBtns = Array.from(document.querySelectorAll('.classic-topper-btn'));
|
? document.getElementById('classic-controls-panel')
|
||||||
const drawerPattern = document.getElementById('classic-drawer-pattern');
|
: document.getElementById('controls-panel'));
|
||||||
const drawerColors = document.getElementById('classic-drawer-colors');
|
|
||||||
|
const currentTab = document.body.dataset.mobileTab;
|
||||||
function openColorsPanel() {
|
|
||||||
const isMobile = window.matchMedia('(max-width: 1023px)').matches;
|
if (tab === currentTab) {
|
||||||
if (isMobile) setMobileTab('colors');
|
// Toggle minimized state
|
||||||
const tab = detectCurrentTab();
|
panel.classList.toggle('minimized');
|
||||||
const panel = tab === '#tab-classic'
|
} else {
|
||||||
? document.getElementById('classic-controls-panel')
|
// Switch tab and ensure expanded
|
||||||
: document.getElementById('controls-panel');
|
panel.classList.remove('minimized');
|
||||||
if (isMobile) panel?.classList.remove('minimized');
|
setMobileTab(tab);
|
||||||
}
|
panel.scrollTop = 0;
|
||||||
|
|
||||||
const openOrganicPanel = (tab = 'controls') => {
|
|
||||||
document.body.dataset.mobileTab = tab;
|
|
||||||
const panel = document.getElementById('controls-panel');
|
|
||||||
panel?.classList.remove('minimized');
|
|
||||||
updateSheets('#tab-organic');
|
|
||||||
updateMobileStacks(tab);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeClassicDrawers = () => {
|
|
||||||
drawerPattern?.classList.add('hidden');
|
|
||||||
drawerColors?.classList.add('hidden');
|
|
||||||
activeClassicMenu = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDrawer = (which) => {
|
|
||||||
closeClassicDrawers();
|
|
||||||
if (which === 'pattern') drawerPattern?.classList.remove('hidden');
|
|
||||||
if (which === 'colors') drawerColors?.classList.remove('hidden');
|
|
||||||
activeClassicMenu = which;
|
|
||||||
};
|
|
||||||
|
|
||||||
function syncDockGroup() {
|
|
||||||
const tab = detectCurrentTab();
|
|
||||||
dockOrganic?.classList.toggle('hidden', tab === '#tab-classic');
|
|
||||||
dockClassic?.classList.toggle('hidden', tab !== '#tab-classic');
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPatternParts = () => {
|
|
||||||
const sel = document.getElementById('classic-pattern');
|
|
||||||
const val = sel?.value || 'Arch 4';
|
|
||||||
const isArch = val.toLowerCase().includes('arch');
|
|
||||||
const variant = val.includes('5') ? '5' : '4';
|
|
||||||
return { base: isArch ? 'Arch' : 'Column', variant };
|
|
||||||
};
|
|
||||||
|
|
||||||
let activeClassicMenu = null;
|
|
||||||
|
|
||||||
const toggleClassicMenu = (target) => {
|
|
||||||
const panel = document.getElementById('classic-controls-panel');
|
|
||||||
const isMobile = window.matchMedia('(max-width: 1023px)').matches;
|
|
||||||
if (!panel) return;
|
|
||||||
|
|
||||||
const alreadyOpen = activeClassicMenu === target && isMobile && !panel.classList.contains('minimized');
|
|
||||||
if (alreadyOpen) {
|
|
||||||
closeClassicDrawers();
|
|
||||||
panel.classList.add('minimized');
|
|
||||||
patternBtns.forEach(btn => btn.classList.remove('active'));
|
|
||||||
topperBtns.forEach(btn => btn.classList.remove('active'));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activeClassicMenu = target;
|
|
||||||
panel.classList.remove('minimized');
|
|
||||||
if (isMobile) setMobileTab(target === 'colors' ? 'colors' : 'controls');
|
|
||||||
|
|
||||||
patternBtns.forEach(btn => btn.classList.toggle('active', target === 'pattern' && btn.dataset.patternBase === currentPatternParts().base));
|
|
||||||
topperBtns.forEach(btn => btn.classList.toggle('active', target === 'topper'));
|
|
||||||
|
|
||||||
if (target === 'pattern') openDrawer('pattern');
|
|
||||||
else if (target === 'colors') openDrawer('colors');
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyPattern = (base, variant) => {
|
|
||||||
const sel = document.getElementById('classic-pattern');
|
|
||||||
if (!sel) return;
|
|
||||||
const target = `${base} ${variant}`;
|
|
||||||
if (sel.value !== target) sel.value = target;
|
|
||||||
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshClassicButtons = () => {
|
|
||||||
const { base } = currentPatternParts();
|
|
||||||
const { variant } = currentPatternParts();
|
|
||||||
const topperOn = !!document.getElementById('classic-topper-enabled')?.checked;
|
|
||||||
patternBtns.forEach(btn => {
|
|
||||||
const b = (btn.dataset.patternBase || '').toLowerCase();
|
|
||||||
const active = base.toLowerCase() === b;
|
|
||||||
btn.classList.toggle('active', active);
|
|
||||||
btn.setAttribute('aria-pressed', String(active));
|
|
||||||
});
|
|
||||||
variantBtns.forEach(btn => {
|
|
||||||
const active = btn.dataset.patternVariant === variant;
|
|
||||||
btn.classList.toggle('active', active);
|
|
||||||
btn.setAttribute('aria-pressed', String(active));
|
|
||||||
});
|
|
||||||
topperBtns.forEach(btn => {
|
|
||||||
btn.classList.toggle('active', topperOn);
|
|
||||||
btn.setAttribute('aria-pressed', String(topperOn));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Organic bindings
|
|
||||||
document.getElementById('dock-draw')?.addEventListener('click', () => {
|
|
||||||
setMode('draw');
|
|
||||||
openOrganicPanel('controls');
|
|
||||||
});
|
|
||||||
document.getElementById('dock-erase')?.addEventListener('click', () => {
|
|
||||||
setMode('erase');
|
|
||||||
openOrganicPanel('controls');
|
|
||||||
});
|
|
||||||
document.getElementById('dock-select')?.addEventListener('click', () => {
|
|
||||||
setMode('select');
|
|
||||||
openOrganicPanel('controls');
|
|
||||||
});
|
|
||||||
document.getElementById('dock-picker')?.addEventListener('click', () => {
|
|
||||||
if (mode === 'eyedropper') setMode('draw');
|
|
||||||
else setMode('eyedropper');
|
|
||||||
openOrganicPanel('controls');
|
|
||||||
});
|
|
||||||
document.getElementById('dock-color-trigger')?.addEventListener('click', () => {
|
|
||||||
const isMobile = window.matchMedia('(max-width: 1023px)').matches;
|
|
||||||
if (isMobile) openOrganicPanel('colors');
|
|
||||||
else openColorsPanel();
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Classic bindings
|
const mq = window.matchMedia('(min-width: 1024px)');
|
||||||
patternBtns.forEach(btn => {
|
const sync = () => {
|
||||||
btn.addEventListener('click', () => {
|
if (mq.matches) {
|
||||||
const { variant } = currentPatternParts();
|
document.body?.removeAttribute('data-mobile-tab');
|
||||||
const base = btn.dataset.patternBase || 'Arch';
|
updateMobileStacks('controls'); // keep controls visible on desktop
|
||||||
applyPattern(base, variant);
|
} else {
|
||||||
toggleClassicMenu('pattern');
|
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
||||||
closeClassicDrawers();
|
}
|
||||||
refreshClassicButtons();
|
updateFloatingNudge();
|
||||||
});
|
};
|
||||||
});
|
mq.addEventListener('change', sync);
|
||||||
variantBtns.forEach(btn => {
|
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
||||||
btn.addEventListener('click', () => {
|
sync();
|
||||||
const { base } = currentPatternParts();
|
|
||||||
const variant = btn.dataset.patternVariant || '4';
|
|
||||||
applyPattern(base, variant);
|
|
||||||
toggleClassicMenu('pattern');
|
|
||||||
closeClassicDrawers();
|
|
||||||
refreshClassicButtons();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
topperBtns.forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const cb = document.getElementById('classic-topper-enabled');
|
|
||||||
if (!cb) return;
|
|
||||||
cb.checked = !cb.checked;
|
|
||||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
toggleClassicMenu('topper');
|
|
||||||
refreshClassicButtons();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.getElementById('dock-classic-color')?.addEventListener('click', () => {
|
|
||||||
if (activeClassicMenu === 'colors') closeClassicDrawers();
|
|
||||||
else toggleClassicMenu('colors');
|
|
||||||
refreshClassicButtons();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Header Export
|
const nudgeToggle = document.getElementById('floating-nudge-toggle');
|
||||||
document.getElementById('header-export')?.addEventListener('click', () => exportPng());
|
nudgeToggle?.addEventListener('click', () => {
|
||||||
document.getElementById('header-undo')?.addEventListener('click', undo);
|
floatingNudgeCollapsed = !floatingNudgeCollapsed;
|
||||||
document.getElementById('header-redo')?.addEventListener('click', redo);
|
updateFloatingNudge();
|
||||||
|
});
|
||||||
const mq = window.matchMedia('(min-width: 1024px)');
|
|
||||||
const sync = () => {
|
|
||||||
if (mq.matches) {
|
|
||||||
document.body?.removeAttribute('data-mobile-tab');
|
|
||||||
updateMobileStacks('all');
|
|
||||||
// Remove minimized on desktop just in case
|
|
||||||
const orgPanel = document.getElementById('controls-panel');
|
|
||||||
const claPanel = document.getElementById('classic-controls-panel');
|
|
||||||
if (orgPanel) { orgPanel.classList.remove('minimized'); orgPanel.style.display = ''; }
|
|
||||||
if (claPanel) { claPanel.classList.remove('minimized'); claPanel.style.display = ''; }
|
|
||||||
} else {
|
|
||||||
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
|
||||||
// Start minimized on mobile
|
|
||||||
document.getElementById('controls-panel')?.classList.add('minimized');
|
|
||||||
document.getElementById('classic-controls-panel')?.classList.add('minimized');
|
|
||||||
}
|
|
||||||
syncDockGroup();
|
|
||||||
refreshClassicButtons();
|
|
||||||
};
|
|
||||||
mq.addEventListener('change', sync);
|
|
||||||
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
|
||||||
sync();
|
|
||||||
|
|
||||||
// keep dock in sync when tab switches
|
|
||||||
document.querySelectorAll('#mode-tabs .tab-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => setTimeout(() => { syncDockGroup(); refreshClassicButtons(); }, 50));
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('classic-pattern')?.addEventListener('change', refreshClassicButtons);
|
|
||||||
document.getElementById('classic-topper-enabled')?.addEventListener('change', refreshClassicButtons);
|
|
||||||
refreshClassicButtons();
|
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
260
style.css
260
style.css
@ -160,6 +160,46 @@ body { color: #1f2937; }
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topper-type-group {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.topper-type-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
.topper-type-btn i { font-size: 1rem; }
|
||||||
|
|
||||||
|
.floating-nudge {
|
||||||
|
position: fixed;
|
||||||
|
right: 0.9rem;
|
||||||
|
bottom: 4.6rem;
|
||||||
|
background: rgba(255,255,255,0.95);
|
||||||
|
border: 1px solid rgba(148,163,184,0.3);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
|
||||||
|
width: 210px;
|
||||||
|
z-index: 35;
|
||||||
|
}
|
||||||
|
.floating-nudge-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .5rem;
|
||||||
|
margin-bottom: .35rem;
|
||||||
|
}
|
||||||
|
.floating-nudge-body.collapsed { display: none; }
|
||||||
|
.floating-nudge.collapsed .floating-nudge-body { display: none; }
|
||||||
|
.floating-nudge.collapsed #floating-nudge-toggle { opacity: 0.8; }
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.floating-nudge { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
.slot-label {
|
.slot-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
@ -177,8 +217,8 @@ body { color: #1f2937; }
|
|||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(255,255,255,0.6);
|
border: 1px solid rgba(255,255,255,0.6);
|
||||||
border-radius: 0.75rem;
|
border-radius: 1rem;
|
||||||
padding: 0.75rem;
|
padding: 1rem;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
|
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
|
||||||
}
|
}
|
||||||
.control-stack {
|
.control-stack {
|
||||||
@ -198,15 +238,15 @@ body { color: #1f2937; }
|
|||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border-top: 1px solid rgba(255,255,255,0.5);
|
border-top: 1px solid rgba(255,255,255,0.5);
|
||||||
box-shadow: 0 -4px 30px rgba(0,0,0,0.08);
|
box-shadow: 0 -4px 30px rgba(0,0,0,0.08);
|
||||||
border-radius: 1.25rem 1.25rem 0 0;
|
border-radius: 1.5rem 1.5rem 0 0;
|
||||||
padding: 1rem 0.75rem;
|
padding: 1.25rem 1rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
}
|
}
|
||||||
.control-sheet.hidden { display: none; }
|
.control-sheet.hidden { display: none; }
|
||||||
/* .control-sheet.minimized removed from global scope to fix desktop visibility */
|
.control-sheet.minimized { transform: translateY(100%); }
|
||||||
.panel-title {
|
.panel-title {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@ -238,9 +278,8 @@ body { color: #1f2937; }
|
|||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
body { padding-bottom: 88px; }
|
body { padding-bottom: 88px; }
|
||||||
html, body { height: 100%; }
|
html, body { height: 100%; overflow: hidden; }
|
||||||
|
|
||||||
.control-sheet.minimized { transform: translateY(100%); }
|
|
||||||
.control-sheet .control-stack { display: none; }
|
.control-sheet .control-stack { display: none; }
|
||||||
body[data-mobile-tab="controls"] #controls-panel [data-mobile-tab="controls"],
|
body[data-mobile-tab="controls"] #controls-panel [data-mobile-tab="controls"],
|
||||||
body[data-mobile-tab="colors"] #controls-panel [data-mobile-tab="colors"],
|
body[data-mobile-tab="colors"] #controls-panel [data-mobile-tab="colors"],
|
||||||
@ -254,187 +293,52 @@ body { color: #1f2937; }
|
|||||||
|
|
||||||
.mobile-tabbar {
|
.mobile-tabbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
inset-inline: 0;
|
||||||
width: 100%;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
padding: .55rem .85rem .8rem;
|
padding: .6rem .9rem .9rem;
|
||||||
background: rgba(15, 23, 42, 0.95);
|
background: linear-gradient(135deg, rgba(255,255,255,0.95), rgba(224,242,254,0.92));
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
color: #fff;
|
color: #0f172a;
|
||||||
z-index: 9999;
|
z-index: 40;
|
||||||
|
gap: .4rem;
|
||||||
|
box-shadow: 0 -6px 30px rgba(15, 23, 42, 0.12);
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
.mobile-tabbar .mobile-tab-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: .35rem;
|
gap: .35rem;
|
||||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
.mobile-tool-btn {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
.mobile-tool-btn svg { width: 1.5rem; height: 1.5rem; fill: currentColor; }
|
|
||||||
.mobile-tool-btn.active {
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
color: #38bdf8; /* Sky blue */
|
|
||||||
}
|
|
||||||
.mobile-tool-btn:active { transform: scale(0.9); }
|
|
||||||
#dock-classic { width: 100%; }
|
|
||||||
#dock-organic { align-items: center; }
|
|
||||||
.classic-drawer {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 4.2rem;
|
|
||||||
z-index: 9500;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.6rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
background: rgba(15,23,42,0.96);
|
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
-webkit-backdrop-filter: blur(14px);
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.08);
|
|
||||||
box-shadow: 0 -8px 30px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
.classic-drawer.hidden { display: none; }
|
|
||||||
.drawer-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.drawer-label { color: #cbd5e1; font-weight: 700; font-size: 0.9rem; }
|
|
||||||
.drawer-pill-group { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
|
||||||
.dial-row { flex-direction: column; align-items: flex-start; }
|
|
||||||
.topper-row { gap: 0.75rem; }
|
|
||||||
.topper-inline { display: flex; align-items: center; gap: 0.45rem; }
|
|
||||||
.select-xs { padding: 0.3rem 0.45rem; font-size: 0.85rem; height: 2.2rem; }
|
|
||||||
.nudge-pad { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; }
|
|
||||||
.topper-size-wrap { display: flex; flex-direction: column; gap: 0.1rem; min-width: 120px; }
|
|
||||||
.topper-size-wrap input[type=range] { accent-color: #2563eb; }
|
|
||||||
.dock-color-btn {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 4px solid #fff;
|
|
||||||
box-shadow: 0 8px 20px rgba(0,0,0,0.18);
|
|
||||||
transition: transform 0.15s ease;
|
|
||||||
}
|
|
||||||
.dock-color-btn:active { transform: scale(0.95); }
|
|
||||||
.dock-variant-btn {
|
|
||||||
min-width: 2.4rem;
|
|
||||||
height: 2.4rem;
|
|
||||||
padding: 0 .5rem;
|
|
||||||
border-radius: 0.65rem;
|
|
||||||
background: rgba(255,255,255,0.08);
|
|
||||||
color: #e2e8f0;
|
|
||||||
border: 1px solid rgba(255,255,255,0.15);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: .95rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
.dock-variant-btn.active {
|
|
||||||
background: #38bdf8;
|
|
||||||
color: #0f172a;
|
|
||||||
border-color: #38bdf8;
|
|
||||||
box-shadow: 0 6px 18px rgba(56,189,248,0.35);
|
|
||||||
}
|
|
||||||
.dock-variant-btn:active { transform: scale(0.95); }
|
|
||||||
.dock-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
background: rgba(255,255,255,0.04);
|
|
||||||
border: 1px solid rgba(255,255,255,0.12);
|
|
||||||
border-radius: 0.9rem;
|
|
||||||
padding: 0.4rem 0.65rem;
|
|
||||||
}
|
|
||||||
.dock-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
.dock-pill-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.dock-pill {
|
|
||||||
padding: 0.5rem 0.8rem;
|
|
||||||
border-radius: 0.8rem;
|
|
||||||
background: rgba(255,255,255,0.08);
|
|
||||||
color: #e2e8f0;
|
|
||||||
border: 1px solid rgba(255,255,255,0.14);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.dock-pill:hover { background: rgba(255,255,255,0.14); }
|
|
||||||
.dock-pill.active {
|
|
||||||
background: #38bdf8;
|
|
||||||
color: #0f172a;
|
|
||||||
border-color: #38bdf8;
|
|
||||||
box-shadow: 0 6px 18px rgba(56,189,248,0.35);
|
|
||||||
}
|
|
||||||
.dock-pill:active { transform: scale(0.96); }
|
|
||||||
.classic-quick-label { font-weight: 700; color: #334155; font-size: 0.9rem; }
|
|
||||||
.btn-nudge {
|
|
||||||
background: #0f172a;
|
|
||||||
color: #fff;
|
|
||||||
width: 2.4rem;
|
|
||||||
height: 2.4rem;
|
|
||||||
border-radius: 0.6rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 700;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.18);
|
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
.btn-nudge:active { transform: translateY(1px) scale(0.97); box-shadow: 0 2px 8px rgba(0,0,0,0.18); }
|
|
||||||
.length-dial {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0.35rem 0.25rem;
|
|
||||||
scroll-snap-type: x mandatory;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
position: relative;
|
|
||||||
mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
|
|
||||||
}
|
|
||||||
.length-dial .dock-pill {
|
|
||||||
scroll-snap-align: center;
|
|
||||||
min-width: 72px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: transform 0.15s ease, box-shadow 0.2s ease;
|
padding: .7rem .5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: .9rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||||
}
|
}
|
||||||
.length-dial .dock-pill.active {
|
.mobile-tabbar .mobile-tab-icon {
|
||||||
transform: scale(1.02);
|
font-size: 1.2rem;
|
||||||
box-shadow: 0 6px 18px rgba(37,99,235,0.28);
|
line-height: 1;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.length-dial .dock-pill.ping {
|
.mobile-tabbar .mobile-tab-btn[aria-pressed="true"] {
|
||||||
animation: tap-pulse 260ms ease;
|
background: linear-gradient(135deg, #2563eb, #0ea5e9);
|
||||||
|
border-color: rgba(37, 99, 235, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
@keyframes tap-pulse {
|
|
||||||
0% { transform: scale(0.95); }
|
|
||||||
50% { transform: scale(1.08); }
|
|
||||||
100% { transform: scale(1.0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Removed old mobile-tab-btn styles as they are replaced by the new layout */
|
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user