Compare commits
3 Commits
346f6ff917
...
0f325ca1d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f325ca1d8 | |||
| 8bb46389f3 | |||
| 7ba62b3d2b |
403
classic.js
@ -73,27 +73,30 @@
|
||||
}
|
||||
return { color: '#0f172a', shadow: 'none' };
|
||||
}
|
||||
function outlineColorFor(colorInfo) {
|
||||
// Mirrors the topper chip text color choice for consistent contrast.
|
||||
const chipStyle = textStyleForColor(colorInfo || { hex: '#ffffff' });
|
||||
return chipStyle.color || '#0f172a';
|
||||
}
|
||||
|
||||
// -------- persistent color selection (now supports image textures) ----------
|
||||
const PALETTE_KEY = 'classic:colors:v2';
|
||||
const TOPPER_COLOR_KEY = 'classic:topperColor:v2';
|
||||
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_OVERRIDES_KEY = 'classic:manualOverrides:v1';
|
||||
const MANUAL_EXPANDED_KEY = 'classic:manualExpanded:v1';
|
||||
const NUMBER_IMAGE_MAP = {
|
||||
'0': 'output_webp/0.svg',
|
||||
'1': 'output_webp/1.svg',
|
||||
'2': 'output_webp/2.svg',
|
||||
'3': 'output_webp/3.svg',
|
||||
'4': 'output_webp/4.svg',
|
||||
'5': 'output_webp/5.svg',
|
||||
'6': 'output_webp/6.svg',
|
||||
'7': 'output_webp/7.svg',
|
||||
'8': 'output_webp/8.svg',
|
||||
'9': 'output_webp/9.svg'
|
||||
'0': 'output_webp/0.webp',
|
||||
'1': 'output_webp/1.webp',
|
||||
'2': 'output_webp/2.webp',
|
||||
'3': 'output_webp/3.webp',
|
||||
'4': 'output_webp/4.webp',
|
||||
'5': 'output_webp/5.webp',
|
||||
'6': 'output_webp/6.webp',
|
||||
'7': 'output_webp/7.webp',
|
||||
'8': 'output_webp/8.webp',
|
||||
'9': 'output_webp/9.webp'
|
||||
};
|
||||
|
||||
const MAX_SLOTS = 20;
|
||||
@ -103,7 +106,15 @@
|
||||
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', 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
|
||||
const numberSpriteSet = new Set(Object.values(NUMBER_IMAGE_MAP));
|
||||
const sanitizeTopperColor = (colorObj = {}) => {
|
||||
const base = defaultTopper();
|
||||
const hex = normHex(colorObj.hex || base.hex);
|
||||
const img = colorObj.image || null;
|
||||
const image = (img && !numberSpriteSet.has(img)) ? img : null;
|
||||
return { hex, image };
|
||||
};
|
||||
|
||||
function getClassicColors() {
|
||||
let arr = defaultColors();
|
||||
@ -136,38 +147,15 @@
|
||||
function getTopperColor() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(TOPPER_COLOR_KEY));
|
||||
return (saved && saved.hex) ? saved : defaultTopper();
|
||||
} catch { return defaultTopper(); }
|
||||
if (saved && saved.hex) return sanitizeTopperColor(saved);
|
||||
} catch {}
|
||||
return sanitizeTopperColor(defaultTopper());
|
||||
}
|
||||
|
||||
function setTopperColor(colorObj) {
|
||||
const clean = { hex: normHex(colorObj.hex), image: colorObj.image || null };
|
||||
const clean = sanitizeTopperColor(colorObj);
|
||||
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() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(MANUAL_MODE_KEY));
|
||||
@ -307,8 +295,6 @@
|
||||
let topperOffsetX_Px = 0;
|
||||
let topperOffsetY_Px = 0;
|
||||
let topperSizeMultiplier = 1;
|
||||
let numberTintHex = getNumberTintColor();
|
||||
let numberTintOpacity = getNumberTintOpacity();
|
||||
let shineEnabled = true;
|
||||
let borderEnabled = false;
|
||||
let manualMode = loadManualMode();
|
||||
@ -332,8 +318,6 @@
|
||||
setTopperOffsetX(val) { topperOffsetX_Px = (Number(val) || 0) * 5; },
|
||||
setTopperOffsetY(val) { topperOffsetY_Px = (Number(val) || 0) * -5; },
|
||||
setTopperSize(multiplier) { topperSizeMultiplier = Number(multiplier) || 1; },
|
||||
setNumberTintHex(hex) { numberTintHex = setNumberTintColor(hex); },
|
||||
setNumberTintOpacity(val) { numberTintOpacity = setNumberTintOpacity(val); },
|
||||
setShineEnabled(on) { shineEnabled = !!on; },
|
||||
setBorderEnabled(on) { borderEnabled = !!on; },
|
||||
setManualMode(on) { manualMode = !!on; saveManualMode(manualMode); },
|
||||
@ -375,7 +359,8 @@
|
||||
const base = shape.base || {};
|
||||
const scale = cellScale(cell);
|
||||
const expandedOn = model.manualMode && (model.explodedGapPx || 0) > 0;
|
||||
const manualScale = expandedOn ? 1.35 : 1;
|
||||
// Keep arch geometry consistent when expanded; only scale the alpha (wireframe) ring slightly to improve hit targets.
|
||||
const manualScale = expandedOn && model.patternName?.toLowerCase().includes('arch') ? 1 : (expandedOn ? 1.15 : 1);
|
||||
const transform = [(base.transform||''), `scale(${scale * manualScale})`].join(' ');
|
||||
const isUnpainted = !colorInfo || explicitFill === 'none';
|
||||
const wireframe = !!opts.wireframe || (model.manualMode && isUnpainted);
|
||||
@ -401,24 +386,35 @@
|
||||
const isNumTopper = cell.isTopper && (model.topperType || '').startsWith('num-');
|
||||
if (base.image) {
|
||||
const w = base.width || 1, h = base.height || 1;
|
||||
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' }));
|
||||
}
|
||||
const tintColor = model.numberTintHex || '#ffffff';
|
||||
const tintOpacity = model.numberTintOpacity || 0;
|
||||
if (tintOpacity > 0 && isNumTopper) {
|
||||
const maskId = `mask-${id}`;
|
||||
kids.push(svg('mask', { id: maskId, maskUnits: 'userSpaceOnUse' }, [
|
||||
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', {
|
||||
x: -w/2, y: -h/2, width: w, height: h,
|
||||
fill: tintColor, opacity: tintOpacity,
|
||||
mask: `url(#${maskId})`,
|
||||
style: 'mix-blend-mode:multiply; pointer-events:none'
|
||||
if (isNumTopper) {
|
||||
const maskId = base.maskId || 'classic-num-mask';
|
||||
const fillVal = (colorInfo && colorInfo.image)
|
||||
? 'url(#classic-pattern-topper)'
|
||||
: (colorInfo?.hex || '#ffffff');
|
||||
const lum = luminance(colorInfo?.hex || colorInfo?.colour || '#ffffff');
|
||||
const outlineFilterId = lum >= 0.55 ? 'classic-num-outline-dark' : 'classic-num-outline-light';
|
||||
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',
|
||||
filter: `url(#${outlineFilterId})`,
|
||||
mask: `url(#${maskId})`
|
||||
}));
|
||||
// Also draw the image beneath with zero opacity to keep mask refs consistent
|
||||
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('rect', {
|
||||
x: -w/2,
|
||||
y: -h/2,
|
||||
width: w,
|
||||
height: h,
|
||||
fill: fillVal,
|
||||
mask: `url(#${maskId})`,
|
||||
style: 'pointer-events:none'
|
||||
}));
|
||||
} 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)) {
|
||||
base.paths.forEach(p => {
|
||||
@ -431,7 +427,7 @@
|
||||
}
|
||||
|
||||
const allowShine = base.allowShine !== false;
|
||||
const applyShine = !wireframe && model.shineEnabled && (!cell.isTopper || allowShine);
|
||||
const applyShine = !wireframe && model.shineEnabled && (!cell.isTopper || (allowShine && !isNumTopper));
|
||||
if (applyShine) {
|
||||
const shine = classicShineStyle(colorInfo);
|
||||
const shineAttrs = {
|
||||
@ -457,13 +453,18 @@
|
||||
const gap = model.explodedGapPx || 0;
|
||||
const isArch = (model.patternName || '').toLowerCase().includes('arch');
|
||||
if (isArch) {
|
||||
// Move along the arch tangent to increase spacing without distorting the curve.
|
||||
// Move outward along the radial vector and add a tangential nudge for even spread; push ends a bit more.
|
||||
const dist = Math.hypot(xPx, yPx) || 1;
|
||||
const tx = -yPx / dist;
|
||||
const ty = xPx / dist;
|
||||
const push = rowIndex * gap;
|
||||
xPx += tx * push;
|
||||
yPx += ty * push;
|
||||
const maxRow = Math.max(1, (pattern.cellsPerRow * model.rowCount) - 1);
|
||||
const t = Math.max(0, Math.min(1, y / maxRow)); // 0 first row, 1 last row
|
||||
const radialPush = gap * (1.6 + Math.abs(t - 0.5) * 1.6); // ends > crown
|
||||
const tangentialPush = (t - 0.5) * (gap * 0.8); // small along-arc spread
|
||||
const nx = xPx / dist;
|
||||
const ny = yPx / dist;
|
||||
const tx = -ny;
|
||||
const ty = nx;
|
||||
xPx += nx * radialPush + tx * tangentialPush;
|
||||
yPx += ny * radialPush + ty * tangentialPush;
|
||||
} else {
|
||||
yPx += rowIndex * gap; // columns: separate along the vertical path
|
||||
}
|
||||
@ -500,7 +501,12 @@ function distinctPaletteSlots(palette) {
|
||||
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 =
|
||||
[
|
||||
[5, 2, 3, 4, 1],
|
||||
@ -626,14 +632,14 @@ function distinctPaletteSlots(palette) {
|
||||
if (isArch) {
|
||||
// Radial slide outward; preserve layout.
|
||||
const dist = Math.hypot(c.x, c.y) || 1;
|
||||
const offset = 80;
|
||||
const offset = (model.manualMode && (model.explodedGapPx || 0) > 0) ? 120 : 80;
|
||||
const nx = c.x / dist, ny = c.y / dist;
|
||||
slideX = nx * offset;
|
||||
slideY = ny * offset;
|
||||
// Slight tangent spread (~5px) to separate balloons without reshaping the quad.
|
||||
const txDirX = -ny;
|
||||
const txDirY = nx;
|
||||
const fan = spread * 10;
|
||||
const fan = spread * ((model.manualMode && (model.explodedGapPx || 0) > 0) ? 16 : 10);
|
||||
slideX += txDirX * fan;
|
||||
slideY += txDirY * fan;
|
||||
}
|
||||
@ -750,8 +756,11 @@ function distinctPaletteSlots(palette) {
|
||||
const vb = [ minX, minY, vbW, vbH ].join(' ');
|
||||
|
||||
const patternsDefs = [];
|
||||
const maskDefs = [];
|
||||
const SVG_PATTERN_ZOOM = 2.5;
|
||||
const offset = (1 - SVG_PATTERN_ZOOM) / 2;
|
||||
const OUTLINE_DARK = '#0f172a'; // matches chip text for light colors
|
||||
const OUTLINE_LIGHT = '#f8fafc'; // matches chip text for dark colors
|
||||
|
||||
Object.entries(model.palette).forEach(([slot, colorInfo]) => {
|
||||
if (colorInfo.image) {
|
||||
@ -770,16 +779,60 @@ function distinctPaletteSlots(palette) {
|
||||
[ svg('image', { href: model.topperColor.image, x: offset, y: offset, width: SVG_PATTERN_ZOOM, height: SVG_PATTERN_ZOOM, preserveAspectRatio: 'xMidYMid slice' }) ]
|
||||
));
|
||||
}
|
||||
const svgDefs = svg('defs', {}, patternsDefs);
|
||||
Object.entries(numberTopperShapes).forEach(([key, shape]) => {
|
||||
const base = shape.base || {};
|
||||
if (!base.image) return;
|
||||
const w = base.width || 1, h = base.height || 1;
|
||||
const maskId = base.maskId || `classic-num-mask-${key.replace('topper-num-', '')}`;
|
||||
maskDefs.push(svg('mask', { id: maskId, maskUnits: 'userSpaceOnUse', x: -w/2, y: -h/2, width: w, height: h }, [
|
||||
svg('image', { href: base.image, x: -w/2, y: -h/2, width: w, height: h, preserveAspectRatio: base.preserveAspectRatio || 'xMidYMid meet' })
|
||||
]));
|
||||
});
|
||||
const svgDefs = svg('defs', {}, [
|
||||
// Tint: use source alpha to clip topper color, then reapply ink.
|
||||
svg('filter', { id: 'classic-num-tint', 'color-interpolation-filters': 'sRGB', x: '-10%', y: '-10%', width: '120%', height: '120%' }, [
|
||||
svg('feColorMatrix', { in: 'SourceGraphic', type: 'matrix', values: '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0', result: 'alpha' }),
|
||||
svg('feFlood', { 'flood-color': model.topperColor?.hex || '#ffffff', result: 'tint' }),
|
||||
svg('feComposite', { in: 'tint', in2: 'alpha', operator: 'in', result: 'fill' }),
|
||||
svg('feComposite', { in: 'SourceGraphic', in2: 'alpha', operator: 'in', result: 'ink' }),
|
||||
svg('feMerge', {}, [
|
||||
svg('feMergeNode', { in: 'fill' }),
|
||||
svg('feMergeNode', { in: 'ink' })
|
||||
])
|
||||
]),
|
||||
svg('filter', { id: 'classic-num-outline-dark', 'color-interpolation-filters': 'sRGB', x: '-22%', y: '-22%', width: '144%', height: '144%' }, [
|
||||
svg('feMorphology', { in: 'SourceAlpha', operator: 'dilate', radius: 3.1, result: 'spread' }),
|
||||
svg('feComposite', { in: 'spread', in2: 'SourceAlpha', operator: 'out', result: 'stroke' }),
|
||||
svg('feFlood', { 'flood-color': OUTLINE_DARK, 'flood-opacity': 1, result: 'strokeColor' }),
|
||||
svg('feComposite', { in: 'strokeColor', in2: 'stroke', operator: 'in', result: 'coloredStroke' }),
|
||||
svg('feMerge', {}, [
|
||||
svg('feMergeNode', { in: 'coloredStroke' }),
|
||||
svg('feMergeNode', { in: 'SourceGraphic' })
|
||||
])
|
||||
]),
|
||||
svg('filter', { id: 'classic-num-outline-light', 'color-interpolation-filters': 'sRGB', x: '-22%', y: '-22%', width: '144%', height: '144%' }, [
|
||||
svg('feMorphology', { in: 'SourceAlpha', operator: 'dilate', radius: 3.1, result: 'spread' }),
|
||||
svg('feComposite', { in: 'spread', in2: 'SourceAlpha', operator: 'out', result: 'stroke' }),
|
||||
svg('feFlood', { 'flood-color': OUTLINE_LIGHT, 'flood-opacity': 1, result: 'strokeColor' }),
|
||||
svg('feComposite', { in: 'strokeColor', in2: 'stroke', operator: 'in', result: 'coloredStroke' }),
|
||||
svg('feMerge', {}, [
|
||||
svg('feMergeNode', { in: 'coloredStroke' }),
|
||||
svg('feMergeNode', { in: 'SourceGraphic' })
|
||||
])
|
||||
]),
|
||||
...maskDefs,
|
||||
...patternsDefs
|
||||
]);
|
||||
|
||||
const mainGroup = svg('g', null, kids);
|
||||
const zoomPercent = classicZoom * 100;
|
||||
m.render(container, svg('svg', {
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
width:'100%',
|
||||
height:'100%',
|
||||
viewBox: vb,
|
||||
preserveAspectRatio:'xMidYMid meet',
|
||||
style: `isolation:isolate; transform:scale(${classicZoom}); transform-origin:center center;`
|
||||
style: `isolation:isolate; width:${zoomPercent}%; height:${zoomPercent}%; min-width:${zoomPercent}%; min-height:${zoomPercent}%; transform-origin:center center;`
|
||||
}, [svgDefs, mainGroup]));
|
||||
}
|
||||
|
||||
@ -801,8 +854,6 @@ function distinctPaletteSlots(palette) {
|
||||
topperColor: getTopperColor(),
|
||||
topperType,
|
||||
shineEnabled,
|
||||
numberTintHex,
|
||||
numberTintOpacity,
|
||||
manualMode,
|
||||
manualFocusEnabled,
|
||||
manualFloatingQuad,
|
||||
@ -868,33 +919,47 @@ function distinctPaletteSlots(palette) {
|
||||
}
|
||||
|
||||
function buildNumberTopperShapes() {
|
||||
const r = 1.0;
|
||||
const baseTransform = 'scale(0.58)';
|
||||
const fallbackPaths = {
|
||||
'0': { d: 'M 0 -0.7 C 0.38 -0.7 0.62 -0.42 0.62 0 C 0.62 0.42 0.38 0.7 0 0.7 C -0.38 0.7 -0.62 0.42 -0.62 0 C -0.62 -0.42 -0.38 -0.7 0 -0.7 Z M 0 -0.4 C -0.2 -0.4 -0.34 -0.22 -0.34 0 C -0.34 0.24 -0.2 0.42 0 0.42 C 0.2 0.42 0.34 0.24 0.34 0 C 0.34 -0.22 0.2 -0.4 0 -0.4 Z', fillRule: 'evenodd' },
|
||||
'1': { d: 'M -0.12 -0.55 Q 0.1 -0.72 0.28 -0.6 Q 0.36 -0.52 0.34 -0.42 L 0.34 0.65 Q 0.34 0.82 0 0.82 Q -0.34 0.82 -0.34 0.65 L -0.34 -0.18 Q -0.34 -0.32 -0.46 -0.32 Q -0.6 -0.32 -0.62 -0.45 Q -0.64 -0.58 -0.52 -0.65 Z' },
|
||||
'2': { d: 'M -0.55 -0.25 Q -0.55 -0.7 -0.18 -0.9 Q 0.1 -1.05 0.48 -0.98 Q 0.86 -0.9 1 -0.55 Q 1.12 -0.25 0.92 0.06 Q 0.78 0.28 0.36 0.5 Q 0.02 0.68 -0.2 0.88 Q -0.36 1.04 -0.32 1.12 Q -0.28 1.2 -0.12 1.2 L 0.78 1.2 Q 0.98 1.2 0.98 0.94 Q 0.98 0.7 0.78 0.7 L 0.14 0.7 Q 0.02 0.7 0.02 0.6 Q 0.02 0.52 0.24 0.38 Q 0.76 0.08 0.96 -0.22 Q 1.2 -0.58 1 -0.98 Q 0.82 -1.36 0.38 -1.48 Q -0.2 -1.64 -0.7 -1.34 Q -1.1 -1.1 -1.12 -0.6 Q -1.14 -0.38 -0.96 -0.3 Q -0.8 -0.24 -0.68 -0.32 Q -0.55 -0.42 -0.55 -0.25 Z', fillRule: 'nonzero' },
|
||||
'3': { d: 'M -0.42 -0.88 Q -0.1 -1.08 0.26 -1.02 Q 0.7 -0.94 0.94 -0.62 Q 1.16 -0.32 1 -0.02 Q 0.86 0.24 0.58 0.36 Q 0.88 0.5 1 0.76 Q 1.16 1.12 0.88 1.38 Q 0.6 1.64 0.08 1.64 Q -0.3 1.64 -0.62 1.44 Q -0.88 1.26 -0.78 0.98 Q -0.7 0.72 -0.44 0.82 Q -0.06 0.96 0.26 0.88 Q 0.42 0.82 0.42 0.62 Q 0.42 0.38 0.1 0.36 L -0.24 0.34 Q -0.44 0.32 -0.44 0.12 Q -0.44 -0.08 -0.24 -0.12 L 0.08 -0.2 Q 0.32 -0.24 0.4 -0.42 Q 0.48 -0.62 0.26 -0.76 Q -0.02 -0.94 -0.4 -0.78 Q -0.62 -0.7 -0.74 -0.9 Q -0.86 -1.1 -0.68 -1.26 Q -0.58 -1.36 -0.42 -0.88 Z' },
|
||||
'4': { d: 'M 0.42 -0.94 Q 0.64 -0.94 0.7 -0.74 L 0.7 -0.1 L 0.92 -0.1 Q 1.12 -0.08 1.14 0.16 Q 1.16 0.38 0.92 0.46 L 0.7 0.54 L 0.7 0.98 Q 0.7 1.14 0.5 1.18 Q 0.3 1.22 0.18 1.08 L -0.34 0.48 L -0.6 0.48 Q -0.82 0.48 -0.86 0.28 Q -0.88 0.08 -0.7 -0.02 L -0.36 -0.18 L -0.36 -0.76 Q -0.36 -0.96 -0.14 -0.96 Q 0.08 -0.96 0.12 -0.76 L 0.12 -0.36 L 0.08 -0.36 Q 0.28 -0.62 0.42 -0.94 Z' },
|
||||
'5': { d: 'M 0.92 -0.94 Q 0.92 -1.16 0.72 -1.16 L -0.58 -1.16 Q -0.86 -1.16 -0.86 -0.86 Q -0.86 -0.56 -0.58 -0.56 L -0.2 -0.56 Q -0.02 -0.56 0.14 -0.5 Q 0.44 -0.38 0.44 -0.06 Q 0.44 0.18 0.22 0.36 Q 0.06 0.5 -0.16 0.5 L -0.52 0.5 Q -0.8 0.5 -0.8 0.8 Q -0.8 1.12 -0.52 1.12 L 0.24 1.12 Q 0.7 1.12 0.96 0.84 Q 1.2 0.58 1.12 0.24 Q 1.04 -0.02 0.82 -0.16 Q 0.96 -0.38 0.98 -0.62 Q 0.98 -0.86 0.92 -0.94 Z' },
|
||||
'6': { d: 'M 0.94 -0.6 Q 0.88 -0.98 0.52 -1.16 Q 0.06 -1.4 -0.44 -1.1 Q -0.94 -0.82 -1.02 -0.22 Q -1.12 0.4 -0.8 0.96 Q -0.48 1.48 0.14 1.48 Q 0.52 1.48 0.82 1.24 Q 1.12 1.02 1.12 0.66 Q 1.12 0.32 0.86 0.1 Q 0.66 -0.08 0.42 -0.08 Q 0.08 -0.08 -0.12 0.18 Q -0.26 0.36 -0.28 0.6 Q -0.5 0.26 -0.48 -0.18 Q -0.46 -0.66 -0.12 -0.86 Q 0.08 -0.98 0.32 -0.9 Q 0.56 -0.82 0.62 -0.58 Q 0.68 -0.34 0.92 -0.32 Q 1.02 -0.32 0.94 -0.6 Z M -0.06 0.6 C 0.12 0.6 0.26 0.44 0.26 0.26 C 0.26 0.08 0.12 -0.08 -0.06 -0.08 C -0.24 -0.08 -0.38 0.08 -0.38 0.26 C -0.38 0.44 -0.24 0.6 -0.06 0.6 Z', fillRule: 'evenodd' },
|
||||
'7': { d: 'M -0.74 -0.96 Q -0.94 -0.96 -0.94 -0.72 Q -0.94 -0.5 -0.74 -0.5 L 0.2 -0.5 Q 0.46 -0.5 0.52 -0.3 Q 0.58 -0.1 0.42 0.1 L -0.28 1.02 Q -0.42 1.2 -0.22 1.32 Q 0 1.44 0.18 1.28 L 0.98 0.2 Q 1.22 -0.12 1.1 -0.46 Q 0.98 -0.84 0.58 -0.96 Q 0.32 -1.04 0 -1.04 Z' },
|
||||
'8': { d: 'M 0 -1 C 0.44 -1 0.78 -0.72 0.78 -0.34 C 0.78 0.02 0.46 0.28 0.18 0.36 C 0.46 0.44 0.8 0.66 0.8 1.02 C 0.8 1.44 0.44 1.72 0 1.72 C -0.44 1.72 -0.8 1.44 -0.8 1.02 C -0.8 0.66 -0.48 0.44 -0.2 0.36 C -0.48 0.28 -0.8 0.02 -0.8 -0.34 C -0.8 -0.72 -0.44 -1 -0.02 -1 Z M 0 0.48 C 0.2 0.48 0.34 0.64 0.34 0.84 C 0.34 1.04 0.2 1.18 0 1.18 C -0.2 1.18 -0.34 1.04 -0.34 0.84 C -0.34 0.64 -0.2 0.48 0 0.48 Z M 0 -0.46 C 0.18 -0.46 0.3 -0.64 0.3 -0.8 C 0.3 -0.98 0.18 -1.12 0 -1.12 C -0.18 -1.12 -0.3 -0.98 -0.3 -0.8 C -0.3 -0.64 -0.18 -0.46 0 -0.46 Z', fillRule: 'evenodd' },
|
||||
'9': { d: 'M 0 -0.72 C 0.42 -0.72 0.7 -0.44 0.7 -0.08 C 0.7 0.2 0.56 0.46 0.32 0.6 C 0.12 0.72 0.1 0.84 0.1 1.06 C 0.1 1.24 -0.16 1.32 -0.28 1.18 L -0.64 0.72 C -0.92 0.38 -1.08 -0.1 -0.96 -0.54 C -0.82 -1.02 -0.46 -1.26 -0.08 -1.26 C 0.14 -1.26 0.32 -1.18 0.48 -1.04 C 0.62 -0.9 0.62 -0.74 0.5 -0.66 C 0.38 -0.58 0.26 -0.66 0.08 -0.74 C -0.14 -0.84 -0.34 -0.68 -0.42 -0.44 C -0.5 -0.24 -0.46 0.04 -0.32 0.26 C -0.16 0.5 0.14 0.46 0.26 0.26 C 0.38 0.06 0.3 -0.1 0.14 -0.18 C 0.02 -0.24 0 -0.42 0.12 -0.52 C 0.2 -0.58 0.46 -0.62 0.64 -0.42 C 0.82 -0.22 0.86 0.02 0.76 0.24 C 0.6 0.58 0.18 0.76 -0.16 0.7 C -0.36 0.66 -0.54 0.56 -0.68 0.42 C -0.64 0.82 -0.44 1.22 -0.14 1.46 C 0.16 1.7 0.54 1.72 0.86 1.52 C 1.26 1.26 1.42 0.84 1.36 0.38 C 1.26 -0.3 0.72 -0.72 0 -0.72 Z', fillRule: 'evenodd' }
|
||||
// Reuse the exported SVG glyphs for numbers and recolor them via an alpha mask.
|
||||
// ViewBox sizes vary a lot, so we normalize the aspect ratio with a shared base height.
|
||||
const viewBoxes = {
|
||||
'0': { w: 28.874041, h: 38.883382 },
|
||||
'1': { w: 20.387968, h: 39.577327 },
|
||||
'2': { w: 27.866452, h: 39.567209 },
|
||||
'3': { w: 27.528916, h: 39.153201 },
|
||||
'4': { w: 39.999867, h: 39.999867 },
|
||||
'5': { w: 39.999867, h: 39.999867 },
|
||||
'6': { w: 27.952782, h: 39.269062 },
|
||||
'7': { w: 39.999867, h: 39.999867 },
|
||||
'8': { w: 39.999867, h: 39.999867 },
|
||||
'9': { w: 39.999867, h: 39.999867 }
|
||||
};
|
||||
|
||||
const shapes = {};
|
||||
const topperSize = 9.5; // ≈34" foil height when base balloons are ~11"
|
||||
Object.keys({ ...fallbackPaths, ...NUMBER_IMAGE_MAP }).forEach(num => {
|
||||
const img = NUMBER_IMAGE_MAP[num];
|
||||
const hasImage = !!img;
|
||||
const topperSize = 8.2; // closer to round topper scale
|
||||
const baseHeight = 1.05;
|
||||
|
||||
Object.entries(NUMBER_IMAGE_MAP).forEach(([num, href]) => {
|
||||
const vb = viewBoxes[num] || { w: 40, h: 40 };
|
||||
const aspect = vb.w / Math.max(1, vb.h);
|
||||
const height = baseHeight;
|
||||
const width = height * aspect;
|
||||
const radius = Math.max(width, height) / 2;
|
||||
const maskId = `classic-num-mask-${num}`;
|
||||
|
||||
shapes[`topper-num-${num}`] = {
|
||||
base: hasImage
|
||||
? { type: 'image', image: img, width: 1, height: 1, radius: 0.9, allowShine: false, transform: 'scale(0.9)' }
|
||||
: { type: 'path', paths: [{ d: fallbackPaths[num].d, fillRule: fallbackPaths[num].fillRule || 'nonzero' }], radius: r, allowShine: true, transform: baseTransform },
|
||||
base: {
|
||||
image: href,
|
||||
width,
|
||||
height,
|
||||
preserveAspectRatio: 'xMidYMid meet',
|
||||
maskId,
|
||||
radius,
|
||||
allowShine: false // keep number toppers matte; shine causes halo
|
||||
},
|
||||
size: topperSize
|
||||
};
|
||||
});
|
||||
|
||||
return shapes;
|
||||
}
|
||||
|
||||
@ -1010,6 +1075,8 @@ function distinctPaletteSlots(palette) {
|
||||
|
||||
function initClassicColorPicker(onColorChange) {
|
||||
const slotsContainer = document.getElementById('classic-slots'), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors'), addSlotBtn = document.getElementById('classic-add-slot'), activeChip = document.getElementById('classic-active-chip'), floatingChip = document.getElementById('classic-active-chip-floating'), activeDot = document.getElementById('classic-active-dot'), floatingDot = document.getElementById('classic-active-dot-floating');
|
||||
const projectBlock = document.getElementById('classic-project-block');
|
||||
const replaceBlock = document.getElementById('classic-replace-block');
|
||||
const replaceFromSel = document.getElementById('classic-replace-from');
|
||||
const replaceToSel = document.getElementById('classic-replace-to');
|
||||
const replaceBtn = document.getElementById('classic-replace-btn');
|
||||
@ -1017,9 +1084,8 @@ function distinctPaletteSlots(palette) {
|
||||
const replaceFromChip = document.getElementById('classic-replace-from-chip');
|
||||
const replaceToChip = document.getElementById('classic-replace-to-chip');
|
||||
const replaceCountLabel = document.getElementById('classic-replace-count');
|
||||
const numberTintSlider = document.getElementById('classic-number-tint');
|
||||
const topperBlock = document.getElementById('classic-topper-color-block');
|
||||
if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
|
||||
if (!slotsContainer || !topperSwatch || !swatchGrid) return;
|
||||
topperSwatch.classList.add('tab-btn');
|
||||
let classicColors = getClassicColors(), activeTarget = '1', slotCount = getStoredSlotCount();
|
||||
const publishActiveTarget = () => {
|
||||
@ -1083,6 +1149,13 @@ function distinctPaletteSlots(palette) {
|
||||
renderSlots();
|
||||
}
|
||||
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 palette = buildClassicPalette();
|
||||
@ -1283,23 +1356,14 @@ function distinctPaletteSlots(palette) {
|
||||
slot.style.textShadow = txt.shadow;
|
||||
});
|
||||
|
||||
const topperColor = getTopperColor();
|
||||
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.backgroundBlendMode = 'normal';
|
||||
topperSwatch.style.backgroundColor = topperColor.hex;
|
||||
topperSwatch.style.backgroundSize = '200%';
|
||||
topperSwatch.style.backgroundPosition = 'center';
|
||||
}
|
||||
const topperTxt = textStyleForColor({ hex: tintColor || topperColor.hex, image: topperColor.image });
|
||||
const topperColor = getTopperColor();
|
||||
const currentType = document.querySelector('.topper-type-btn[aria-pressed="true"]')?.dataset.type || 'round';
|
||||
topperSwatch.style.backgroundImage = topperColor.image ? `url("${topperColor.image}")` : 'none';
|
||||
topperSwatch.style.backgroundBlendMode = 'normal';
|
||||
topperSwatch.style.backgroundColor = topperColor.hex;
|
||||
topperSwatch.style.backgroundSize = '200%';
|
||||
topperSwatch.style.backgroundPosition = 'center';
|
||||
const topperTxt = textStyleForColor({ hex: topperColor.hex, image: topperColor.image });
|
||||
topperSwatch.style.color = topperTxt.color;
|
||||
topperSwatch.style.textShadow = topperTxt.shadow;
|
||||
const patName = (document.getElementById('classic-pattern')?.value || '').toLowerCase();
|
||||
@ -1319,15 +1383,24 @@ function distinctPaletteSlots(palette) {
|
||||
|
||||
const manualModeOn = isManual();
|
||||
const sharedActive = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
||||
activeLabel.textContent = manualModeOn ? 'Manual paint color' : (activeTarget === 'T' ? 'Topper' : `Slot #${activeTarget}`);
|
||||
if (activeChip) {
|
||||
const idx = parseInt(activeTarget, 10) - 1;
|
||||
const color = manualModeOn ? sharedActive : (classicColors[idx] || { hex: '#ffffff', image: null });
|
||||
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';
|
||||
activeChip.style.backgroundImage = bgImg;
|
||||
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) {
|
||||
activeDot.style.backgroundImage = bgImg;
|
||||
activeDot.style.backgroundColor = bgCol;
|
||||
@ -1350,9 +1423,8 @@ function distinctPaletteSlots(palette) {
|
||||
if (activeChip) {
|
||||
activeChip.style.display = manualModeOn ? '' : 'none';
|
||||
}
|
||||
if (projectPaletteBox) {
|
||||
projectPaletteBox.parentElement?.classList.toggle('hidden', !manualModeOn);
|
||||
}
|
||||
if (projectBlock) projectBlock.classList.toggle('hidden', !manualModeOn);
|
||||
if (replaceBlock) replaceBlock.classList.toggle('hidden', !manualModeOn);
|
||||
}
|
||||
|
||||
swatchGrid.innerHTML = '';
|
||||
@ -1378,13 +1450,7 @@ function distinctPaletteSlots(palette) {
|
||||
const meta = item.meta || {};
|
||||
const selectedColor = { hex: meta.hex || item.hex, image: meta.image || null };
|
||||
if (activeTarget === 'T') {
|
||||
if (currentType.startsWith('num-')) {
|
||||
setNumberTintColor(selectedColor.hex);
|
||||
setNumberTintOpacity(1);
|
||||
if (numberTintSlider) numberTintSlider.value = 1;
|
||||
} else {
|
||||
setTopperColor(selectedColor);
|
||||
}
|
||||
setTopperColor(selectedColor);
|
||||
} else if (isManual()) {
|
||||
manualActiveColorGlobal = window.shared?.setActiveColor?.(selectedColor) || selectedColor;
|
||||
} else {
|
||||
@ -1487,7 +1553,6 @@ function distinctPaletteSlots(palette) {
|
||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
||||
projectPaletteBox = null;
|
||||
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display');
|
||||
const numberTintRow = document.getElementById('classic-number-tint-row'), numberTintSlider = document.getElementById('classic-number-tint');
|
||||
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
||||
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
||||
const toolbar = document.getElementById('classic-canvas-toolbar');
|
||||
@ -1496,6 +1561,8 @@ function distinctPaletteSlots(palette) {
|
||||
const toolbarZoomOut = document.getElementById('classic-toolbar-zoomout');
|
||||
const toolbarReset = document.getElementById('classic-toolbar-reset');
|
||||
const focusLabelCanvas = document.getElementById('classic-focus-label-canvas');
|
||||
const reverseLabel = reverseCb?.closest('label');
|
||||
const reverseHint = reverseLabel?.parentElement?.querySelector('.hint');
|
||||
const quadModal = document.getElementById('classic-quad-modal');
|
||||
const quadModalClose = document.getElementById('classic-quad-modal-close');
|
||||
const quadModalDisplay = document.getElementById('classic-quad-modal-display');
|
||||
@ -1504,6 +1571,7 @@ function distinctPaletteSlots(palette) {
|
||||
const patternLayoutBtns = Array.from(document.querySelectorAll('[data-pattern-layout]'));
|
||||
const topperNudgeBtns = Array.from(document.querySelectorAll('.nudge-topper'));
|
||||
const topperTypeButtons = Array.from(document.querySelectorAll('.topper-type-btn'));
|
||||
const topperSizeInput = document.getElementById('classic-topper-size');
|
||||
const slotsContainer = document.getElementById('classic-slots');
|
||||
projectPaletteBox = document.getElementById('classic-project-palette');
|
||||
const manualPaletteBtn = document.getElementById('classic-manual-palette');
|
||||
@ -1559,7 +1627,6 @@ function distinctPaletteSlots(palette) {
|
||||
};
|
||||
// Force UI to reflect initial manual state
|
||||
if (manualModeState) patternLayout = 'manual';
|
||||
if (numberTintSlider) numberTintSlider.value = getNumberTintOpacity();
|
||||
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 },
|
||||
@ -1572,6 +1639,16 @@ function distinctPaletteSlots(palette) {
|
||||
};
|
||||
if (!display) return fail('#classic-display not found');
|
||||
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;
|
||||
|
||||
const getTopperType = () => topperTypeButtons.find(btn => btn.getAttribute('aria-pressed') === 'true')?.dataset.type || 'round';
|
||||
@ -1584,21 +1661,18 @@ function distinctPaletteSlots(palette) {
|
||||
});
|
||||
window.ClassicDesigner.lastTopperType = type;
|
||||
};
|
||||
function applyNumberTopperTexture(type) {
|
||||
function ensureNumberTopperImage(type) {
|
||||
if (!type || !type.startsWith('num-')) return;
|
||||
const num = type.split('-')[1];
|
||||
if (!num) return;
|
||||
const imgPath = NUMBER_IMAGE_MAP[num];
|
||||
if (imgPath) setTopperColor({ hex: '#ffffff', image: imgPath });
|
||||
else setTopperColor({ hex: '#d4d4d8', image: null }); // fallback silver fill if image missing
|
||||
refreshClassicPaletteUi?.();
|
||||
const cur = getTopperColor();
|
||||
if (cur?.image && numberSpriteSet.has(cur.image)) {
|
||||
setTopperColor({ hex: cur?.hex || '#ffffff', image: null });
|
||||
refreshClassicPaletteUi?.();
|
||||
}
|
||||
}
|
||||
|
||||
function resetNonNumberTopperColor(type) {
|
||||
if (type && type.startsWith('num-')) return;
|
||||
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 && numberSpriteSet.has(fallback.image)) {
|
||||
setTopperColor({ hex: fallback.hex || '#ffffff', image: null });
|
||||
refreshClassicPaletteUi?.();
|
||||
}
|
||||
@ -1612,10 +1686,10 @@ function distinctPaletteSlots(palette) {
|
||||
if (lastPresetKey === key || lastPresetKey === 'custom') return;
|
||||
topperOffsetX = preset.offsetX;
|
||||
topperOffsetY = preset.offsetY;
|
||||
if (topperSizeInp) topperSizeInp.value = preset.size;
|
||||
if (topperSizeInput) topperSizeInput.value = preset.size;
|
||||
if (topperEnabledCb) topperEnabledCb.checked = preset.enabled;
|
||||
setTopperType(type);
|
||||
applyNumberTopperTexture(type);
|
||||
ensureNumberTopperImage(type);
|
||||
resetNonNumberTopperColor(type);
|
||||
lastPresetKey = key;
|
||||
}
|
||||
@ -1680,8 +1754,7 @@ function distinctPaletteSlots(palette) {
|
||||
topperType: getTopperType(),
|
||||
topperOffsetX,
|
||||
topperOffsetY,
|
||||
topperSize: topperSizeInp?.value || '',
|
||||
numberTint: numberTintSlider ? numberTintSlider.value : getNumberTintOpacity()
|
||||
topperSize: topperSizeInput?.value || ''
|
||||
};
|
||||
saveClassicState(state);
|
||||
}
|
||||
@ -1695,12 +1768,8 @@ function distinctPaletteSlots(palette) {
|
||||
if (topperEnabledCb) topperEnabledCb.checked = !!saved.topperEnabled;
|
||||
if (typeof saved.topperOffsetX === 'number') topperOffsetX = saved.topperOffsetX;
|
||||
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 (numberTintSlider && typeof saved.numberTint !== 'undefined') {
|
||||
numberTintSlider.value = saved.numberTint;
|
||||
setNumberTintOpacity(saved.numberTint);
|
||||
}
|
||||
syncPatternStateFromSelect();
|
||||
lastPresetKey = 'custom';
|
||||
}
|
||||
@ -1970,20 +2039,23 @@ function distinctPaletteSlots(palette) {
|
||||
const isColumn = patternName.toLowerCase().includes('column');
|
||||
const hasTopper = patternName.includes('4') || patternName.includes('5');
|
||||
const showToggle = isColumn && hasTopper;
|
||||
if (patternName.toLowerCase().includes('column')) {
|
||||
const baseName = patternName.includes('5') ? 'Column 5' : 'Column 4';
|
||||
applyTopperPreset(baseName, getTopperType());
|
||||
}
|
||||
if (patternName.toLowerCase().includes('column')) {
|
||||
const baseName = patternName.includes('5') ? 'Column 5' : 'Column 4';
|
||||
applyTopperPreset(baseName, getTopperType());
|
||||
}
|
||||
if (topperToggleRow) topperToggleRow.classList.toggle('hidden', !showToggle);
|
||||
const showTopper = showToggle && topperEnabledCb?.checked;
|
||||
const isNumberTopper = getTopperType().startsWith('num-');
|
||||
|
||||
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);
|
||||
const showReverse = patternLayout === 'spiral' && !manualOn;
|
||||
if (reverseLabel) reverseLabel.classList.toggle('hidden', !showReverse);
|
||||
if (reverseHint) reverseHint.classList.toggle('hidden', !showReverse);
|
||||
if (reverseCb) {
|
||||
reverseCb.disabled = manualOn;
|
||||
if (manualOn) reverseCb.checked = false;
|
||||
reverseCb.disabled = manualOn || !showReverse;
|
||||
if (!showReverse) reverseCb.checked = false;
|
||||
}
|
||||
|
||||
GC.setTopperEnabled(showTopper);
|
||||
@ -1991,18 +2063,15 @@ function distinctPaletteSlots(palette) {
|
||||
GC.setManualMode(manualOn);
|
||||
GC.setReverse(!!reverseCb?.checked);
|
||||
GC.setTopperType(getTopperType());
|
||||
GC.setNumberTintHex(getNumberTintColor());
|
||||
GC.setNumberTintOpacity(numberTintSlider ? numberTintSlider.value : getNumberTintOpacity());
|
||||
applyNumberTopperTexture(getTopperType());
|
||||
GC.setTopperOffsetX(topperOffsetX);
|
||||
GC.setTopperOffsetY(topperOffsetY);
|
||||
GC.setTopperSize(topperSizeInp?.value);
|
||||
GC.setTopperSize(topperSizeInput?.value);
|
||||
GC.setShineEnabled(!!shineEnabledCb?.checked);
|
||||
GC.setBorderEnabled(!!borderEnabledCb?.checked);
|
||||
const expandedOn = manualOn && manualExpandedState;
|
||||
GC.setExplodedSettings({
|
||||
scale: expandedOn ? 1.18 : 1,
|
||||
gapPx: expandedOn ? 26 : 0,
|
||||
gapPx: expandedOn ? 90 : 0,
|
||||
staggerPx: expandedOn ? 6 : 0
|
||||
});
|
||||
if (display) {
|
||||
@ -2329,15 +2398,15 @@ function distinctPaletteSlots(palette) {
|
||||
}));
|
||||
topperTypeButtons.forEach(btn => btn.addEventListener('click', () => {
|
||||
setTopperType(btn.dataset.type);
|
||||
applyNumberTopperTexture(btn.dataset.type);
|
||||
ensureNumberTopperImage(btn.dataset.type);
|
||||
resetNonNumberTopperColor(btn.dataset.type);
|
||||
if (topperEnabledCb) {
|
||||
topperEnabledCb.checked = true;
|
||||
GC.setTopperEnabled(true);
|
||||
}
|
||||
lastPresetKey = null;
|
||||
updateClassicDesign();
|
||||
}));
|
||||
numberTintSlider?.addEventListener('input', () => {
|
||||
GC.setNumberTintOpacity(numberTintSlider.value);
|
||||
updateClassicDesign();
|
||||
});
|
||||
nudgeOpenBtn?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.__showFloatingNudge?.();
|
||||
@ -2361,8 +2430,8 @@ function distinctPaletteSlots(palette) {
|
||||
}
|
||||
});
|
||||
document.addEventListener('fullscreenchange', updateFullscreenLabel);
|
||||
[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(); }); });
|
||||
[lengthInp, reverseCb, topperEnabledCb, topperSizeInput]
|
||||
.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);
|
||||
shineEnabledCb?.addEventListener('change', (e) => { const on = !!e.target.checked; GC.setShineEnabled(on); updateClassicDesign(); window.syncAppShine?.(on); });
|
||||
borderEnabledCb?.addEventListener('change', (e) => {
|
||||
|
||||
233
index.html
@ -37,7 +37,7 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<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-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>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3">
|
||||
@ -155,41 +155,44 @@
|
||||
</div>
|
||||
|
||||
<div class="control-stack" data-mobile-tab="colors">
|
||||
<div class="panel-heading">Project Palette</div>
|
||||
<div class="panel-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<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>
|
||||
</div>
|
||||
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-heading mt-4">Color Library</div>
|
||||
<div class="panel-card">
|
||||
<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 class="panel-heading">Organic Colors</div>
|
||||
<div class="panel-card space-y-4">
|
||||
<div class="space-y-2">
|
||||
<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 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">
|
||||
<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>
|
||||
<button type="button" class="replace-chip" id="replace-to-chip" aria-label="Pick replacement color"></button>
|
||||
<span id="replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
||||
</div>
|
||||
<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-to" class="sr-only"></select>
|
||||
<button id="replace-btn" class="btn-blue">Replace</button>
|
||||
<p id="replace-msg" class="hint"></p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold text-gray-700">Replace Color</div>
|
||||
<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>
|
||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||
<button type="button" class="replace-chip" id="replace-to-chip" aria-label="Pick replacement color"></button>
|
||||
<span id="replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<select id="replace-from" class="sr-only"></select>
|
||||
<select id="replace-to" class="sr-only"></select>
|
||||
<button id="replace-btn" class="btn-blue">Replace</button>
|
||||
<p id="replace-msg" class="hint"></p>
|
||||
</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>
|
||||
@ -289,7 +292,7 @@
|
||||
<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">
|
||||
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
|
||||
Add Topper (24")
|
||||
Add Topper
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -317,18 +320,6 @@
|
||||
</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">
|
||||
<button type="button" id="classic-nudge-open" class="btn-dark text-xs px-3 py-2">Nudge Panel</button>
|
||||
</div>
|
||||
@ -364,55 +355,57 @@
|
||||
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="text-sm font-semibold text-gray-700">Active color:</span>
|
||||
<button id="classic-active-chip" type="button" class="slot-swatch border border-gray-300" title="Tap to scroll to palette">
|
||||
<span class="color-dot" id="classic-active-dot"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-heading mt-2">Project Palette</div>
|
||||
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
|
||||
<div class="panel-heading mt-4">Replace Color (Manual)</div>
|
||||
<div class="panel-card space-y-3">
|
||||
<div class="flex items-center gap-2 replace-row">
|
||||
<button type="button" class="replace-chip" id="classic-replace-from-chip" aria-label="Pick color to replace"></button>
|
||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||
<button type="button" class="replace-chip" id="classic-replace-to-chip" aria-label="Pick replacement color"></button>
|
||||
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<p class="hint text-xs">Manual paint only. “From” lists colors already used on canvas; “To” comes from the Classic library.</p>
|
||||
<select id="classic-replace-from" class="sr-only"></select>
|
||||
<select id="classic-replace-to" class="sr-only"></select>
|
||||
<button id="classic-replace-btn" class="btn-blue">Replace</button>
|
||||
<p id="classic-replace-msg" class="hint"></p>
|
||||
<span class="text-sm text-gray-700 classic-label">Active color</span>
|
||||
<div id="classic-active-chip" class="current-color-chip cursor-pointer" title="Tap to scroll to palette">
|
||||
<span id="classic-active-label" class="text-xs font-semibold text-slate-700"></span>
|
||||
</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>
|
||||
|
||||
<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="flex items-center gap-2 replace-row">
|
||||
<button type="button" class="replace-chip" id="classic-replace-from-chip" aria-label="Pick color to replace"></button>
|
||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||
<button type="button" class="replace-chip" id="classic-replace-to-chip" aria-label="Pick replacement color"></button>
|
||||
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<select id="classic-replace-from" class="sr-only"></select>
|
||||
<select id="classic-replace-to" class="sr-only"></select>
|
||||
<button id="classic-replace-btn" class="btn-blue">Replace</button>
|
||||
<p id="classic-replace-msg" class="hint"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
|
||||
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<button id="classic-randomize-colors" class="btn-dark">Randomize</button>
|
||||
</div>
|
||||
<div id="classic-topper-color-block" class="mt-3 hidden">
|
||||
<div class="panel-heading">Topper Color</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<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 class="control-stack" data-mobile-tab="save">
|
||||
<div class="panel-heading">Save & Share</div>
|
||||
<div class="panel-card space-y-3">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
||||
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
||||
<p class="hint w-full">SVG keeps the vector Classic layout; PNG is raster.</p>
|
||||
</div>
|
||||
<p class="hint text-red-500">Classic JSON save/load coming soon.</p>
|
||||
</div>
|
||||
<div class="control-stack" data-mobile-tab="save">
|
||||
<div class="panel-heading">Save & Share</div>
|
||||
<div class="panel-card space-y-3">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
||||
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
||||
<p class="hint w-full">SVG keeps the vector Classic layout; PNG is raster.</p>
|
||||
</div>
|
||||
<p class="hint text-red-500">Classic JSON save/load coming soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section id="classic-canvas-panel"
|
||||
@ -453,10 +446,10 @@
|
||||
</div>
|
||||
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
||||
<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>
|
||||
</div>
|
||||
<div class="floating-nudge-body">
|
||||
<div class="floating-nudge-body space-y-3">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div></div>
|
||||
<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>
|
||||
<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>
|
||||
</section>
|
||||
@ -520,42 +517,42 @@
|
||||
</div>
|
||||
|
||||
<div class="control-stack" data-mobile-tab="colors">
|
||||
<div class="panel-heading mt-4">Active Color</div>
|
||||
<div class="panel-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-700">Current</span>
|
||||
<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>
|
||||
<div class="panel-heading">Wall Colors</div>
|
||||
<div class="panel-card space-y-4">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-700">Active color</span>
|
||||
<div id="wall-active-color-chip" class="current-color-chip">
|
||||
<span id="wall-active-color-label" class="text-xs font-semibold text-slate-700"></span>
|
||||
</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 class="panel-heading mt-4">Used Colors</div>
|
||||
<div class="panel-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-xs text-gray-600">Click to pick. Remove unused clears empty/transparent entries.</div>
|
||||
<button type="button" id="wall-remove-unused" class="btn-yellow text-xs px-2 py-1">Remove Unused</button>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-gray-700">Project Palette</span>
|
||||
</div>
|
||||
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
|
||||
</div>
|
||||
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
|
||||
</div>
|
||||
<div class="panel-heading mt-4">Wall Palette</div>
|
||||
<div class="panel-card">
|
||||
<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">
|
||||
<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>
|
||||
<button type="button" class="replace-chip" id="wall-replace-to-chip" aria-label="Pick wall replacement color"></button>
|
||||
<span id="wall-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||
|
||||
<div class="space-y-2">
|
||||
<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>
|
||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
||||
<button type="button" class="replace-chip" id="wall-replace-to-chip" aria-label="Pick wall replacement color"></button>
|
||||
<span id="wall-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<select id="wall-replace-from" 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>
|
||||
<div id="wall-replace-msg" class="text-xs text-gray-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<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-to" class="sr-only"></select>
|
||||
<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 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>
|
||||
|
||||
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/"
|
||||
160
wall.js
@ -18,7 +18,6 @@
|
||||
|
||||
let wallState = null;
|
||||
let selectedColorIdx = 0; // This should be synced with organic's selectedColorIdx
|
||||
let wallToolMode = 'paint';
|
||||
|
||||
// DOM elements
|
||||
const wallDisplay = document.getElementById('wall-display');
|
||||
@ -326,6 +325,18 @@
|
||||
return { mode: 'auto' };
|
||||
};
|
||||
|
||||
// Shared stroke helpers:
|
||||
// - Outline only when filled AND outline is enabled.
|
||||
// - Wireframe only when empty AND wireframes are enabled.
|
||||
const strokeFor = (isEmpty, { outline = '#111827', wire = '#cbd5e1' } = {}) => {
|
||||
if (isEmpty) return showWireframes ? wire : 'none';
|
||||
return showOutline ? outline : 'none';
|
||||
};
|
||||
const strokeWidthFor = (isEmpty, { outline = 0.6, wire = 1.4 } = {}) => {
|
||||
if (isEmpty) return showWireframes ? wire : 0;
|
||||
return showOutline ? outline : 0;
|
||||
};
|
||||
|
||||
// Helper to create a shine ellipse with coordinates relative to (0,0)
|
||||
const shineNodeRelative = (rx, ry, hex, rot = -20) => {
|
||||
const shine = shineStyle(hex || WALL_FALLBACK_COLOR);
|
||||
@ -351,13 +362,14 @@
|
||||
|
||||
const meta = wallColorMeta(customIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : (isEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? (showWireframes ? 1.4 : 0) : (showOutline ? 0.6 : 0));
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty);
|
||||
const filter = (isEmpty || invisible) ? '' : `filter="url(#${smallShadow})"`;
|
||||
const shine = isEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
|
||||
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
|
||||
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
|
||||
<circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -380,14 +392,14 @@
|
||||
|
||||
const meta = wallColorMeta(customIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
||||
console.log(`h-r-c: keyId: ${keyId}, customIdx: ${customIdx}, isEmpty: ${isEmpty}, invisible: ${invisible}, fill: ${fill}, meta:`, meta);
|
||||
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
|
||||
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y})">
|
||||
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y})">
|
||||
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -409,13 +421,14 @@
|
||||
|
||||
const meta = wallColorMeta(customIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty, { outline: '#111827', wire: '#cbd5e1' });
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
|
||||
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(90)">
|
||||
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(90)">
|
||||
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -438,13 +451,14 @@
|
||||
const invisible = isEmpty;
|
||||
const meta = wallColorMeta(gapIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex);
|
||||
const stroke = invisible || isEmpty ? 'none' : (showOutline ? '#111827' : 'none');
|
||||
const strokeW = invisible || isEmpty ? 0 : (showOutline ? 0.6 : 0);
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const rGap = bigR * 0.82; // slightly smaller 11" gap balloon
|
||||
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${gapKey}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${center.x},${center.y})">
|
||||
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${gapKey}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${center.x},${center.y})">
|
||||
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shineGap}
|
||||
</g>`);
|
||||
@ -468,13 +482,13 @@
|
||||
const meta = wallColorMeta(centerCustomIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? 'rgba(0,0,0,0.001)' : (centerIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
console.log(`c-r-c: keyId: ${centerKey}, customIdx: ${centerCustomIdx}, isEmpty: ${centerIsEmpty}, invisible: ${invisible}, fill: ${fill}, meta:`, meta);
|
||||
const stroke = invisible ? 'none' : (centerIsEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (centerIsEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const stroke = invisible ? 'none' : strokeFor(centerIsEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(centerIsEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const filter = centerIsEmpty || invisible ? '' : `filter="url(#${smallShadow})"`;
|
||||
const shine = centerIsEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
|
||||
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${centerKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${center.x},${center.y})">
|
||||
const displayIdxCenter = centerCustomIdx ?? -1;
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${centerKey}" data-wall-color="${displayIdxCenter}" style="cursor:pointer; pointer-events:all;" transform="translate(${center.x},${center.y})">
|
||||
<circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -492,19 +506,21 @@
|
||||
const linkCustomIdx = linkOverride.mode === 'color' ? linkOverride.idx : null;
|
||||
const linkIsEmpty = linkOverride.mode === 'empty' || linkCustomIdx === null;
|
||||
|
||||
const invisibleLink = linkIsEmpty && !showWireframes;
|
||||
|
||||
const meta = wallColorMeta(linkCustomIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisibleLink ? 'rgba(0,0,0,0.001)' : (linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
console.log(`l#-r-c: keyId: ${linkKey}, customIdx: ${linkCustomIdx}, isEmpty: ${linkIsEmpty}, invisible: ${invisibleLink}, fill: ${fill}, meta:`, meta);
|
||||
// Outline only when filled; light wireframe when empty and wireframes shown.
|
||||
const stroke = invisibleLink ? 'none' : (linkIsEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisibleLink ? 0 : (linkIsEmpty ? (showWireframes ? 1.2 : 0) : (showOutline ? 0.8 : 0));
|
||||
const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const fill = linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex);
|
||||
// Wireframe shows hit area when empty; outline shows only when filled and outline enabled.
|
||||
const stroke = linkIsEmpty
|
||||
? (showWireframes ? '#cbd5e1' : 'none')
|
||||
: (showOutline ? '#111827' : 'none');
|
||||
const strokeW = linkIsEmpty
|
||||
? (showWireframes ? 1.0 : 0)
|
||||
: (showOutline ? 0.9 : 0);
|
||||
const filter = linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
|
||||
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${linkKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(${angle})">
|
||||
const displayIdxLink = linkIsEmpty ? -1 : (linkCustomIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${linkKey}" data-wall-color="${displayIdxLink}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(${angle})">
|
||||
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -527,9 +543,9 @@
|
||||
const fillerInvisible = fillerEmpty && !showWireframes;
|
||||
const fillerMeta = wallColorMeta(fillerIdx);
|
||||
const fillerPat = ensurePattern(fillerMeta);
|
||||
const fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? (showWireframes ? 'none' : 'rgba(0,0,0,0.001)') : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
|
||||
const fillerStroke = fillerInvisible ? 'none' : (fillerEmpty ? (showWireframes ? '#cbd5e1' : 'none') : 'none');
|
||||
const fillerStrokeW = fillerInvisible ? 0 : (fillerEmpty ? (showWireframes ? 1.2 : 0) : 0);
|
||||
const fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? 'none' : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
|
||||
const fillerStroke = fillerInvisible ? 'none' : strokeFor(fillerEmpty);
|
||||
const fillerStrokeW = fillerInvisible ? 0 : strokeWidthFor(fillerEmpty, { outline: 0.6, wire: 1.2 });
|
||||
const fillerFilter = fillerInvisible || fillerEmpty ? '' : `filter="url(#${smallShadow})"`;
|
||||
const fillerShine = fillerEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, fillerMeta.hex);
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${fillerKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
|
||||
@ -553,12 +569,13 @@
|
||||
const patId = ensurePattern(meta);
|
||||
const invisible = isEmpty && !showGaps;
|
||||
const fill = invisible ? 'rgba(0,0,0,0.001)' : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const rGap = bigR * 0.82;
|
||||
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shineGap}
|
||||
</g>`);
|
||||
@ -584,7 +601,8 @@
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const rGap = bigR * 0.82;
|
||||
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shineGap}
|
||||
</g>`);
|
||||
@ -847,6 +865,17 @@
|
||||
return val;
|
||||
}
|
||||
|
||||
// Resolve current color (custom override only).
|
||||
function getCurrentColorIdxForKey(key) {
|
||||
if (!wallState) wallState = wallDefaultState();
|
||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||
const raw = wallState.customColors?.[key];
|
||||
const parsed = Number.isInteger(raw) ? raw : Number.parseInt(raw, 10);
|
||||
if (!Number.isInteger(parsed)) return null;
|
||||
if (parsed < 0) return null;
|
||||
return normalizeColorIdx(parsed);
|
||||
}
|
||||
|
||||
function updateWallActiveChip(idx) {
|
||||
if (!wallActiveChip || !wallActiveLabel) return;
|
||||
ensureFlatColors();
|
||||
@ -866,19 +895,6 @@
|
||||
wallActiveLabel.textContent = meta.name || meta.hex || '';
|
||||
}
|
||||
|
||||
function setWallToolMode(mode) {
|
||||
wallToolMode = mode === 'erase' ? 'erase' : 'paint';
|
||||
if (wallToolPaintBtn && wallToolEraseBtn) {
|
||||
const isErase = wallToolMode === 'erase';
|
||||
wallToolPaintBtn.setAttribute('aria-pressed', String(!isErase));
|
||||
wallToolEraseBtn.setAttribute('aria-pressed', String(isErase));
|
||||
wallToolPaintBtn.classList.toggle('tab-active', !isErase);
|
||||
wallToolEraseBtn.classList.toggle('tab-active', isErase);
|
||||
wallToolPaintBtn.classList.toggle('tab-idle', isErase);
|
||||
wallToolEraseBtn.classList.toggle('tab-idle', !isErase);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint a specific group of nodes with the active color.
|
||||
function paintWallGroup(group) {
|
||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||
@ -1022,7 +1038,29 @@
|
||||
else if (window.organic?.getColor) selectedColorIdx = normalizeColorIdx(window.organic.getColor());
|
||||
else selectedColorIdx = defaultActiveColorIdx();
|
||||
setActiveColor(selectedColorIdx);
|
||||
setWallToolMode('paint');
|
||||
// Hide legacy paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
|
||||
if (wallToolPaintBtn) {
|
||||
wallToolPaintBtn.classList.add('hidden');
|
||||
wallToolPaintBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolPaintBtn.tabIndex = -1;
|
||||
}
|
||||
if (wallToolEraseBtn) {
|
||||
wallToolEraseBtn.classList.add('hidden');
|
||||
wallToolEraseBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolEraseBtn.tabIndex = -1;
|
||||
}
|
||||
// Hide legacy paint/erase toggles; always use click-to-paint/click-again-to-clear.
|
||||
if (wallToolPaintBtn) {
|
||||
wallToolPaintBtn.classList.add('hidden');
|
||||
wallToolPaintBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolPaintBtn.tabIndex = -1;
|
||||
}
|
||||
if (wallToolEraseBtn) {
|
||||
wallToolEraseBtn.classList.add('hidden');
|
||||
wallToolEraseBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolEraseBtn.tabIndex = -1;
|
||||
}
|
||||
|
||||
// Allow picking active wall color by clicking the chip.
|
||||
if (wallActiveChip && window.openColorPicker) {
|
||||
wallActiveChip.style.cursor = 'pointer';
|
||||
@ -1093,8 +1131,7 @@
|
||||
wallReplaceToSel?.addEventListener('change', updateWallReplacePreview);
|
||||
wallReplaceFromChip?.addEventListener('click', () => openWallReplacePicker('from'));
|
||||
wallReplaceToChip?.addEventListener('click', () => openWallReplacePicker('to'));
|
||||
wallToolPaintBtn?.addEventListener('click', () => setWallToolMode('paint'));
|
||||
wallToolEraseBtn?.addEventListener('click', () => setWallToolMode('erase'));
|
||||
// Remove explicit paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
|
||||
|
||||
const findWallNode = (el) => {
|
||||
let cur = el;
|
||||
@ -1119,12 +1156,11 @@
|
||||
const key = hit.dataset.wallKey;
|
||||
if (!key) return;
|
||||
|
||||
const activeColor = getActiveWallColorIdx();
|
||||
const activeColor = normalizeColorIdx(getActiveWallColorIdx());
|
||||
if (!Number.isInteger(activeColor)) return;
|
||||
const rawStored = wallState.customColors?.[key];
|
||||
const parsedStored = Number.isInteger(rawStored) ? rawStored : Number.parseInt(rawStored, 10);
|
||||
const storedColor = Number.isInteger(parsedStored) && parsedStored >= 0 ? normalizeColorIdx(parsedStored) : null;
|
||||
const hasStoredColor = Number.isInteger(storedColor) && storedColor >= 0;
|
||||
const datasetColor = Number.parseInt(hit.dataset.wallColor ?? '', 10);
|
||||
const currentColor = Number.isInteger(datasetColor) ? datasetColor : getCurrentColorIdxForKey(key);
|
||||
const hasCurrent = Number.isInteger(currentColor) && currentColor >= 0;
|
||||
|
||||
if (e.altKey) {
|
||||
if (Number.isInteger(storedColor)) {
|
||||
@ -1135,9 +1171,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Paint/erase based on tool mode; modifiers still erase.
|
||||
const isEraseClick = wallToolMode === 'erase' || e.shiftKey || e.metaKey || e.ctrlKey;
|
||||
wallState.customColors[key] = isEraseClick ? -1 : activeColor;
|
||||
// Simple toggle: click paints with active; clicking again with the same active clears it.
|
||||
const sameAsActive = hasCurrent && currentColor === activeColor;
|
||||
wallState.customColors[key] = sameAsActive ? -1 : activeColor;
|
||||
|
||||
saveActivePatternState();
|
||||
saveWallState();
|
||||
|
||||