chore: snapshot v5 version
307
classic.js
@ -69,6 +69,21 @@
|
||||
// -------- 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 NUMBER_IMAGE_MAP = {
|
||||
'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;
|
||||
const SLOT_COUNT_KEY = 'classic:slotCount:v1';
|
||||
@ -118,6 +133,43 @@
|
||||
const clean = { hex: normHex(colorObj.hex), image: colorObj.image || null };
|
||||
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 0.5;
|
||||
}
|
||||
function setNumberTintOpacity(v) {
|
||||
const clamped = clamp01(parseFloat(v));
|
||||
try { localStorage.setItem(NUMBER_TINT_OPACITY_KEY, String(clamped)); } catch {}
|
||||
return clamped;
|
||||
}
|
||||
function getTopperTypeSafe() {
|
||||
try { return (window.ClassicDesigner?.lastTopperType) || null; } catch { return null; }
|
||||
}
|
||||
function loadClassicState() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(CLASSIC_STATE_KEY));
|
||||
if (saved && typeof saved === 'object') return saved;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function saveClassicState(state) {
|
||||
try { localStorage.setItem(CLASSIC_STATE_KEY, JSON.stringify(state || {})); } catch {}
|
||||
}
|
||||
|
||||
function buildClassicPalette() {
|
||||
const colors = getClassicColors();
|
||||
@ -157,6 +209,8 @@
|
||||
let topperOffsetX_Px = 0;
|
||||
let topperOffsetY_Px = 0;
|
||||
let topperSizeMultiplier = 1;
|
||||
let numberTintHex = getNumberTintColor();
|
||||
let numberTintOpacity = getNumberTintOpacity();
|
||||
let shineEnabled = true;
|
||||
let borderEnabled = false;
|
||||
|
||||
@ -172,6 +226,8 @@
|
||||
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; }
|
||||
};
|
||||
@ -198,8 +254,9 @@
|
||||
|
||||
function cellView(cell, id, explicitFill, model, colorInfo){
|
||||
const shape = cell.shape;
|
||||
const base = shape.base || {};
|
||||
const scale = cellScale(cell);
|
||||
const transform = [(shape.base.transform||''), `scale(${scale})`].join(' ');
|
||||
const transform = [(base.transform||''), `scale(${scale})`].join(' ');
|
||||
const commonAttrs = {
|
||||
'vector-effect': 'non-scaling-stroke',
|
||||
stroke: borderEnabled ? '#111827' : 'none',
|
||||
@ -214,12 +271,37 @@
|
||||
commonAttrs['data-quad-number'] = cell.y + 1;
|
||||
}
|
||||
|
||||
let shapeEl;
|
||||
if (shape.base.type === 'path') shapeEl = svg('path', { ...commonAttrs, d: shape.base.d });
|
||||
else shapeEl = svg('ellipse', { ...commonAttrs, cx:0, cy:0, rx:0.5, ry:0.5 });
|
||||
const kids = [];
|
||||
const fillRule = base.fillRule || base['fill-rule'] || null;
|
||||
if (base.image) {
|
||||
const w = base.width || 1, h = base.height || 1;
|
||||
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 && cell.isTopper && (model.topperType || '').startsWith('num-')) {
|
||||
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'
|
||||
}));
|
||||
}
|
||||
} else if (Array.isArray(base.paths)) {
|
||||
base.paths.forEach(p => {
|
||||
kids.push(svg('path', { ...commonAttrs, d: p.d, 'fill-rule': p.fillRule || fillRule || 'nonzero' }));
|
||||
});
|
||||
} else if (base.type === 'path' || base.d) {
|
||||
kids.push(svg('path', { ...commonAttrs, d: base.d, 'fill-rule': fillRule || 'nonzero' }));
|
||||
} else {
|
||||
kids.push(svg('ellipse', { ...commonAttrs, cx:0, cy:0, rx:0.5, ry:0.5 }));
|
||||
}
|
||||
|
||||
const kids = [shapeEl];
|
||||
const applyShine = model.shineEnabled && (!cell.isTopper || (cell.isTopper && model.topperType === 'round'));
|
||||
const allowShine = base.allowShine !== false;
|
||||
const applyShine = model.shineEnabled && (!cell.isTopper || allowShine);
|
||||
if (applyShine) {
|
||||
const shine = classicShineStyle(colorInfo);
|
||||
const shineAttrs = {
|
||||
@ -381,7 +463,11 @@ function distinctPaletteSlots(palette) {
|
||||
if (patterns['Column 5']) patterns['Column 5']._reverse = reverse;
|
||||
if (patterns['Arch 5']) patterns['Arch 5']._reverse = reverse;
|
||||
|
||||
const model = { patternName: name, pattern, cells: [], rowCount: clusters, palette: buildClassicPalette(), topperColor: getTopperColor(), topperType, shineEnabled };
|
||||
const model = {
|
||||
patternName: name, pattern, cells: [], rowCount: clusters, palette: buildClassicPalette(),
|
||||
topperColor: getTopperColor(), topperType, shineEnabled,
|
||||
numberTintHex, numberTintOpacity
|
||||
};
|
||||
const rows = pattern.cellsPerRow * model.rowCount, cols = pattern.cellsPerColumn;
|
||||
for (let y=0; y<rows; y++){
|
||||
let balloonIndexInCluster = 0;
|
||||
@ -419,12 +505,64 @@ function distinctPaletteSlots(palette) {
|
||||
return d + ' Z';
|
||||
}
|
||||
|
||||
function roundedRectPath(cx, cy, w, h, r = 0.08) {
|
||||
const x0 = cx - w / 2, x1 = cx + w / 2;
|
||||
const y0 = cy - h / 2, y1 = cy + h / 2;
|
||||
const rad = Math.min(r, w / 2, h / 2);
|
||||
return [
|
||||
`M ${x0 + rad} ${y0}`,
|
||||
`H ${x1 - rad}`,
|
||||
`Q ${x1} ${y0} ${x1} ${y0 + rad}`,
|
||||
`V ${y1 - rad}`,
|
||||
`Q ${x1} ${y1} ${x1 - rad} ${y1}`,
|
||||
`H ${x0 + rad}`,
|
||||
`Q ${x0} ${y1} ${x0} ${y1 - rad}`,
|
||||
`V ${y0 + rad}`,
|
||||
`Q ${x0} ${y0} ${x0 + rad} ${y0}`,
|
||||
'Z'
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
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' }
|
||||
};
|
||||
|
||||
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;
|
||||
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 },
|
||||
size: topperSize
|
||||
};
|
||||
});
|
||||
return shapes;
|
||||
}
|
||||
|
||||
const numberTopperShapes = buildNumberTopperShapes();
|
||||
|
||||
// --- Column 4: This is the existing logic from classic.js, which matches your template file ---
|
||||
patterns['Column 4'] = {
|
||||
baseBalloonSize: 25, _reverse: false, balloonsPerCluster: 4,
|
||||
balloonShapes: {
|
||||
'front':{zIndex:4, base:{radius:0.5}, size:3}, 'front-inner':{zIndex:3, base:{radius:0.5}, size:3}, 'back-inner':{zIndex:2, base:{radius:0.5}, size:3}, 'back':{zIndex:1, base:{radius:0.5}, size:3},
|
||||
'topper-round':{base:{type:'ellipse', radius:0.5}, size:8}, 'topper-star':{base:{type:'path', d:roundedStarPath({}), radius:0.5}, size:8}, 'topper-heart':{base:{type:'path', d:'M0,0.35 C-0.5,0, -0.14,-0.35, 0,-0.14 C0.14,-0.35, 0.5,0, 0,0.35 Z', radius:0.5}, size:20}
|
||||
'topper-round':{base:{type:'ellipse', radius:0.5, allowShine:true}, size:8}, 'topper-star':{base:{type:'path', d:roundedStarPath({}), radius:0.5, allowShine:false}, size:8}, 'topper-heart':{base:{type:'path', d:'M0,0.35 C-0.5,0, -0.14,-0.35, 0,-0.14 C0.14,-0.35, 0.5,0, 0,0.35 Z', radius:0.5, allowShine:false}, size:20},
|
||||
...numberTopperShapes
|
||||
},
|
||||
tile: { size:{x:5,y:1} }, cellsPerRow: 1, cellsPerColumn: 5,
|
||||
gridX(row, col){ return col + [0, -0.12, -0.24, -0.36, -0.48][col % 5]; },
|
||||
@ -465,9 +603,10 @@ function distinctPaletteSlots(palette) {
|
||||
"middle2": { zIndex:2, base:{radius:0.5}, size:3.0 },
|
||||
"back": { zIndex:1, base:{radius:0.5}, size:3.0 },
|
||||
"back2": { zIndex:0, base:{radius:0.5}, size:3.0 },
|
||||
'topper-round':{base:{type:'ellipse', radius:0.5}, size:8},
|
||||
'topper-star':{base:{type:'path', d:roundedStarPath({}), radius:0.5}, size:8},
|
||||
'topper-heart':{base:{type:'path', d:'M0,0.35 C-0.5,0, -0.14,-0.35, 0,-0.14 C0.14,-0.35, 0.5,0, 0,0.35 Z', radius:0.5}, size:20}
|
||||
'topper-round':{base:{type:'ellipse', radius:0.5, allowShine:true}, size:8},
|
||||
'topper-star':{base:{type:'path', d:roundedStarPath({}), radius:0.5, allowShine:false}, size:8},
|
||||
'topper-heart':{base:{type:'path', d:'M0,0.35 C-0.5,0, -0.14,-0.35, 0,-0.14 C0.14,-0.35, 0.5,0, 0,0.35 Z', radius:0.5, allowShine:false}, size:20},
|
||||
...numberTopperShapes
|
||||
},
|
||||
gridX(row, col) {
|
||||
var mid = 0.6;
|
||||
@ -527,6 +666,7 @@ 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');
|
||||
const numberTintSlider = document.getElementById('classic-number-tint');
|
||||
const topperBlock = document.getElementById('classic-topper-color-block');
|
||||
if (!slotsContainer || !topperSwatch || !swatchGrid || !activeLabel) return;
|
||||
topperSwatch.classList.add('tab-btn');
|
||||
@ -583,11 +723,22 @@ function distinctPaletteSlots(palette) {
|
||||
});
|
||||
|
||||
const topperColor = getTopperColor();
|
||||
topperSwatch.style.backgroundImage = topperColor.image ? `url("${topperColor.image}")` : 'none';
|
||||
topperSwatch.style.backgroundColor = topperColor.hex;
|
||||
topperSwatch.style.backgroundSize = '200%';
|
||||
topperSwatch.style.backgroundPosition = 'center';
|
||||
const topperTxt = textStyleForColor(topperColor);
|
||||
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 });
|
||||
topperSwatch.style.color = topperTxt.color;
|
||||
topperSwatch.style.textShadow = topperTxt.shadow;
|
||||
const patName = (document.getElementById('classic-pattern')?.value || '').toLowerCase();
|
||||
@ -625,8 +776,15 @@ function distinctPaletteSlots(palette) {
|
||||
|
||||
sw.addEventListener('click', () => {
|
||||
const selectedColor = { hex: colorItem.hex, image: colorItem.image };
|
||||
if (activeTarget === 'T') setTopperColor(selectedColor);
|
||||
else {
|
||||
if (activeTarget === 'T') {
|
||||
const currentType = document.querySelector('.topper-type-btn[aria-pressed="true"]')?.dataset.type || 'round';
|
||||
if (currentType.startsWith('num-')) {
|
||||
setNumberTintColor(selectedColor.hex);
|
||||
if (numberTintSlider) numberTintSlider.value = getNumberTintOpacity();
|
||||
} else {
|
||||
setTopperColor(selectedColor);
|
||||
}
|
||||
} else {
|
||||
const index = parseInt(activeTarget, 10) - 1;
|
||||
if (index >= 0 && index < MAX_SLOTS) { classicColors[index] = selectedColor; setClassicColors(classicColors); }
|
||||
}
|
||||
@ -672,6 +830,9 @@ function distinctPaletteSlots(palette) {
|
||||
try {
|
||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
||||
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled');
|
||||
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 patternShapeBtns = Array.from(document.querySelectorAll('[data-pattern-shape]'));
|
||||
const patternCountBtns = Array.from(document.querySelectorAll('[data-pattern-count]'));
|
||||
const patternLayoutBtns = Array.from(document.querySelectorAll('[data-pattern-layout]'));
|
||||
@ -680,13 +841,18 @@ function distinctPaletteSlots(palette) {
|
||||
const slotsContainer = document.getElementById('classic-slots');
|
||||
let topperOffsetX = 0, topperOffsetY = 0;
|
||||
let lastPresetKey = null; // 'custom' means user-tweaked; otherwise `${pattern}:${type}`
|
||||
window.ClassicDesigner = window.ClassicDesigner || {};
|
||||
window.ClassicDesigner.lastTopperType = window.ClassicDesigner.lastTopperType || 'round';
|
||||
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 },
|
||||
'Column 4:round': { enabled: true, offsetX: 3, offsetY: -2, size: 1.25 },
|
||||
'Column 4:number': { enabled: true, offsetX: 3, offsetY: -7, size: 1.05 },
|
||||
'Column 5:heart': { enabled: true, offsetX: 2, offsetY: -10, size: 1.15 },
|
||||
'Column 5:star': { enabled: true, offsetX: 2.5, offsetY: -7.5, size: 1.75 },
|
||||
'Column 5:round': { enabled: true, offsetX: 2.5, offsetY: -2, size: 1.3 }
|
||||
'Column 5:round': { enabled: true, offsetX: 2.5, offsetY: -2, size: 1.3 },
|
||||
'Column 5:number': { enabled: true, offsetX: 2.5, offsetY: -6.5, size: 1.05 }
|
||||
};
|
||||
if (!display) return fail('#classic-display not found');
|
||||
const GC = GridCalculator(), ctrl = GC.controller(display);
|
||||
@ -700,10 +866,31 @@ function distinctPaletteSlots(palette) {
|
||||
btn.classList.toggle('tab-active', active);
|
||||
btn.classList.toggle('tab-idle', !active);
|
||||
});
|
||||
window.ClassicDesigner.lastTopperType = type;
|
||||
};
|
||||
function applyNumberTopperTexture(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?.();
|
||||
}
|
||||
|
||||
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)) {
|
||||
setTopperColor({ hex: fallback.hex || '#ffffff', image: null });
|
||||
refreshClassicPaletteUi?.();
|
||||
}
|
||||
}
|
||||
|
||||
function applyTopperPreset(patternName, type) {
|
||||
const key = `${patternName}:${type}`;
|
||||
const presetType = (type || '').startsWith('num-') ? 'number' : type;
|
||||
const key = `${patternName}:${presetType}`;
|
||||
const preset = topperPresets[key];
|
||||
if (!preset) return;
|
||||
if (lastPresetKey === key || lastPresetKey === 'custom') return;
|
||||
@ -712,6 +899,8 @@ function distinctPaletteSlots(palette) {
|
||||
if (topperSizeInp) topperSizeInp.value = preset.size;
|
||||
if (topperEnabledCb) topperEnabledCb.checked = preset.enabled;
|
||||
setTopperType(type);
|
||||
applyNumberTopperTexture(type);
|
||||
resetNonNumberTopperColor(type);
|
||||
lastPresetKey = key;
|
||||
}
|
||||
|
||||
@ -740,6 +929,42 @@ function distinctPaletteSlots(palette) {
|
||||
setActive(patternLayoutBtns, 'patternLayout', patternLayout);
|
||||
};
|
||||
syncPatternStateFromSelect();
|
||||
|
||||
function persistState() {
|
||||
const state = {
|
||||
patternName: patSel?.value || '',
|
||||
length: lengthInp?.value || '',
|
||||
reverse: !!reverseCb?.checked,
|
||||
topperEnabled: !!topperEnabledCb?.checked,
|
||||
topperType: getTopperType(),
|
||||
topperOffsetX,
|
||||
topperOffsetY,
|
||||
topperSize: topperSizeInp?.value || '',
|
||||
numberTint: numberTintSlider ? numberTintSlider.value : getNumberTintOpacity()
|
||||
};
|
||||
saveClassicState(state);
|
||||
}
|
||||
|
||||
function applySavedState() {
|
||||
const saved = loadClassicState();
|
||||
if (!saved) return;
|
||||
if (patSel && saved.patternName) patSel.value = saved.patternName;
|
||||
if (lengthInp && saved.length) lengthInp.value = saved.length;
|
||||
if (reverseCb) reverseCb.checked = !!saved.reverse;
|
||||
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 (saved.topperType) setTopperType(saved.topperType);
|
||||
if (numberTintSlider && typeof saved.numberTint !== 'undefined') {
|
||||
numberTintSlider.value = saved.numberTint;
|
||||
setNumberTintOpacity(saved.numberTint);
|
||||
}
|
||||
syncPatternStateFromSelect();
|
||||
lastPresetKey = 'custom';
|
||||
}
|
||||
|
||||
applySavedState();
|
||||
applyPatternButtons();
|
||||
|
||||
function updateClassicDesign() {
|
||||
@ -755,13 +980,19 @@ function distinctPaletteSlots(palette) {
|
||||
}
|
||||
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));
|
||||
if (nudgeOpenBtn) nudgeOpenBtn.classList.toggle('hidden', !showTopper);
|
||||
|
||||
GC.setTopperEnabled(showTopper);
|
||||
GC.setClusters(Math.round((parseFloat(lengthInp.value) || 0) * 2));
|
||||
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);
|
||||
@ -775,6 +1006,7 @@ function distinctPaletteSlots(palette) {
|
||||
if(clusterHint) clusterHint.textContent = `≈ ${Math.round((parseFloat(lengthInp.value) || 0) * 2)} clusters (rule: 2 clusters/ft)`;
|
||||
refreshClassicPaletteUi?.();
|
||||
ctrl.selectPattern(patternName);
|
||||
persistState();
|
||||
}
|
||||
|
||||
const setLengthForPattern = () => {
|
||||
@ -812,9 +1044,38 @@ function distinctPaletteSlots(palette) {
|
||||
}));
|
||||
topperTypeButtons.forEach(btn => btn.addEventListener('click', () => {
|
||||
setTopperType(btn.dataset.type);
|
||||
applyNumberTopperTexture(btn.dataset.type);
|
||||
resetNonNumberTopperColor(btn.dataset.type);
|
||||
lastPresetKey = null;
|
||||
updateClassicDesign();
|
||||
}));
|
||||
numberTintSlider?.addEventListener('input', () => {
|
||||
GC.setNumberTintOpacity(numberTintSlider.value);
|
||||
updateClassicDesign();
|
||||
});
|
||||
nudgeOpenBtn?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.__showFloatingNudge?.();
|
||||
});
|
||||
const updateFullscreenLabel = () => {
|
||||
if (!fullscreenBtn) return;
|
||||
const active = !!document.fullscreenElement;
|
||||
fullscreenBtn.textContent = active ? 'Exit Fullscreen' : 'Fullscreen';
|
||||
};
|
||||
fullscreenBtn?.addEventListener('click', async () => {
|
||||
try {
|
||||
if (!document.fullscreenElement) {
|
||||
await document.documentElement.requestFullscreen({ navigationUI: 'hide' });
|
||||
} else {
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fullscreen toggle failed', err);
|
||||
} finally {
|
||||
updateFullscreenLabel();
|
||||
}
|
||||
});
|
||||
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(); }); });
|
||||
topperEnabledCb?.addEventListener('change', updateClassicDesign);
|
||||
@ -827,7 +1088,11 @@ function distinctPaletteSlots(palette) {
|
||||
});
|
||||
refreshClassicPaletteUi = initClassicColorPicker(updateClassicDesign);
|
||||
try { const saved = localStorage.getItem('app:shineEnabled:v1'); if (saved !== null && shineEnabledCb) shineEnabledCb.checked = JSON.parse(saved); } catch {}
|
||||
try { const saved = localStorage.getItem('classic:borderEnabled:v1'); if (saved !== null && borderEnabledCb) borderEnabledCb.checked = JSON.parse(saved); } catch {}
|
||||
try {
|
||||
const saved = localStorage.getItem('classic:borderEnabled:v1');
|
||||
if (saved !== null && borderEnabledCb) borderEnabledCb.checked = JSON.parse(saved);
|
||||
else if (borderEnabledCb) borderEnabledCb.checked = true; // default to outline on
|
||||
} catch { if (borderEnabledCb) borderEnabledCb.checked = true; }
|
||||
setLengthForPattern();
|
||||
updateClassicDesign();
|
||||
refreshClassicPaletteUi?.();
|
||||
|
||||
29
index.html
@ -45,6 +45,7 @@
|
||||
<span id="current-color-label-global" class="text-[10px] font-semibold text-slate-700"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button id="app-fullscreen-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Toggle fullscreen">Fullscreen</button>
|
||||
<button id="clear-canvas-btn-top" class="btn-danger text-xs px-3 py-2">Start Fresh</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -300,11 +301,37 @@
|
||||
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="star" aria-pressed="false"><i class="fa-solid fa-star"></i><span class="hidden sm:inline">Star</span></button>
|
||||
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="heart" aria-pressed="false"><i class="fa-solid fa-heart"></i><span class="hidden sm:inline">Heart</span></button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="text-xs font-semibold text-gray-600 mb-1">Numbers</div>
|
||||
<div class="topper-number-grid">
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-0" aria-pressed="false">0</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-1" aria-pressed="false">1</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-2" aria-pressed="false">2</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-3" aria-pressed="false">3</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-4" aria-pressed="false">4</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-5" aria-pressed="false">5</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-6" aria-pressed="false">6</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-7" aria-pressed="false">7</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-8" aria-pressed="false">8</button>
|
||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-9" aria-pressed="false">9</button>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
@ -372,7 +399,7 @@
|
||||
<div id="floating-topper-nudge" class="floating-nudge hidden">
|
||||
<div class="floating-nudge-header">
|
||||
<div class="panel-heading">Nudge Topper</div>
|
||||
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2">Hide</button>
|
||||
<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="grid grid-cols-3 gap-2">
|
||||
|
||||
BIN
output_webp/0.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
output_webp/1.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
output_webp/2.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
output_webp/3.webp
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
output_webp/4.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
output_webp/5.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
output_webp/6.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
output_webp/7.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
output_webp/8.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
output_webp/9.webp
Normal file
|
After Width: | Height: | Size: 95 KiB |
134
script.js
@ -186,7 +186,7 @@
|
||||
let currentDiameterInches = 11;
|
||||
let currentRadius = inchesToRadiusPx(currentDiameterInches);
|
||||
let isShineEnabled = true; // will be initialized from localStorage
|
||||
let isBorderEnabled = false;
|
||||
let isBorderEnabled = true;
|
||||
|
||||
let dpr = 1;
|
||||
let mode = 'draw';
|
||||
@ -1136,17 +1136,20 @@
|
||||
type: 'filler'
|
||||
});
|
||||
|
||||
// Accent cluster of three 5" balloons to add texture
|
||||
if (rng() < Math.min(0.8, 0.35 * garlandDensity + 0.1)) {
|
||||
const clusterCenterX = baseX + nx * r * 0.4 * side;
|
||||
const clusterCenterY = baseY + ny * r * 0.4 * side;
|
||||
// Tight cluster of three 5" balloons, slightly more open
|
||||
{
|
||||
const clusterCenterX = baseX + nx * r * 0.25 * side;
|
||||
const clusterCenterY = baseY + ny * r * 0.25 * side;
|
||||
const baseAng = rng() * Math.PI * 2;
|
||||
const mags = [0.7, 0.95, 0.8].map(m => m * accentRadius);
|
||||
const angs = [baseAng, baseAng + (2 * Math.PI / 3), baseAng + (4 * Math.PI / 3)];
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const ang = rng() * Math.PI * 2;
|
||||
const mag = accentRadius * (0.8 + rng() * 0.5);
|
||||
const mag = mags[c] * (0.98 + rng() * 0.08);
|
||||
const jitterAng = angs[c] + (rng() * 0.25 - 0.125);
|
||||
nodes.push({
|
||||
x: clusterCenterX + Math.cos(ang) * mag,
|
||||
y: clusterCenterY + Math.sin(ang) * mag,
|
||||
radius: accentRadius * (0.85 + rng() * 0.25),
|
||||
x: clusterCenterX + Math.cos(jitterAng) * mag,
|
||||
y: clusterCenterY + Math.sin(jitterAng) * mag,
|
||||
radius: accentRadius * (0.88 + rng() * 0.15),
|
||||
type: 'accent'
|
||||
});
|
||||
}
|
||||
@ -2156,19 +2159,75 @@
|
||||
}
|
||||
window.__setMobileTab = setMobileTab;
|
||||
|
||||
const NUDGE_POS_KEY = 'classic:nudgePos:v1';
|
||||
const NUDGE_MARGIN = 12;
|
||||
const NUDGE_SIZE_HINT = { w: 180, h: 200 };
|
||||
let floatingNudgeCollapsed = false;
|
||||
function clampNudgePos(pos, el) {
|
||||
const vw = window.innerWidth || 1024;
|
||||
const vh = window.innerHeight || 768;
|
||||
const rect = el?.getBoundingClientRect();
|
||||
const w = rect?.width || NUDGE_SIZE_HINT.w;
|
||||
const h = rect?.height || NUDGE_SIZE_HINT.h;
|
||||
return {
|
||||
x: Math.min(Math.max(pos.x, NUDGE_MARGIN), Math.max(NUDGE_MARGIN, vw - w - NUDGE_MARGIN)),
|
||||
y: Math.min(Math.max(pos.y, NUDGE_MARGIN), Math.max(NUDGE_MARGIN, vh - h - NUDGE_MARGIN))
|
||||
};
|
||||
}
|
||||
let nudgePos = null;
|
||||
let nudgePosInitialized = false;
|
||||
function loadNudgePos(el) {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(NUDGE_POS_KEY));
|
||||
if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') return clampNudgePos(saved, el);
|
||||
} catch {}
|
||||
return clampNudgePos({ x: (window.innerWidth || 1024) - 240, y: 120 }, el);
|
||||
}
|
||||
function ensureNudgePos(el) {
|
||||
if (!nudgePos) nudgePos = loadNudgePos(el);
|
||||
return clampNudgePos(nudgePos, el);
|
||||
}
|
||||
function saveNudgePos(pos, el) {
|
||||
nudgePos = clampNudgePos(pos, el);
|
||||
try { localStorage.setItem(NUDGE_POS_KEY, JSON.stringify(nudgePos)); } catch {}
|
||||
return nudgePos;
|
||||
}
|
||||
function applyNudgePos(el, pos) {
|
||||
const p = clampNudgePos(pos || ensureNudgePos(el), el);
|
||||
el.style.left = `${p.x}px`;
|
||||
el.style.top = `${p.y}px`;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
nudgePos = p;
|
||||
nudgePosInitialized = true;
|
||||
}
|
||||
function updateFloatingNudge() {
|
||||
const el = document.getElementById('floating-topper-nudge');
|
||||
if (!el) return;
|
||||
const classicActive = document.body?.dataset.activeTab === '#tab-classic';
|
||||
const topperActive = document.body?.dataset.topperOverlay === '1';
|
||||
const shouldShow = classicActive && topperActive;
|
||||
el.classList.toggle('hidden', !shouldShow);
|
||||
const shouldShowPanel = shouldShow && !floatingNudgeCollapsed;
|
||||
el.classList.toggle('hidden', !shouldShowPanel);
|
||||
el.classList.toggle('collapsed', floatingNudgeCollapsed);
|
||||
const toggle = document.getElementById('floating-nudge-toggle');
|
||||
if (toggle) toggle.textContent = floatingNudgeCollapsed ? 'Show' : 'Hide';
|
||||
el.style.display = shouldShowPanel ? 'block' : 'none';
|
||||
if (shouldShowPanel && !nudgePosInitialized) applyNudgePos(el, ensureNudgePos(el));
|
||||
}
|
||||
function showFloatingNudge() {
|
||||
floatingNudgeCollapsed = false;
|
||||
updateFloatingNudge();
|
||||
}
|
||||
function hideFloatingNudge() {
|
||||
floatingNudgeCollapsed = true;
|
||||
const nudge = document.getElementById('floating-topper-nudge');
|
||||
if (nudge) { nudge.classList.add('hidden'); nudge.style.display = 'none'; }
|
||||
const tab = document.getElementById('floating-nudge-tab');
|
||||
if (tab) tab.classList.remove('hidden');
|
||||
updateFloatingNudge();
|
||||
}
|
||||
window.__updateFloatingNudge = updateFloatingNudge;
|
||||
window.__showFloatingNudge = showFloatingNudge;
|
||||
window.__hideFloatingNudge = hideFloatingNudge;
|
||||
|
||||
if (orgSection && claSection && tabBtns.length > 0) {
|
||||
let current = '#tab-organic';
|
||||
@ -2200,6 +2259,11 @@
|
||||
}
|
||||
|
||||
if (document.body) delete document.body.dataset.controlsHidden;
|
||||
// Toggle header controls based on tab
|
||||
const isOrganic = id === '#tab-organic';
|
||||
document.getElementById('clear-canvas-btn-top')?.classList.toggle('hidden', !isOrganic);
|
||||
const headerActiveSwatch = document.getElementById('current-color-chip-global')?.closest('.flex');
|
||||
headerActiveSwatch?.classList.toggle('hidden', !isOrganic);
|
||||
setMobileTab(document.body?.dataset?.mobileTab || 'controls');
|
||||
orgSheet?.classList.toggle('hidden', id !== '#tab-organic');
|
||||
claSheet?.classList.toggle('hidden', id !== '#tab-classic');
|
||||
@ -2270,9 +2334,49 @@
|
||||
|
||||
const nudgeToggle = document.getElementById('floating-nudge-toggle');
|
||||
nudgeToggle?.addEventListener('click', () => {
|
||||
floatingNudgeCollapsed = !floatingNudgeCollapsed;
|
||||
updateFloatingNudge();
|
||||
hideFloatingNudge();
|
||||
});
|
||||
|
||||
// Dragging for floating nudge
|
||||
const nudge = document.getElementById('floating-topper-nudge');
|
||||
const nudgeHeader = document.querySelector('#floating-topper-nudge .floating-nudge-header');
|
||||
if (nudge && nudgeHeader) {
|
||||
let dragId = null;
|
||||
let start = null;
|
||||
nudgeHeader.addEventListener('pointerdown', (e) => {
|
||||
if (e.target.closest('#floating-nudge-toggle')) return; // don't start drag when clicking close
|
||||
dragId = e.pointerId;
|
||||
start = { x: e.clientX, y: e.clientY, pos: loadNudgePos(nudge) };
|
||||
nudge.classList.add('dragging');
|
||||
nudgeHeader.setPointerCapture(dragId);
|
||||
});
|
||||
nudgeHeader.addEventListener('pointermove', (e) => {
|
||||
if (dragId === null || e.pointerId !== dragId) return;
|
||||
if (!start) return;
|
||||
const dx = e.clientX - start.x;
|
||||
const dy = e.clientY - start.y;
|
||||
const next = clampNudgePos({ x: start.pos.x + dx, y: start.pos.y + dy });
|
||||
applyNudgePos(nudge, next);
|
||||
});
|
||||
const endDrag = (e) => {
|
||||
if (dragId === null || e.pointerId !== dragId) return;
|
||||
const rect = nudge.getBoundingClientRect();
|
||||
const next = clampNudgePos({ x: rect.left, y: rect.top }, nudge);
|
||||
saveNudgePos(next, nudge);
|
||||
nudge.classList.remove('dragging');
|
||||
dragId = null;
|
||||
start = null;
|
||||
try { nudgeHeader.releasePointerCapture(e.pointerId); } catch {}
|
||||
};
|
||||
nudgeHeader.addEventListener('pointerup', endDrag);
|
||||
nudgeHeader.addEventListener('pointercancel', endDrag);
|
||||
window.addEventListener('resize', () => {
|
||||
const pos = clampNudgePos(loadNudgePos(nudge), nudge);
|
||||
saveNudgePos(pos, nudge);
|
||||
nudgePosInitialized = false;
|
||||
applyNudgePos(nudge, pos);
|
||||
});
|
||||
}
|
||||
})();
|
||||
});
|
||||
})();
|
||||
|
||||
59
style.css
@ -200,6 +200,31 @@ body { color: #1f2937; }
|
||||
gap: .4rem;
|
||||
}
|
||||
.topper-type-btn i { font-size: 1rem; }
|
||||
.topper-number-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.topper-number-btn {
|
||||
padding: 0.4rem 0.5rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.number-tint-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
background: rgba(255,255,255,0.7);
|
||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-radius: 0.85rem;
|
||||
}
|
||||
.number-tint-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.floating-nudge {
|
||||
position: fixed;
|
||||
@ -208,21 +233,31 @@ body { color: #1f2937; }
|
||||
background: rgba(255,255,255,0.95);
|
||||
border: 1px solid rgba(148,163,184,0.3);
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem;
|
||||
padding: 0.55rem;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
|
||||
width: 210px;
|
||||
width: 180px;
|
||||
max-width: 85vw;
|
||||
z-index: 35;
|
||||
touch-action: none;
|
||||
}
|
||||
.floating-nudge-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .5rem;
|
||||
margin-bottom: .35rem;
|
||||
gap: .35rem;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.floating-nudge-body.collapsed { display: none; }
|
||||
.floating-nudge.collapsed .floating-nudge-body { display: none; }
|
||||
.floating-nudge.collapsed #floating-nudge-toggle { opacity: 0.8; }
|
||||
.floating-nudge.dragging { cursor: grabbing; }
|
||||
.floating-nudge .panel-heading { font-size: 0.95rem; margin-bottom: 0; }
|
||||
.floating-nudge .btn-dark { padding: 0.35rem 0.5rem; font-size: 0.8rem; }
|
||||
.floating-nudge-tab { display: none; }
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.floating-nudge { bottom: 6rem; left: auto; right: 0.9rem; }
|
||||
}
|
||||
|
||||
.slot-label {
|
||||
font-weight: 600;
|
||||
@ -301,8 +336,8 @@ body { color: #1f2937; }
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
body { padding-bottom: 88px; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
body { padding-bottom: 0; overflow: auto; }
|
||||
html, body { height: auto; overflow: auto; }
|
||||
|
||||
.control-sheet .control-stack { display: none; }
|
||||
body[data-mobile-tab="controls"] #controls-panel [data-mobile-tab="controls"],
|
||||
@ -380,3 +415,15 @@ body { color: #1f2937; }
|
||||
}
|
||||
body { padding-bottom: 0; overflow: auto; }
|
||||
}
|
||||
|
||||
/* Compact viewport fallback */
|
||||
@media (max-height: 760px) {
|
||||
body { overflow: auto; }
|
||||
.control-sheet {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
.floating-nudge {
|
||||
bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||