column 5 and arch 5 complete, reverse broken
571
classic.js
Normal file
@ -0,0 +1,571 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
// -------- helpers ----------
|
||||
const log = (...a) => console.log('[Classic]', ...a);
|
||||
const fail = (msg) => {
|
||||
console.error('[Classic ERROR]', msg);
|
||||
const d = document.getElementById('classic-display');
|
||||
if (d) d.innerHTML = `<div style="padding:1rem;color:#b91c1c;font-family:system-ui,Arial">
|
||||
<strong>Classic failed:</strong> ${String(msg)}
|
||||
</div>`;
|
||||
};
|
||||
const normHex = (h) => (String(h || '')).trim().toLowerCase();
|
||||
|
||||
// -------- persistent color selection (now supports image textures) ----------
|
||||
const PALETTE_KEY = 'classic:colors:v2';
|
||||
const TOPPER_COLOR_KEY = 'classic:topperColor:v2';
|
||||
|
||||
const defaultColors = () => [
|
||||
{ hex: '#d92e3a', image: null }, { hex: '#ffffff', image: null },
|
||||
{ hex: '#0055a4', image: null }, { hex: '#40e0d0', image: null },
|
||||
{ hex: '#fcd34d', image: null }
|
||||
];
|
||||
const defaultTopper = () => ({ hex: '#a18b67', image: 'images/chrome-gold.webp' });
|
||||
|
||||
function getClassicColors() {
|
||||
let arr = defaultColors();
|
||||
try {
|
||||
const savedJSON = localStorage.getItem(PALETTE_KEY);
|
||||
if (!savedJSON) return arr;
|
||||
const saved = JSON.parse(savedJSON);
|
||||
if (Array.isArray(saved) && saved.length > 0) {
|
||||
if (typeof saved[0] === 'string') {
|
||||
arr = saved.slice(0, 5).map(hex => ({ hex: normHex(hex), image: null }));
|
||||
} else if (typeof saved[0] === 'object' && saved[0] !== null) {
|
||||
arr = saved.slice(0, 5);
|
||||
}
|
||||
while (arr.length < 5) arr.push({ hex: '#ffffff', image: null });
|
||||
}
|
||||
} catch (e) { console.error('Failed to parse classic colors:', e); }
|
||||
return arr;
|
||||
}
|
||||
|
||||
function setClassicColors(arr) {
|
||||
const clean = (arr || []).slice(0, 5).map(c => ({
|
||||
hex: normHex(c.hex), image: c.image || null
|
||||
}));
|
||||
while (clean.length < 5) clean.push({ hex: '#ffffff', image: null });
|
||||
try { localStorage.setItem(PALETTE_KEY, JSON.stringify(clean)); } catch {}
|
||||
return clean;
|
||||
}
|
||||
|
||||
function getTopperColor() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(TOPPER_COLOR_KEY));
|
||||
return (saved && saved.hex) ? saved : defaultTopper();
|
||||
} catch { return defaultTopper(); }
|
||||
}
|
||||
|
||||
function setTopperColor(colorObj) {
|
||||
const clean = { hex: normHex(colorObj.hex), image: colorObj.image || null };
|
||||
try { localStorage.setItem(TOPPER_COLOR_KEY, JSON.stringify(clean)); } catch {}
|
||||
}
|
||||
|
||||
function buildClassicPalette() {
|
||||
const colors = getClassicColors();
|
||||
const palette = { 0: { colour: '#FFFFFF', name: 'No Colour', image: null } };
|
||||
colors.forEach((c, i) => {
|
||||
palette[i + 1] = { colour: c.hex, image: c.image };
|
||||
});
|
||||
return palette;
|
||||
}
|
||||
|
||||
function flattenPalette() {
|
||||
const out = [];
|
||||
if (Array.isArray(window.PALETTE)) {
|
||||
window.PALETTE.forEach(group => {
|
||||
(group.colors || []).forEach(c => {
|
||||
if (!c?.hex) return;
|
||||
out.push({
|
||||
hex: normHex(c.hex), name: c.name || c.hex,
|
||||
family: group.family || '', image: c.image || null
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
const seen = new Set();
|
||||
return out.filter(c => (seen.has(c.hex) ? false : (seen.add(c.hex), true)));
|
||||
}
|
||||
|
||||
// -------- tiny grid engine (Mithril) ----------
|
||||
function GridCalculator() {
|
||||
if (typeof window.m === 'undefined') throw new Error('Mithril (m) not loaded');
|
||||
|
||||
let pxUnit = 10;
|
||||
let clusters = 10;
|
||||
let reverse = false;
|
||||
let topperEnabled = false;
|
||||
let topperType = 'round';
|
||||
let topperOffsetX_Px = 0;
|
||||
let topperOffsetY_Px = 0;
|
||||
let topperSizeMultiplier = 1;
|
||||
let shineEnabled = true;
|
||||
|
||||
const patterns = {};
|
||||
const api = {
|
||||
patterns,
|
||||
initialPattern: 'Arch 4',
|
||||
controller: (el) => makeController(el),
|
||||
setClusters(n) { clusters = Math.max(1, (Number(n)|0) || 10); },
|
||||
setReverse(on){ reverse = !!on; },
|
||||
setTopperEnabled(on) { topperEnabled = !!on; },
|
||||
setTopperType(type) { topperType = type || 'round'; },
|
||||
setTopperOffsetX(val) { topperOffsetX_Px = (Number(val) || 0) * 5; },
|
||||
setTopperOffsetY(val) { topperOffsetY_Px = (Number(val) || 0) * -5; },
|
||||
setTopperSize(multiplier) { topperSizeMultiplier = Number(multiplier) || 1; },
|
||||
setShineEnabled(on) { shineEnabled = !!on; }
|
||||
};
|
||||
|
||||
const svg = (tag, attrs, children) => m(tag, attrs, children);
|
||||
|
||||
function extend(p){
|
||||
const parentName = p.deriveFrom; if (!parentName) return;
|
||||
const base = patterns[parentName]; if (!base) return;
|
||||
if (base.deriveFrom) extend(base);
|
||||
Object.keys(base).forEach(k => { if (!(k in p)) p[k] = base[k]; });
|
||||
p.parent = base;
|
||||
}
|
||||
|
||||
function BBox(){ this.min={x:Infinity,y:Infinity}; this.max={x:-Infinity,y:-Infinity}; }
|
||||
BBox.prototype.add = function(x,y){ if(isNaN(x)||isNaN(y)) return this;
|
||||
this.min.x=Math.min(this.min.x,x); this.min.y=Math.min(this.min.y,y);
|
||||
this.max.x=Math.max(this.max.x,x); this.max.y=Math.max(this.max.y,y); return this; };
|
||||
BBox.prototype.w=function(){return this.max.x-this.min.x;};
|
||||
BBox.prototype.h=function(){return this.max.y-this.min.y;};
|
||||
|
||||
const balloonSize = (cell)=> (cell.shape.size ?? 1);
|
||||
const cellScale = (cell)=> balloonSize(cell) * pxUnit;
|
||||
|
||||
function cellView(cell, id, explicitFill, model){
|
||||
const shape = cell.shape;
|
||||
const scale = cellScale(cell);
|
||||
const transform = [(shape.base.transform||''), `scale(${scale})`].join(' ');
|
||||
const commonAttrs = {
|
||||
'vector-effect': 'non-scaling-stroke', stroke: '#111827',
|
||||
'stroke-width': 2, 'paint-order': 'stroke fill', class: 'balloon',
|
||||
fill: explicitFill || '#cccccc'
|
||||
};
|
||||
if (cell.isTopper) {
|
||||
commonAttrs['data-is-topper'] = true;
|
||||
} else {
|
||||
commonAttrs['data-color-code'] = cell.colorCode || 0;
|
||||
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 = [shapeEl];
|
||||
const applyShine = model.shineEnabled && (!cell.isTopper || (cell.isTopper && model.topperType === 'round'));
|
||||
if (applyShine) {
|
||||
kids.push(svg('ellipse', {
|
||||
class: 'shine', cx: -0.15, cy: -0.15, rx: 0.22, ry: 0.13,
|
||||
fill: '#ffffff', opacity: 0.45, transform: 'rotate(-25)', 'pointer-events': 'none'
|
||||
}));
|
||||
}
|
||||
return svg('g', { id, transform }, kids);
|
||||
}
|
||||
|
||||
function gridPos(x,y,z,inflate,pattern,model){
|
||||
const base = patterns[model.patternName].parent || patterns[model.patternName];
|
||||
const rel = (pattern.baseBalloonSize && base.baseBalloonSize) ? pattern.baseBalloonSize/base.baseBalloonSize : 1;
|
||||
let p = { x: pattern.gridX(model.pattern.cellsPerRow > 1 ? y : x, x), y: pattern.gridY(y,x) };
|
||||
if (pattern.transform) p = pattern.transform(p,x,y,model);
|
||||
return { x: p.x * rel * pxUnit, y: p.y * rel * pxUnit };
|
||||
}
|
||||
|
||||
// === Spiral coloring helpers (shared by 4- and 5-balloon clusters) ===
|
||||
function distinctPaletteSlots(palette) {
|
||||
// Collapse visually identical slots so 3-color spirals work even if you filled 5 slots.
|
||||
const seen = new Set(), out = [];
|
||||
for (let s = 1; s <= 5; s++) {
|
||||
const c = palette[s];
|
||||
if (!c) continue;
|
||||
const key = (c.image || '') + '|' + String(c.colour || '').toLowerCase();
|
||||
if (!seen.has(key)) { seen.add(key); out.push(s); }
|
||||
}
|
||||
return out.length ? out : [1,2,3,4,5];
|
||||
}
|
||||
|
||||
|
||||
|
||||
function newGrid(pattern, cells, container, model){
|
||||
const kids = [], layers = [], bbox = new BBox();
|
||||
const balloonsPerCluster = pattern.balloonsPerCluster || 4;
|
||||
const reversed = !!(pattern._reverse || (pattern.parent && pattern.parent._reverse));
|
||||
const rowColorPatterns = {};
|
||||
|
||||
const colorBlock4 = [[1, 2, 3, 4], [3, 1, 4, 2], [4, 3, 2, 1], [2, 4, 1, 3]];
|
||||
const colorBlock5 =
|
||||
[
|
||||
[5, 2, 3, 4, 1],
|
||||
[2, 3, 4, 5, 1],
|
||||
[2, 4, 5, 1, 3],
|
||||
[4, 5, 1, 2, 3],
|
||||
[4, 1, 2, 3, 5],
|
||||
];
|
||||
|
||||
for (let cell of cells) {
|
||||
let c, fill;
|
||||
if (cell.isTopper) {
|
||||
const topRowYIndex = 0, topClusterY = pattern.gridY(topRowYIndex, 0) * pxUnit;
|
||||
const regularBalloonRadius = (pattern.balloonShapes['front'] || pattern.balloonShapes['penta'] || pattern.balloonShapes['middle']).size * pxUnit * 0.5;
|
||||
const highestPoint = topClusterY - regularBalloonRadius;
|
||||
const topperRadius = cell.shape.size * pxUnit * cell.shape.base.radius;
|
||||
const topperY = highestPoint - topperRadius - (pxUnit * 0.5) + topperOffsetY_Px;
|
||||
c = { x: topperOffsetX_Px, y: topperY };
|
||||
fill = model.topperColor.image ? `url(#classic-pattern-topper)` : model.topperColor.hex;
|
||||
} else {
|
||||
c = gridPos(cell.x, cell.y, cell.shape.zIndex, cell.inflate, pattern, model);
|
||||
|
||||
const rowIndex = cell.y;
|
||||
if (!rowColorPatterns[rowIndex]) {
|
||||
const qEff = rowIndex + 1;
|
||||
let pat;
|
||||
|
||||
if (balloonsPerCluster === 5) {
|
||||
const base = (qEff - 1) % 5;
|
||||
pat = colorBlock5[base].slice();
|
||||
} else {
|
||||
const base = Math.floor((qEff - 1) / 2);
|
||||
pat = colorBlock4[base % 4].slice();
|
||||
if (qEff % 2 === 0) {
|
||||
pat = [pat[0], pat[2], pat[1], pat[3]];
|
||||
}
|
||||
}
|
||||
|
||||
if (reversed && pat.length > 1) {
|
||||
const first = pat.shift();
|
||||
pat.reverse();
|
||||
pat.unshift(first);
|
||||
}
|
||||
|
||||
// --- NEW: swap left/right after every 5 clusters ---
|
||||
const SWAP_EVERY = 5; // clusters per block
|
||||
const blockIndex = Math.floor(rowIndex / SWAP_EVERY);
|
||||
|
||||
// swap on blocks #2, #4, #6, ... (i.e., rows 6–10, 16–20, ...)
|
||||
if (blockIndex % 2 === 1) {
|
||||
if (balloonsPerCluster === 5) {
|
||||
// [leftMid, leftBack, front, rightBack, rightMid]
|
||||
[pat[0], pat[4]] = [pat[4], pat[0]];
|
||||
// [pat[1], pat[3]] = [pat[3], pat[1]];
|
||||
}
|
||||
}
|
||||
rowColorPatterns[rowIndex] = pat;
|
||||
}
|
||||
|
||||
const colorCode = rowColorPatterns[rowIndex][cell.balloonIndexInCluster];
|
||||
cell.colorCode = colorCode;
|
||||
const colorInfo = model.palette[colorCode];
|
||||
fill = colorInfo ? (colorInfo.image ? `url(#classic-pattern-slot-${colorCode})` : colorInfo.colour) : 'transparent';
|
||||
}
|
||||
|
||||
const scale = cellScale(cell), shapeRadius = cell.shape.base.radius || 0.5, size = shapeRadius * scale;
|
||||
bbox.add(c.x - size, c.y - size);
|
||||
bbox.add(c.x + size, c.y + size);
|
||||
const v = cellView(cell, `balloon_${cell.x}_${cell.y}`, fill, model);
|
||||
v.attrs.transform = `translate(${c.x},${c.y}) ${v.attrs.transform || ''}`;
|
||||
const zi = cell.isTopper ? 100 + 2 : (100 + (cell.shape.zIndex || 0));
|
||||
(layers[zi] ||= []).push(v);
|
||||
};
|
||||
|
||||
layers.forEach(layer => layer && layer.forEach(v => kids.push(v)));
|
||||
const margin = 20;
|
||||
const vb = [ bbox.min.x - margin, bbox.min.y - margin, Math.max(1,bbox.w()) + margin*2, Math.max(1,bbox.h()) + margin*2 ].join(' ');
|
||||
|
||||
const patternsDefs = [];
|
||||
const SVG_PATTERN_ZOOM = 2.5;
|
||||
const offset = (1 - SVG_PATTERN_ZOOM) / 2;
|
||||
|
||||
Object.entries(model.palette).forEach(([slot, colorInfo]) => {
|
||||
if (colorInfo.image) {
|
||||
patternsDefs.push(svg('pattern', {id: `classic-pattern-slot-${slot}`, patternContentUnits: 'objectBoundingBox', width: 1, height: 1},
|
||||
[ svg('image', { href: colorInfo.image, x: offset, y: offset, width: SVG_PATTERN_ZOOM, height: SVG_PATTERN_ZOOM, preserveAspectRatio: 'xMidYMid slice' }) ]
|
||||
));
|
||||
}
|
||||
});
|
||||
if (model.topperColor.image) {
|
||||
patternsDefs.push(svg('pattern', {id: 'classic-pattern-topper', patternContentUnits: 'objectBoundingBox', width: 1, height: 1},
|
||||
[ 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);
|
||||
|
||||
const mainGroup = svg('g', null, kids);
|
||||
m.render(container, svg('svg', { xmlns: 'http://www.w3.org/2000/svg', width:'100%', height:'100%', viewBox: vb, preserveAspectRatio:'xMidYMid meet', style: 'isolation:isolate' }, [svgDefs, mainGroup]));
|
||||
}
|
||||
|
||||
function makeController(displayEl){
|
||||
const models = [];
|
||||
function buildModel(name){
|
||||
const pattern = patterns[name];
|
||||
if (patterns['Column 4']) patterns['Column 4']._reverse = reverse;
|
||||
if (patterns['Arch 4']) patterns['Arch 4']._reverse = reverse;
|
||||
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 rows = pattern.cellsPerRow * model.rowCount, cols = pattern.cellsPerColumn;
|
||||
for (let y=0; y<rows; y++){
|
||||
let balloonIndexInCluster = 0;
|
||||
for (let x=0; x<cols; x++) {
|
||||
const cellData = pattern.createCell(x,y);
|
||||
if (cellData) model.cells.push({ ...cellData, x, y, balloonIndexInCluster: balloonIndexInCluster++ });
|
||||
}
|
||||
}
|
||||
if (name === 'Column 4' && topperEnabled) {
|
||||
const shapeName = `topper-${topperType}`;
|
||||
const originalShape = pattern.balloonShapes[shapeName];
|
||||
if (originalShape) {
|
||||
const shape = {...originalShape};
|
||||
shape.size *= topperSizeMultiplier;
|
||||
model.cells.push({ isTopper: true, shape, inflate: 0, x:0, y:rows });
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
function selectPattern(name){
|
||||
const m = buildModel(name); models.push(m);
|
||||
newGrid(m.pattern, m.cells, displayEl, m); return m;
|
||||
}
|
||||
return { selectPattern };
|
||||
}
|
||||
|
||||
function roundedStarPath({ points = 5, outerR = 0.5, innerR = 0.22, round = 0.28, rotate = -90 }) {
|
||||
const toRad = Math.PI / 180; const rot = rotate * toRad; const verts = [];
|
||||
for (let i = 0; i < points * 2; i++) { const ang = rot + i * Math.PI / points; const R = (i % 2 === 0) ? outerR : innerR; verts.push([Math.cos(ang) * R, Math.sin(ang) * R]); }
|
||||
const t = Math.max(0, Math.min(0.49, round));
|
||||
const lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
|
||||
let v0 = verts[0], v1 = verts[1]; let p0 = lerp(v0, v1, t);
|
||||
let d = `M ${p0[0].toFixed(4)} ${p0[1].toFixed(4)}`;
|
||||
for (let i = 0; i < verts.length; i++) { const v = verts[(i + 1) % verts.length], vNext = verts[(i + 2) % verts.length]; const p = lerp(v, vNext, t); d += ` Q ${v[0].toFixed(4)} ${v[1].toFixed(4)} ${p[0].toFixed(4)} ${p[1].toFixed(4)}`; }
|
||||
return d + ' Z';
|
||||
}
|
||||
|
||||
// --- 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}
|
||||
},
|
||||
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]; },
|
||||
gridY(row, col){ return 2.2 * (1 - 1/5) * (Math.floor(row/2) + Math.floor((row+1)/2)); },
|
||||
createCell(x, y) {
|
||||
const odd = !!(y % 2);
|
||||
const A = ['front-inner','back','', 'front','back-inner'], B = ['back-inner', 'front','', 'back', 'front-inner'];
|
||||
const arr = this._reverse ? (odd ? B : A) : (odd ? A : B);
|
||||
const shapeName = arr[x % 5];
|
||||
const shape = this.balloonShapes[shapeName];
|
||||
return shape ? { shape:{...shape} } : null;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Arch 4: This is the existing logic from classic.js, which matches your template file ---
|
||||
patterns['Arch 4'] = {
|
||||
deriveFrom: 'Column 4',
|
||||
transform(point, col, row, model){
|
||||
const len = this.gridY(model.rowCount*this.tile.size.y, 0) - this.gridY(0, 0);
|
||||
const r = (len / Math.PI) + point.x;
|
||||
const y = point.y - this.gridY(0, 0);
|
||||
const a = Math.PI * (y / len);
|
||||
return { x: -r*Math.cos(a), y: -r*Math.sin(a) };
|
||||
}
|
||||
};
|
||||
|
||||
// --- START: MODIFIED SECTION ---
|
||||
// This is the new 'Column 5' definition, adapted from your template file.
|
||||
patterns['Column 5'] = {
|
||||
baseBalloonSize: 25,
|
||||
_reverse: false,
|
||||
balloonsPerCluster: 5, // Kept this from classic.js to ensure 5-color spiral
|
||||
tile: { size: { x: 5, y: 1 } },
|
||||
cellsPerRow: 1,
|
||||
cellsPerColumn: 5,
|
||||
|
||||
// Balloon shapes from your template, converted to classic.js format
|
||||
// (type: "qlink" is approx size: 3.0)
|
||||
balloonShapes: {
|
||||
"front": { zIndex:5, base:{radius:0.5}, size:3.0 },
|
||||
"front2": { zIndex:4, base:{radius:0.5}, size:3.0 },
|
||||
"middle": { zIndex:3, base:{radius:0.5}, size:3.0 },
|
||||
"middle2": { zIndex:2, base:{radius:0.5}, size:3.0 },
|
||||
"back": { zIndex:1, base:{radius:0.5}, size:3.0 },
|
||||
"back2": { zIndex:0, base:{radius:0.5}, size:3.0 }
|
||||
},
|
||||
|
||||
// gridX function from your template
|
||||
// (I've hard-coded `this.exploded` to false, as it's not in classic.js)
|
||||
gridX(row, col) {
|
||||
var mid = 0.6; // this.exploded ? 0.2 : 0.6
|
||||
return (0.9) * (col + (0 === col % 5 && -0.5) + (1 === col % 5 && -mid) + (3 === col % 5 && mid) + (4 === col % 5 && 0.5) - 0.5);
|
||||
},
|
||||
|
||||
// gridY function is inherited from Column 4 via `deriveFrom` in your template.
|
||||
// So, we use the gridY function from this file's 'Column 4'.
|
||||
gridY(row, col){
|
||||
return 2.2 * (1 - 1/5) * (Math.floor(row/2) + Math.floor((row+1)/2));
|
||||
},
|
||||
|
||||
// createCell function from your template, adapted for classic.js
|
||||
createCell(x, y) {
|
||||
var yOdd = !!(y % 2);
|
||||
|
||||
// Re-created logic from template's createCell
|
||||
const shapePattern = yOdd ?
|
||||
['middle', 'back', 'front', 'back', 'middle'] :
|
||||
['middle2', 'front2', 'back2', 'front2', 'middle2'];
|
||||
|
||||
var shapeName = shapePattern[x % 5];
|
||||
var shape = this.balloonShapes[shapeName];
|
||||
|
||||
// Return in classic.js format
|
||||
return shape ? { shape: {...shape} } : null;
|
||||
}
|
||||
};
|
||||
|
||||
// This is the new 'Arch 5' definition.
|
||||
// It derives from the new 'Column 5' and uses the same arching logic as 'Arch 4'.
|
||||
patterns['Arch 5'] = {
|
||||
deriveFrom: 'Column 5',
|
||||
transform(point, col, row, model){
|
||||
// This transform logic is standard and will work with the new Column 5's gridY
|
||||
const len = this.gridY(model.rowCount * this.tile.size.y, 0) - this.gridY(0, 0);
|
||||
const r = (len / Math.PI) + point.x;
|
||||
const y = point.y - this.gridY(0, 0);
|
||||
const a = Math.PI * (y / len);
|
||||
return { x: -r * Math.cos(a), y: -r * Math.sin(a) };
|
||||
}
|
||||
};
|
||||
// --- END: MODIFIED SECTION ---
|
||||
|
||||
|
||||
Object.keys(patterns).forEach(n => extend(patterns[n]));
|
||||
return api;
|
||||
}
|
||||
|
||||
function initClassicColorPicker(onColorChange) {
|
||||
const slots = Array.from(document.querySelectorAll('#classic-slots .slot-btn')), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors');
|
||||
if (!slots.length || !topperSwatch || !swatchGrid || !activeLabel) return;
|
||||
topperSwatch.classList.add('tab-btn');
|
||||
let classicColors = getClassicColors(), activeTarget = '1';
|
||||
|
||||
function updateUI() {
|
||||
[...slots, topperSwatch].forEach(el => { const id = el.dataset.slot || 'T'; el.classList.toggle('tab-active', activeTarget === id); el.classList.toggle('tab-idle', activeTarget !== id); });
|
||||
|
||||
slots.forEach((slot, i) => {
|
||||
const color = classicColors[i];
|
||||
if (!color) return; // Safeguard against errors
|
||||
slot.style.backgroundImage = color.image ? `url("${color.image}")` : 'none';
|
||||
slot.style.backgroundColor = color.hex;
|
||||
slot.style.backgroundSize = '200%';
|
||||
slot.style.backgroundPosition = 'center';
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
activeLabel.textContent = activeTarget === 'T' ? 'Topper' : `Slot #${activeTarget}`;
|
||||
}
|
||||
|
||||
const allPaletteColors = flattenPalette(); swatchGrid.innerHTML = '';
|
||||
(window.PALETTE || []).forEach(group => {
|
||||
const title = document.createElement('div'); title.className = 'family-title'; title.textContent = group.family; swatchGrid.appendChild(title);
|
||||
const row = document.createElement('div'); row.className = 'swatch-row';
|
||||
(group.colors || []).forEach(colorItem => {
|
||||
const sw = document.createElement('div'); sw.className = 'swatch'; sw.title = colorItem.name;
|
||||
|
||||
sw.style.backgroundImage = colorItem.image ? `url("${colorItem.image}")` : 'none';
|
||||
sw.style.backgroundColor = colorItem.hex;
|
||||
sw.style.backgroundSize = '500%';
|
||||
sw.style.backgroundPosition = 'center';
|
||||
|
||||
sw.addEventListener('click', () => {
|
||||
const selectedColor = { hex: colorItem.hex, image: colorItem.image };
|
||||
if (activeTarget === 'T') setTopperColor(selectedColor);
|
||||
else {
|
||||
const index = parseInt(activeTarget, 10) - 1;
|
||||
if (index >= 0 && index < 5) { classicColors[index] = selectedColor; setClassicColors(classicColors); }
|
||||
}
|
||||
updateUI(); onColorChange();
|
||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||
});
|
||||
row.appendChild(sw);
|
||||
});
|
||||
swatchGrid.appendChild(row);
|
||||
});
|
||||
slots.forEach(slot => { slot.addEventListener('click', () => { activeTarget = slot.dataset.slot; updateUI(); }); });
|
||||
topperSwatch.addEventListener('click', () => { activeTarget = 'T'; updateUI(); });
|
||||
randomizeBtn?.addEventListener('click', () => {
|
||||
const pool = allPaletteColors.slice(); const picks = [];
|
||||
const colorCount = (patterns[document.getElementById('classic-pattern')?.value] || {}).balloonsPerCluster || 5;
|
||||
for (let i = 0; i < colorCount && pool.length; i++) { picks.push(pool.splice(Math.floor(Math.random() * pool.length), 1)[0]); }
|
||||
classicColors = setClassicColors(picks.map(c => ({ hex: c.hex, image: c.image })));
|
||||
updateUI(); onColorChange();
|
||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||
});
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function initClassic() {
|
||||
try {
|
||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
||||
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), rebuildBtn = document.getElementById('classic-rerender'), topperControls = document.getElementById('topper-controls'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperTypeSelect = document.getElementById('classic-topper-type'), topperOffsetX_Inp = document.getElementById('classic-topper-offset-x'), topperOffsetY_Inp = document.getElementById('classic-topper-offset-y'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled');
|
||||
if (!display) return fail('#classic-display not found');
|
||||
if (window.setupAccordionPanel) { window.setupAccordionPanel({ panelId: 'classic-controls-panel', expandBtnId: 'classic-expand-all', collapseBtnId: 'classic-collapse-all', reorderBtnId: 'classic-toggle-reorder', storagePrefix: 'cbd' }); }
|
||||
const GC = GridCalculator(), ctrl = GC.controller(display);
|
||||
|
||||
function updateClassicDesign() {
|
||||
if (!lengthInp || !patSel) return;
|
||||
const patternName = patSel.value || 'Arch 4';
|
||||
const isColumn = patternName.toLowerCase().includes('column');
|
||||
const hasTopper = patternName.includes('4');
|
||||
|
||||
topperControls.classList.toggle('hidden', !isColumn || !hasTopper);
|
||||
topperTypeSelect.disabled = !topperEnabledCb.checked;
|
||||
|
||||
GC.setTopperEnabled(isColumn && hasTopper && topperEnabledCb.checked);
|
||||
GC.setClusters(Math.round((parseFloat(lengthInp.value) || 0) * 2));
|
||||
GC.setReverse(!!reverseCb?.checked);
|
||||
GC.setTopperType(topperTypeSelect.value);
|
||||
GC.setTopperOffsetX(topperOffsetX_Inp?.value);
|
||||
GC.setTopperOffsetY(topperOffsetY_Inp?.value);
|
||||
GC.setTopperSize(topperSizeInp?.value);
|
||||
GC.setShineEnabled(!!shineEnabledCb?.checked);
|
||||
if(clusterHint) clusterHint.textContent = `≈ ${Math.round((parseFloat(lengthInp.value) || 0) * 2)} clusters (rule: 2 clusters/ft)`;
|
||||
ctrl.selectPattern(patternName);
|
||||
}
|
||||
|
||||
window.ClassicDesigner = window.ClassicDesigner || {};
|
||||
window.ClassicDesigner.api = GC;
|
||||
window.ClassicDesigner.redraw = updateClassicDesign;
|
||||
window.ClassicDesigner.getColors = getClassicColors;
|
||||
window.ClassicDesigner.getTopperColor = getTopperColor;
|
||||
|
||||
document.querySelector('#mode-tabs')?.addEventListener('click', () => setTimeout(() => { if (window.updateExportButtonVisibility) window.updateExportButtonVisibility() }, 50));
|
||||
patSel?.addEventListener('change', () => {
|
||||
const isArch = patSel.value.toLowerCase().includes('arch');
|
||||
lengthInp.value = isArch ? 25 : 5;
|
||||
updateClassicDesign();
|
||||
});
|
||||
[lengthInp, reverseCb, topperEnabledCb, topperTypeSelect, topperOffsetX_Inp, topperOffsetY_Inp, topperSizeInp, rebuildBtn]
|
||||
.forEach(el => { if (!el) return; const eventType = (el.type === 'range' || el.type === 'number') ? 'input' : 'change'; el.addEventListener((el === rebuildBtn) ? 'click' : eventType, updateClassicDesign); });
|
||||
shineEnabledCb?.addEventListener('change', (e) => { const on = !!e.target.checked; GC.setShineEnabled(on); updateClassicDesign(); window.syncAppShine?.(on); });
|
||||
initClassicColorPicker(updateClassicDesign);
|
||||
try { const saved = localStorage.getItem('app:shineEnabled:v1'); if (saved !== null && shineEnabledCb) shineEnabledCb.checked = JSON.parse(saved); } catch {}
|
||||
updateClassicDesign();
|
||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||
log('Classic ready');
|
||||
} catch (e) { fail(e.message || e); }
|
||||
}
|
||||
|
||||
window.ClassicDesigner = window.ClassicDesigner || { init: initClassic, api: null, redraw: null };
|
||||
document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('classic-display') && !window.__classicInit) { window.__classicInit = true; initClassic(); } });
|
||||
})();
|
||||
66
colors.js
Normal file
@ -0,0 +1,66 @@
|
||||
const PALETTE = [
|
||||
{ family: "Whites & Neutrals", colors: [
|
||||
{ name:"White",hex:"#ffffff"},{name:"Retro White",hex:"#e8e3d9"},{name:"Sand",hex:"#e1d8c6"},
|
||||
{ name:"Cameo",hex:"#e9ccc8"},{name:"Grey",hex:"#ced3d4"},{name:"Stone",hex:"#989689"},
|
||||
{ name:"Fog",hex:"#6b9098"},{name:"Smoke",hex:"#75777b"},{name:"Black",hex:"#0b0d0f"}
|
||||
]},
|
||||
{ family: "Pinks & Reds", colors: [
|
||||
{name:"Blush",hex:"#fbd6c0"},{name:"Light Pink",hex:"#fcccda"},{name:"Melon",hex:"#fac4bc"},
|
||||
{name:"Rose Pink",hex:"#d984a3"},{name:"Fuchsia",hex:"#eb4799"},{name:"Aloha",hex:"#e45c56"},
|
||||
{name:"Red",hex:"#ef2a2f"},{name:"Pastel Magenta",hex:"#B72E6C"},{name:"Coral",hex:"#bd4b3b"},
|
||||
{name:"Wild Berry",hex:"#79384c"},{name:"Maroon",hex:"#80011f"}
|
||||
]},
|
||||
{ family: "Oranges & Browns & Yellows", colors: [
|
||||
{name:"Pastel Yellow",hex:"#fcfd96"},{name:"Yellow",hex:"#f5e812"},{name:"Goldenrod",hex:"#f7b615"},
|
||||
{name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"}
|
||||
]},
|
||||
{ family: "Greens", colors: [
|
||||
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},
|
||||
{name:"Seafoam",hex:"#479a87"},{name:"Grass Green",hex:"#28b35e"},{name:"Empowermint",hex:"#779786"},
|
||||
{name:"Forest Green",hex:"#218b21"},{name:"Willow",hex:"#4a715c"}
|
||||
]},
|
||||
{ family: "Blues", colors: [
|
||||
{name:"Sky Blue",hex:"#87ceec"},{name:"Sea Glass",hex:"#80a4bc"},{name:"Caribbean Blue",hex:"#0bbbb6"},
|
||||
{name:"Medium Blue",hex:"#1b89e8"},{name:"Blue Slate",hex:"#327295"},{name:"Tropical Teal",hex:"#0d868f"},
|
||||
{name:"Royal Blue",hex:"#005eb7"},{name:"Dark Blue",hex:"#26408e"},{name:"Navy",hex:"#262266"}
|
||||
]},
|
||||
{ family: "Purples", colors: [
|
||||
{name:"Pastel Dusk",hex:"#d7c4c8"},{name:"Lilac",hex:"#c69edb"},{name:"Canyon Rose",hex:"#ca93b3"},
|
||||
{name:"Rosewood",hex:"#ad7271"},{name:"Lavender",hex:"#866c92"},{name:"Orchid",hex:"#a42487"},
|
||||
{name:"Violet",hex:"#812a8c"}
|
||||
]},
|
||||
|
||||
// === Pearl & Metallic: image-backed swatches ===
|
||||
{ family: "Pearl and Matallic Colors", colors: [
|
||||
{ name:"Pearl White", hex:"#F8F8F8", metallic:true, pearlType:"white", image:"images/pearl-white.webp" },
|
||||
{ name:"Classic Silver", hex:"#F4C2C2", metallic:true, pearlType:"silver", image:"images/classic-silver.webp" },
|
||||
{ name:"Pearl Pink", hex:"#F4C2C2", metallic:true, pearlType:"pink", image:"images/pearl-pink.webp" },
|
||||
{ name:"Pearl Peach", hex:"#F4C2C2", metallic:true, pearlType:"pink", image:"images/pearl-peach.webp" },
|
||||
{ name:"Classic Rose Gold", hex:"#F4C2C2", metallic:true, pearlType:"pink", image:"images/metalic-rosegold.webp" },
|
||||
{ name:"Pearl Lilac", hex:"#C8A2C8", metallic:true, pearlType:"lilac", image:"images/pearl-lilac.webp" },
|
||||
{ name:"Pearl Light Blue", hex:"#87CEEB", metallic:true, pearlType:"blue", image:"images/pearl-lightblue.webp" },
|
||||
{ name:"Pearl Periwinkle", hex:"#F4C2C2", metallic:true, pearlType:"blue", image:"images/pearl-periwinkle.webp" },
|
||||
{ name:"Pearl Fuchsia", hex:"#FD49AB", metallic:true, pearlType:"fuchsia", image:"images/pearl-fuchsia.webp" },
|
||||
{ name:"Pearl Violet", hex:"#8F00FF", metallic:true, pearlType:"violet", image:"images/pearl-violet.webp" },
|
||||
{ name:"Pearl Sapphire", hex:"#0F52BA", metallic:true, pearlType:"sapphire", image:"images/pearl-sapphire.webp" },
|
||||
{ name:"Pearl Midnight Blue",hex:"#191970", metallic:true, pearlType:"midnight-blue", image:"images/pearl-midnightblue.webp" },
|
||||
{ name:"Classic Gold", hex:"#E32636", metallic:true, pearlType:"gold", image:"images/classic-gold.webp" }
|
||||
]},
|
||||
|
||||
// === Chrome: image-backed swatches ===
|
||||
{ family: "Chrome Colors", colors: [
|
||||
{ name:"Chrome Rose Gold", hex:"#FFBF00", metallic:true, chromeType:"rosegold", image:"images/chrome-rosegold.webp" },
|
||||
{ name:"Chrome Pink", hex:"#FFBF00", metallic:true, chromeType:"rosegold", image:"images/chrome-pink.webp" },
|
||||
{ name:"Chrome Purple", hex:"#DFFF00", metallic:true, chromeType:"purple", image:"images/chrome-purple.webp" },
|
||||
{ name:"Chrome Champagne", hex:"#FF1DCE", metallic:true, chromeType:"champagne", image:"images/chrome-champagne.webp" },
|
||||
{ name:"Chrome Truffle", hex:"#FF1DCE", metallic:true, chromeType:"champagne", image:"images/chrome-truffle.webp" },
|
||||
{ name:"Chrome Silver", hex:"#a8a9a4", metallic:true, chromeType:"silver", image:"images/chrome-silver.webp" },
|
||||
{ name:"Chrome Space Grey",hex:"#a8a9a4", metallic:true, chromeType:"spacegrey", image:"images/chrome-spacegrey.webp" },
|
||||
{ name:"Chrome Gold", hex:"#a18b67", metallic:true, chromeType:"gold", image:"images/chrome-gold.webp" },
|
||||
{ name:"Chrome Green", hex:"#457066", metallic:true, chromeType:"green", image:"images/chrome-green.webp" },
|
||||
{ name:"Chrome Blue", hex:"#2d576f", metallic:true, chromeType:"blue", image:"images/chrome-blue.webp" }
|
||||
]}
|
||||
];
|
||||
|
||||
window.CLASSIC_COLORS = ['#D92E3A', '#FFFFFF', '#0055A4', '#40E0D0'];
|
||||
window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []);
|
||||
71
images/1balloon-mask.svg
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="12in"
|
||||
height="12in"
|
||||
viewBox="0 0 39.999867 39.999867"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="balloon-mask1.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="true"
|
||||
inkscape:pageopacity="0.00392157"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="in"
|
||||
showgrid="true"
|
||||
inkscape:zoom="0.93262836"
|
||||
inkscape:cx="847.06839"
|
||||
inkscape:cy="614.39264"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1532"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:lockguides="false"
|
||||
showborder="true"
|
||||
inkscape:snap-global="false"><inkscape:grid
|
||||
id="grid1"
|
||||
units="in"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="3.3333222"
|
||||
spacingy="3.3333222"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.41176471"
|
||||
color="#e51100"
|
||||
opacity="0.30196078"
|
||||
empspacing="5"
|
||||
dotted="false"
|
||||
gridanglex="30"
|
||||
gridanglez="30"
|
||||
visible="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
enabled="true" /></sodipodi:namedview><defs
|
||||
id="defs1"><color-profile
|
||||
name="Built-in-display"
|
||||
xlink:href="../../../.local/share/icc/edid-88fcf6ba6729e8e4531280899cfccbad.icc"
|
||||
id="color-profile3338" /><color-profile
|
||||
name="Built-in-display"
|
||||
xlink:href="../../../.local/share/icc/edid-88fcf6ba6729e8e4531280899cfccbad.icc"
|
||||
id="color-profile3340" /></defs><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><path
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-dashoffset:297.6;paint-order:stroke markers fill"
|
||||
d="m 21.227687,32.109589 c 0.624172,-0.03563 0.710957,-0.06698 0.809593,-0.292541 0.111029,-0.253897 0.09912,-0.307345 -0.240234,-1.078141 -0.174461,-0.396274 -0.294294,-0.741734 -0.2663,-0.76769 0.028,-0.02596 0.228136,-0.112851 0.44475,-0.193094 1.995094,-0.739052 3.728405,-2.506163 5.411409,-5.516916 1.220107,-2.182667 2.190925,-4.927783 2.72858,-7.715409 0.205455,-1.065242 0.27528,-3.230083 0.132803,-4.117189 -0.92694,-5.3136132 -5.113634,-8.7321766 -9.099685,-8.8288819 -1.386756,-0.032517 -1.869622,0.036057 -2.992621,0.4247129 -3.317278,1.1480578 -5.807323,4.2711432 -6.501356,8.154187 -0.262848,1.470608 -0.187185,3.472708 0.205696,5.44264 0.625764,3.137673 1.896442,6.36622 3.388147,8.608653 0.43191,0.649277 1.544521,1.935737 2.054649,2.375694 0.442106,0.381296 1.399196,0.967298 1.866288,1.142685 0.188138,0.07064 0.341629,0.146181 0.341096,0.167867 -5.41e-4,0.02169 -0.143212,0.330201 -0.317063,0.685577 -0.403624,0.825076 -0.418669,1.148088 -0.06362,1.36587 0.156034,0.09572 1.415279,0.180933 2.097874,0.141981 z"
|
||||
id="path2013"
|
||||
sodipodi:nodetypes="sssssssccssssssssss" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
51
images/balloon-mask(1).svg
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="266.66666"
|
||||
height="266.66666"
|
||||
viewBox="0 0 266.66666 266.66666"
|
||||
sodipodi:docname="970844-200.png"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
inkscape:dataloss="true"
|
||||
inkscape:export-filename="balloon.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8537501"
|
||||
inkscape:cx="133.33333"
|
||||
inkscape:cy="133.33333"
|
||||
inkscape:window-width="1913"
|
||||
inkscape:window-height="965"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Image"
|
||||
id="g1">
|
||||
<path
|
||||
style="fill:#000000"
|
||||
d="m 110.86728,176.98292 c -4.14002,0 -4.72369,-2.23401 -2.50756,-9.59765 l 1.86611,-6.20063 -6.64984,-2.17505 C 79.220392,151.04328 59.783306,123.31229 54.44661,88.916447 50.643745,64.406394 58.908048,38.185685 75.880855,20.91057 112.83686,-16.70367 169.04213,6.1526753 179.48832,63.043415 c 1.80388,9.824085 1.93369,13.895611 0.74359,23.324138 -3.90357,30.925927 -19.83967,57.568167 -41.24178,68.948697 -4.48207,2.38333 -8.96523,4.34252 -9.96258,4.35375 -4.50331,0.0507 -5.26813,1.96681 -3.1647,7.92856 2.23565,6.33649 1.10587,9.38436 -3.47859,9.38436 z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="sscssssssssss" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
51
images/balloon-mask.svg
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="266.66666"
|
||||
height="266.66666"
|
||||
viewBox="0 0 266.66666 266.66666"
|
||||
sodipodi:docname="970844-200.png"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
inkscape:dataloss="true"
|
||||
inkscape:export-filename="balloon.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8537501"
|
||||
inkscape:cx="133.33333"
|
||||
inkscape:cy="133.33333"
|
||||
inkscape:window-width="1913"
|
||||
inkscape:window-height="965"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Image"
|
||||
id="g1">
|
||||
<path
|
||||
style="fill:#000000"
|
||||
d="m 110.86728,176.98292 c -4.14002,0 -4.72369,-2.23401 -2.50756,-9.59765 l 1.86611,-6.20063 -6.64984,-2.17505 C 79.220392,151.04328 59.783306,123.31229 54.44661,88.916447 50.643745,64.406394 58.908048,38.185685 75.880855,20.91057 112.83686,-16.70367 169.04213,6.1526753 179.48832,63.043415 c 1.80388,9.824085 1.93369,13.895611 0.74359,23.324138 -3.90357,30.925927 -19.83967,57.568167 -41.24178,68.948697 -4.48207,2.38333 -8.96523,4.34252 -9.96258,4.35375 -4.50331,0.0507 -5.26813,1.96681 -3.1647,7.92856 2.23565,6.33649 1.10587,9.38436 -3.47859,9.38436 z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="sscssssssssss" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
images/chrome-blue.webp
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
images/chrome-champagne.webp
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
images/chrome-gold.webp
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
images/chrome-green.webp
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
images/chrome-pink.webp
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
images/chrome-purple.webp
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
images/chrome-rosegold.webp
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
images/chrome-silver.webp
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
images/chrome-spacegrey.webp
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
images/chrome-truffle.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
images/classic-gold.webp
Normal file
|
After Width: | Height: | Size: 452 KiB |
BIN
images/classic-silver.webp
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
images/metalic-rosegold.webp
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
images/pearl-fuchsia.webp
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/pearl-lightblue.webp
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
images/pearl-lilac.webp
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
images/pearl-midnightblue.webp
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
images/pearl-peach.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
images/pearl-periwinkle.webp
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
images/pearl-pink.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
images/pearl-sapphire.webp
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
images/pearl-violet.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
images/pearl-white.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
336
index.html
Normal file
@ -0,0 +1,336 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Balloon Designer — Organic & Classic</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mithril/mithril.js" defer></script>
|
||||
|
||||
<script src="colors.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<style>
|
||||
.tab-btn{padding:.5rem .75rem;border-radius:.5rem;font-size:.875rem;font-weight:600;transition:background-color .2s,color .2s,box-shadow .2s}
|
||||
.tab-active{background:#2563eb;color:#fff;box-shadow:0 2px 6px rgba(37,99,235,.35)}
|
||||
.tab-idle{background:#e5e7eb;color:#1f2937}.tab-idle:hover{background:#d1d5db}
|
||||
#classic-display{width:100%;max-width:1200px;height:70vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;border-radius:.75rem}
|
||||
.copy-message{opacity:0;pointer-events:none;transition:opacity .2s}.copy-message.show{opacity:1}
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-4 md:p-8 flex flex-col items-center justify-center min-h-screen bg-gray-100 text-gray-900">
|
||||
<div class="container mx-auto p-6 bg-white rounded-2xl shadow-x3 flex flex-col gap-6 max-w-7xl lg:h-[calc(100vh-4rem)]">
|
||||
|
||||
<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>
|
||||
</nav>
|
||||
|
||||
<div id="global-export-bar" class="p-3 bg-gray-100/80 backdrop-blur-sm border border-gray-200 rounded-lg flex flex-wrap items-center justify-center gap-4 sticky top-4 z-20">
|
||||
<h3 class="text-base font-semibold text-gray-700 mr-2 hidden sm:block">Export Design:</h3>
|
||||
<button id="export-png-btn" class="btn-dark">Export as PNG</button>
|
||||
<button id="export-svg-btn" class="btn-dark">Export as SVG</button>
|
||||
<p class="text-xs text-gray-500 w-full text-center mt-1">(PNG for both modes, SVG for Classic mode)</p>
|
||||
</div>
|
||||
|
||||
|
||||
<section id="tab-organic" class="flex flex-col lg:flex-row gap-8 lg:h-[calc(100vh-12rem)]">
|
||||
<aside id="controls-panel"
|
||||
class="w-full lg:w-1/3 p-6 bg-gray-50 rounded-xl shadow-md flex flex-col gap-4
|
||||
lg:min-h-0 lg:h-full lg:overflow-y-auto lg:pr-2">
|
||||
<h1 class="text-3xl font-bold text-center text-blue-800">Organic Balloon Designer</h1>
|
||||
<p class="text-gray-600 text-sm text-center">Click adds. Double-click deletes. Tools below for erase/select.</p>
|
||||
|
||||
<div id="controls-toolbar"
|
||||
class="sticky top-0 z-10 -mx-6 px-6 pb-2 bg-gray-50/95 backdrop-blur supports-[backdrop-filter]:bg-gray-50/70 flex flex-wrap items-center gap-2">
|
||||
<button id="expand-all" class="btn-dark">Expand all</button>
|
||||
<button id="collapse-all" class="btn-dark">Collapse all</button>
|
||||
<button id="toggle-reorder" class="btn-dark" aria-pressed="false">Reorder panels</button>
|
||||
</div>
|
||||
|
||||
<details class="group" data-acc-id="tools" open>
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Tools</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||
<button id="tool-draw" class="tool-btn" aria-pressed="true" title="V">Draw</button>
|
||||
<button id="tool-erase" class="tool-btn" aria-pressed="false" title="E">Eraser</button>
|
||||
<button id="tool-select" class="tool-btn" aria-pressed="false" title="S">Select</button>
|
||||
</div>
|
||||
<div id="eraser-controls" class="hidden flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-gray-700">Eraser Size: <span id="eraser-size-label">30</span>px</label>
|
||||
<input type="range" id="eraser-size" min="10" max="120" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<p class="hint">Click-drag to erase. Preview circle shows the area.</p>
|
||||
</div>
|
||||
<div id="select-controls" class="hidden flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<button id="delete-selected" class="btn-danger" disabled>Delete Selected</button>
|
||||
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
|
||||
</div>
|
||||
<p class="hint">Click a balloon to select. <kbd>Del</kbd>/<kbd>Backspace</kbd> removes. <kbd>Esc</kbd> clears.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="share">
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Share</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<div class="relative mb-3">
|
||||
<input type="text" id="share-link-output" class="w-full p-3 pr-10 border border-gray-300 rounded-lg text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500" readonly placeholder="Click 'Generate Link' to create a shareable URL">
|
||||
<div id="copy-message" class="copy-message absolute right-2 top-1/2 -translate-y-1/2 bg-blue-500 text-white px-2 py-1 text-xs rounded-full">Copied!</div>
|
||||
</div>
|
||||
<button id="generate-link-btn" class="btn-indigo">Generate Shareable Link</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="save">
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Save & Load</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<div class="flex flex-wrap gap-3 mb-3">
|
||||
<button id="clear-canvas-btn" class="btn-danger">Clear Canvas</button>
|
||||
<button id="save-json-btn" class="btn-green">Save Design</button>
|
||||
<label for="load-json-input" class="btn-yellow text-center cursor-pointer">Load JSON</label>
|
||||
<input type="file" id="load-json-input" class="hidden" accept=".json">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="allowed" open>
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Colors</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<p class="hint mb-2">Alt+Click a balloon on canvas to pick its color.</p>
|
||||
<div id="color-palette" class="palette-box"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="replace">
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Replace Color</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<label class="text-sm font-medium">From (used):</label>
|
||||
<select id="replace-from" class="select"></select>
|
||||
|
||||
<label class="text-sm font-medium">To (allowed):</label>
|
||||
<select id="replace-to" class="select"></select>
|
||||
|
||||
<button id="replace-btn" class="btn-blue">Replace</button>
|
||||
<p id="replace-msg" class="hint"></p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="size">
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Balloon Size (Diameter)</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<div id="size-preset-group" class="grid grid-cols-5 gap-2 mb-2"></div>
|
||||
<p class="hint mb-3">Global scale lives in <code>PX_PER_INCH</code> (see <code>script.js</code>).</p>
|
||||
<button id="toggle-shine-btn" class="btn-dark">Turn Off Shine</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="used" open>
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Color Palette</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<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>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
<section id="canvas-panel" class="w-full lg:flex-1 flex flex-col items-stretch lg:sticky lg:top-8 lg:self-start shadow-x3">
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button id="expand-workspace-btn" class="bg-gray-700 text-white px-3 py-2 rounded">Expand workspace</button>
|
||||
<button id="fullscreen-btn" class="bg-gray-700 text-white px-3 py-2 rounded">Fullscreen</button>
|
||||
</div>
|
||||
<canvas id="balloon-canvas" class="balloon-canvas w-full aspect-video"></canvas>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="tab-classic" class="hidden flex flex-col lg:flex-row gap-8 lg:h-[calc(100vh-12rem)]">
|
||||
|
||||
<aside id="classic-controls-panel"
|
||||
class="w-full lg:w-1/3 p-6 bg-gray-50 rounded-xl shadow-md flex flex-col gap-4
|
||||
lg:min-h-0 lg:h-full lg:overflow-y-auto lg:pr-2">
|
||||
|
||||
<h2 class="text-2xl font-bold text-blue-800">Classic Designer (Arch / Column)</h2>
|
||||
<p class="text-gray-600 text-sm">Quad-wrap column or arch with a 4-color spiral.</p>
|
||||
|
||||
<div id="classic-toolbar"
|
||||
class="sticky top-0 z-10 -mx-6 px-6 pb-2 bg-gray-50/95 backdrop-blur supports-[backdrop-filter]:bg-gray-50/70
|
||||
flex flex-wrap items-center gap-2">
|
||||
<button id="classic-expand-all" class="btn-dark">Expand all</button>
|
||||
<button id="classic-collapse-all" class="btn-dark">Collapse all</button>
|
||||
<button id="classic-toggle-reorder" class="btn-dark" aria-pressed="false">Reorder panels</button>
|
||||
</div>
|
||||
|
||||
<details class="group" data-acc-id="classic-layout" open>
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Pattern & Layout</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="text-sm">Pattern:
|
||||
<select id="classic-pattern" class="select align-middle">
|
||||
<option value="Arch 4">Arch 4 (4-color spiral)</option>
|
||||
<option value="Column 4">Column 4 (quad wrap)</option>
|
||||
<option value="Arch 5">Arch 5 (5-color spiral)</option>
|
||||
<option value="Column 5">Column 5 (5-balloon wrap)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm">Length (ft):
|
||||
<input id="classic-length-ft" type="number" min="1" max="100" step="0.5" value="5" class="w-full px-2 py-1 border rounded align-middle">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="topper-controls" class="hidden grid grid-cols-1 sm:grid-cols-4 gap-3 items-end pt-2 border-t border-gray-200">
|
||||
<label class="text-sm sm:col-span-2">Topper Type:
|
||||
<select id="classic-topper-type" class="select align-middle" disabled>
|
||||
<option value="round">24" Round</option>
|
||||
<option value="star">24" Star</option>
|
||||
<option value="heart">24" Heart</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
||||
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
|
||||
Add Topper
|
||||
</label>
|
||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
||||
<input id="classic-shine-enabled" type="checkbox" class="align-middle" checked>
|
||||
Enable Shine
|
||||
</label>
|
||||
<label class="text-sm">Topper Color:
|
||||
<div id="classic-topper-color-swatch" class="slot-swatch mx-auto" title="Click to change topper color">T</div>
|
||||
</label>
|
||||
<label class="text-sm">X Offset:
|
||||
<input id="classic-topper-offset-x" type="number" step="0.5" value="0" class="w-full px-2 py-1 border rounded align-middle">
|
||||
</label>
|
||||
<label class="text-sm">Y Offset:
|
||||
<input id="classic-topper-offset-y" type="number" step="0.5" value="0" class="w-full px-2 py-1 border rounded align-middle">
|
||||
</label>
|
||||
<label class="text-sm">Topper Size:
|
||||
<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">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
<span id="classic-cluster-hint">≈ 10 clusters (rule: 2 clusters/ft)</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-3 pt-2 border-t border-gray-200">
|
||||
<label class="text-sm inline-flex items-center gap-2">
|
||||
<input id="classic-reverse" type="checkbox" class="align-middle">
|
||||
Reverse spiral
|
||||
</label>
|
||||
<button id="classic-rerender" class="btn-blue ml-auto">Rebuild</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-acc-id="classic-colors" open>
|
||||
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
|
||||
<span class="text-lg font-semibold text-gray-800">Classic Colors</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
|
||||
</summary>
|
||||
|
||||
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
|
||||
<div id="classic-slots" class="flex items-center gap-2 mb-3">
|
||||
<button type="button" class="slot-btn tab-btn" data-slot="1">#1</button>
|
||||
<button type="button" class="slot-btn tab-btn" data-slot="2">#2</button>
|
||||
<button type="button" class="slot-btn tab-btn" data-slot="3">#3</button>
|
||||
<button type="button" class="slot-btn tab-btn" data-slot="4">#4</button>
|
||||
<button type="button" class="slot-btn tab-btn" data-slot="5">#5</button>
|
||||
</div>
|
||||
<div 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 5</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</aside>
|
||||
|
||||
<section id="classic-canvas-panel"
|
||||
class="w-full lg:flex-1 flex flex-col items-stretch lg:sticky lg:top-8 lg:self-start shadow-x3">
|
||||
<div id="classic-display"
|
||||
class="rounded-xl"
|
||||
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
<div id="message-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
||||
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm text-center">
|
||||
<p id="modal-text" class="text-gray-800 text-lg"></p>
|
||||
<button id="modal-close-btn" class="mt-4 btn-blue">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js" defer></script>
|
||||
|
||||
<script src="script.js" defer></script>
|
||||
|
||||
<script src="classic.js" defer></script>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
147
style.css
Normal file
@ -0,0 +1,147 @@
|
||||
/* Minimal extras (Tailwind handles most styling) */
|
||||
body { color: #1f2937; }
|
||||
|
||||
#balloon-canvas { touch-action: none; }
|
||||
|
||||
.balloon-canvas {
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
|
||||
border: 1px black solid;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.tool-btn {
|
||||
padding: .5rem .75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: .5rem;
|
||||
background: #fff;
|
||||
}
|
||||
.tool-btn[aria-pressed="true"] { background:#1f2937; color:#fff; border-color:#1f2937; }
|
||||
|
||||
.btn-dark { background:#1f2937; color:#fff; padding:.6rem .8rem; border-radius:.5rem; }
|
||||
.btn-blue { background:#2563eb; color:#fff; padding:.6rem .8rem; border-radius:.5rem; }
|
||||
.btn-green { background:#16a34a; color:#fff; padding:.6rem .8rem; border-radius:.5rem; }
|
||||
.btn-yellow { background:#eab308; color:#fff; padding:.6rem .8rem; border-radius:.5rem; }
|
||||
.btn-danger { background:#ef4444; color:#fff; padding:.6rem .8rem; border-radius:.5rem; }
|
||||
.btn-indigo { background:#4f46e5; color:#fff; padding:.6rem .8rem; border-radius:.5rem; }
|
||||
|
||||
.copy-message{ opacity:0; transition:opacity .3s; }
|
||||
.copy-message.show{ opacity:1; }
|
||||
|
||||
.hint { font-size:.8rem; color:#6b7280; }
|
||||
|
||||
/* Palette / Swatches */
|
||||
.palette-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
padding: .5rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
position: relative;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 9999px;
|
||||
border: 2px solid rgba(0,0,0,.15);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.08);
|
||||
cursor: pointer;
|
||||
}
|
||||
.swatch.active { outline: 2px solid #3b82f6; outline-offset: 2px; }
|
||||
|
||||
.swatch-row { display:flex; flex-wrap:wrap; gap:.5rem; }
|
||||
.family-title { font-weight:600; color:#4b5563; margin-top:.25rem; font-size:.95rem; }
|
||||
|
||||
.badge {
|
||||
position:absolute;
|
||||
right:-6px; top:-6px;
|
||||
min-width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0 .25rem;
|
||||
background:#111827;
|
||||
color:#fff;
|
||||
border-radius: 9999px;
|
||||
font-size:.7rem;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
/* Selects */
|
||||
.select {
|
||||
width: 100%;
|
||||
padding: .5rem .6rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: .5rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
|
||||
/* hidden by default; only show in reorder mode */
|
||||
.drag-handle {
|
||||
display: none;
|
||||
width: 28px; height: 28px; margin-right: .25rem;
|
||||
align-items: center; justify-content: center;
|
||||
border-radius: .5rem; border: 1px solid #e5e7eb; /* gray-200 */
|
||||
background: #f8fafc; /* slate-50 */
|
||||
font-size: 18px; line-height: 1;
|
||||
touch-action: none; /* better touch-drag */
|
||||
cursor: grab;
|
||||
}
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
|
||||
.reorder-on .drag-handle { display: inline-flex; }
|
||||
.reorder-on section { outline: 2px dashed #cbd5e1; outline-offset: 4px; } /* slate-300 */
|
||||
.drag-ghost { opacity: .6; }
|
||||
|
||||
#classic-swatch-grid .sw { width: 24px; height: 24px; border-radius: 6px; border: 1px solid rgba(0,0,0,.1); cursor: pointer; }
|
||||
#classic-swatch-grid .sw:focus { outline: 2px solid #2563eb; outline-offset: 2px; }
|
||||
.slot-btn[aria-pressed="true"] { background:#2563eb; color:#fff; }
|
||||
/* Add these new rules to your stylesheet */
|
||||
.slot-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.slot-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.slot-swatch {
|
||||
width: 2.5rem; /* 40px */
|
||||
height: 2.5rem; /* 40px */
|
||||
border-radius: 9999px;
|
||||
border: 3px solid #e5e7eb; /* gray-200 */
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
transition: border-color .2s, transform .2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: rgba(0,0,0,0.4);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.slot-swatch:hover {
|
||||
border-color: #9ca3af; /* gray-400 */
|
||||
}
|
||||
|
||||
.slot-swatch.active {
|
||||
border-color: #2563eb; /* blue-600 */
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.slot-label {
|
||||
font-weight: 600;
|
||||
color: #4b5563; /* gray-600 */
|
||||
}
|
||||