const PX = 96; // px per inch at screen const LOGO = "https://beachpartyballoons.com/assets/logo/BeachPartyBalloons-logo.webp"; const FOLD = { w: 11*PX, h: 8.5*PX }; const FLAT = { w: 8.5*PX, h: 11*PX }; const MM = 96 / 25.4; // px per mm at screen const PAPER_DIMS = { letter: FLAT, a4: { w: Math.round(210*MM), h: Math.round(297*MM) } }; /* ---- Theme decorations (inline SVG, scale to fill their face) ---- */ function deco(theme, variant){ switch(theme.id){ case 'confetti':{ const c=['#FF5A7A','#FFC94D','#4DB6FF','#7BD389','#B07CFF']; let s=''; const pts=[[8,10,0],[20,22,1],[33,8,2],[46,18,3],[60,9,4],[72,20,0],[86,12,1],[92,28,2], [6,80,3],[18,90,4],[30,78,0],[44,88,1],[58,82,2],[70,92,3],[84,84,4],[94,76,0]]; pts.forEach((p,i)=>{ const col=c[p[2]]; if(i%3===0) s+=``; else if(i%3===1) s+=``; else s+=``; }); return svg(s); } case 'gold':{ const a=theme.vars['--c-accent']; const fr=` `; const fl=(x,y,r)=>` `; return svg(fr+fl(12,15,0)+fl(88,15,90)+fl(12,85,-90)+fl(88,85,180)); } case 'botanical':{ const a=theme.vars['--c-accent']; // leaves-outline-with-vines-svgrepo-com.svg path (viewBox 0 0 136.492 136.492) const vp=`M121.036,78.928c-13.526-14.684-30.95-14.207-47.129-12.3c-0.375-0.216-0.802-0.34-1.266-0.292c-4.219,0.458-9.566-0.227-11.18-6.275c-0.937-3.562,1.187-7.235,4.725-8.174c2.624-0.709,5.332,0.878,6.027,3.493c0.491,1.867-0.622,3.789-2.476,4.282c-0.681,0.171-1.35,0.026-1.891-0.324c0.562-1.081,0.304-2.426-0.701-3.169c-1.118-0.849-2.7-0.612-3.536,0.511c-0.01,0.016-0.01,0.04-0.021,0.056c-0.768,0.496-1.179,1.419-0.937,2.354c0.947,3.541,4.622,5.69,8.182,4.757c4.158-1.103,6.643-5.39,5.545-9.563c-1.308-4.915-6.381-7.852-11.295-6.576c-5.843,1.553-9.336,7.586-7.796,13.461c0.793,2.987,2.386,5.323,4.588,6.966c-1.965,0.227-3.913,0.433-5.838,0.572c-19.478,1.506-30.855,1.822-38.333-9.207c-2.354-3.473-3.483-7.528-3.369-11.687c1.782,4.688,5.679,8.248,11.625,10.563c2.742,1.071,5.276,1.592,7.744,1.592l0,0c9.671,0,17.133-7.992,26.757-21.808c4.422-6.357,10.167-11.962,23.838-18.686c0.143-0.069,0.227-0.209,0.349-0.312c0.079-0.066,0.152-0.124,0.227-0.2c0.132-0.129,0.248-0.259,0.337-0.427c0.158-0.269,0.285-0.567,0.296-0.917c0.011-0.356-0.09-0.691-0.232-1c-0.037-0.097-0.1-0.15-0.143-0.232c-0.142-0.232-0.332-0.422-0.554-0.577c-0.053-0.037-0.068-0.098-0.115-0.114C83.399,15.051,58.057,0,35.935,0C21.926,0,12.332,5.957,7.417,17.708c-1.155,2.777-1.139,7.863,1.179,11.826c1.339,2.299,3.298,3.899,5.695,4.822c-1.804,2.716-3.13,5.746-3.789,8.976c-1.345,6.555-0.056,13.165,3.639,18.592c9.255,13.634,24.542,12.458,42.245,11.093c3.995-0.316,8.092-0.834,12.229-1.382c17.494-2.265,35.588-4.609,49.272,10.22c2.352,2.562,4.155,5.416,5.437,8.438c-0.005,0.116-0.047,0.211-0.042,0.327c0.69,10.515-3.617,17.323-12.145,19.147c-6.603,1.439-13.157-2.8-14.57-9.402c-1.076-5.073,2.151-10.072,7.208-11.148c1.852-0.385,3.724-0.053,5.311,0.971c1.588,1.023,2.685,2.605,3.074,4.472c0.27,1.234,0.026,2.474-0.563,3.565c-0.228-0.338-0.448-0.686-0.786-0.949c-1.756-1.461-4.366-1.203-5.822,0.563c-1.45,1.772-1.196,4.372,0.575,5.827c0.459,0.369,0.98,0.623,1.524,0.781c0.501,0.506,1.223,0.785,1.972,0.633c2.5-0.544,4.625-2.015,6.007-4.166c1.382-2.131,1.851-4.684,1.312-7.178c-0.643-2.99-2.409-5.537-4.967-7.193c-2.548-1.65-5.585-2.188-8.554-1.555c-7.378,1.565-12.103,8.869-10.531,16.284c1.683,7.763,8.569,13.089,16.211,13.089c1.149,0,2.325-0.111,3.49-0.364c6.318-1.351,10.827-4.941,13.337-10.235c-0.881,8.849-5.864,17.254-13.711,23.087c-5.759,4.282-12.852,6.075-19.955,5.021c-7.093-1.045-13.373-4.794-17.655-10.558c-0.712-0.955-2.062-1.149-3.029-0.443c-0.957,0.718-1.157,2.067-0.448,3.027c4.976,6.687,12.258,11.048,20.5,12.266c1.535,0.222,3.075,0.332,4.609,0.332c6.66,0,13.109-2.125,18.558-6.175c8.88-6.598,14.512-16.189,15.456-26.325C130.538,94.806,127.469,85.904,121.036,78.928z M20.945,7.966c2.17-1.284,4.627-2.226,7.338-2.824L23.637,24.83c-1.748,1.031-3.33,2.159-4.653,3.436l2.02-19.672C21.03,8.372,20.982,8.171,20.945,7.966z M39.099,54.288L20.252,33.816c1.152-1.205,2.399-2.31,3.813-3.22c0.432-0.274,0.704-0.691,0.854-1.139l19.765,21.065c-1.936,1.745-3.76,3.024-5.532,3.847C39.131,54.346,39.125,54.314,39.099,54.288z M33.705,55.653c-1.917,0-3.937-0.427-6.173-1.289c-4.777-1.87-7.873-4.602-9.191-8.124c-1.113-2.964-0.81-6.041-0.243-8.385L34.411,55.58C34.179,55.59,33.942,55.653,33.705,55.653L33.705,55.653z M47.734,47.466L28.589,27.061c1.859-0.864,3.889-1.653,6.088-2.339l17.65,17.216C50.704,44.055,49.174,45.895,47.734,47.466z M46.632,5.487c2.305,0.448,4.591,0.988,6.842,1.627L44.99,17.858c-1.695,0.309-3.386,0.633-5.044,1.016l6.61-13.115C46.606,5.672,46.601,5.579,46.632,5.487z M63.982,10.676l-6.265,5.218c-0.103,0.087-0.156,0.197-0.243,0.306c-2.041,0.189-4.124,0.427-6.215,0.704l6.637-8.419C60.003,9.176,62.028,9.906,63.982,10.676z M74.524,15.312c-2.816,0.037-6.146,0.135-9.774,0.338l3.746-3.114C70.71,13.5,72.751,14.441,74.524,15.312z M68.702,23.723l-3.932-3.736c3.629-0.216,7.069-0.335,10.181-0.361C72.573,21.018,70.526,22.383,68.702,23.723z M61.324,30.209l-9.746-8.996c2.468-0.33,4.936-0.588,7.38-0.805l6.315,6.015C63.771,27.715,62.466,28.978,61.324,30.209z M54.927,38.449L39.502,23.406c2.133-0.52,4.343-0.968,6.61-1.358l12.411,11.449c-0.567,0.741-1.107,1.463-1.605,2.193C56.222,36.69,55.584,37.536,54.927,38.449z M42.237,4.733l-7.862,15.593c-1.999,0.598-3.905,1.273-5.714,2.025l4.213-17.874c0.997-0.077,1.991-0.161,3.045-0.161C37.997,4.316,40.109,4.467,42.237,4.733z M12.31,27.369c-1.622-2.781-1.577-6.426-0.917-7.992c1.263-3.024,2.943-5.537,4.952-7.652l-1.843,17.969C13.615,29.112,12.882,28.34,12.31,27.369z`; const vine=(tx,ty,r,s,op=0.3)=>``; // Bottom-left and top-right opposing corners (vine SVG is 136.5×136.5, scaled to ~55 units) return svg(vine(0,45,0,0.4) + vine(100,55,180,0.4)); } case 'watercolor':{ const cols = ['#A389D4', '#7DADE2', '#E27DA1', '#7DE2C9', '#B87DE2', '#7D96E2']; const blob = (x, y, rx, ry, r, col, op) => ``; let s = ''; // Top-Left Cluster s += blob(5, 5, 35, 28, 30, cols[0], 0.15); s += blob(18, 8, 25, 20, -15, cols[1], 0.12); s += blob(10, 22, 28, 22, 45, cols[2], 0.1); s += blob(25, 15, 18, 15, 10, cols[4], 0.08); // Top-Right Cluster s += blob(95, 5, 32, 25, -30, cols[3], 0.14); s += blob(82, 12, 24, 20, 20, cols[5], 0.12); s += blob(90, 25, 20, 18, -45, cols[0], 0.09); // Bottom-Left Cluster s += blob(5, 95, 38, 24, 15, cols[1], 0.16); s += blob(22, 88, 28, 18, -10, cols[4], 0.11); s += blob(12, 80, 22, 16, 35, cols[3], 0.13); // Bottom-Right Cluster s += blob(95, 95, 34, 28, 45, cols[2], 0.17); s += blob(78, 88, 25, 22, -25, cols[5], 0.12); s += blob(88, 75, 20, 18, 15, cols[1], 0.1); s += blob(98, 82, 15, 12, -10, cols[0], 0.08); // Edge and Middle Washes s += blob(50, 5, 45, 14, 0, cols[4], 0.08); s += blob(50, 95, 50, 18, 5, cols[0], 0.07); s += blob(5, 50, 12, 35, 0, cols[2], 0.06); s += blob(95, 50, 14, 40, 0, cols[3], 0.06); return svg(s); } case 'balloon':{ const cols=['#5A9AFF','#7AB5FF','#A0CFFF']; const bal=(x,y,col,sc,r)=>` `; // Group of balloons tied together const group=` ${bal(-8,-12,cols[0],1.2,10)}${bal(8,-10,cols[1],1.1,-10)}${bal(0,-20,cols[2],1.3,0)} `; // Floating sparkles let sparkles=''; for(let i=0;i<12;i++) sparkles+=``; return svg(sparkles + group); } case 'kraft':{ const a=theme.vars['--c-accent']; const border=``; const sprig=` `; return svg(border+sprig); } case 'flowers':{ const a=theme.vars['--c-accent']; const img=(href,x,y,w,h,op=1)=>``; const fg='assets/flower-green-svgrepo-com(1).svg'; const fl='assets/flower-leaf-2-svgrepo-com.svg'; const fb='assets/flower-bouquet-svgrepo-com(2).svg'; // Accent petals scattered near centre let s=''; [[22,38],[78,35],[50,12],[50,86],[15,65],[85,62]].forEach(([x,y])=>{ s+=``; }); s+=img(fb,8,18,84,72,0.07); // faint bouquet watermark behind text s+=img(fg,-8,-8,52,52); // flower top-left s+=img(fg,56,56,52,52); // flower bottom-right s+=img(fl,54,-8,52,52,0.85); // leaf top-right s+=img(fl,-8,56,52,52,0.85); // leaf bottom-left return svg(s); } case 'retro':{ const a=theme.vars['--c-accent']; const cy='#00EEFF'; let s=''; // Pixel stars [[6,4],[19,7],[31,3],[47,9],[62,5],[77,8],[91,4],[12,15],[55,12],[83,14], [3,25],[38,28],[70,23],[96,18],[25,35],[85,32]].forEach(([x,y],i)=>{ const col=i%3===0?cy:i%3===1?a:'#FFFFFF'; s+=``; }); // Synthwave horizon sun const hz=70; s+=``; s+=``; [0,2.4,4.8].forEach(dy=>{ s+=``; }); // Perspective grid lines for(let i=0;i<=6;i++) s+=``; for(let i=1;i<=4;i++){ const t=i/4, y=hz+t*(100-hz), hw=t*50; s+=``; } // Space invader sprite (top-center, each pixel = 1.8 units) const ps=1.8, ox=50-9*ps/2, oy=5; [[0,0,1,0,0,0,1,0,0], [0,0,0,1,1,1,0,0,0], [0,1,1,1,1,1,1,1,0], [1,1,0,1,1,1,0,1,1], [1,1,1,1,1,1,1,1,1], [0,1,0,0,0,0,0,1,0], [1,0,1,0,0,0,1,0,1], ].forEach((row,r)=>row.forEach((on,c)=>{ if(on) s+=``; })); // Neon corner brackets const BW=6, BT=1.2; s+=``; s+=``; s+=``; s+=``; s+=``; s+=``; s+=``; s+=``; return svg(s); } } return svg(''); } function svg(inner){ return `${inner}`; } /* ---- Themes ---- */ const THEMES = [ { id:'confetti', name:'Confetti', vars:{ '--c-bg':'#FFFBF2','--c-ink':'#2A2438','--c-accent':'#FF5A7A', '--c-titleFont':"'Fredoka', sans-serif",'--c-titleWeight':'600', '--c-titleSize':'48px','--c-titleLS':'-.01em','--c-titleCase':'none', '--c-bodyFont':"'Quicksand', sans-serif",'--c-subSize':'17px', }, chipBg:'#FFFBF2', chipInk:'#FF5A7A', chipGlyph:'★' }, { id:'gold', name:'Midnight Gold', vars:{ '--c-bg':'#12243F','--c-ink':'#EDE3CB','--c-accent':'#C9A24B', '--c-titleFont':"'Cinzel', serif",'--c-titleWeight':'700', '--c-titleSize':'40px','--c-titleLS':'.08em','--c-titleCase':'uppercase', '--c-bodyFont':"'Lora', serif",'--c-subSize':'14px', '--c-subLS':'.22em','--c-subCase':'uppercase','--c-subWeight':'500', }, chipBg:'#12243F', chipInk:'#C9A24B', chipGlyph:'❧' }, { id:'botanical', name:'Botanical', vars:{ '--c-bg':'#FDFCF9','--c-ink':'#2D362E','--c-accent':'#6B8E6B', '--c-titleFont':"'Playfair Display', serif",'--c-titleWeight':'700', '--c-titleSize':'46px','--c-titleLS':'0.02em','--c-titleCase':'none', '--c-bodyFont':"'Lora', serif",'--c-subSize':'18px','--c-subStyle':'italic', }, chipBg:'#FDFCF9', chipInk:'#6B8E6B', chipGlyph:'❀' }, { id:'watercolor', name:'Watercolor', vars:{ '--c-bg':'#FCF9FF','--c-ink':'#3D2B52','--c-accent':'#A389D4', '--c-titleFont':"'Dancing Script', cursive",'--c-titleWeight':'700', '--c-titleSize':'52px','--c-titleLS':'0','--c-titleCase':'none', '--c-bodyFont':"'Raleway', sans-serif",'--c-subSize':'16px', '--c-subLS':'.05em','--c-subWeight':'500', }, chipBg:'#FCF9FF', chipInk:'#A389D4', chipGlyph:'༄' }, { id:'balloon', name:'Balloons', vars:{ '--c-bg':'#F7FBFF','--c-ink':'#2A4B6B','--c-accent':'#5A9AFF', '--c-titleFont':"'Fredoka', sans-serif",'--c-titleWeight':'600', '--c-titleSize':'48px','--c-titleLS':'-.01em', '--c-bodyFont':"'Quicksand', sans-serif",'--c-subSize':'17px', }, chipBg:'#F7FBFF', chipInk:'#5A9AFF', chipGlyph:'🎈' }, { id:'kraft', name:'Kraft Note', vars:{ '--c-bg':'#D7C3A3','--c-ink':'#41301F','--c-accent':'#8A5A38', '--c-titleFont':"'Caveat', cursive",'--c-titleWeight':'700', '--c-titleSize':'42px','--c-titleLS':'0', '--c-bodyFont':"monospace",'--c-subSize':'16px','--c-subLS':'0.04em', }, chipBg:'#D7C3A3', chipInk:'#8A5A38', chipGlyph:'✶' }, { id:'flowers', name:'Garden Blooms', vars:{ '--c-bg':'#FEFCF6','--c-ink':'#273A1C','--c-accent':'#D4517A', '--c-titleFont':"'Dancing Script', cursive",'--c-titleWeight':'700', '--c-titleSize':'56px','--c-titleLS':'0.01em','--c-titleCase':'none', '--c-bodyFont':"'Lora', serif",'--c-subSize':'17px','--c-subStyle':'italic', }, chipBg:'#FEFCF6', chipInk:'#D4517A', chipGlyph:'✿' }, { id:'retro', name:'Retro Arcade', vars:{ '--c-bg':'#0D0A1E','--c-ink':'#00EEFF','--c-accent':'#FF1177', '--c-titleFont':"'Press Start 2P', monospace",'--c-titleWeight':'400', '--c-titleSize':'20px','--c-titleLS':'0.04em','--c-titleCase':'uppercase', '--c-bodyFont':"monospace",'--c-subSize':'10px','--c-subLS':'0.12em','--c-subCase':'uppercase','--c-subWeight':'400', }, chipBg:'#0D0A1E', chipInk:'#FF1177', chipGlyph:'👾' }, ]; const PRESETS = [ { name:"Birthday", title:"Happy Birthday", sub:"Wishing you a wonderful day", msg:"Hope your day is filled with joy, laughter, and a little extra cake.\n\nWith love,", font:"’Fredoka’, sans-serif" }, { name:"Thank You", title:"Thank You", sub:"with heartfelt gratitude", msg:"Thank you so much for your kindness — it truly meant the world to me.\n\nWarmly,", font:"’Dancing Script’, cursive" }, { name:"Congrats", title:"Congratulations!", sub:"on your wonderful news", msg:"So thrilled for you and everything you’ve accomplished. Here’s to what comes next!", font:"’Montserrat’, sans-serif" }, { name:"Invitation", title:"You’re Invited", sub:"Saturday · 4:00 PM", msg:"Join us to celebrate!\n\nPlease RSVP by [date]\n[Address]", font:"’Cinzel’, serif" }, { name:"Thinking of You", title:"Thinking of You", sub:"sending warm wishes", msg:"Just a little note to let you know you’re in my thoughts today.", font:"’Lora’, serif" }, { name:"Get Well", title:"Get Well Soon", sub:"feel better", msg:"Wishing you rest, comfort, and a speedy recovery.", font:"’Caveat’, cursive" }, { name:"Anniversary", title:"Happy Anniversary", sub:"celebrating you both", msg:"Wishing you another year filled with love, laughter, and beautiful memories together.\n\nWith love,", font:"’Playfair Display’, serif" }, { name:"Baby Shower", title:"Baby Shower", sub:"a new little one is on the way!", msg:"Wishing you all the joy and wonder of welcoming your new arrival.\n\nWith love,", font:"’Quicksand’, sans-serif" }, { name:"Sympathy", title:"With Deepest Sympathy", sub:"in your time of loss", msg:"Please know that you are in my thoughts and heart during this difficult time.\n\nWith love,", font:"’Lora’, serif" }, { name:"Graduation", title:"Congratulations, Graduate!", sub:"the world is yours", msg:"So proud of everything you’ve accomplished. This is just the beginning of an incredible journey!\n\nWith love,", font:"’Montserrat’, sans-serif" }, { name:"Valentine’s", title:"Happy Valentine’s Day", sub:"with all my love", msg:"Every day with you is a gift. Thank you for being my favorite person.\n\nAll my love,", font:"’Dancing Script’, cursive" }, { name:"Holiday", title:"Happy Holidays", sub:"wishing you joy & peace", msg:"May this season bring warmth, laughter, and all the things that make your heart happy.\n\nWith love,", font:"’Cinzel’, serif" }, ]; /* ---- State ---- */ const state = { style:'quarter', paper:'letter', theme:THEMES[0], title:'Happy Birthday', sub:'Wishing you a wonderful day', msg:'Hope your day is filled with joy, laughter, and a little extra cake.\n\nWith love,', back:'', logo:true, logoPos:'back', inside:false, guide:true, accent:null, text:null, bg:null, fontOverride:'', doubleSided: false, activeSide: 'outside', titleScale: 1, bodyScale: 1, align: 'center', msgSide: 'right' }; /* ---- State Persistence ---- */ function saveState(){ const data = { ...state, themeId: state.theme.id }; delete data.theme; // Don't store the whole object reference localStorage.setItem('cardStudioState', JSON.stringify(data)); } function loadState(){ const saved = localStorage.getItem('cardStudioState'); if(!saved) return; try { const data = JSON.parse(saved); const theme = THEMES.find(t => t.id === data.themeId) || THEMES[0]; Object.assign(state, data, { theme }); syncUI(); } catch(e) { console.error("Failed to load state", e); } } function syncUI(){ $('#fTitle').value = state.title; $('#fSub').value = state.sub; $('#fMsg').value = state.msg; $('#fBack').value = state.back; $('#fFont').value = state.fontOverride; const ts = Math.round((state.titleScale || 1) * 100); const bs = Math.round((state.bodyScale || 1) * 100); $('#fTitleScale').value = ts; $('#fTitleScaleVal').textContent = ts + '%'; $('#fBodyScale').value = bs; $('#fBodyScaleVal').textContent = bs + '%'; $('#cInside').checked = state.inside; $('#cDouble').checked = state.doubleSided; $('#cGuide').checked = state.guide; $('#cLogo').checked = state.logo; $('#cAccent').value = toHex(state.accent || state.theme.vars['--c-accent']); $('#cText').value = toHex(state.text || state.theme.vars['--c-ink']); // Update segments document.querySelectorAll('#paperSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.paper === (state.paper || 'letter'))); document.querySelectorAll('#styleSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.style === state.style)); document.querySelectorAll('#logoSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.pos === state.logoPos)); document.querySelectorAll('#sideSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.side === state.activeSide)); document.querySelectorAll('#alignSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.align === (state.align || 'center'))); document.querySelectorAll('#msgSideSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.msgside === (state.msgSide || 'right'))); $('#msgSideWrap').style.display = state.style === 'quarter' ? '' : 'none'; document.querySelectorAll('.swatch').forEach(s => s.setAttribute('aria-pressed', s.dataset.id === state.theme.id)); // Visibility logic const isFolded = (state.style === 'folded'); // Legacy support if (state.style === 'folded') state.style = 'quarter'; // Migration $('#optGroup').style.display = (state.style === 'folded') ? 'flex' : 'none'; // Only for legacy folded $('#printGroup').style.display = (state.inside && state.style === 'folded') ? '' : 'none'; $('#sideSelector').style.display = state.doubleSided ? 'none' : ''; $('#logoPosWrap').style.display = (state.logo && state.style !== 'flat') ? '' : 'none'; document.body.classList.toggle('manual-flip', !state.doubleSided && state.inside && state.style === 'folded'); const notes={ quarter:'Quarter-fold: one portrait sheet, folded twice, makes a small 4.25 × 5.5" card — front, back, and inside message all print on this one sheet.', flat:'Flat: four identical cards on one portrait sheet. Cut along the dashed lines — no folding. Great for invitations and thank-you cards.' }; $('#styleNote').innerHTML = notes[state.style] || notes.quarter; } /* ---- Build controls ---- */ const $ = s=>document.querySelector(s); const sheets = $('#sheets'); const swWrap=$('#swatches'); THEMES.forEach(t=>{ const b=document.createElement('button'); b.className='swatch';b.type='button';b.dataset.id=t.id; b.setAttribute('aria-pressed', t.id===state.theme.id); b.innerHTML=`${t.chipGlyph}${t.name}`; b.addEventListener('click',()=>{ state.theme=t; state.accent=null; state.text=null; syncColorInputs(); document.querySelectorAll('.swatch').forEach(s=>s.setAttribute('aria-pressed', s.dataset.id===t.id)); render(); }); swWrap.appendChild(b); }); const pWrap=$('#presets'); PRESETS.forEach(p=>{ const b=document.createElement('button'); b.className='preset';b.type='button';b.textContent=p.name; b.addEventListener('click',()=>{ state.title=p.title;state.sub=p.sub;state.msg=p.msg; if(p.font) { state.fontOverride=p.font; $('#fFont').value=p.font; } $('#fTitle').value=p.title;$('#fSub').value=p.sub;$('#fMsg').value=p.msg; render(); }); pWrap.appendChild(b); }); $('#fTitle').addEventListener('input',e=>{state.title=e.target.value;render();}); $('#fSub').addEventListener('input',e=>{state.sub=e.target.value;render();}); $('#fMsg').addEventListener('input',e=>{state.msg=e.target.value;render();}); $('#msgSideSeg').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return; state.msgSide=b.dataset.msgside; document.querySelectorAll('#msgSideSeg button').forEach(x=>x.setAttribute('aria-pressed', x===b)); render(); }); $('#fBack').addEventListener('input',e=>{state.back=e.target.value;render();}); $('#fFont').addEventListener('change',e=>{state.fontOverride=e.target.value;render();}); $('#alignSeg').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return; state.align=b.dataset.align; document.querySelectorAll('#alignSeg button').forEach(x=>x.setAttribute('aria-pressed', x===b)); render(); }); $('#fTitleScale').addEventListener('input',e=>{ state.titleScale = e.target.value / 100; $('#fTitleScaleVal').textContent = e.target.value + '%'; render(); }); $('#fBodyScale').addEventListener('input',e=>{ state.bodyScale = e.target.value / 100; $('#fBodyScaleVal').textContent = e.target.value + '%'; render(); }); $('#cInside').addEventListener('change',e=>{ state.inside=e.target.checked; $('#printGroup').style.display = (state.inside && state.style==='folded') ? '' : 'none'; render(); }); $('#cDouble').addEventListener('change',e=>{ state.doubleSided=e.target.checked; $('#sideSelector').style.display = state.doubleSided ? 'none' : ''; document.body.classList.toggle('manual-flip', !state.doubleSided && state.inside); render(); }); $('#sideSeg').addEventListener('click', e => { const b = e.target.closest('button'); if(!b) return; state.activeSide = b.dataset.side; document.querySelectorAll('#sideSeg button').forEach(x => x.setAttribute('aria-pressed', x === b)); render(); }); $('#paperSeg').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return; state.paper=b.dataset.paper; document.querySelectorAll('#paperSeg button').forEach(x=>x.setAttribute('aria-pressed', x===b)); render(); }); $('#styleSeg').addEventListener('click',e=>{ const btn=e.target.closest('button'); if(!btn) return; state.style=btn.dataset.style; document.querySelectorAll('#styleSeg button').forEach(b=>b.setAttribute('aria-pressed', b===btn)); const s=state.style; const notes={ quarter:'Quarter-fold: one portrait sheet, folded twice, makes a small 4.25 × 5.5" card — front, back, and inside message all print on this one sheet.', flat:'Flat: four identical cards on one portrait sheet. Cut along the dashed lines — no folding. Great for invitations and thank-you cards.' }; $('#styleNote').innerHTML = notes[s]; const isFolded = (s === 'folded'); $('#printGroup').style.display = (state.inside && isFolded) ? '' : 'none'; const cut = (s==='flat'); $('#guideOptLabel').firstChild.textContent = cut ? 'Show cut lines when printing' : 'Show fold lines when printing'; $('#guideOptSub').textContent = cut ? 'Faint dashed lines to trim along.' : 'Faint dashed creases to fold against.'; $('#msgLabel').textContent = (s==='flat') ? 'Card message' : 'Inside message'; $('#msgHint').textContent = s==='flat' ? 'Shown on each flat card.' : s==='quarter'? 'Printed inside the small card (same sheet).' : 'Used on the inside page (enable below) — and shown on flat cards.'; $('#msgSideWrap').style.display = s === 'quarter' ? '' : 'none'; syncLogoUI(); render(); }); $('#cGuide').addEventListener('change',e=>{state.guide=e.target.checked;render();}); function syncLogoUI(){ $('#logoPosWrap').style.display = (state.logo && state.style!=='flat') ? '' : 'none'; } $('#cLogo').addEventListener('change',e=>{state.logo=e.target.checked;syncLogoUI();render();}); $('#logoSeg').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return; state.logoPos=b.dataset.pos; document.querySelectorAll('#logoSeg button').forEach(x=>x.setAttribute('aria-pressed',x===b)); render(); }); function syncColorInputs(){ const t=state.theme.vars; $('#cAccent').value = toHex(state.accent || t['--c-accent']); $('#cText').value = toHex(state.text || t['--c-ink']); $('#cBg').value = toHex(state.bg || t['--c-bg']); } function toHex(c){ if(/^#([0-9a-f]{6})$/i.test(c)) return c; return '#000000'; } $('#cAccent').addEventListener('input',e=>{state.accent=e.target.value;render();}); $('#cText').addEventListener('input',e=>{state.text=e.target.value;render();}); $('#cBg').addEventListener('input',e=>{state.bg=e.target.value;render();}); $('#resetColors').addEventListener('click',()=>{state.accent=null;state.text=null;state.bg=null;syncColorInputs();render();}); $('#printBtn').addEventListener('click',()=>window.print()); window.addEventListener('beforeprint',()=>{ document.querySelectorAll('.sheet').forEach(s=>s.style.transform='none'); // Hide images still loading — Chrome hangs on "Preparing preview" if any image fetch is pending document.querySelectorAll('.sheet img').forEach(img=>{if(!img.complete)img.style.display='none';}); }); window.addEventListener('afterprint',()=>{ document.querySelectorAll('.sheet img').forEach(img=>img.style.display=''); fit(); }); /* ---- Rendering ---- */ function faceVars(){ const v={...state.theme.vars}; if(state.accent) v['--c-accent']=state.accent; if(state.text) v['--c-ink']=state.text; if(state.bg) v['--c-bg']=state.bg; const itemsMap = { left:'flex-start', center:'center', right:'flex-end' }; v['--c-align'] = state.align || 'center'; v['--c-items'] = itemsMap[state.align || 'center']; if(state.fontOverride) v['--c-titleFont']=state.fontOverride; const ts = state.titleScale || 1; if(ts !== 1){ const base = parseFloat(v['--c-titleSize'] || '40px'); v['--c-titleSize'] = (base * ts).toFixed(1) + 'px'; } const bs = state.bodyScale || 1; if(bs !== 1){ const subBase = parseFloat(v['--c-subSize'] || '16px'); v['--c-subSize'] = (subBase * bs).toFixed(1) + 'px'; const msgBase = parseFloat(v['--c-msgSize'] || '15px'); v['--c-msgSize'] = (msgBase * bs).toFixed(1) + 'px'; } return Object.entries(v).map(([k,val])=>`${k}:${val}`).join(';'); } function esc(s){return (s||'').replace(/&/g,'&').replace(//g,'>');} function logoImg(cls){ return `
Beach Party Balloons
`; } function coverFace(){ const front = state.logo && state.logoPos==='front'; return `
${deco(state.theme,'cover')}
${state.title?`

${esc(state.title)}

`:''} ${state.sub?`

${esc(state.sub)}

`:''} ${front?logoImg('logo-sm'):''}
`; } function backFace(){ const back = state.logo && state.logoPos==='back'; return `
${deco(state.theme,'back')}
${back?`
${logoImg('')}${state.back?`
${esc(state.back)}
`:''}
` :(state.back?`
${esc(state.back)}
`:'')}
`; } function insideFace(side){ const hasMsg = (side === (state.msgSide || 'right')); if(hasMsg){ return `

${esc(state.msg)}

`; } return `
`; } function flatCell(){ return `
${deco(state.theme,'cover')}
${state.title?`

${esc(state.title)}

`:''} ${state.sub?`

${esc(state.sub)}

`:''} ${state.msg?`

${esc(state.msg)}

`:''} ${state.logo?logoImg('logo-sm'):''}
`; } function render(){ let html=''; if(state.style==='folded'){ const manual = !state.doubleSided && state.inside; const isOutside = !manual || state.activeSide === 'outside'; const isInside = !manual || state.activeSide === 'inside'; if(isOutside){ const outside = `
${backFace()}
${coverFace()}
fold
`; html += `
${holderLabel(state.inside?'Outside (front & back)':'Card sheet — fold in half', outside)}
`; } if(state.inside && isInside){ const inside = `
${insideFace('left')}
${insideFace('right')}
fold
`; html += `
${holderLabel('Inside (message)', inside)}
`; } } else if(state.style==='quarter'){ const g = `
${insideFace('left')}
${insideFace('right')}
${backFace()}
${coverFace()}
`; const labels = `
Inside ↧ (prints upside-down)Message ↧ BackFront cover
`; const sheet = `
${g}
${labels}
`; html += `
${holderLabel('Small card — fold in half, then half again', sheet)}
`; } else { const flat = `
${flatCell()}${flatCell()}${flatCell()}${flatCell()}
`; html += `
${holderLabel('4 cards — cut to separate', flat)}
`; } sheets.innerHTML = html; updatePageStyle(); fit(); saveState(); } function holderLabel(label, inner){ return `
${label}
${inner}`; } const guideClass = ()=> 'guide ' + (state.guide ? '' : 'noprint-guide'); let pageStyleEl=document.createElement('style');document.head.appendChild(pageStyleEl); function updatePageStyle(){ const land = state.style==='folded'; const orient = land ? 'landscape' : 'portrait'; const pageSize = state.paper === 'a4' ? 'A4' : 'Letter'; const sheetW = state.paper === 'a4' ? '210mm' : '8.5in'; const sheetH = state.paper === 'a4' ? '297mm' : '11in'; pageStyleEl.textContent = `@page{ size: ${pageSize} ${orient}; margin:0; } .sheet.flat,.sheet.quarter{ width:${sheetW}; height:${sheetH}; }`; } function fit(){ const dim = PAPER_DIMS[state.paper || 'letter']; const wrap = document.getElementById('previewWrap'); if(!wrap) return; // Use a more robust width measurement const isMobile = window.innerWidth <= 960; const w = (isMobile ? window.innerWidth : wrap.clientWidth) || 360; const h = wrap.clientHeight || (window.innerHeight * 0.5); const style = window.getComputedStyle(wrap); const padW = (parseFloat(style.paddingLeft)||0) + (parseFloat(style.paddingRight)||0); const padH = (parseFloat(style.paddingTop)||0) + (parseFloat(style.paddingBottom)||0); // Leave a bit more breathing room on mobile const margin = isMobile ? 24 : 12; const availW = Math.max(40, w - padW - margin); const availH = Math.max(40, h - padH - margin); let scale = Math.min(1, availW / dim.w, availH / dim.h); if (isNaN(scale) || scale <= 0) scale = 0.1; document.querySelectorAll('.sheetHolder').forEach(holder => { const sheet = holder.querySelector('.sheet'); if(sheet) sheet.style.transform = `scale(${scale})`; holder.style.width = Math.floor(dim.w * scale) + 'px'; holder.style.height = Math.floor(dim.h * scale) + 'px'; }); } window.addEventListener('resize',fit); window.addEventListener('load',fit); window.addEventListener('orientationchange',()=>setTimeout(fit,200)); if('ResizeObserver' in window){ new ResizeObserver(fit).observe(document.getElementById('previewWrap')); } /* ---- Tabs ---- */ document.getElementById('tabBar').addEventListener('click', e => { const btn = e.target.closest('.tab'); if (!btn) return; const target = btn.dataset.tab; document.querySelectorAll('.tab').forEach(t => { t.classList.toggle('active', t === btn); t.setAttribute('aria-selected', t === btn); }); document.querySelectorAll('.tab-panel').forEach(p => { p.classList.toggle('active', p.dataset.tab === target); }); requestAnimationFrame(fit); }); loadState(); syncColorInputs(); syncLogoUI(); render(); requestAnimationFrame(fit);