Make number toppers tint via SVG masks
189
classic.js
@ -78,8 +78,6 @@
|
|||||||
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 CLASSIC_STATE_KEY = 'classic:state:v1';
|
const CLASSIC_STATE_KEY = 'classic:state:v1';
|
||||||
const NUMBER_TINT_COLOR_KEY = 'classic:numberTintColor:v1';
|
|
||||||
const NUMBER_TINT_OPACITY_KEY = 'classic:numberTintOpacity:v1';
|
|
||||||
const MANUAL_MODE_KEY = 'classic:manualMode:v1';
|
const MANUAL_MODE_KEY = 'classic:manualMode:v1';
|
||||||
const MANUAL_OVERRIDES_KEY = 'classic:manualOverrides:v1';
|
const MANUAL_OVERRIDES_KEY = 'classic:manualOverrides:v1';
|
||||||
const MANUAL_EXPANDED_KEY = 'classic:manualExpanded:v1';
|
const MANUAL_EXPANDED_KEY = 'classic:manualExpanded:v1';
|
||||||
@ -103,7 +101,7 @@
|
|||||||
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', image: null },
|
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', image: null },
|
||||||
{ hex: '#fcd34d', image: null }
|
{ hex: '#fcd34d', image: null }
|
||||||
];
|
];
|
||||||
const defaultTopper = () => ({ hex: '#a18b67', image: 'images/chrome-gold.webp' });
|
const defaultTopper = () => ({ hex: '#E32636', image: 'images/chrome-gold.webp' }); // Classic gold
|
||||||
|
|
||||||
function getClassicColors() {
|
function getClassicColors() {
|
||||||
let arr = defaultColors();
|
let arr = defaultColors();
|
||||||
@ -144,30 +142,6 @@
|
|||||||
const clean = { hex: normHex(colorObj.hex), image: colorObj.image || null };
|
const clean = { hex: normHex(colorObj.hex), image: colorObj.image || null };
|
||||||
try { localStorage.setItem(TOPPER_COLOR_KEY, JSON.stringify(clean)); } catch {}
|
try { localStorage.setItem(TOPPER_COLOR_KEY, JSON.stringify(clean)); } catch {}
|
||||||
}
|
}
|
||||||
function getNumberTintColor() {
|
|
||||||
try {
|
|
||||||
const saved = JSON.parse(localStorage.getItem(NUMBER_TINT_COLOR_KEY));
|
|
||||||
if (saved && saved.hex) return normHex(saved.hex);
|
|
||||||
} catch {}
|
|
||||||
return '#ffffff';
|
|
||||||
}
|
|
||||||
function setNumberTintColor(hex) {
|
|
||||||
const clean = normHex(hex || '#ffffff');
|
|
||||||
try { localStorage.setItem(NUMBER_TINT_COLOR_KEY, JSON.stringify({ hex: clean })); } catch {}
|
|
||||||
return clean;
|
|
||||||
}
|
|
||||||
function getNumberTintOpacity() {
|
|
||||||
try {
|
|
||||||
const saved = parseFloat(localStorage.getItem(NUMBER_TINT_OPACITY_KEY));
|
|
||||||
if (!isNaN(saved)) return clamp01(saved);
|
|
||||||
} catch {}
|
|
||||||
return 1; // default to full tint so number color changes are obvious
|
|
||||||
}
|
|
||||||
function setNumberTintOpacity(v) {
|
|
||||||
const clamped = clamp01(parseFloat(v));
|
|
||||||
try { localStorage.setItem(NUMBER_TINT_OPACITY_KEY, String(clamped)); } catch {}
|
|
||||||
return clamped;
|
|
||||||
}
|
|
||||||
function loadManualMode() {
|
function loadManualMode() {
|
||||||
try {
|
try {
|
||||||
const saved = JSON.parse(localStorage.getItem(MANUAL_MODE_KEY));
|
const saved = JSON.parse(localStorage.getItem(MANUAL_MODE_KEY));
|
||||||
@ -307,8 +281,6 @@
|
|||||||
let topperOffsetX_Px = 0;
|
let topperOffsetX_Px = 0;
|
||||||
let topperOffsetY_Px = 0;
|
let topperOffsetY_Px = 0;
|
||||||
let topperSizeMultiplier = 1;
|
let topperSizeMultiplier = 1;
|
||||||
let numberTintHex = getNumberTintColor();
|
|
||||||
let numberTintOpacity = getNumberTintOpacity();
|
|
||||||
let shineEnabled = true;
|
let shineEnabled = true;
|
||||||
let borderEnabled = false;
|
let borderEnabled = false;
|
||||||
let manualMode = loadManualMode();
|
let manualMode = loadManualMode();
|
||||||
@ -332,8 +304,6 @@
|
|||||||
setTopperOffsetX(val) { topperOffsetX_Px = (Number(val) || 0) * 5; },
|
setTopperOffsetX(val) { topperOffsetX_Px = (Number(val) || 0) * 5; },
|
||||||
setTopperOffsetY(val) { topperOffsetY_Px = (Number(val) || 0) * -5; },
|
setTopperOffsetY(val) { topperOffsetY_Px = (Number(val) || 0) * -5; },
|
||||||
setTopperSize(multiplier) { topperSizeMultiplier = Number(multiplier) || 1; },
|
setTopperSize(multiplier) { topperSizeMultiplier = Number(multiplier) || 1; },
|
||||||
setNumberTintHex(hex) { numberTintHex = setNumberTintColor(hex); },
|
|
||||||
setNumberTintOpacity(val) { numberTintOpacity = setNumberTintOpacity(val); },
|
|
||||||
setShineEnabled(on) { shineEnabled = !!on; },
|
setShineEnabled(on) { shineEnabled = !!on; },
|
||||||
setBorderEnabled(on) { borderEnabled = !!on; },
|
setBorderEnabled(on) { borderEnabled = !!on; },
|
||||||
setManualMode(on) { manualMode = !!on; saveManualMode(manualMode); },
|
setManualMode(on) { manualMode = !!on; saveManualMode(manualMode); },
|
||||||
@ -402,24 +372,31 @@
|
|||||||
const isNumTopper = cell.isTopper && (model.topperType || '').startsWith('num-');
|
const isNumTopper = cell.isTopper && (model.topperType || '').startsWith('num-');
|
||||||
if (base.image) {
|
if (base.image) {
|
||||||
const w = base.width || 1, h = base.height || 1;
|
const w = base.width || 1, h = base.height || 1;
|
||||||
if (!isNumTopper) {
|
if (isNumTopper) {
|
||||||
kids.push(svg('image', { href: base.image, x: -w/2, y: -h/2, width: w, height: h, preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet', style: 'pointer-events:none' }));
|
// Use the SVG alpha as a mask and fill with the selected topper color.
|
||||||
}
|
|
||||||
const tintColor = model.numberTintHex || '#ffffff';
|
|
||||||
const tintOpacity = model.numberTintOpacity || 0;
|
|
||||||
if (tintOpacity > 0 && isNumTopper) {
|
|
||||||
const maskId = `mask-${id}`;
|
const maskId = `mask-${id}`;
|
||||||
kids.push(svg('mask', { id: maskId, maskUnits: 'userSpaceOnUse' }, [
|
kids.push(svg('mask', { id: maskId, maskUnits: 'userSpaceOnUse', maskContentUnits: 'userSpaceOnUse', 'mask-type': 'alpha' }, [
|
||||||
svg('image', { href: base.image, x: -w/2, y: -h/2, width: w, height: h, preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet', style: 'pointer-events:none' })
|
svg('image', { href: base.image, x: -w/2, y: -h/2, width: w, height: h, preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet', style: 'pointer-events:none' })
|
||||||
]));
|
]));
|
||||||
kids.push(svg('rect', {
|
kids.push(svg('rect', {
|
||||||
x: -w/2, y: -h/2, width: w, height: h,
|
x: -w/2, y: -h/2, width: w, height: h,
|
||||||
fill: tintColor, opacity: tintOpacity,
|
fill: model.topperColor?.hex || '#ffffff',
|
||||||
|
opacity: 1,
|
||||||
mask: `url(#${maskId})`,
|
mask: `url(#${maskId})`,
|
||||||
style: 'mix-blend-mode:multiply; pointer-events:none'
|
style: 'pointer-events:none'
|
||||||
}));
|
}));
|
||||||
// Also draw the image beneath with zero opacity to keep mask refs consistent
|
// Overlay the original SVG lightly to keep stroke/detail without overpowering the fill.
|
||||||
kids.push(svg('image', { href: base.image, x: -w/2, y: -h/2, width: w, height: h, preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet', style: 'pointer-events:none;opacity:0' }));
|
kids.push(svg('image', {
|
||||||
|
href: base.image,
|
||||||
|
x: -w/2,
|
||||||
|
y: -h/2,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet',
|
||||||
|
style: 'pointer-events:none;mix-blend-mode:multiply;opacity:0.4'
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
kids.push(svg('image', { href: base.image, x: -w/2, y: -h/2, width: w, height: h, preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet', style: 'pointer-events:none' }));
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(base.paths)) {
|
} else if (Array.isArray(base.paths)) {
|
||||||
base.paths.forEach(p => {
|
base.paths.forEach(p => {
|
||||||
@ -506,7 +483,12 @@ function distinctPaletteSlots(palette) {
|
|||||||
return slots.slice(0, limit);
|
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, 4, 3],
|
||||||
|
[4, 1, 3, 2],
|
||||||
|
[3, 4, 2, 1],
|
||||||
|
[2, 3, 1, 4],
|
||||||
|
];
|
||||||
const colorBlock5 =
|
const colorBlock5 =
|
||||||
[
|
[
|
||||||
[5, 2, 3, 4, 1],
|
[5, 2, 3, 4, 1],
|
||||||
@ -808,8 +790,6 @@ function distinctPaletteSlots(palette) {
|
|||||||
topperColor: getTopperColor(),
|
topperColor: getTopperColor(),
|
||||||
topperType,
|
topperType,
|
||||||
shineEnabled,
|
shineEnabled,
|
||||||
numberTintHex,
|
|
||||||
numberTintOpacity,
|
|
||||||
manualMode,
|
manualMode,
|
||||||
manualFocusEnabled,
|
manualFocusEnabled,
|
||||||
manualFloatingQuad,
|
manualFloatingQuad,
|
||||||
@ -893,12 +873,13 @@ function distinctPaletteSlots(palette) {
|
|||||||
const shapes = {};
|
const shapes = {};
|
||||||
const topperSize = 9.5; // ≈34" foil height when base balloons are ~11"
|
const topperSize = 9.5; // ≈34" foil height when base balloons are ~11"
|
||||||
Object.keys({ ...fallbackPaths, ...NUMBER_IMAGE_MAP }).forEach(num => {
|
Object.keys({ ...fallbackPaths, ...NUMBER_IMAGE_MAP }).forEach(num => {
|
||||||
|
const fallback = fallbackPaths[num];
|
||||||
const img = NUMBER_IMAGE_MAP[num];
|
const img = NUMBER_IMAGE_MAP[num];
|
||||||
const hasImage = !!img;
|
|
||||||
shapes[`topper-num-${num}`] = {
|
shapes[`topper-num-${num}`] = {
|
||||||
base: hasImage
|
// Prefer provided SVG image for numbers so we keep the photo look.
|
||||||
? { type: 'image', image: img, width: 1, height: 1, radius: 0.9, allowShine: false, transform: 'scale(0.9)' }
|
base: img
|
||||||
: { type: 'path', paths: [{ d: fallbackPaths[num].d, fillRule: fallbackPaths[num].fillRule || 'nonzero' }], radius: r, allowShine: true, transform: baseTransform },
|
? { type: 'image', image: img, width: 1, height: 1, radius: 0.9, allowShine: false, preserveAspectRatio: 'xMidYMid meet', transform: 'scale(0.9)' }
|
||||||
|
: { type: 'path', paths: [{ d: fallback.d, fillRule: fallback.fillRule || 'nonzero' }], radius: r, allowShine: true, transform: baseTransform },
|
||||||
size: topperSize
|
size: topperSize
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -1017,6 +998,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
function initClassicColorPicker(onColorChange) {
|
function initClassicColorPicker(onColorChange) {
|
||||||
const slotsContainer = document.getElementById('classic-slots'), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors'), addSlotBtn = document.getElementById('classic-add-slot'), activeChip = document.getElementById('classic-active-chip'), floatingChip = document.getElementById('classic-active-chip-floating'), activeDot = document.getElementById('classic-active-dot'), floatingDot = document.getElementById('classic-active-dot-floating');
|
const slotsContainer = document.getElementById('classic-slots'), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors'), addSlotBtn = document.getElementById('classic-add-slot'), activeChip = document.getElementById('classic-active-chip'), floatingChip = document.getElementById('classic-active-chip-floating'), activeDot = document.getElementById('classic-active-dot'), floatingDot = document.getElementById('classic-active-dot-floating');
|
||||||
|
const projectBlock = document.getElementById('classic-project-block');
|
||||||
|
const replaceBlock = document.getElementById('classic-replace-block');
|
||||||
const replaceFromSel = document.getElementById('classic-replace-from');
|
const replaceFromSel = document.getElementById('classic-replace-from');
|
||||||
const replaceToSel = document.getElementById('classic-replace-to');
|
const replaceToSel = document.getElementById('classic-replace-to');
|
||||||
const replaceBtn = document.getElementById('classic-replace-btn');
|
const replaceBtn = document.getElementById('classic-replace-btn');
|
||||||
@ -1024,9 +1007,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
const replaceFromChip = document.getElementById('classic-replace-from-chip');
|
const replaceFromChip = document.getElementById('classic-replace-from-chip');
|
||||||
const replaceToChip = document.getElementById('classic-replace-to-chip');
|
const replaceToChip = document.getElementById('classic-replace-to-chip');
|
||||||
const replaceCountLabel = document.getElementById('classic-replace-count');
|
const replaceCountLabel = document.getElementById('classic-replace-count');
|
||||||
const numberTintSlider = document.getElementById('classic-number-tint');
|
|
||||||
const topperBlock = document.getElementById('classic-topper-color-block');
|
const topperBlock = document.getElementById('classic-topper-color-block');
|
||||||
if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
|
if (!slotsContainer || !topperSwatch || !swatchGrid) return;
|
||||||
topperSwatch.classList.add('tab-btn');
|
topperSwatch.classList.add('tab-btn');
|
||||||
let classicColors = getClassicColors(), activeTarget = '1', slotCount = getStoredSlotCount();
|
let classicColors = getClassicColors(), activeTarget = '1', slotCount = getStoredSlotCount();
|
||||||
const publishActiveTarget = () => {
|
const publishActiveTarget = () => {
|
||||||
@ -1090,6 +1072,13 @@ function distinctPaletteSlots(palette) {
|
|||||||
renderSlots();
|
renderSlots();
|
||||||
}
|
}
|
||||||
const allPaletteColors = flattenPalette();
|
const allPaletteColors = flattenPalette();
|
||||||
|
const labelForColor = (color) => {
|
||||||
|
const hex = normHex(color?.hex || color?.colour || '');
|
||||||
|
const image = color?.image || '';
|
||||||
|
const byImage = image ? allPaletteColors.find(c => c.image === image) : null;
|
||||||
|
const byHex = hex ? allPaletteColors.find(c => c.hex === hex) : null;
|
||||||
|
return color?.name || byImage?.name || byHex?.name || hex || (image ? 'Texture' : 'Current');
|
||||||
|
};
|
||||||
|
|
||||||
const colorKeyFromVal = (val) => {
|
const colorKeyFromVal = (val) => {
|
||||||
const palette = buildClassicPalette();
|
const palette = buildClassicPalette();
|
||||||
@ -1292,21 +1281,12 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
const topperColor = getTopperColor();
|
const topperColor = getTopperColor();
|
||||||
const currentType = document.querySelector('.topper-type-btn[aria-pressed="true"]')?.dataset.type || 'round';
|
const currentType = document.querySelector('.topper-type-btn[aria-pressed="true"]')?.dataset.type || 'round';
|
||||||
const tintColor = getNumberTintColor();
|
|
||||||
if (currentType.startsWith('num-') && topperColor.image) {
|
|
||||||
topperSwatch.style.backgroundImage = `linear-gradient(${tintColor}99, ${tintColor}99), url("${topperColor.image}")`;
|
|
||||||
topperSwatch.style.backgroundBlendMode = 'multiply, normal';
|
|
||||||
topperSwatch.style.backgroundSize = '220%';
|
|
||||||
topperSwatch.style.backgroundPosition = 'center';
|
|
||||||
topperSwatch.style.backgroundColor = tintColor;
|
|
||||||
} else {
|
|
||||||
topperSwatch.style.backgroundImage = topperColor.image ? `url("${topperColor.image}")` : 'none';
|
topperSwatch.style.backgroundImage = topperColor.image ? `url("${topperColor.image}")` : 'none';
|
||||||
topperSwatch.style.backgroundBlendMode = 'normal';
|
topperSwatch.style.backgroundBlendMode = 'normal';
|
||||||
topperSwatch.style.backgroundColor = topperColor.hex;
|
topperSwatch.style.backgroundColor = topperColor.hex;
|
||||||
topperSwatch.style.backgroundSize = '200%';
|
topperSwatch.style.backgroundSize = '200%';
|
||||||
topperSwatch.style.backgroundPosition = 'center';
|
topperSwatch.style.backgroundPosition = 'center';
|
||||||
}
|
const topperTxt = textStyleForColor({ hex: topperColor.hex, image: topperColor.image });
|
||||||
const topperTxt = textStyleForColor({ hex: tintColor || topperColor.hex, image: topperColor.image });
|
|
||||||
topperSwatch.style.color = topperTxt.color;
|
topperSwatch.style.color = topperTxt.color;
|
||||||
topperSwatch.style.textShadow = topperTxt.shadow;
|
topperSwatch.style.textShadow = topperTxt.shadow;
|
||||||
const patName = (document.getElementById('classic-pattern')?.value || '').toLowerCase();
|
const patName = (document.getElementById('classic-pattern')?.value || '').toLowerCase();
|
||||||
@ -1326,15 +1306,24 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
const manualModeOn = isManual();
|
const manualModeOn = isManual();
|
||||||
const sharedActive = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
const sharedActive = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
||||||
activeLabel.textContent = manualModeOn ? 'Manual paint color' : (activeTarget === 'T' ? 'Topper' : `Slot #${activeTarget}`);
|
|
||||||
if (activeChip) {
|
if (activeChip) {
|
||||||
const idx = parseInt(activeTarget, 10) - 1;
|
const idx = parseInt(activeTarget, 10) - 1;
|
||||||
const color = manualModeOn ? sharedActive : (classicColors[idx] || { hex: '#ffffff', image: null });
|
const color = manualModeOn ? sharedActive : (classicColors[idx] || { hex: '#ffffff', image: null });
|
||||||
activeChip.setAttribute('title', manualModeOn ? 'Active color' : `Slot #${activeTarget}`);
|
activeChip.setAttribute('title', manualModeOn ? 'Active color' : `Slot #${activeTarget}`);
|
||||||
const bgImg = color.image ? `url("${color.image}")` : 'none';
|
const bgImg = color.image ? `url(\"${color.image}\")` : 'none';
|
||||||
const bgCol = color.hex || '#ffffff';
|
const bgCol = color.hex || '#ffffff';
|
||||||
activeChip.style.backgroundImage = bgImg;
|
activeChip.style.backgroundImage = bgImg;
|
||||||
activeChip.style.backgroundColor = bgCol;
|
activeChip.style.backgroundColor = bgCol;
|
||||||
|
activeChip.style.backgroundSize = color.image ? '200%' : 'cover';
|
||||||
|
activeChip.style.backgroundPosition = 'center';
|
||||||
|
const txt = textStyleForColor({ hex: color.hex || '#ffffff', image: color.image });
|
||||||
|
activeChip.style.color = txt.color;
|
||||||
|
activeChip.style.textShadow = txt.shadow;
|
||||||
|
if (activeLabel) {
|
||||||
|
activeLabel.textContent = labelForColor(color);
|
||||||
|
activeLabel.style.color = txt.color;
|
||||||
|
activeLabel.style.textShadow = txt.shadow;
|
||||||
|
}
|
||||||
if (activeDot) {
|
if (activeDot) {
|
||||||
activeDot.style.backgroundImage = bgImg;
|
activeDot.style.backgroundImage = bgImg;
|
||||||
activeDot.style.backgroundColor = bgCol;
|
activeDot.style.backgroundColor = bgCol;
|
||||||
@ -1357,9 +1346,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (activeChip) {
|
if (activeChip) {
|
||||||
activeChip.style.display = manualModeOn ? '' : 'none';
|
activeChip.style.display = manualModeOn ? '' : 'none';
|
||||||
}
|
}
|
||||||
if (projectPaletteBox) {
|
if (projectBlock) projectBlock.classList.toggle('hidden', !manualModeOn);
|
||||||
projectPaletteBox.parentElement?.classList.toggle('hidden', !manualModeOn);
|
if (replaceBlock) replaceBlock.classList.toggle('hidden', !manualModeOn);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
swatchGrid.innerHTML = '';
|
swatchGrid.innerHTML = '';
|
||||||
@ -1385,10 +1373,11 @@ function distinctPaletteSlots(palette) {
|
|||||||
const meta = item.meta || {};
|
const meta = item.meta || {};
|
||||||
const selectedColor = { hex: meta.hex || item.hex, image: meta.image || null };
|
const selectedColor = { hex: meta.hex || item.hex, image: meta.image || null };
|
||||||
if (activeTarget === 'T') {
|
if (activeTarget === 'T') {
|
||||||
if (currentType.startsWith('num-')) {
|
const currentTypeLocal = document.querySelector('.topper-type-btn[aria-pressed="true"]')?.dataset.type || 'round';
|
||||||
setNumberTintColor(selectedColor.hex);
|
if (currentTypeLocal.startsWith('num-')) {
|
||||||
setNumberTintOpacity(1);
|
const num = currentTypeLocal.split('-')[1];
|
||||||
if (numberTintSlider) numberTintSlider.value = 1;
|
const imgPath = num ? NUMBER_IMAGE_MAP[num] : null;
|
||||||
|
setTopperColor({ hex: selectedColor.hex, image: imgPath || selectedColor.image || null });
|
||||||
} else {
|
} else {
|
||||||
setTopperColor(selectedColor);
|
setTopperColor(selectedColor);
|
||||||
}
|
}
|
||||||
@ -1494,7 +1483,6 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
||||||
projectPaletteBox = null;
|
projectPaletteBox = null;
|
||||||
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display');
|
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display');
|
||||||
const numberTintRow = document.getElementById('classic-number-tint-row'), numberTintSlider = document.getElementById('classic-number-tint');
|
|
||||||
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
||||||
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
||||||
const toolbar = document.getElementById('classic-canvas-toolbar');
|
const toolbar = document.getElementById('classic-canvas-toolbar');
|
||||||
@ -1513,6 +1501,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
const patternLayoutBtns = Array.from(document.querySelectorAll('[data-pattern-layout]'));
|
const patternLayoutBtns = Array.from(document.querySelectorAll('[data-pattern-layout]'));
|
||||||
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
||||||
const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
|
const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
|
||||||
|
const topperSizeInput = document.getElementById('classic-topper-size');
|
||||||
const slotsContainer = document.getElementById('classic-slots');
|
const slotsContainer = document.getElementById('classic-slots');
|
||||||
projectPaletteBox = document.getElementById('classic-project-palette');
|
projectPaletteBox = document.getElementById('classic-project-palette');
|
||||||
const manualPaletteBtn = document.getElementById('classic-manual-palette');
|
const manualPaletteBtn = document.getElementById('classic-manual-palette');
|
||||||
@ -1568,7 +1557,6 @@ function distinctPaletteSlots(palette) {
|
|||||||
};
|
};
|
||||||
// Force UI to reflect initial manual state
|
// Force UI to reflect initial manual state
|
||||||
if (manualModeState) patternLayout = 'manual';
|
if (manualModeState) patternLayout = 'manual';
|
||||||
if (numberTintSlider) numberTintSlider.value = getNumberTintOpacity();
|
|
||||||
const topperPresets = {
|
const topperPresets = {
|
||||||
'Column 4:heart': { enabled: true, offsetX: 3, offsetY: -10.5, size: 1.05 },
|
'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:star': { enabled: true, offsetX: 3, offsetY: -7.5, size: 1.65 },
|
||||||
@ -1581,6 +1569,16 @@ function distinctPaletteSlots(palette) {
|
|||||||
};
|
};
|
||||||
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);
|
||||||
|
if (topperSizeInput) {
|
||||||
|
const val = Math.max(0.5, Math.min(2, parseFloat(topperSizeInput.value) || 1));
|
||||||
|
GC.setTopperSize(val);
|
||||||
|
topperSizeInput.addEventListener('input', () => {
|
||||||
|
const next = Math.max(0.5, Math.min(2, parseFloat(topperSizeInput.value) || 1));
|
||||||
|
GC.setTopperSize(next);
|
||||||
|
updateClassicDesign();
|
||||||
|
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||||
|
});
|
||||||
|
}
|
||||||
let refreshClassicPaletteUi = null;
|
let refreshClassicPaletteUi = null;
|
||||||
|
|
||||||
const getTopperType = () => topperTypeButtons.find(btn => btn.getAttribute('aria-pressed') === 'true')?.dataset.type || 'round';
|
const getTopperType = () => topperTypeButtons.find(btn => btn.getAttribute('aria-pressed') === 'true')?.dataset.type || 'round';
|
||||||
@ -1593,20 +1591,19 @@ function distinctPaletteSlots(palette) {
|
|||||||
});
|
});
|
||||||
window.ClassicDesigner.lastTopperType = type;
|
window.ClassicDesigner.lastTopperType = type;
|
||||||
};
|
};
|
||||||
function applyNumberTopperTexture(type) {
|
function ensureNumberTopperImage(type) {
|
||||||
if (!type || !type.startsWith('num-')) return;
|
if (!type || !type.startsWith('num-')) return;
|
||||||
const num = type.split('-')[1];
|
const num = type.split('-')[1];
|
||||||
if (!num) return;
|
const imgPath = num ? NUMBER_IMAGE_MAP[num] : null;
|
||||||
const imgPath = NUMBER_IMAGE_MAP[num];
|
if (!imgPath) return;
|
||||||
if (imgPath) setTopperColor({ hex: '#ffffff', image: imgPath });
|
const cur = getTopperColor();
|
||||||
else setTopperColor({ hex: '#d4d4d8', image: null }); // fallback silver fill if image missing
|
if (cur?.image === imgPath) return;
|
||||||
|
setTopperColor({ hex: cur?.hex || '#ffffff', image: imgPath });
|
||||||
refreshClassicPaletteUi?.();
|
refreshClassicPaletteUi?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetNonNumberTopperColor(type) {
|
function resetNonNumberTopperColor(type) {
|
||||||
if (type && type.startsWith('num-')) return;
|
if (type && type.startsWith('num-')) return;
|
||||||
const fallback = getTopperColor();
|
const fallback = getTopperColor();
|
||||||
// If last topper type was a number, strip image to avoid leaking photo texture.
|
|
||||||
if (fallback?.image && Object.values(NUMBER_IMAGE_MAP).includes(fallback.image)) {
|
if (fallback?.image && Object.values(NUMBER_IMAGE_MAP).includes(fallback.image)) {
|
||||||
setTopperColor({ hex: fallback.hex || '#ffffff', image: null });
|
setTopperColor({ hex: fallback.hex || '#ffffff', image: null });
|
||||||
refreshClassicPaletteUi?.();
|
refreshClassicPaletteUi?.();
|
||||||
@ -1621,10 +1618,10 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (lastPresetKey === key || lastPresetKey === 'custom') return;
|
if (lastPresetKey === key || lastPresetKey === 'custom') return;
|
||||||
topperOffsetX = preset.offsetX;
|
topperOffsetX = preset.offsetX;
|
||||||
topperOffsetY = preset.offsetY;
|
topperOffsetY = preset.offsetY;
|
||||||
if (topperSizeInp) topperSizeInp.value = preset.size;
|
if (topperSizeInput) topperSizeInput.value = preset.size;
|
||||||
if (topperEnabledCb) topperEnabledCb.checked = preset.enabled;
|
if (topperEnabledCb) topperEnabledCb.checked = preset.enabled;
|
||||||
setTopperType(type);
|
setTopperType(type);
|
||||||
applyNumberTopperTexture(type);
|
ensureNumberTopperImage(type);
|
||||||
resetNonNumberTopperColor(type);
|
resetNonNumberTopperColor(type);
|
||||||
lastPresetKey = key;
|
lastPresetKey = key;
|
||||||
}
|
}
|
||||||
@ -1689,8 +1686,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
topperType: getTopperType(),
|
topperType: getTopperType(),
|
||||||
topperOffsetX,
|
topperOffsetX,
|
||||||
topperOffsetY,
|
topperOffsetY,
|
||||||
topperSize: topperSizeInp?.value || '',
|
topperSize: topperSizeInput?.value || ''
|
||||||
numberTint: numberTintSlider ? numberTintSlider.value : getNumberTintOpacity()
|
|
||||||
};
|
};
|
||||||
saveClassicState(state);
|
saveClassicState(state);
|
||||||
}
|
}
|
||||||
@ -1704,12 +1700,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
if (topperEnabledCb) topperEnabledCb.checked = !!saved.topperEnabled;
|
if (topperEnabledCb) topperEnabledCb.checked = !!saved.topperEnabled;
|
||||||
if (typeof saved.topperOffsetX === 'number') topperOffsetX = saved.topperOffsetX;
|
if (typeof saved.topperOffsetX === 'number') topperOffsetX = saved.topperOffsetX;
|
||||||
if (typeof saved.topperOffsetY === 'number') topperOffsetY = saved.topperOffsetY;
|
if (typeof saved.topperOffsetY === 'number') topperOffsetY = saved.topperOffsetY;
|
||||||
if (topperSizeInp && saved.topperSize) topperSizeInp.value = saved.topperSize;
|
if (topperSizeInput && saved.topperSize) topperSizeInput.value = saved.topperSize;
|
||||||
if (saved.topperType) setTopperType(saved.topperType);
|
if (saved.topperType) setTopperType(saved.topperType);
|
||||||
if (numberTintSlider && typeof saved.numberTint !== 'undefined') {
|
|
||||||
numberTintSlider.value = saved.numberTint;
|
|
||||||
setNumberTintOpacity(saved.numberTint);
|
|
||||||
}
|
|
||||||
syncPatternStateFromSelect();
|
syncPatternStateFromSelect();
|
||||||
lastPresetKey = 'custom';
|
lastPresetKey = 'custom';
|
||||||
}
|
}
|
||||||
@ -1988,7 +1980,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
const isNumberTopper = getTopperType().startsWith('num-');
|
const isNumberTopper = getTopperType().startsWith('num-');
|
||||||
|
|
||||||
topperControls.classList.toggle('hidden', !showTopper);
|
topperControls.classList.toggle('hidden', !showTopper);
|
||||||
if (numberTintRow) numberTintRow.classList.toggle('hidden', !(showTopper && isNumberTopper));
|
// Number tint controls removed; always use base SVG appearance for numbers.
|
||||||
if (nudgeOpenBtn) nudgeOpenBtn.classList.toggle('hidden', !showTopper);
|
if (nudgeOpenBtn) nudgeOpenBtn.classList.toggle('hidden', !showTopper);
|
||||||
const showReverse = patternLayout === 'spiral' && !manualOn;
|
const showReverse = patternLayout === 'spiral' && !manualOn;
|
||||||
if (reverseLabel) reverseLabel.classList.toggle('hidden', !showReverse);
|
if (reverseLabel) reverseLabel.classList.toggle('hidden', !showReverse);
|
||||||
@ -2003,12 +1995,9 @@ function distinctPaletteSlots(palette) {
|
|||||||
GC.setManualMode(manualOn);
|
GC.setManualMode(manualOn);
|
||||||
GC.setReverse(!!reverseCb?.checked);
|
GC.setReverse(!!reverseCb?.checked);
|
||||||
GC.setTopperType(getTopperType());
|
GC.setTopperType(getTopperType());
|
||||||
GC.setNumberTintHex(getNumberTintColor());
|
|
||||||
GC.setNumberTintOpacity(numberTintSlider ? numberTintSlider.value : getNumberTintOpacity());
|
|
||||||
applyNumberTopperTexture(getTopperType());
|
|
||||||
GC.setTopperOffsetX(topperOffsetX);
|
GC.setTopperOffsetX(topperOffsetX);
|
||||||
GC.setTopperOffsetY(topperOffsetY);
|
GC.setTopperOffsetY(topperOffsetY);
|
||||||
GC.setTopperSize(topperSizeInp?.value);
|
GC.setTopperSize(topperSizeInput?.value);
|
||||||
GC.setShineEnabled(!!shineEnabledCb?.checked);
|
GC.setShineEnabled(!!shineEnabledCb?.checked);
|
||||||
GC.setBorderEnabled(!!borderEnabledCb?.checked);
|
GC.setBorderEnabled(!!borderEnabledCb?.checked);
|
||||||
const expandedOn = manualOn && manualExpandedState;
|
const expandedOn = manualOn && manualExpandedState;
|
||||||
@ -2341,15 +2330,15 @@ function distinctPaletteSlots(palette) {
|
|||||||
}));
|
}));
|
||||||
topperTypeButtons.forEach(btn => btn.addEventListener('click', () => {
|
topperTypeButtons.forEach(btn => btn.addEventListener('click', () => {
|
||||||
setTopperType(btn.dataset.type);
|
setTopperType(btn.dataset.type);
|
||||||
applyNumberTopperTexture(btn.dataset.type);
|
ensureNumberTopperImage(btn.dataset.type);
|
||||||
resetNonNumberTopperColor(btn.dataset.type);
|
resetNonNumberTopperColor(btn.dataset.type);
|
||||||
|
if (topperEnabledCb) {
|
||||||
|
topperEnabledCb.checked = true;
|
||||||
|
GC.setTopperEnabled(true);
|
||||||
|
}
|
||||||
lastPresetKey = null;
|
lastPresetKey = null;
|
||||||
updateClassicDesign();
|
updateClassicDesign();
|
||||||
}));
|
}));
|
||||||
numberTintSlider?.addEventListener('input', () => {
|
|
||||||
GC.setNumberTintOpacity(numberTintSlider.value);
|
|
||||||
updateClassicDesign();
|
|
||||||
});
|
|
||||||
nudgeOpenBtn?.addEventListener('click', (e) => {
|
nudgeOpenBtn?.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.__showFloatingNudge?.();
|
window.__showFloatingNudge?.();
|
||||||
@ -2373,8 +2362,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener('fullscreenchange', updateFullscreenLabel);
|
document.addEventListener('fullscreenchange', updateFullscreenLabel);
|
||||||
[lengthInp, reverseCb, topperEnabledCb, topperSizeInp]
|
[lengthInp, reverseCb, topperEnabledCb, topperSizeInput]
|
||||||
.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(); }); });
|
.forEach(el => { if (!el) return; const eventType = (el.type === 'range' || el.type === 'number') ? 'input' : 'change'; el.addEventListener(eventType, () => { if (el === topperSizeInput || el === topperEnabledCb) lastPresetKey = 'custom'; updateClassicDesign(); }); });
|
||||||
topperEnabledCb?.addEventListener('change', updateClassicDesign);
|
topperEnabledCb?.addEventListener('change', updateClassicDesign);
|
||||||
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); });
|
||||||
borderEnabledCb?.addEventListener('change', (e) => {
|
borderEnabledCb?.addEventListener('change', (e) => {
|
||||||
|
|||||||
131
index.html
@ -37,7 +37,7 @@
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<nav id="mode-tabs" class="flex gap-2">
|
<nav id="mode-tabs" class="flex gap-2">
|
||||||
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
||||||
<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-idle" data-target="#tab-classic" aria-pressed="false">Classic</button>
|
||||||
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
|
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@ -155,29 +155,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
<div class="panel-heading">Project Palette</div>
|
<div class="panel-heading">Organic Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card space-y-4">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="space-y-2">
|
||||||
<span class="text-sm text-gray-600">Built from the current design. Click a swatch to select that color.</span>
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-sm font-medium text-gray-700">Active color</span>
|
||||||
|
<div id="current-color-chip" class="current-color-chip">
|
||||||
|
<span id="current-color-label" class="text-xs font-semibold text-slate-700"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-semibold text-gray-700">Project Palette</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="space-y-2">
|
||||||
<div class="panel-card">
|
<div class="text-sm font-semibold text-gray-700">Replace Color</div>
|
||||||
<p class="hint mb-2">Tap or click on canvas to sample a balloon’s color (use the eyedropper).</p>
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<span class="text-sm font-medium text-gray-700">Active Color</span>
|
|
||||||
<div id="current-color-chip" class="current-color-chip">
|
|
||||||
<span id="current-color-label" class="text-xs font-semibold text-slate-700"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="color-palette" class="palette-box"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-heading mt-4">Replace Color</div>
|
|
||||||
<div class="panel-card space-y-3">
|
|
||||||
<div class="flex items-center gap-2 replace-row">
|
<div class="flex items-center gap-2 replace-row">
|
||||||
<button type="button" class="replace-chip" id="replace-from-chip" aria-label="Pick color to replace"></button>
|
<button type="button" class="replace-chip" id="replace-from-chip" aria-label="Pick color to replace"></button>
|
||||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||||
@ -185,13 +183,18 @@
|
|||||||
<span id="replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
<span id="replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-2">
|
<div class="grid grid-cols-1 gap-2">
|
||||||
<p class="hint text-xs">Tap a chip to choose colors. “From” shows only colors used on canvas.</p>
|
|
||||||
<select id="replace-from" class="sr-only"></select>
|
<select id="replace-from" class="sr-only"></select>
|
||||||
<select id="replace-to" class="sr-only"></select>
|
<select id="replace-to" class="sr-only"></select>
|
||||||
<button id="replace-btn" class="btn-blue">Replace</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>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-sm font-semibold text-gray-700">Color Library</div>
|
||||||
|
<div id="color-palette" class="palette-box"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="save">
|
<div class="control-stack" data-mobile-tab="save">
|
||||||
@ -289,7 +292,7 @@
|
|||||||
<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">
|
||||||
<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="classic-topper-enabled" type="checkbox" class="align-middle">
|
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
|
||||||
Add Topper (24")
|
Add Topper
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -317,18 +320,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-2">
|
|
||||||
<div class="text-sm font-medium mb-1">Topper Size</div>
|
|
||||||
<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 id="classic-number-tint-row" class="sm:col-span-2 hidden number-tint-row">
|
|
||||||
<div class="number-tint-header">
|
|
||||||
<div class="text-sm font-semibold text-gray-700">Number Tint</div>
|
|
||||||
<span class="text-xs text-gray-500">Soft overlay for photo digits</span>
|
|
||||||
</div>
|
|
||||||
<input id="classic-number-tint" type="range" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
|
||||||
<p class="hint">Pick a color in Classic Colors, select Topper (T), then adjust strength.</p>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-2 flex flex-wrap gap-2 justify-end">
|
<div class="sm:col-span-2 flex flex-wrap gap-2 justify-end">
|
||||||
<button type="button" id="classic-nudge-open" class="btn-dark text-xs px-3 py-2">Nudge Panel</button>
|
<button type="button" id="classic-nudge-open" class="btn-dark text-xs px-3 py-2">Nudge Panel</button>
|
||||||
</div>
|
</div>
|
||||||
@ -364,14 +355,24 @@
|
|||||||
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="text-sm font-semibold text-gray-700">Active color:</span>
|
<span class="text-sm text-gray-700 classic-label">Active color</span>
|
||||||
<button id="classic-active-chip" type="button" class="slot-swatch border border-gray-300" title="Tap to scroll to palette">
|
<div id="classic-active-chip" class="current-color-chip cursor-pointer" title="Tap to scroll to palette">
|
||||||
<span class="color-dot" id="classic-active-dot"></span>
|
<span id="classic-active-label" class="text-xs font-semibold text-slate-700"></span>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-heading mt-2">Project Palette</div>
|
</div>
|
||||||
|
<div id="classic-topper-color-block" class="mb-3 hidden">
|
||||||
|
<div class="text-sm text-gray-700 classic-label">Topper Color</div>
|
||||||
|
<div class="flex items-center gap-3 mt-1">
|
||||||
|
<button id="classic-topper-color-swatch" class="slot-swatch" title="Click to change topper color">T</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="classic-project-block" class="space-y-1">
|
||||||
|
<div class="text-sm text-gray-700 classic-label">Project Palette</div>
|
||||||
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
|
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
|
||||||
<div class="panel-heading mt-4">Replace Color (Manual)</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="classic-replace-block" class="mt-2">
|
||||||
|
<div class="text-sm text-gray-700 classic-label">Replace Color</div>
|
||||||
<div class="panel-card space-y-3">
|
<div class="panel-card space-y-3">
|
||||||
<div class="flex items-center gap-2 replace-row">
|
<div class="flex items-center gap-2 replace-row">
|
||||||
<button type="button" class="replace-chip" id="classic-replace-from-chip" aria-label="Pick color to replace"></button>
|
<button type="button" class="replace-chip" id="classic-replace-from-chip" aria-label="Pick color to replace"></button>
|
||||||
@ -380,25 +381,17 @@
|
|||||||
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-2">
|
<div class="grid grid-cols-1 gap-2">
|
||||||
<p class="hint text-xs">Manual paint only. “From” lists colors already used on canvas; “To” comes from the Classic library.</p>
|
|
||||||
<select id="classic-replace-from" class="sr-only"></select>
|
<select id="classic-replace-from" class="sr-only"></select>
|
||||||
<select id="classic-replace-to" class="sr-only"></select>
|
<select id="classic-replace-to" class="sr-only"></select>
|
||||||
<button id="classic-replace-btn" class="btn-blue">Replace</button>
|
<button id="classic-replace-btn" class="btn-blue">Replace</button>
|
||||||
<p id="classic-replace-msg" class="hint"></p>
|
<p id="classic-replace-msg" class="hint"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
|
</div>
|
||||||
<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</button>
|
<button id="classic-randomize-colors" class="btn-dark">Randomize</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="classic-topper-color-block" class="mt-3 hidden">
|
|
||||||
<div class="panel-heading">Topper Color</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
@ -453,10 +446,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
||||||
<div class="floating-nudge-header">
|
<div class="floating-nudge-header">
|
||||||
<div class="panel-heading">Nudge Topper</div>
|
<div class="panel-heading">Nudge & Size Topper</div>
|
||||||
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Close nudge panel">×</button>
|
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Close nudge panel">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="floating-nudge-body">
|
<div class="floating-nudge-body space-y-3">
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div></div>
|
<div></div>
|
||||||
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up">↑</button>
|
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up">↑</button>
|
||||||
@ -468,6 +461,10 @@
|
|||||||
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down">↓</button>
|
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down">↓</button>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium mb-1">Topper Size</div>
|
||||||
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -520,30 +517,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
<div class="panel-heading mt-4">Active Color</div>
|
<div class="panel-heading">Wall Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-sm font-medium text-gray-700">Current</span>
|
<span class="text-sm font-medium text-gray-700">Active color</span>
|
||||||
<div id="wall-active-color-chip" class="current-color-chip">
|
<div id="wall-active-color-chip" class="current-color-chip">
|
||||||
<span id="wall-active-color-label" class="text-[10px] font-semibold text-slate-700"></span>
|
<span id="wall-active-color-label" class="text-xs font-semibold text-slate-700"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint mt-2">Tap a swatch to set. Tap a balloon to paint; tap again (same color) to clear. Use the eyedropper to pick from the canvas.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-heading mt-4">Used Colors</div>
|
|
||||||
<div class="panel-card">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-xs text-gray-600">Click to pick. Remove unused clears empty/transparent entries.</div>
|
<span class="text-sm font-semibold text-gray-700">Project Palette</span>
|
||||||
<button type="button" id="wall-remove-unused" class="btn-yellow text-xs px-2 py-1">Remove Unused</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
|
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-heading mt-4">Wall Palette</div>
|
|
||||||
<div class="panel-card">
|
<div class="space-y-2">
|
||||||
<div id="wall-palette" class="palette-box min-h-[3rem]"></div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-heading mt-4">Replace Colors</div>
|
|
||||||
<div class="panel-card space-y-3">
|
|
||||||
<div class="flex items-center gap-2 replace-row">
|
<div class="flex items-center gap-2 replace-row">
|
||||||
<button type="button" class="replace-chip" id="wall-replace-from-chip" aria-label="Pick wall color to replace"></button>
|
<button type="button" class="replace-chip" id="wall-replace-from-chip" aria-label="Pick wall color to replace"></button>
|
||||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||||
@ -551,13 +543,18 @@
|
|||||||
<span id="wall-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
<span id="wall-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-2">
|
<div class="grid grid-cols-1 gap-2">
|
||||||
<p class="hint text-xs">Tap a chip to choose colors. “Replace” shows only colors used in this wall.</p>
|
|
||||||
<select id="wall-replace-from" class="sr-only"></select>
|
<select id="wall-replace-from" class="sr-only"></select>
|
||||||
<select id="wall-replace-to" class="sr-only"></select>
|
<select id="wall-replace-to" class="sr-only"></select>
|
||||||
<button type="button" id="wall-replace-btn" class="btn-dark text-sm">Replace</button>
|
<button type="button" id="wall-replace-btn" class="btn-dark text-sm">Replace</button>
|
||||||
<div id="wall-replace-msg" class="text-xs text-gray-500"></div>
|
<div id="wall-replace-msg" class="text-xs text-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-sm font-semibold text-gray-700">Color Library</div>
|
||||||
|
<div id="wall-palette" class="palette-box min-h-[3rem]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="save">
|
<div class="control-stack" data-mobile-tab="save">
|
||||||
|
|||||||
58
output_webp/0.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
59
output_webp/1.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
58
output_webp/2.svg
Normal file
|
After Width: | Height: | Size: 27 KiB |
58
output_webp/3.svg
Normal file
|
After Width: | Height: | Size: 31 KiB |
57
output_webp/4.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
57
output_webp/5.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
58
output_webp/6.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
57
output_webp/7.svg
Normal file
|
After Width: | Height: | Size: 18 KiB |
57
output_webp/8.svg
Normal file
|
After Width: | Height: | Size: 28 KiB |
57
output_webp/9.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
18
svg.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Backup originals once
|
||||||
|
mkdir -p output_webp_backup
|
||||||
|
cp --no-clobber output_webp/*.svg output_webp_backup/ || true
|
||||||
|
|
||||||
|
# Convert strokes to filled shapes and force a solid fill
|
||||||
|
FILL="#ffffff" # solid white interior for mask; stroke stays black for outline
|
||||||
|
for f in output_webp/*.svg; do
|
||||||
|
echo "Fixing $f"
|
||||||
|
inkscape "$f" --batch-process \
|
||||||
|
--actions="select-all;object-stroke-to-path;object-to-path;object-set-attribute:fill,$FILL;object-set-attribute:stroke,#000000;object-set-attribute:fill-rule,evenodd;object-set-attribute:style," \
|
||||||
|
--export-type=svg --export-filename="$f" --export-overwrite
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. Originals in output_webp_backup/"
|
||||||